网络编程:用 Go 构建底层网络应用

深入学习 Go 的 net 包,掌握 TCP、UDP 编程以及处理粘包等网络底层问题

网络编程:用 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 的网络编程:

  1. net 包基础:Conn 接口、Read/Write/Close
  2. TCP 编程:服务器、客户端、粘包处理
  3. UDP 编程:无连接的数据报传输
  4. HTTP 底层实现:理解 HTTP 是建立在 TCP 之上的
  5. 超时和 Keep-Alive:网络编程的重要细节
  6. 实战 RPC 框架:用 TCP 和 gob 构建远程调用

网络编程是构建分布式系统的基础。Go 的 net 包把复杂的 socket 编程变得简单,让你能专注于业务逻辑。

练习时间

  1. 文件传输服务器:实现一个 TCP 服务器,支持客户端上传和下载文件
  2. DNS 查询器:用 UDP 实现一个简单的 DNS 查询工具
  3. 端口扫描器:写一个工具,扫描指定主机的开放端口
  4. 聊天室:完善 TCP 聊天室,支持多用户、私聊、房间等功能

我们下篇见!👋


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页