115-并发的 UDP 服务器

116 篇文章 22 订阅

TCP 服务并发对我们来说已经不陌生了,你有各种手段处理,比如多进程,多线程,IO 复用 + 单/多线程。但是 UDP 处理并发,如果不仔细思考一下,可能你会觉得这没什么嘛,还不是和 TCP 差不多。

1. TCP & UDP 多进程并发

这里以多进程 TCP 服务器并发作为参照,看看和 UDP 的区别在哪里。

  • tcp

如果你不记得多进程并发模型,还请回去再复习一下《并发服务器(多进程)》,下面是多进程并发模型的伪代码。

// 片段 1
void server_routine() {
   //先执行 bind, listen,伪代码略去
   while(1) {
     sockfd = accept(listenfd);

     // 让子进程去处理 IO
     pid = fork();
     if (pid == 0) {
       // child
       close(listenfd);
       doServer(sockfd);
       close(sockfd);
       exit(0);
     }

     // father;
     close(sockfd);
   }
   close(listenfd)
}
  • udp

如果是 udp,那伪代码是什么样的呢?

// 片段 2 (危险!请勿模仿!)
void server_routine() {
    while(1) {
        recvfrom(sockfd, &clientAddress);
        pid = fork(); // 执行 fork() 吗? 
        if (pid == 0) {
            doServer(sockfd); // ??? 可以吗
            exit(0);
        }
    }
}

看起来似乎不是那么好实现,所以大家就无视代码片段 2 了的实现吧。接下来再想想,为什么 udp 没法像 tcp 那样,通过 fork 子进程来单独处理客户请求?

对比一下区别:

  1. TCP 有一个监听套接字 listenfd,使用 accept 可以返回一个新的连接套接字 sockfd,子进程 fork 后,只要拿这个新的 sockfd 就可以处理用户请求了。
  2. UDP 只有一个套接字,所有用户请求发来的数据,都可以在这个套接字上读取到,直接 fork 是不可以的。

2. 解决方案

除非……

UDP 收到用户数据后,自己也创建了一个新的套接字描述符,然后用这个新的描述符来处理用户请求,用户以后应该往这个新的套接字上发数据。

问题来了,用户不知道服务器创建的新的描述符。有点抝口,用代码来说话。

// 片段 3
void server_routine() {
    while(1) {
        // 无论如何,新用户的第一份数据一定是在这里收到
        recvfrom(sockfd, buf, &clientAddress);
        int newSockfd = socket(UDP);    
        pid = fork(); 
        if (pid == 0) {
            doServer(newSockfd);
            exit(0);
        }
        close(newSockfd);
    }
}

void doServer(int conn) {
    while(1) {
        // 问题来了,客户端不知道这个新的 conn 套接字 bind 的地址是多少
        // 所以客户端后面的数据,还是得发送到 server_routine 函数的 sockfd 上
        recvfrom(conn, buf, &clientAddress)
        sendto(conn, buf, &clientAddress)
    }
}

看起来好像卡在什么地方了,就像片段 2 中的注释,客户端不知道新的 conn 套接字 bind 的地址。没关系,客户端不知道,那么服务器就自己告诉客户端吧。客户端不知道,服务器知道啊!

再修改一次,伪代码如下:

// 片段 4
void server_routine() {
    // 先执行 bind, listen,伪代码略去
    // 假设服务绑定的是 8000 号端口
    int port = 8001;
    while(1) {
        // 无论如何,新用户的第一份数据一定是在这里收到
        recvfrom(sockfd, buf, &clientAddress);
        int newSockfd = socket(UDP);
        newServerAddress = "0.0.0.0:{{port}}"; // 每次有客户端过来,就绑定一个新端口
        bind(newSockfd, &newServerAddresss); // bind 新地址
        sendto(sockfd, &newServerAddress, &clientAddress); // 把服务器的新绑定地址发给客户端
        port++;
        pid = fork(); 
        if (pid == 0) {
            doServer(newSockfd);
            exit(0);
        }
        close(newSockfd);
    }
}

void doServer(int conn) {
    while(1) {
        // 静静的等待客户端往此套接字地址上发数据吧……
        recvfrom(conn, buf, &clientAddress)
        sendto(conn, buf, &clientAddress)
    }
}

3. 似乎不是那么完美

看过了上面的解决方案,你肯定又要吐槽了,每次来了个新客户端进来,port 端口就 + 1,有两个问题:

  • 特喵的你就不怕端口被别人占过了么?
  • 这样加下去迟早药丸啊!端口地址最大 65535,告诉我,65536 你怎么办?有人说会绕回。。。绕回到 0 开始,你去绑定 21, 22, 80 吗?你是 IANA (Internet Assigned Numbers Authority) 它爹吗?

先回答第一个问题,先不要着急,自己选端口 bind 有问题,那就让 OS 帮我们选一个合适的吧!如何让 OS 动态分配端口呢?很简单,指定你的绑定的 port 端口号为 0 就行了。比如:

struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(0);

接下来再执行 bind 就 ok 了。还差最后一步,怎么知道动态分配的端口是多少呢?(我说你事是不是有点多啊)。。。提示一下,getsockname 这个函数还记得吧,如果又忘记了,参考这一篇《面向连接的 UDP》,顺便你也复习一下面向连接的 UDP,哦,忘记了说了,git 里的源码我使用了面向连接的 UDP。

问题 2,如果并发数过大,端口迟早要用完,抱歉,除了加网卡,加机器,没有办法了。

4. 编写多进程并发 UDP 服务的步骤

4.1 第一种方案

  • 主进程接收数据报
  • 主进程创建新套接字,并 bind 新套接字地址(动态分配端口)
  • 获取动态分配的端口,并把新端口从旧套接字发送给客户端
  • fork 子进程,处理从新套接字发来的请求
  • 主进程返回第 1 步

4.2 第二种方案

  • 主进程接收数据报
  • 主进程创建新套接字,并 bind 新套接字地址(动态分配端口)
  • 从新套接字发送一些数据给客户端,这样客户端就知道新套接字地址是多少了
  • fork 子进程,处理从新套接字发来的请求
  • 主进程返回第 1 步

5. 任务

学完了本节,你需要做的一件事情就完成第 4 节里的需求。方法很多,坑也很多。在我提供的实现里,有两份源码,一份是 concurrent(对应 4.2),另一份是 concurrent_nat(对应 4.1)。如果你完全是按照第 4.1 节的步骤来实现的话,完全没问题,如果你选则了另一种方案 4.2,会出现问题(然鹅,好多博文似乎使用了 4.2 的步骤)。后面准备了思考题。

具体任务内容先看图 1.


这里写图片描述
图1 并发 udp(点击图片查看大图)

图 1 中第一、二窗口是两个 udp 客户端,在内网上。第三个窗口是位于某云主机上的服务,也就是说使用了公网地址。客户端往服务器发数据,服务端返回客户所发送的所有历史数据,就是这么简单。

git 地址在这里:https://gitee.com/ivan_allen/unp

源码路径是: /unp/program/advcudp,当然我希望你自己能实现一遍。

6. 思考题

你应该知道我要问什么了。为什么 4.2 的方案不行?提示两个点:

  • 客户端突然收到一个奇妙端口发来的数据,奇不奇怪?
  • 如果客户端在内网,服务器在公网或者在另一个网络(在另一个网络是指客户端和服务器不在同一个网关后面),服务器通过新套接字发送给客户端的数据,客户端能收到吗?

上面的问题牵扯到 NAT 相关的知识,有点难度,同学们多多查阅资料,在评论里回答问题。

  • 0
    点赞
  • 5
    评论
  • 3
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值