参考文献

TCP协议

  • TCP (Transport Control Protocol)是一个传输层协议,提供 Host-To-Host 数据的可靠传输,支持全双工,是一个连接导向的协议.
  • TCP 提供的是 Host-To-Host 传输,一台主机通过 TCP 发送数据给另一台主机.这里的主机(Host)是一个抽象的概念,可以是手机、平板、手表等.收发数据的设备都是主机,所以双方是平等的.
  • TCP 要实现主机到主机通信,就需要知道主机们的 网络地址(IP 地址),但是 TCP 不负责实际地址到地址(Address-To-Address)的传输,因此 TCP 协议把 IP 地址给底层的互联网层处理.

连接和会话

  • 连接是通信双方的一个约定,目标是让两个在通信的程序之间产生一个默契,保证两个程序都在线,而且尽快地响应对方的请求.

    • 设计上,连接是一种传输数据的行为
  • 会话是应用的行为

  • 会话是应用层的概念,连接是传输层的概念

可靠性

  • 可靠性指数据保证无损传输.如果发送方按照顺序发送,然后数据无序地在网络间传递,就必须有一种算法在接收方将数据恢复原有的顺序.
  • 如果发送方同时要把消息发送给多个接收方,这种情况叫作多播,可靠性要求每个接收方都无损收到相同的副本.多播情况还有强可靠性,就是如果有一个消息到达任何一个接收者,那么所有接受者都必须收到这个消息.

双工/单工

  • 单工: 在任何一个时刻,如果数据只能单向发送,就是单工
    • 单工需要至少一条线路
  • 半双工: 如果在某个时刻数据可以向一个方向传输,也可以向另一个方向反方向传输,而且交替进行,叫作半双工
    • 半双工需要至少 1 条线路
  • 全双工: 如果任何时刻数据都可以双向收发,这就是全双工
    • 全双工需要大于 1 条线路

为什么需要TCP协议?TCP工作在哪一层?

  • IP层是[不可靠],它不保证网络包的交付,不保证网络包的按序交付,也不保证网络中的数据完整性.

    • 如果需要保证网络数据包的可靠性,那么就需要由上层(传输层)的TCP协议来负责.
    • 因为TCP是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏,无间隔,非冗余和按序的.
  • TCP是面向连接,可靠的,基于字节流的传输层通信协议.

    • 面向连接:一定是一对一才能连接,不能想UDP协议可以一个主机同时向多个主机发送消息.
    • 可靠的:无论网络链路中出现了怎样的链路变化,TCP都可以保证一个报文一定能够到达接收端.
    • 字节流:消息是没有边界的,所以无论消息有多大都可以进行传输,并且消息是有序的,当前一个消息没有收到的时候,即使它先收到了后面的字节,那么也不能扔给应用层去处理,同时对重复的报文会自动放弃.

TCP连接

  • 用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket,序列号和窗口大小称为连接.
    • Socket: 由IP地址和端口号组成
    • 序列号(Sequence number): 用来解决乱序问题等
    • 窗口大小(Windows sizes): 用来做流量控制

TCP连接的确定–TCP四元组

  • TCP四元组

    • 源地址

    • 源端口

    • 目的地址

    • 目的端口

  • 源地址和目的地址的字段(32位)是在IP头部中,作用是通过IP协议发送报文给对方主机.

  • 源端口和目的端口的字段(16位)是在TCP头部中,作用是告诉TCP协议应该把报文给哪个进程.

TCP最大连接数

  • 服务器通常固定在某个端口上监听,等待客户端的连接请求.

  • 客户端IP和端口是可变的,其理论值计算公式如下:

    1
    最大TCP连接数 = 客户端的IP数 * 客户端的端口数
    • 对于IPv4,客户端的IP数最多为2的32次方,客户端的端口数最多为2的16次方,也就是服务端单机最大TCP连接数,约为2的48次方.
    • 当然,服务端最大并发TCP连接数远不能达到理论上限.
      • 首先主要是文件描述符限制,Socket都是文件,所以首先要通过ulimit配置文件描述符的数目;
      • 另一个是内存限制,每个TCP连接都要占用一定内存,操作系统的内存是有限的.

