tcp特点+TCP的状态转换图+time wait详解
目录
一、tcp特点解释
1.1 面向连接
1.1.1 连接建立——三次握手
1.1.2 连接释放——四次挥手
1.2 可靠的
1.2.1 应答确认
1.2.2 超时重传
1.2.3 乱序重排
1.2.4 去重
1.2.5 滑动窗口进行流量控制
1.3 流失服务(字节流传输)
1.3.1 tcp粘包概念
1.3.2 产生 TCP 粘包的原因
发送端原因
接收端原因
1.3.3 解决 TCP 粘包的方法
定长协议
分隔符协议
消息头 + 消息体协议
二、服务器接收多个客户端
三、TCP(传输控制协议)的状态转换图
2.1 状态
2.2 转换
一、tcp特点解释
1.1 面向连接
1.1.1 连接建立——三次握手
在进行数据传输之前,TCP 需要通过 “三次握手” 来建立连接。具体过程为:客户端向服务器发送一个 SYN 包,请求建立连接;服务器收到 SYN 包后,向客户端发送一个 SYN + ACK 包,表示同意建立连接;客户端收到 SYN + ACK 包后,再向服务器发送一个 ACK 包,连接建立完成。这种方式确保了双方都有发送和接收数据的能力,并且双方对连接的初始序列号达成一致。
1.1.2 连接释放——四次挥手
数据传输结束后,TCP 使用 “四次挥手” 来释放连接。客户端发送一个 FIN 包,表示请求关闭连接;服务器收到 FIN 包后,发送一个 ACK 包表示同意关闭;接着服务器发送一个 FIN 包,表示自己也请求关闭连接;客户端收到 FIN 包后,发送一个 ACK 包表示同意关闭,连接释放完成。这种机制保证了双方都能正确地结束数据传输。
1.2 可靠的
1.2.1 应答确认
TCP 使用确认机制来确保数据的可靠传输。发送方发送数据后,会等待接收方的确认信息(ACK)。如果在一定时间内没有收到确认信息,发送方会重新发送该数据。例如,发送方发送了一个数据包,接收方收到后会返回一个带有确认号的 ACK 包,告知发送方已经正确接收了哪些数据。
1.2.2 超时重传
当发送方发送的数据丢失或者接收方返回的确认信息丢失时,发送方会在超时后重传数据。TCP 通过设置定时器来实现超时重传,定时器的时间会根据网络状况动态调整。
1.2.3 乱序重排
我们每发一个tcp报文都有相应的序号。TCP 保证字节流中的数据按照发送的顺序到达接收方。如果数据在传输过程中出现乱序,TCP 会在接收方进行重新排序,确保应用层接收到的数据是有序的
1.2.4 去重
俩个相同序号的报文去重
1.2.5 滑动窗口进行流量控制
滑动窗口用于控制数据的发送速率和流量,同时保证数据的可靠传输。发送方和接收方都有一个滑动窗口,窗口的大小表示可以发送或接收的数据量。滑动窗口越大代表我能发送的数据越大.发送方在发送数据时,会根据接收方的窗口大小来决定发送多少数据,避免接收方缓冲区溢出。窗口内的允许发送,窗口外的不允许发送
(不会丢包因为是可靠的,底层可能会丢但是会重传时间特别快,我们应用层感受不到)
1.3 流失服务(字节流传输)
多线程并发——给一个服务器同时链接俩个以上客户端
发送缓冲区:send数据写到这里
接收缓冲区:recv接收数据
TCP 将应用层的数据看作是无边界的字节流进行传输。发送方可以将多个应用层的消息合并成一个字节流发送,接收方需要自己从字节流中提取出各个消息。例如,应用层发送了两条消息 “Hello” 和 “World”,TCP 可能会将它们合并成一个字节流 “HelloWorld” 进行发送,接收方需要根据具体的协议来区分这两条消息。
1.3.1 tcp粘包概念
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。而 TCP 粘包是在使用 TCP 进行数据传输时可能遇到的一个常见问题。
TCP 粘包指的是在 TCP 连接中,发送方发送的若干个数据包,到接收方接收时,这些数据包粘连在一起,接收方难以区分哪些字节属于哪个原始数据包的现象。
1.3.2 产生 TCP 粘包的原因
发送端原因
- Nagle 算法:TCP 为了提高传输效率,采用了 Nagle 算法。该算法会将小的数据包合并成大的数据包进行发送,以此减少网络中的数据包数量。例如,当你连续发送多个小数据包时,Nagle 算法可能会将它们合并成一个大的数据包发送,从而造成粘包。
- TCP 缓冲区:TCP 协议的发送缓冲区用于暂存待发送的数据。如果发送方的数据产生速度大于网络的发送速度,那么数据就会在缓冲区中累积,当缓冲区满或者达到一定条件时,就会将缓冲区中的数据一起发送出去,这也可能导致多个数据包粘连在一起。
接收端原因
- TCP 接收缓冲区:接收端在接收数据时,会将数据先存放在接收缓冲区中。如果接收方没有及时从缓冲区中读取数据,后续的数据也会不断地存入缓冲区,这样就可能导致多个数据包的数据混合在一起,形成粘包。
1.3.3 解决 TCP 粘包的方法
定长协议
规定每个数据包的长度是固定的。接收方按照固定长度来读取数据,这样就可以明确区分每个数据包。
分隔符协议
在每个数据包的末尾添加一个特殊的分隔符,接收方根据分隔符来区分不同的数据包。
消息头 + 消息体协议
在每个数据包的前面添加一个消息头,消息头中包含消息体的长度信息。接收方先读取消息头,根据消息头中的长度信息来读取相应长度的消息体。
二、服务器接收多个客户端 用fork做并发
它是一个简单的TCP服务器程序,它在本地6000端口上监听连接请求,接受客户端的连接,并为每个连接创建一个新的进程来处理
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建监听套接字
if( -1 == sockfd )
{
exit(1);
}
//定义套接字地址结构, ipv4,ipv5,unix
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//将套接字绑定到指定的IP地址和端口。如果绑定失败(返回-1),则打印错误信息并退出程序。
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//指定ip port
if( -1 == res)
{
printf("bind err\n");
exit(1);
}
//使套接字进入监听状态,监听队列大小设置为5。如果监听失败(返回-1),则退出程序。
res = listen(sockfd,5);//设置监听队列 大小是5
if( -1 == res)
{
exit(1);
}
//进入一个无限循环,等待客户端的连接请求。accept函数用于接受连接请求,并将客户端的地址信息存储在caddr中。
while( 1 )
{
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
//检查是否成功接受连接。如果失败,则继续下一次循环。
if( c < 0 )
{
continue;
}
//创建一个新的进程来处理这个连接。如果创建进程失败,则关闭连接并继续下一次循环
pid_t pid = fork();
if( pid == -1 )
{
close(c);
continue;
}
//在子进程中,进入一个无限循环,接收客户端发送的数据,并发送"ok"作为响应。如果接收失败或客户端关闭连接,则打印信息,关闭连接,并退出子进程。
if( pid == 0 )
{
while( 1 )
{
char buff[128] = {0};
int n = recv(c,buff,127,0);
if( n<= 0 )
{
break;
}
printf("recv:%s\n",buff);
send(c,"ok",2,0);
}
printf("client close\n");
close(c);
exit(0);
}
close(c);
}
}
这个程序实现了一个简单的TCP服务器,它可以在本地6000端口上监听连接请求,接受客户端的连接,并为每个连接创建一个新的进程来处理。服务器接收客户端发送的数据,并发送"ok"作为响应。服务器使用多进程模型来处理多个客户端的连接。
TCP客户端程序cli,它连接到本地服务器(127.0.0.1),发送消息,并接收服务器的响应
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
//创建一个TCP套接字。AF_INET 表示使用IPv4地址,SOCK_STREAM 表示使用面向连接的流式套接字。
int sockfd = socket(AF_INET,SOCK_STREAM,0);
//检查套接字是否创建成功。如果失败(返回-1),则退出程序。
if( sockfd == -1 )
{
exit(1);
}
//定义并初始化服务器的地址结构。memset 用于清零结构体,sin_family 设置为IPv4,sin_port 设置为6000端口(使用htons函数转换为网络字节序),sin_addr.s_addr 设置为本地地址127.0.0.1(使用inet_addr函数转换为网络字节序)。
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//尝试连接到服务器。如果连接失败(返回-1),则打印错误信息并退出程序。
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if ( res == -1 )
{
printf("connect err\n");
exit(1);
}
//进入一个无限循环,提示用户输入,然后使用fgets函数从标准输入读取一行文本(最多127个字符),存储在缓冲区buff中。
while( 1 )
{
printf("input\n");
char buff[128] = {0};
fgets(buff,128,stdin);
//检查用户输入是否为"end"。如果是,则退出循环。
if( strncmp(buff,"end",3) == 0 )
{
break;
}
//发送用户输入的消息到服务器(不包括换行符),清空缓冲区,接收服务器的响应,并打印出来。
send(sockfd,buff,strlen(buff)-1,0);
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("read:%s\n",buff);
}
close(sockfd);
exit(0);
}
这个程序实现了一个简单的TCP客户端,可以连接到本地服务器,发送消息,并接收服务器的响应。它使用了基本的套接字编程技术,包括创建套接字、连接服务器、发送和接收数据以及关闭套接字。程序通过一个无限循环来持续接收用户输入,并在输入"end"时退出。
这两段代码分别实现了一个简单的 TCP 客户端和一个 TCP 服务器。
TCP 服务器代码解释
这段代码实现了一个 TCP 服务器,它在本地计算机的 6000 端口上监听来自客户端的连接请求。
1. **创建套接字**:
- 使用 `socket` 函数创建一个 TCP 套接字 `sockfd`。2. **绑定地址**:
- 使用 `bind` 函数将套接字绑定到本地地址 `127.0.0.1` 和端口 `6000` 上。这样,服务器就准备好在指定的地址和端口上监听客户端的连接请求。3. **监听连接**:
- 使用 `listen` 函数使套接字进入监听状态,等待客户端的连接请求。监听队列的大小设置为 5,这意味着服务器可以同时处理最多 5 个未处理的连接请求。4. **接受连接**:
- 使用 `accept` 函数接受客户端的连接请求。每当有新的客户端连接时,`accept` 函数会返回一个新的套接字 `c`,用于与该客户端进行通信。5. **处理客户端请求**:
- 使用 `fork` 创建一个新的子进程来处理每个客户端的请求。这样,服务器可以同时处理多个客户端的连接。
- 在子进程中,使用 `recv` 函数接收客户端发送的数据,并使用 `send` 函数发送响应(在这个例子中,响应是字符串 "ok")。
- 当客户端关闭连接或发生错误时,子进程会退出。6. **关闭连接**:
- 在父进程中,关闭与客户端的连接套接字 `c`。TCP 客户端代码解释
这段代码实现了一个 TCP 客户端,它连接到本地服务器(127.0.0.1:6000),发送消息,并接收服务器的响应。
1. **创建套接字**:
- 使用 `socket` 函数创建一个 TCP 套接字 `sockfd`。2. **连接到服务器**:
- 使用 `connect` 函数连接到服务器的地址 `127.0.0.1` 和端口 `6000` 上。3. **发送和接收数据**:
- 进入一个无限循环,提示用户输入消息。
- 使用 `fgets` 函数从标准输入读取用户输入的消息。
- 如果用户输入 "end",则退出循环,关闭连接并退出程序。
- 使用 `send` 函数将用户输入的消息发送到服务器。
- 使用 `recv` 函数接收服务器的响应,并使用 `printf` 函数打印响应。4. **关闭连接**:
- 在退出循环后,关闭与服务器的连接套接字 `sockfd`。### 总结
这两段代码实现了一个简单的 TCP 客户端-服务器通信模型:
- **服务器**:在本地计算机的 6000 端口上监听客户端的连接请求,使用多进程模型来处理每个客户端的请求。服务器接收客户端发送的数据,并发送 "ok" 作为响应。
- **客户端**:连接到本地服务器,发送用户输入的消息,并接收服务器的响应。这种模型可以用于实现各种基于 TCP 的网络应用程序,如聊天程序、文件传输等。
会出现僵死进程,所以需要加入信号的使用来解决僵死进程
三、TCP(传输控制协议)的状态转换图
展示了TCP连接从建立到关闭的整个过程中可能经历的各种状态。
2.1 状态
1. **CLOSED**:初始状态,表示连接尚未建立。
2. **LISTEN**:服务器在该状态监听来自客户端的连接请求。
3. **SYN_SENT**:客户端发送SYN请求连接,等待服务器确认。
4. **SYN_RCVD**:服务器收到SYN请求后,发送SYN+ACK响应,进入此状态,等待客户端的确认。
5. **ESTABLISHED**:双方确认连接后,连接建立,可以开始传输数据。
6. **FIN_WAIT_1**:主动关闭连接的一方发送FIN请求,希望关闭连接。
7. **FIN_WAIT_2**:在FIN_WAIT_1状态下收到对方的ACK后,进入此状态,等待对方的FIN请求。
8. **CLOSING**:双方同时发送FIN请求,等待对方的ACK。
9. **TIME_WAIT**:主动关闭连接的一方在发送FIN请求并收到对方的ACK后,进入此状态,等待一段时间以确保对方收到ACK。
10. **CLOSE_WAIT**:被动关闭连接的一方收到FIN请求后,进入此状态,等待应用程序关闭连接。
11. **LAST_ACK**:被动关闭连接的一方发送FIN请求后,等待对方的ACK。
2.2 转换
1. **被动打开**:服务器从CLOSED状态进入LISTEN状态,等待客户端的连接请求。
2. **主动打开**:客户端从CLOSED状态发送SYN请求,进入SYN_SENT状态。
3. **连接建立**:
- 客户端发送SYN请求,进入SYN_SENT状态。
- 服务器收到SYN请求,发送SYN+ACK响应,进入SYN_RCVD状态。
- 客户端收到SYN+ACK响应,发送ACK确认,进入ESTABLISHED状态。
- 服务器收到ACK确认,进入ESTABLISHED状态。
4. **数据传输**:在ESTABLISHED状态下,双方可以进行数据传输。
5. **连接关闭**:
- 主动关闭的一方发送FIN请求,进入FIN_WAIT_1状态。
- 被动关闭的一方收到FIN请求,发送ACK确认,进入CLOSE_WAIT状态。
- 被动关闭的一方发送FIN请求,进入LAST_ACK状态。
- 主动关闭的一方收到FIN请求,发送ACK确认,进入TIME_WAIT状态。
- TIME_WAIT状态持续一段时间后,连接关闭,进入CLOSED状态。
TCP连接的建立和关闭是一个复杂的过程,涉及到多个状态和状态转换。连接建立需要三次握手(SYN, SYN+ACK, ACK),而连接关闭需要四次挥手(FIN, ACK, FIN, ACK)。这种设计确保了连接的可靠性和数据的完整性。
在Linux系统中使用
netstat
命令查看网络连接状态的输出结果。netstat
是一个常用的网络工具,用于显示网络连接、路由表、接口统计等信息。图片中的命令netstat -nat
用于显示所有网络连接和监听端口,包括TCP和UDP协议,并且不解析服务名称
输出字段:
Proto
:协议类型,如TCP或TCP6。
Recv-Q
:接收队列长度。
Send-Q
:发送队列长度。
Local Address
:本地地址和端口。
Foreign Address
:远程地址和端口。
State
:连接状态,如LISTEN、ESTABLISHED、CLOSE_WAIT等。
PID/Program name
:进程ID和程序名称。状态解释:
LISTEN
:端口正在监听,等待连接请求。
ESTABLISHED
:连接已建立,数据可以传输。
CLOSE_WAIT
:被动关闭连接的一方等待关闭连接。
FIN_WAIT_1
:主动关闭连接的一方等待对方的FIN请求。
FIN_WAIT_2
:主动关闭连接的一方等待对方的ACK确认。
TIME_WAIT
:主动关闭连接的一方等待一段时间以确保对方收到ACK。
第一张图显示了多个端口处于LISTEN状态,表示这些端口正在等待连接请求。
第二张图显示了一个端口(127.0.0.1:6000)已经完成了三次握手,处于ESTABLISHED状态,表示连接已经建立。
第三张图显示了一个端口(127.0.0.1:6000)处于CLOSE_WAIT状态,表示被动关闭连接的一方等待关闭连接。
变成time wait端口被占用 状态服务器不能启动
为什么需要time wait状态?
1. 可靠的终止TCP连接
TCP 连接的终止需要通过四次挥手过程来完成。在主动关闭连接的一方发送了 FIN 报文后,它需要等待对方的 ACK 报文。然而,由于网络延迟或其他原因,这个 ACK 报文可能会丢失或延迟到达。如果主动关闭方在发送 FIN 报文后立即关闭,那么它将无法接收到这个 ACK 报文,从而导致连接没有被正确关闭。
为了避免这种情况,主动关闭方会进入
TIME_WAIT
状态,等待一段时间(通常是 2 倍的 MSL,即 Maximum Segment Lifetime,报文段的最大生存时间)。这段时间足够长,可以确保即使 ACK 报文丢失或延迟,主动关闭方也能够接收到来自对方的重传 ACK 报文,从而可靠地终止连接。2. 保证让迟来的TCP报文段有足够的时间被识别并丢弃
TCP 连接的标识是由源 IP 地址、目的 IP 地址、源端口号和目的端口号共同组成的。当一个 TCP 连接被关闭后,这些标识可能会被新的连接所使用。然而,由于网络延迟或其他原因,旧连接的报文段可能会在连接关闭后仍然存在于网络中。
如果新的连接使用了相同的标识,那么这些迟来的旧报文段可能会被错误地识别为新连接的报文段,从而导致数据混乱。
TIME_WAIT
状态通过等待一段时间,确保所有旧的报文段都已经被丢弃或过期,从而避免这种情况的发生。