跳转至

Socket 基础

是什么 / 解决什么问题

Socket 是进程间网络通信的抽象接口。通过 socket API,程序可以在不关心底层网络协议细节的情况下进行 TCP/UDP 通信。

使用模式

TCP 服务器基本流程

#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

// 1. 创建 socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// AF_INET:  IPv4
// AF_INET6: IPv6
// SOCK_STREAM: TCP (可靠、有序、面向连接)
// SOCK_DGRAM:  UDP (不可靠、无序、无连接)

// 2. 绑定地址
struct sockaddr_in addr = {
    .sin_family = AF_INET,
    .sin_port = htons(8080),        // 端口号(网络字节序)
    .sin_addr.s_addr = INADDR_ANY,  // 监听所有网卡
};
bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr));

// 3. 开始监听
listen(listen_fd, 128);  // backlog: 未完成连接队列大小

// 4. 接受连接
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &addrlen);

// 5. 读写数据
char buf[1024];
ssize_t n = read(conn_fd, buf, sizeof(buf));
write(conn_fd, buf, n);

// 6. 关闭
close(conn_fd);
close(listen_fd);

TCP 客户端基本流程

int fd = socket(AF_INET, SOCK_STREAM, 0);

struct sockaddr_in server = {
    .sin_family = AF_INET,
    .sin_port = htons(8080),
};
inet_pton(AF_INET, "192.168.1.1", &server.sin_addr);

// 连接服务器
connect(fd, (struct sockaddr *)&server, sizeof(server));

// 收发数据
write(fd, "hello", 5);
char buf[1024];
ssize_t n = read(fd, buf, sizeof(buf));

close(fd);

UDP 通信(无连接)

// 服务端
int fd = socket(AF_INET, SOCK_DGRAM, 0);
bind(fd, (struct sockaddr *)&addr, sizeof(addr));

struct sockaddr_in client;
socklen_t clen = sizeof(client);
ssize_t n = recvfrom(fd, buf, sizeof(buf), 0,
                     (struct sockaddr *)&client, &clen);
sendto(fd, reply, reply_len, 0,
       (struct sockaddr *)&client, clen);

// 客户端
int fd = socket(AF_INET, SOCK_DGRAM, 0);
sendto(fd, msg, msg_len, 0, (struct sockaddr *)&server, sizeof(server));
recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);

创建时直接设置属性(Linux 扩展)

// SOCK_NONBLOCK: 创建时就是非阻塞
// SOCK_CLOEXEC: exec 时自动关闭
int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);

// accept4 也支持
int conn = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);

行为与语义

listen 的 backlog 参数

客户端 connect ──→ [SYN队列(半连接)] ──→ [Accept队列(全连接)] ──→ accept() 取走
                   三次握手进行中          三次握手完成,等待accept

backlog 控制的是 Accept 队列(全连接队列)的大小。
SYN 队列大小由 /proc/sys/net/ipv4/tcp_max_syn_backlog 控制。
  • listen(fd, 128) — Accept 队列最大 128 个已完成连接
  • 队列满时新连接的 SYN 会被静默丢弃(客户端会重试)
  • 实际值被 /proc/sys/net/core/somaxconn 截断(默认 4096)

read/write vs recv/send vs recvfrom/sendto

API 适用 额外功能
read/write TCP socket
recv/send TCP socket flags 参数(MSG_PEEK, MSG_NOSIGNAL 等)
recvfrom/sendto UDP socket 可指定/获取对端地址
recvmsg/sendmsg 任意 辅助数据、scatter/gather、fd 传递

地址复用

// 为什么服务器重启时 bind 报 "Address already in use"?
// 因为旧连接处于 TIME_WAIT 状态,地址还被占用

int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
// 允许 bind 到处于 TIME_WAIT 的地址

setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));
// 允许多个 socket bind 到同一个 address:port(负载均衡)

shutdown vs close

close(fd);
// 关闭 fd,引用计数减1。只有引用计数为0时才真正关闭连接。
// 如果 fd 被 dup/fork 过,close 不会立即断开。

shutdown(fd, SHUT_RD);    // 关闭读端(后续 read 返回 0)
shutdown(fd, SHUT_WR);    // 关闭写端(发送 FIN,对端 read 返回 0)
shutdown(fd, SHUT_RDWR);  // 同时关闭

// shutdown 不管引用计数,立即影响底层连接
// 典型用途:通知对端"我不再发送数据了"

性能考量

  • SO_REUSEPORT:多线程服务器各自 bind+listen,内核负载均衡,消除 accept 锁竞争
  • accept4:避免额外的 fcntl 调用设置 NONBLOCK/CLOEXEC
  • TCP_FASTOPEN:减少一次 RTT(客户端在 SYN 中携带数据)
  • 连接池:避免频繁 connect 的三次握手和 TIME_WAIT 开销

常见陷阱与 FAQ

1. 忘记网络字节序转换

// 错误:直接赋值端口号
addr.sin_port = 8080;  // 在小端机器上变成 0x901F

// 正确:htons 转换
addr.sin_port = htons(8080);  // 正确的网络字节序 0x1F90

2. connect 超时太长

默认 TCP connect 超时约 2 分钟(内核重试 SYN)。对于需要快速失败的场景:

// 方法1:非阻塞 connect + epoll + 自定义超时
// 方法2:setsockopt 设置 TCP_USER_TIMEOUT
int timeout_ms = 3000;
setsockopt(fd, IPPROTO_TCP, TCP_USER_TIMEOUT, &timeout_ms, sizeof(timeout_ms));

3. read 返回的数据量不确定

TCP 是字节流,没有消息边界:

// 发送方:
write(fd, "hello world", 11);

// 接收方可能收到:
// 一次 read: "hello world" (11字节)
// 或两次 read: "hello" + " world"
// 或三次 read: "hel" + "lo wo" + "rld"

// 需要应用层协议界定消息边界(长度前缀、分隔符等)

观测与调试

# 查看监听端口
ss -tlnp

# 查看连接状态
ss -tnp dst :8080

# 查看 backlog 使用情况
ss -tlnp | grep 8080
# Recv-Q: 当前 Accept 队列中的连接数
# Send-Q: Accept 队列最大值(backlog)

# strace 看连接过程
strace -e trace=socket,bind,listen,accept4,connect -f ./server

延伸阅读

  • man 7 socket — socket 通用概念
  • man 7 tcp / man 7 udp — 协议特定行为
  • man 7 ip — IP 层选项
  • Stevens《Unix Network Programming》第一卷 — 经典教材