• 网络编程一些问题总结


    1、TCP三次握手、四次挥手

    1.1、三次握手与全连接、半连接的关系:

    1、服务器调用listen,等待客户端的连接。
    2、客户端调用connect函数,发送syn包给服务器,客户端此时会变成syn_send状态。
    3、服务器收到syn信号后,将新建一个连接放到syn半连接队列中,并发送syn ack信号给客户端,此时服务端变成syn_recv状态。
    4、客户端收到服务端的syn ack信号,再次发送ack信号给服务端,并且客户端状态变成establish状态。
    5、服务端收到ack信号后状态变成establish,并将开始放入半连接队列的那个连接移出、放到accept全连接队列中。
    6、服务端调用accept()函数,从accept全连接队列中取连接,然后新建一个socket。
    在这里插入图片描述

    wireshark三次握手抓包测试方法:
    wireshark选择"loopback"环回网卡还是抓包。然后打开2个NetAssist,一个客户端、一个服务器,服务器端口配置8888
    在这里插入图片描述
    抓到的TCP三次握手包如下:
    在这里插入图片描述

    1.2、四次挥手

    1、客户端和服务器处于Establish正常通信状态
    2、客户端调用close函数,给服务器发送FIN数据包,此时客户端变为FIN-WAIT-1状态
    3、服务器接收到FIN信号,发送ACK信号给客户端,此时服务器改变状态为CLOSE_WAIT状态
    4、客户端收到ACK信号后,改变状态为FIN-WAIT-2
    5、服务器调用Close函数,给客户端发送FIN信号,然后服务器状态变为LAST-ACK
    6、客户端收到FIN信号,给服务器发送一个ACK包,客户端将状态变为TIME-WAIT
    7、服务器收到ACK信号后,将状态变为CLOSED
    8、客户端状态变为TIME-WAIT
    在这里插入图片描述
    断开客户端连接,抓包如下:
    在这里插入图片描述

    1.3、TCP扩展知识

    DDOS攻击:客户端只发送SYN信号,不给服务器回复ACK信号,三次握手只进行第一步,会导致服务器的syn半连接队列溢出

    send()返回大于0不代表发送成功,只是代表将数据放到了内核协议栈的发送缓冲buffer中,只有当对方发送ack并且自己收到ack消息后,才表明发送真正的成功了。

    客户端宕机的检测方式:发送心跳报

    2、网络编程常见问题

    2.1、服务器只执行到listen
        if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
     
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(9999);
     
        if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
            printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
     
        if (listen(listenfd, 10) == -1) {
            printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这段代码可以支持多个客户端的连接,可以完成三次握手,但是连接只会存放到全连接队列Accept队列中,客户端所有的读写操作都是失败的。只有listen执行而无accept,也是可以完成客户端的连接的。三次握手不由应用程序管理,而是应该在执行listen之后由底层的内核协议栈执行。

    listenfd为3的原因:因为stdin,stdout,stderr占据了0、1、2三个文件描述符,所以一般listenfd从3开始。

    2.2、服务器执listen和accpet在一起
        if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
     
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(9999);
     
        if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
            printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
     
        if (listen(listenfd, 10) == -1) {
            printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
    
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
            printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
    
        while (1) {
            n = recv(connfd, buff, MAXLNE, 0);
            if (n > 0) {
                buff[n] = '\0';
                printf("recv msg from client: %s\n", buff);
    
    	    	send(connfd, buff, n, 0);
            } else if (n == 0) {
                close(connfd);
            }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    这段代码可以支持多个客户端连接,但只有第一个客户端可以正常与服务器进行通信。注意,listen是非阻塞的,而accept是阻塞的。当第一个客户端连接进来后,accept返回的时第一个连接的fd,然后进入while(1)一直执行,期间不再会调用accept函数再从Accept全连接队列中取出新的连接生成新的客户端fd,所以读写操作都是执行的第一个连接的fd的操作。

    accept函数的作用:
    1、从accept全连接队列中取出一个连接,如果全连接队列为空那么会阻塞等待
    2、为新的连接分配一个socket fd

    2.3、accept和recv都放到while(1)中
    	if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    	    printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
    	    return 0;
    	}
    	
    	memset(&servaddr, 0, sizeof(servaddr));
    	servaddr.sin_family = AF_INET;
    	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    	servaddr.sin_port = htons(9999);
    	
    	if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
    	    printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
    	    return 0;
    	}
    	
    	if (listen(listenfd, 10) == -1) {
    	    printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
    	    return 0;
    	}
    	
    	while (1) {
    	    struct sockaddr_in client;
    	    socklen_t len = sizeof(client);
    	    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
    	        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
    	        return 0;
    	    }
    	
    	    n = recv(connfd, buff, MAXLNE, 0);
    	    if (n > 0) {
    	        buff[n] = '\0';
    	        printf("recv msg from client: %s\n", buff);
    	 	send(connfd, buff, n, 0);
    	    } else if (n == 0) {
    	        close(connfd);
    	    }
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    2.4、每个连接创建一个线程

    这段代码解决了多个连接的问题,但是没办法正常工作。由于accept、recv和send都是阻塞的,程序开始运行到accept等待客户端连接。当一个新的客户端连接后,服务器从Accept全连接队列取出连接,然后新建一个新的客户端fd。 然后程序执行到recv,等待客户端发送消息。服务器收到的消息后将消息回发给客户端,完成一次连接的任务。然后程序再次运行到accept,如果Accept全连接队列不为空,那么会再次从队列中取出一个连接,创建一个新的fd,然后再次执行到recv等待客户端发送消息,以此往复。综上所述,每次只会有一个客户端连接上,并且接收和发送一次消息。

    void *client_routine(void *arg) { //
    
    	int connfd = *(int *)arg;
    
    	char buff[MAXLNE];
    
    	while (1) {
    
    		int n = recv(connfd, buff, MAXLNE, 0);
            if (n > 0) {
                buff[n] = '\0';
                printf("recv msg from client: %s\n", buff);
    
    	    	send(connfd, buff, n, 0);
            } else if (n == 0) {
                close(connfd);
    			break;
            }
    
    	}
    
    	return NULL;
    }
    
    
    int main(int argc, char **argv) 
    {
        int listenfd, connfd, n;
        struct sockaddr_in servaddr;
        char buff[MAXLNE];
     
        if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
     
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(9999);
     
        if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
            printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
     
        if (listen(listenfd, 10) == -1) {
            printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
            return 0;
        }
    
    
        while (1) {
    
    	    struct sockaddr_in client;
    	    socklen_t len = sizeof(client);
    	    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
    	        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
    	        return 0;
    	    }
    
    		pthread_t threadid;
    		pthread_create(&threadid, NULL, client_routine, (void*)&connfd);
    
        }
        
        close(listenfd);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    这段代码算不上有问题,在客户端连接数不多的情况下完全是正常的,适用于CPU密集型而非IO密集型的应用场景,比如绘图、运算等场景,但是不适合互联网大量客户端连接的情况。

    按照Posix线程分配8M的空间来计算,1G的内存大概只能分配1024M / 8M =128个线程。如果4G的内存最多只能分配512个线程。线程过多会导致内存一直涨,最终导致服务器因为内存不足崩溃重启。

    推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

  • 相关阅读:
    fifa将采用半自动越位技术计算进球
    《向量数据库指南》——向量数据库 有必要走向专业化吗?
    CSS垂直居中的方法
    JVM内存模型及分区
    Android手机连接电脑弹出资源管理器
    MFC 常用控件
    Spacedrive:开源跨平台文件管理 | 开源日报 No.57
    Debian安装Redis、RabbitMQ、Nacos
    K8S集群进行分布式负载测试
    Java获取时间戳、字符串和Date对象的相互转换、日期时间格式化、获取年月日
  • 原文地址:https://blog.csdn.net/qq_23350817/article/details/126442535