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; }
这段代码可以支持多个客户端的连接,可以完成三次握手,但是连接只会存放到全连接队列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); } }
这段代码可以支持多个客户端连接,但只有第一个客户端可以正常与服务器进行通信。注意,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); } }
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; }
这段代码算不上有问题,在客户端连接数不多的情况下完全是正常的,适用于CPU密集型而非IO密集型的应用场景,比如绘图、运算等场景,但是不适合互联网大量客户端连接的情况。
按照Posix线程分配8M的空间来计算,1G的内存大概只能分配1024M / 8M =128个线程。如果4G的内存最多只能分配512个线程。线程过多会导致内存一直涨,最终导致服务器因为内存不足崩溃重启。