Socket

Socket是网络编程的一个抽象概念,通常我们用一个 Socket 表示 “打开了一个网络连接”,在 Go 中主要使用 net 包。

使用 netfunc Dial(network, address string) (Conn, error) 函数就可轻松建立一个 Socket 连接。Socket 创建成功后,我们可以对其进行 I/O 操作,最后不要记得对其进行关闭操作。

本章将从 TCP, UDP, Unix 入手,带领大家全面了解 Socket 在 Go 中的应用。

基本知识

Socket 连接又分为客户端和服务端,如图:

/projects/go-basic-courses/ch10/socket.png

核心步骤包括:

  • 创建连接:
  1. Dial(network, address string) (Conn, error)

注意, 这里的 network 可以为:

  1. "tcp", "tcp4", "tcp6"
  2. "udp", "udp4", "udp6"
  3. "ip", "ip4", "ip6"
  4. "unix", "unixgram", "unixpacket"
  • 通过连接发送数据:
  1. conn.Write([]byte("GET / HTTP/1.0\r\n\r\n"))
  • 通过连接读取数据:
  1. buf := make([]byte, 256)
  2. conn.Read(buf)
  • 关闭连接:
  1. conn.Close()

注意: conn 是一个 IO 对象,我们主要使用 IO 相关的帮助方法来进行读写操作。

实际例子之 google 首页访问

  1. package main
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "log"
  6. "net"
  7. )
  8. func main() {
  9. // 尝试与 google.com:80 建立 tcp 连接
  10. conn, err := net.Dial("tcp", "google.com:80")
  11. if err != nil {
  12. log.Fatal(err)
  13. }
  14. defer conn.Close() // 退出关闭连接
  15. // 通过连接发送 GET 请求,访问首页
  16. _, err = fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
  17. if err != nil {
  18. log.Fatal(err)
  19. }
  20. dat, err := ioutil.ReadAll(conn)
  21. if err != nil {
  22. log.Fatal(err)
  23. }
  24. fmt.Println(string(dat))
  25. }

当运行代码,可以得到 google.com 的首页内容,如下:

  1. HTTP/1.0 200 OK
  2. Date: Tue, 05 Jun 2018 14:45:30 GMT
  3. Expires: -1
  4. Cache-Control: private, max-age=0
  5. Content-Type: text/html; charset=ISO-8859-1
  6. P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info."
  7. Server: gws
  8. X-XSS-Protection: 1; mode=block
  9. X-Frame-Options: SAMEORIGIN
  10. Set-Cookie: 1P_JAR=2018-06-05-14; expires=Thu, 05-Jul-2018 14:45:30 GMT; path=/; domain=.google.com
  11. Set-Cookie: NID=131=mqkJocXSsDCD6zdcMyc12DCUqt3X19HIoS0HGTsAzsiuvFx56rBsliga5Uj22QyA8p2IZ6E7lkMGzchqam0RQ58PT6WV5Csllv80MO0uauY9P-FvzCLdYYY9tT0KYtVv; expires=Wed, 05-Dec-2018 14:45:30 GMT; path=/; domain=.google.com; HttpOnly
  12. Accept-Ranges: none
  13. Vary: Accept-Encoding
  14. ....

说明:google.com 网站后端是一个 HTTP server, 因为 HTTP 建立在 TCP 协议基础上,所以我们这里可以使用 TCP 协议来进行访问。

TCP 操作

在这个例子中,我们先使用 net 包创建一个 TCP Server ,然后尝试连接 Server, 最后再通过客户端发送 hello 到 Server,同时 Server 响应 word

我们来看完整例子:

  • server/main.go
  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "log"
  6. "net"
  7. )
  8. func main() {
  9. l, err := net.Listen("tcp", "127.0.0.1:8888")
  10. if err != nil {
  11. log.Fatal(err)
  12. }
  13. log.Printf("Start server with: %s", l.Addr())
  14. defer l.Close()
  15. for {
  16. conn, err := l.Accept()
  17. if err != nil {
  18. log.Fatal(err)
  19. }
  20. go handleConnection(conn)
  21. }
  22. }
  23. func handleConnection(conn net.Conn) {
  24. reader := bufio.NewReader(conn)
  25. for {
  26. dat, _, err := reader.ReadLine()
  27. if err != nil {
  28. log.Println(err)
  29. return
  30. }
  31. fmt.Println("client:", string(dat))
  32. _, err = conn.Write([]byte("word\n"))
  33. if err != nil {
  34. log.Println(err)
  35. return
  36. }
  37. }
  38. }

