文章目录
一、认识TCP报头
1.1 简单认识TCP
TCP的特点
- 是传输层协议,负责制定网络传输策略。
- 面向连接的。数据只有在建立连接之后才能正常传递,对应的有连接管理机制。
- 可靠性。 为此提供了确认应答机制、超时重传机制、流量控制机制、拥塞控制等。
- 面向字节流。将数据按字节发送和接收。
- 全双工。可同时发送和接收。
TCP是可靠的,UDP是不可靠的,第一次容易产生误解,这并不是说不可靠的不好,只是用来形容一种特性。UDP的特点就是没有策略,简单,不用向TCP那么复杂的管理数据的发送和接收。
不同的特性也是用来使用不同的场景。
- 需要严格要求数据准确发送和接收,保证可靠性,就使用TCP。
- 若之前发送的数据少部分丢失不重要,允许少量丢包,就可以使用UDP。比如直播,游戏等,之前的数据意义是很小的,玩随时在移动,少量的数据丢失并不影响。
1.2 TCP协议端格式
- 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
- 4位TCP报头长度: 用来将报头和有效载荷分离
表示该TCP头部有多少个32位bit(有多少个4字节),所以TCP头部最大长度是15 * 4 = 60字节;如果选项没有,len = 20 / 4 = 5,所以一般值为5 - 6位标志位:
URG: 紧急指针是否有效
ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
- 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.
二、TCP确保可靠性
2.1 五个方面理解TCP的可靠性
TCP是可靠的,那么是如何保证的呢?
核心机制:基于序号的确认应答机制
后面谈论的发送数据,接受应答都是发送和接收一整条报文,即使没有数据。
-
通过收到应答来保证上一条信息被收到。但并不是完全可靠,因为总有新信息在发送,总有还没来得及确认的数据。
-
收到的顺序与发送的顺序不同 ---- 导致乱序问题。
32位序号保证按序发送 -
确认序号用来保证收到的顺序与发的顺序一致
32位确认序号的值 = 历史已经收到的最大序号 + 1;保证历史数据完全收到,通过确认序号可以知道下一个要发送的报文序号。
例如收到11 表示 11之前(不包括11)已经完全收到,下次要发送的序号从11开始 。 -
只要收到确认信息就能保证历史数据被对方完全收到(由序号解决)。通信双方都如此,就保证对历史数据的可靠性。
-
TCP也是一个全双工协议,可以同时发送数据和确认序号(一个报文里面有序号和确认序号两个字段,所以互不影响)
2.2 TCP的六个标志位
服务器在任何一个时刻都可能有成百上千的服务端向它发送数据,面临的首先问题是如何建立连接、如何区分TCP报文类别。
- ACK(acknowledge): 设置为1后表明是确认报文
几乎所有的TCP通信过程中ACK都会被设置 - SYN(Synchronize): 同步,发起请求连接
- RST:重置异常连接
- PSH:接受缓冲区快要慢了,但应用层不读或者很慢的时候,PSH可以告知对方尽快将缓冲区数据向上交付。
- URG:设置URG表明该报文有紧急数据,需要优先处理,数据在16位紧急指针存放数据在报文中的地址。
- FIN:请求断开连接
2.3 确认应答机制
TCP将每个字节的数据都进行了编号. 即为序列号.
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发。
确认序号的含义:
确认序号=历史已经收到的数据的最大序号 + 1
确认序号的值是下次需要传送的序号。
2.4 超时重传机制
系统为每个发送的报文设置了一个定时器,超过指定时间就重新发送。
发送方没有收到ACK,报文就一定没有被对方收到吗?不一定,可能是数据丢了,可能是ACK丢了,不管是什么情况都需要重传。
因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果.
那么, 如果超时的时间如何确定?
- 最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”.
- 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
- 如果超时时间设的太长, 会影响整体的重传效率;
- 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.
三、TCP面向连接
面向连接也是确保可靠性的基础。
3.1 连接的三次握手
3.1.1 图示
建立连接的本质:
- 三次握手成功,双方OS内都要为维护连接创建对应的数据结构,有一定成本(时间加空间)。
- 客户端发出ACK,它就认为已经建立连接了,而服务端等收到ACK的时候才认为已经建立号连接,它们之间是有时间差的。
- 三次握手也不一定成功,因为最后一次ACK没有确认应答,只是以较大概率建立连接。
3.1.2 为什么是三次
- 确认双方主机是否健康,这个两次就行。
- 验证全双工。 三次握手是能看到双方都有收发能力的最小次数。一次的发送,一次的接受表示主机具有全双工能力。当然本来应该是四次,一段两次,因为TCP有捎到应答机制,中间两次合并为一次。
- 如果是一次,或者两次,不能预防SYN洪水(客户端不断向服务器发送SYN请求,很容易建立连接,导致服务器的资源被大量占用)
3.2 断开的四次挥手
3.2.1 图示
建立连接的时候,如果服务器不想建立,就不会发送ACK,如果想要建立连接,由于捎带应答机制将ACK和SYN防止一个报文里面发送,否则也是四次握手。
由于两端断开连接的时间很多时候不相同,所以需要逐一发送,如果两边同时想断开连接也可以是三次挥手。
3.2.2 为什么是四次
- 断开连接的本质是双方达成断开的共识。
- 四次是双方协商断开连接的最小次数。一端的发送和接收共两次。
3.2.3 断开经历的两个状态
- TIME_WAIT状态
- 主动断开连接的一方要进入一个TIME_WAIT状态,此时连接是否成功断开,资源是否释放?
- 对于主动断开连接的一方,认为已经断开,但是不能释放连接,因为还不确定发送的ACK被对方收到,一般保持的时间是2 * MSL(报文最长存活时间)即ACK没有被对方接受,对方又发送FIN传递到本主机的时间。
理由:
- 尽量保证历史发送的数据正常到达,不到达也会收到信号,再做处理。
- 假设ACK没有被对方收到,对方重新发送FIN,本身收到FIN,证明对方没有收到ACK,可以重发。
所以处于TIME_WAIT状态的端口,还不能被再次绑定,这就解释前面做实验服务器断开后立马又运行出现的bind error
现象。
- TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态.
- 我们使用Ctrl-C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听同样的server端口;
- MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;
- 可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值;
- 为什么是TIME_WAIT的时间是2MSL?
- MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
- 同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);
- 怎么解决处于这种状态下无法绑定的问题
使用接口函数setsockopt
使用setsockopt()
设置socket描述符的选项SO_REUSEADDR
为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符.
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
// 使用
int opt = 1;
setsockopt(sockfd, level, optname, &opt, sizeof (opt));
-
理解 CLOSE_WAIT 状态
-
对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题.
-
所以不能忘记关闭文件描述符,文件描述符也是有限的,管理文件描述符也需要占用资源,不关闭会造成文件描述符泄漏,资源泄漏。