TCP协议头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 0                   1                   2                   3   
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

TCP Header Format
  • 源端口号 Source Port: 16 bits The source port number.

  • 目标端口号 Destination Port: 16 bits The destination port number.

  • 序列号 Sequence Number: 32 bits

    • 目的为了解决乱序的问题.

    The sequence number of the first data octet in this segment (except when SYN is present). If SYN is present the sequence number is the initial sequence number (ISN) and the first data octet is ISN+1.

  • 确认应答号 Acknowledgment Number: 32 bits

    If the ACK control bit is set this field contains the value of the next sequence number the sender of the segment is expecting to receive. Once a connection is established this is always sent.

  • 首部长度 Data Offset: 4 bits

    • 这个量存在的原因是 TCP Header 部分的长度是可变的,因此需要一个数值来描述数据从哪个字节开始

    The number of 32 bit words in the TCP Header. This indicates where the data begins. The TCP header (even one including options) is an integral number of 32 bits long.

  • 保留 Reserved: 6 bits

    Reserved for future use. Must be zero.

  • 控制位 Control Bits: 6 bits (from left to right)

    • 用于描述 TCP 段的行为.也就是一个 TCP 封包到底是做什么用的
    标识 全称 说明 场景示例
    URG Urgent Pointer field significant 表示紧急指针字段有效 远程操作的时候,用户按下了 Ctrl+C,要求终止程序,这种请求需要紧急处理.
    ACK Acknowledgment field significant 该位为1时,[确认应答]的字段变为有效,TCP规定除了最初建立连接时的SYN包之外该位必须设置为1
    PSH Push Function 表示通过 flush 操作发送的数据
    RST Reset the connection 该位为1时,表示TCP连接中出现异常必须强制断开连接
    SYN Synchronize sequence numbers 该位为1,表示希望建立连接,并在[序列号]的字段进行序列号初始值的设定
    FIN No more data from sender 该位为1时,表示今后不会再有数据发送,希望断开连接.当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN位为1的TCP段
    • 如果一个 Host 主动向另一个 Host 发起连接,称为 SYN(Synchronization),请求同步;

    • 如果一个 Host 主动断开请求,称为 FIN(Finish),请求完成;

    • 如果一个 Host 给另一个 Host 发送数据,称为 PSH(Push),数据推送.

  • 窗口大小 Window: 16 bits

    The number of data octets beginning with the one indicated in the acknowledgment field which the sender of this segment is willing to accept.

  • 校验和 Checksum: 16 bits

  • 紧急指针 Urgent Pointer 16bits

    • 指向最后一个紧急数据的序号(Sequence Number).它存在的原因是:有时候紧急数据是连续的很多个段,所以需要提前告诉接收方进行准备.

TCP连接运行过程中状态

  • LISTEN - represents waiting for a connection request from any remote TCP and port.
  • SYN-SENT - represents waiting for a matching connection requestafter having sent a connection request.
  • SYN-RECEIVED - represents waiting for a confirming connection request acknowledgment after having both received and sent a connection request.
  • ESTABLISHED - represents an open connection, data received can be delivered to the user. The normal state for the data transfer phase of the connection.
  • FIN-WAIT-1 - represents waiting for a connection termination request from the remote TCP, or an acknowledgment of the connection termination request previously sent.
  • FIN-WAIT-2 - represents waiting for a connection termination request from the remote TCP.
  • CLOSE-WAIT - represents waiting for a connection termination request from the local user.
  • CLOSING - represents waiting for a connection termination request acknowledgment from the remote TCP.
  • LAST-ACK - represents waiting for an acknowledgment of the connection termination request previously sent to the remote TCP (which includes an acknowledgment of its connection termination request).
  • TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.
  • CLOSED - represents no connection state at all.

TCP三次握手

img
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sequenceDiagram

participant c as client
participant s as server
participant sq as syns queue
participant aq as accept queue

