UDP socket查询高速缓存

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。

是不是很简单呢?当然,经理并不这么认为。


浙江温州皮鞋湿,下雨进水不会胖。

发布了1563 篇原创文章 · 获赞 4937 · 访问量 1097万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览