Golang TCP长连接服务实现示例
在网络编程场景中,TCP长连接服务常用于需要服务端和客户端持续交互的场景,比如即时通讯、实时数据推送等。本文将通过一个完整的示例,讲解如何使用Golang实现基础的TCP长连接服务,包含服务端监听、客户端连接、消息收发以及连接保活等核心逻辑。
服务端实现
服务端的核心逻辑是监听指定端口,接收客户端连接,为每个连接启动独立的协程处理收发消息,同时实现心跳检测机制保证长连接的可用性。下面是完整的服务端代码:
package main
import (
"bufio"
"fmt"
"net"
"strings"
"time"
)
// 心跳超时时间,超过该时间未收到客户端消息则断开连接
const heartbeatTimeout = 3 * time.Minute
// 处理单个客户端连接
func handleConn(conn net.Conn) {
defer conn.Close()
// 设置连接读取超时时间
conn.SetReadDeadline(time.Now().Add(heartbeatTimeout))
reader := bufio.NewReader(conn)
remoteAddr := conn.RemoteAddr().String()
fmt.Printf("客户端 %s 已连接\n", remoteAddr)
for {
// 读取客户端发送的消息,按换行符分割
msg, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("客户端 %s 连接断开: %v\n", remoteAddr, err)
return
}
// 重置读取超时时间,说明客户端存活
conn.SetReadDeadline(time.Now().Add(heartbeatTimeout))
// 去除消息首尾的空白字符
msg = strings.TrimSpace(msg)
if msg == "" {
continue
}
fmt.Printf("收到客户端 %s 消息: %s\n", remoteAddr, msg)
// 处理消息,这里简单返回带时间戳的响应
response := fmt.Sprintf("服务端已收到消息: %s, 时间: %s\n", msg, time.Now().Format("2006-01-02 15:04:05"))
_, err = conn.Write([]byte(response))
if err != nil {
fmt.Printf("向客户端 %s 发送消息失败: %v\n", remoteAddr, err)
return
}
}
}
func main() {
// 监听本地8080端口
listener, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Printf("启动监听失败: %v\n", err)
return
}
defer listener.Close()
fmt.Println("TCP长连接服务已启动,监听地址: 127.0.0.1:8080")
for {
// 阻塞等待客户端连接
conn, err := listener.Accept()
if err != nil {
fmt.Printf("接收客户端连接失败: %v\n", err)
continue
}
// 为每个连接启动独立的协程处理
go handleConn(conn)
}
}上述代码中,服务端首先通过net.Listen监听127.0.0.1的8080端口,然后循环调用listener.Accept接收客户端连接。每收到一个连接,就启动一个独立的goroutine执行handleConn函数处理该连接。
在handleConn函数中,我们设置了读取超时时间为3分钟,每当收到客户端消息时就重置超时时间,实现简单的心跳检测。如果超过3分钟没有收到客户端消息,读取操作会返回超时错误,服务端会自动关闭该连接。消息处理部分只是简单地将收到的消息带时间戳返回给客户端,实际场景中可以根据需求替换为业务处理逻辑。
客户端实现
客户端需要能够主动连接服务端,发送消息并接收服务端的响应,同时可以持续输入消息实现长连接交互。下面是完整的客户端代码:
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
"time"
)
func main() {
// 连接服务端
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Printf("连接服务端失败: %v\n", err)
return
}
defer conn.Close()
fmt.Println("已连接到服务端 127.0.0.1:8080,输入消息按回车发送,输入exit退出")
// 启动协程接收服务端消息
go func() {
reader := bufio.NewReader(conn)
for {
msg, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("接收服务端消息失败: %v\n", err)
os.Exit(0)
return
}
fmt.Printf("收到服务端响应: %s", strings.TrimSpace(msg))
}
}()
// 主协程负责读取用户输入并发送消息
inputReader := bufio.NewReader(os.Stdin)
for {
fmt.Print("请输入要发送的消息: ")
input, err := inputReader.ReadString('\n')
if err != nil {
fmt.Printf("读取输入失败: %v\n", err)
return
}
input = strings.TrimSpace(input)
if input == "exit" {
fmt.Println("准备退出客户端")
return
}
if input == "" {
continue
}
// 发送消息,末尾加上换行符方便服务端按行读取
_, err = conn.Write([]byte(input + "\n"))
if err != nil {
fmt.Printf("发送消息失败: %v\n", err)
return
}
// 稍微休眠避免消息发送过快
time.Sleep(100 * time.Millisecond)
}
}客户端代码首先通过net.Dial连接服务端的127.0.0.1:8080地址,然后启动一个独立的goroutine用于接收服务端返回的响应消息,主协程则循环读取用户在终端的输入,将消息发送给服务端。
当用户输入"exit"时,客户端会主动关闭连接并退出。如果服务端主动断开连接,接收消息的goroutine会收到错误并打印提示,随后退出整个客户端程序。
运行测试
测试时可以先将服务端代码保存为server.go,客户端代码保存为client.go,然后分别打开两个终端执行以下命令:
- 服务端终端:执行
go run server.go,启动后会看到"TCP长连接服务已启动"的提示 - 客户端终端:执行
go run client.go,连接成功后会提示输入消息
在客户端终端输入任意消息按回车,就能看到服务端返回的带时间戳的响应。如果客户端超过3分钟没有发送消息,服务端会自动断开连接,客户端也会收到断开提示。
注意事项
上述示例是一个基础的长连接实现,实际生产环境中还需要考虑更多场景:
- 连接断开后的重连机制,客户端可以在连接断开后尝试自动重连
- 消息边界的处理,示例中使用换行符分割消息,实际场景可能需要更严谨的协议设计,比如固定长度消息、添加消息头标记长度等
- 并发安全问题,如果多个goroutine操作同一个连接,需要加锁保证写入的原子性
- 资源释放,确保连接断开后相关的资源都能正确回收,避免内存泄漏