跳转到正文
zeno's blog
返回

Linux I/O(三):BSD socket 编程手册

专题: Linux I/O

Table of contents

Open Table of contents

TL;DR

socket API 的本质是把”可寻址的双向通信”塞进 fd 抽象(everything is a file),代价是必须通过 setsockopt/shutdown/getsockopt 等元操作补齐 TCP 状态机的语义;生产代码的复杂度集中在短读/短写循环EAGAIN/EINTR 重试SIGPIPE 屏蔽字节序转换TIME_WAIT 与 SO_REUSEADDR 的误解这五个点上。


1. Why:socket 解决了什么问题

4.2BSD(1983)之前 Unix 的 IPC 是碎片化的:pipe 只能亲缘进程、FIFO 单向无跨机、SysV IPC 臃肿、ARPANET NCP 协议特定。没有统一的、可寻址、双向、可跨机的通信 API

socket 通过三层解耦消除了这个碎片化:

  1. 地址族(domain)与协议解耦AF_INET / AF_INET6 / AF_UNIX / AF_PACKET 共用一套 socket/bind/listen/accept/connect/send/recv
  2. 通信模式与协议解耦SOCK_STREAM / SOCK_DGRAM / SOCK_SEQPACKET / SOCK_RAW 成为正交维度
  3. endpoint 变成 fd — 可被 read/write/close/dup/select/poll/epoll 统一处理

TCP socket vs UDP socket 的 API 本质差异

维度SOCK_STREAM (TCP)SOCK_DGRAM (UDP)
建立连接必须 connect()listen/accept可选(仅为固定 peer)
数据边界recv(n) 返回 [1, n] 任意长度;一次 recvfrom = 一个完整数据报
可靠性内核保证到达 + 顺序 + 去重无,应用层负责
recv 返 0对端 FIN(优雅关闭)合法:收到 0 长度数据报
关闭shutdown → 四次挥手close 直接释放

最本质的差异是消息边界。TCP 应用必须自己做 framing(length-prefix、delimiter、HTTP chunked),UDP 天然有 framing 但要自己处理丢包和乱序。


2. sockaddr 家族的多态设计

BSD 1983 年设计时 C 没有 void*,于是用 “tagged struct + 强制转型” 模拟多态:

// 所有 sockaddr_XX 的第一字段都是 sa_family_t —— 这是 tag
struct sockaddr     { sa_family_t sa_family; char sa_data[14]; };
struct sockaddr_in  { sa_family_t sin_family; in_port_t sin_port;
                      struct in_addr sin_addr; unsigned char sin_zero[8]; };
struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port;
                      uint32_t sin6_flowinfo; struct in6_addr sin6_addr;
                      uint32_t sin6_scope_id; };
struct sockaddr_storage { sa_family_t ss_family; char __ss_padding[...]; };

所有 API 统一用 struct sockaddr* + socklen_t,调用方先填具体结构再 reinterpret_cast

sockaddr_storage 的用途accept / recvfrom 时你不知道对端是 v4 还是 v6,这个类型保证对齐和大小能容纳任意地址族。生产代码里接收端用它,发送端用具体 sockaddr_in / sockaddr_in6

为什么不用 union:BSD 尝试过但放弃——union 要求所有成员在同一翻译单元可见,会让 <sys/socket.h> 耦合所有协议头。“tag + 长度自描述” 换来了协议头解耦和 ABI 扩展性(加 IPv6 无需改 <sys/socket.h>),代价是失去类型安全。


3. 字节序:network byte order = big-endian

转换函数<arpa/inet.h>):

函数宽度用途
htons / ntohs16-bit端口号
htonl / ntohl32-bitIPv4 地址常量(INADDR_ANY、INADDR_LOOPBACK)
htobe64 / be64toh64-bit自定义协议 payload

黄金规则


4. 核心 API 精确语义

所有行为描述以 man7.org 当前版本为准,Linux 6.x 基准。