s ->> s : bind()
s ->> s : listen()
c ->> c : connect()
c ->> s : 1. SYN
Note left of c : SYN_SEND
s ->> sq : put
Note right of s : SYN_RCVD
s ->> c : 2. SYN + ACK
Note left of c : ESTABLISHED
c ->> s : 3. ACK
sq ->> aq : put
Note right of s : ESTABLISHED
aq -->> s :
s ->> s : accept()
  • 注: 上图x表示client_isn(客户端随机初始化的序号),y表示server_isn(服务端随机初始化的序号)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   TCP A                                                TCP B

1. CLOSED LISTEN

2. SYN-SENT --> <SEQ=100><CTL=SYN> --> SYN-RECEIVED

3. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED

4. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK> --> ESTABLISHED

5. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK><DATA> --> ESTABLISHED

Basic 3-Way Handshake for Connection Synchronization

Figure 7.
  • 使用TCP协议进行通信的双方必须先建立连接,然后才能开始传输数据.为了确保连接双方可靠性,在双方建立连接时,TCP协议采用了三次握手策略;

握手过程

  • 一开始,客户端和服务端都处于CLOSED状态.先是服务端主动鉴定某个端口,处于LISTEN状态.

  • 客户端会随机初始化序号(client_isn),将此序号置于TCP首部的序号字段中,同时把SYN标志位置为1,表示SYN报文.接着把第一个SYN报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT状态

    img

  • 服务端收到客户端的SYN报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入TCP首部的序号字段,其次把TCP首部的确认应答号字段填入client_isn + 1,接着把SYNACK标志位置为1.最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD状态.

    img
  • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文TCP首部ACK标志位置为1,其次确认应答号字段填入server_isn + 1,最后把报文发送给服务端,这次报文可以携带客户端到服务器的数据,之后客户端处于ESTABLISHED状态.

    img
  • 服务器收到客户端的应答报文后,也进入ESTABLISHED状态.

  • 上述过程中可以发现第三次握手是可以携带数据的,前两次握手是不可用携带数据的.

  • 一旦完成三次握手,双方处于ESTABLISHED状态,此时连接就已建立完成.客户端和服务端可以相互发送数据了.

  • 可以使用netstat -napt查看TCP连接状态.

采用三次握手的原因
三次握手才能保证双方具有接收和发送的能力.
三次握手才可以阻止重复历史连接的初始化(主要原因).
  • 为了防止旧的重复连接初始化造成混乱.
  • 三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文判断当前连接是否是历史连接:
    • 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是RST报文,以此中止历史连接.
    • 如果不是历史连接,则第三次发送的报文是ACK,通信双方就会成功建立连接.
三次握手才可以同步双方的初始序列号.
  • TCP协议的通信双方,都必须维护一个序列号,序列号时可靠传输的一个关键因素,它的作用:
    • 接收方可以去除重复的数据.
    • 接收方可以根据数据包的序列号按序接收.
    • 可以标识发送出去的数据包中,哪些是已经被对方收到的.
  • 序列号在TCP连接中占据着非常重要的作用,所以当客户端发送携带初始化序列号的SYN报文的时候,需要服务端回一个ACK应答报文,表示客户端的SYN报文已被服务端成功接收,那当服务端发送初始序列号给客户端的时候,依然也要得到客户端的应答,这样一来一回,才能确保双方的初始化序列号能被可靠的同步.
三次握手才可以避免资源浪费
总结
  • 三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号.序列号能保证数据包不重复,不丢弃和按序传输.
  • 两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号.
  • 四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数.

SYN攻击

  • TCP连接建立是需要三次握手,假设攻击者短时间伪造不同IP地址的SYN报文,服务端每接收到一个SYN报文,就进入SYN_RCVD状态,但服务端发送出去的ACK + SYN报文,无法得到未知IP主机的ACK应答,久而久之就会占满服务端的SYN接收队列(未连接队列),使得服务器不能为正常用户服务.
