首页 > 编程笔记 > Go语言笔记 阅读:35

Go语言TCP通信详解(附带实例)

TCP/IP(transmission control protocol/internet protocol)协议,即传输控制协议/网络协议,是一种面向连接的、可靠的、基于字节流的传输层(transport layer)通信协议。

因为是面向连接的协议,所以数据像水流一样传输,存在黏包问题。所谓黏包问题,主要还是因为发送方一次性把所有数据都存入缓存区,接收方不知道消息之间的界限,不知道一次性提取多少字节的数据。

如下图所示,一个 TCP 服务端可以同时连接多个 TCP 客户端:


图 1 一个TCP服务端可以同时连接多个TCP客户端

例如,我国各个地区的用户都使用自己计算机的浏览器访问淘宝网。因为 Go 语言通过创建多个goroutine实现并发非常方便和高效,所以可以每建立一次链接就创建一个goroutine。

建立TCP连接

在使用 Go 语言编写 TCP 服务端时,一般按照如下步骤展开:
下面演示如何编写简单的 TCP 服务端。代码如下:
package main

import (
     "fmt"
     "net"
)

func main() {
     // 使用 net.Listen() 函数监听连接的地址与端口
     listener, err := net.Listen("tcp", "127.0.0.1:3000")
     if err != nil {
          fmt.Printf("监听失败!发生错误:%v\n", err)
          return
     }
     fmt.Println("服务端已开启!等待客户端的连接请求......")

     // 响应由 TCP 客户端发送的连接请求
     conn, err := listener.Accept()
     if err != nil {
          fmt.Printf("响应失败!发生错误:%v\n", err)
          return
     }
     fmt.Println("服务端已成功连接客户端!")

     defer conn.Close()
}
在编写上述 TCP 服务端时,需要明确一个重点:为了创建 TCP 连接,需要调用 net.Listen() 函数,并向该函数传入 3 个参数,即协议类型(tcp)、IP 地址和端口号。

TCP 服务端编写完毕后,下面需要编写 TCP 客户端。在使用 Go 语言编写 TCP 客户端时,一般按照如下步骤展开:
  1. 定义通信的地址和端口;
  2. 使用 Dial() 函数建立与服务端的连接;
  3. 关闭 TCP 连接。

下面演示如何编写 TCP 客户端。代码如下:
package main

import (
     "fmt"
     "net"
)

func main() {
     // 建立与服务端的连接
     conn, err := net.Dial("tcp", "127.0.0.1:3000")
     if err != nil {
          fmt.Printf("连接失败!发生错误:%v\n", err.Error())
          return
     }
     fmt.Println("客户端向服务端发送连接请求......")

     defer conn.Close() // 关闭 TCP 连接
}
在编写上述 TCP 客户端时需要明确一个重点:为了创建一个 TCP 连接需要调用 net.Dial() 函数,并向该函数传入 3 个参数,即协议类型(tcp)、IP 地址和端口号。

为了保证上述程序成功运行,要打开两个 VS Code 窗口:
一个窗口是 TCP 服务端(见下图):


图 2 TCP服务端

一个窗口是 TCP 客户端(见下图):


图 3 TCP 客户端

当运行程序时,要先运行 TCP 服务端,随即在 TCP 服务端所在 VS Code 窗口的控制台上打印如下信息:

服务端已开启!等待客户端的连接请求……


再运行 TCP 客户端,随即在 TCP 客户端所在 VS Code 窗口的控制台上打印如下信息:

客户端向服务端发送连接请求……


打开 TCP 服务端所在的 VS Code 窗口,在控制台上打印一行新信息:

服务端已成功连接客户端!

这说明 TCP 服务端与 TCP 客户端成功建立了连接。

实现交互通信

TCP 服务端与 TCP 客户端建立连接后,就可以发送和接收数据,进而实现 TCP 服务端与 TCP 客户端交互通信。通过连接对象调用 Write() 函数,发送数据;通过连接对象调用 Read() 函数,接收数据。

为了让 TCP 服务端与 TCP 客户端交互通信,TCP 服务端除了要使用 net.Listen() 函数得到连接信息,还要使用 for 循环不停响应由 TCP 客户端发送的连接请求。每响应一次由 TCP 客户端发送的连接请求,就创建一个用于执行发送和接收数据操作的 goroutine。

对上述实现 TCP 服务端的代码修改如下:
package main

import (
     "fmt"
     "net"
)

func main() {
     // 使用 net.Listen() 函数监听连接的地址与端口
     listener, err := net.Listen("tcp", "127.0.0.1:3000")
     if err != nil {
          fmt.Printf("监听失败!发生错误:%v\n", err)
          return
     }
     fmt.Println("服务端已开启!等待客户端的连接请求......")

     for {
          // 响应由 TCP 客户端发送的连接请求
          conn, err := listener.Accept()
          if err != nil {
               fmt.Printf("响应失败!发生错误:%v\n", err)
               continue
          }
          // 对每个新连接创建协程收发数据
          go process(conn)
     }
}

func process(conn net.Conn) {
     defer conn.Close()
     for {
          var buf [128]byte
          // 接收数据
          n, err := conn.Read(buf[:])
          if err != nil {
               fmt.Printf("接收数据失败!发生错误:%v\n", err)
               break
          }
          fmt.Printf("已成功接收数据:%v\n", string(buf[:n]))

          // 发送数据
          if _, err = conn.Write([]byte("服务端消息!")); err != nil {
               fmt.Printf("发送数据失败!发生错误:%v\n", err)
               break
          }
     }
}

此外,还要修改上述实现 TCP 客户端的代码:
package main

import (
     "bufio"
     "fmt"
     "net"
     "os"
     "strings"
)

func main() {
     // 建立与服务端的连接
     conn, err := net.Dial("tcp", "127.0.0.1:3000")
     if err != nil {
          fmt.Printf("连接失败!发生错误:%v\n", err.Error())
          return
     }
     fmt.Println("客户端向服务端发送连接请求......")
     defer conn.Close()

     inputReader := bufio.NewReader(os.Stdin)

     for {
          // 读取用户在控制台的输入
          input, err := inputReader.ReadString('\n')
          if err != nil {
               fmt.Printf("无法读取在控制台上输入的数据!发生错误:%v\n", err)
               break
          }

          trimmedInput := strings.TrimSpace(input)
          if trimmedInput == "Q" { // 输入 Q 退出
               break
          }

          // 发送数据
          if _, err = conn.Write([]byte(trimmedInput)); err != nil {
               fmt.Printf("发送数据失败!发生错误:%v\n", err)
               break
          }

          // 接收数据
          var recvData = make([]byte, 1024)
          if _, err = conn.Read(recvData); err != nil {
               fmt.Printf("接收数据失败!发生错误:%v\n", err)
               break
          }
          fmt.Printf("已成功接收数据:%v\n", string(recvData))
     }
}
修改后的 TCP 客户端代码中,加入了用户能在控制台上输入任意数据,并且把输入的数据发送给服务器端的功能。不过,VS Code 窗口不支持控制台的输入,因此这里不提供运行结果。读者可以使用其他开发工具运行修改后的 TCP 服务端和 TCP 客户端的代码,以查看运行结果。

相关文章