端口复用
1、背景
操作系统如何区分一个socket的呢?
socket = 《A进程的IP地址:端口号,B进程的IP地址:端口号》
也就是说,只要五元素不完全一致,操作系统就能区分socket。
场景分析:
在A机上进行客户端网络编程,假如它所使用的本地端口号是1234,如果没有开启端口复用的话,它用本地端口1234去连接B机再用本地端口连接C机时就不可以,若开启端口复用的话在用本地端口1234访问B机的情况下还可以用本地端口1234访问C机。若是服务器程序中监听的端口,即使开启了复用,也不可以用该端口望外发起连接了。
端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。
2、定义
- 中间程序即是一个服务端程序(监听连接),也是一个客户端程序(发送给端口程序).操作系统内核支持通过配置socket参数的方式来实现多个进程绑定同一个端口。
- 要复用端口,必须使中间程序的监听
SOCKET
用setsockopt()
函数设置.- 复用端口的原理是用在服务器安装一个中间程序,在客户端发送数据给端口前劫获这个数据,判断这个是不是HACKER发来的数据,如果是把它发给后门程序,如果不是则转发给端口程序,返回信息再发给客户端.
3、setsockopt
用于任意类型、任意状态套接口的设置选项值。
- 选项可能存在于多层协议中,它们总会出现在最上面的套接字层。
- 当操作套接字选项时,选项位于的层和选项的名称必须给出。
- 为了操作套接字层的选项,应该将层的值指定为SOL_SOCKET。
- 为了操作其它层的选项,控制选项的合适协议号必须给出。
- 例如,为了表示一个选项由TCP协议解析,层应该设定为协议 号TCP。
#include <sys/types.h>
#include <sys/socket.h>
3.1、函数原型
int setsockopt(int sockFd, int level, int optname, const void *optval, socklen_t optlen);
3.2、参数说明
- 1、sockfd:将要被设置或者获取选项的套接字。
- 2、level:选项定义的层次;支持
SOL_SOCKET
、IPPROTO_TCP
、IPPROTO_IP
和IPPROTO_IPV6
。一般设成SOL_SOCKET
以存取socket层。- SOL_SOCKET:通用套接字选项.
- IPPROTO_IP:IP选项.IPv4套接口
- IPPROTO_TCP:TCP选项.
- IPPROTO_IPV6: IPv6套接口
- 3、optname: 欲设置的选项,有下列几种数值:
- 4、optval: 对于setsockopt(),指针,指向存放选项待设置的新值的缓冲区。获得或者是设置套接字选项.根据选项名称的数据类型进行转换。
- 5、optlen:optval缓冲区长度。
3.3、SO_REUSEADDR参数单独说明(端口复用)
optname选项之一:允许套接口和一个已在使用中的地址捆绑。
SO_REUSEADDR提供如下四个功能:
- 允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。
- 允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。对于TCP,我们根本不可能启动捆绑相同IP地址和相同端口号的多个服务器。
- 允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。
- 允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)。
SO_REUSEPORT有如下语义:
- 此选项允许完全重复捆绑,但仅在想捆绑相同IP地址和端口的套接口都指定了此套接口选项才行。
- 如果被捆绑的IP地址是一个多播地址,则SO_REUSEADDR和SO_REUSEPORT等效。
编写 TCP/SOCK_STREAM 服务程序时,SO_REUSEADDR到底什么意思?
这个套接字选项通知内核,如果端口忙,但TCP
状态位于 TIME_WAIT
,可以重用端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息,指明"地址已经使用中"。如果你的服务程序停止后想立即重启,而新套接字依旧使用同一端口,此时SO_REUSEADDR
选项非常有用。必须意识到,此时任何非期望数据到达,都可能导致服务程序反应混乱,不过这只是一种可能,事实上很不可能。
3.4、返回说明
成功执行时,返回0。失败返回-1,errno被设为以下的某个值
- EBADF:sockfd不是有效的文件描述词
- EFAULT:optval指向的内存并非有效的进程空间
- EINVAL:在调用setsockopt()时,optlen无效
- ENOPROTOOPT:指定的协议层不能识别选项
- ENOTSOCK:socket描述的不是套接字
4、实验案例
在所有TCP服务器中,在调用bind之前设置SO_REUSEADDR套接口选项;
没有设置端口复用:
//https://blog.youkuaiyun.com/tennysonsky/article/details/44062173
#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;
}
置socket的SO_REUSEADDR选项,即可实现端口复用:
server端:
#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);
// 在sockfd_one绑定bind之前,设置其端口复用
int opt = 1;
setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR,
(const void *)&opt, sizeof(opt) );
// 绑定,端口为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绑定bind之前,设置其端口复用
opt = 1;
setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,
(const void *)&opt, sizeof(opt) );
// 新套接字sockfd_two,继续绑定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;
}
端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。同时,这 n 个套接字发送信息都正常,没有问题。但是,这些套接字并不是所有都能读取信息,只有最后一个套接字会正常接收数据。
在之前的代码上,添加两个线程,分别负责接收sockfd_one,sockfd_two的信息:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
// 线程1的回调函数
void *recv_one(void *arg)
{
printf("===========recv_one==============\n");
int sockfd = (int )arg;
while(1){
int recv_len;
char recv_buf[512] = "";
struct sockaddr_in client_addr;
char cli_ip[INET_ADDRSTRLEN] = "";//INET_ADDRSTRLEN=16
socklen_t cliaddr_len = sizeof(client_addr);
recv_len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*)&client_addr, &cliaddr_len);
inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
printf("\nip:%s ,port:%d\n",cli_ip, ntohs(client_addr.sin_port));
printf("sockfd_one =========== data(%d):%s\n",recv_len,recv_buf);
}
return NULL;
}
// 线程2的回调函数
void *recv_two(void *arg)
{
printf("+++++++++recv_two++++++++++++++\n");
int sockfd = (int )arg;
while(1){
int recv_len;
char recv_buf[512] = "";
struct sockaddr_in client_addr;
char cli_ip[INET_ADDRSTRLEN] = "";//INET_ADDRSTRLEN=16
socklen_t cliaddr_len = sizeof(client_addr);
recv_len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*)&client_addr, &cliaddr_len);
inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
printf("\nip:%s ,port:%d\n",cli_ip, ntohs(client_addr.sin_port));
printf("sockfd_two @@@@@@@@@@@@@@@ data(%d):%s\n",recv_len,recv_buf);
}
return NULL;
}
int main(int argc, char *argv[])
{
int err_log;
/sockfd_one
int sockfd_one;
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);
// 在sockfd_one绑定bind之前,设置其端口复用
int opt = 1;
setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR,
(const void *)&opt, sizeof(opt) );
// 绑定,端口为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);
}
//接收信息线程1
pthread_t tid_one;
pthread_create(&tid_one, NULL, recv_one, (void *)sockfd_one);
/sockfd_two
int sockfd_two;
sockfd_two = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字two
if(sockfd_two < 0)
{
perror("sockfd_two");
exit(-1);
}
// 在sockfd_two绑定bind之前,设置其端口复用
opt = 1;
setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,
(const void *)&opt, sizeof(opt) );
// 新套接字sockfd_two,继续绑定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);
}
//接收信息线程2
pthread_t tid_two;
pthread_create(&tid_two, NULL, recv_two, (void *)sockfd_two);
while(1){ // 让程序阻塞在这,不结束
NULL;
}
close(sockfd_one);
close(sockfd_two);
return 0;
}
通过网络调试助手给这个服务器发送数据,结果显示
我们上面的用法,实际上没有太大的意义。端口复用最常用的用途应该是防止服务器重启时之前绑定的端口还未释放或者程序突然退出而系统没有释放端口。这种情况下如果设定了端口复用,则新启动的服务器进程可以直接绑定端口。如果没有设定端口复用,绑定会失败。
参考
1、https://blog.youkuaiyun.com/tennysonsky/article/details/44062173
2、https://blog.youkuaiyun.com/weibo1230123/article/details/79978745
3、https://blog.youkuaiyun.com/weixin_38638777/article/details/79667697
4、https://www.cnblogs.com/eeexu123/p/5275783.html