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、客户端和服务器处于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

断开客户端连接,抓包如下:

DDOS攻击:客户端只发送SYN信号,不给服务器回复ACK信号,三次握手只进行第一步,会导致服务器的syn半连接队列溢出
send()返回大于0不代表发送成功,只是代表将数据放到了内核协议栈的发送缓冲buffer中,只有当对方发送ack并且自己收到ack消息后,才表明发送真正的成功了。
客户端宕机的检测方式:发送心跳报
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开始。
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
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);
}
}
这段代码解决了多个连接的问题,但是没办法正常工作。由于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个线程。线程过多会导致内存一直涨,最终导致服务器因为内存不足崩溃重启。
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习