Linux网络编程——端口复用(多个套接字绑定同一个端口)

在《绑定( bind )端口需要注意的问题》提到:一个网络应用程序只能绑定一个端口( 一个套接字只能绑定一个端口 )。

实际上,默认的情况下,如果一个网络应用程序的一个套接字 绑定了一个端口( 占用了 8000 ),这时候,别的套接字就无法使用这个端口( 8000 ), 验证例子如下:

[objc] view
plain
copy

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <arpa/inet.h>
  8. int main(int argc, charchar *argv[])
  9. {
  10. int sockfd_one;
  11. int err_log;
  12. sockfd_one = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字one
  13. if(sockfd_one < 0)
  14. {
  15. perror("sockfd_one");
  16. exit(-1);
  17. }
  18. // 设置本地网络信息
  19. struct sockaddr_in my_addr;
  20. bzero(&my_addr, sizeof(my_addr));
  21. my_addr.sin_family = AF_INET;
  22. my_addr.sin_port = htons(8000);     // 端口为8000
  23. my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  24. // 绑定,端口为8000
  25. err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
  26. if(err_log != 0)
  27. {
  28. perror("bind sockfd_one");
  29. close(sockfd_one);
  30. exit(-1);
  31. }
  32. int sockfd_two;
  33. sockfd_two = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字two
  34. if(sockfd_two < 0)
  35. {
  36. perror("sockfd_two");
  37. exit(-1);
  38. }
  39. // 新套接字sockfd_two,继续绑定8000端口,绑定失败
  40. // 因为8000端口已被占用,默认情况下,端口没有释放,无法绑定
  41. err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
  42. if(err_log != 0)
  43. {
  44. perror("bind sockfd_two");
  45. close(sockfd_two);
  46. exit(-1);
  47. }
  48. close(sockfd_one);
  49. close(sockfd_two);
  50. return 0;
  51. }

程序编译运行后结果如下:

那如何让sockfd_one, sockfd_two两个套接字都能成功绑定8000端口呢?这时候就需要要到端口复用了。端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。

设置socket的SO_REUSEADDR选项,即可实现端口复用:

[objc] view
plain
copy

  1. int opt = 1;
  2. // sockfd为需要端口复用的套接字
  3. setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const voidvoid *)&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。

需要注意的是,设置端口复用函数要在绑定之前调用,而且只要绑定到同一个端口的所有套接字都得设置复用:

[objc] view
plain
copy

  1. // sockfd_one, sockfd_two都要设置端口复用
  2. // 在sockfd_one绑定bind之前,设置其端口复用
  3. int opt = 1;
  4. setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR, (const voidvoid *)&opt, sizeof(opt) );
  5. err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
  6. // 在sockfd_two绑定bind之前,设置其端口复用
  7. opt = 1;
  8. setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,(const voidvoid *)&opt, sizeof(opt) );
  9. err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));

端口复用完整代码如下:

[objc] view
plain
copy

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <arpa/inet.h>
  8. int main(int argc, charchar *argv[])
  9. {
  10. int sockfd_one;
  11. int err_log;
  12. sockfd_one = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字one
  13. if(sockfd_one < 0)
  14. {
  15. perror("sockfd_one");
  16. exit(-1);
  17. }
  18. // 设置本地网络信息
  19. struct sockaddr_in my_addr;
  20. bzero(&my_addr, sizeof(my_addr));
  21. my_addr.sin_family = AF_INET;
  22. my_addr.sin_port = htons(8000);     // 端口为8000
  23. my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  24. // 在sockfd_one绑定bind之前,设置其端口复用
  25. int opt = 1;
  26. setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR,
  27. (const voidvoid *)&opt, sizeof(opt) );
  28. // 绑定,端口为8000
  29. err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
  30. if(err_log != 0)
  31. {
  32. perror("bind sockfd_one");
  33. close(sockfd_one);
  34. exit(-1);
  35. }
  36. int sockfd_two;
  37. sockfd_two = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字two
  38. if(sockfd_two < 0)
  39. {
  40. perror("sockfd_two");
  41. exit(-1);
  42. }
  43. // 在sockfd_two绑定bind之前,设置其端口复用
  44. opt = 1;
  45. setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,
  46. (const voidvoid *)&opt, sizeof(opt) );
  47. // 新套接字sockfd_two,继续绑定8000端口,成功
  48. err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
  49. if(err_log != 0)
  50. {
  51. perror("bind sockfd_two");
  52. close(sockfd_two);
  53. exit(-1);
  54. }
  55. close(sockfd_one);
  56. close(sockfd_two);
  57. return 0;
  58. }

端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。同时,这 n 个套接字发送信息都正常,没有问题。但是,这些套接字并不是所有都能读取信息,只有最后一个套接字会正常接收数据。

