知识回顾

  • Socket 是对 TCP/IP 协议的封装,Socket 本身并不是协议,而是一个调用接口(API),通过 Socket,我们才能使用 TCP/IP 协议。实际上,Socket 跟 TCP/IP 协议没有必然的联系。Socket 编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket 的出现只是使得程序员更方便地使用 TCP/IP 协议栈而已,是对 TCP/IP 协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如 create、 listen、connect、accept、send、read 和 write 等等。
  • 如果一个程序创建了一个 socket,并让其监听 80 端口,其实是向 TCP/IP 协议栈声明了其对 80 端口的占有。以后,所有目标是 80 端口的 TCP 数据包都会转发给该程序(这里的程序,因为使用的是 Socket 编程接口,所以首先由 Socket 层来处理)。
  • 所谓 accept 函数,其实抽象的是 TCP 的连接建立过程。accept 函数返回的新 socket 其实指代的是本次创建的连接,而一个连接是包括两部分信息的,一个是源 IP 和源端口,另一个是宿 IP 和宿端口。所以,accept 可以产生多个不同的 socket,而这些 socket 里包含的宿 IP 和宿端口是不变的,变化的只是源 IP 和源端口。
  • 这样的话,这些 socket 宿端口就可以都是 80,而 Socket 层还是能根据源 / 宿对来准确地分辨出 IP 包和 socket 的归属关系,从而完成对 TCP/IP 协议的操作封装!

Socket 编程

在讲代码结构前,还需要回忆(学习)几个 Socket 编程(套接字编程)的知识点。

  1. 在 Linux 上一切皆文件。所以各端口的读写服务可以认为是读取/写入文件, 一般使用文件描述符 fd (file descriptor) 表示。在Windows上,各端口的读写服务是一个通信链的句柄操作,通过句柄实现网络发出请求和读取数据。在 go 中为了统一,采用 linux 的 fd 代表一个链接节点。
  2. TCP 是面向连接的、可靠的流协议,可以理解为不断从文件中读取数据(STREAM)。UDP 是无链接的、面向报文的协议,是无序,不可靠的(DGRAM)(目前很多可靠的协议都是基于UDP 开发的)。
  3. UNIXDomain Socket 是一种 进程间通信的协议,之前仅在*nix上使用,17年 17063 版本后支持了该协议。虽然是一个 IPC 协议,但是在实现上是基于套接字 (socket) 实现的。因此,UNIXDomain Socket 也放在了net 包中。
  4. unixDomain Socket 也可以选择采用比特流的方式,或者无序的,不可靠的通讯方式,有序数据包的方式(SEQPACKET, Linux 2.6 内核才支持)

代码结构

下面我们看看 net 包中一些接口,以及一些接口的实现。

golang_net_interface.jpg

从图中可以看出,基于 TCP、UDP、IP、Unix (Stream 方式)的链接抽象出来都是 Conn 接口。基于包传递的 UDP、IP、UnixConn (DGRAM 包方式) 都实现了 PacketConn 接口。对于面向流的监听器,比如: TCPListener、 UnixListener 都实现了 Listener 接口。

整体上可以看出,net 包对网络链接是基于我们复习的网络知识实现的。对于代码的底层实现,也是比较简单的。正对不同的平台,调用不同平台套接字的系统调用即可。直观上看,对于不同的链接,我们都是可以通过Conn 的接口来做网络io的交互。

如何使用

在了解了包的构成后,我们基于不同的网络协议分两类来学习如何调用网络包提供的方法。

基于流的协议

基于流的协议,net 包中支持了常见的 TCP,Unix (Stream 方式) 两种。基于流的协议需要先于对端建立链接,然后再发送消息。下面是 Unix 套接字编程的一个流程:

golang_net_stream.jpg

首先,服务端需要绑定并监听端口,然后等待客户端与其建立链接,通过 Accept 接收到客户端的连接后,开始读写消息。最后,当服务端收到EOF标识后,关闭链接即可。 HTTP, SMTP 等应用层协议都是使用的 TCP 传输层协议。

基于包的协议

基于包的协议,net 包中支持了常见的 UDP,Unix (DGRAM 包方式,PacketConn 方式),Ip (网络层协议,支持了icmp, igmp) 几种。基于包的协议在bind 端口后,无需建立连接,是一种即发即收的模式。

基于包的协议,例如基于UDP 的 DNS解析, 文件传输(TFTP协议)等协议,在网络层应该都是基于包的协议。 下面是基于包请求的Server 端和Client端:

golang_net_dgram.jpg

可以看到,在Socket 编程里, 基于包的协议是不需要 Listen 和 Accept 的。在 net 包中,使用ListenPacket,实际上仅是构造了一个UDP连接,做了端口绑定而已。端口绑定后,Server 端开始阻塞读取包数据,之后二者开始通信。由于基于包协议,因此,我们也可以采用PacketConn 接口(看第一个实现接口的图)构造UDP包。

一个简单的例子

下面,我们构造一个简单的 Redis Server (支持多线程),实现了支持Redis协议的简易Key-Value操作(可以使用Redis-cli直接验证):

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
package main

