知识回顾
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 编程(套接字编程)的知识点。
在 Linux 上一切皆文件。所以各端口的读写服务可以认为是读取/写入文件 , 一般使用文件描述符 fd (file descriptor) 表示。在Windows上,各端口的读写服务是一个通信链的句柄操作,通过句柄实现网络发出请求和读取数据。在 go 中为了统一,采用 linux 的 fd 代表一个链接节点。
TCP 是面向连接的、可靠的流协议,可以理解为不断从文件中读取数据(STREAM)。UDP 是无链接的、面向报文的协议,是无序,不可靠的(DGRAM)(目前很多可靠的协议都是基于UDP 开发的)。
UNIXDomain Socket 是一种 进程间通信的协议,之前仅在*nix上使用,17年 17063 版本后支持了该协议。虽然是一个 IPC 协议,但是在实现上是基于套接字 (socket) 实现的。因此,UNIXDomain Socket 也放在了net 包中。
unixDomain Socket 也可以选择采用比特流的方式,或者无序的,不可靠的通讯方式,有序数据包的方式(SEQPACKET, Linux 2.6 内核才支持)
代码结构 下面我们看看 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直接验证):
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 mainimport ( "bufio" "fmt" "io" "net" "strconv" "strings" "sync" ) var KVMap sync.Mapfunc main () { listener, _ := net.Listen("tcp" , "127.0.0.1:6379" ) defer func () { _ = listener.Close() }() for { conn, _ := listener.Accept() go FakeRedis(conn) } } 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服务一样,可以任意的读写操作。
正常情况下,流处理的请求,都会开启一个协程来做连接处理,主协程仅用来接收连接请求 。(基于包的网络协议则可以不用开启协程处理)
总结
基于 Conn 的消息都是有三种过期时间,这其实是在底层epoll_wait中设置的超时时间。 Deadline 设置了Dail中建立连接的超时时间, ReadDeadline 是 Read 操作的超时时间, WriteDeadline 为 Write 操作的超时时间。
net 包作为基础包,基于net开发应用层协议比较多,例如 net/http, net/rpc/smtp 等。
网络的io操作底层是基于epoll来实现的, unixDomain 基于文件来实现的。
net 包实现的套接字编程仅是我们日常生活中用的比较多的一些方法,还有很多未实现的配置待我们去探索。
网络模型比较简单,实际用起来,还是需要分门别类的。
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 mainimport ( "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 mainimport ( "encoding/binary" "flag" "fmt" "net" "os" "time" ) var host = flag.String("host" , "localhost" , "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.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 package mainimport "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 package mainimport ( "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 ) } }