传输层
网络互连模型
请求过程
网络分层
传输层
传输层有2个协议
- TCP(Transmission Control Protocol),传输控制协议
- UDP(User Datagram Protocol),用户数据报协议
TCP 面向连接:首先需要建立连接,3 次握手,之后可以通过一条管道来互相发数据。发送完数据要断开连接,要不然服务器会同一个端口去监听客户端发送过来的数据,这样会占用服务器资源
UDP基础
数据格式
UDP 是无连接的,减少了建立和释放连接的开销
- UDP 尽最大能力交付,不保证可可靠交付
- 因此不需要维护一些复杂的参数,首部只有 8 个字节(TCP 的首部至少 20 个字节)
UDP 长度(Length)
- 占 16 位,首部的长度 + 数据的长度
检验和
- 检验和的计算内容:伪首部 + 首部 + 数据
- 伪首部:仅在计算检验和时起作用,并不会传递给网络层
端口号
UDP 首部中端口号是占用 2 字节(16 位)
- 可以推测出端口号的取值范围是:0 ~ 65535
客户端的源端口是临时开启的随机端口
防火墙可以设置开启/关闭某些端口来提高安全性
常用命令行:
netstat -an
:查看被占用的端口netstat -anb
:查看被占用的端口、占用端口的应用程序telenet 主机 端口
:查看是否可以访问主机的某个端口
安装 telnet:控制面板-程序-启用或关闭Windows功能-勾选Telnet Client-确定
TCP基础
数据格式
数据偏移
- 占 4 位,取值范围是 0x0101~0x1111
- 乘以 4:首部长度(Header Length),二进制乘以 4 才是最终长度
- 0b0101:20(最小值)
- 0b1111:60(最大值)
保留
- 占 6 位,目前全为 0
小细节
有些资料中,TCP 首部的保留(Reserved)字段占 3 位,标志(Flags)字段占 9 位
UDP 的首部中有个 16 位的字段记录了整个 UDP 报文段的长度(首部 + 数据)
- 但是,TCP 的首部中仅仅有 4 位的字段记录了 TCP 报文段的首部长度,并没有字段记录 TCP 报文段的数据长度
分析
- UDP 首部中占 16 位的长度字段是冗余的,纯粹是为了保证首部是 32bit 对齐
- TCP/UDP 的数据长度,完全可以由 IP 数据包的首部推测出来
传输层的数据传输层 = 网络层的总长度 - 网络层的首部长度 - 传输层数据首部长度
校验和
跟 UDP 一样,TCP 校验和的计算内容:伪首部 + 首部 + 数据
- 伪首部:占用 12 字节,仅在计算校验和时起作用,并不会传递给网络层
标识位
URG(Urgent)紧急
- 当 URG=1 时,紧急指针字段才有效。表明当前报文段中有紧急数据,应优先尽快传送
ACK(Acknowledgment)确认
- 当 ACK=1 时,确认号字段才有效
PSH(Push)
RST(Reset)重置
- 当 RST=1 时,表明连接中出现严重差错,必须释放连接,然后再重新建立连接
SYN(Synchronization)同步
- 当 SYN=1、ACK=0 时,表明这是一个建立连接的请求
- 若对方同意建立连接,则回复 SYN=1、ACK=1
FIN(Finish)结束
- 当 Fin=1 时,表明数据已经发送完毕,要求释放连接
序号、确认号、窗口
序号(Sequence Number)
- 占 4 字节
- 首先,在传输过程的每一个字节都会有一个编号
- 在建立连接后,序号代表:这一次传给对方的 TCP 数据部分的第一个字节的编号
确认号(Acknowledgment Number)
- 占 4 字节
- 在建立连接后,确认代表:期望对方下一次传过来的 TCP 数据部分的第一个字节的编号
窗口(Window)
- 占 2 字节
- 这个字节有流量控制功能,用以告知对方下一次允许发送的数据大小(字节为单位)
要点
- 可靠传输(丢掉的包都发给你)
- 等了一段时间之后发现对方没有给回复,就重新传(超时重传)
- 如果是一个一个发,给确认再发下一个,这样效率很低。可以一口气发很多个包,之后统一回复
- 连续收到 3 个重新确认,也重新传(快重传)
- 流量控制(发包慢一点)
- 互相告诉接收窗口大小(点对点、端对端)
- 通过控制拥塞窗口、接收窗口来控制发送窗口
- 拥塞控制(大家一起维护一个网络)
- 慢开始("指数增大"),当拥塞窗口达到一定阈值,开始拥塞避免("加法增大")
- 当包发到一定程度,会出现网络拥塞,开始阈值减半("乘法减小")
- 之后再次循环如上操作
- 新版本改为 快重传+快恢复,直接从阈值开始进行拥塞避免("加法增大")
- 连接管理
- 建立连接
- 释放连接
TCP可靠传输
停止等待ARQ协议
ARQ(Automatic Repeat-reQuest),自动重传请求
连续ARQ协议+滑动窗口协议
- 假设每一组数据是 100 个字节,代表一个数据段的数据
- 每一组一个编号
SACK(选择性确认)
- 在 TCP 通信过程中,如果发送序列中间某个数据包丢失(比如:1、2、3、4、5 中的 3 丢失了)
- TCP 会通过重传最后确认的分组后续的分组(最后确认的是 2,会重传 3、4、5)
- 这样原先已经正确传输的分组也可能重复发送(比如:4、5),降低了 TCP 性能
- 为了改善上述情况,发展出了 SACK(Selective Acknowledgment 选择性确认)技术
- 告诉发送方哪些数据丢失,哪些数据已经提前收到
- 使 TCP 只重新发送丢失的包(比如:3),不用发送后续所有的分组(比如:4、5)
SACK 信息会放在 TCP 首部的选项部分
- Kind:占 1 字节。值为 5 代表这是 SACK 选项
- Length:占 1 字节。表明 SACK 选项一共占用多少字节
- Left Edge:占 4 字节,左边界
- Right Edge:占 4 字节,右边界
一对边界信息需要占用 8 字节,由于 TCP 首部的选项部分最多 40 字节,所以
- SACK Kind、Length 为固定位,占用 2 字节
- SACK 选项最多携带 4 组边界信息
- SACK 选项的最大占用字节数 = 4 * 8 + 2 = 34
TCP疑问
重发次数
若有个包重传了 N 次还是失败,会一直重传到成功为止么?
- 这个取决于系统的设置,比如有些系统,重传 5 次还未成功就会发送 reset 报文(RST)断开 TCP 连接
不足接收窗口大小
如果接收窗口最多能接收 4 个包,但是发送方只发了 2 个包,接收方如何确定后面还有没有 2 个包?
- 等待一定时间后没有第 3 个包,就会返回确认收到 2 个包发给发送方
为什么传输层分割
为什选择在传输层就将数据 "大卸八块" 分成多个段,而不是等到网络层再进行分片传递给数据链路层?
- 因为可以提高重传的性能
- 需要明确的是:可靠传输是在传输层进行控制的
- 如果传输层不分段,一旦出现数据丢失,整个传输层的数据都得重传
- 如果传输层分了段,一旦出现数据丢失,只需要重传丢失的那些段即可
TCP流量控制
RWND(Receive Window)滑动窗口
- 滑动窗口技术是 TCP 流量控制的核心,存在于 TCP 的 Header 中,主要用于并发处理网络 Seq
- 滑动窗口是动态改变的,随着发送放每次发送 Seq 的时候,接收方都会根据当前机器的执行效率、缓存上限、当前缓存大小得出一个合适的窗口大小,并且随着 Ack 回传到发送方
- 发送方在下次发送数据包的时候,就可以根据新的窗口大小去发送数据了
流量控制
如果接收方的缓存区满了,发送方还在疯狂着发送数据
- 接收方只能把收到的数据包丢掉,大量的丢包会极大的浪费网络资源
- 所以要进行流量控制
什么是流量控制?
- 让发送方的发送速率不要太快,让接收方来得及接收处理
原理:
- 通过确认报文中窗口字段来控制发送方的发送速率
- 发送方的发送窗口大小不能超过接收方给出窗口大小
- 当发送方接收到窗口的大小为 0 时,发送方就会停止发送数据
特殊情况
有一种特殊情况
- 一开始,接收方给发送方发送了 0 窗口的报文段
- 后面,接收方又有了一些存储空间,给发送方发送的非 0 窗口的报文段丢失了
- 发送方的发送窗口一直为 0,双方陷入僵局
解决方案
- 当发送方收到 0 窗口通知时,这时发送方停止发送报文
- 并且同时开启一个定时器,隔一段时间就发个测试报文去询问接收方最新的窗口大小
- 如果接收的窗口大小还是 0,则发送方再次刷新启动定时器
TCP拥塞控制
拥塞控制
- 防止过多的数据注入到网络中
- 避免网络中的路由器或链路过载
拥塞控制是一个全局性的过程
- 涉及到所有主机、路由器
- 以及与降低网络传输性能有关的所有因素
- 是大家共同努力的结果
相比而言,流量控制是点对点通信的控制
拥塞方法
- 慢开始(slow start)
- 拥塞避免(congestion avoidance)
- 快速重传(fast retransmit)
- 快速恢复(fast recovery)
几个缩写:
mss(maximum segment size):每个段最大的数据部分大小
在建立连接时确定
数据链路层(以太网帧规定1500字节) -> 网络层(首部包20字节,数据包1480) -> 传输层(首部段至少20字节,数据段至多1460字节)
帧(frame) -> 包(packet) -> 段(segment)
cwnd(congestion window):拥塞窗口。发送方自己调整的
rwnd(receive window):接收窗口。接收方告诉发送方
swnd(send window):发送窗口
swnd = min(cwnd, rwnd)
cwnd=1460
rwnd=1380
TCP拥塞方法
慢开始
- cwnd 的初始值比较小,然后随着数据包倍接收方确认(收到一个 ACK)
- cwnd 就成倍增长(指数级)
拥塞避免
- ssthresh(slow start threshold):慢开始阈值,cwnd 达到阈值后,以线性方式增加
- 拥塞避免(加法增大):拥塞窗口缓慢增大,以防网络过早出现拥塞
- http://email.163.com/乘法减少:只要网络出现拥塞,把 ssthresh 减半,与此同时,执行慢开始算法(cwnd 又恢复到初始值)
快重传
接收方
- 每收到一个失序的分组后就立即发出重复确认
- 使发送方及时知道有分组没有到达
- 而不要等待自己发送数据时才进行确认
发送方
- 只要连续收到三个重复确认(总共 4 个相同的确认),就应当立即重传对方尚未收到的报文段
- 而不必继续等待重传计时器到期后再重传
快恢复
当发送方连续收到三个重复确认,说明网络出现拥塞
- 就执行 "乘法减小" 算法,把 ssthresh 减为拥塞峰值的一半
与慢开始不同之处是现在不执行慢开始算法,即 cwnd 现在不恢复到初始值
- 而是把 cwnd 值设置为新的 ssthresh 值(减小后的值)
- 然后开始执行拥塞避免算法("加法增大"),使拥塞窗口缓慢线性增大
发送窗口的最大值
发送窗口最大值:swnd = min(cwnd, rwnd)
- 当
rwnd < cwnd
时,是接收方的接收能力限制发送窗口的最大值 - 当
cwnd < rwnd
时,则是网络的拥塞限制发送窗口的最大值
TCP序号确认号
详细
seq=0
没有发送数据,ack=0
对方没发东西,没有东西可确认seq=0
没有发送数据,ack=seq1+1
期望对方发下一个字节seq=1
虽然上一次没有发数据,但是为了响应服务器seq
置为 1,期望对方发下一个字节ack=seq2+1
- 客户端发 http 给服务器,这时有数据了,假设占用了 k 字节,
ack=seq2+1
期望对方发下一个字节,seq=1
虽然上一次没有发数据,但是为了响应服务器seq
置为 1 ack=k+1
ack是对上一次对方发过来的回应
真正序号
相对,咱们理解的
真正的序号是在建立连接时确认的,如果用 1、1025 这样的数很容易暴露出示第几个包
- 1
syn=1, seq=123456
- 2
syn=1, ack=1, seq=234567
- 4
seq = 123456+1, len=100
- 5
ack=123456+1+100
- 6
seq=234567+1, len=100
- 7
ack=234567+1+100
TCP建立连接
3次握手
closed:client 处于关闭状态
listen:server 处于监听状态,等待 client 连接
服务器首先要占用一个端口监听把服务器程序启动,之后进行监听
syn-rcvd(received):表示 server 接收到了 SYN 报文,当收到 client 的 ACK 报文后,它会进入到 ESTABLISHED 状态
syn-sent:表示 client 已发送 SYN 报文,等待 server 的第 2 次握手
established:表示连接已经建立
前 2 次握手特点:
- SYN 都设置为 1
SYN=1
- 数据部分的长度都为 0
LEN=0
- TCP 头部的长度一般是 32 字节
- 固定头部:20字节
- 选项部分:12字节
- 双方会交换一些信息
- 比如:MSS、是否支持 SACK、Window scale(窗口缩放系数)等
- 这些数据都放在了 TCP 头部的选项部分中(12 字节)
为什么要3次握手
为什么建立连接的时候,要进行 "3次握手"?2次不行么?
- 主要目的:防止 server 端一直等待,浪费资源
如果建立连接只需要 "2次握手",可能会出现的情况
- 假设 client 发出的第一个连接请求报文段,因为网络延迟,在连接释放以后的某个时间才到达 server
- 本来这就是一个早已失效的连接请求,但 server 收到此消失的请求后,误认为是 client 再次发出的一个新的连接请求
- 于是 server 就向 client 发出确认报文段,同意建立连接
- 如果不采用 "3次握手",那么只要 server 发出确认,新的连接就建立了
- 由于现在 client 并没有真正想连接服务器的意愿,因此不会理睬 server 的确认,也不会向 server 发送数据,这样,server 的很多资源就白白浪费掉了
采用 "3次握手" 的办法可以防止上述现象发生
- 例如上述情况,client 没有向 server 的确认发出确认,server 由于收不到确认,就知道 client 并没有要建立连接
第3次握手失败了,会怎么处理?
- 此时 server 的状态为 SYN-RCVD,若等不到 client 的 ACK,server 会重新发送 SYN+ACK 包
- 如果 server 多次重发 SYN+ACK 都等不到 client 的 ACK,就会发送 RST 包,强制关闭连接
TCP释放连接
4次挥手
- fin-wait-1:表示想主动关闭连接
- 向对方发送 FIN 报文,此时进入 FIN-WAIT-1 状态
- close-wait:表示在等待关闭
- 当对方发送 FIN 给自己,自己会回应一个 ACK 报文给对方,此时进入到 CLOSE-WAIT 状态
- 在此状态下,需要考虑自己是否还有数据要发送给对方,如果没有,发送 FIN 报文给对方
- fin-wait-2:只要对方发送 ACK 确认后,主动发就会处于 FIN-WAIT-2 状态,然后等待对方发送 FIN 报文
- closing:一种比较罕见的例外状态
- 表示你发送 FIN 报文,并没有收到对方的 ACK 报文,反而却收到了对方的 FIN 报文
- 如果双方几乎在同时准备关闭连接的话,那么就会出现双方同时发送 FIN 报文的情况,也会出现 CLOSING 状态
- 表示双方都正在关闭连接
- last-ack:被动关闭一方在发送 FIN 报文后,最后等待对方的 ACK 报文
- 当收到 ACK 报文后,即可进入 CLOSED 状态
- time-wait:表示收到了对方的 FIN 报文,并发送了 ACK 报文,就等 2msl 后即可进入 CLOSED 状态了
- 如果 FIN-WAIT-1 状态,收到了对方同时带 FIN 标志和 ACK 标志的报文是
- 可以直接进入到 TIME-WAIT 状态,而无需经过 FIN-WAIT-2 状态
- 如果 FIN-WAIT-1 状态,收到了对方同时带 FIN 标志和 ACK 标志的报文是
- closed:关闭状态
由于有些状态的时间比较短暂,所以很难用 netstat 命令看到,比如:SYN-RCVD、FIN-WAIT-1 等
TCP/IP 协议栈在设计上,允许任何一方先发起断开请求
client 发送 ACK 后,需要有个 TIME-WAIT 阶段,等待一段时间,再真正关闭连接
一般等待 2 倍的 MSL(Maximum Segment Lifetime,最大分段生存期)
- MSL 是 TCP 报文在 Internet 上的最长生存时间
- 每个具体的 TCP 实现都必须选择一个确定的 MSL 值,RFC 1122 建议是 2 分钟
- 可以防止本次连接中产生的数据误传到下一次连接中(因为本次连接中的数据包都会在 2MSL 时间内消失了)
如果 client 发送 ACK 后马上释放了,然后又因为网络原因,server 没有收到 client 的 ACK,server 就会重发 FIN
这时可能出现的情况是:
- client 没有任何响应,服务器那边会干等,甚至多次重发 FIN,浪费资源
- client 有个新的应用程序刚好分配了同一个端口号,新的应用程序收到 FIN 后马上执行断开连接的操作,本来它可能是想跟 server 建立连接的
为什么要4次挥手
为什么释放连接的时候,要进行 "4次挥手"?
TCP 是全双工模式
- 第1次挥手:当主机1发出 FIN 报文段时
- 表示主机1告诉主机2,主机1已经没有数据要发送了,但是,此时主机1还是可以接收主机2的数据
- 第2次挥手:当主机2返回 ACK 报文时
- 表示主机2已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的
- 第3次挥手:当主机2也发送了 FIN 报文段时
- 表示主机2告诉主机1,主机2英没有数据要发送了
- 第4次挥手:当主机1返回 ACK 报文段时
- 主机1已经知道主机2没有数据发送了。随后正式断开整个 TCP 连接