注意:

  1. 1. 通过 `net.Listen("tcp", "127.0.0.1:8888")` 新建一个 TCP Server
  2. 2. 通过 `l.Accept()` 获取创建的连接。
  3. 3. 通过 `go handleConnection(c)` 新建的 goroutine 来处理连接。
  • client/main.go
  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "log"
  6. "net"
  7. "time"
  8. )
  9. func main() {
  10. conn, err := net.Dial("tcp", "127.0.0.1:8888")
  11. if err != nil {
  12. log.Fatal(err)
  13. }
  14. defer conn.Close()
  15. reader := bufio.NewReader(conn)
  16. for {
  17. _, err := conn.Write([]byte("hello\n"))
  18. if err != nil {
  19. log.Fatal(err)
  20. }
  21. dat, _, err := reader.ReadLine()
  22. if err != nil {
  23. log.Fatal(err)
  24. }
  25. fmt.Println("sever:", string(dat))
  26. time.Sleep(5 * time.Second)
  27. }
  28. }

注意:

  1. 1. 通过 `net.DialTCP("tcp", nil, addr)` 尝试创建到 TCP Sever 的连接。
  2. 2. 通过 `conn.Write([]byte("hello\n"))` 向服务端发送数据。
  3. 3. 通过 `reader.ReadLine()` 读取服务端响应数据。

当我们运行代码的时候,可以在终端看到如下输入:

  1. go run server/main.go
  2. 2018/06/08 08:12:23 Start server with: 127.0.0.1:8888
  3. client: hello
  4. client: hello
  1. go run client/main.go
  2. 2018/06/08 08:12:23 Start server with: 127.0.0.1:8888
  3. sever: word
  4. sever: word

UDP 操作

UDP 相较于 TCP 简单的多,它具有以下特点:

  • 无连接的
  • 要求系统资源较少
  • UDP 程序结构较简单
  • 基于数据报模式(UDP)
  • UDP 可能丢包
  • UDP 不保证数据顺序性

下面我们通过一个统计服务在线人数的例子来了解它:

  • server/main.go
  1. package main
  2. import (
  3. "log"
  4. "net"
  5. "time"
  6. )
  7. func main() {
  8. // listen to incoming udp packets
  9. pc, err := net.ListenPacket("udp", "127.0.0.1:8888")
  10. if err != nil {
  11. log.Fatal(err)
  12. }
  13. log.Printf("Start server with: %s", pc.LocalAddr())
  14. defer pc.Close()
  15. clients := make([]net.Addr, 0)
  16. go func() {
  17. for {
  18. for _, addr := range clients {
  19. _, err := pc.WriteTo([]byte("pong\n"), addr)
  20. if err != nil {
  21. log.Println(err)
  22. }
  23. }
  24. time.Sleep(5 * time.Second)
  25. }
  26. }()
  27. for {
  28. buf := make([]byte, 256)
  29. n, addr, err := pc.ReadFrom(buf)
  30. if err != nil {
  31. log.Println(err)
  32. continue
  33. }
  34. clients = append(clients, addr)
  35. log.Println(string(buf[0:n]))
  36. log.Println(addr.String(), "connecting...", len(clients), "connected")
  37. }
  38. }

注意:

  1. 1. 监听本地 UDP `127.0.0.1:8888`
  2. 2. 使用 `pc.ReadFrom(buf)` 方法读取客户端发送的消息。
  3. 3. 使用 `clients` 来保存所有连上的客户端连接。
  4. 4. 通过 `pc.WriteTo([]byte("pong\n"), addr)` 向所有客户端发送消息。
  • client/main.go
  1. package main
  2. import (
  3. "bufio"
  4. "log"
  5. "net"
  6. )
  7. func main() {
  8. conn, err := net.Dial("udp", "127.0.0.1:8888")
  9. if err != nil {
  10. log.Fatal(err)
  11. }
  12. defer conn.Close()
  13. _, err = conn.Write([]byte("ping..."))
  14. if err != nil {
  15. log.Fatal(err)
  16. }
  17. reader := bufio.NewReader(conn)
  18. for {
  19. dat, _, err := reader.ReadLine()
  20. if err != nil {
  21. log.Fatal(err)
  22. }
  23. log.Println(string(dat))
  24. }
  25. }

