IT业界:谁拔了我的网线?Go 网络异常对程序行为的影响

    作者:课课家教育更新于: 2020-06-16 14:26:16

    Go语言是谷歌2009发布的第二款开源编程语言。

    Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。

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

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

    这是我首次采用微课的方式分享技术内容,本文是视频内容的整理版。本来是想录制一个 10 分钟的视频,一不小心录制了半小时。视频在此:https://player.bilibili.com/player.html?aid=80946416&cid=138546780&page=1

    TCP 协议介绍

    谁拔了我的网线?Go 网络异常对程序行为的影响

     

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

     

     

    握手

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

     

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

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

    你:姑娘您好,贵庚啊?

     

    姑娘:小女子18,请问大哥您贵庚啊?

     

    你:我81了

     

    ......

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

    分手

     

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

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

    姑娘:我要走了

     

    你:再见

     

     

    ......你依依不舍

     

     

    你:我也要走了

     

    姑娘:再见

    tcpdump

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

     

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

    网络异常状况

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

     

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

    server.go

     

    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

     

    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会怎么样
    • Go的目标是希望提升现有编程语言对程序库等依赖性(dependency)的管理,这些软件元素会被应用程序反复调用。由于存在并行编程模式,因此这一语言也被设计用来解决多处理器的任务。

      谷歌大约2007年开始开发Go,并于2008年投入了一组全职员工。谷歌当初将该语言设计为一款系统编程语言,可以被用于网络服务器、存储系统和数据库中。但是谷歌认为,该语言还有望被用于其它领域。

课课家教育

未登录