【Linux】【网络】传输层协议:UDP

本文详细介绍了UDP协议的工作原理,包括其无连接和不可靠特性,以及如何在UDP基础上实现通用服务器和客户端的编程示例。


UDP 协议

UDP传输的过程类似于寄信。

  • 无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接。
  • 不可靠:没有确认机制,没有重传机制;如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息。
  • 面向数据报: 不能够灵活的控制读写数据的次数和数量。

1. 面向数据报

数据报是独立的一整个,应用层交给 UDP 多长的报文,UDP原样发送,既不会拆分,也不会合并。

例如:用 UDP 传输 100 个字节的数据:
	如果发送端调用一次 sendto,发送100个字节,那么接收端也必须调用对应的一次recvfrom,接收100字节。
	而不能循环调用10次recvfrom, 每次接收10个字节。

2. UDP 协议端格式

在这里插入图片描述

UPD 的协议报头长度是 固定的 8字节

16位 UDP 长度,表示整个数据报(UDP 首部 + UDP 数据)的 最大长度,即 216 = 64kb

如果校验和出错, 就会直接丢弃。

报头(协议)的本质其实就是:结构化数据(结构体、位段)

// 结构体实现
struct udp_header
{
	uint16_t src_port;
	uint16_t dst_port;
	uint16_t udp_len;
	uint16_t check;
};
// 位段实现
struct udp_header
{
	uint32_t src_port:16;
	uint32_t dst_port:16;
	//...
};

3. UDP 的封装和解包

🎯封装

  • 应用层将信息拷贝给传输层,用 char* p 指针指向一个缓冲区,前面放 UDP 结构报头(固定 8 字节),后面放有效载荷,对报头内容的填充就可以写作:
((struct udp_header*)p)->src_port = xx;
((struct udp_header*)p)->dst_port = xx;
((struct udp_header*)p)->udp_len = xx;
((struct udp_header*)p)->check = xx;

🎯解包

  • 传输层拿到信息,用 char* start 指针指向头部,对 upd 报头、有效数据的提取就可以写作:
// 读取报头信息
xx = ((struct udp_header*)p)->src_port;
xx = ((struct udp_header*)p)->dst_port;
xx = ((struct udp_header*)p)->udp_len;
xx =((struct udp_header*)p)->check;
// 找到有效载荷的起始
void* p = start + sizeof(struct udp_header);

4. UDP 的缓冲区

UDP 没有真正意义上的 发送缓冲区。调用 sendto 会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。

UDP 具有接收缓冲区。但是这个接收缓冲区不能保证收到的 UDP 报的顺序和发送 UDP 报的顺序一致。如果缓冲区满了,再到达的UDP数据就会被丢弃。

UDP 的 socket 既能读,也能写, 这个概念叫做 全双工


接下来用 UDP 实现一个简单的服务器和客户端~
🔗一些前置知识及 API


UDP 通用服务器

udp_server.hpp

#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unordered_map>
#include "err.hpp"
#include "RingQueue.hpp"
#include "lockGuard.hpp"
#include "Thread.hpp"

namespace ns_server
{
    const static uint16_t default_port = 8080;
    using func_t = std::function<std::string(std::string)>;

    class UdpServer
    {
    public:
        UdpServer(uint16_t port = default_port) : port_(port)
        {
            std::cout << "server addr: " << port_ << std::endl;
            pthread_mutex_init(&lock, nullptr);

            p = new Thread(1, std::bind(&UdpServer::Recv, this));
            c = new Thread(1, std::bind(&UdpServer::Broadcast, this));
        }
        void start()
        {
            //【1】创建 socket 接口,打开网络文件
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
            if (sock_ < 0)
            {
                std::cerr << "create socket error: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            std::cout << "create socket success: " << sock_ << std::endl; // 3

            //【2】给服务器指明IP地址(云服务器上不行)和Port
            struct sockaddr_in local; // 这个 local 定义在 用户空间的特定函数的栈帧上,不在内核中!需要bind函数绑定socket
            bzero(&local, sizeof(local));   // 等效于memset(&local,0,sizeof(local))

            local.sin_family = AF_INET; // == PF_INET,是选择通信方式,这里选网络通信
            local.sin_port = htons(port_);  // 主机序列转成望楼序列
            // inet_addr: 1,2
            // 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化
            // 2. 需要将主机序列转化成为网络序列
            // local.sin_addr.s_addr = inet_addr(ip_.c_str());
            // 实际上,云服务器,或者一款服务器,一般不要指明某一个确定的IP!!
            // INADDR_ANY:让我们的 udpserver 在启动的时候,bind 本主机上的任意 IP
            local.sin_addr.s_addr = INADDR_ANY; 
            if (bind(sock_, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                std::cerr << "bind socket error: " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            std::cout << "bind socket success: " << sock_ << std::endl; // 3

            p->run();
            c->run();
        }

        void addUser(const std::string &name, const struct sockaddr_in &peer)
        {
            //?
            // onlineuserp[name] = peer;
            LockGuard lockguard(&lock);
            auto iter = onlineuser.find(name);
            if (iter != onlineuser.end())
                return;
            // onlineuser.insert(std::make_pair<const std::string, const struct sockaddr_in>(name, peer));
            onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name, peer));
        }
        void Recv()
        {
            char buffer[1024];
            while (true)
            {
                // 收
                struct sockaddr_in peer;        // 要提取信息的缓冲区
                socklen_t len = sizeof(peer);   // 这里一定要写清楚,未来你传入的缓冲区大小
                int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
                if (n > 0)
                    buffer[n] = '\0';
                else
                    continue;

                std::cout << "recv done ..." << std::endl;

                // 提取client信息 -- debug 
                std::string clientip = inet_ntoa(peer.sin_addr);
                uint16_t clientport = ntohs(peer.sin_port);     // 网络中读出来的是网络序列,需要转成主机序列
                std::cout << clientip << "-" << clientport << "# " << buffer << std::endl;

                // 构建一个用户,并检查
                std::string name = clientip;
                name += "-";
                name += std::to_string(clientport);
                // 如果不存在,就插入,如果存在,什么都不做
                addUser(name, peer);
                rq.push(buffer);

                // // 做业务处理
                // std::string message = service_(buffer);

                // 发
                // sendto(sock_, message.c_str(), message.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
            }
        }
        void Broadcast()
        {
            while (true)
            {
                std::string sendstring;
                rq.pop(&sendstring);

                std::vector<struct sockaddr_in> v;  // 
                {
                    LockGuard lockguard(&lock);
                    for (auto user : onlineuser)
                    {
                        v.push_back(user.second);
                    }
                }
                for (auto user : v)
                {
                    // std::cout << "Broadcast message to " << user.first << sendstring << std::endl;
                    sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr *)&(user), sizeof(user));
                    std::cout << "send done ..." << sendstring << std::endl;
                }
            }
        }
        ~UdpServer()
        {
            pthread_mutex_destroy(&lock);
            c->join();
            p->join();

            delete c;
            delete p;
        }