import (
"bufio"
"fmt"
"io"
"net"
"strconv"
"strings"
"sync"
)

var KVMap sync.Map

func 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"))
}

conn.Write([]byte("Message received."))
}
}

上述代码没有任何的异常处理,仅作为网络连接的一个简单例子。
从代码中可以看出,我们的数据流式的网络协议,在建立连接后,可以和文件IO服务一样,可以任意的读写操作。

正常情况下,流处理的请求,都会开启一个协程来做连接处理,主协程仅用来接收连接请求。(基于包的网络协议则可以不用开启协程处理)

总结

  1. 基于 Conn 的消息都是有三种过期时间,这其实是在底层epoll_wait中设置的超时时间。 Deadline 设置了Dail中建立连接的超时时间, ReadDeadline 是 Read 操作的超时时间, WriteDeadline 为 Write 操作的超时时间。
  2. net 包作为基础包,基于net开发应用层协议比较多,例如 net/http, net/rpc/smtp 等。
  3. 网络的io操作底层是基于epoll来实现的, unixDomain 基于文件来实现的。
  4. net 包实现的套接字编程仅是我们日常生活中用的比较多的一些方法,还有很多未实现的配置待我们去探索。
  5. 网络模型比较简单,实际用起来,还是需要分门别类的。

UDP

服务器

Go语言包中处理UDP Socket和TCP Socket不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP缺少了对客户端连接请求的Accept函数。其他基本几乎一模一样,只有TCP换成了UDP而已。UDP的几个主要函数如下所示:

1
2
3
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)

net.ListenUDP返回UDPConn对象, 可以读取数据,以及往相应的client发送数据 (指定UDPAddr)。

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
package main
import (
"encoding/binary"
"flag"
"fmt"
"net"
"os"
"time"
)
var host = flag.String("host", "", "host")
var port = flag.String("port", "37", "port")
func main() {
flag.Parse()
addr, err := net.ResolveUDPAddr("udp", *host+":"+*port)
if err != nil {
fmt.Println("Can't resolve address: ", err)
os.Exit(1)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Println("Error listening:", err)
os.Exit(1)
}
defer conn.Close()
for {
handleClient(conn)
}
}
func handleClient(conn *net.UDPConn) {
data := make([]byte, 1024)
n, remoteAddr, err := conn.ReadFromUDP(data)
if err != nil {
fmt.Println("failed to read UDP msg because of ", err.Error())
return
}
daytime := time.Now().Unix()
fmt.Println(n, remoteAddr)
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, uint32(daytime))
conn.WriteToUDP(b, remoteAddr)
}

时间数是64位的,需要将其转换成一个32位的字节。

执行 go run timeserver.go启动服务器

客户端

客户端代码和TCP类似。

1
2
3
4
5
func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) Read(b []byte) (int, error)
func (c *UDPConn) Write(b []byte) (int, error)
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
package main
import (
"encoding/binary"
"flag"
"fmt"
"net"
"os"
"time"
)
var host = flag.String("host", "localhost", "host")
var port = flag.String("port", "37", "port")

//go run timeclient.go -host time.nist.gov
func main() {
flag.Parse()
addr, err := net.ResolveUDPAddr("udp", *host+":"+*port)
if err != nil {
fmt.Println("Can't resolve address: ", err)
os.Exit(1)
}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
fmt.Println("Can't dial: ", err)
os.Exit(1)
}
defer conn.Close()
_, err = conn.Write([]byte(""))
if err != nil {
fmt.Println("failed:", err)
os.Exit(1)
}
data := make([]byte, 4)
_, err = conn.Read(data)
if err != nil {
fmt.Println("failed to read UDP msg because of ", err)
os.Exit(1)
}
t := binary.BigEndian.Uint32(data)
fmt.Println(time.Unix(int64(t), 0).String())
os.Exit(0)
}

执行go run timeclient.go测试UDP。
也可以用公网的时间服务器测试: go run timeclient.go -host time.nist.gov

Unix domain socket

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
// server.go
package main

import "net"

func echoServer(c net.Conn) {
for {
buf := make([]byte, 512)
nr, err := c.Read(buf)
if err != nil {
return
}

data := buf[0:nr]
println("Server got:", string(data))
_, err = c.Write(data)
if err != nil {
panic("Write: " + err.Error())
}
}
}

func main() {
l, err := net.Listen("unix", "/tmp/echo.sock")
if err != nil {
println("listen error", err.Error())
return
}

for {
fd, err := l.Accept()
if err != nil {
println("accept error", err.Error())
return
}

go echoServer(fd)
}
}
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
// client.go
package main

import (
"io"
"net"
"time"
)

func reader(r io.Reader) {
buf := make([]byte, 1024)
for {
n, err := r.Read(buf[:])
if err != nil {
return
}
println("Client got:", string(buf[0:n]))
}
}

func main() {
c, err := net.Dial("unix", "/tmp/echo.sock")
if err != nil {
panic(err.Error())
}
defer c.Close()

go reader(c)
for {
_, err := c.Write([]byte("hi...."))
if err != nil {
println(err.Error())
break
}
time.Sleep(1e9)
}
}