4.1 socket / accept4 的现代创建法

// 推荐:一步到位原子设置 NONBLOCK + CLOEXEC(Linux 2.6.27+)
int fd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
int c  = ::accept4(listen_fd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC);

必须用 SOCK_CLOEXEC:多线程程序中,A 线程 socket() 与后续 fcntl(F_SETFD, FD_CLOEXEC) 之间存在窗口,B 线程可能 fork+execve 导致 fd 泄漏到子进程。原子设置消除这个竞态。

accept 不继承 O_NONBLOCK(man accept(2) 原文警告):即使 listening socket 是非阻塞,accept 返回的新 socket 默认是阻塞的。这是与 BSD 不同的 Linux 行为。用 accept4 原子设置能避免事后 fcntl 的竞态 + 额外 syscall。

4.2 listen 的 backlog 真实语义

backlog 控制的是 accept queue(全连接队列),不是 SYN queue:

队列状态上限控制
SYN queue收到 SYN 未完成三次握手/proc/sys/net/ipv4/tcp_max_syn_backlog
accept queue三次握手完,等 accept 取走listen(fd, backlog),但会被 /proc/sys/net/core/somaxconn 静默截断

Linux 5.4+ somaxconn 默认 4096(之前是 128,commit 19f92a030ca6)。

accept queue 溢出的行为net.ipv4.tcp_abort_on_overflow=0 默认):新 SYN 被默默丢弃,依赖客户端 SYN 重传。表现:client connect() 看似成功但发数据后 server 无响应——监控上需要看 ss -lnt 的 Send-Q 列或 nstat | grep ListenOverflow

4.3 非阻塞 connect 的正确流程

阻塞 connect 默认 SYN 重试 6 次,总耗时约 127 秒才返回 ETIMEDOUT,会把业务线程拖死。非阻塞 connect 的标准流程:

  1. connect() 返回 -1 + errno == EINPROGRESS → 进行中
  2. poll/epoll 监听 POLLOUT 并带超时
  3. POLLOUT 触发后必须 getsockopt(fd, SOL_SOCKET, SO_ERROR, ...) 读真实结果
    • err == 0 真的连上了
    • err != 0 连接失败,err 是实际错误码

为什么必须用 SO_ERRORconnect() 已经返回过了不能再调;POLLOUT 只告诉你”可写”不区分”握手成功的可写”和”RST 后关闭的可写”。只有 SO_ERROR 能区分。

4.4 send / recv 返回值语义

短写短读是 TCP 的常态,不是 bug。正确代码必须循环到发完/收够,或遇 EAGAIN 切到事件驱动。

关键 MSG_ flags

Flag方向用途
MSG_NOSIGNALsend对端关闭时返 EPIPE 而不杀进程(生产必用)
MSG_DONTWAIT双向本次调用非阻塞,不改 socket 全局标志
MSG_PEEKrecv不从内核缓冲区移除,用于协议 sniff
MSG_WAITALLrecv尽量凑够 len 才返回(仍可能被信号打断)
MSG_MOREsend累积发送,类似 TCP_CORK

4.5 shutdown vs close

维度shutdown(fd, how)close(fd)
作用对象TCP 状态机(直接操作连接)fd 引用计数
多进程共享 fd 时所有共享者都看到连接关闭只减 1;引用到 0 才真正关
半关闭支持SHUT_RD / SHUT_WR / SHUT_RDWR总是全关
数据丢失风险无(SHUT_WR 发 FIN,仍可读 ACK 的数据)(见下)

close 的数据丢失:默认是”后台异步发送剩余数据 + FIN”。进程随即 _exit 或内核资源紧张时数据可能丢失。需要可靠送达:shutdown(fd, SHUT_WR) → 循环 recv 到返 0 → close

Linux 的 close 遇 EINTR 不能重试(man close(2)):fd 已经被释放,重试会关到可能被其他线程 reuse 的 fd。直接丢弃错误继续执行。

