socket 网络编程——端口复用技术(setsockopt())(linux下多个进程监听同一个端口)

端口复用

1、背景

操作系统如何区分一个socket的呢?
socket = 《A进程的IP地址:端口号,B进程的IP地址:端口号》
在这里插入图片描述
也就是说,只要五元素不完全一致,操作系统就能区分socket。

场景分析:
在A机上进行客户端网络编程,假如它所使用的本地端口号是1234,如果没有开启端口复用的话,它用本地端口1234去连接B机再用本地端口连接C机时就不可以,若开启端口复用的话在用本地端口1234访问B机的情况下还可以用本地端口1234访问C机。若是服务器程序中监听的端口,即使开启了复用,也不可以用该端口望外发起连接了。

端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错。

2、定义

  • 中间程序即是一个服务端程序(监听连接),也是一个客户端程序(发送给端口程序).操作系统内核支持通过配置socket参数的方式来实现多个进程绑定同一个端口。
  • 要复用端口,必须使中间程序的监听SOCKETsetsockopt()函数设置.
    • 复用端口的原理是用在服务器安装一个中间程序,在客户端发送数据给端口前劫获这个数据,判断这个是不是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_SOCKETIPPROTO_TCPIPPROTO_IPIPPROTO_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.csdn.net/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.csdn.net/tennysonsky/article/details/44062173
2、https://blog.csdn.net/weibo1230123/article/details/79978745
3、https://blog.csdn.net/weixin_38638777/article/details/79667697
4、https://www.cnblogs.com/eeexu123/p/5275783.html

  • 34
    点赞
  • 121
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 使用 Socket 在 C 语言中监听指定网络端口的代码片段如下: ``` #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int main(int argc, char *argv[]) { // 监听端口 int port = 8080; // 创建 socket int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket error"); exit(1); } // 绑定端口 struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡的请求 addr.sin_port = htons(port); if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind error"); exit(1); } // 开始监听 if (listen(sockfd, 10) < 0) { perror("listen error"); exit(1); } // 等待客户端的连接 struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); int client_sockfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_addr_len); if (client_sockfd < 0) { perror("accept error"); exit(1); } // 处理客户端的请求 // ... // 关闭 socket close(client_sockfd); close(sockfd); return 0; } ``` 在上面的代码中,首先使用 `socket` 函数创建了一个 socket,然后使用 `bind` 函数将其绑定到指定的端口上。接着,使用 `listen` 函数开始监听端口,并使用 `accept` 函数等待客户端的连接。最后,使用 `close` 函数关闭 socket。 希望上面的代 ### 回答2: 下面是一个使用Socket监听指定网络端口的C代码片段(基于Linux操作系统): ```c #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 serverSocket, clientSocket; struct sockaddr_in serverAddress, clientAddress; int portNumber = 1234; // 要监听端口号 // 创建套接字 serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (serverSocket < 0) { perror("套接字创建失败"); exit(1); } // 设置服务器地址 memset(&serverAddress, 0, sizeof(serverAddress)); serverAddress.sin_family = AF_INET; serverAddress.sin_addr.s_addr = htonl(INADDR_ANY); serverAddress.sin_port = htons(portNumber); // 将套接字绑定到指定端口 if (bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) < 0) { perror("绑定套接字失败"); exit(1); } // 监听连接请求 if (listen(serverSocket, 5) < 0) { perror("监听失败"); exit(1); } printf("服务器正在监听端口 %d\n", portNumber); // 接受客户端连接请求 socklen_t clientAddressLength = sizeof(clientAddress); clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddress, &clientAddressLength); if (clientSocket < 0) { perror("接受连接失败"); exit(1); } // 从客户端接收数据 char buffer[1024]; memset(buffer, 0, sizeof(buffer)); ssize_t bytesRead = read(clientSocket, buffer, sizeof(buffer) - 1); if (bytesRead < 0) { perror("读取数据失败"); exit(1); } printf("接收到来自客户端的数据:%s\n", buffer); // 向客户端发送响应数据 char response[] = "这是服务器的响应数据"; ssize_t bytesSent = write(clientSocket, response, strlen(response)); if (bytesSent < 0) { perror("发送数据失败"); exit(1); } printf("响应数据已发送给客户端\n"); // 关闭套接字 close(clientSocket); close(serverSocket); return 0; } ``` 该代码片段使用C语言编写,基于Linux操作系统。首先创建一个套接字,然后设置服务器地址并将套接字绑定到指定的端口。然后使用listen函数开始监听连接请求。接着使用accept函数接受客户端的连接请求,并通过read函数接收来自客户端的数据。在接收数据后,通过write函数向客户端发送响应数据。最后关闭套接字。 ### 回答3: 在Linux操作系统下,使用C语言编写一个使用Socket监听指定网络端口的代码片段如下: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> int main() { int server_fd, new_socket, valread; struct sockaddr_in address; int opt = 1; int addrlen = sizeof(address); char buffer[1024] = {0}; const int PORT = 8888; // 创建套接字 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket creation failed"); exit(EXIT_FAILURE); } // 设置套接字选项 if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { perror("setsockopt failed"); exit(EXIT_FAILURE); } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); // 绑定套接字 if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 监听端口 if (listen(server_fd, 3) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } printf("Listening on port %d...\n", PORT); // 接受连接 if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) { perror("accept failed"); exit(EXIT_FAILURE); } // 读取客户端的消息 valread = read(new_socket, buffer, 1024); printf("Received message: %s\n", buffer); return 0; } ``` 以上代码实现了一个使用Socket监听指定网络端口的简单服务器。首先,创建一个Socket套接字,并设置套接字选项。然后,绑定套接字到指定端口,并开始监听端口。最后,当有客户端连接时,接受连接并读取客户端发送的消息。 这段代码中监听端口为8888,可以根据实际需求进行修改。另外,代码中只处理了一个客户端的连接和消息读取,如果需要处理多个客户端连接,可以使用循环结构来接受多个连接,并在子进程或线程中处理每个连接的消息。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值