避免SYN攻击方式
  • 修改Linux内核参数,控制队列大小和当队列满时应做什么处理.

    1
    2
    3
    4
    5
    6
    # 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包.控制该队列的最大值
    net.core.netdev_max_backlog
    # SYN_REVD状态连接的最大个数
    net.ipv4.tcp_max_syn_backlog
    # 超出处理能力时,对新的SYN直接回复RST,丢弃连接
    net.ipv4.tcp_abort_onoverflow
  • Linux内核的SYN(未完成连接建立)队列与Accpet(已完成连接建立)队列如何工作的?

    • 正常流程

      • 当服务端收到客户端的SYN报文时,会将其加入到内核的SYN队列;
      • 接着发送SYN+ACK给客户端,等待客户端回应ACK报文;
      • 服务端接收到ACK报文后,从SYN队列移除放入到Accept队列;
      • 应用通过调用accept() socket接口,从Accpet队列取出连接.
    • 应用程序过慢

      • 如果应用程序过慢时,就会导致Accpet队列被占满.
    • 受到SYN攻击

      • 如果不断受到SYN攻击,就会导致SYN队列被占满

        1
        2
        # tcp_syncookies的方式可以应对SYN攻击的方法
        net.ipv4.tcp_syncookies = 1
    • SYN队列占满,启动cookie

      • SYN队列满之后,后续服务器收到SYN包,不进入SYN队列
      • 计算出一个cookie值,再以SYN + ACK中的序列号返回客户端
      • 服务端收到客户端的应答报文时,服务器会检查这个ACK包的合法性.如果合法,直接放入到Accpet队列.
      • 最后应用通过accpet()socket接口,从Accpet队列取出连接

TCP四次挥手

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
         
TCP A TCP B

1. ESTABLISHED ESTABLISHED

2. (Close)
FIN-WAIT-1 --> <SEQ=100><ACK=300><CTL=FIN,ACK> --> CLOSE-WAIT

3. FIN-WAIT-2 <-- <SEQ=300><ACK=101><CTL=ACK> <-- CLOSE-WAIT

4. (Close)
TIME-WAIT <-- <SEQ=300><ACK=101><CTL=FIN,ACK> <-- LAST-ACK

5. TIME-WAIT --> <SEQ=101><ACK=301><CTL=ACK> --> CLOSED

6. (2 MSL)
CLOSED

Normal Close Sequence

Figure 13.
  • 主动关闭连接的一方,调用close()协议层发送FIN
  • 被动关闭的一方收到FIN包后,协议层回复ACK;然后被动关闭的一方,进入CLOSE_WAIT状态,主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方等待被动关闭一方的应用程序,调用close操作
  • 被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态;
  • 主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态
  • 等待2MSL时间,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态

MSL

  • Maximum Segment Lifetime,报文最大生存时间
  • 它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃.因为TCP报文基于是IP协议的,而IP头中有一个TTL域,是IP数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机.协议规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等.
  • 2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用.这个连接只能在2MSL结束后才能再被使用

结论

  • 主动关闭连接的一方A,被动关闭连接的一方B - A 主动调用socketclose操作的一方,最终会进入TIME_WAIT状态

    • A发送FIN包后进入FIN_WAIT_1状态,B收到FIN包,回复ACK包后会进入CLOSED_WAIT
    • A收到B的ACK包后进入FIN_WAIT_2状态,若此时B挂了,则A将永远处于FIN_WAIT_2状态,TCP协议里面并没有对这个状态的处理,但是Linux有,可以调整tcp_fin_timeout这个参数,设置一个超时时间.
  • 被动关闭连接的一方B,有一个中间状态,即CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用close操作后才主动关闭这条连接

  • TIME_WAIT会默认等待2MSL时间后,才最终进入CLOSED状态;

  • 在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的!

img

TCP连接状态转换图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
                             +---------+ ---------\      active OPEN  
