Go语言TCP通信详解(附带实例)
TCP/IP(transmission control protocol/internet protocol)协议,即传输控制协议/网络协议,是一种面向连接的、可靠的、基于字节流的传输层(transport layer)通信协议。
因为是面向连接的协议,所以数据像水流一样传输,存在黏包问题。所谓黏包问题,主要还是因为发送方一次性把所有数据都存入缓存区,接收方不知道消息之间的界限,不知道一次性提取多少字节的数据。
如下图所示,一个 TCP 服务端可以同时连接多个 TCP 客户端:

图 1 一个TCP服务端可以同时连接多个TCP客户端
例如,我国各个地区的用户都使用自己计算机的浏览器访问淘宝网。因为 Go 语言通过创建多个goroutine实现并发非常方便和高效,所以可以每建立一次链接就创建一个goroutine。
下面演示如何编写简单的 TCP 服务端。代码如下:
TCP 服务端编写完毕后,下面需要编写 TCP 客户端。在使用 Go 语言编写 TCP 客户端时,一般按照如下步骤展开:
下面演示如何编写 TCP 客户端。代码如下:
为了保证上述程序成功运行,要打开两个 VS Code 窗口:
一个窗口是 TCP 服务端(见下图):

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

图 3 TCP 客户端
当运行程序时,要先运行 TCP 服务端,随即在 TCP 服务端所在 VS Code 窗口的控制台上打印如下信息:
再运行 TCP 客户端,随即在 TCP 客户端所在 VS Code 窗口的控制台上打印如下信息:
打开 TCP 服务端所在的 VS Code 窗口,在控制台上打印一行新信息:
为了让 TCP 服务端与 TCP 客户端交互通信,TCP 服务端除了要使用 net.Listen() 函数得到连接信息,还要使用 for 循环不停响应由 TCP 客户端发送的连接请求。每响应一次由 TCP 客户端发送的连接请求,就创建一个用于执行发送和接收数据操作的 goroutine。
对上述实现 TCP 服务端的代码修改如下:
此外,还要修改上述实现 TCP 客户端的代码:
因为是面向连接的协议,所以数据像水流一样传输,存在黏包问题。所谓黏包问题,主要还是因为发送方一次性把所有数据都存入缓存区,接收方不知道消息之间的界限,不知道一次性提取多少字节的数据。
如下图所示,一个 TCP 服务端可以同时连接多个 TCP 客户端:

图 1 一个TCP服务端可以同时连接多个TCP客户端
例如,我国各个地区的用户都使用自己计算机的浏览器访问淘宝网。因为 Go 语言通过创建多个goroutine实现并发非常方便和高效,所以可以每建立一次链接就创建一个goroutine。
建立TCP连接
在使用 Go 语言编写 TCP 服务端时,一般按照如下步骤展开:- 定义通信的地址和端口;
- 使用 Listen() 函数监听 TCP 的地址和端口信息,并得到连接信息;
- 使用连接信息的 Accept 函数等待连接;
- 关闭 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 客户端时,一般按照如下步骤展开:
- 定义通信的地址和端口;
- 使用 Dial() 函数建立与服务端的连接;
- 关闭 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 客户端的代码,以查看运行结果。