网络编程:用 Go 构建底层网络应用
虽然我们平时写 Web 应用大多数时候都在用 HTTP,但 HTTP 本身也是建立在 TCP/IP 协议之上的。有时候我们需要更底层的控制,比如写一个游戏服务器、一个物联网设备的数据收集器,或者一个自定义的 RPC 框架,这时候就需要直接操作 TCP 或 UDP 了。
Go 语言的 net 包提供了强大而优雅的网络编程接口。它把底层复杂的 socket 编程封装得非常简洁,让你能用很少的代码就写出高性能的网络应用。
今天我们就来学习如何用 Go 进行网络编程。
net 包基础
net 包的核心是 Conn 接口,它代表一个网络连接。最常用的方法是:
Read(b []byte) (n int, err error):从连接读取数据Write(b []byte) (n int, err error):向连接写入数据Close() error:关闭连接
另外,net.Dial 用于创建客户端连接,net.Listen 用于创建服务器监听。
TCP 编程
TCP(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层协议。
TCP 服务器
让我们写一个简单的 Echo 服务器,它会把客户端发来的消息原样返回:
package main
import (
"fmt"
"io"
"log"
"net"
)
func handleConnection(conn net.Conn) {
defer conn.Close()
clientAddr := conn.RemoteAddr().String()
log.Printf("新连接: %s", clientAddr)
// 循环读取和发送
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
if err == io.EOF {
log.Printf("客户端 %s 断开连接", clientAddr)
} else {
log.Printf("读取失败: %v", err)
}
return
}
message := string(buf[:n])
log.Printf("收到来自 %s 的消息: %s", clientAddr, message)
// Echo 回去
_, err = conn.Write(buf[:n])
if err != nil {
log.Printf("写入失败: %v", err)
return
}
}
}
func main() {
// 监听 8080 端口
listener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalf("监听失败: %v", err)
}
defer listener.Close()
log.Println("TCP 服务器启动在 :8080")
for {
// 接受新连接
conn, err := listener.Accept()
if err != nil {
log.Printf("接受连接失败: %v", err)
continue
}
// 用 goroutine 处理每个连接
go handleConnection(conn)
}
}
TCP 客户端
package main
import (
"bufio"
"fmt"
"log"
"net"
"os"
)
func main() {
// 连接服务器
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
log.Fatalf("连接失败: %v", err)
}
defer conn.Close()
log.Println("已连接到服务器")
// 启动 goroutine 接收消息
go func() {
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
log.Printf("读取失败: %v", err)
return
}
fmt.Printf("服务器回复: %s", buf[:n])
}
}()
// 从标准输入读取消息并发送
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
message := scanner.Text() + "\n"
_, err := conn.Write([]byte(message))
if err != nil {
log.Printf("发送失败: %v", err)
return
}
}
}
处理粘包问题
TCP 是面向字节流的协议,它不保证消息的边界。这意味着你发送的两次数据可能会被接收方一次性读出来(粘包),或者一次发送的数据被拆成多次读出来(拆包)。
解决粘包问题的常用方法是定义消息格式。最常见的做法是在消息前面加上长度字段:
package main
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"log"
"net"
)
// 消息格式:[4字节长度][消息体]
// 编码消息
func encodeMessage(message string) ([]byte, error) {
var buf bytes.Buffer
// 写入长度(4字节,大端序)
length := uint32(len(message))
if err := binary.Write(&buf, binary.BigEndian, length); err != nil {
return nil, err
}
// 写入消息体
buf.WriteString(message)
return buf.Bytes(), nil
}
// 解码消息
func decodeMessage(conn net.Conn) (string, error) {
// 读取长度
var length uint32
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
return "", err
}
// 读取消息体
buf := make([]byte, length)
if _, err := io.ReadFull(conn, buf); err != nil {
return "", err
}
return string(buf), nil
}
func main() {
// 发送端
go func() {
conn, _ := net.Dial("tcp", "localhost:9090")
defer conn.Close()
messages := []string{"Hello", "World", "Go 语言网络编程"}
for _, msg := range messages {
data, _ := encodeMessage(msg)
conn.Write(data)
}
}()
// 接收端
listener, _ := net.Listen("tcp", ":9090")
defer listener.Close()
conn, _ := listener.Accept()
defer conn.Close()
for {
msg, err := decodeMessage(conn)
if err != nil {
log.Printf("解码失败: %v", err)
break
}
fmt.Println("收到消息:", msg)
}
}
UDP 编程
UDP(User Datagram Protocol)是一种无连接的、不可靠的、基于数据报的协议。它比 TCP 快,但不保证消息的到达和顺序。适合用于实时游戏、视频流、DNS 查询等场景。
UDP 服务器
package main
import (
"fmt"
"log"
"net"
)
func main() {
// 监听 UDP 端口
addr, err := net.ResolveUDPAddr("udp", ":8081")
if err != nil {
log.Fatalf("解析地址失败: %v", err)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
log.Fatalf("监听失败: %v", err)
}
defer conn.Close()
log.Println("UDP 服务器启动在 :8081")
buf := make([]byte, 1024)
for {
// 接收数据
n, clientAddr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Printf("读取失败: %v", err)
continue
}
message := string(buf[:n])
log.Printf("收到来自 %s 的消息: %s", clientAddr, message)
// 回复
reply := fmt.Sprintf("已收到: %s", message)
_, err = conn.WriteToUDP([]byte(reply), clientAddr)
if err != nil {
log.Printf("回复失败: %v", err)
}
}
}
UDP 客户端
package main
import (
"fmt"
"log"
"net"
)
func main() {
// 连接服务器(UDP 的"连接"只是记录目标地址)
conn, err := net.Dial("udp", "localhost:8081")
if err != nil {
log.Fatalf("连接失败: %v", err)
}
defer conn.Close()
// 发送数据
message := "Hello, UDP!"
_, err = conn.Write([]byte(message))
if err != nil {
log.Fatalf("发送失败: %v", err)
}
// 接收回复
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
log.Fatalf("读取失败: %v", err)
}
fmt.Println("服务器回复:", string(buf[:n]))
}
HTTP 服务器的底层实现
实际上,HTTP 服务器就是建立在 TCP 之上的。我们可以用 net 包自己实现一个简单的 HTTP 服务器:
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
)
func handleHTTP(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
// 读取请求行(如:GET / HTTP/1.1)
requestLine, err := reader.ReadString('\n')
if err != nil {
return
}
fmt.Printf("请求: %s", requestLine)
// 读取请求头
for {
line, err := reader.ReadString('\n')
if err != nil || line == "\r\n" {
break
}
fmt.Printf("头: %s", line)
}
// 构造响应
body := "<html><body><h1>Hello from my HTTP server!</h1></body></html>"
response := fmt.Sprintf(
"HTTP/1.1 200 OK\r\n"+
"Content-Type: text/html\r\n"+
"Content-Length: %d\r\n"+
"Connection: close\r\n"+
"\r\n"+
"%s",
len(body),
body,
)
conn.Write([]byte(response))
}
func main() {
listener, err := net.Listen("tcp", ":8082")
if err != nil {
log.Fatalf("监听失败: %v", err)
}
defer listener.Close()
log.Println("HTTP 服务器启动在 :8082")
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go handleHTTP(conn)
}
}
访问 http://localhost:8082 就能看到响应。
超时和 Keep-Alive
设置超时
// 客户端连接超时
conn, err := net.DialTimeout("tcp", "example.com:80", 5*time.Second)
// 读取超时
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
// 写入超时
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
TCP Keep-Alive
Keep-Alive 可以检测死连接:
// 使用 Dialer 设置 Keep-Alive
dialer := net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}
conn, err := dialer.Dial("tcp", "example.com:80")
实战:简单的 RPC 框架
让我们用 TCP 实现一个简单的 RPC(远程过程调用)框架:
package main
import (
"encoding/gob"
"fmt"
"log"
"net"
)
// 请求和响应结构
type RPCRequest struct {
Method string
Args []interface{}
}
type RPCResponse struct {
Result interface{}
Error string
}
// RPC 服务器
type RPCServer struct {
methods map[string]func([]interface{}) (interface{}, error)
}
func NewRPCServer() *RPCServer {
return &RPCServer{
methods: make(map[string]func([]interface{}) (interface{}, error)),
}
}
func (s *RPCServer) Register(name string, fn func([]interface{}) (interface{}, error)) {
s.methods[name] = fn
}
func (s *RPCServer) Serve(addr string) error {
listener, err := net.Listen("tcp", addr)
if err != nil {
return err
}
defer listener.Close()
log.Printf("RPC 服务器启动在 %s", addr)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go s.handleConn(conn)
}
}
func (s *RPCServer) handleConn(conn net.Conn) {
defer conn.Close()
decoder := gob.NewDecoder(conn)
encoder := gob.NewEncoder(conn)
for {
var req RPCRequest
if err := decoder.Decode(&req); err != nil {
return
}
// 查找并调用方法
fn, ok := s.methods[req.Method]
var resp RPCResponse
if !ok {
resp.Error = fmt.Sprintf("方法 %s 不存在", req.Method)
} else {
result, err := fn(req.Args)
if err != nil {
resp.Error = err.Error()
} else {
resp.Result = result
}
}
if err := encoder.Encode(resp); err != nil {
return
}
}
}
// RPC 客户端
type RPCClient struct {
conn net.Conn
decoder *gob.Decoder
encoder *gob.Encoder
}
func DialRPC(addr string) (*RPCClient, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
return &RPCClient{
conn: conn,
decoder: gob.NewDecoder(conn),
encoder: gob.NewEncoder(conn),
}, nil
}
func (c *RPCClient) Call(method string, args ...interface{}) (interface{}, error) {
req := RPCRequest{
Method: method,
Args: args,
}
if err := c.encoder.Encode(req); err != nil {
return nil, err
}
var resp RPCResponse
if err := c.decoder.Decode(&resp); err != nil {
return nil, err
}
if resp.Error != "" {
return nil, fmt.Errorf(resp.Error)
}
return resp.Result, nil
}
func (c *RPCClient) Close() error {
return c.conn.Close()
}
// 示例:数学服务
func main() {
// 启动服务器
go func() {
server := NewRPCServer()
server.Register("Add", func(args []interface{}) (interface{}, error) {
a := args[0].(int)
b := args[1].(int)
return a + b, nil
})
server.Register("Multiply", func(args []interface{}) (interface{}, error) {
a := args[0].(int)
b := args[1].(int)
return a * b, nil
})
server.Serve(":9999")
}()
// 等待服务器启动
// time.Sleep(100 * time.Millisecond)
// 客户端调用
client, err := DialRPC("localhost:9999")
if err != nil {
log.Fatal(err)
}
defer client.Close()
// 调用 Add
result, err := client.Call("Add", 10, 20)
if err != nil {
log.Fatal(err)
}
fmt.Println("Add(10, 20) =", result)
// 调用 Multiply
result, err = client.Call("Multiply", 5, 6)
if err != nil {
log.Fatal(err)
}
fmt.Println("Multiply(5, 6) =", result)
}
小结
今天我们学习了 Go 的网络编程:
- net 包基础:Conn 接口、Read/Write/Close
- TCP 编程:服务器、客户端、粘包处理
- UDP 编程:无连接的数据报传输
- HTTP 底层实现:理解 HTTP 是建立在 TCP 之上的
- 超时和 Keep-Alive:网络编程的重要细节
- 实战 RPC 框架:用 TCP 和 gob 构建远程调用
网络编程是构建分布式系统的基础。Go 的 net 包把复杂的 socket 编程变得简单,让你能专注于业务逻辑。
练习时间
- 文件传输服务器:实现一个 TCP 服务器,支持客户端上传和下载文件
- DNS 查询器:用 UDP 实现一个简单的 DNS 查询工具
- 端口扫描器:写一个工具,扫描指定主机的开放端口
- 聊天室:完善 TCP 聊天室,支持多用户、私聊、房间等功能
我们下篇见!👋
参考资料:
TCP 套接字选项详解
Go 的 net 包提供了 TCPConn 类型,可以通过 Set 系列方法设置各种 TCP 选项:
package main
import (
"fmt"
"net"
"syscall"
"time"
)
func createOptimizedConn() (net.Conn, error) {
dialer := &net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}
// 设置自定义控制函数
dialer.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
// 启用 TCP_NODELAY,禁用 Nagle 算法
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_NODELAY, 1)
// 设置接收缓冲区
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUF, 65536)
// 设置发送缓冲区
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_SNDBUF, 65536)
})
}
return dialer.Dial("tcp", "example.com:80")
}
func main() {
fmt.Println("TCP 套接字优化选项已设置")
}
连接池实现
package main
import (
"context"
"fmt"
"net"
"sync"
"time"
)
// ConnPool 带有 TTL 的连接池
type ConnPool struct {
mu sync.Mutex
conns []net.Conn
factory func() (net.Conn, error)
maxSize int
ttl time.Duration
}
func NewConnPool(factory func() (net.Conn, error), maxSize int, ttl time.Duration) *ConnPool {
return &ConnPool{
conns: make([]net.Conn, 0, maxSize),
factory: factory,
maxSize: maxSize,
ttl: ttl,
}
}
func (p *ConnPool) Get() (net.Conn, error) {
p.mu.Lock()
if len(p.conns) > 0 {
conn := p.conns[len(p.conns)-1]
p.conns = p.conns[:len(p.conns)-1]
p.mu.Unlock()
return conn, nil
}
p.mu.Unlock()
return p.factory()
}
func (p *ConnPool) Put(conn net.Conn) {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.conns) < p.maxSize {
p.conns = append(p.conns, conn)
} else {
conn.Close()
}
}
func (p *ConnPool) Close() {
p.mu.Lock()
defer p.mu.Unlock()
for _, conn := range p.conns {
conn.Close()
}
p.conns = p.conns[:0]
}
func main() {
fmt.Println("连接池示例实现完成")
}
常见错误与 FAQ
Q: net.Listen 的 backlog 是多少?
A: Go 使用操作系统的默认 backlog 值。在 Linux 上,可以通过 /proc/sys/net/core/somaxconn 查看和修改。
Q: 如何处理 “too many open files” 错误?
A: 这是系统文件描述符限制导致的。可以通过 ulimit -n 查看限制,在 Linux 上修改 /etc/security/limits.conf 增加限制,同时在程序中确保正确关闭连接。
Q: Go 的网络 IO 使用 epoll 吗?
A: 在 Linux 上,Go 的运行时确实使用 epoll 来监控文件描述符。但 Go 的调度器隐藏了这些细节,开发者通过阻塞调用即可实现高效的事件驱动。
延伸阅读
- Go net 包文档
- TCP/IP 详解
- Linux 高性能服务器编程
- Go netpoll — Go 网络轮询器源码
HTTP 连接复用与 keep-alive
Go 的 http.Transport 默认启用 HTTP keep-alive,通过连接池复用 TCP 连接,显著提升 HTTP 请求的吞吐量。
package main
import (
"fmt"
"net/http"
"time"
)
func createOptimizedClient() *http.Client {
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
MaxConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DisableKeepAlives: false,
}
return &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
}
func main() {
client := createOptimizedClient()
resp, err := client.Get("https://example.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
}
自定义 TCP 协议服务器
package main
import (
"bufio"
"fmt"
"net"
"strings"
)
func startEchoServer(addr string) error {
listener, err := net.Listen("tcp", addr)
if err != nil {
return err
}
defer listener.Close()
fmt.Printf("Server listening on %s\n", addr)
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept error:", err)
continue
}
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
line, err := reader.ReadString('\n')
if err != nil {
return
}
msg := strings.TrimSpace(line)
if msg == "QUIT" {
conn.Write([]byte("Bye!\n"))
return
}
response := fmt.Sprintf("Echo: %s\n", msg)
conn.Write([]byte(response))
}
}
func main() {
go startEchoServer(":8080")
select {} // 保持运行
}
延伸阅读
- High Performance Browser Networking — Ilya Grigorik 的网络性能经典
- Go net/http 源码 — 标准库 HTTP 实现
- 网络编程指南 — 通用的 socket 编程教程
进阶概念
TCP 三次握手与四次挥手
TCP 建立连接需要三次握手:客户端发送 SYN,服务端回复 SYN+ACK,客户端回复 ACK。这个过程在 Go 的 net.Dial 和 net.Listen/Accept 内部透明完成。
关闭连接需要四次挥手:主动关闭方发送 FIN,被动方确认 ACK;待被动方数据发送完毕后也发送 FIN,主动方确认 ACK。TIME_WAIT 状态会保持 2MSL(通常 60 秒),防止最后 ACK 丢失后被动方重发 FIN 无法被正确处理。
Go 应用中大量短连接会导致大量 TIME_WAIT 状态连接,可能耗尽端口。解决方法是复用连接(HTTP keep-alive)或减少连接数。
套接字选项
net 包通过 net.ListenConfig 和 net.Dialer 提供套接字选项配置:
// 配置监听
lc := net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
// 设置 SO_REUSEADDR
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
// 设置 TCP_NODELAY 禁用 Nagle 算法
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_NODELAY, 1)
})
},
}
ln, _ := lc.Listen(context.Background(), "tcp", ":8080")
// 配置连接
dialer := net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}
conn, _ := dialer.Dial("tcp", "example.com:80")
tcpConn := conn.(*net.TCPConn)
tcpConn.SetNoDelay(true)
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)
SO_REUSEPORT
SO_REUSEPORT 允许多个进程绑定到同一端口,操作系统在连接到达时使用负载均衡算法分发,是实现多进程/多 goroutine 监听同一端口的常用方式:
lc := net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1)
})
},
}
ln, _ := lc.Listen(context.Background(), "tcp", ":8080")
连接复用
频繁创建/销毁连接的开销包括三次握手、TLS 握手和连接预热。连接复用通过连接池将连接保持打开以供后续请求使用。HTTP/1.1 keep-alive、HTTP/2 多路复用、net/http 默认的 Transport 连接池都是连接复用的实现。
零拷贝
零拷贝通过避免在用户态和内核态之间不必要的数据拷贝来提升性能。Go 中可利用 sendfile 实现文件到 socket 的零拷贝传输:
func serveFile(conn net.Conn, path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return err
}
// 使用 sendfile(Go http.ServeContent 内部使用此优化)
_, err = io.CopyN(conn, file, stat.Size())
return err
}
Go 标准库的 net/http 在发送静态文件时自动使用 sendfile 零拷贝。
实战案例
1. 自定义 TCP 服务器
package main
import (
"bufio"
"fmt"
"log"
"net"
"sync"
"time"
)
type Server struct {
listener net.Listener
conns map[net.Conn]struct{}
mu sync.RWMutex
wg sync.WaitGroup
shutdownCh chan struct{}
}
func NewServer(addr string) (*Server, error) {
ln, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
return &Server{
listener: ln,
conns: make(map[net.Conn]struct{}),
shutdownCh: make(chan struct{}),
}, nil
}
func (s *Server) handleConn(conn net.Conn) {
defer func() {
s.mu.Lock()
delete(s.conns, conn)
s.mu.Unlock()
conn.Close()
s.wg.Done()
}()
s.mu.Lock()
s.conns[conn] = struct{}{}
s.mu.Unlock()
reader := bufio.NewReader(conn)
for {
line, err := reader.ReadString('\n')
if err != nil {
return
}
response := fmt.Sprintf("Echo: %s", line)
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
conn.Write([]byte(response))
}
}
func (s *Server) Start() {
for {
select {
case <-s.shutdownCh:
return
default:
}
conn, err := s.listener.Accept()
if err != nil {
select {
case <-s.shutdownCh:
return
default:
log.Printf("accept error: %v", err)
continue
}
}
s.wg.Add(1)
go s.handleConn(conn)
}
}
func (s *Server) Shutdown() {
close(s.shutdownCh)
s.listener.Close()
s.mu.RLock()
for conn := range s.conns {
conn.Close()
}
s.mu.RUnlock()
s.wg.Wait()
}
2. 反向代理
// 简单 TCP 反向代理
func proxy(dst, src net.Conn) {
defer dst.Close()
defer src.Close()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
io.Copy(dst, src)
}()
go func() {
defer wg.Done()
io.Copy(src, dst)
}()
wg.Wait()
}
func startProxy(localAddr, backendAddr string) error {
ln, err := net.Listen("tcp", localAddr)
if err != nil {
return err
}
defer ln.Close()
for {
client, err := ln.Accept()
if err != nil {
log.Printf("accept error: %v", err)
continue
}
go func(c net.Conn) {
backend, err := net.Dial("tcp", backendAddr)
if err != nil {
c.Close()
return
}
proxy(backend, c)
}(client)
}
}
3. 心跳检测
type HeartbeatConn struct {
net.Conn
interval time.Duration
timeout time.Duration
stopCh chan struct{}
}
func NewHeartbeatConn(conn net.Conn, interval, timeout time.Duration) *HeartbeatConn {
hc := &HeartbeatConn{
Conn: conn,
interval: interval,
timeout: timeout,
stopCh: make(chan struct{}),
}
go hc.heartbeatLoop()
return hc
}
func (hc *HeartbeatConn) heartbeatLoop() {
ticker := time.NewTicker(hc.interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
hc.SetWriteDeadline(time.Now().Add(hc.timeout))
if _, err := hc.Write([]byte{0x00}); err != nil {
hc.Close()
return
}
case <-hc.stopCh:
return
}
}
}
func (hc *HeartbeatConn) Close() error {
close(hc.stopCh)
return hc.Conn.Close()
}
4. 连接池
type Pool struct {
mu sync.Mutex
conns chan net.Conn
factory func() (net.Conn, error)
maxConns int
}
func NewPool(maxConns int, factory func() (net.Conn, error)) *Pool {
return &Pool{
conns: make(chan net.Conn, maxConns),
factory: factory,
maxConns: maxConns,
}
}
func (p *Pool) Get() (net.Conn, error) {
select {
case conn := <-p.conns:
return conn, nil
default:
return p.factory()
}
}
func (p *Pool) Put(conn net.Conn) {
select {
case p.conns <- conn:
default:
conn.Close()
}
}
性能对比:net 包 vs epoll/kqueue
| 维度 | Go net 包 | Linux epoll | macOS/BSD kqueue |
|---|---|---|---|
| 实现方式 | goroutine per connection | 事件驱动,单线程/多线程 | 事件驱动 |
| 并发模型 | M:N 调度 | O(1) 事件通知 | O(1) 事件通知 |
| 适用场景 | 大多数网络应用 | 极高并发(百万连接) | 极高并发 |
| 代码复杂度 | 低 | 中 | 中 |
| 性能 | 优秀(百万级连接) | 极致 | 极致 |
Go 的 goroutine 模型在大多数情况下性能足够优秀(单个 goroutine 最小栈仅 2KB,百万连接仅占用约 2GB 内存)。只有在极端场景(如 C10M 问题)下才需要直接操作 epoll/kqueue。
// Go net 百万连接服务器性能特征
// 内存:~2GB(2KB * 1M goroutine)
// CPU:调度器负担随连接数线性增长
// 适用:绝大多数生产场景
FAQ
Q1:如何处理大量 TIME_WAIT 连接?
A:复用连接(keep-alive)、调整系统参数(net.ipv4.tcp_tw_reuse)、或使用长连接池。
Q2:TCP 粘包怎么解决?
A:定义应用层协议(如消息前加长度字段)、使用固定长度消息、或用特殊分隔符。
Q3:UDP 如何保证可靠传输?
A:UDP 本身不可靠,需在应用层实现重传、顺序、去重等机制,或使用 KCP、QUIC 等协议。
Q4:Go 的网络编程是否使用了 epoll?
A:Go 网络 I/O 在 Linux 底层确实使用 epoll(由 runtime 的 netpoll 实现),但开发者无需直接接触。
Q5:如何限制每个 IP 的连接数?
A:维护一个连接计数器(map[string]int),超过阈值时拒绝新连接。
Q6:SetReadDeadline 和 SetWriteDeadline 的区别?
A:读超时控制读取操作的等待时间,写超时控制写入操作的完成时间。超时后连接不会被关闭,但 I/O 操作会返回超时错误。
Q7:为什么 Accept 放在循环中?
A:Accept 每次只返回一个新连接,需要循环持续接受新客户端。使用 goroutine 让每个连接并发处理。
Q8:如何优雅关闭 TCP 连接?
A:先关闭写端(tcpConn.CloseWrite() 关闭写方向,发送 FIN),等待对方也关闭连接,再完全关闭。
最佳实践
- 每连接一个 goroutine:Go 的轻量 goroutine 是处理并发的最佳方式
- 设置 I/O 超时:避免僵尸连接长期占用资源
- 处理粘包:设计应用层消息协议(如 TLV 格式)
- 连接池:避免频繁创建/销毁连接
- 优雅关闭:使用 sync.WaitGroup 等待处理中的连接
- 心跳检测:及时发现静默断开的连接
- 限制连接数:防止资源耗尽和 DoS 攻击
- TLS 加密:生产环境必须使用 TLS
延伸阅读
- Go 网络 I/O 模型
- TCP/IP 详解
- Go net 包源码
- Linux
man 7 socket、man 7 tcp
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。