UDP没有连接的概念,所以UDP不会保存 “正在和谁通信的信息” ,换句话说,UDP数据的发送是oneshot的。
我们来做个实验,两台机器分别部署UDP的server和client。
先看server:
#define PORT 2222
int main()
{
int sockfd;
char buffer[MAXLINE];
char *tosend = "aaaaaaaaaaa";
struct sockaddr_in servaddr, cliaddr;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("192.168.56.101");
servaddr.sin_port = htons(PORT);
bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr));
n = recvfrom(sockfd, (char *)buffer, 64, MSG_WAITALL, ( struct sockaddr *) &cliaddr, &len);
sendto(sockfd, (const char *)tosend, strlen(tosend), MSG_CONFIRM, (const struct sockaddr *) &cliaddr, len);
return 0;
}
server逻辑很简单,接受client的一个buffer,然后回复一堆aaaaaaa…
再看client:
#define PORT 2222
int main()
{
int sockfd;
char buffer[MAXLINE];
char *tosend = "bbbbbbbbbbbbb";
struct sockaddr_in servaddr;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
servaddr.sin_addr.s_addr = inet_addr("192.168.56.101");
sendto(sockfd, (const char *)tosend, strlen(tosend), MSG_CONFIRM, (const struct sockaddr *) &servaddr, sizeof(servaddr));
n = recvfrom(sockfd, (char *)buffer, 64, MSG_WAITALL, (struct sockaddr *) &servaddr, &len);
printf("data: %s %d\n", buffer, htons(servaddr.sin_port));
return 0;
}
在client上配置iptables规则:
iptables -t raw -A INPUT -p udp -j NOTRAC
iptables -t nat -A INPUT -p udp --sport 2222 -j SNAT --to-source :12345
意思很简单,client给server的2222端口发送一堆b,然后server使用12345端口回复一堆a给client。作为iptables的替代,在recvfrom和sendto之间再次bind不同的address即可。
这肯定是可行的,数据是可以发送成功的。
- 两个UDP socket互相通信,源端口是可以不断变化的。
UDP接收端在真的收到数据报文之前,是不知道源端口变化了的,按照UDP的规范,UDP socket在协议栈里不能以4元组来组织,而只能通过 {本地IP地址,本地端口} 来组织。
即便是connected UDP也不行。
connected UDP在查找时只是在compare时附加了源IP地址和源端口的检测。
这意味着一些高负载流式的UDP服务(用UDP传输流式数据,比如quic)在 连接数 (即唯一四元组的数量)很多时,socket无法通过四元组很好地进行散列,而只能退化成一条链表:
- 服务端的端口与IP均相同。(相关的socket会hash到同一个bucket)
- 客户端的端口与IP无法被利用(按照UDP语义和规范,它们‘可能’会发生变化)。
但是,可以为四元组加个缓存!
我们看Linux内核的实现,为了优化查询性能,在 目标端口 hash之外,引入了一个 {目标IP,目标端口} 二元组hash,当端口hash表冲突链长度大于10时,启用二元组hash查询。
然而,无济于事!
为了解决这个问题,加个四元组hash表查询缓存是一个正确的思路。缓存是规范外的。
我们可以看到,类似nf_conntrack,UDP防火墙等等均采用了 “UDP四元组作为连接性” 来优化查询性能的。我们看一个nf_conntrack的例子:
ipv4 2 udp 17 22 src=192.168.56.101 dst=192.168.56.110 sport=2222 dport=45069 [UNREPLIED] src=192.168.56.110 dst=192.168.56.101 sport=45069 dport=12345 mark=0 zone=0 use=2
实现这个代码非常简单,照猫画虎UDP socket组织的hash2即可,在hash2操作的地方,按照引入一个四元组hash3的对应操作即可:
- udp_lib_get_port: 按照四元组插入hash3。
- __udp4_lib_lookup_skb: 先按照四元组查询hash3。
- udp_lib_rehash: 如果探测到元组变化,则更新hash3。
- udp_lib_unhash: 结束时,从hash3中摘除当前socket四元组item。
是不是很简单呢?当然,经理并不这么认为。
浙江温州皮鞋湿,下雨进水不会胖。