文章目录
tcp也叫 传输控制协议,是TCP/IP体系中非常复杂的一个协议,下面来说说它的特点:
TCP的特点:
1.面向连接的运输层协议
2、每一条tcp的连接只能有两个端点,即点对点,一对一的
3、tcp提供可靠交付的服务
4、tcp提供全双工通信(即两边都可以同时接收或者发送)
5、面向字节流的,流指的是流入到进程或从进程流出的字节序列
TCP协议段格式
- 源端口号/目的端口号: 表示数据从哪个进程来, 到哪个进程去.
- 32位序号:tcp不是以整个报文的方式发送的,而是拆分后以字节流的方式,而在接收方需要进行重排序,(因为流到达的先后顺序是不确定的),就需要直到他们原来的顺序,即这里所说的把字节按顺序编号就是序号。
- 32位确认号:期望收到对方下一个报文段的第一个数据字节的序号。
- 4位首部长度(数据偏移): 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4
= 60 - 6位保留: 顾名思义, 先保留着, 以防万一
- 6位标志位,含义如下
URG: 标识紧急指针是否有效
ACK: 标识确认序号是否有效
PSH: 用来提示接收端应用程序立刻将数据从tcp缓冲区读走
RST: 要求重新建立连接. 我们把含有RST标识的报文称为复位报文段
SYN: 请求建立连接. 我们把含有SYN标识的报文称为同步报文段
FIN: 通知对端, 本端即将关闭. 我们把含有FIN标识的报文称为结束报文段
- 16位窗口大小:接收方允许发送方发送的数据量(以字节为单位),是发送方设置其发送窗口大小的依据(只是依据,而不是决定)
- 16位检验和: 由发送端填充, 检验形式有CRC校验等. 如果接收端校验不通过, 则认为数据有问题. 此处的校验和不光包含TCP首部, 也包含TCP数据部分.
- 16位紧急指针: 用来标识哪部分数据是紧急数据.
- 选项:长度可变,最大为40字节,当没有选项时,tcp的首部长度位0字节
TCP实现可靠传输的机制
1、确认应答(ACK)机制
存在后发先致的情况,所以TCP对每个字节的数据都进行了排序并编号.方便在接收缓存区进行重组, 也就是为序列号.
每一个ACK都带有自己对应的确认序列号, 意思是告诉发送放, 接收方已经收到了哪些数据(比如上面的2001,则表示前2000个都收到了); 下一次发送方从哪里再开始发
2、超时重传机制
解决确认重传的异常情况(1,请求丢了,2没收到确认)
像上面的一样,主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法抵达主机B;
如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;
上面只是一种可能,,还有种可能主机A没有收到主机B发来的确认应答信息, 也可能是因为确认ACK丢失了;如下图
因此主机B会收到很多重复数据. 那么TCP协议就在接收缓存区里根据序列号,把重复的去掉。
重传策略1:以二的指数倍进行重传
重传策略2:经过一定的发送次数后,还没有收到结果,则发送端认为接收端已经下线了,就“停止”发送数据了。(并不是真的停止,还会发送探测报文,看接收端是否上限)
3、连接管理机制
一般情况下, tcp需要经过三次握手建立连接, 再通过四次挥手断开连接.
三次握手
三次握手示意图
一开始, 客户端和服务器都处于 CLOSE 状态.
1, TCP服务器进程先创建传输控制块TCB, 时刻准备接受客户端进程的连接请求, 此时服务器就进入了 LISTEN(监听)状态
2, TCP客户端进程也是先创建传输控制块TCB, 然后向服务器发出连接请求报文,此时报文首部中的同步标志位SYN=1, 同时选择一个初始序列号 seq = x, 此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定, SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
3, TCP服务器收到请求报文后, 如果同意连接, 则发出确认报文。确认报文中的 ACK=1, SYN=1, 确认序号是 x+1, 同时也要为自己初始化一个序列号 seq = y, 此时, TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据, 但是同样要消耗一个序号。
4, TCP客户端进程收到确认后还, 要向服务器给出确认。确认报文的ACK=1,确认序号是 y+1,自己的序列号是 x+1.
5, 这时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。当服务器收到客户端的确认后也进入ESTABLISHED状态,双方就可通信了。
四次挥手
四次挥手示意图
数据传输完后,客户端和服务器都是处于ESTABLISHED状态,客户端先发出断开请求。
1, 客户端进程发出连接释放请求,并且停止发送数据。
释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2, 服务器收到连接释放报文,发出确认报文,ACK=1,确认序号为 u+1,并且带上自己的序列号seq=v,此时服务端就进入了CLOSE-WAIT(关闭等待)状态。
TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
3, 客户端收到服务器的确认请求后,此时客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最终数据)
4, 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,确认序号为v+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5, 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,确认序号为w+1,而自己的序列号是u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6, 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
如果程序出现大量的CLOSE-WAIT状态,说明你的程序有bug,没有关闭资源,即调用close方法
三次握手与四次挥手
下图是TCP状态转换的一个汇总:
较粗的虚线表示服务端的状态变化情况;
较粗的实线表示客户端的状态变化情况;
CLOSED是一个假想的起始点, 不是真实状态;
4、滑动窗口
,因为确认机制下,每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.这样性能比较差,可以一次发多个数据段,也就是滑动窗口机制。
滑动窗口并不是tcp首部里面的窗口大小(属于服务器),滑动窗口是属于发送端的;
- 滑动窗口的大小指的是无需等待确认应答而直接可以继续发送数据的最大值.
- 上面发送前四个段的时候, 不需要等待任何ACK, 直接发送;
- 在收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
- 操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答(没收到); 只有确认应答过的数据, 才能从缓冲区删掉,没应答的继续存在缓存区里;
- 滑动窗口越大, 则网络的吞吐率就越高;
出现丢包的两种情况;
情况一: , 数据包收到, 可确认应答ACK没收到、丢了.
这种情况, 部分ACK丢了并不要紧,不需要重传, 因为可以通过后续的ACK进行确认;
情况二: 数据包就直接丢了.
- 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 表示没有收到。
- 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
- 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了,前面的接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;
- 这种机制被称为 “高速重发控制”(也叫 “快重传”).
5、 流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被填满, 这个时候如果发送端继续发送, 就会造成丢包, 进而引起丢包重传等一系列连锁反应.
因此TCP会根据接收端的处理能力, 来决定发送端的发送速度.
这个机制就叫做 流量控制(Flow Control)
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,通过ACK通知发送端;滑动窗口属于客户端(发送端)
- 窗口大小越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口大小的通知之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为0;
- 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 让接收端把窗口大小再告诉发送端.
那么接收端是怎样把窗口大小告知发送端的?
我们的TCP首部中, 有一个16位窗口大小字段, 就存放了接收端窗口大小的信息;
16位数字最大表示65536, 那么TCP窗口最大就是65536字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是窗口字段的值左移 M 位(左移一位相当于乘以2).
6、 拥塞控制
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发一些问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.
而TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
开始的时候, 定义拥塞窗口大小为1;
每次收到一个ACK应答, 拥塞窗口加1;(2的指数加1)
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口
“慢启动” 只是指初使时慢, 但是增长速度非常快.(指数增长)
为了不增长得那么快, 这里引入一个名词叫做慢启动的阈值, 当拥塞窗口的大小超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长.(每次加1)
当TCP开始启动的时候, 慢启动阈值等于窗口最大值
在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1
少量的丢包, 我们仅仅是触发超时重传;
大量的丢包, 我们就认为是网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升;
随着网络发生拥堵, 吞吐量会立刻下降.
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.
7、延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.假设接收端缓冲区为1M. 一次收到了500K的数据;如果立刻应答, 返回的窗口大小就是500K;但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了; 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;如果接收端稍微等一会儿再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M(比原来大多了)
窗口越大, 网络吞吐量就越大, 传输效率就越高.
TCP的目标是在保证网络不拥堵的情况下尽量提高传输效率;
不是所有的数据包都可以延迟应答。
1、数量限制: 每隔N个包就应答一次
2、时间限制: 超过最大延迟时间就应答一次
具体的数量N和最大延迟时间, 依操作系统不同也有差异一般 情况下N 取2, 最大延迟时间取200ms,不能超过MSL,如果超过了,就会触发超时重传机制
8、捎带应答
在延迟应答的基础上, 继续优化传输效率。我们发现, 很多情况下,客户端和服务器在应用层也是 “一发一收” 的,意味着客户端给服务器说了 “How are you”,服务器也会给客户端回一个 “Fine, thank you”,那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起发送给客户端。
9、面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;调用write时, 数据会先写入发送缓冲区中;如果发送的字节数太大, 会被拆分成多个TCP的数据包发出;如果发送的字节数太小, 就会先在缓冲区里等待, 等到缓冲区大小差不多了, 或者到了其他合适的时机再发送出去;接收数据的时候, 数据也是先从网卡驱动程序到达内核的接收缓冲区;然后应用程序可以调用read从接收缓冲区拿数据;
由于缓冲区的存在, 所以TCP程序的读和写不需要一一匹配
例如:
写10个字节的数据, 可以调用一次write写10个字节, 也可以调用10次write, 每次写一个字节;
读10个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 10个字节, 也可以一次read一个字节, 重复10次;
10、粘包/半包问题
面向字节流导致的。首先要明确, 粘包问题中的 “包”, 是指应用层的数据包.在TCP的协议头中, 没有如同UDP一样的 “报文长度” 字段,但是有一个序号字段.站在传输层的角度, TCP是一个一个报文传过来的. 按照序号排好序放在缓冲区中.站在应用层的角度, 看到的只是一串连续的字节数据.
那么应用程序看到了这一连串的字节数据, 就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包.此时数据之间就没有了边界, 就产生了粘包问题
明确两个包之间的边界,就能解决粘包问题
对于定长的包
- 保证每次都按固定大小读取即可
例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可
对于变长的包
- 可以在数据包的头部, 约定一个数据包总长度的字段, 从而就知道了包的结束位置
还可以在包和包之间使用明确的分隔符来作为边界(应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可)
对于UDP协议来说, 是否也存在 “粘包问题” 呢?
- 不会,对于UDP, 如果还没有向上层交付数据, UDP的报文长度仍然存在.同时, UDP是一个一个把数据交付给应用层的, 就有很明确的数据边界.
- 站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收.不会出现收到 “半个” 的情况.
TCP 异常情况
1、进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
2、机器重启: 和进程终止的情况相同.
3、机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
另外, 应用层的某些协议, 也有一些这样的检测机制.
例如HTTP长连接中, 也会定期检测对方的状态.
例如QQ, 在QQ断线之后, 也会定期尝试重新连接.
TCP这么复杂为了即要保证可靠性, 又同时要尽可能的提高性能.
保证可靠性的基本机制:校验和、序列号(按序到达)、确认应答、超时重发、连接管理
提高性能. 滑动窗口、快速重传、延迟应答、捎带应答、流量控制、拥塞控制
其他方面:定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)
基于TCP应用层协议:HTTP、 HTTPS、 SSH、Telnet 、FTP、SMTP
如何保证udp的稳定性特性?
其实就是在问tcp保证可靠性的机制。