谁拔了我的网线?

Go网络异常对程序行为的影响

Go编写网络程序非常的高效,而且有是那么的简单,寥寥几行代码就可以写一个ECHO协议的程序,所以现在很多网络程序都采用Go语言开发。但是网络状况是复杂的,会有很多的异常状况,如果不能很好和正确的处理这些异常状况,会导致网络程序出现莫名其妙的现象,或者hang住。

本文尝试探讨几种网络异常的情况,研究在这些情况下客户端和服务端的的行为,包括连接断掉的检测能力、half-close情况下两端的读写能力、丢包的情况等等。

这是我首次采用微课的方式分享技术内容,本文是视频内容的整理版。 本来是想录制一个10分钟的视频,一不小心录制了半小时。



TCP 协议介绍

tcp的数据格式包含header和payload, header中会包含消息的状态,比如我们常见的SYNACKPSHFIN等。通过 tcpdump可以根据消息的状态进行筛选。

握手

客户端和服务器端建立连接的时候,需要三路握手。

因为双方都需要和对方同步seq号,所以需要来回确认。服务器把SYN和ACK合并成一条消息,所以最终只需要三次交流就可以了。当然如果你想把SYN和ACK拆开成两个消息也可以,只不过协议栈一般不这样实现。

比如你参加一次相亲聚会,看到一个漂亮的姑娘,你想去搭讪,首先得先了解一下。

1
2
3
4
你: 姑娘您好,贵庚啊?
姑娘:小女子18,请问大哥您贵庚啊?
你:我81
......

这样寒暄之后你们双方就可以进一步的深入的交流了。

分手

客户端和服务器端都可以主动关闭连接。主动关闭的一方我们称之为发起者,被动关闭接收的那一方我们称之为接受者。

发起者要关闭连接,需要发送FIN,然后接收者发送ACK。这个时候被动者有可能恋恋不舍,还有数据想发送给你,所以接受者这一端它的连接还没有释放,直到它发送FIN,发起者回复ACK,接收端的连接才释放。

1
2
3
4
5
6
7
姑娘:我要走了
你:再见
......你依依不舍
你:我也要走了
姑娘:再见

tcpdump

tcpdump是分析网络情况的神器,经常用来分析疑难杂症,并且让狡辩者哑口无言。

打印一张tcpdump的小抄放在案头是明智之举。

网络异常状况

视频中,我测试了以下6种网络异常情况下的程序响应情况。

使用的代码基本上是从下面的代码修改而来。

server.go
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
package main
import (
"bufio"
"flag"
"fmt"
"log"
"net"
"os"
)
var (
addr = flag.String("addr", ":8972", "listened address")
)
func main() {
flag.Parse()
ln, err := net.Listen("tcp", *addr)
panicOnErr(err)
// 接收一个连接
conn, err := ln.Accept()
panicOnErr(err)
clientAddr := conn.RemoteAddr().String()
// 读 goroutine
go func() {
var buf = make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
log.Printf("read err from client %s: %v", clientAddr, err)
return
}
log.Printf("read %d bytes from client %s", n, clientAddr)
}
}()
// 写
id := 0
write := func() {
msg := fmt.Sprintf("sent id: %d from server", id)
id++
n, err := conn.Write([]byte(msg))
if err != nil {
log.Printf("write err to client %s: %v", clientAddr, err)
return
}
log.Printf("write %d bytes to client %s", n, clientAddr)
}
// 继续监听新的连接
go func() {
for {
_, err := ln.Accept()
if err != nil {
log.Printf("accept err : %v", err)
}
}
}()
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
cmd := scanner.Text()
switch cmd {
case "close_conn":
conn.Close()
case "close_ln":
ln.Close()
case "write":
write()
case "exit", "quit":
return
}
}
}
func panicOnErr(err error) {
if err != nil {
panic(err)
}
}
client.go
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
package main
import (
"bufio"
"flag"
"fmt"
"log"
"net"
"os"
)
var (
addr = flag.String("addr", "127.0.0.1:8972", "server address")
)
func main() {
flag.Parse()
// 连接服务器
conn, err := net.Dial("tcp", *addr)
panicOnErr(err)
// 读 goroutine
go func() {
var buf = make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
log.Printf("read err from server %s: %v", *addr, err)
return
}
log.Printf("read %d bytes from server %s", n, *addr)
}
}()
// 写
id := 0
write := func() {
msg := fmt.Sprintf("sent clientid: %d from client", id)
id++
n, err := conn.Write([]byte(msg))
if err != nil {
log.Printf("write err to server %s: %v", *addr, err)
return
}
log.Printf("write %d bytes to server %s", n, *addr)
}
// 阻塞在这里避免客户端退出
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
cmd := scanner.Text()
switch cmd {
case "close_conn":
conn.Close()
case "write":
write()
case "exit", "quit":
return
}
}
}
func panicOnErr(err error) {
if err != nil {
panic(err)
}
}

服务器主动关闭连接, 客户端不关闭连接

  • 服务器的 conn.Read会怎样?
  • 服务器继续 conn.Write会怎么样?
  • 客户端的 conn.Read会怎样?
  • 客户端继续 conn.Write会怎么样?

服务器主动关闭连接, 客户端检测到异常后也关闭连接

  • 服务器的 conn.Read会怎样?
  • 服务器继续 conn.Write会怎么样
  • 客户端的 conn.Read会怎样?
  • 客户端继续 conn.Write会怎么样

服务器只关闭Read

  • 服务器的 conn.Read会怎样?
  • 服务器继续 conn.Write会怎么样
  • 客户端的 conn.Read会怎样?
  • 客户端继续 conn.Write会怎么样

服务器只关闭Write

  • 服务器的 conn.Read会怎样?
  • 服务器继续 conn.Write会怎么样
  • 客户端的 conn.Read会怎样?
  • 客户端继续 conn.Write会怎么样

服务器被kill掉

  • 服务器的 conn.Read会怎样?
  • 服务器继续 conn.Write会怎么样
  • 客户端的 conn.Read会怎样?
  • 客户端继续 conn.Write会怎么样

把网线、挖光纤、雷暴机房、防火墙始乱终弃

只分析其中一种情况: 包丢了

  • 服务器的 conn.Read会怎样?
  • 服务器继续 conn.Write会怎么样
  • 客户端的 conn.Read会怎样?
  • 客户端继续 conn.Write会怎么样