4.6 getaddrinfo 替代 gethostbyname

gethostbyname 返回静态 buffer 线程不安全、只支持 IPv4、API 已废弃。新代码一律 getaddrinfo

C++17 下用 std::unique_ptr<addrinfo, decltype(&::freeaddrinfo)> 做 RAII。


5. SO_* 选项速查(生产必知)

SOL_SOCKET 层

选项类型关键点
SO_REUSEADDRbool允许绑已有 active listener 的地址;只允许绑 TIME_WAIT 中的地址
SO_REUSEPORTboolLinux 3.9+,多 socket 绑同 (ip:port),内核按四元组 hash 负载均衡;所有 socket 必须同 UID
SO_KEEPALIVEbool只是开关,行为由下面 TCP_KEEPxxx 三参数决定
SO_LINGERstruct三态,见下
SO_RCVBUF/SO_SNDBUFint内核自动翻倍;上限 rmem_max/wmem_max一旦手动设 autotuning 关闭(生产建议不设)
SO_RCVTIMEO/SO_SNDTIMEOtimeval对 select/poll/epoll 无效,只对阻塞 I/O
SO_ERRORint (只读)非阻塞 connect 必查

SO_REUSEADDR vs SO_REUSEPORT(经常搞混)

SO_REUSEADDRSO_REUSEPORT
版本BSD 以来Linux 3.9+
解决TIME_WAIT 复用真正多 socket 绑同端口 + 内核负载均衡
谁能绑无 active listener 时都设此选项且同 UID
典型场景服务重启Nginx worker 并行 accept / DPDK

SO_LINGER 三态

struct linger { int l_onoff; int l_linger; };
配置close 行为
{0, *}(默认)立即返回,内核后台发 FIN 走正常四次挥手
{1, N>0}阻塞最多 N 秒等所有数据 ACK;超时强制 RST
{1, 0}立即发 RST,丢弃未发数据,跳过 TIME_WAIT

典型误用{1, N} 不保证对端应用层读走了数据,只保证对端 TCP ACK 了。真正可靠关闭要应用层双向确认。

{1, 0} 只用于紧急断连(安全场景、防慢连接占资源),绝对不能用在正常业务关闭——对端会得到 ECONNRESET 误以为是网络错误。

TCP 层选项(man tcp(7))

选项默认关键点
TCP_NODELAY0(Nagle 开)关 Nagle,小包立即发
TCP_KEEPIDLE7200 秒空闲多久首次探测。默认 2 小时对生产几乎无用,必须调到 60
TCP_KEEPINTVL75 秒探测间隔。生产建议 10
TCP_KEEPCNT9 次探测失败多少次判定死链。生产建议 3
TCP_USER_TIMEOUT0(系统默认)未 ACK 数据强制关的总超时(ms),比 keepalive 更直接
TCP_DEFER_ACCEPT0listen 上设置后,accept 只在收到首个数据包时返回
TCP_FASTOPEN0Linux 3.6/3.7+,现实中中间盒破坏导致采用率低

Nagle vs delayed ACK 互相加害:Nagle 说”小包攒着”,delayed ACK 说”ACK 攒着”,两端互等产生 40ms 延迟。交互式协议一律 TCP_NODELAY=1

IPV6_V6ONLY 的跨平台陷阱

跨平台代码必须显式 setsockopt(IPV6_V6ONLY, ...),永远不依赖平台默认。


6. 错误码语义与处理策略

