目录
再谈端口号
端口号范围划分
netstat
pidof
UDP协议
UDP协议端格式
UDP的特点
面向数据报
UDP的缓冲区
UDP使用注意事项
基于UDP的应用层协议
TCP协议
TCP的特点及其目的
TCP协议段格式
可靠性问题
确认应答机制(ACK)
超时重传机制
重发超时如何确定
流量控制
连接管理——3次握手 4次挥手
tcp三次握手
tcp四次挥手
验证CLOSE_WAIT状态
listen的第二个参数
验证TIME_WAIT状态
解决TIME_WAIT状态引起的bind失败的方法
滑动窗口
拥塞控制
延迟应答
捎带应答
面向字节流
粘包问题
UDP是否存在粘包问题?
TCP异常情况
用UDP实现可靠传输
TCP小结
基于TCP应用层的协议
传输层负责数据能够从发送端传输到接收端。TCP/IP中有两个具有代表性的传输层协议,分别是UDP和TCP。TCP提供可靠的通信传输,而UDP则常被用于让广播和细节控制交给应用的通信传输。总之,根据通信的具体特征,选择合适的传输层协议是非常重要的。
端口号(Port)标识了一个主机上进行通信的不同的应用程序,一台计算机上同时可以运行多个程序。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确的将数据传输。
在TCP/IP协议中,"源IP","源端口号","目的IP","目的端口号", "协议号" 这样的五元组来表示一个通信(可以通过netstat -n查看)
- 0-1023:知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的
- 1024-65535:操作系统动态分配的端口号,客户端程序的端口号,就是由操作系统从这个范围分配的
有些服务器是非常常用的,为了使用方便,人们约定一些常用的服务器,都是用以下这些固定的端口号
- ssh服务器,使用22端口号
- ftp服务器,使用21端口号
- telnet服务器,使用23端口号
- http服务器,使用80端口号
- https服务器,使用443
cat /etc/services可以查看知名端口号,我们自己写一个程序使用端口号时要避开这些知名端口号。
一个进程可以bind多个端口号但是一个端口号只能被一个进程bind
netstat是一个用来查看网络状态的重要工具
常用选项:
- n拒绝显示别名,能显示数字的全部转化成数字
- l 仅列出有Listen(监听)的服务状态
- p 显示建立相关链接的程序名
- t 仅显示tcp相关选项
- u 仅显示udp相关选项
- a 显示所有选项,默认不显示Listen相关
再查看服务器的进程id时非常方便,通过进程名,查看进程id
UDP ( User Datagram Protocol )不提供复杂的控制机制,利用IP提供面向无连接的通信服务。并且它是将应用程序发来数据在收到的那一刻,立即按照原样发送到网络上的一种机制。
前8个字段称为UDP报头,数据叫做有效载荷,如果应用层发送Hello,那么Hello就在数据中。
我们可以看到UDP采用定长报头。那么UDP在封装时,直接在有效载荷前加上UDP报头即可,而在分用的时候,应用层直接提取前8个字节就可以拿到UDP的报头字段。
网络协议栈的tcp/ip协议是内核中实现的,内核使用C语言实现的
struct udp_hdr { unsigned int src_port : 16; unsigned int dst_port : 16; unsigned int udp_len : 16; unsigned int udp_check : 16; };
这就是udp报头字段,在C语言中我们称作位段。位段在申请空间的时候会以前面的类型的申请的。因此报文的宽度是0-31 ,因此udp的报头就是8字节。
UDP传输的过程类似于寄信
- 无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接
- 不可靠:没有确认机制,没有重传机制,如果我因为网络故障该端无法发送到对方,UDP协议层也不会给应用层返回任何错误信息
- 面向数据报:不能够灵活的控制读写数据的次数和数量
应用层交给UDP多长的报文,UDP原样发送,既不会拆分也不会合并
用UDP传输100个字节的数据:如果发送端调用一次sendto,发送100个字节,那么接收端也必须调用一次对应的recvfrom,接受100个字节,而不能循环调用10次recvfrom,每次接受10个字节。
UDP的socket既能读也能写这个概念叫做全双工
我们注意到UDP协议首部中有一个16位的最大长度,也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部),然而64K在当今的互联网环境下,是一个非常小的数字,如果我们需要传输的数据超过64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼接。
为了通过IP数据报实现可靠性传输,需要考虑很多事情。TCP通过检验和,序列号,确认应答,重发控制,连接管理以及窗口控制等等机制实现可靠性传输。
- 源/目的端口号:表示数据是从哪个进程来,到哪个进程去
- 32位序号/32位确认序号:
- 序列号(32位):是指发送数据的位置。每发送一次数据,就累加一次该数据字节数的大小。序列号不会从0或1开始,而是建立连接时由计算机生成的随机数作为其初始值,通过SYN包传给接收端主机。
- 确认序号(32位):确认应答序号长32位,是指下一次应该收到的数据的序列号。实际上,它是指已收到确认应答号前一位位置的数据。发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接受
- 4位首部长度(数据偏移):该字段长4位,单位字节,实际的大小是15*4 = 60字节,该字段表示TCP所传输的数据部分应该从TCP包的哪个位开始计算,当然也可以把它看做TCP首部长度。因此不包括选项字段的话,TCP的首部长度为20字节长,因此4位首部长度最小设置为5.反之,如果该字段的值为5,那说明从TCP包的最一开始到20字节为止都是TCP首部,余下的部分为TCP数据。
- 6位保留:该字段主要是为了以后扩展时使用。
TCP的可靠性有一部分是体现在报头字段的。
1.什么是不可靠?
丢包,乱序,数据包校验失败.....
2.怎么确认一个报文是丢了还是没丢呢?
我们如果收到了应答,我们确认是没丢;否则就是不确定!
比如:你给你朋友发送一条消息:吃了吗?你能确认他收到了吗?其实是不能的。但是如果他给你回复一条:吃了,吃的饺子! 通过他的回复我们可以确认我们刚刚发送出去的消息,对方收到了。因此我们只要得到应答就意味着我们刚刚发送的消息对方100%收到了。
而在长距离交互的时候,永远有一条最新的数据是没有应答的,但是我们只要发送的消息有对应的应答,我们就认为我们发送的消息,对方是收到的 ! 而这个思想就是TCP可靠性的根本思想。这个机制就是确认应答机制
TCP通过肯定的确认应答(ACK)实现可靠的数据传输。当发送端将数据发出之后会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端,反之,则数据可能丢失了。
报文中有个序列号字段,序列号是按顺序发送数据的每一个字节都标上好嘛的编号,接收端查询接受数据TCP首部中的序列号和数据的长度,将自己下一步应该接受的序号作为确认应答返回回去,就这样,通过序列号和确认应答好,TCP实习可靠传输
发送的数据
序列号与确认应答号
当主机A发送数据(1-1000字节),如果主机B收到了这1000个字节,则确认序号为1001,发给主机A,当主机A收到发现确认应答是1001时,主机A就知道前1000字节主机B已经成功收到,就可以继续发送下面的数据了。每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.
那么为什么需要两组序号?为什么既有序号又有确认序号?
因为TCP是全双工的,我在给你发消息的同时,我也可以收消息。
举例:客户端给服务端发送消息,序号10;服务器给客户端回消息是也想发送自己的消息,那么也要有自己的序号!因此如果服务器想给你应答的同时也想给客户端发送消息,要应答就要填充确认序号,要发消息就要保证可靠性,因此服务端也需要携带自己的序号!因此需要同时设置,就需要一个序号一个确认序号!
但是主机A未收到B发送来的确认应答,也可能是因为ACK丢失了
由主机B返回的确认应答,因网络拥堵等原因在传输的途中丢失,没有到达主机A,主机A会等待一段时间,若在特定的时间间隔内始终未能收到这个确认应答,主机A会对数据进行重新发送,此时,主机B将第二次发送已接收此数据的确认应答。由于主机B已经收到过1~1000的数据,当再有相同数据送达时它会放弃。而主机B是如何确认这段数据是重复的呢?是通过序号,如果这段数据的TCP报头中的序号我已经收到过,那么就可以确定这段数据重复了。因此序号还有一个作用是去重报文。
重发超时是指在重发数据之前,等待确认应答到来的那个特定时间间隔。如果超过了这个时间仍未收到确认应答,发送端将进行数据重发。那么如果超时,时间如何确定呢?
- 最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".
- 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
- 如果超时时间设的太长, 会影响整体的重传效率;
- 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间。
在Linux中,超时都以0.5秒为单位进行控制,因此重发超时都是0.5秒的整数倍'。不过,由于最初的数据包还不知道往返时间,所以其重发超时一般设置为6秒左右。 在BSD的Unix以及Windows系统中,超时都以0.5秒为单位进行控制,因此重发超时都是0.5秒的整数倍‘。不过,由于最初的数据包还不知道往返时间,所以其重发超时一般设置为6秒左右.
数据被重发之后若还是收不到确认应答,则进行再次发送。此时,等待确认应答的时间将会以2倍、4倍的指数函数延长。
此外,数据也不会被无限、反复地重发。达到一定重发次数之后,如果仍没有任何确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接。并且通知应用通信异常强行终止。
数据什么时候发,发多少,出错了怎么办,要不要添加提高效率的策略 -- 都是由OS内TCP自主决定的。因此TCP协议叫做传输控制协议
如果客户端发送的太快了,导致Server来不及接受怎么办?由于服务端接受缓冲区满了,所以多余的报文只能丢弃。但是发送报文都会消耗网络资源,因此我们在发送报文的时候都要根据对端的接受能力发送对应大小的报文。因此就需要让Client知道Server的接受能力。因此Server的接受能力就是由接受缓冲区剩余空间的大小,那么如何让Client知道,因此当server应答时,在TCP报头中有一个保存Server接受能力的属性字段:正是16位窗口大小
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
16 位数字最大表示 65535, 那么 TCP 窗口最大就是 65535 字节么 ?实际上 , TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是 窗口字段的值左移 M 位 ;
如果接收端接收缓冲区打满,发送端就不会发送消息,但是会不定期发送窗口探测报文来得知接收端目的主机的实时的接受能力。
TCP提供面向连接的通信传输。面向有连接是指在数据通信开始之前做好通信两端的准备工作。
一个连接的建立与断开,正常过程至少需要来回发送7个包才能完成。只有完成了3次握手,才算建立连接成功,才能正式通信!通信完毕之后,需要四次挥手才能断开连接!
站在Server的角度,收到的报文有的是用来建立连接的,有的是断开连接的,有的是用来传输数据的,因此报文也是有类别的!因此作为Server来讲,必须要区别报文的类别!
- SYN :只要报文是建立连接的请求,SYN标志位需要设定为1
- FIN:该报文是一个申请断开连接请求的标志位,因此SYN和FIN不会同时置位1.
- ACK:表示该报文是对历史报文的确认。TCP规定除了最初建立的连接时的SYN包之外该位必须设置为1.
PSH:该为1时,表示需要将受到的数据立刻传给上层的应用层协议。PSH为0时,则不需要立即传而是先进行缓存。
URG:表示包中有需要紧急处理的数据。报文在发送的时候是可能乱序到达的,乱序到达是不可靠的一种,因此需要让报文按序到达就需要序号。所以如果数据是必须在TCP中进行按序到达的话,也就说如果有一些数据优先级更高,但是序号较晚,无法做到数据被紧急处理吗,这样的报文如果想被优先处理,那么把URG标志位设置为1。与之配合的是16位紧急指针,是标定的紧急数据在数据中的标记位的。因此一个报文中的数据并不都是紧急数据,而是16位紧急指针指向的一个字节的数据是紧急数据。
一般在暂时中断通信,或中断通信的情况下使用。例如在Web浏览器中点击停止按钮,或者使用telent输出Ctrl+C时都会有URG为1的包。此外,紧急指针也用作表示数据流分段的标志
RST:重新连接标志位,下面会解释。
为什么要三次握手?
因为tcp是面向连接的,因此在通信之前就必须先建立连接。
三次握手中客户端(主动断开连接的一方)状态转换:
三次握手中服务器端状态转换:
为什么是3次?不是1次?2次?4次?
一次握手 -- 不行:
答案是不行,因为只有一次握手,也就是客户端只给服务器发送一个SYN,那么客户端如果循环给服务端发送SYN,那么服务端要维护此链接,就要消费很多有效资源,那么只需要一套机器,就可以让服务器浪费许多资源--SYN洪水。因此一次连接是肯定不行的。
二次握手 -- 不行:
两次握手时客户端给服务端发送SYN,服务器端回复ACK。其实两次握手和一次握手是一样的效果。当客户端给服务器继续发送SYN洪水,当服务器端接收到请求之后回复一个ACK,也会维护此链接,而服务端给客户端回复的ACK客户端直接丢弃,那么效果就和一次握手很类似了。
三次握手 -- 可以:
一次和两次之所以不行,是因为都是让服务器端先认为链接已经建立好了,而三次握手可以把最后一次确认的机会交给服务器端。也就是说,客户端如果给服务器发送SYN洪水,服务器端要维护这些链接,消耗资源,而回复ACK的时候,客户端也要建立维护链接,再返回ACK给服务器端,因此多一次握手,客户端也要和服务器端一样消耗资源维护链接,服务器端也会把客户端拉下水。因此三次握手即使失败或者非法,可以把最后一次报文丢失的成本嫁接给客户端。
6.RST:如果最后一次ACK丢失,客户端认为链接建立成功,服务器端等待对端回复ACK,服务器端过段时间会超时重传SYN+ACK。那么如果在这段时间,由于客户端认为链接建立成功,客户端已经开始向服务器端发送报文,当服务端收到消息的时候就疑惑?不是连接还没建立完成,怎么消息就来了呢?因此此时服务器端就立马给客户端发来的数据进行ACK响应,并且把RST标志位置为1,告诉客户端让其关闭连接,进行重新连接。
四次挥手中客户端(主动断开连接的一方) 状态转换:
四次挥手中服务端状态转换:
为什么是四次挥手
客户端发送的FIN要保证服务器端收到,因此FIN必须要有ACK,因此两个FIN都必须要有ACK,这也就是4次挥手。当客户端和服务器端同时想要断开连接,在服务器端回复ACK的时候同时发送FIN。那么整个挥手过程变成了3次挥手。
CLOSE_WAIT状态是一端想要断开连接,另一方不断开连接,那么另一方就会一直维持在CLOSE_WAIT状态。注意先不能accept.
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class ServerTcp
{
public:
ServerTcp(uint16_t port,const std::string& ip = "")
:port_(port),ip_(ip),listenSock_(-1)
{
quit_ = false;
}
~ServerTcp()
{
if(listenSock_>= 0)
{
close(listenSock_);
}
}
public:
void init()
{
//1.创建socket
listenSock_ = socket(PF_INET,SOCK_STREAM,0);
if(listenSock_ < 0) exit(1);
//2.bind 绑定
//填充服务器信息
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty()?(local.sin_addr.s_addr = INADDR_ANY):(inet_aton(ip_.c_str(),&local.sin_addr));
if(bind(listenSock_,(const struct sockaddr*)&local,sizeof(local)) < 0) exit(2);
//3.监听
if(listen(listenSock_,2) < 0) exit(3);
//让别人来链接你
}
void loop()
{
signal(SIGCHLD,SIG_IGN);//only linux
while(!quit_)
{
sleep(1);
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(listenSock_,(struct sockaddr *)&peer,&len);
if(quit_) break;
if(serviceSock<0)
{
//获取链接失败
std::cerr<<"accept error........." <
#include "server.hpp"
static void Usage()
{
std::cout << "Usgae:\n\t ./server port" <
当运行起来之后,我们可以使用另外一台及其进行对服务器连接(这里不推荐使用一台机器,因此如果是一台机器,服务器和客户端是一台机器,那么我们在查询的时候就会出现两个字段,不方便查看)
启动服务之后,在另外一台机器上链接服务器
此时
我们发现了8082的外部地址是81,正式我们另外一台机器。现在他的状态是ESTABLISHED -- 因此我们得到一个结论,我们现在不accpet,三次握手也会成功。
我们现在让81的机器同时链接4个会怎么样的?
我们来查看一下状态,发现其中有一个状态是SYN_RECV,我们快快来看看这个状态是什么?
我们发现,SYN_RECV意味着服务器收到了你连接的请求,但是我先不给你ACK。那么为什么到第四个客户端再连接时就不能完成三次握手呢?这是listen的第二个参数有关。
listen的第二个参数叫做底层的全连接队列的长度,算法是:n+1表示在不accept的情况下,服务器最多能够维护的链接个数。因此我们刚刚最多只能有3个客户端同时连接我们
客户端状态正常 , 但是服务器端出现了 SYN_RECV 状态 , 而不是 ESTABLISHED 状态这是因为 , Linux 内核协议栈为一个 tcp 连接管理使用两个队列:
- 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
- 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)
而全连接队列的长度会受到 listen 第二个参数的影响 . 全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了 . 这个队列的长度通过上述实验可知, 是 listen 的第二个参数 + 1.
当我们把第一个客户端关闭的时候,我们再来查看状态,我们发现此时,他的状态就变成了CLOSE_WAIT,结合我们四次挥手的流程图, 可以认为四次挥手没有正确完成.
小结:
当全部关闭客户端时,状态便全部变成了CLOSE_WAIT
此时我们立马让服务其CTRL-C
这是因为虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。
服务器需要处理非常大量的客户端的连接 ( 每个连接的生存时间可能很短 , 但是每秒都有很大数量的客户端来请求). 这个时候如果由服务器端主动关闭连接( 比如某些客户端不活跃 , 就需要被服务器端主动清理掉 ), 就会产 生大量TIME_WAIT 连接 . 由于我们的请求量很大, 就可能导致 TIME_WAIT 的连接数很多 , 每个连接都会占用一个通信五元组 ( 源 ip, 源端口, 目的 ip, 目的端口 , 协议 ). 其中服务器的 ip 和端口和协议是固定的 . 如果新来的客户端连接的 ip 和端口号和TIME_WAIT 占用的链接重复了 , 就会出现问题 .
解决:
TCP如果每一个发送数据段,都要给一个ACK的确认应答,收到ACK后在发送下一个数据段,这样做有一个比较大的确定啊,就是性能较差,尤其是数据往返的时间较长的时候。
为了解决这个问题,TCP引入了一个滑动窗口的概念。即使在往返时间较长的情况下,它也能控制网性能的下降。确认应答不再是以每个分段,而是以更大的单位进行确认时,转发时间将会被大幅度的缩短。也就是说,发送端主机在发送一个段以后不必要一直等待确认应答,而是继续发送。
- 窗口大小指:无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000个字节.(4个段)
- 发送前4个段的时候,不需要等待任何ACK,直接发送。
- 收到第一个ACK以后,滑动窗口向后移动,继续 发送第5个段的数据;以此类推
- 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有那些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉。
- 窗口越大,则网络的吞吐量就越高
那么如果出现了丢包,如何进行重传?这里分两种情况讨论
情况一:数据包已经抵达,ACK丢失了。这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认。
情况二:数据包直接丢了
- 当某一段报文端丢失之后,发送端会一直收到1001这样的ACK,就像是在提醒发送端"我想要的是1001一样"
- 如果发送端主机连续3次收到了同样"1001"这样的应答,就会将对应的数据1001-2000重新发送
- 这个时候接收端收到了1001之后,再次返回的ACK就是7001(因为2001-7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中。
这种机制被称为"高速重发控制"(也叫快重传)。滑动窗口不一定向右滑动,滑动窗口有可能增大也有可能减小。
start_index什么时候会向右走:
start_index等于确认序号;end_index=start_index+窗口大小
虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据,但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。因为网络上有很多计算机,可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的。
因此为了解决这个问题,TCP引入慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
- 此处引入了一个概念称为拥塞窗口
- 发送开始的时候,定义拥塞窗口大小为1
- 每次收到一个ACK应答,拥塞窗口加1
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口
因此发送方向接收方一次可以发送的数据量 = min ( 对方的接受能力,网络的拥塞窗口 )
因此滑动窗口的大小 = min (对方窗口大小的剩余值,网络的拥塞窗口)
因此end_index = start_index + min(窗口大小,拥塞窗口)
为了不增长的那么快 , 因此不能使拥塞窗口单纯的加倍 . 此处引入一个叫做慢启动的阈值 当拥塞窗口超过这个阈值的时候 , 不再按照指数方式增长 , 而是按照线性方式增长
少量的丢包,我们仅仅是触发超时重传,大量的丢包,我们就认为网络拥塞,当TCP通信开始后,网络吞吐量会逐渐上升,随着网络发生拥堵,吞吐量会立即下降。拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
如果接受数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小.
一定要记得,窗口越大,网络吞吐量就越大,传输效率就越高,我们的目标是在保证网络不拥塞的情况下尽量提高传输效率
那么所有的包都可以延迟应答吗?肯定也不是
创建一个TCP的socket,同时在内核中创建一个发送缓冲区和一个接受缓冲区
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
应用层只管向发送缓冲区拷贝数据,而TCP会按照自己的规则发送数据。写入的时候与写入的格式没有关系。读取的时候与读取的格式毫不相关,因此发送和接受都与格式毫不相关。这就叫做面向字节流。
tcp是面向字节流的,根本不关心任何的数据格式。但是要正确使用这个数据,必须得有特定的格式。这个格式是应用层进行处理的(保证读取到一个完整的报文)
- 粘包问题中的“包”,是指应用层的数据包
- 在TCP的协议头中,没有如同UDP一样的“报文长度”这样的字段,但是有一个序号这样的字段
- 站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。
- 站在应用层的角度,看到的只是一串连续的字节数据
- 那么应用程序看到了这么一连串的字节数据,就不知道从那个部分开始到哪个部分,是一个完整的应用层数据包
避免粘包问题:明确报文与报文之间的边界
不存在
进程终止:进程终止会释放文件描述符,依然可以发送FIN,和正常关闭没有什么区别
机器重启:和进程终止的情况相同
机器掉电/网线断开:接收端认为链接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset,即使没有写入操作,TCP自己内置了一个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放掉。
另外,应用层的某些协议,也有一些这样的检测机制。例如HTTP长连接中会定期检测对方的状态。
udp可以参考tcp可靠性机制,在应用层实现类似的逻辑
例如:
- 引入序列号,保证数据顺序
- 引入确认应答,保证对端收到了数据
- 引入超时重传,如果隔一段时间没有应答,就重发数据。
- ................
谷歌开发了一个QUIC是基于UDP开发的。文档链接:(英文)RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport (quicwg.org)
QUIC ,即 快速UDP网络连接 ( Quick UDP Internet Connections ), 是由 Google 提出的实验性网络传输协议 ,位于 OSI 模型传输层。 QUIC 旨在解决 TCP 协议的缺陷,并最终替代 TCP 协议, 以减少数据传输,降低连接建立延 迟时间,加快网页传输速度。
QUIC的特点
- 连接建立低时延
- 多路复用
- 无队头阻塞
- 灵活的拥塞控制机制
- 连接迁移
- 数据包头和包内数据的身份认证和加密
- FEC前向纠错
- 可靠性传输
- 其他
TCP保证可靠性的策略:
提高性能的策略:
等等