| CLOSED | \ -----------
+---------+<---------\ \ create TCB
| ^ \ \ snd SYN
passive OPEN | | CLOSE \ \
------------ | | ---------- \ \
create TCB | | delete TCB \ \
V | \ \
+---------+ CLOSE | \
| LISTEN | ---------- | |
+---------+ delete TCB | |
rcv SYN | | SEND | |
----------- | | ------- | V
+---------+ snd SYN,ACK / \ snd SYN +---------+
| |<----------------- ------------------>| |
| SYN | rcv SYN | SYN |
| RCVD |<-----------------------------------------------| SENT |
| | snd ACK | |
| |------------------ -------------------| |
+---------+ rcv ACK of SYN \ / rcv SYN,ACK +---------+
| -------------- | | -----------
| x | | snd ACK
| V V
| CLOSE +---------+
| ------- | ESTAB |
| snd FIN +---------+
| CLOSE | | rcv FIN
V ------- | | -------
+---------+ snd FIN / \ snd ACK +---------+
| FIN |<----------------- ------------------>| CLOSE |
| WAIT-1 |------------------ | WAIT |
+---------+ rcv FIN \ +---------+
| rcv ACK of FIN ------- | CLOSE |
| -------------- snd ACK | ------- |
V x V snd FIN V
+---------+ +---------+ +---------+
|FINWAIT-2| | CLOSING | | LAST-ACK|
+---------+ +---------+ +---------+
| rcv ACK of FIN | rcv ACK of FIN |
| rcv FIN -------------- | Timeout=2MSL -------------- |
| ------- x V ------------ x V
\ snd ACK +---------+delete TCB +---------+
------------------------>|TIME WAIT|------------------>| CLOSED |
+---------+ +---------+

TCP Connection State Diagram

img

img

  • 在这个图(来自于趣谈网络协议)中,加黑加粗的部分,是上面说到的主要流程,其中阿拉伯数字的序号,是连接过程中的顺序,而大写中文数字的序号,是连接断开过程中的顺序.加粗的实线是客户端A的状态变迁,加粗的虚线是服务端B的状态变迁.

TIME_WAIT状态

  • TIME_WAIT状态也被称为2MSL等待状态.在该状态将会等待两倍于最大段生存期(Maximum Segment Lifetime,MSL)的时间

    • TIME_WAIT等待2倍的MSL的原因为:网络中可能存在来自发送的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回响应等待2倍的时间
    • 2MSL的时间是从客户端接收到FIN后发送ACK开始计时的.如果在TIME_WAIT时间内,因为客户端的ACK没有传输到服务端,客户端又接收到了服务端重发的FIN报文,那么2MSL时间将重新计时.
  • 在Linux系统/proc/sys/net/ipv4/tcp_fin_timeout 文件中,net.ipv4.tcp_fin_timeout的数值记录了2MSL状态需要等待的超时时间(以秒为单位)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    net.ipv4.tcp_fin_timeout = 2
    # 为1表示允许将TIME-WAIT的句柄重新用于新的TCP连接
    net.ipv4.tcp_tw_reuse = 1
    # 为1表示开启TCP连接中TIME-WAIT的快速回收,NAT环境可能导致DROP掉SYN包(回复RST)
    net.ipv4.tcp_tw_recycle = 1
    # 为1时SYN Cookies,当SYN等待队列溢出时启用cookies来处理,可防范少量SYN攻击
    net.ipv4.tcp_syncookies = 1
    # 端口最大backlog内核限制,防止占用过大内核内存
    net.ipv4.ip_local_port_range = 4000 65000
    net.ipv4.tcp_max_syn_backlog = 16384
    # 保持TIME_WAIT套接字的最大个数,超过这个数字TIME_WAIT套接字将立刻被清除并打印警告信息
    net.ipv4.tcp_max_tw_buckets = 36000
    net.ipv4.route.gc_timeout = 100
    # 对一个新建连接,内核要发送多少个SYN连接请求才决定放弃,不应该大于255
    net.ipv4.tcp_syn_retries = 1
    net.ipv4.tcp_synack_retries = 1
    net.core.somaxconn = 16384
    net.core.netdev_max_backlog = 16384
    # 不属于任何进程(已经从进程上下文中删除)的sockets最大个数,超过这个值会被立即RESET,并同时显示警告信息
    net.ipv4.tcp_max_orphans = 16384
  • 假设已设定MSL的数值,按照规则:当TCP执行一个主动关闭并发送最终的ACK时,连接必须处于TIME_WAIT状态并持续两倍于最大生存周期(2MSL)的时间.这样能够让TCP重新发送最终的ACK以避免出现丢失的情况.重新发送最终的ACK并不是因为重传了ACK(它们并不消耗序列号,也不会被TCP重传),而是因为通信另一方重传了它的FIN(它消耗一个序列号).事实上,TCP总是重传FIN,直到它收到一个最终的ACK.

  • 影响2MSL等待状态的因素是当TCP处于等待状态时,通信双方将该连接(客户端IP地址,客户端端口号,服务器IP地址,服务器端口号)(四元组)定义为不可重新使用.只有当2MSL等待结束时,或一条新连接使用的初始化序列号超过了连接之前的实例使用的最高序列号时,或者允许使用时间戳选项来区分之前连接实例的报文段以避免混淆时,这条连接才能被再次使用.不幸的是,一些实现施加了更加严格的约束,在这些系统中,如果一个端口号被处于2MSL等待状态的任何通信端所用,那么该端口号将不能被再次使用.

  • 使用ss -tan state time-wait | wc -l 查看TIME_WAIT的连接数量

