学完这篇,你能用 Go 标准库 net 包写出可用的 TCP 和 UDP 服务器:能并发处理连接、正确处理消息边界、设置超时,并且能优雅退出。
第一步:环境与最小 TCP 服务器
新建目录,执行 go mod init tcpserver(Go 1.16 之后必须有 go.mod,否则 go run 会报错)。然后在 main.go 写一个最小的回声服务器(echo Server):
package main
import (
"log"
"net"
)
func main() {
ln, err := net.Listen("tcp", ":9000")
if err != nil {
log.Fatal(err)
}
defer ln.Close()
log.Println("listen on", ln.Addr())
for {
conn, err := ln.Accept()
if err != nil {
log.Println("accept:", err)
continue
}
go handle(conn)
}
}
net.Listen 的第一个参数是网络类型,TCP 就写 "tcp";第二个是地址,:9000 表示监听所有网卡的 9000 端口,只想本机访问就写 127.0.0.1:9000。
注意:Accept 返回的 err 不要直接 log.Fatal,因为服务器关闭时 Accept 必然返回错误;如果因此让进程退出,就无法优雅关闭。这里用 continue 或按 net.ErrClosed 判断。
第二步:并发处理每个连接
上面代码最关键的一行是 go handle(conn)。Go 的 goroutine 很轻,一个连接一个 goroutine 是标准做法,几千个连接毫无压力。handle 函数:
func handle(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
return // 对端关闭或出错
}
conn.Write([]byte("echo: "))
conn.Write(buf[:n])
}
}
conn.Read 返回 0 或错误就说明连接结束,必须 return,否则这个 goroutine 永远不退出,长期运行就会造成 goroutine 泄漏。
注意:defer conn.Close() 一定要写在 handle 里而不是 Accept 循环里,写错位置就会导致连接没有关闭,最终耗尽文件描述符。
第三步:处理消息边界(最容易出错的地方)
TCP 是字节流,没有“一条消息”的概念。客户端发两次 hello,服务端一次 Read 可能收到 hellohello,也可能只收到 hel。所以必须自己定协议:要么用换行分隔,要么在包头写长度。
换行分隔最简单,用 bufio.Scanner:
func handle(conn net.Conn) {
defer conn.Close()
sc := bufio.NewScanner(conn)
for sc.Scan() {
line := sc.Text()
fmt.Fprintf(conn, "echo: %s\n", line)
}
}
长度前缀协议的思路也一样:先读 4 字节得到长度 N,再用 io.ReadFull(conn, buf[:N]) 读满 N 字节。
注意:bufio.Scanner 默认单行上限 64KB,超过会直接报错停止。传输大消息时,用 sc.Buffer(make([]byte, 0, 64*1024), 1024*1024) 调大上限,或改用 bufio.Reader。
第四步:超时控制与优雅关闭
没有超时的服务器会被慢连接占用。在每个连接上设置截止时间,每次读之前重新设置:
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
优雅关闭:接收 os.Interrupt 信号,关闭监听器,用 sync.WaitGroup 等所有 handle 返回,再退出进程。监听器关闭后 Accept 会返回 net.ErrClosed,据此跳出循环即可。
第五步:UDP 服务器
UDP 是无连接、面向报文的,不需要 Accept,一个 socket 收所有包:
pc, err := net.ListenPacket("udp", ":9001")
if err != nil {
log.Fatal(err)
}
defer pc.Close()
buf := make([]byte, 1500)
for {
n, addr, err := pc.ReadFrom(buf)
if err != nil {
log.Println("read:", err)
continue
}
pc.WriteTo([]byte("echo: "+string(buf[:n])), addr)
}
ReadFrom 一次调用就对应一个完整报文,天然有边界,不用自己拆包。回包必须用 WriteTo 带上 addr,因为 UDP 没有连接的概念。
注意:UDP 单包建议不超过 1472 字节(以太网 MTU 1500 减去 IP/UDP 头),超过会被分片,丢一个分片整包就废了。传输大文件或要求可靠,使用 TCP。
第六步:动手验证
Windows 或 Linux 有 nc 的话,直接 nc 127.0.0.1 9000,输入一行回车看回显;测 UDP 用 nc -u 127.0.0.1 9001。没有 nc 就自己写个 20 行的 Go 客户端,net.Dial("tcp", "127.0.0.1:9000") 连上去写数据。
小结
- TCP 服务器 =
net.Listen + 循环 Accept + go handle(conn),一个连接一个 goroutine。
handle 里必须 defer conn.Close(),读出错要 return,否则连接和 goroutine 都无法回收。
- TCP 是字节流,务必自己处理消息边界(换行分隔或长度前缀);UDP 是报文,一次
ReadFrom 一个包。
- 加读超时避免慢连接占用连接,收到信号先关监听器再等 goroutine 收尾才是优雅关闭。
- 要可靠选 TCP,要低延迟、能容忍丢包选 UDP。