下面,我们在之前的代码上,添加两个线程,分别负责接收sockfd_one,sockfd_two的信息:

[objc] view
plain
copy

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <sys/socket.h>
  6. #include <netinet/in.h>
  7. #include <arpa/inet.h>
  8. #include <pthread.h>
  9. // 线程1的回调函数
  10. voidvoid *recv_one(voidvoid *arg)
  11. {
  12. printf("===========recv_one==============\n");
  13. int sockfd = (int )arg;
  14. while(1){
  15. int recv_len;
  16. char recv_buf[512] = "";
  17. struct sockaddr_in client_addr;
  18. char cli_ip[INET_ADDRSTRLEN] = "";//INET_ADDRSTRLEN=16
  19. socklen_t cliaddr_len = sizeof(client_addr);
  20. recv_len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*)&client_addr, &cliaddr_len);
  21. inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
  22. printf("\nip:%s ,port:%d\n",cli_ip, ntohs(client_addr.sin_port));
  23. printf("sockfd_one =========== data(%d):%s\n",recv_len,recv_buf);
  24. }
  25. return NULL;
  26. }
  27. // 线程2的回调函数
  28. voidvoid *recv_two(voidvoid *arg)
  29. {
  30. printf("+++++++++recv_two++++++++++++++\n");
  31. int sockfd = (int )arg;
  32. while(1){
  33. int recv_len;
  34. char recv_buf[512] = "";
  35. struct sockaddr_in client_addr;
  36. char cli_ip[INET_ADDRSTRLEN] = "";//INET_ADDRSTRLEN=16
  37. socklen_t cliaddr_len = sizeof(client_addr);
  38. recv_len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*)&client_addr, &cliaddr_len);
  39. inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
  40. printf("\nip:%s ,port:%d\n",cli_ip, ntohs(client_addr.sin_port));
  41. printf("sockfd_two @@@@@@@@@@@@@@@ data(%d):%s\n",recv_len,recv_buf);
  42. }
  43. return NULL;
  44. }
  45. int main(int argc, charchar *argv[])
  46. {
  47. int err_log;
  48. /////////////////////////sockfd_one
  49. int sockfd_one;
  50. sockfd_one = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字one
  51. if(sockfd_one < 0)
  52. {
  53. perror("sockfd_one");
  54. exit(-1);
  55. }
  56. // 设置本地网络信息
  57. struct sockaddr_in my_addr;
  58. bzero(&my_addr, sizeof(my_addr));
  59. my_addr.sin_family = AF_INET;
  60. my_addr.sin_port = htons(8000);     // 端口为8000
  61. my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  62. // 在sockfd_one绑定bind之前,设置其端口复用
  63. int opt = 1;
  64. setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR,
  65. (const voidvoid *)&opt, sizeof(opt) );
  66. // 绑定,端口为8000
  67. err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
  68. if(err_log != 0)
  69. {
  70. perror("bind sockfd_one");
  71. close(sockfd_one);
  72. exit(-1);
  73. }
  74. //接收信息线程1
  75. pthread_t tid_one;
  76. pthread_create(&tid_one, NULL, recv_one, (voidvoid *)sockfd_one);
  77. /////////////////////////sockfd_two
  78. int sockfd_two;
  79. sockfd_two = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字two
  80. if(sockfd_two < 0)
  81. {
  82. perror("sockfd_two");
  83. exit(-1);
  84. }
  85. // 在sockfd_two绑定bind之前,设置其端口复用
  86. opt = 1;
  87. setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,
  88. (const voidvoid *)&opt, sizeof(opt) );
  89. // 新套接字sockfd_two,继续绑定8000端口,成功
  90. err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
  91. if(err_log != 0)
  92. {
  93. perror("bind sockfd_two");
  94. close(sockfd_two);
  95. exit(-1);
  96. }
  97. //接收信息线程2
  98. pthread_t tid_two;
  99. pthread_create(&tid_two, NULL, recv_two, (voidvoid *)sockfd_two);
  100. while(1){   // 让程序阻塞在这,不结束
  101. NULL;
  102. }
  103. close(sockfd_one);
  104. close(sockfd_two);
  105. return 0;
  106. }

接着,通过网络调试助手给这个服务器发送数据,结果显示,只有最后一个套接字sockfd_two会正常接收数据:

我们上面的用法,实际上没有太大的意义。端口复用最常用的用途应该是防止服务器重启时之前绑定的端口还未释放或者程序突然退出而系统没有释放端口。这种情况下如果设定了端口复用,则新启动的服务器进程可以直接绑定端口。如果没有设定端口复用,绑定会失败,提示ADDR已经在使用中——那只好等等再重试了,麻烦!

时间: 2024-11-03 21:00:49

Linux网络编程——端口复用(多个套接字绑定同一个端口)的相关文章

