TCP/IP编程之SO_REUSEADDR和SO_REUSEPORT套接字选项

基本概念:

SO_REUSEADDR套接字选项能起到以下4个不同的功用:

(1)SO_REUSEADDR允许启动一个监听服务器并捆绑众所周知端口,即使以前建立的该端口用作它们的本地端口的连接仍存在。

这个条件通常是这样碰到的:

a)启动一个监听服务器;

b)连接请求到的,派生一个子进程来处理这个客户;

c)监听服务器终止,但子进程继续为现有的连接上的客户提供服务;

d)重启监听服务器。

默认情况下,当监听服务器在步骤d调用socket、bind和listen重新启动时,由于它试图捆绑一个现有连接上的端口,从而bind调用会失败。但是如果该服务器在socket和bind两个调用之间设置了SO_REUSEADDR套接字选项,那么bind将成功。所有TCP服务器都应该指定本套接字选项,以允许服务器在这种情形下被重新启动。

(2)SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。

(3)SO_REUSEADDR允许单个进程捆绑同一个端口到多个套接字上,只要每次捆绑指定不同的本地IP地址即可。

(4)SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口已绑定到某个套接字上时。如果传输协议支持,同样的IP地址和端口还可以捆绑到另一个套接字上。一般来说,本特性仅支持UDP套接字。

 

SO_REUSEPORT套接字选项能起到以下2个不同的功用:

(1)本选项允许完全重复的捆绑,不过只有在想要捆绑同一IP地址和端口的每个套接字都指定了本套接字选项才行。

(2)如果被捆绑的IP地址是一个多播地址,那么SO_REUSEADDR和SO_REUSEPORT被认为是等效的。

 

这里我们的重点讨论内容是:

SO_REUSEADDR的第(1)个功用(蓝色字体)

SO_REUSEPORT的第(1)个功用(蓝色字体)

 

应用场景:nginx平滑升级就应用到了相关知识。

 

例子1:

我们需要知道,

a) 如果不对TCP的套接字选项进行任何限制时,如果启动两个进程,第二个进程就会在调用bind函数的时候出错(Address already in use)。

b) 如果在调用bind之前我们设置了SO_REUSEADDR,但是不在第二个进程启动前close这个套接字,那么第二个进程仍然会在调用bind函数的时候出错(Address already in use)。

c)如果在调用bind之前我们设置了SO_REUSEADDR,并接收了一个客户端连接,并且在第二个进程启动前关闭了bind的套接字,这个时候第一个进程只拥有一个套接字(与客户端的连接),那么第二个进程则可以bind成功,符合预期。

代码:

 

 
  1. #include

  2. #include

  3. #include

  4. #include

  5. #include

  6. #include

  7. #include

  8.  
  9. void Perror(const char *s)

  10. {

  11. perror(s);

  12. exit(EXIT_FAILURE);

  13. }

  14.  
  15. int main()

  16. {

  17. int sockfd = socket(AF_INET, SOCK_STREAM, 0); //TCP

  18. int backlog = 100;

  19. short port = 9527; //端口

  20. struct sockaddr_in servaddr;

  21. servaddr.sin_family = AF_INET; //IPv4

  22. servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //表示由内核去选择IP地址

  23. servaddr.sin_port = htons(port);

  24.  
  25. int flag = 1;

  26. if (-1 == setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag))) {

  27. Perror("setsockopt fail");

  28. }

  29.  
  30. int res = bind(sockfd, (sockaddr *)&servaddr, sizeof(servaddr));

  31. if (0 == res)

  32. printf("server bind success, 0.0.0.0:%d\n", port);

  33. else {

  34. Perror("bind fail");

  35. }

  36.  
  37. if (-1 == listen(sockfd, backlog)) {

  38. Perror("listen fail");

  39. }

  40.  
  41. //等待连接

  42. struct sockaddr_in cliaddr;

  43. socklen_t len = sizeof(cliaddr);

  44. int connfd = accept(sockfd, (sockaddr *)&cliaddr, &len);

  45. if (-1 == connfd) {

  46. Perror("accept fail");

  47. }

  48.  
  49. //解析客户端地址

  50. char buff[INET_ADDRSTRLEN + 1] = {0};

  51. inet_ntop(AF_INET, &cliaddr.sin_addr, buff, INET_ADDRSTRLEN);

  52. uint16_t cli_port = ntohs(cliaddr.sin_port);

  53. printf("connection from %s, port %d\n", buff, cli_port);

  54.  
  55. //关闭bind的sockfd

  56. close(sockfd);

  57.  
  58. //

  59. sleep(1200);

  60.  
  61. return 0;

  62. }

 

 

编译:

g++ server.cpp -o s1

g++ server.cpp -o s2

运行结果:

 

例子2,

SO_REUSEPORT可能在比较旧的内核版本上不支持,我的测试环境的内核版本是3.10,相对SO_REUSEADDR来说,SO_REUSEPORT没有那么多的限制条件,允许两个毫无血缘关系的进程使用相同的IP地址同时监听同一个端口,并且不会出现惊群效应。

代码:

 

 
  1. #include

  2. #include

  3. #include

  4. #include

  5. #include

  6. #include

  7. #include

  8.  
  9. void Perror(const char *s)

  10. {

  11. perror(s);

  12. exit(EXIT_FAILURE);

  13. }

  14.  
  15. int main()

  16. {

  17. int sockfd = socket(AF_INET, SOCK_STREAM, 0); //TCP

  18. int backlog = 100;

  19. short port = 9527; //端口

  20. struct sockaddr_in servaddr;

  21. servaddr.sin_family = AF_INET; //IPv4

  22. servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //表示由内核去选择IP地址

  23. servaddr.sin_port = htons(port);

  24.  
  25. int flag = 1;

  26. if (-1 == setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &flag, sizeof(flag))) {

  27. Perror("setsockopt fail");

  28. }

  29.  
  30. int res = bind(sockfd, (sockaddr *)&servaddr, sizeof(servaddr));

  31. if (0 == res)

  32. printf("server bind success, 0.0.0.0:%d\n", port);

  33. else {

  34. Perror("bind fail");

  35. }

  36.  
  37. if (-1 == listen(sockfd, backlog)) {

  38. Perror("listen fail");

  39. }

  40.  
  41. //等待连接

  42. while (1) {

  43. struct sockaddr_in cliaddr;

  44. socklen_t len = sizeof(cliaddr);

  45. int connfd = accept(sockfd, (sockaddr *)&cliaddr, &len);

  46. if (-1 == connfd) {

  47. Perror("accept fail");

  48. }

  49.  
  50. //解析客户端地址

  51. char buff[INET_ADDRSTRLEN + 1] = {0};

  52. inet_ntop(AF_INET, &cliaddr.sin_addr, buff, INET_ADDRSTRLEN);

  53. uint16_t cli_port = ntohs(cliaddr.sin_port);

  54. printf("connection from %s, port %d\n", buff, cli_port);

  55. }

  56.  
  57. return 0;

  58. }

 

 

编译:

g++ server.cpp -o s1

 

g++ server.cpp -o s2

运行结果:

 

参考:《unix网络编程》·卷1

End;

你可能感兴趣的:(读书整理)