golang 的 net 包,相关接口和结构比较多,今天做个简单的梳理。
在总结 net 包之前,还需要温习模糊的网络模型知识。下图是大学课本上的网络模型图:
模型图中可以看到,OSI 的七层模型,每一层实现的是与对端相应层的通信接口。但是实际应用中,我们把会话层、表示层、应用层统称为应用层。因此,就变成了TCP/IP 的五层模型。 其中网络层包含了 ip,arp,icmp 等协议,传输层包含了 TCP, UDP 等协议,应用层,比如 SMTP,DNS,HTTP 等协议。
在 net 表中,主要涉及网络层和传输层的协议。支持如下:
网络层:
传输层:
在讲代码结构前,还需要回忆(学习)几个 Socket 编程(套接字编程)的知识点。
下面我们看看 net 包中一些接口,以及一些接口的实现。
从图中可以看出,基于 TCP、UDP、IP、Unix (Stream 方式)的链接抽象出来都是 Conn 接口。基于包传递的 UDP、IP、UnixConn (DGRAM 包方式) 都实现了 PacketConn 接口。对于面向流的监听器,比如: TCPListener、 UnixListener 都实现了 Listener 接口。
整体上可以看出,net 包对网络链接是基于我们复习的网络知识实现的。对于代码的底层实现,也是比较简单的。针对不同的平台,调用不同平台套接字的系统调用即可。直观上看,对于不同的链接,我们都是可以通过Conn 的接口来做网络io的交互。
在了解了包的构成后,我们基于不同的网络协议分两类来学习如何调用网络包提供的方法。
基于流的协议,net 包中支持了常见的 TCP,Unix (Stream 方式) 两种。基于流的协议需要先于对端建立链接,然后再发送消息。下面是 Unix 套接字编程的一个流程:
首先,服务端需要绑定并监听端口,然后等待客户端与其建立链接,通过 Accept 接收到客户端的连接后,开始读写消息。最后,当服务端收到EOF标识后,关闭链接即可。 HTTP, SMTP 等应用层协议都是使用的 TCP 传输层协议。
基于包的协议,net 包中支持了常见的 UDP,Unix (DGRAM 包方式,PacketConn 方式),Ip (网络层协议,支持了icmp, igmp) 几种。基于包的协议在bind 端口后,无需建立连接,是一种即发即收的模式。
基于包的协议,例如基于UDP 的 DNS解析, 文件传输(TFTP协议)等协议,在网络层应该都是基于包的协议。 下面是基于包请求的Server 端和Client端:
可以看到,在Socket 编程里, 基于包的协议是不需要 Listen 和 Accept 的。在 net 包中,使用ListenPacket,实际上仅是构造了一个UDP连接,做了端口绑定而已。端口绑定后,Server 端开始阻塞读取包数据,之后二者开始通信。由于基于包协议,因此,我们也可以采用PacketConn 接口(看第一个实现接口的图)构造UDP包。
下面,我们构造一个简单的 Redis Server (支持多线程),实现了支持Redis协议的简易Key-Value操作(可以使用Redis-cli直接验证):
package mainimport ( "bufio" "fmt" "io" "net" "strconv" "strings" "sync")var KVMap sync.Mapfunc main() { // 构造一个listener listener, _ := net.Listen("tcp", "127.0.0.1:6379") defer func() { _ = listener.Close() }() for { // 接收请求 conn, _ := listener.Accept() // 连接的处理 go FakeRedis(conn) }}// 这里做了io 读写操作,并解析了 Redis 的协议func FakeRedis(conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) for { data, _, err := reader.ReadLine() if err == io.EOF { return } paramCount, _ := strconv.Atoi(string(data[1:])) var params []string for i := 0; i < paramCount; i++ { _, _, _ = reader.ReadLine() // 每个参数的长度,这里忽略了 sParam, _, _ := reader.ReadLine() params = append(params, string(sParam)) } switch strings.ToUpper(params[0]) { case "GET": if v, ok := KVMap.Load(params[1]); !ok { conn.Write([]byte("$-1\r\n")) } else { conn.Write([]byte(fmt.Sprintf("$%d\r\n%v\r\n", len(v.(string)), v))) } case "SET": KVMap.Store(params[1], params[2]) conn.Write([]byte("+OK\r\n")) case "COMMAND": conn.Write([]byte("+OK\r\n")) } }}
上述代码没有任何的异常处理,仅作为网络连接的一个简单例子。
从代码中可以看出,我们的数据流式的网络协议,在建立连接后,可以和文件IO服务一样,可以任意的读写操作。
正常情况下,流处理的请求,都会开启一个协程来做连接处理,主协程仅用来接收连接请求。(基于包的网络协议则可以不用开启协程处理)