errno含义严重性恢复策略
EAGAIN / EWOULDBLOCK非阻塞暂无法完成 / 达 SO_xxxTIMEO非错误事件驱动等下次就绪
EINTR被信号打断非错误while 循环重试
ECONNRESET对端 RST(崩溃 / 强关 / SO_LINGER={1,0}连接死关 fd
EPIPEstream 对端已关闭后 send连接死关 fd;默认同时 SIGPIPE 杀进程
ECONNREFUSEDconnect 目标端口无监听失败可重试
ECONNABORTEDaccept 时 client 立即 RST单连接错continue accept 循环
ETIMEDOUTSYN 重传耗尽严重检查网络
EADDRINUSEbind 端口被占 / 4-tuple 冲突严重SO_REUSEADDR;查 TIME_WAIT
EMFILE / ENFILEfd 耗尽极严重ulimit -n;预留占位 fd
ENOBUFS内核缓冲耗尽严重降发送速率

EAGAIN 与 EWOULDBLOCK 在 Linux 上是同一个值(通常 11),但 POSIX 不保证。可移植写法:if (errno == EAGAIN || errno == EWOULDBLOCK)

EINTR 无论是否 SA_RESTART 都应循环重试(带 SO_xxxTIMEO 的 I/O 永远不重启,总是返 EINTR):

ssize_t n;
do { n = ::recv(fd, buf, len, 0); } while (n == -1 && errno == EINTR);

EMFILE 的经典应对模式

accept 循环里遇 EMFILE 会忙转 CPU。解决:启动时预打开 /dev/null 占住一个 fd;EMFILE 时 close 占位 → accept 新连接 → 立即 close 新连接(让 client 重试)→ 重开占位 fd。


7. C++17 可运行代码示例

7.1 阻塞 echo server(最小可用)

编译:g++ -std=c++17 -Wall -Wextra -O2 echo_server.cpp -o echo_server

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <csignal>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <string>
#include <system_error>

[[noreturn]] static void throw_errno(const char* ctx) {
    throw std::system_error(errno, std::system_category(), ctx);
}

int main(int argc, char* argv[]) {
    std::signal(SIGPIPE, SIG_IGN);  // 必须:否则 send 到已关连接会杀进程

    const uint16_t port = (argc > 1) ? static_cast<uint16_t>(std::stoi(argv[1])) : 9000;

    int listen_fd = ::socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
    if (listen_fd < 0) throw_errno("socket");

    int yes = 1;
    ::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));

    sockaddr_in addr{};
    addr.sin_family      = AF_INET;
    addr.sin_port        = htons(port);          // 字节序
    addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (::bind(listen_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0)
        throw_errno("bind");
    if (::listen(listen_fd, 128) < 0) throw_errno("listen");

    for (;;) {
        sockaddr_storage peer{};
        socklen_t peer_len = sizeof(peer);
        int conn = ::accept4(listen_fd, reinterpret_cast<sockaddr*>(&peer),
                             &peer_len, SOCK_CLOEXEC);
        if (conn < 0) {
            if (errno == EINTR || errno == ECONNABORTED) continue;
            if (errno == EMFILE || errno == ENFILE) { std::perror("fd exhausted"); continue; }
            throw_errno("accept4");
        }

        char buf[4096];
        for (;;) {
            ssize_t n;
            do { n = ::recv(conn, buf, sizeof(buf), 0); } while (n < 0 && errno == EINTR);
            if (n == 0) break;                                     // 对端优雅关闭
            if (n < 0) { std::perror("recv"); break; }

            ssize_t sent = 0;
            while (sent < n) {
                ssize_t w = ::send(conn, buf + sent, n - sent, MSG_NOSIGNAL);
                if (w < 0) { if (errno == EINTR) continue; std::perror("send"); goto drop; }
                sent += w;
            }
        }
    drop:
        ::close(conn);
    }
}

7.2 非阻塞 connect + 超时

编译:g++ -std=c++17 -Wall -Wextra -O2 connect_timeout.cpp -o connect_timeout

#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <string>
#include <variant>

struct Ok  { int fd; };
struct Err { int errnum; const char* where; };
using ConnectResult = std::variant<Ok, Err>;  // C++17 无 std::expected

