TCP/IP 协议
网络分层
应用层
应用层的本质是规定了应用程序之间如何相互传递报文, 以 HTTP
协议为例,它规定了:
报文的类型,是请求报文还是响应报文
报文的语法,报文分为几段,各段是什么含义、用什么分隔,每个部分的每个字段什么什么含义
进程应该以什么样的时序发送报文和处理响应报文
除了 HTTP
协议,还有一些常见的应用层协议:
WebSocket
收发邮件的
POP3
和SMTP
协议FTP
DNS
传输层
传输层的主要任务就是负责向两台终端设备进程之间的通信提供通用的数据传输服务。应用进程使用该服务传送应用层报文。虽然是叫传输层,但是并不是将数据包从一台主机传送到另一台,而是对「传输行为进行控制」。
TCP
:提供面向连接的可靠传输服务。UDP
:提供无连接的尽最大努力的的传输服务。
网络层
网络层负责为分组交换网上的不同主机提供通信服务,选择合适的路由,使源主机传输层所传下来的分组,能通过网络层中的路由器找到目的主机。常见网络层协议如下:
IP
:网际协议,主要作用是定义数据包的格式,对数据包进行路由和寻址。ARP
:地址解析协议,解决网络地址和链路层地址之间的转换问题(IP 地址转 MAC 地址)。ICMP
:互联网控制报文协议,一种传输网络状态和错误消息的协议,常用于网络诊断和故障排除。我们平常使用的ping
就使用的这个协议来测试网络连通性。NAT
:网络地址转换协议。
IP
协议是网络层的主要协议,TCP
和 UDP
都是用 IP
协议作为网络层协议。IP
协议是一个无连接的协议,也不具备重发机制,这也是 TCP
协议复杂的原因之一。
网络接口层
可以把网络接口层看做是数据链路层和物理层的结合体。以太网、Wifi、蓝牙就工作在这一层。
数据链路层:将网络层的
IP
数据报组装成帧,在相邻的两个链路上传送帧。物理层:实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。
分层好处
分层的本质是通过分离关注点而让复杂问题简单化,通过分层可以做到:
各层独立:限制了依赖关系的范围,各层之间使用标准化的接口,各层不需要知道上下层是如何工作的,增加或者修改一个应用层协议不会影响传输层协议。
灵活性更好:比如路由器不需要应用层和传输层,分层以后路由器就可以只用加载更少的几个协议层。
能促进标准化:每一层职责清楚,方便进行标准化。
TCP 报文首部
TCP
报文首部包含很多内容,是支撑 TCP
复杂功能的基石。首部内容如下所示:
端口号
TCP
报文头部里包含了源端口号和目标端口号,源 IP
和目标 IP
是 IP
数据包才会有的。源 IP
、源端口、目标 IP
、目标端口构成了 TCP
连接的「四元组」。一个四元组可以唯一标识一个连接。
序列号
TCP
是面向字节流的协议,通过 TCP
传输的字节流的每个字节都分配了序列号,序列号(Sequence number
)指的是本报文段第一个字节的序列号。
序列号加上报文的长度,就可以确定传输的是哪一段数据。序列号是一个 32 位的无符号整数,达到 2^32-1 后循环到 0。在 SYN
报文中,序列号用于交换彼此的初始序列号,在其它报文中,序列号用于保证包的顺序。因为网络层(IP 层)不保证包的顺序,所以 TCP
协议利用序列号来解决网络包乱序、重复的问题,以保证数据包以正确的顺序组装传递给上层应用。
初始序列号(Initial Sequence Number)
在建立连接(三次握手)之初,通信双方都会各自选择一个序列号,称之为初始序列号,通信双方通过 SYN
报文交换彼此的初始序列号(ISN
)。后续报文传输时报文首部中的序列号就以此为基础。三次握手时 SYN
报文交换过程如下图所示:
初始序列号的计算逻辑是通过源地址、目标地址、源端口、目标端口和随机因子来进行计算,具体实现就不讲了,感兴趣的可以自己了解一下,总之,要记住一点,ISN
并不是从 0 开始的。
确认号(Acknowledgment number)
TCP
使用确认号来告知对方下一个期望接收的序列号,小于此确认号的所有字节都已经收到。例如:接受方返回给发送方的 ACK
为 20,表明序列号小于 20 的包都已经收到了,下次从20开始发送。
TCP Flags
TCP
有很多种标记,有些用来发起连接同步初始序列号,有些用来确认数据包,还有些用来结束连接。TCP
定义了一个 8 位的字段用来表示 flags
,但其实大部分都只用到了后 6 个。
来看一下三次握手的第一个 SYN
包的 flags
:
三次握手和四次挥手的 SYN
、ACK
、FIN
其实只是把 flags
对应的 bit 位置置为 1 而已,这些标记可以组合使用,比如 SYN+ACK
,FIN+ACK
等。
SYN(Synchronize)
:用于发起连接数据包同步双方的初始序列号。ACK(Acknowledge)
:确认数据包。RST(Reset)
:这个标记用来强制断开连接,通常是之前建立的连接已经不在了、包不合法、或者实在无能为力处理。FIN(Finish)
:通知对方我发完了所有数据,准备断开连接,后面我不会再发数据包给你了。PSH(Push)
:告知对方这些数据包收到以后应该马上交给上层应用,不能缓存起来。
窗口大小
Window Size
只有 16 位,即最大窗口大小是 65535 字节(64KB)。当然,实际上是不可能这么小的,因此 TCP
协议引入了TCP 窗口缩放选项 作为窗口缩放的比例因子,比例因子值的范围是 0 ~ 14,其中最小值 0 表示不缩放,最大值 14。比例因子可以将窗口扩大到原来的 2 的 n 次方。
值得注意的是,窗口缩放值在三次握手的时候指定,可以使用 wireshark
查看最终的窗口大小,如果抓包的时候没有抓到 三次握手阶段的包,wireshark
是不知道真正的窗口缩放值是多少的。
可选项
可选项的格式入下所示:
常用的选项有以下几个:
MSS
:最大段大小选项,是TCP
允许的从对方接收的最大报文段。SACK
:选择确认选项。(具体作用下文讲)Window Scale
:窗口缩放选项。
以 MSS
为例,kind=2,length=4,value=1412。
MTU 和 MSS
数据链路层传输的帧大小是有限制的,不能把一个太大的包直接塞给链路层,这个限制被称为 最大传输单元(Maximum Transmission Unit, MTU)。以太网的帧最小的帧是 64 字节,最大的帧是 1518 字节,除去 14 字节头部和 4 字节 CRC
,有效载荷范围是 64~1500,因此如果传输 100KB 的数据,至少需要 (100 * 1024 / 1500) = 69 个以太网帧。
不同的数据链路层的 MTU
是不同的。通过 netstat -i
可以查看网卡的 MTU
。
因为有 MTU
的存在,TCP
每次发包的大小也限制了,这就是 MSS
。TCP
为了避免被发送方分片,会主动把数据分割成小段再交给网络层,最大的分段大小称之为 MSS
(Max Segment Size)。
MSS = MTU - IP header头大小 - TCP 头大小
在以太网中 TCP
的 MSS
= 1500(MTU) - 20(IP 头大小) - 20(TCP 头大小)=1460。
三次握手
三次握手的最重要的是交换彼此的 ISN(初始序列号)。
客户端发送的一个
SYN
报文,这个报文只有SYN
标记被置位,如下图:SYN
报文不携带数据,但是它会占用一个序号,下次发送数据序列号要加一。客户端会随机选择一个数字作为初始序列号(ISN)。服务端收到客户端的
SYN
报文以后,将SYN
和ACK
标记都置位。客户端发送三次握手最后一个
ACK
段,这个ACK
段用来确认收到了服务端发送的SYN
段。因为这个ACK
段不携带任何数据,且不需要再被确认,这个ACK
段不消耗任何序列号。
除了交换彼此的初始序列号,三次握手的另一个重要作用是交换一些辅助信息,比如最大段大小(MSS)、窗口大小(Win)、窗口缩放因子(WS)、是否支持选择确认(SACK_PERM)等。
三次握手过程中,客户端和服务端的状态变化如下:
一个最简单的三次握手过程的 wireshark
抓包如下:
注意 Seq
和 Ack
的值,是不是和上面讲的一样。
TCP 自连接
客户端主动发起请求 connect
,且自身没有指定端口时,操作系统会为它分配一个临时端口,在 Linux
上这个端口的取值范围由 /proc/sys/net/ipv4/ip_local_port_range
文件的值决定。
假设一方主动发起连接时(连接 50000 端口),操作系统会自动分配一个临时端口号给连接主动发起方。如果刚好分配的临时端口是 50000 端口,过程如下:
第一个包是发送
SYN
包给 50000 端口;对于发送方而已,它自己收到了这个
SYN
包,以为对方是想同时打开,会回复SYN+ACK
;回复
SYN+ACK
以后,它自己就会收到这个SYN+ACK
,以为是对方回的,对它而言握手成功,进入ESTABLISHED
状态。
这个就是自连接,带来的问题也很明显:自连接的进程占用了端口,导致真正需要监听端口的服务进程无法监听成功。且自连接的进程看起来 connect 成功,实际上服务是不正常的,无法正常进行数据通信。
那么如何避免自连接呢,前面提到过随机端口号的分配范围,从这个方面入手,只要让服务监听的端口与客户端随机分配的端口不相同即可。以前面的图片为例,只要服务监听的端口小于 32768
就不会出现客户端与服务端口相同的情况。
连接队列
半连接队列
当客户端发起 SYN
到服务端,服务端收到以后会回 ACK
和自己的 SYN
。这时服务端这边的 TCP
从 listen
状态变为 SYN_RCVD (SYN Received)
,此时会将这个连接信息放入半连接队列。
服务端回复 SYN+ACK
包以后等待客户端回复 ACK
,同时开启一个定时器,如果超时还未收到 ACK
会进行 SYN+ACK
的重传,重传的次数由 tcp_synack_retries
值确定。在CentOS
上这个值等于 5。一旦收到客户端的 ACK
,服务端就开始尝试把它加入另外一个全连接队列。
全连接队列
全连接队列包含了服务端所有完成了三次握手,但是还未被应用调用 accept
取走的连接队列。此时的 socket
处于 ESTABLISHED
状态。每次应用调用 accept()
函数会移除队列头的连接。如果队列为空,accept()
通常会阻塞。
SYN FLOOD 攻击
半连接队列大小是有限的,如果半连接队列满,会出现无法处理正常请求的情况。SYN FLOOD
攻击就是基于此的,它是一种广为人知的 DDoS(拒绝服务攻击),客户端大量伪造 IP
发送 SYN
包,服务端回复的 ACK+SYN
去到了一个未知的 IP
地址,造成服务端大量的连接处于 SYN_RCVD
状态,导致半连接队列迅速占满。
如何应对:
增加
SYN
连接数:调大net.ipv4.tcp_max_syn_backlog
的值,一般不推荐,因为受到攻击的时候,再大的值也不管用。减少
SYN+ACK
重试次数:重试次数由/proc/sys/net/ipv4/tcp_synack_retries
控制,默认情况下是 5 次,当收到SYN+ACK
故意不回ACK
或者回复的很慢的时候,调小这个值很有必要。SYN Cookie
机制:最早是在 1996 年提出的,用来解决SYN FLOOD
攻击的,现在服务器上的tcp_syncookies
都是默认等于 1,表示连接队列满时启用,等于 0 表示禁用,等于 2 表示始终启用。由/proc/sys/net/ipv4/tcp_syncookies
控制。它的原理比较简单,就是在三次握手的最后阶段才分配连接资源,如下图所示:
SYN Cookie
的原理是基于「无状态」的机制,服务端收到SYN
包以后不马上为该连接内存资源,而是根据这个SYN
包计算出一个Cookie
值,作为握手第二步的序列号回复SYN+ACK
,等对方回应ACK
包时校验回复的ACK
值是否合法,如果合法才三次握手成功,分配连接资源。
SO_REYUSEADDR
四次挥手的时候,主动断开连接的那一端需要等待 2 个 MSL
才能最终释放这个连接。一般而言,主动断开连接的都是客户端,但如果是服务端程序重启或者出现 bug
崩溃,这时服务端会主动断开连接。因为要等待 2 个 MSL
才能最终释放连接,重启后重新绑定这个端口,默认情况下,操作系统会阻止新的套接字绑定到这个端口。启用 SO_REUSEADDR
套接字选项可以解除这个限制。
评论区