Linux网络编程二(TCP图解三次握手及四次挥手、TCP滑动窗口、MSS、TCP状态转换、多进程/多线程服务器实现)

avatar
作者
筋斗云
阅读量:1

文章目录

1、TCP三次握手

TCP三次握手(TCP three-way handshake)是TCP协议建立可靠连接的过程,确保客户端和服务器之间可以进行可靠的通信。下面是TCP三次握手的详细过程
假设客户端为A,服务器为B。

SYN---> ACK + SYN --->ACK 
(1) 第一次握手

第一次握手(SYN=1,seq=500)
A向B发送一个带有SYN标志位的数据包,表示A请求建立连接。SYN标志位为1表示这是一个连接请求数据包,500是A随机选择的初始序列号。

(2) 第二次握手

第二次握手(SYN=1,ACK=1,ack=500+1,seq=800):
B接收到A发送的连接请求后,会向A回复一个数据包。该数据包中,SYN和ACK标志位都被设置为1。ACK=1表示B确认收到了A的连接请求,ack字段的值为A的初始序列号加1,表明B期望下一个收到的序列号是A初始序列号加1。seq字段800是B随机选择的初始序列号。

(3) 第三次握手

第三次握手(ACK=1,ack=800+1):
A收到B的回复后,检查ACK标志位是否为1,以及ack字段的值是否为B的初始序列号加1。如果正确,A会向B发送一个确认数据包。在该数据包中,ACK标志位被设置为1,表示A确认收到了B的回复。ack字段的值是B的初始序列号加1,表明A期望下一个收到的序列号是B初始序列号加1。
完成这三次握手后,TCP连接就建立成功,A和B之间可以开始传输数据。连接的状态变为已建立(ESTABLISHED)。
三次握手是操作系统内核(Kernel)的TCP协议栈负责处理。用户层的表现:服务器端是accept(),客户端是connect(),其这两个函数成功执行并返回了。
自创

2、TCP四次挥手

TCP四次挥手(TCP four-way handshake)是TCP连接的关闭过程,用于在客户端和服务器之间终止一个已建立的连接。与TCP三次握手不同,四次挥手需要进行四个步骤来关闭连接,以确保数据传输的完整性和可靠性。

FIN--->ACK   FIN--->ACk 
(1) 一次挥手

客户端向服务器发送连接释放请求(FIN)的数据包。
客户端希望关闭连接,因此发送一个带有FIN标志位的数据包,FIN=1表示连接释放请求。设置序列号为seq=501。

(2) 二次挥手

服务器接收到客户端的连接释放请求后,回复确认连接释放(ACK)的数据包。
服务器收到客户端的FIN后,发送一个带有ACK标志位的数据包,ACK=1,ack 502表示确认收到客户端的连接释放请求。

(3) 三次挥手

服务器向客户端发送连接释放请求(FIN)的数据包。
服务器希望关闭连接,因此发送一个带有FIN标志位的数据包,FIN=1表示连接释放请求。设置序列号为seq=701。

(4) 四次挥手

客户端接收到服务器的连接释放请求后,回复确认连接释放(ACK)的数据包。
客户端收到服务器的FIN后,发送一个带有ACK标志位(这个不是数据,是控制报文),ACK=1,ack 702表示确认收到服务器的连接释放请求。
在发送完ACK后,客户端等待一段时间,确保服务器收到了ACK,然后完全关闭连接。

4、经典问题补充

为什么是三次握手,两次握手或者四次握手不可以吗?

答:
1、如果两次握手,客户端发送建立连接请求,由于网络阻塞,服务器端未回应。客户端再次发送请求,服务端回应建立连接。一段时间后,第一次发送的到达服务器端,服务器以为客户端重新请求建立连接(其实并没有),此时服务端会返回响应报文并一直处于待连接状态,这就造成了资源浪费。
2、TCP建立的连接时二次握手过程中,客户端发送一个SYN请求帧,Server收到后发送了确认应答SYN+ACK帧。按照两次握手的协定,Server认为连接已经成功地建立了,可以开始发送数据帧。这个过程中,如果确认应答SYN+ACK帧在传输中被丢失,Client没有收到,Client将不知道Server是否已准备好,也不知道Server的SN序列号,Client认为连接还未建立成功,将忽略Server发来的任何数据分组,会一直等待Server的SYN+ACK确认应答帧。而Server在发出的数据帧后,一直没有收到对应的ACK确认后就会产生超时,重复发送同样的数据帧。这样就形成了死锁

