UDP并发服务器思路

UDP并发与TCP并发的区别

在TCP并发编程中,通常使用one loop per thread的并发模型,也就是使用多个线程,每个线程中都有一个epoll loop,无论是使用epoll还是poll或select,在观察有无数据就绪时,都是针对多个文件描述符。如果只有一个文件描述符,那么进程只要观察那一个文件描述符即可。

网络编程中,一个Socket对应一个文件描述符。在TCP的并发中,服务器在监听端口初始化一个socket套接字描述符,接受客户端后就与每个客户端的连接有一个不同的文件描述符,所以TCP并发中有多个socket套接字描述符。但是,UDP协议的服务器没有真正意义上的“连接”的概念。在消息监听端口和响应请求都只有一个socket套接字描述符。

UDP怎么考虑并发?

《UNP · 卷1》中UDP章节描述了一句话:一般来说大多数TCP服务器是并发的,而大多数UDP服务器是迭代的。也就是服务器等待客户端的请求,然后读取请求后处理,再发回响应。如果是简单的处理响应还可以,如果每个处理都很耗时,那么就不得不考虑在UDP服务器做并发处理。

并发常见的思路就是多线程。服务器读取一个新的请求后,可以交给一个线程处理,该线程在处理之后直接将响应内容发给客户端。

虽然可以多线程处理读取的每个消息,但如果UDP服务器与多个客户端交互,却没有多个socket,这样效率并不是很高。典型的解决方法就是:在服务器为每个客户端创建一个新的socket套接字并绑定一个新的端口,客户端以后就需要以这个新的socket套接字与服务器通信。

总的来说:UDP并发服务器针对多个客户端,可以创建多个socket。针对多个请求,可以使用多线程(线程池)进行处理。

UDP 并发编程模型

  • 多个socket伪代码
for (; ;)
{
    //等待新的客户端连接
    recvfrom(&from_addr);
    //每有一个新的客户端,创建一个线程
    pthread_create(&tid, NULL, thread_fun, &from_addr);
}

//线程函数
void* thread_fun(void* arg)
{
    peer = socket(AF_INET, SOCK_DGRAM, 0);
    servaddr.sin_port = htons(0);	//绑定端口
    bind(peer, (struct sockaddr*)&servaddr, sizeof(servaddr));
    //将这个套接字和客户端地址连接,之后就可以使用write/read或send/recv这些函数,且不用再关心客户端地址
    connect(peer, (struct sockaddr*)&from, sizeof(from));
    //处理请求的loop
}
  • 使用epoll进行处理伪代码
  1. UDP服务器创建socket,并设置socket为REUSEADDRREUSEPORT和非阻塞同时再bind服务器地址local_addr:
listen_fd = socket(PF_INET, SOCK_DGRAM, 0);
fcntl(listen_fd, F_SETFL, fcntl(listen_fd, F_GETFD, 0)|O_NONBLOCK)
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
bind(listen_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
  1. 创建epoll fd,并将listen_fd添加到epoll中,并监听其可读事件:
epoll_fd = epoll_create(100);
ep_event.events = EPOLLIN | EPOLLET;
ep_event.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ep_event);
while (1) 
{
    in_fds = epoll_wait(epoll_fd, in_events, 1000, 5000);
  1. epoll_wait返回时,如果返回的是listen_fd, 调用recvfrom接受client第一个UDP包,并根据recvfrom返回client地址,创建一个新的socket套接字new_fd,设置new_fd为REUSEADDRREUSEPORT和非阻塞,同时bind本地地址local_addr然后connect上recvfrom返回的client地址:
    for (i = 0; i < in_fds; i++)
    {
        if(in_events[i].data.fd = listen_fd)
        {
            recvfrom(listen_fd, buf, sizeof(buf), 0, (struct sockaddr *&client_addr, &client_len);
            new_fd = socket(PF_INET, SOCK_DGRAM, 0);
            fcntl(new_fd, F_SETFL, fcntl(new_fd, F_GETFD, 0)|O_NONBLOCK);
            setsockopt(new_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
            setsockopt(new_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
            bind(new_fd, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
            connect(new_fd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr));
  1. 将新创建的new_fd加入到epoll中并监听其可读事件:
            client_ev.events = EPOLLIN;
            client_ev.data.fd = new_fd;
            epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_fd, &client_ev);
        }
        else if (in_events[i].events & EPOLLIN)
        {
  1. 当epoll_wait返回时,如果返回的是new_fd,那么调用recvfrom来接收特定client的UDP包:
            recvfrom(new_fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&client_addr, &client_len);
            data->fd = new_fd;
            data-> ptr= process(recvbuf); /*data中包括socket信息*/
            ev.data.ptr = data;
            ev.events = EPOLLOUT | EPOLLET;
            epoll_ctl(epoll_fd,EPOLL_CTL_MOD,new_fd,&ev);
        }
        else if (in_events[i].events & EPOLLOUT)
        {
            sockfd = data->fd;
            send( sockfd, data->ptr, strlen((char*)data->ptr), 0 );
            ev.data.ptr = data;
            ev.events = EPOLLIN | EPOLLET;
            epoll_ctl(epoll_fd,EPOLL_CTL_MOD,sockfd,&ev);
        }
        else
        {
        }
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code_peak

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值