Eli's Blog

1. TCP

a

TCP 是面向连接、可靠的、基于字节流传输层通信协议。

  • 面向连接:一对一的连接。不像 UDP 可以同时向多个主机发送消息。
  • 可靠的:网络链路中出现变化,TCP 可以保证一个报文一定能够到达指定端。

1.1 TCP 头

a

序列号:建立连接时,计算机生成的随机数作为初始值,通过 SYN 包传给接收端主机,每发送一次数据,就累加 1。解决网络包乱序问题

确认应答号:下一次“期望”收到的数据的序列号,发生端收到这个确认应答后,认为这个序列号以前的数据都被正确接收。解决丢包问题

控制位

  • ACK:确认应答字段有效。除了最初建立连接时的 SYN 包之外该位必须为1
  • RST:TCP 连接中出现异常,必须强行断开连接
  • SYN:希望建立连接,在其“序列号”字段初始化后设定
  • FIN:通信结束,断开连接,不会再发送数据时设定

1.2 TCP 连接

用于保证可靠性和流量控制维护的某些状态信息,包括Socket、序列号和窗口大小称为连接

a

  • Socket:IP + Port
  • 序列号:解决乱序等问题
  • 窗口大小:流量控制

1.3 唯一确定一个连接

通过 TCP 四元组来确定:

  • 源地址
  • 源端口
  • 目标地址
  • 目标端口

源地址和目标地址 (32-bit):在 IP 头部中,通过 IP 协议发送报文给对方主机

源端口和目标端口 (16-bit):在 TCP 头部中,通过 TCP 协议把报文发给哪一个进程。

1.4 三次握手 & 四次挥手

tcp

TCP 状态:

  • LISTENING: 服务端侦听远端TCP连接请求,等待被连接
  • SYN_SENT: 客户端调用connect方法,发送一个SYN请求建立连接
  • SYN_RCVD: 服务端收到连接请求并确认后,调用accept方法
  • ESTABLISHED: 连接建立
  • FIN_WAIT_1: 主动关闭连接,调用close方法后
  • CLOSING: FIN_WAIT_1后,等待对端关闭确认 (较少出现)
  • CLOSE_WAIT: 收到关闭请求,等待关闭
  • FIN_WAIT_2: 收到关闭ACK确认后
  • LAST_ACK: 收到关闭请求(CLOSE_WAIT)后,被动关闭连接,调用close方法
  • TIME_WAIT: 主动关闭连接,收到被动关闭连接(LAST_ACK)后。等待足够的时间,确保远程TCP连接中断确认,最大程度保证双方正常结束,需等待2*MSL时间才能进行下一次连接
  • CLOSED: 被动关闭端收到ACK后,进入CLOSED,连接结束

总结:

TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。

不使用「两次握手」和「四次握手」的原因:

  • 两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
  • 四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

1.4.1 TIME_WAIT

主动关闭Socket端会进入TIME_WAIT状态,并持续2MSL时间长度。

MSL (maximum segment lifetime):表示一个IP数据包在互联网上生存的最长时间,超过这个时间将在网络中消失。MSL建议值为2分钟,但传统上为30s

因此,TIME_WAIT状态一般维持在1-4分钟

TIME_WAIT 作用:

  • 可靠地实现TCP全双工连接终止

  • 允许老的重复连接在网络中消逝

TIME_WAIT 危害:

  • 过多会占用内存,一个TIME_WAIT占用4k

  • 网络差的情况下,如果主动方无TIME_WAIT等待,关闭当前连接后,主动方与被动方又重新建立新的TCP连接,此时被动方重传或延时过来的FIN包会直接影响当前新的TCP连接

如何避免:

  • 设置socket选项为SO_REUSEADDR,端口可重用

  • 由于TIME_WAIT状态是主动关闭一方出现的,所以在协议逻辑设计时,尽量由客户端主动关闭,避免服务端出现TIME_WAIT

1.4.2 SYN 攻击

什么是SYN攻击?

  • 在三次握手过程中,收到客户端SYN,服务端ACK该请求后进入SYN_RCVD状态,该状态称为半连接(half-open connect),只有等服务端收到ACK再次确认后,才进入ESTABLISHED状态
  • SYN 攻击,即客户端在短时间内大量伪造不存在的IP地址,向服务端不断地发送SYN包,服务端回复ACK确认包,并等待客户端确认。但由于源地址不存在,服务端需要不断重发ACK包直至超时,大量SYN包长时间占用未连接队列,导致正常SYN请求被丢弃,网络阻塞服务不可用。
  • DoS/DDoS 是一种典型的SYN攻击

attack