【UNIX网络编程(一)】套接字地址结构、网络字节序和地址转换函数

引言:套接字地址结构在网络编程的每个实现中都要用到,因此掌握套接字地址结构是以后编写网络程序的前提,地址结构可以在两个方向上传递:从进程到内核和从内核到进程.地址转换函数在地址的文本表达和他们存放在套接字地址结构中的二进制值之间进行转换. 大多数套接字函数都需要一个指向套接字地址结构的指针作为参数.不同协议都有自己的套接字地址结构.通用的套接字地址结构是sockaddr.IPv4套接字地址结构是定义在头文件<netinet/in.h>中的sockaddr_in,其POSIX定义如下: stru

TCP/IP网络编程读书笔记-简单的套接字编程(1)

在linux和windows下都是通过套接字编程进行网络编程.不同的系统上通信有部分差别,现在刚开始学习,给自己学习的时候一个总结. 一,socket函数的套接字步骤 第一,linux网络编程中接受连接请求(服务器端)套接字的四个步骤: 1)调用socket函数创建套接字 2)调用bind函数分配IP地址和端口号 3)调用listen函数转为可接收请求状态 4)调用accept函数受理连接请求 第二,linux网络编程中请求连接(客户端)套接字的两个步骤: 1)调用socket函数创建套接字 2

Linux网络编程-IO复用技术

IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了.Linux中,提供了select.poll.epoll三种接口函数来实现IO复用. 1.select函数 #include <sys/select.h> #include <sys/time.h> int select(int nfds, fd_set *readfds, fd_set *writef

UNIX网络编程笔记(2)—套接字编程简介

套接字编程概述 说到网络编程一定都离不开套接字,以前用起来的时候大多靠记下来它的用法,这一次希望能理解一些更底层的东西,当然这些都是网络编程的基础- (1)套接字地址结构 大多说套接字函数都需要一个指向套接字地址结构的指针作为参数,每个协议族都定义它自己的套接字地址结构,这些结构都以sockadd_开头. IPV4套接字地址结构 IPv4套接字地址结构通常称为"网际套接字地址结构",以sockaddr_in命名,并定义在 /* Internet address. */ typedef

网络编程(二):套接字Socket

socket是基于C/S架构的,也就是说进行socket网络编程,通常需要编写两个py文件,一个服务端,一个客户端. 首先,导入Python中的socket模块: import socket 其通信逻辑如下图所示: 这张图片是整个socket编程的基础,必须牢牢记住. 通过导入模块import socket后用socket.socket()创建套接字,格式如下: sk = socket.socket([family[, type[, proto]]]) 参数说明: family: 套接字家族,可

LINUX网络编程 IO 复用

参考<linux高性能服务器编程> LINUX下处理多个连接时候,仅仅使用多线程和原始socket函数,效率十分低下 于是就出现了selelct poll  epoll等IO复用函数. 这里讨论性能最优的epoll IO复用 用户将需要关注的socket连接使用IO复用函数放进一个事件表中,每当事件表中有一个或者多个SOCKET连接出现读写请求时候,则进行处理 事件表使用一个额外的文件描述符来标识.文件描述符使用 epoll_create函数创建 #inlclude <sys/epoll

linux网络编程基础--(转自网络)

转自 http://www.cnblogs.com/MyLove-Summer/p/5215287.html Linux下的网络编程指的是socket套接字编程,入门比较简单. 1. socket套接字介绍 socket机制其实就是包括socket, bind, listen, connect, accept等函数的方法,其通过指定的函数实现不同的协议(IP4,IP6等)的数据在不同层之间的传输和获取等处理.其实个人理解socket就是处于应用层和TCP/IP协议之间的一个中间层,具体的数据分析

Linux网络编程:端口复用

在<绑定( bind )端口需要注意的问题>提到:一个网络应用程序只能绑定一个端口( 一个套接字只能绑定一个端口 ). 实际上,默认的情况下,如果一个网络应用程序的一个套接字 绑定了一个端口( 占用了 8000 ),这时候,别的套接字就无法使用这个端口( 8000 ), 验证例子如下: #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #i

linux网络编程-(socket套接字编程UDP传输)

今天我们来介绍一下在linux网络环境下使用socket套接字实现两个进程下文件的上传,下载,和退出操作! 在socket套接字编程中,我们当然可以基于TCP的传输协议来进行传输,但是在文件的传输中,如果我们使用TCP传输,会造成传输速度较慢的情况,所以我们在进行文件传输的过程中,最好要使用UDP传输. 在其中,我们需要写两个程序,一个客户端,一个服务端,在一个终端中,先运行服务端,在运行客户端,在服务端和客户端都输入IP地址和端口号,注意服务端和客户端的端口号要相同,然后选择功能,在linux