为什么是四次挥手,三次挥手不可以吗?

答:不可以。当客户端发送断开连接请求后,停止发送数据(客户端还能接收数据),有可能此时服务端还有数据需要发给客户端,所以它先回一个确认报文ACK,等发送完所有数据,再发送断开连接的报文FIN,通知客户端可以断开连接了。

四次挥手结束后,为什么客户端没有立刻关闭呢?

客户端没有立刻关闭,而是进入TIME_WAIT状态,等待2个MSL的时长后,客户端才进入CLOSE状态,这是为了确保第四次挥手的确认消息到达服务端。
如果服务端在规定时间内未收到最后的确认消息,会重新进行第三次挥手请求断开连接,客户端重新发送确认消息。

3、TCP滑动窗口

TCP滑动窗口是TCP协议中的一个重要概念,用于实现流量控制可靠性传输。滑动窗口机制允许发送方和接收方在数据传输过程中动态调整可发送和可接收的数据量,从而适应不同的网络条件和接收方的处理能力。每次通信时,接收方利用win(4096)告知发送方缓冲区剩余大小。
MSS(Maximum Segment Size)是指TCP数据包中的最大有效载荷大小,它表示在TCP协议中一次性发送的最大数据量(即数据包中的有效数据部分,不包括TCP头部和IP头部)。
在TCP连接建立时,通过TCP三次握手的过程中,双方会交换彼此的MSS值,然后根据两端通信的网络链路的MTU大小进行协商,确定实际使用的MSS。

MSS = 1500 - 20 (TCP头部) - 20 (IP头部) = 1460 字节  

这意味着在该TCP连接中,一次可以发送的最大有效数据量为1460字节,超过这个大小的数据将被拆分成多个TCP数据包进行传输。
MSS的设置对于TCP性能和网络吞吐量很重要。合理设置MSS可以避免网络分段和数据重组,提高数据传输效率,特别是在一些高延迟、低带宽的网络环境中。

4、TCP状态时序图

使用命令查看状态

netstat -aptn | grep 端口号  #查看tcp端口 
netstat -apn | grep 端口号  #查看tcp、udp端口 

1、主动发起连接请求端
CLOSE----发送 SYN—SYN_SEND—接收 ACK、SYN—发送 ACK-ESTABLISHED(数据通信态)

2、主动关闭连接请求端
ESTABLISHED(数据通信态)—发送 FIN—FIN_WAIT_1 --接收 ACK --FIN_WAIT_2(半关闭)—接收对端发送 FIN—FIN_WAIT_2(半关闭)—回发ACK–TIME_WAIT(只有主动关闭连接方,会经历该状态)—等2MSL时长—CLOSE

3、被动接收连接请求端
CLOSE—LISTEN—接收 SYN—LISTEN—发送 ACK、SYN—SYN_RCVD—接收ACK—ESTABLISHED(数据通信态)

