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进行处理(伪代码)
- UDP服务器创建socket,并设置socket为
REUSEADDR
、REUSEPORT
和非阻塞同时再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));
- 创建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);
- epoll_wait返回时,如果返回的是listen_fd, 调用
recvfrom
接受client第一个UDP包,并根据recvfrom返回client地址,创建一个新的socket套接字new_fd,设置new_fd为REUSEADDR
、REUSEPORT
和非阻塞,同时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));
- 将新创建的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)
{
- 当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
{
}
}
}