    private:
        int sock_;          // 套接字
        uint16_t port_;     // 端口号
        // func_t service_; // 我们的网络服务器刚刚解决的是网络IO的问题,要进行业务处理
        std::unordered_map<std::string, struct sockaddr_in> onlineuser;
        pthread_mutex_t lock;
        RingQueue<std::string> rq;
        Thread *c;
        Thread *p;
        // std::string ip_; 
    };
} 

udp_server.cc

#include "udp_server.hpp"
#include <memory>
#include <string>
#include <cstdio>

using namespace ns_server;
using namespace std;

static void usage(string proc)
{
    std::cout << "Usage:\n\t" << proc << " port\n" << std::endl;
}

//【业务处理】
// 上层的业务处理,不关心网络发送,只负责信息处理即可
std::string transactionString(std::string request) // request 就是一个string
{
    std::string result;
    char c;
    for (auto &r : request)
    {
        if (islower(r))
        {
            c = toupper(r);
            result.push_back(c);
        }
        else
        {
            result.push_back(r);
        }
    }

    return result;
}
static bool isPass(const std::string &command)
{   
    bool pass = true;
    auto pos = command.find("rm");
    if(pos != std::string::npos) pass=false;
    pos = command.find("mv");
    if(pos != std::string::npos) pass=false;
    pos = command.find("while");
    if(pos != std::string::npos) pass=false;
    pos = command.find("kill");
    if(pos != std::string::npos) pass=false;
    return pass;
}

// 需要实现:client把命令给server,server再把结果给client!
// ls -a -l
std::string excuteCommand(std::string command) // command用作一个命令
{
    // 1. 安全检查
    if(!isPass(command)) return "you are a bad man!";

    // 2. 业务逻辑处理
    FILE *fp = popen(command.c_str(), "r");
    if(fp == nullptr) return "None";
    // 3. 获取结果了
    char line[1024];
    std::string result;
    while(fgets(line, sizeof(line), fp) != NULL)
    {
        result += line;
    }
    pclose(fp);

    return result;
}

// 设置执行程序的命令为:./udp_server port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    // unique_ptr<UdpServer> usvr(new UdpServer("120.78.126.148", 8082));
    // unique_ptr<UdpServer> usvr(new UdpServer(transactionString, port));
    // unique_ptr<UdpServer> usvr(new UdpServer(excuteCommand, port));
    unique_ptr<UdpServer> usvr(new UdpServer(port));

    // usvr->InitServer(); // 服务器的初始化
    usvr->start();

    return 0;
}



UDP 通用客户端

udp_client.cc

#include <iostream>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include "err.hpp"
// 127.0.0.1: 本地环回,就表示的就是当前主机,通常用来进行本地通信或者测试

static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl; 
}

// ./udp_client serverip serverport 客户端必须知道服务器的IP和端口号
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(SOCKET_ERR);
    }
    // Q1:client 这里要不要bind呢?
    // A1:要的!socket通信的本质[clientip:clientport, serverip:serverport]
    // Q2:要不要自己bind呢?
    // A2:不需要自己bind,也不要自己bind,OS自动给我们进行bind -- 为什么?client的port要随机让OS分配防止client出现
    // 启动冲突 -- server 为什么要自己bind?1. server的端口不能随意改变,众所周知且不能随意改变的 2. 同一家公司的port号
    // 需要统一规范化

    // 明确server是谁
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while(true)
    {
        //多线程化??
        
        // 用户输入
        std::string message;
        std::cout << "[蛋哥的服务器]# ";
        // std::cin >> message;

        std::getline(std::cin,message);
        // 什么时候bind的?在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP,1. bind 2. 构建发送的数据报文
        //发送
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

        //接受
        char buffer[2048];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int n = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout <<  buffer << std::endl;
        }
    }

    return 0;
}

🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值