为什么需要TIME_WAIT状态

  • 主动发起关闭连接的一方,才会有TIME_WAIT状态
  • 需要TIME_WAIT状态,主要是两个原因:
    • 防止具有相同四元组的旧数据包被收到;
      • 经过2MSL这个时间,足以让两个方向的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的.
    • 保证被动关闭的连接的一方被正确关闭,即保证最后的ACK能被动关闭接收,从而帮助其正常关闭;

TIME_WAIT过多的危害

  • 第一是内存资源占用
  • 第二是对端口资源的占用,一个TCP连接至少消耗一个本地端口
    • 一般可以开启的端口为32768~61000,也可以通过net.ipv4.ip_local_port_range
  • 如果发起连接一方的TIMET_WAIT状态过多,占满了所有端口资源,则会导致无法创建新连接.
    • 客户端手端口资源限制:
      • 客户端TIME_WAIT过多,就会导致端口资源被占用,因为端口就65536个,被占满就会导致无法创建新的连接.
    • 服务端受系统资源限制:
      • 由于一个四元组标志TCP连接,理论上服务端可以建立很多连接,服务端确实只监听一个端口,但是会把连接扔给线程,所以理论上监听的端口可以继续监听.但是线程池处理不了那么多一直不断的连接了.所以当服务端出现大量TIME_WAIT时,系统资源被占满时,会导致不过来新的连接.

优化TIME_WAIT

  • 打开net.ipv4.tcp_tw_reusenet.ipv4_tcp_timestamps选项;
  • net.ipv4.tcp_max_tw_buckets
    • 这个值默认为18000,当系统中处于TIME_WAIT的连接一旦超过这个值时,系统就会将后面的TIME_WAIT连接重置.
  • 程序中使用SO_LINGER,应用强制使用RST关闭

net.ipv4.tcp_tw_reusenet.ipv4.tcp_timestamps

  • 开启net.ipv4.tcp_tw_reuse = 1 ,可以复用处于TIME_WAIT的socket为新的连接所用
    • 注: tcp_tw_reuse功能只能用于客户端(连接发起方),因为开启了该功能,在调用connect()函数时,内核会随机找一个time_wait状态超过1秒的连接给新的连接复用;解决的是accpet后的问题.
    • 使用这个选项,还有个前提,需要打开TCP时间戳的支持,即net.ipv4.tcp_timestamps = 1(默认即为1)
    • 这个时间戳的字段是在TCP头部的选项里,用于记录TCP发送方的当前时间戳和从对端接收到的最新时间戳
    • 由于引入了时间戳,之前的2MSL问题就不存在了,因为重复的数据包会因为时间戳过期被自然丢弃.
  • SO_REUSEADDR是用户态的选项,用于连接的服务方,用来告诉操作系统内核,如果端口已被占用,但是TCP连接状态位于TIME_WAIT,可以重用端口.如果端口忙,而TCP处于其他状态,重用会有"Address already in use" 的错误信息.
    • SO_REUSEADDR是为了解决TIME_WAIT状态带来的端口占用问题,以及支持同一个port对应多个IP,解决的是bind时的问题.