static ConnectResult connect_with_timeout(const char* ip, uint16_t port, int timeout_ms) {
    int fd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
    if (fd < 0) return Err{errno, "socket"};

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(port);
    if (::inet_pton(AF_INET, ip, &addr.sin_addr) != 1) {
        ::close(fd); return Err{EINVAL, "inet_pton"};
    }

    if (::connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == 0) return Ok{fd};
    if (errno != EINPROGRESS) { int e = errno; ::close(fd); return Err{e, "connect"}; }

    pollfd pfd{fd, POLLOUT, 0};
    int pr;
    do { pr = ::poll(&pfd, 1, timeout_ms); } while (pr < 0 && errno == EINTR);
    if (pr == 0) { ::close(fd); return Err{ETIMEDOUT, "poll timeout"}; }
    if (pr  < 0) { int e = errno; ::close(fd); return Err{e, "poll"}; }

    int sock_err = 0;
    socklen_t len = sizeof(sock_err);
    if (::getsockopt(fd, SOL_SOCKET, SO_ERROR, &sock_err, &len) < 0) {
        int e = errno; ::close(fd); return Err{e, "getsockopt"};
    }
    if (sock_err != 0) { ::close(fd); return Err{sock_err, "async connect"}; }
    return Ok{fd};
}

7.3 getaddrinfo 双栈 + RAII

#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <memory>
#include <optional>
#include <string>
#include <string_view>

using AddrInfoPtr = std::unique_ptr<addrinfo, decltype(&::freeaddrinfo)>;

AddrInfoPtr resolve(std::string_view host, std::string_view port,
                    int socktype, bool passive) {
    addrinfo hints{};
    hints.ai_family   = AF_UNSPEC;              // v4 + v6
    hints.ai_socktype = socktype;
    hints.ai_flags    = AI_ADDRCONFIG | (passive ? AI_PASSIVE : 0);

    addrinfo* raw = nullptr;
    std::string h{host}, p{port};
    int rc = ::getaddrinfo(host.empty() ? nullptr : h.c_str(), p.c_str(), &hints, &raw);
    if (rc != 0) return {nullptr, &::freeaddrinfo};
    return {raw, &::freeaddrinfo};
}

std::optional<int> connect_any(const AddrInfoPtr& res) {
    for (addrinfo* ai = res.get(); ai; ai = ai->ai_next) {
        int fd = ::socket(ai->ai_family, ai->ai_socktype | SOCK_CLOEXEC, ai->ai_protocol);
        if (fd < 0) continue;
        if (::connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) return fd;
        ::close(fd);
    }
    return std::nullopt;
}

7.4 RAII socket 包装器

// socket_guard.hpp
#pragma once
#include <sys/socket.h>
#include <unistd.h>
#include <utility>
#include <cerrno>
#include <system_error>

class Socket {
public:
    Socket() = default;
    explicit Socket(int fd) noexcept : fd_(fd) {}

    static Socket create(int domain, int type, int protocol = 0) {
        int fd = ::socket(domain, type, protocol);
        if (fd < 0) throw std::system_error(errno, std::system_category(), "socket");
        return Socket{fd};
    }

    ~Socket() noexcept { close_quiet(); }

    Socket(const Socket&)            = delete;
    Socket& operator=(const Socket&) = delete;
    Socket(Socket&& o) noexcept : fd_(std::exchange(o.fd_, -1)) {}
    Socket& operator=(Socket&& o) noexcept {
        if (this != &o) { close_quiet(); fd_ = std::exchange(o.fd_, -1); }
        return *this;
    }

    [[nodiscard]] int  get() const noexcept { return fd_; }
    [[nodiscard]] bool valid() const noexcept { return fd_ >= 0; }
    [[nodiscard]] int  release() noexcept { return std::exchange(fd_, -1); }

private:
    int fd_ = -1;
    void close_quiet() noexcept {
        if (fd_ >= 0) {
            ::shutdown(fd_, SHUT_WR);    // best-effort FIN
            ::close(fd_);                 // Linux: EINTR 不能重试
            fd_ = -1;
        }
    }
};

