
本文详解如何使用 go 语言让一个服务器同时监听两个 tcp 端口,分别接入两个客户端,并通过服务端中转实现跨端口消息互通;包含完整可运行代码、错误处理规范及关键注意事项。
本文详解如何使用 go 语言让一个服务器同时监听两个 tcp 端口,分别接入两个客户端,并通过服务端中转实现跨端口消息互通;包含完整可运行代码、错误处理规范及关键注意事项。
要实现“两个客户端连接不同端口、经由同一服务器相互通信”,核心在于:服务器需并发监听多个端口,并在连接建立后,将来自任一客户端的消息转发给另一客户端。原代码仅支持单端口回显(echo),且忽略了错误处理与连接管理逻辑。下面我们将从监听、路由、转发三个层面重构方案。
✅ 正确监听多个端口:使用 goroutine + 独立 accept 循环
Go 不允许在主线程中阻塞地调用 Accept() 多次,但可通过启动多个 goroutine 分别运行独立的 acceptLoop 来实现多端口监听。每个 acceptLoop 负责一个 net.Listener,接受连接并派生 listenConnection 协程处理 I/O:
func main() {
fmt.Println("Server starting: listening on :3000 and :8080")
listener1, err := net.Listen("tcp", "localhost:3000")
if err != nil {
log.Fatal("Failed to bind port 3000:", err)
}
defer listener1.Close()
listener2, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatal("Failed to bind port 8080:", err)
}
defer listener2.Close()
// 启动两个 accept 循环(goroutine)
go acceptLoop(listener1, "port-3000")
go acceptLoop(listener2, "port-8080")
// 主 goroutine 保持运行(避免程序退出)
select {} // 阻塞等待信号
}
func acceptLoop(l net.Listener, tag string) {
for {
conn, err := l.Accept()
if err != nil {
log.Printf("Accept error on %s: %v", tag, err)
continue // 继续监听,不终止循环
}
log.Printf("New connection from %s: %s", tag, conn.RemoteAddr())
go listenConnection(conn, tag)
}
}
⚠️ 注意:defer l.Close() 不能放在 acceptLoop 函数开头(否则首次循环后就关闭了 listener),应移至 main() 中统一管理,或在 acceptLoop 退出时显式关闭(本例采用前者)。
? 实现客户端间消息中转:引入全局连接池与路由逻辑
单纯监听双端口还不够——当前 listenConnection 仍是 echo 模式。要让 client-A(连 3000)发的消息抵达 client-B(连 8080),需:
- 为每个连接打上标识(如所属端口或客户端 ID);
- 将活跃连接存入线程安全的 map(如 sync.Map);
- 接收消息后,根据来源端口,向另一端口的连接广播。
以下是增强版 listenConnection(含连接注册、消息路由与清理):
import (
"log"
"net"
"sync"
)
var (
clients sync.Map // key: string (e.g., "port-3000"), value: []net.Conn
)
func listenConnection(conn net.Conn, tag string) {
defer func() {
conn.Close()
// 从连接池中移除
if conns, ok := clients.Load(tag); ok {
if list, ok := conns.([]net.Conn); ok {
var remaining []net.Conn
for _, c := range list {
if c != conn {
remaining = append(remaining, c)
}
}
clients.Store(tag, remaining)
}
}
log.Printf("Connection closed: %s", conn.RemoteAddr())
}()
// 注册连接
clients.LoadOrStore(tag, []net.Conn{})
if conns, _ := clients.Load(tag); conns != nil {
if list, ok := conns.([]net.Conn); ok {
clients.Store(tag, append(list, conn))
}
}
buffer := make([]byte, 1400)
for {
n, err := conn.Read(buffer)
if err != nil {
log.Printf("Read error from %s: %v", tag, err)
return
}
msg := buffer[:n]
log.Printf("Received from %s: %s", tag, string(msg))
// 路由:发往另一端口的所有连接
targetTag := map[string]string{"port-3000": "port-8080", "port-8080": "port-3000"}[tag]
if targetConns, ok := clients.Load(targetTag); ok {
if list, ok := targetConns.([]net.Conn); ok {
for _, targetConn := range list {
if _, wErr := targetConn.Write(msg); wErr != nil {
log.Printf("Write error to %s: %v", targetConn.RemoteAddr(), wErr)
}
}
}
}
}
}
? 客户端适配建议(可选优化)
- 原客户端为请求-响应模式(发一条→等一条回),不适合实时聊天。建议改为:独立 goroutine 读取服务端消息 + 主循环发送;
- 可为两个客户端指定不同端口(如 clientA 连 :3000,clientB 连 :8080),无需修改协议;
- 添加简单粘包处理(如按行 \n 分割)可提升健壮性(bufio.Scanner 替代原始 Read)。
✅ 总结与关键提醒
- ✅ 多端口监听可行:通过多个 net.Listen + 多个 go acceptLoop 实现,无需修改底层网络栈;
- ✅ 跨端口通信本质是服务端路由:服务器作为中继,需主动维护连接状态并转发数据;
- ⚠️ 务必处理错误:忽略 err(如 listener.Accept() 或 conn.Read())会导致静默失败、goroutine 泄漏;
- ⚠️ 注意资源清理:连接关闭时需从连接池移除,避免内存泄漏与无效写入;
- ? 进阶方向:使用 WebSocket 替代裸 TCP 提升浏览器兼容性;引入消息队列解耦收发逻辑;添加用户认证与房间隔离。
该方案已在 Go 1.21+ 验证通过,可直接编译运行。双客户端即刻开始跨端口实时对话。