如果已建立了连接,但是客户端突然出现故障了怎么办?

  • TCP有个保活机制.这个机制的原理为:

    定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP报活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的TCP连接已经死亡,系统内核将错误信息通知给上层应用程序.

  • 在Linux内核可以有对应的参数可以设置保活时间,保活探测的次数,保活探测的时间间隔,一下都为默认值:

    1
    2
    3
    4
    5
    6
    # 表示保活时间是7200秒,单位为秒,缺省是7200秒(即2小时),也就是2个小时内如果没有任何连接相关的活动,则会启动保活机制
    net.ipv4.tcp_keepalive_time = 7200
    # 表示每次检测间隔为75秒
    net.ipv4.tcp_keepalive_intvl=75
    # 表示检测9次无响应,认为对方是不可达的,从而终端本次的连接.
    net.ipv4.tcp_keepalive_probes=9
    • 也就是说在Linux中,最少需要经过2小时11分15秒才能发现一个死亡连接tcp_keepalive_time + (tcp_keepalive_intvl * tcp_keepalive_probes)

TCP拆包和粘包

  • 在传输层封包不能太大.这种限制,往往是以缓冲区大小为单位的.也就是 TCP 协议,会将数据拆分成不超过缓冲区大小的一个个部分.每个部分有一个独特的名词,叫作 TCP 段(TCP Segment).

  • 在接收数据的时候,一个个TCP段又被重组成原来的数据

  • 数据经过拆分,然后传输,然后在目的地重组,俗称拆包.所以拆包是将数据拆分成多个 TCP 段传输

  • 如果发往一个目的地的多个数据太小了,为了防止多次发送占用资源,TCP 协议有可能将它们合并成一个 TCP 段发送,在目的地再还原成多个数据,这个过程俗称粘包.所以粘包是将多个数据合并成一个 TCP 段发送.

  • TCP 拆包的作用是将任务拆分处理,降低整体任务出错的概率,以及减小底层网络处理的压力.拆包过程需要保证数据经过网络的传输,又能恢复到原始的顺序.这中间,需要数学提供保证顺序的理论依据.TCP 利用(发送字节数、接收字节数)的唯一性来确定封包之间的顺序关系.

MTUMSS

img
  • MTU(Maxinmum Transmission Unit) 最大传输单元:一个网络包的最大长度,以太网中一般为1500字节.
    • 当IP层数据超过MTU大小要发送,则IP层就要进行分片,把数据分片成
  • MSS(Maximun Segment Size) 最大段大小:除去IP和TCP头部之后,一个网络包所能容纳的TCP数据的最大长度.
    • 当TCP层发现数据超过MSS时,则就先会进行分片
    • 当一个TCP分片丢失后,进行重发时也是以MSS为单位,而不用重传所有的分片,大大增加了重传的效率.
  • MSS是 TCP 段,或者称为 TCP 分组(TCP Packet)的最大大小.MSS 是传输层概念,MTU 是链路层概念.

Socket编程

img
  • 服务端和客户端初始化socket,得到文件描述符;
  • 服务端调用bind绑定IP地址和端口;
  • 服务端调用listen,进行监听;
  • 服务端调用accept,等待客户端连接;
  • 客户端调用connect,向服务器端的地址和端口发起连接请求;
  • 客户端调用write写入数据;服务端调用read读取数据;
  • 客户端断开连接时会调用close,服务端read读取数据的时候,就会读取到了EOF,待处理完数数据后,服务端调用close,表示连接关闭.
  • 注: 当服务端调用accept时,连接成功了会返回一个已完成连接的socket,后续用来传输数据.所以,监听的socket和真正用来传送数据的socket,是两个socket,一个叫做监听socket,一个叫做已完成连接socket,成功建立连接之后,双方开始通过read和write函数来读写数据,就像往一个文件流里面写东西.

listen时候参数backlog的意义

  • Linux内核中会维护两个队列

    • 未完成连接队列(SYN队列):接收到一个SYN建立连接请求,处于SYN_RCVD状态;

    • 已完成连接队列(Accpet队列): 已完成TCP三次握手过程,处于ESTABLISHED状态;

      img
      • 在早期Linux内核backlogSYN队列大小,也就是未完成的队列大小.
      • 在Linux内核2.2之后,backlog编程accpet队列,也就是已完成连接建立的队列长度,所以通常认为backlog是accpet队列,但是上限值是内核参数somaxconn的大小,也就是说accpet队列长度=min(backlog,somaxconn)