8. Pitfalls(生产环境 12 大坑)

8.1 TIME_WAIT 与 SO_REUSEADDR 的误解

:服务重启 bind 返 EADDRINUSE,以为设了 SO_REUSEADDR 就万事大吉。 真相SO_REUSEADDR 只允许绑 TIME_WAIT 中的地址,允许绑有 active listener 的地址。要多进程绑同端口用 SO_REUSEPORT

8.2 SIGPIPE 默认杀进程

对已关闭连接 send,默认 SIGPIPE → 进程死。三重防御signal(SIGPIPE, SIG_IGN) + 每次 send 用 MSG_NOSIGNAL + pthread 线程级屏蔽。

8.3 短写:send 返回值 != 请求长度

必须包装 send_all() 循环到发完或遇 EAGAIN。

8.4 recv 返 0 vs < 0 混淆

== 0 是对端 FIN(优雅关闭),不是错误。只判 < 0 会死循环。

8.5 close 后对端可能收不到已发数据

close 默认后台异步发剩余数据 + FIN,进程立即退出或资源紧张时数据可能丢。可靠关闭:shutdown(SHUT_WR) → 循环 recv 到返 0 → close不要SO_LINGER={1,N} 代替应用层确认。

8.6 accept 队列溢出

server 慢 accept 填满 accept queue,新 SYN 被默默丢弃。client 表现为 connect 成功但发数据后 server 无响应。监控 ss -lnt 的 Send-Q 和 nstat | grep ListenOverflow

8.7 字节序错误

addr.sin_port = 8080;htons,实际连到 0x901f = 36895 端口。端口永远 htonsinet_pton 输出已是 network order 不要再转。

8.8 用 gethostbyname 代替 getaddrinfo

线程不安全 + 只 v4 + API 已废弃。新代码一律 getaddrinfo

8.9 阻塞 connect 无超时

内核 SYN 重试 6 次耗时约 127 秒。推荐非阻塞 connect + poll 超时 + SO_ERROR(见 7.2)。或用 TCP_USER_TIMEOUTSO_SNDTIMEO 在 Linux 上不作用于 connect

8.10 fd 泄漏到子进程

socket() + fork() + 再起新程序时会继承所有 fd。一律用 SOCK_CLOEXEC / accept4(SOCK_CLOEXEC)

8.11 IPv6 双栈行为跨平台不一致

Linux 默认 IPV6_V6ONLY=0(双栈),Windows/FreeBSD/OpenBSD 默认 1。必须显式 setsockopt,不依赖平台默认。

8.12 Linux close 遇 EINTR 不能重试

fd 已释放,重试会关到可能被其他线程 reuse 的 fd。直接吞错误继续。


9. 生产环境 Checklist

Listening socket

setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes));  // 多 worker 负载均衡
int v6only = 1;  // 或 0,显式设定
setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only));

已建立连接

setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes));       // 关 Nagle
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(yes));
int idle=60, intvl=10, cnt=3;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE,  &idle,  sizeof(idle));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl));
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT,   &cnt,   sizeof(cnt));
unsigned int to = 30000;
setsockopt(fd, IPPROTO_TCP, TCP_USER_TIMEOUT, &to, sizeof(to));    // 总超时

内核参数(sysctl

net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_syncookies = 1
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.ip_local_port_range = 10000 65535

资源限制

ulimit -n 1048576          # 或 systemd: LimitNOFILE=1048576

信号

std::signal(SIGPIPE, SIG_IGN);   // 进程启动最前面

观测

ss -lntp                       # Send-Q = 当前 accept queue 深度
nstat | grep -iE 'Overflow|Drops'

参考

所有 man page 行为以 man7.org 当前版本为准:

I/O 多路复用相关见 epoll 使用手册


分享这篇文章:

上一篇
C++ 网络编程:epoll、Reactor 与 one loop per thread
下一篇
GORM(三):生产环境最佳实践