如何检测 SYN 攻击?

  • 服务器上存在大量半连接状态 (SYN_RCVD)
  • 大量随机的源 IP 地址

如何预防 SYN 攻击?

完全阻止SYN攻击是不可能的,可通过一些方法减轻SYN攻击:

  • 缩短超时时间(SYN Timeout)
  • 增加最大半数连接数
  • 过滤网关防护
  • SYN cookies 技术

Linux 内核参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 队列最大值
net.core.netdev_max_backlog

# SYN_RCVD 状态连接的最大个数
net.ipv4.tcp_max_syn_backlog

# 超出处理能时,对新的 SYN 直接回报 RST,丢弃连接
net.ipv4.tcp_abort_on_overflow

# 启用 cookie
net.ipv4.tcp_syncookies = 1

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

1.5 KeepAlive

TCP数据交互完成后,未主动释放连接,在无法知道对端的情况下保持了这个连接,长时间累积导致非常多的半打开连接,造成系统资源浪费。

KeepAlive: 隔一段时间给对端发送一个探测包,如果对方回应ACK,则认为连接还是存活的。在超过一定重试次数之后还是未收到对方的回应,则丢弃该连接。

1.6 如何实现长连接

  • HeartBeat心跳包

    客户端每隔一小段时间向服务器发送一个数据包,通知服务器自己仍然在线。30s 00 00 03

  • TCP协议的KeepAlive机制

    默认不打开,要用setsockopt将SOL_SOCKET.SO_KEEPALIVE设置为1,并且设置参数tcp_keepalive_time/tcp_keepalive_probes/tcp_keepalive_intvl

    keep-alive机制,可以减少tcp连接建立的次数,也意味着减少TIME_WAIT连接状态,以此来提高服务器性能

    但keep-alive也可能导致系统资源被无效占用,合适设置keep-alive timeout时间非常重要

1.7 滑动窗口

滑动窗口(Sliding window)是一种流量控制技术,它被用来改善网络吞吐量,即容许发送方在接收任何应答之前传送附加的包,接收方告诉发送方在某一个时刻能送多少包(成为窗口尺寸)

让发送的每一个包都有一个id,接收端必须对每一个包进行确认,这样设备A一次多发送几个片段,而不必等候ACK,同时接收端也要告知它能够收多少,这样发送端发起来也有个限制,当然还需要保证顺序性,不要乱序,对于乱序的状况,我们可以允许等待一定情况下的乱序,比如说先缓存提前到的数据,然后去等待需要的数据,如果一定时间没来就DROP掉,来保证顺序性!

接收端可以根据自己的状况通告窗口大小,从而控制发送端的发送,进行流量控制。

sliding_window

滑动窗口原理:

  • TCP并不是每一个报文段都会回复ACK确认,可能会对多个报文段发送1个ACK (累积ACK确认)。

  • 比如发送方有1/2/3个报文段,接收方收到2/3报文段后,一直未收到报文段“1”,将会丢弃报文段2/3.

实现滑动窗口:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
var (
limitCount int32 = 10 // 限频总数
limitBucket int = 6 // 滑动窗口个数
curCount int32 = 0 // 当前限频数量
head *ring.Ring // 环形队列 (链表)
)

func main() {
addr, err := net.ResolveTCPAddr("tcp4", ":3000")
if err != nil {
log.Fatal(err)
}

listener, err := net.ListenTCP("tcp", addr)
if err != nil {
log.Fatal(err)
}
defer listener.Close()

// 初始化滑动窗口
head = ring.New(limitBucket)
for i := 0; i < limitBucket; i++ {
head.Value = 0
head = head.Next()
}

// 启动执行器
go func() {
ticker := time.NewTicker(time.Second)
for {
select {
case <-ticker.C:
subCount := int32(0 - head.Value.(int))
newCount := atomic.AddInt32(&curCount, subCount)

// useless, only for print
arr := [6]int{}
for i := 0; i < limitBucket; i++ {
arr[i] = head.Value.(int)
head = head.Next()
}
fmt.Printf("subCount: %d, newCount: %d, arr: %v\n", subCount, newCount, arr)

head.Value = 0
head = head.Next()
}
}
}()

// 处理请求
for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}

go handle(&conn)
}
}

func handle(conn *net.Conn) {
defer (*conn).Close()

count := atomic.AddInt32(&curCount, 1)
if count > limitCount {
atomic.AddInt32(&curCount, -1)
msg := "HTTP/1.1 404 NOT FOUND\r\n\r\nError, too many request, please try later."
(*conn).Write([]byte(msg))
} else {
mu := sync.Mutex{}
mu.Lock()
pos := head.Prev()
val := pos.Value.(int)
val++
pos.Value = val
mu.Unlock()

time.Sleep(time.Second)
msg := "HTTP/1.1 200 OK\r\n\r\nWell done."
(*conn).Write([]byte(msg))
}
}