accpet发生在三次握手哪一步?

img
  • 客户端的协议栈向服务器端发送了SYN包,并告诉服务器端当前发送序列号client_isn,客户端进入SYN_SENT状态;
  • 服务端的协议栈收到这个包,和客户端进行ACK应答,应答值为client_isn+1,表示SYNclient_isn的确认,同时服务器也发送SYN包,告诉客户端当前我的发送序列号为server_isn,服务端进入SYN_RCVD状态;
  • 客户端协议栈收到ACK之后,使得应用程序从connect调用返回,表示客户端到服务端的单向连接建立成功,客户端的状态为ESTABLISHED,同时客户端协议栈也会对服务器端的SYN包进行应答,应答数据为server_isn+1;
  • 应答包达到服务器端后,服务器端协议栈使得accpet阻塞调用返回,这个时候服务端到客户端的单向连接也建立成功,服务器端也进入ESTABLISHED状态.
  • 客户端connect成功返回是第二次握手,服务端accpet成功返回在三次握手成功后.

客户端调用了close了,连接断开的流程是什么?

img
  • 客户端调用close,表明客户端没有数据需要发送了,则此时会向服务端发送FIN报文,进入FIN_WAIT_1状态;
  • 服务端接收到了FIN报文,TCP协议栈会为FIN包插入一个文件结束符EOF到接收缓冲区中,应用程序可以通过read调用感知这个FIN包.这个EOF会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为EOF表示在该连接上再无额外数据到达.此时服务端进入CLOSE_WAIT状态.
  • 接着,当处理完成数据后,自然就会读到EOF,于是也调用close关闭它的套接字,这回使得客户端发出一个FIN包,之后处于LAST_ACK状态;
  • 客户端接收到服务端的FIN包,并发送ACK确认包给服务端,此时客户端将进入TIME_WAIT状态;
  • 服务端收到ACK确认包,就进入最后的CLOSE状态
  • 客户端进过2MSL时间之后,也进入CLOSE状态

TCPUDP的区别

连接

  • TCP是面向连接的传输层协议,传输数据前先要建立连接.
  • UDP是不需要连接,即刻传输数据.

服务对象

  • TCP是一对一的两点服务,即一条连接只有两个端点.
  • UDP支持一对一,一对多,多对多的交互通信.

可靠性

  • TCP是可靠交付数据的,数据可以无差错,不丢失,不重复,按需到达.
  • UDP是尽最大努力交付,不保证可靠交互数据.

拥塞控制,流量控制

  • TCP有拥塞控制和流量控制机制,保证数据传输的安全性.
  • UDP则没有,即使网络非常拥堵了,也不会影响UDP的发送速率.

首部开销

  • TCP首部长度较长,会有一定的开销,首部在没有使用选项字段时是20个字节,如果使用了选项字段则会变长的.
  • UDP首部只有8个字节,并且是固定不变的,开销较小.

传输方式

  • TCP是流式传输,没有边界,保证顺序和可靠.
  • UDP是一个包一个包的发送,是有边界的,但可能会丢包和乱序.

分片不同

  • TCP的数据大小如果大于MSS大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装TCP数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片.
  • UDP的数据大小如果小于MTU大小,则会在IP层进行分片,目标主机收到后,在IP层组装完数据,接着再传给传输层,但是如果中途丢失了一分片,在实现可靠传输的UDP时则就需要重传所有的数据包,这样传输效率非常差,所有通常UDP的报文应该小于MTU.

TCPUDP应用场景

  • 由于TCP是面向连接,能保证数据的可靠性交付,因此长用于
    • FTP文件传输
    • HTTP/HTTPS
  • 由于UDP面向无连接,它可以随时发送数据,再机上UDP本身的处理既简单又高效,因此常用于:
    • 包总量较少的通信,如DNS,SNMP等
    • 视频,音频等多媒体通信
    • 广播通信