TCP协议
TCP协议概念
TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名, 要对数据的传输进行一个详细的控制
TCP协议格式
1. 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
2. 32位序号/32位确认号: 后面详细讲;
3. 4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部大长度是15 * 4 = 60
4. 6位标志位:
- URG: 紧急指针是否有效
- ACK: 确认号是否有效
- PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
- SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
- FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
5. 16位窗口大小
6. 16位校验和
发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也 包含TCP数据部分.
7. 16位紧急指针
标识哪部分数据是紧急数据;
8. 40字节头部选项
三次握手和四次挥手图示
关于TCP协议的重点问题
1. 三次握手建立连接以及四次挥手断开连接的流程?
三次握手过程
当客户端调用connect时,触发了连接请求,向服务器发送了SYN X包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN X包,调用accept函 数接收请求向客户端发送SYN K ,ACK X+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK X+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。
四次挥手
- 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
- 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
- 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
- 接收到这个FIN的源发送端TCP对它进行确认。
2. 握手为什么是三次,挥手为什么是四次?
三次握手足以建立连接,四次握手就有些多余
如果两次握手的话,就会出现已失效的请求报文段突然又传送到了服务端而产生连接的误判
例如:
有这样一种情况,当A发送一个消息给B,但是由于网络原因,消息被阻塞在了某个节点,然后阻塞的时间超出设定的时间,A会认为这个消息丢失了,然后重新发送消息。
当A和B通信完成后,这个被A认为失效的消息,到达了B
对于B而言,以为这是一个新的请求链接消息,就向A发送确认,
对于A而言,它认为没有给B再次发送消息(因为上次的通话已经结束)所有A不会理睬B的这个确认,但是B则会一直等待A的消息
四次挥手
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
3. 三次握手失败,服务端是如何处理的?
失败时服务器并不会重传ack报文,而是直接发送RTS报文段,进入CLOSED状态。这样做的目的是为了防止SYN洪泛攻击。
4. TIME_WAIT状态的作用?
如果四次挥手中最后一个ACK丢了,那么服务端会再次发送一个FIN。如果没有TIME_WAIT的话,那么客户端直接关闭,有可能重新与别的服务器建立连接,此时用的还是原来的端口和IP,那么之前的服务端重新发送的FIN又发给了客户端,此时客户端就懵逼了,我才刚建立连接,怎么就要分手?
总结
- 假如没有TIME_WAIT,客户端直接关闭,但是又重启了相同地址的客户端
- 有可能因为四次挥手最后一次ACK丢失导致服务器重传FIN包,对后续连接造成影响
- 因此主动关闭方,发送最后一ACK后。不能直接关闭,需要等待一段时间-----2个MSL时间
MSL-----报文最大生存周期
等待1个MSL时间是为了能够处理对端重传的FIN包进行ACK回复
等待2个MSL时间是为了让所有网络中延迟的报文都消失在网络中,不会对后续连接造成影响
5. 服务端出现大量TIME_WAIT状态的原因以及解决方法
由于主动关闭TCP连接的一方才会进入TIME_WAIT状态,一般情况服务器端不会出现TIME_WAIT状态,因为大多数情况都是客户端主动发起连接并主动关闭连接。但是某些服务如pop/smtp、ftp却是服务端收到客户端的QUIT命令后主动关闭连接,这就造成这类服务器上容易出现大量的TIME_WAIT状态的连接,而且并发量越大处于此种状态的连接越多。另外,对于被动关闭连接的服务在主动关闭客户端非法请求或清理长时间不活动的连接时(这种情况很可能是客户端程序忘记关闭连接)也会出现TIME_WAIT的状态。
方法一
C/C++中提供了一个接口,如果服务器重启时需要对端口号以及socket地址进行复用,从而避免了TIME_WAIT状态
方法二
通过修改Linux内核的方式解决该问题
在 /etc/sysctl.conf中加入
net.ipv4.tcp_tw_recycle = 1 (表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭)
net.ipv4.tcp_fin_timeout=30 (修改系統默认的 TIMEOUT 时间)
TCP的可靠传输
1. 确认应答机制
2. 超时重传机制
- 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;
超时的时间如何确定?
理想的情况下, 找到一个小的时间, 保证 “确认应答一定能在这个时间内返回”. 但是这个时间的长短, 随着网络环境的不同, 是有差异的. 如果超时时间设的太长, 会影响整体的重传效率; 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个大超时时间.
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时 时间都是500ms的整数倍.
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接
3. 包序管理
TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉. 这时候我们利用序列号, 就可以很容易做到去重的效果.
序号+长度
TCP连接管理机制
服务端状态转化
- [CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
- [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端 发送SYN确认报文
- [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行 读写数据了.
- [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器 返回确认报文段并进入CLOSE_WAIT;
- [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当 服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待后一个 ACK到来(这个ACK是客户端确认收到了FIN)
- [LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接
客户端状态转化
- [CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
- [SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据;
- [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入 FIN_WAIT_1;
- [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服 务器的结束报文段;
- [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
- [TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文大生存时间)的时间, 才会 进入CLOSED状态.
滑动窗口
一次性可以发送大量的数据(受限于协议字段中的窗口大小);然后等待回复
对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段. 这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候.既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时 间重叠在一起了).
- 窗口大小指的是无需等待确认应答而可以继续发送数据的大值.
- 发送前四个段的时候, 不需要等待任何ACK, 直接发送;
- 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
- 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确 认应答过的数据, 才能从缓冲区删掉;
- 窗口越大, 则网络的吞吐率就越高
服务端:接受数据,后沿往后走并回复客户端,前沿不能动,客户端拿走数据后,前沿往后走
客户端:发送第一条数据,窗口不动,等到接受到第一条数据的回复后,后沿向后走,根据回复的窗口大小,决定前沿是否向后动
滑动窗口中快速重传机制
如果出现了丢包, 如何进行重传?
情况一: 数据包已经抵达, ACK被丢了.
这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认;
只要后面有一次ack成功了,就代表前面的都接收到了
情况二: 数据包就直接丢了.
- 当某一段报文段丢失之后, 发送端会一直收到 1 这样的ACK, 就像是在提醒发送端 "我想要的是 1 一样;
- 如果发送端主机连续三次收到了同样一个 “1” 这样的应答, 就会将对应的数据 1~1024 重新发送;
- 这个时候接收端收到了 1 之后, 再次返回的ACK就是3073了(因为前面的已经接收到了)接收端其实之前就已 经收到了, 被放到了接收端操作系统内核的接收缓冲区中
滑动窗口中数据的连续发送,尽力避免了因为ack丢失而导致的重传
确认回复中的ACK确认序号能够表示,这个序号之前的数据都已经接收到了
若前边的数据没有接受到,反而接收到了后边的数据,则不会对后边的数据进行ACK确认
总结:
当接收端接受数据的时候,若第一条数据没到,但是接受到了第二条数据,认为第一条数据可能丢失,立即向发送端连续三次发送重传请求;发送端连续接受到三条重传请求,则对这条数据进行重传
流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应. 因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数 据段, 使接收端把窗口大小告诉发送端
接收端如何把窗口大小告诉发送端呢?
TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;
16位数字大表示65535, 那么TCP窗口大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;
总结
- 通信双方通过协议字段中的窗口大小来协商接下来应该发送的最大数据长度
- 窗口大小不大于当前接收缓冲区中空闲空间大小,避免因为发送数据过多导致缓冲区发满,而丢数据导致重传
TCP的拥塞窗口
TCP引入 慢启动,快增长 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
通信初始双方协商窗口大小,窗口有可能很大,一次会发送很多数据,可能会因为网络原因导致大量丢包,导致重传,降低效率
发送端维护一个拥塞窗口,控制/限制发送端发送的数据最大大小,这个数字随着每次ack的确认回复快速增长,但是一旦出现包重传,则立即重新初始化
- 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
- 此处引入一个叫做慢启动的阈值
- 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
- 当TCP开始启动的时候, 慢启动阈值等于窗口大值;
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞; 当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降; 拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.
TCP拥塞控制这样的过程, 就好像 热恋的感觉
TCP的延迟应答机制
尽可能保证窗口大小(因为接受方,有可能很快就会把数据从缓冲区拿走)
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小
- 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
- 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
- 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
- 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输 效率;
那么所有的包都可以延迟应答么? 肯定也不是;
- 数量限制: 每隔N个包就应答一次;
- 时间限制: 超过大延迟时间就应答一次;
TCP捎带应答机制
尽可能避免纯报头的确认回复
例如:
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的. 意味着客户端给服务器说 了 “How are you”, 服务器也会给客户端回一个 “Fine, thank you”; 那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起回给客户端
面向字节流
对数据按照以字节为单位的流式传输
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
- 调用write时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出 去;
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可 以写数据. 这个概念叫做 全双工
- 特性:传输比较灵活,但是缺点是tcp的粘包问题
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节; 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次 read一个字节, 重复100次;
粘包问题
tcp产生粘包就是内核并没有对send要发送的数据进行明确的边界区分。
- 首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包.
- 在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段.
- 站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.
- 站在应用层的角度, 看到的只是一串连续的字节数据.
- 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层 数据包
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界
- 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲 区从头开始按sizeof(Request)依次读取即可;
- 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
- 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔 符不和正文冲突即可)
思考: 对于UDP协议来说, 是否也存在 “粘包问题” 呢?
- 对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用 层. 就有很明确的数据边界.
- 站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半 个"的情况
TCP断开的体现
- recv返回0
- send触发SIGPIPE异常
- tcp只有连接断开的时候才会返回0
- 进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别
- 机器重启: 和进程终止的情况相同.
- 机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即 使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
- 另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ 断线之后, 也会定期尝试重新连接.
基于TCP应用层协议
- HTTP
- HTTPS
- SSH
- Telnet
- FTP
- SMTP