4、被动关闭连接请求端
ESTABLISHED(数据通信态)—接收 FIN —ESTABLISHED(数据通信态)— 发送ACK — CLOSE_WAIT(说明对端【主动关闭连接端】处于FIN_WAIT_2(半关闭)状态—发送FIN —LAST_ACK—接收ACK—CLOSE
重点:ESTABLISHED(数据通信态)、FIN_WAIT_2、CLOSE_WAIT、TIME_WAIT(2MSL).
在这里插入图片描述
先启动服务器,只有LISTEN状态。
在这里插入图片描述
启动客户端,此时三次握手建立完成,进入ESTABLISHED(数据通信态)。
在这里插入图片描述
尝试关闭一个客户端,此时该客户端进入TIME_WAIT状态。
在这里插入图片描述
服务器先主动关闭,服务器进入FIN_WAIT_2(半关闭)状态。客户端进入CLOSE_WAIT状态。
在这里插入图片描述
此时迅速关闭客户端,客户端处于TIME_WAIT状态。
在这里插入图片描述
提示:#include "wrap.h"错误处理函数,已经封装
错误处理函数

5、多进程并发服务器

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <ctype.h> #include <pthread.h> #include <signal.h> #include <sys/wait.h> #include "wrap.h"				//错误处理函数,已经封装 								//https://blog.csdn.net/qq_45009309/article/details/131813756?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171204506416800184170823%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=171204506416800184170823&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-131813756-null-null.nonecase&utm_term=%E9%94%99%E8%AF%AF&spm=1018.2226.3001.4450 #define SRV_PORT 9999  void catch_child(int signum)		//回调函数 内核操作 产生信号后进来 { 	while(waitpid(0, NULL, WNOHANG) > 0);	 //非阻塞回收子进程 											//循环回收是因为可能产生多个子进程死亡 	return ;				 }  int main(int argc, char* argv[]) { 	int lfd, cfd;								 	pid_t pid; 	int ret; 	char buf[BUFSIZ]; 	struct sockaddr_in srv_addr, clt_addr; 	socklen_t clt_addr_len;  	//memset(&srv_addr, 0, sizeof(srv_addr));				//地址结构清零 	bzero(&srv_addr, sizeof(srv_addr));  	srv_addr.sin_family = AF_INET;							 	srv_addr.sin_port = htons(SRV_PORT); 	srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  	lfd = Socket(AF_INET, SOCK_STREAM, 0);		//创建套接字 返回用于监听的文件描述符  	Bind(lfd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));	//绑定服务器IP+地址  	Listen(lfd, 128);							//设置监听上线 	clt_addr_len = sizeof(clt_addr);  	while (1) { 		 		cfd = Accept(lfd, (struct sockaddr*)&clt_addr, &clt_addr_len);	//返回用于双方通信的文件描述符  		pid = fork();							//创建子进程 		if (pid < 0) { 			perr_exit("fork error"); 		} 		else if (pid == 0) { 			close(lfd); 			break; 		} 		else {									//父进程使用信号捕捉回收子进程 			struct sigaction act; 			act.sa_handler = catch_child; 			sigemptyset(&act.sa_mask); 			act.sa_flags = 0;  			ret = sigaction(SIGCHLD, &act, NULL); 			if (ret != 0) { 				perr_exit("sigaction error"); 			} 			close(cfd); 			continue; 		} 	}  	if (pid == 0) {								//子进程实现读写功能 		for (;;) { 			ret = Read(cfd, buf, sizeof(buf)); 			for (int i = 0; i < ret; i++) { 				buf[i] = toupper(buf[i]);  			} 			write(cfd, buf, ret); 			write(STDOUT_FILENO, buf, ret);		//实现大小写转换功能  			if (ret == 0) { 				close(cfd); 				exit(1); 			} 		} 	} 	return 0; } 

在这里插入图片描述

6、多线程并发服务器

//头文件同上  #define MAXLINE 8192 #define SERV_PORT 8000  struct s_info {							//定义一个结构体,将地址结构跟cfd捆绑 	struct sockaddr_in cliaddr;		 	int connfd; };  void* do_work(void* arg)				//子线程主调函数 { 	int n, i; 	struct s_info* ts = (struct s_info*)arg; 	char buf[MAXLINE]; 	char str[INET_ADDRSTRLEN];			//16  	while (1) { 		n = Read(ts->connfd, buf, MAXLINE); 		if (n == 0) { 			printf("the client %d closed...\n", ts->connfd); 			break; 		} 		printf("received from %s at PORT %d\n",			//打印客户端IP+端口 			inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)), 			ntohs((*ts).cliaddr.sin_port));  		for (i = 0; i < n; i++) 			buf[i] = toupper(buf[i]);  		Write(STDOUT_FILENO, buf, n); 		Write(ts->connfd, buf, n);					//回写到客户端 	} 	Close(ts->connfd); 	return (void*)0; }  int main(int argc, char* argv[]) { 	struct sockaddr_in servaddr, cliaddr; 	socklen_t cliaddr_len; 	int listenfd, connfd; 	pthread_t tid;  	struct s_info ts[256];		//创建结构体数组 	int i = 0;  	listenfd = Socket(AF_INET, SOCK_STREAM, 0);			//创建一个socket,得到lfd 	bzero(&servaddr, sizeof(servaddr)); 	servaddr.sin_family = AF_INET; 	servaddr.sin_port = htons(SERV_PORT);				//指定端口号 	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);		//指定本地任意IP  	Bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));	//绑定 	Listen(listenfd, 128);  	printf("Accepting client connect...\n");  	while (1) { 		cliaddr_len = sizeof(cliaddr); 		connfd = Accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len);		//阻塞监听客户端链接请求 		ts[i].cliaddr = cliaddr; 		ts[i].connfd = connfd;  		pthread_create(&tid, NULL, do_work, (void*)&ts[i]); 		pthread_detach(tid);							//子线程分离,防止僵尸线程产生 		i++; 	} 	return 0; } 

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!