【本文正在参加金石计划附加挑战赛------第四期命题】
前言
本篇文章给掘友们介绍 Linux 操作系统的几种网络 I/O 模型。
阻塞式I/O
- 描述 : 在阻塞模式下,系统调用(如
read
或recv
)会阻塞进程,直到数据准备好。只有在数据准备好并被完全复制到用户空间后,调用才会返回。 - 优点: 实现简单,代码逻辑直观。
- 缺点: 阻塞会导致线程闲置等待,浪费资源,无法高效处理大量并发连接。
- 使用场景: 小规模的网络应用或性能要求不高的程序。
go
package main
import (
"fmt"
"net"
)
func main() {
// 创建 TCP 监听器
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()
fmt.Println("Server listening on :8080")
for {
// 阻塞等待客户端连接
conn, err := listener.Accept()
if err != nil {
fmt.Println("Connection error:", err)
continue
}
// 处理连接(阻塞读取)
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
for {
// 阻塞读取数据
n, err := conn.Read(buf)
if err != nil {
fmt.Println("Read error:", err)
return
}
fmt.Printf("Received: %s\n", string(buf[:n]))
conn.Write([]byte("Echo: " + string(buf[:n])))
}
}
非阻塞式I/O
- 描述 : 非阻塞模式下,系统调用会立即返回,而不管数据是否准备好。如果数据尚未准备好,系统会返回错误(如
EAGAIN
)。程序可以不断轮询检查数据是否可用。 - 优点: 线程不会被阻塞,可以用于高性能的网络服务。
- 缺点: 频繁轮询会导致高 CPU 占用。
- 使用场景: 简单场景的非阻塞网络服务。
go
package main
import (
"fmt"
"net"
"syscall"
"time"
)
func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()
fmt.Println("Server listening on :8080")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Connection error:", err)
continue
}
// 设置非阻塞模式
if tcpConn, ok := conn.(*net.TCPConn); ok {
fd, _ := tcpConn.File()
syscall.SetNonblock(int(fd.Fd()), true)
}
go handleNonBlocking(conn)
}
}
func handleNonBlocking(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
for {
// 非阻塞读取
n, err := conn.Read(buf)
if err != nil {
time.Sleep(10 * time.Millisecond) // 避免忙轮询
continue
}
fmt.Printf("Received: %s\n", string(buf[:n]))
conn.Write([]byte("Echo: " + string(buf[:n])))
}
}
多路复用I/O
- 描述 : 使用系统调用(如
select
、poll
或epoll
)监听多个文件描述符。当某些文件描述符变为就绪状态时,调用返回,由程序处理对应的 I/O 操作。 - 优点: 支持处理大量连接而无需为每个连接分配线程。
- 缺点 :
select
和poll
在大规模连接时效率较低,而epoll
在这方面表现优异。 - 使用场景 : 高并发的网络服务器,如 Nginx 使用的就是
epoll
。
go
package main
import (
"fmt"
"net"
"syscall"
)
func main() {
ln, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer ln.Close()
listenerFd := int(ln.(*net.TCPListener).File().Fd())
// 设置监听 socket 为非阻塞
syscall.SetNonblock(listenerFd, true)
pollFds := []syscall.PollFd{
{Fd: int32(listenerFd), Events: syscall.POLLIN},
}
fmt.Println("Server listening on :8080")
connections := make(map[int]net.Conn)
for {
// 等待文件描述符就绪
_, err := syscall.Poll(pollFds, -1)
if err != nil {
panic(err)
}
// 检查监听 socket 是否有新连接
if pollFds[0].Revents&syscall.POLLIN != 0 {
conn, err := ln.Accept()
if err == nil {
fd := int(conn.(*net.TCPConn).File().Fd())
syscall.SetNonblock(fd, true)
connections[fd] = conn
pollFds = append(pollFds, syscall.PollFd{Fd: int32(fd), Events: syscall.POLLIN})
}
}
// 处理现有连接
for i := 1; i < len(pollFds); i++ {
fd := pollFds[i].Fd
if pollFds[i].Revents&syscall.POLLIN != 0 {
buf := make([]byte, 1024)
n, err := connections[int(fd)].Read(buf)
if err == nil {
fmt.Printf("Received: %s\n", string(buf[:n]))
connections[int(fd)].Write([]byte("Echo: " + string(buf[:n])))
} else {
// 关闭连接并移除
connections[int(fd)].Close()
pollFds = append(pollFds[:i], pollFds[i+1:]...)
delete(connections, int(fd))
i--
}
}
}
}
}
信号驱动I/O
- 描述 : 通过为文件描述符设置信号(如
SIGIO
),当数据准备好时内核发送信号通知应用程序进行处理。 - 优点: 无需主动轮询,资源占用较低。
- 缺点: 实现复杂,信号处理可能引入额外的复杂性和延迟。
- 使用场景: 适合对响应时间要求高的应用,但实际使用不多。
go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
// 打开一个文件描述符(可以是 socket 文件描述符)
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
if err != nil {
panic(err)
}
defer syscall.Close(fd)
// 绑定地址
addr := &syscall.SockaddrInet4{Port: 8080}
copy(addr.Addr[:], []byte{127, 0, 0, 1})
if err := syscall.Bind(fd, addr); err != nil {
panic(err)
}
// 设置非阻塞模式
if err := syscall.SetNonblock(fd, true); err != nil {
panic(err)
}
// 设置信号驱动模式
if err := syscall.FcntlInt(fd, syscall.F_SETFL, syscall.O_ASYNC); err != nil {
panic(err)
}
// 设置当前进程为信号目标
if err := syscall.FcntlInt(fd, syscall.F_SETOWN, syscall.Getpid()); err != nil {
panic(err)
}
// 监听
if err := syscall.Listen(fd, 10); err != nil {
panic(err)
}
// 捕获 SIGIO 信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGIO)
fmt.Println("Server listening on :8080")
for {
select {
case sig := <-sigCh:
if sig == syscall.SIGIO {
fmt.Println("SIGIO received: Ready for I/O")
connFd, _, err := syscall.Accept(fd)
if err == nil {
fmt.Println("Connection accepted:", connFd)
go handleConnection(connFd)
} else {
fmt.Println("Accept error:", err)
}
}
}
}
}
func handleConnection(connFd int) {
defer syscall.Close(connFd)
buf := make([]byte, 1024)
for {
n, err := syscall.Read(connFd, buf)
if err != nil {
fmt.Println("Read error:", err)
return
}
fmt.Printf("Received: %s\n", string(buf[:n]))
syscall.Write(connFd, []byte("Echo: "+string(buf[:n])))
}
}
异步I/O
- 描述 : 使用异步系统调用(如
aio_read
),内核会在后台处理 I/O。当 I/O 操作完成后,通过回调或信号通知应用程序。 - 优点: 应用程序完全非阻塞,效率高。
- 缺点: 实现复杂,Linux 的异步 I/O 支持并不成熟,使用较少。
- 使用场景: 需要极高性能的系统中。
go
package main
import (
"fmt"
"net"
)
func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()
fmt.Println("Server listening on :8080")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Connection error:", err)
continue
}
go handleAsync(conn)
}
}
func handleAsync(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
// 模拟异步读写
readCh := make(chan []byte)
go func() {
for {
n, err := conn.Read(buf)
if err != nil {
close(readCh)
return
}
readCh <- buf[:n]
}
}()
for data := range readCh {
fmt.Printf("Received: %s\n", string(data))
conn.Write([]byte("Echo: " + string(data)))
}
}
直接I/O
- 描述 : 跳过内核缓存,直接在用户空间和设备之间传输数据。通过设置特定标志(如
O_DIRECT
)实现。 - 优点: 减少数据拷贝,适合 I/O 密集型场景。
- 缺点: 实现复杂,对应用程序要求较高。
- 使用场景: 数据库、存储服务等高性能系统。
go
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
// 打开文件,使用 O_DIRECT 标志
file, err := syscall.Open("/tmp/testfile", syscall.O_RDWR|syscall.O_DIRECT|syscall.O_CREAT, 0666)
if err != nil {
panic(err)
}
defer syscall.Close(file)
// 数据必须是对齐的(例如 512 字节对齐)
buf := make([]byte, 512)
copy(buf, []byte("Hello, Direct I/O!"))
// 写入文件
n, err := syscall.Write(file, buf)
if err != nil {
panic(err)
}
fmt.Printf("Written %d bytes\n", n)
// 将文件指针移回开头
if _, err := syscall.Seek(file, 0, 0); err != nil {
panic(err)
}
// 读取文件
readBuf := make([]byte, 512)
n, err = syscall.Read(file, readBuf)
if err != nil {
panic(err)
}
fmt.Printf("Read %d bytes: %s\n", n, string(readBuf[:n]))
}
总结
- 单线程或少量连接 : 使用 阻塞 I/O 或 非阻塞 I/O。
- 高并发服务器 : 首选 I/O 多路复用 (推荐
epoll
)。 - 对性能极高要求 : 尝试 异步 I/O 或 直接 I/O。
每种模型的选择依赖于具体的应用场景和性能需求。