Go 网络编程的实现


提要:

Go 网络编程主要通过 net 包(package)实现,它支持 TCP/IP, UDP, domain name resolution, Unix domain sockets 等连接,此外,还通过 net/http ,net/rpc 等提供了 http,rpc 等主流应用层的连接协议。

1 TCP 服务示例

TCP 是最常用的网络连接方式,以 TCP 连接为例,一个简单的 TCP 连接代码示例:

TCP Client

1
2
3
4
5
6
7
conn, err := net.Dial("tcp", "golang.org:80")
if err != nil {
	// handle error
}
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
// ...D

TCP server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ln, err := net.Listen("tcp", ":8080")
if err != nil {
	// handle error
}
for {
	conn, err := ln.Accept()
	if err != nil {
		// handle error
	}
	go handleConnection(conn)
}

那么 net 包背后是如何实现 tcp 连接的?

2. TCP 连接在系统调用层面的实现

包括 TCP/IP 在内的各种网络连接,在类 unix 的操作系统里,都是通过网络系统调用(network system calls) 实现的,相关系统调用接口主要有:

socket()bind()listen()accept()send()

使用系统调用创建 TCP 服务器的核心流程是:

创建 TCP 客户端的核心流程:

socket() 等系统调用的实现,是 kernel 层面完成的。

对于 net 包而言,包括 TCP 在内网络连接,底层上是通过这些系统调用实现的,它的任务则是对这个工作流程做了封装。

3. Golang TCP 连接的实现

3.1 核心方法

以客户端连接为例,conn, err := net.Dial("tcp", "golang.org:80") 的核心实现方法是 dial.go 的

1
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)

这个方法的主要功能是:

3.1.1 context 处理

3.1.2 网络和地址解析, d.resolver().resolveAddrList(.....)

3.1.3 sysDialer ,拨号连接

连接的两种方式:

sd.dialSingle(dialCtx, ra) 支持四种类型:

1
2
3
4
5
6
7
sd.dialTCP(ctx, la, ra)

sd.dialUDP(ctx, la, ra)

sd.dialIP(ctx, la, ra)

sd.dialUnix(ctx, la, ra)

以 TCP 为例:

sd.dialTCP(ctx, la, ra) 以及 sl.listenTCP(ctx, la) 的下层实现都可以追溯到 sock_posix.go 的

1
func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {

即实现系统调用socket()

如果是服务端,调用 fd.listenStream(laddr, listenerBacklog(), ctrlFn)

如果是客户端,调用 fd.dial(ctx, laddr, raddr, ctrlFn)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if laddr != nil {
		if lsa, err = laddr.sockaddr(fd.family); err != nil {
			return err
		} else if lsa != nil {
			if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
				return os.NewSyscallError("bind", err)
			}
		}
	}
	var rsa syscall.Sockaddr  // remote address from the user
	var crsa syscall.Sockaddr // remote address we actually connected to
	if raddr != nil {
		if rsa, err = raddr.sockaddr(fd.family); err != nil {
			return err
		}
		if crsa, err = fd.connect(ctx, lsa, rsa); err != nil {
			return err
		}
		fd.isConnected = true
	} else {
		if err := fd.init(); err != nil {
			return err
		}
	}

3.2 小结

net 在调用syscall 之前,主要是增加 context 控制,封装 udp,tcp, uninx domain socket 等不同的连接类型,DNS 查找等,同时在有需要的地方引入 goroutine 提高处理效率。

问题:

proxy 是如何实现的

TCP 如何保持长连接

1
 *int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);*

4 TCP 数据发送与接收

4.1 服务端和客户端创建连接

TCP 连接建立后,如果是服务端,每当有客户端发来请求时,会建立新的连接。

1
conn, err := ln.Accept()

这个方法的下层实现是,tcpsock.go

1
func (l *TCPListener) Accept() (Conn, error)

最终可以追溯到 fd_unix.go

1
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) 

而最底层的实现则是系统调用:hook_unix.go, syscall.Accept

4.2 读写数据

在 unix 的设计哲学里,everything is a file,延伸到网络连接设计,发送和接收数据对应也就是写入数据和读取数据。

网络连接建立后的 Conn 类型,其实也是一个文件描述符(file discreptor),故发送数据就是向 conn 写入数据

1
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")

接收数据即是从 conn 读取数据

1
status, err := bufio.NewReader(conn).ReadString('\n')

5 Http 的实现

go http 连接就是先实现 http 协议,然后再调用上述的 TCP 连接。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var DefaultTransport RoundTripper = &Transport{
	Proxy: ProxyFromEnvironment,
	DialContext: (&net.Dialer{
		Timeout:   30 * time.Second,
		KeepAlive: 30 * time.Second,
		DualStack: true,
	}).DialContext,
	ForceAttemptHTTP2:     true,
	MaxIdleConns:          100,
	IdleConnTimeout:       90 * time.Second,
	TLSHandshakeTimeout:   10 * time.Second,
	ExpectContinueTimeout: 1 * time.Second,
}

参考:

https://www.tutorialspoint.com/unix_sockets/socket_server_example.htm

TCP Keepalive HOWTO

http://www.haifux.org/lectures/217/netLec5.pdf

http://linasm.sourceforge.net/docs/syscalls/network.php

https://www.kernel.org/doc/html/latest/networking/rds.html#socket-interface