socket端口复用问题一二
实际上,默认的情况下,如果一个网络应用程序的一个套接字 绑定了一个端口( 占用了 8000 ),这时候,别的套接字就无法使用这个端口( 8000 ), 验证例子如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
int sockfd_one;
int err_log;
sockfd_one = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字one
if(sockfd_one < 0)
{
perror("sockfd_one");
exit(-1);
}
// 设置本地网络信息
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8000); // 端口为8000
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定,端口为8000
err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
if(err_log != 0)
{
perror("bind sockfd_one");
close(sockfd_one);
exit(-1);
}
int sockfd_two;
sockfd_two = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字two
if(sockfd_two < 0)
{
perror("sockfd_two");
exit(-1);
}
// 新套接字sockfd_two,继续绑定8000端口,绑定失败
// 因为8000端口已被占用,默认情况下,端口没有释放,无法绑定
err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
if(err_log != 0)
{
perror("bind sockfd_two");
close(sockfd_two);
exit(-1);
}
close(sockfd_one);
close(sockfd_two);
return 0;
}
程序编译运行后结果如下:
那如何让sockfd_one, sockfd_two两个套接字都能成功绑定8000端口呢?这时候就需要要到端口复用了。端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。
设置socket的SO_REUSEADDR选项,即可实现端口复用:
int opt = 1;
// sockfd为需要端口复用的套接字
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt));
SO_REUSEADDR可以用在以下四种情况下。 (摘自《Unix网络编程》卷一,即UNPv1)
1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。
2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可以测试这种情况。
3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。这和2很相似,区别请看UNPv1。
4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。
需要注意的是,设置端口复用函数要在绑定之前调用,而且只要绑定到同一个端口的所有套接字都得设置复用:
// sockfd_one, sockfd_two都要设置端口复用
// 在sockfd_one绑定bind之前,设置其端口复用
int opt = 1;
setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR, (const void *)&opt, sizeof(opt) );
err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
// 在sockfd_two绑定bind之前,设置其端口复用
opt = 1;
setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,(const void *)&opt, sizeof(opt) );
err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
2020/2/27更新
SO_REUSEPORT解决了什么问题
linux man文档中一段文字描述其作用:
The new socket option allows multiple sockets on the same host to bind to the same port, and is intended to improve the performance of multithreaded network server applications running on top of multicore systems.
SO_REUSEPORT支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题:
- 允许多个套接字 bind()/listen() 同一个TCP/UDP端口
- 每一个线程拥有自己的服务器套接字
- 在服务器套接字上没有了锁的竞争
- 内核层面实现负载均衡
- 安全层面,监听同一个端口的套接字只能位于同一个用户下面
其核心的实现主要有三点:
- 扩展 socket option,增加 SO_REUSEPORT 选项,用来设置 reuseport。
- 修改 bind 系统调用实现,以便支持可以绑定到相同的 IP 和端口
- 修改处理新建连接的实现,查找 listener 的时候,能够支持在监听相同 IP 和端口的多个 sock 之间均衡选择。
代码分析,可以参考引用资料 [多个进程绑定相同端口的实现分析[Google Patch]]。
CPU之间平衡处理,水平扩展
以前通过fork
形式创建多个子进程,现在有了SO_REUSEPORT,可以不用通过fork
的形式,让多进程监听同一个端口,各个进程中accept socket fd
不一样,有新连接建立时,内核只会唤醒一个进程来accept
,并且保证唤醒的均衡性。
模型简单,维护方便了,进程的管理和应用逻辑解耦,进程的管理水平扩展权限下放给程序员/管理员,可以根据实际进行控制进程启动/关闭,增加了灵活性。
这带来了一个较为微观的水平扩展思路,线程多少是否合适,状态是否存在共享,降低单个进程的资源依赖,针对无状态的服务器架构最为适合了。
新特性测试或多个版本共存
可以很方便的测试新特性,同一个程序,不同版本同时运行中,根据运行结果决定新老版本更迭与否。
针对对客户端而言,表面上感受不到其变动,因为这些工作完全在服务器端进行。
服务器无缝重启/切换
想法是,我们迭代了一版本,需要部署到线上,为之启动一个新的进程后,稍后关闭旧版本进程程序,服务一直在运行中不间断,需要平衡过度。这就像Erlang语言层面所提供的热更新一样。
想法不错,但是实际操作起来,就不是那么平滑了,还好有一个hubtime开源工具,原理为SIGHUP信号处理器+SO_REUSEPORT+LD_RELOAD
,可以帮助我们轻松做到,有需要的同学可以检出试用一下。