当我运行代码可以得到如下输出:

  1. # 执行命令
  2. go run server/main.go
  3. # 输出
  4. 2018/06/08 14:36:13 Start server with: 127.0.0.1:8888
  5. 2018/06/08 14:36:15 ping...
  6. 2018/06/08 14:36:15 127.0.0.1:61790 connecting... 1 connected
  7. 2018/06/08 14:36:18 ping...
  8. 2018/06/08 14:36:18 127.0.0.1:59989 connecting... 2 connected
  1. # 启动 client1
  2. go run client/main.go
  3. # 输出
  4. 2018/06/08 14:36:18 pong
  5. 2018/06/08 14:36:23 pong
  1. # 启动 client2
  2. go run client/main.go
  3. # 输出
  4. 2018/06/08 14:37:58 pong
  5. 2018/06/08 14:38:03 pong

Unix 操作

Unix 和 TCP 很相似,只不过监听的地址是一个 Socket 文件,例如:

  1. l, err := net.Listen("unix", "/tmp/echo.sock")

下面我们就通过一个实际的例子来练习:

  • server/main.go
  1. package main
  2. import (
  3. "log"
  4. "net"
  5. )
  6. func main() {
  7. l, err := net.Listen("unix", "/tmp/unix.sock")
  8. if err != nil {
  9. log.Fatal("listen error:", err)
  10. }
  11. for {
  12. conn, err := l.Accept()
  13. if err != nil {
  14. log.Fatal("accept error:", err)
  15. }
  16. go helloServer(conn)
  17. }
  18. }
  19. func helloServer(c net.Conn) {
  20. for {
  21. buf := make([]byte, 512)
  22. nr, err := c.Read(buf)
  23. if err != nil {
  24. return
  25. }
  26. data := buf[0:nr]
  27. log.Println(string(data))
  28. _, err = c.Write([]byte("hello"))
  29. if err != nil {
  30. log.Fatal("Write: ", err)
  31. }
  32. }
  33. }

说明:

  1. - 使用 `net.Listen("unix", "/tmp/unix.sock")` 启动一个 Server
  2. - 使用 `conn, err := l.Accept()` 来接受客户端的连接。
  3. - 使用 `go helloServer(conn)` 来处理客户端连接,并读取客户端发送的数据 `hi` 并返回 `hello`
  • client/main.go
  1. package main
  2. import (
  3. "io"
  4. "log"
  5. "net"
  6. "time"
  7. )
  8. func reader(r io.Reader) {
  9. buf := make([]byte, 512)
  10. for {
  11. n, err := r.Read(buf[:])
  12. if err != nil {
  13. return
  14. }
  15. log.Println(string(buf[0:n]))
  16. }
  17. }
  18. func main() {
  19. c, err := net.Dial("unix", "/tmp/unix.sock")
  20. if err != nil {
  21. log.Fatal(err)
  22. }
  23. defer c.Close()
  24. go reader(c)
  25. for {
  26. _, err := c.Write([]byte("hi"))
  27. if err != nil {
  28. log.Fatal("write error:", err)
  29. break
  30. }
  31. time.Sleep(3 * time.Second)
  32. }
  33. }

注意:

  1. - 使用 `c, err := net.Dial("unix", "/tmp/unix.sock")` 来连接服务端。
  2. - 使用 `c.Write([]byte("hi"))` 向服务端发送 `hi` 消息。
  3. - 使用 `r.Read(buf)` 读取客户端发送的消息。

当运行代码可以得到如下输出:

  1. # go run server/main.go
  2. 2018/06/09 20:42:14 hi
  3. 2018/06/09 20:42:16 hi
  4. 2018/06/09 20:42:17 hi
  1. # go run client/main.go
  2. 2018/06/09 20:41:47 hello
  3. 2018/06/09 20:41:50 hello
  4. 2018/06/09 20:41:53 hello