tcp特点+TCP的状态转换图+time wait详解
目录
一、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
状态通过等待一段时间,确保所有旧的报文段都已经被丢弃或过期,从而避免这种情况的发生。