告知你不为人知的 UDP:连接性和负载均衡( 三 )


2.3 UDP和Epoll结合 - UDP的Accept模型到此,为了充分利用多核CPU资源,进行UDP的多处理,我们会预先创建多个进程,每个进程都创建一个或多个绑定相同端口,相同IP地址(SO_REUSEADDR、SO_REUSEPORT)的UDP socket,这样利用内核的UDP socket查找算法来达到UDP的多进程负载均衡 。然而,这完全依赖于Linux内核处理UDP socket查找时的一个算法,我们不能保证其它的系统或者未来的Linux内核不会改变算法的行为;同时,算法的查找能否做到比较好的均匀分布到不同的UDP socket,(每个处理进程只处理自己初始化时候创建的那些UDP socket)负载是否均衡是个问题 。于是,我们多么想给UPD建立一个accept模型,按需分配UDP socket来处理 。
在高性能Server编程中,对于TCP Server而已有比较成熟的解决方案,TCP天然的连接性可以充分利用epoll等高性能event机制,采用多路复用、异步处理的方式,哪个worker进程空闲就去accept连接请求来处理,这样就可以达到比较高的并发,可以极限利用CPU资源 。然而对于UDP server而言,由于整个Svr就一个UDP socket,接收并响应所有的client请求,于是也就不存在什么多路复用的问题了 。UDP svr无法充分利用epoll的高性能event机制的主要原因是,UDP svr只有一个UDP socket来接收和响应所有client的请求 。然而如果能够为每个client都创建一个socket并虚拟一个“连接”与之对应,这样不就可以充分利用内核UDP层的socket查找结果和epoll的通知机制了么 。server端具体过程如下:

  1. UDP svr创建UDP socket fd,设置socket为REUSEADDR和REUSEPORT、同时bind本地地址local_addr listen_fd = socket(PF_INET, SOCK_DGRAM, 0) 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))
  2. 创建epoll fd,并将listen_fd放到epoll中 并监听其可读事件 epoll_fd = epoll_create(1000); ep_event.events = EPOLLIN|EPOLLET; ep_event.data.fd = listen_fd; epoll_ctl(epoll_fd , EPOLL_CTL_ADD, listen_fd, &ep_event) in_fds = epoll_wait(epoll_fd, in_events, 1000, -1);
  3. epoll_wait返回时,如果epoll_wait返回的事件fd是listen_fd,调用recvfrom接收client第一个UDP包并根据recvfrom返回的client地址, 创建一个新的socket(new_fd)与之对应,设置new_fd为REUSEADDR和REUSEPORT、同时bind本地地址local_addr,然后connect上recvfrom返回的client地址 recvfrom(listen_fd, buf, sizeof(buf), 0, (struct sockaddr )&client_addr, &client_len) new_fd = socket(PF_INET, SOCK_DGRAM, 0) setsockopt(new_fd , SOL_SOCKET, SO_REUSEADDR, &reuse,sizeof(reuse)) setsockopt(new_fd , SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse)) bind(new_fd , (struct sockaddr ) &local_addr, sizeof(struct sockaddr)); connect(new_fd , (struct sockaddr * ) &client_addr, sizeof(struct sockaddr)
  4. 将新创建的new_fd加入到epoll中并监听其可读等事件 client_ev.events = EPOLLIN; client_ev.data.fd = new_fd ; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_fd , &client_ev)
  5. 当epoll_wait返回时,如果epoll_wait返回的事件fd是new_fd 那么就可以调用recvfrom来接收特定client的UDP包了 recvfrom(new_fd , recvbuf, sizeof(recvbuf), 0, (struct sockaddr * )&client_addr, &client_len)
通过上面的步骤,这样 UDP svr 就能充分利用 epoll 的事件通知机制了 。第一次收到一个新的 client 的 UDP 数据包,就创建一个新的UDP socket和这个client对应,这样接下来的数据交互和事件通知都能准确投递到这个新的UDP socket fd了 。
这里的UPD和Epoll结合方案,有以下几个注意点:
  • [1] client要使用固定的ip和端口和server端通信,也就是client需要bind本地local address 。如果client没有bind本地local address,那么在发送UDP数据包的时候,可能是不同的Port了,这样如果server 端的new_fd connect的是client的Port_CA端口,那么当Client的Port_CB端口的UDP数据包来到server时,内核不会投递到new_fd,相反是投递到listen_fd 。由于需要bind和listen fd一样的IP地址和端口,因此SO_REUSEADDR和SO_REUSEPORT是必须的 。
  • [2] 要小心处理上面步骤3中connect返回前,Client已经有多个UDP包到达Server端的情况 。如果server没处理好这个情况,在connect返回前,有2个UDP包到达server端了,这样server会new出两个new_fd1和new_fd2分别connect到client,那么后续的client的UDP到达server的时候,内核会投递UDP包给new_fd1和new_fd2中的一个
上面的UDP和Epoll结合的accept模型有个不好处理的小尾巴(也就是上面的注意点[2]),这个小尾巴的存在其本质是UDP和4元组没有必然的对应关系,也就是UDP的无连接性 。
2.3 UDP Fork 模型 - UDP accept模型之按需建立UDP处理进程


推荐阅读