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


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

文章插图
 
引言说起网络 socket,大家自然会想到 TCP ,用的最多也是 TCP,UDP 在大家的印象中是作为 TCP 的补充而存在,是无连接、不可靠、无序、无流量控制的传输层协议 。UDP的无连接性已经深入人心,协议上的无连接性指的是一个 UDP 的 Endpoint1(IP,PORT),可以向多个 UDP 的 Endpointi ( IP ,PORT )发送数据包,也可以接收来自多个 UDP 的 Endpointi(IP,PORT) 的数据包 。实现上,考虑这样一个特殊情况:UDP Client 在 Endpoint_C1只往 UDP Server 的 Endpoint_S1 发送数据包,并且只接收来自 Endpoint_S1 的数据包,把 UDP 通信双方都固定下来,这样不就形成一条单向的虚”连接”了么?
1. UDP的”连接性”估计很多同学认为UDP的连接性只是将UDP通信双方都固定下来了,一对一只是多对多的一个特例而已,这样UDP连接不连接到无所谓了 。果真如此吗?其实不然,UDP的连接性可以带来以下两个好处:
1.1 高效率、低消耗我们知道linux系统有用户空间(用户态)和内核空间(内核态)之分,对于x86处理器以及大多数其它处理器,用户空间和内核空间之前的切换是比较耗时(涉及到上下文的保存和恢复,一般3种情况下会发生用户态到内核态的切换:发生系统调用时、产生异常时、中断时) 。那么对于一个高性能的服务应该减少频繁不必要的上下文切换,如果切换无法避免,那么尽量减少用户空间和内核空间的数据交换,减少数据拷贝 。熟悉socket编程的同学对下面几个系统调用应该比较熟悉了,由于UDP是基于用户数据报的,只要数据包准备好就应该调用一次send或sendto进行发包,当然包的大小完全由应用层逻辑决定的 。
细看两个系统调用的参数便知道,sendto比send的参数多2个,这就意味着每次系统调用都要多拷贝一些数据到内核空间,同时,参数到内核空间后,内核还需要初始化一些临时的数据结构来存储这些参数值(主要是对端Endpoint_S的地址信息),在数据包发出去后,内核还需要在合适的时候释放这些临时的数据结构 。进行UDP通信的时候,如果首先调用connect绑定对端Endpoint_S的后,那么就可以直接调用send来给对端Endpoint_S发送UDP数据包了 。用户在connect之后,内核会永久维护一个存储对端Endpoint_S的地址信息的数据结构,内核不再需要分配/删除这些数据结构,只需要查找就可以了,从而减少了数据的拷贝 。这样对于connect方而言,该UDP通信在内核已经维护这一个“连接”了,那么在通信的整个过程中,内核都能随时追踪到这个“连接” 。
int connect(int socket, const struct sockaddr *address,socklen_t address_len);ssize_t send(int socket, const void *buffer, size_t length,int flags);ssize_t sendto(int socket, const void *message,size_t length,int flags, const struct sockaddr *dest_addr,socklen_t dest_len);ssize_t recv(int socket, void *buffer, size_t length,int flags);ssize_t recvfrom(int socket, void *restrict buffer,size_t length,int flags, struct sockaddr *restrict address,socklen_t *restrict address_len);1.2 错误提示相信大家写 UDP Socket 程序的时候,有时候在第一次调用 sendto 给一个 unconnected UDP socket 发送 UDP 数据包时,接下来调用 recvfrom() 或继续调sendto的时候会返回一个 ECONNREFUSED 错误 。对于一个无连接的 UDP 是不会返回这个错误的,之所以会返回这个错误,是因为你明确调用了 connect 去连接远端的 Endpoint_S 了 。那么这个错误是怎么产生的呢?没有调用 connect 的 UDP Socket 为什么无法返回这个错误呢?
当一个 UDP socket 去 connect 一个远端 Endpoint_S 时,并没有发送任何的数据包,其效果仅仅是在本地建立了一个五元组映射,对应到一个对端,该映射的作用正是为了和 UDP 带外的 ICMP 控制通道捆绑在一起,使得 UDP socket 的接口含义更加丰满 。这样内核协议栈就维护了一个从源到目的地的单向连接,当下层有ICMP(对于非IP协议,可以是其它机制)错误信息返回时,内核协议栈就能够准确知道该错误是由哪个用户socket产生的,这样就能准确将错误转发给上层应用了 。对于下层是IP协议的时候,ICMP 错误信息返回时,ICMP 的包内容就是出错的那个原始数据包,根据这个原始数据包可以找出一个五元组,根据该五元组就可以对应到一个本地的connect过的UDP socket,进而把错误消息传输给该 socket,应用程序在调用socket接口函数的时候,就可以得到该错误消息了 。
对于一个无“连接”的UDP,sendto系统调用后,内核在将数据包发送出去后,就释放了存储对端Endpoint_S的地址等信息的数据结构了,这样在下层的协议有错误返回的时候,内核已经无法追踪到源socket了 。


推荐阅读