
Golang多路复用的核心逻辑
Golang的多路复用处理请求,本质是通过单线程或多线程监听多个网络连接,当某个连接有读写事件时才触发对应的处理逻辑,避免为每个连接单独创建线程造成的资源浪费。Golang的标准库已经封装了底层操作系统的IO多路复用机制,比如Linux下的epoll、macOS下的kqueue,开发者可以直接基于net包快速实现多路复用服务。
基于net包的基础多路复用实现
net包中的Listen函数会创建一个监听socket,然后通过Accept方法阻塞等待新连接,结合goroutine可以为每个连接分配独立的处理逻辑,这是最常见的多路复用实现方式。
package main
import (
"fmt"
"net"
"bufio"
)
func handleConn(conn net.Conn) {
defer conn.Close()
// 读取客户端发送的数据
reader := bufio.NewReader(conn)
data, err := reader.ReadString('\n')
if err != nil {
fmt.Println("读取数据失败:", err)
return
}
fmt.Println("收到客户端数据:", data)
// 向客户端返回响应
conn.Write([]byte("已收到你的请求\n"))
}
func main() {
// 监听本地8080端口
listener, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("监听端口失败:", err)
return
}
defer listener.Close()
fmt.Println("服务启动,监听端口8080")
// 循环接收新连接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("接收连接失败:", err)
continue
}
// 为每个连接启动一个goroutine处理,实现多路复用
go handleConn(conn)
}
}结合channel的优雅多路复用方案
如果需要对连接做统一的管理,比如限制最大并发数、批量关闭连接,可以结合channel来传递连接和信号,让多路复用的逻辑更清晰。
package main
import (
"fmt"
"net"
"bufio"
"sync"
)
type Server struct {
listener net.Listener
connChan chan net.Conn
stopChan chan struct{}
wg sync.WaitGroup
}
func NewServer(port string) (*Server, error) {
listener, err := net.Listen("tcp", port)
if err != nil {
return nil, err
}
return &Server{
listener: listener,
connChan: make(chan net.Conn, 100),
stopChan: make(chan struct{}),
}, nil
}
func (s *Server) start() {
// 启动接收连接的goroutine
s.wg.Add(1)
go s.acceptConn()
// 启动工作goroutine处理连接,这里启动3个工作协程
for i := 0; i < 3; i++ {
s.wg.Add(1)
go s.worker()
}
fmt.Println("服务启动,等待连接")
}
func (s *Server) acceptConn() {
defer s.wg.Done()
for {
select {
case <-s.stopChan:
return
default:
conn, err := s.listener.Accept()
if err != nil {
fmt.Println("接收连接失败:", err)
continue
}
// 把新连接发送到channel中
s.connChan <- conn
}
}
}
func (s *Server) worker() {
defer s.wg.Done()
for {
select {
case <-s.stopChan:
return
case conn := <-s.connChan:
s.handleConn(conn)
}
}
}
func (s *Server) handleConn(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
data, err := reader.ReadString('\n')
if err != nil {
fmt.Println("读取数据失败:", err)
return
}
fmt.Println("工作协程处理请求:", data)
conn.Write([]byte("请求处理完成\n"))
}
func (s *Server) stop() {
close(s.stopChan)
s.listener.Close()
s.wg.Wait()
fmt.Println("服务已停止")
}
func main() {
server, err := NewServer(":8080")
if err != nil {
fmt.Println("启动服务失败:", err)
return
}
server.start()
// 模拟运行一段时间后停止
select {}
}不同多路复用方案对比
可以根据业务场景选择合适的实现方式,以下是两种常见方案的对比:
| 方案类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 基础net包+goroutine | 实现简单,代码易懂,无需额外管理逻辑 | 连接数过多时goroutine数量随之增加,资源消耗较高 | 连接数较少、请求处理简单的场景 |
| channel+固定工作协程 | 可以限制并发数,资源可控,便于统一管理连接 | 需要额外编写管理逻辑,实现复杂度略高 | 高并发、需要控制资源消耗的场景 |
注意事项
- 处理连接时一定要记得调用
Close()方法关闭连接,避免资源泄漏 - 如果业务逻辑中有耗时操作,不要阻塞工作协程,避免影响其他请求的处理
- 生产环境中需要添加日志、错误上报、优雅关闭等逻辑,提升服务的稳定性
需要注意的是,Golang的net包底层已经自动使用了操作系统的IO多路复用机制,开发者不需要手动调用epoll相关的系统调用,直接基于标准库开发即可满足大部分场景的需求。