十年码农内功:端口篇

一、背景

最近一个服务升级,原来仅能处理TCP请求,现在又想处理UDP,为了方便维护,想使用同一个端口处理业务请求,那么TCP和UDP能同时绑定同一个端口吗?

然后引申出两个问题:

  • 多个UDP可以同时绑定同一端口吗?
  • 多个TCP可以同时绑定同一端口吗?

所以想详细研究和总结下端口相关的知识,一劳永逸。

二、结论:地址 / 端口复用

  1. 多个Socket(均未开启 SO_REUSEADDR 和 SO_REUSEPORT)同时绑定相同IP:PORT,除第一个外所有绑定的Socket会报错(Address already in use);

图1

  1. 多个Socket(均开启SO_REUSEADDR或者均开启SO_REUSEPORT)同时绑定相同IP:PORT,所有绑定的Socket均能绑定成功;

图2

  1. 多个Socket(均开启 SO_REUSEADDR 且均未开启 SO_REUSEPORT )同时绑定相同IP:PORT,但是只有最后绑定的Socket能收到数据;

图3

  1. 多个Socket(均开启 SO_REUSEPORT )同时绑定相同IP:PORT,内核根据客户端的IP:PORT把数据hash到对应的Socket上;

图4

三、应用场景:地址复用

场景一

TCP服务器重启有时会遇到“地址已经被使用”错误,这个错误就是多个Socket绑定到同一个IP:PORT所致。那为啥第一次启动服务不会报错呢?因为这里的“多个Socket”指的是服务启动前的Socket(处于TCP的TIME_WAIT状态,还未关闭)和服务启动后新创建的Socket。

复习一下TIME_WAIT,下面是TCP的四次挥手:

图5

主动发起关闭链接的TCP Socket在TIME_WAIT停留持续时间是固定的,是最长分节生命期MSL(maximum segment lifetime)的两倍,一般称之为2MSL。Linux系统停留在TIME_WAIT的时间为固定的60秒。

如果重启前服务的TCP Socket处于TIME_WAIT状态,重启服务一般都是几秒的事,那么服务重启后肯定会报错(Address already in use)。

那么如何解决呢,答案就Socket设置SO_REUSEADDR。

UDP没有像TCP那么多状态,UDP服务重启立刻就关闭了所依赖的连接,所以单纯为了重启场景不需要设置SO_REUSEADDR。

场景二

如图6,机器上有多张网卡时,Socket 1 绑定0.0.0.0:12345,Socket 2 绑定172.17.0.3:12345,在绑定之前均开启了SO_REUSEADDR并且均未开启SO_REUSEPORT,那么客户端发给172.17.0.1:12345和172.17.0.2:12345的数据均会被Socket 1接收到,而发给172.17.0.3:12345的数据会被Socket 2接收到。

图6

说白了就是 Socket 1 少绑定了一步操作,除非IP特别多,否则不建议这么操作,做了就是给自己挖坑。

四、应用场景:端口复用

多个Socket在Bind之前开启了SO_REUSEPORT,那么它们可以同时绑定和监听同一个IP:PORT,每个线程/进程处理一个Socket的数据收发,这样可以充分发挥多核的性能。

TCP服务架构的演进

  1. 如图7,单线程accept socket,然后把接收到来自客户端发起(connect)的连接返回的socket分发到其他工作线程。性能瓶颈在单accept线程。

图7

  1. 如图8,多个线程accept 同一个listen socket,所有新链接只保存在一个listen socket 的全链接队列中,那么多个线程去这个队列里获取(accept)新的链接,势必会出现多个线程对一个公共资源的争抢,争抢过程中,大量资源的损耗,Linux<2.6还会出现惊群现象。

图8

  1. 如图9,多个工作线程的socket同时bind同一个IP:PORT,并且各自独立运行listen、accept和process。每个socket要设置SO_REUSEPORT。当客户端发起连接请求(connect),内核根据客户端的IP:PORT hash映射到对应工作线程的socket上。

图9

nginx 开启 SO_REUSEPORT 功能后,性能有立竿见影的提升。

UDP使用场景

和TCP类似,只不过UDP相对简单。

  1. 如图10,单线程bind socket并处理数据收发,通常只做这一件事,一般上限是处理10万QPS。

图10

  1. 如图11,多个工作线程共用一个绑定过后的socket,与TCP一样存在资源竞争。实际工作中很少这么用。

图11

  1. 如同12,多个工作线程的 socket 同时 bind 同一个IP:PORT,并且各自独立运行 process。每个 socket 要设置 SO_REUSEPORT。当客户端发送(sendto)数据,内核根据客户端的IP:PORT hash映射到对应工作线程的socket上。

图12

我最近写的一个服务就是采用这种方式,性能明显提升,由原来的10万QPS(图10的方式),提升到50万QPS(5个工作线程)。

五、客户端:端口复用

对于TCP的客户端来说不需要Bind,但是客户端可能出现大量TIME_WAIT状态的TCP连接,导致端口被耗尽。

net.ipv4.tcp_tw_reuse这个内核参数开启后,客户端调用connect函数时,如果选择到的端口,已经被相同五元组的连接占用的时候,就会判断该连接是否处于TIME_WAIT状态。

如果该连接处于TIME_WAIT状态并且TIME_WAIT状态持续的时间超过了1秒,那么就会重用这个连接,然后就可以正常使用该端口了。只适用于连接的发起方。

六、TCP和UDP可以同时绑定同一端口吗?

回到文章开头的问题,为啥放在最后解答,是因为默认情况下,TCP和UDP就可以同时绑定相同的IP:PORT

TCP Socket创建函数和参数:

int fd = socket(AF_INET, SOCK_STREAM, 0);

UDP Socket创建函数和参数:

int fd = socket(AF_INET, SOCK_DGRAM, 0);

TCP和UDP的Bind操作一样:

int ret = bind(fd, (struct sockaddr *)&svrAddr, sizeof(svrAddr));

实际上Socket绑定端口前都设置了具体的协议,所以不是绑定IP:PORT,而是绑定了PROTOCOL:IP:PORT

如图13,TCP Socket绑定TCP:172.17.0.2:12345,UDP Socket绑定UDP:172.17.0.2:12345,它们各自处理自己的数据,互不干扰。

图13

七、参考

创作不易,你的点赞、转发、收藏是我创作的最大动力!

更多分享尽在微信公众号(科英)!

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY1b7LZx' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片