• socket 问题: 连续bind/listen 同一个socket fd


    找一个进程可用端口号

    如果我们想尝试哪个端口号可用,然后监听该端口,该如何处理?比如FTP协议里面有这样一个需求,PASV模式下,Server需要监听本地数据端口,通常是找一个随机端口号进行监听。而且每收到一个客户PASV命令后,就需要提供一个不同的数据端口,这也就是说,不能两个连接同时使用同一个数据端口。那么,我们如何确定一个可用的本地端口号呢?

    我们知道,正常的监听本地端口流程是:socket(), bind(), listen()。如果从socket到listen,监听一个端口号,都调用成功,说明该端口号可用。因此,我们可以用这种方法创建一个可用的socket fd。

    #include <unistd.h>
    #include <errno.h>
    #include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    #include <stdio.h>
    #include <arpa/inet.h>
    using namespace std;
    
    typedef sockaddr SA;
    int socket_create(const char* ip, const int port)
    {
    	if (ip == NULL || port < 0) {
    		return -1;
    	}
    
    	/* create socket */
    	int sock = socket(AF_INET, SOCK_STREAM, 0);
    	if (sock < 0) {
    		perror("socket error");
    		return -1;
    	}
    
    	/* set sock option SO_REUSEADDR for re-bind when calling socket failed */
    	int op = 1;
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op));
    
    	struct sockaddr_in local;
    	local.sin_family = AF_INET;
    	local.sin_port = htons(static_cast<uint16_t>(port));
    	local.sin_addr.s_addr = inet_addr(ip);
    
    	if (bind(sock, reinterpret_cast<SA*>(&local), sizeof(local)) < 0) {
    		perror("bind error");
    		return -1;
    	}
    
    	if (listen(sock, 10) < 0) {
    		perror("listen error");
    		return -1;
    	}
    
    	return sock;
    }
    

    但如果失败了,怎么办?
    我们不用关心是哪一步失败,而是直接跳过该端口,尝试监听下一个端口,直到成功为止。
    注意:知名端口号范围0 ~ 1023;应用程序能用的动态端口号范围1024 ~ 65535。如果非专用应用程序,通常只能用动态端口号,而且在Linux下使用知名端口号,需要root权限。

    /**
     * 打开数据连接,尝试在指定端口范围[start_port, end_port]中,找一个可用端口号并监听之
     */
    int ftp_create_datasocket(const char *ip, int start_port, int end_port)
    {
        int listenfd = -1;
        int i;
        if (start_port < 1024) start_port = 1024;
        if (end_port >= 65535) end_port = 65535;
        if (start_port > end_port) return -1;
    
        for (i = start_port; i <= end_port; ++i) {
            if ((listenfd = socket_create(ip, i)) != -1) /* success */
                break;
        }
        return listenfd;
    }
    

    端口已被同一进程占用

    到这里,我们最开始的问题解决了。但由此让人不得不思考另外几个问题:如果本地端口已经被同一个进程占用,到底是哪一步会失败?或者说,我们有一个sockfd,如果连续多次bind、listen,到底是哪一步会失败?
    下面用几组实验来验证。

    首先,是对通过一个socket fd调用bind 2次,看是否报错,以及报错信息。

    int test_bindTwoTimes(const char* ip, const int port)
    {
    	if (ip == NULL || port < 0) {
    		return -1;
    	}
    
    	/* create socket */
    	int sock = socket(AF_INET, SOCK_STREAM, 0);
    	if (sock < 0) {
    		perror("socket error");
    		return -1;
    	}
    
    	/* set sock option SO_REUSEADDR for re-bind when calling socket failed */
    	int op = 1;
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op));
    
    	struct sockaddr_in local;
    	local.sin_family = AF_INET;
    	local.sin_port = htons(static_cast<uint16_t>(port));
    	local.sin_addr.s_addr = inet_addr(ip);
    
    	if (bind(sock, reinterpret_cast<SA*>(&local), sizeof(local)) < 0) { /* first time */
    		perror("bind1 error");
    		return -1;
    	}
    
    	if (bind(sock, reinterpret_cast<SA*>(&local), sizeof(local)) < 0) { /* second time */
    		perror("bind2 error");
    		return -1;
    	}
    
    	if (listen(sock, 10) < 0) {
    		perror("bind error");
    		return -1;
    	}
    
    	return sock;
    }
    

    接着,对同一个socket fd调用listen 2次,看是否报错及报错信息。

    int test_listenTwoTimes(const char* ip, const int port)
    {
    	if (ip == NULL || port < 0) {
    		return -1;
    	}
    
    	/* create socket */
    	int sock = socket(AF_INET, SOCK_STREAM, 0);
    	if (sock < 0) {
    		perror("socket error");
    		return -1;
    	}
    
    	/* set sock option SO_REUSEADDR for re-bind when calling socket failed */
    	int op = 1;
    	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op));
    
    	struct sockaddr_in local;
    	local.sin_family = AF_INET;
    	local.sin_port = htons(static_cast<uint16_t>(port));
    	local.sin_addr.s_addr = inet_addr(ip);
    
    	if (bind(sock, reinterpret_cast<SA*>(&local), sizeof(local)) < 0) {
    		perror("bind error");
    		return -1;
    	}
    
    	if (listen(sock, 10) < 0) {
    		perror("listen1 error");
    		return -1;
    	}
    
    	if (listen(sock, 10) < 0) {
    		perror("listen2 error");
    		return -1;
    	}
    
    	return sock;
    }
    

    测试用例:
    分为参照组,实验组:连续bind 2次、连续listen 2次。
    为每个实验组设置2个测试用例,是为了观察不同端口号是否有影响。

    int main()
    {
    	printf("Test on different socket fd\n");
    	{// 参照组
    		printf("Test Case: background\n");
    		const char* ip = "0.0.0.0";
    		int port = 1025; /* 使用well-known端口号(0~1023)需要root权限*/
    		int sockfd = socket_create(ip, port);
    		if (sockfd >= 0) {
    			printf("Success to create sockfd %d on %s:%d\n", sockfd, ip, port);
    		}
    		else {
    			printf("Fail to create sockfd %d on %s:%d\n", sockfd, ip, port);
    		}
    
    		close(sockfd);
    	}
    
    	{// 连续bind 2次
    		printf("Test Case1: bind two times on same sockfd\n");
    		const char* ip = "0.0.0.0";
    		int port = 1025;
    		int sockfd = test_bindTwoTimes(ip, port);
    		if (sockfd >= 0) {
    			printf("Success to create sockfd %d on %s:%d\n", sockfd, ip, port);
    		}
    		else {
    			printf("Fail to create sockfd %d on %s:%d\n", sockfd, ip, port);
    		}
    		close(sockfd);
    	}
    
    	{// 连续bind 2次
    		printf("Test Case2: bind two times on same sockfd\n");
    		const char* ip = "0.0.0.0";
    		int port = 65536;
    		int sockfd = test_bindTwoTimes(ip, port);
    		if (sockfd >= 0) {
    			printf("Success to create sockfd %d on %s:%d\n", sockfd, ip, port);
    		}
    		else {
    			printf("Fail to create sockfd %d on %s:%d\n", sockfd, ip, port);
    		}
    		close(sockfd);
    	}
    
    	{// 连续listen 2次
    		printf("Test Case1: listen two times on same sockfd\n");
    		const char* ip = "0.0.0.0";
    		int port = 1025;
    		int sockfd = test_listenTwoTimes(ip, port);
    		if (sockfd >= 0) {
    			printf("Success to create sockfd %d on %s:%d\n", sockfd, ip, port);
    		}
    		else {
    			printf("Fail to create sockfd %d on %s:%d\n", sockfd, ip, port);
    		}
    		close(sockfd);
    	}
    
    	{// 连续listen 2次
    		printf("Test Case2: listen two times on same sockfd\n");
    		const char* ip = "0.0.0.0";
    		int port = 65536;
    		int sockfd = test_listenTwoTimes(ip, port);
    		if (sockfd >= 0) {
    			printf("Success to create sockfd %d on %s:%d\n", sockfd, ip, port);
    		}
    		else {
    			printf("Fail to create sockfd %d on %s:%d\n", sockfd, ip, port);
    		}
    		close(sockfd);
    	}
    	return 0;
    }
    

    实验结果:

    Test on different socket fd
    Test Case: background
    Success to create sockfd 3 on 0.0.0.0:1025
    Test Case1: bind two times on same sockfd
    bind2 error: Invalid argument
    Fail to create sockfd -1 on 0.0.0.0:1025
    Test Case2: bind two times on same sockfd
    bind2 error: Invalid argument
    Fail to create sockfd -1 on 0.0.0.0:65536
    Test Case1: listen two times on same sockfd
    Success to create sockfd 5 on 0.0.0.0:1025
    Test Case2: listen two times on same sockfd
    Success to create sockfd 5 on 0.0.0.0:65536
    

    从实验结果,我们可以看到,连续bind同一个端口号,失败的是第二次bind;而listen不受此影响,两次都成功。

    同一进程下,不同sockfd能多次监听同一地址+端口号吗?

    前面提到的是,同一进程下,同一个sockfd只能监听一次同一地址+端口号。那如果是不同sockfd呢,能多次监听同一地址+端口号吗?比如一种常见情形,是多线程环境下,不同业务线程同时收到建立数据连接请求时,需要监听本地ip+端口。
    我第一反应应该跟上面一样,都是不行。但实际情况是这样吗?还是利用前面的socket_create()根据指定IP+port,创建一个sockfd并监听port,做实验验证一下。

    // 生成2个不同sockfd,都绑定到同一个ip地址 + 端口port
    void bind_same_port_in_two_sockfd(const char* ip, int port)
    {
    	int ret;
    	fprintf(stdout, "case1:\n");
    	ret = socket_create(ip, port);
    	if (ret < 0) {
    		fprintf(stdout, "fail to create socket listen on %s:%d\n", ip, port);
    	}
    	else {
    		fprintf(stdout, "success to create socket listen on %s:%d\n", ip, port);
    	}
    
    	fprintf(stdout, "case2:\n");
    	ret = socket_create(ip, port);
    	if (ret < 0) {
    		fprintf(stdout, "fail to create socket listen on %s:%d\n", ip, port);
    	}
    	else {
    		fprintf(stdout, "success to create socket listen on %s:%d\n", ip, port);
    	}
    }
    
    /**
     * 同一个进程, 用2个fd bind同一个port, 观察会发生什么
     */
    int main()
    {
    	const char* ip = "192.168.1.100"; // 根据自己主机ip地址设置,也可以偷懒设为通配ip地址"0.0.0.0"
    	int port = 2000;
    	bind_same_port_in_two_sockfd(ip, port);
    	return 0;
    }
    

    运行结果:

    case1:
    success to create socket listen on 192.168.1.100:2000
    case2:
    success to create socket listen on 192.168.1.100:2000
    

    可以发现,两次创建的sockfd,均能绑定到同一个ip地址+port,这与前面的直觉并不相同。这也就是说,如果创建了不同sockfd,我们不能依赖于bind/listen返回值,来判断同一个进程下,是否能监听同一ip+port,进而决定该port是否可用。

    让内核分配随机端口号

    显然,前面的做法:同一进程下,创建不同sockfd,绑定同一ip+port,是有问题的。比如收到对端连接请求,内核是将连接请求交给哪个sockfd呢?

    如果想要分配不同的端口号,该怎么办?
    可用为bind指定端口号0,作为通配,让内核决定使用哪个随机端口号,这样内核能保证每次分配不同的端口。

    我们如何知道内核分配的哪个端口号,绑定到sockfd呢?可用调用getsockname(),根据sockfd来获取本地绑定的ip、port。

    void bind_same_port_in_two_sockfd(const char* ip, int port)
    {
    	int sockfd;
    	fprintf(stdout, "case1:\n");
    	sockfd = socket_create(ip, port);
    	if (sockfd < 0) {
    		fprintf(stdout, "fail to create socket listen on %s:%d\n", ip, port);
    	}
    	else {
    		char buf[INET_ADDRSTRLEN];
    		sockaddr_in localAddr = getLocalAddr(sockfd);
    
    		const char* str = inet_ntop(AF_INET, static_cast<void*>(&localAddr.sin_addr), buf, sizeof(buf));
    		int localPort = static_cast<int>(ntohs(localAddr.sin_port));
    		if (str == NULL) {
    			perror("inet_ntop error");
    		}
    		fprintf(stdout, "success to create socket listen on %s:%d\n", buf, localPort);
    	}
    
    	fprintf(stdout, "case2:\n");
    	sockfd = socket_create(ip, port);
    	if (sockfd < 0) {
    		fprintf(stdout, "fail to create socket listen on %s:%d\n", ip, port);
    	}
    	else {
    		char buf[INET_ADDRSTRLEN];
    		sockaddr_in localAddr = getLocalAddr(sockfd);
    
    		const char* str = inet_ntop(AF_INET, static_cast<void*>(&localAddr.sin_addr), buf, sizeof(buf));
    		int localPort = static_cast<int>(ntohs(localAddr.sin_port));
    		if (str == NULL) {
    			perror("inet_ntop error");
    		}
    		fprintf(stdout, "success to create socket listen on %s:%d\n", buf, localPort);
    	}
    }
    
    /**
     * 同一个进程,
     * 1) 用2个sockfd, bind同一个port, 观察会发生什么
     * 2) 用2个sockfd, 让内核决定绑定哪个端口号, 观察会发生什么
     */
    int main()
    {
    	const char* ip = "192.168.1.100";
    
    	{ // 第一组
    		int port = 2000; // 由应用程序知道端口号
    		bind_same_port_in_two_sockfd(ip, port);
    	}
    
    	{ // 第二组
    		int port = 0; // 通配端口, 让内核决定具体使用哪个端口号
    		bind_same_port_in_two_sockfd(ip, port);
    	}
    	return 0;
    }
    
    // 获取本地sockfd绑定的ip、port地址信息
    struct sockaddr_in getLocalAddr(int sockfd)
    {
    	struct sockaddr_in localaddr;
    	memset(&localaddr, 0, sizeof(localaddr));
    	socklen_t addrlen = static_cast<socklen_t>(sizeof(localaddr));
    	// get local ip addr info bound to sockfd
    	if (::getsockname(sockfd, reinterpret_cast<sockaddr*>(&localaddr), &addrlen) < 0)
    	{
    		perror("getsockname error");
    	}
    	return localaddr;
    }
    

    运行结果:

    case1:
    success to create socket listen on 192.168.1.100:2000
    case2:
    success to create socket listen on 192.168.1.100:2000
    case1:
    success to create socket listen on 192.168.1.100:2838
    case2:
    success to create socket listen on 192.168.1.100:2839
    

    可以看到,在第二组实验结果中,内核为不同sockfd分配了不同port。

  • 相关阅读:
    jQuery诞生记-原理与机制
    你所不知的 CSS ::before 和 ::after 伪元素用法
    http中get与post的区别
    Http请求方法
    TCP/IP详解学习笔记(4)-ICMP协议,ping和Traceroute
    TCP/IP详解学习笔记(3)-IP协议,ARP协议,RARP协议
    TCP/IP详解学习笔记(2)-数据链路层
    TCP/IP详解学习笔记(1)-基本概念
    全面解析Java的垃圾回收机制
    深入Java虚拟机:JVM中的Stack和Heap
  • 原文地址:https://www.cnblogs.com/fortunely/p/16367542.html
Copyright © 2020-2023  润新知