使用HTTP压测工具hey:

https://github.com/rakyll/hey

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
hey -c 6 -n 300 -q 6 -t 80 http://localhost:3000

Summary:
Total: 11.6708 secs
Slowest: 1.0423 secs
Fastest: 0.0013 secs
Average: 0.0735 secs
Requests/sec: 25.7051


Response time histogram:
0.001 [1] |
0.105 [279] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.210 [0] |
0.314 [0] |
0.418 [0] |
0.522 [0] |
0.626 [0] |
0.730 [0] |
0.834 [0] |
0.938 [0] |
1.042 [20] |■■■


Latency distribution:
10% in 0.0033 secs
25% in 0.0052 secs
50% in 0.0065 secs
75% in 0.0076 secs
90% in 0.0091 secs
95% in 1.0066 secs
99% in 1.0417 secs

Details (average, fastest, slowest):
DNS+dialup: 0.0052 secs, 0.0013 secs, 1.0423 secs
DNS-lookup: 0.0036 secs, 0.0002 secs, 0.0359 secs
req write: 0.0001 secs, 0.0000 secs, 0.0023 secs
resp wait: 19.8696 secs, 0.0001 secs, 851.1748 secs
resp read: 0.0002 secs, 0.0000 secs, 0.0023 secs

Status code distribution:
[200] 20 responses
[404] 280 responses

1.8 MTU & MSS

a

MTU: 一个网络包的最大长度,以太网一般未 1500 字节

MSS:除去 IP 和 TCP 头部后,一个网络包能容纳的 TCP 数据的最大长度

当 IP 层有一个超过 MTU 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,再交给上一层 TCP 传输层。

但是,如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。

因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。

当接收方发现 TCP 报文(头部 + 数据)的某一片丢失后,则不会响应 ACK 给对方,那么发送方的 TCP 在超时后,就会重发「整个 TCP 报文(头部 + 数据)」。

因此,可以得知由 IP 层进行分片传输,是非常没有效率的。

所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。

a

经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。

2. UDP

a

  • 目标和源端口:告诉 UDP 协议应该把报文发给哪个进程。
  • 包长度: UDP 首部的长度跟数据的长度之和。
  • 校验和:提供可靠的 UDP 首部和数据而设计。

UDP是一个简单的传输层协议,与TCP相比,有如下特征:

  • UDP 缺乏可靠性。不提供确认、序列号、超时重传等机制。
  • UDP 数据报可能在网络中被复制,被重新排序。即UDP不保证数据一定到达目的地,也不保证数据报的先后顺序,也不保证每个数据报只到达一次
  • UDP 数据报有长度的。如果一个数据报正确地到达目的地,该数据报的长度也随着随着数据一起传给了接收方。
  • UDP 面向无连接的。UDP客户端与服务器不存在长期关系,不需要经过三次握手和四次挥手操作
  • UDP支持多播和广播

3. TCP vs UDP

连接 协议 可靠性 使用场景
TCP 面向连接 流协议,无大小限制 可靠 可靠的通信。使用校验和、确认和重传机制来确保可靠传输
UDP 无连接 数据包协议,有限制 不可靠 1. 包总量较小的通信(DNS, SNMP) 2.视频、音频等流媒体(即时通信)3.广播通信

tcp 传输的是数据流,udp是数据包;tcp要进行三次握手、udp不需要

TCP 和 UDP 区别:

1. 连接

  • TCP 面向连接,传输数据前先要建立连接。
  • UDP 不需要连接,即刻传输数据。

2. 服务对象

  • TCP 是一对一的两点服务。
  • UDP 支持一对一、一对多、多对多的交互通信

3. 可靠性

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

4. 拥塞控制、流量控制

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

5. 首部开销

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

6. 传输方式

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

7. 分片不同

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

TCP 和 UDP 应用场景:

由于 TCP 是面向连接,能保证数据的可靠性交付,常用于:

  • FTP 文件传输
  • HTTP / HTTPS

由于 UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,常用于:

  • 包总量较少的通信,如 DNSSNMP
  • 视频、音频等多媒体通信
  • 广播通信

为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?

原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度。

为什么 UDP 头部有「包长度」字段,而 TCP 头部则没有「包长度」字段呢?

TCP 负载数据长度:

1
TCP数据总长度 = IP总长度 - IP首部长度 - TCP首部长度

4. 网络包

a