网络编程:用 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 聊天室,支持多用户、私聊、房间等功能
我们下篇见!👋
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。