2. Socket 类型与协议:TCP Socket、UDP Socket、Unix Domain Socket
Socket 这东西,说白了就是应用层与传输层之间的一个编程接口。很多刚入行的同事容易把 Socket 和协议混为一谈,其实 Socket 是门,协议是路。你走哪条路,决定了你选哪扇门。
我个人习惯把 Socket 分成三大类:TCP Socket、UDP Socket、Unix Domain Socket。这三类在生产环境中几乎覆盖了 99% 的通信场景。下面我逐个拆开讲,顺便聊聊我在线上踩过的坑。
2.1 TCP Socket:可靠但需要伺候
TCP Socket 是面向连接的、可靠的、基于字节流的传输层 Socket。你想想看,HTTP、MySQL、Redis、Kafka,这些主流服务底层几乎全是 TCP。
为什么大家都选 TCP?因为它帮你干了三件脏活:丢包重传、顺序保证、流量控制。你只管往缓冲区里写数据,TCP 栈会帮你把数据切成合适的段,发出去,收回来,拼好。
但可靠是有代价的。我在项目中遇到过一个问题:某次线上服务突然大量报 Connection timed out,排查下来发现是 TCP 三次握手超时。默认的 tcp_syn_retries 是 6 次,每次超时时间指数退避,最长能卡 127 秒。用户等不了那么久。
TCP Socket 核心属性
- 协议类型:SOCK_STREAM
- 传输单位:字节流(无边界)
- 连接方式:三次握手建立,四次挥手关闭
- 可靠性:确认重传、序列号、校验和
- 典型场景:Web 服务、数据库连接、消息队列
创建 TCP Socket 的代码很简单,但生产环境里真正要命的不是创建,而是参数调优。我贴一段常见的服务端代码,注意看 setsockopt 那几行:
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
exit(1);
}
// 避坑:一定要设置 SO_REUSEADDR
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
listen(sock, 128);
while (1) {
int client = accept(sock, NULL, NULL);
// 处理客户端请求...
}
我曾经踩过的坑:有一次线上服务重启后报 Address already in use,就是因为没有设置 SO_REUSEADDR。TCP 的 TIME_WAIT 状态会占用端口 2MSL 时间,默认约 60 秒。不加这个选项,服务重启就得等一分钟,这在生产环境是不可接受的。
2.2 UDP Socket:快但不靠谱
UDP Socket 是无连接的、不可靠的、基于数据报的传输层 Socket。它不握手、不确认、不重传,发出去就不管了。
你可能会问:那为什么还要用 UDP?因为快。DNS 查询、DHCP 请求、视频直播、VoIP 语音,这些场景对实时性要求高,丢几个包无所谓,但延迟高了就卡顿。
我记得有一次排查 DNS 解析慢的问题,发现客户端的 DNS 查询用的是 TCP 而不是 UDP。因为某些网络设备会截断大于 512 字节的 UDP DNS 响应,导致客户端回退到 TCP。这一回退,延迟直接翻倍。
| 对比维度 | TCP Socket | UDP Socket |
|---|---|---|
| 协议类型 | SOCK_STREAM | SOCK_DGRAM |
| 连接状态 | 面向连接 | 无连接 |
| 数据边界 | 无边界(字节流) | 有边界(数据报) |
| 可靠性 | 可靠 | 不可靠 |
| 传输效率 | 较低(有开销) | 较高(无开销) |
| 典型应用 | HTTP、MySQL、Redis | DNS、DHCP、NTP、RTP |
UDP Socket 的代码比 TCP 更简洁,因为没有连接维护:
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket");
exit(1);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(5353);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
char buf[1500];
struct sockaddr_in client;
socklen_t len = sizeof(client);
while (1) {
int n = recvfrom(sock, buf, sizeof(buf), 0,
(struct sockaddr *)&client, &len);
// 处理数据报...
}
生产环境建议:UDP 的接收缓冲区默认通常只有 212992 字节(约 208KB)。如果业务流量大,建议调大 net.core.rmem_default 和 net.core.rmem_max。我曾经遇到过 DNS 服务器因为缓冲区太小导致丢包,调大后问题解决。
2.3 Unix Domain Socket:本地通信的王者
Unix Domain Socket(UDS)是同一台机器上进程间通信的专用 Socket。它不走网络协议栈,不经过网卡,直接在内核层面拷贝数据。
说白了,UDS 比 TCP Loopback 快 30%-50%。为什么?因为 TCP Loopback 虽然也在本机,但它仍然要走完整的 TCP 协议栈——三次握手、校验和、滑动窗口,这些全是开销。UDS 直接绕过了这些。
我在项目中遇到过这样一个场景:Nginx 和 PHP-FPM 之间通信,原来用的是 TCP 的 127.0.0.1:9000。后来改成 Unix Socket /var/run/php-fpm.sock,QPS 直接提升了 15%。
UDS 的两种类型
- SOCK_STREAM:面向连接的流式通信,类似 TCP
- SOCK_DGRAM:面向数据报的通信,类似 UDP
注意:UDS 的 SOCK_DGRAM 是可靠的,不会丢包,和网络 UDP 完全不同。
UDS 的创建方式和网络 Socket 类似,只是地址族不同:
// 服务端
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/app.sock");
unlink("/tmp/app.sock"); // 避免文件已存在
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
listen(sock, 128);
// 客户端
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/app.sock");
connect(sock, (struct sockaddr *)&addr, sizeof(addr));
我曾经踩过的坑:UDS 文件有权限问题。有一次我把 Socket 文件放在了 /tmp 下,结果其他用户启动的进程连不上。后来我统一把 UDS 文件放在 /var/run 下,并设置 chmod 777 或使用 umask 控制权限。另外,记得在服务退出时 unlink 清理 Socket 文件,否则下次启动会报 Address already in use。
2.4 三种 Socket 的选型决策
嗯,这里我画了一张图,帮你快速决策该用哪种 Socket:
2.5 生产环境避坑清单
最后,我总结几个生产环境中常见的 Socket 问题,都是我用真金白银换来的教训:
- TCP 端口耗尽:客户端频繁创建短连接,导致大量 TIME_WAIT 堆积,端口被占满。解决方案:启用连接池,或调大
net.ipv4.ip_local_port_range。 - UDP 缓冲区溢出:接收端处理速度跟不上发送端,导致内核丢包。解决方案:调大
rmem_max,或改用 TCP。 - UDS 文件残留:进程异常退出后,Socket 文件没被清理,下次启动失败。解决方案:启动前先
unlink,或使用abstract socket(Linux 特有,不生成文件)。 - Socket backlog 太小:高并发下
accept来不及处理,连接被内核丢弃。解决方案:调大listen的 backlog 参数,同时配合net.core.somaxconn内核参数。
快速自查命令:
ss -tlnp:查看 TCP 监听端口ss -ulnp:查看 UDP 监听端口ss -xlp:查看 Unix Domain Socketnetstat -s:查看协议栈统计信息,包括丢包、重传
三种 Socket 各有各的脾气。TCP 可靠但慢,UDP 快但不靠谱,UDS 快又可靠但只能本机用。选型的时候想清楚你的场景要什么,别盲目跟风。嗯,这一章就到这里,记住这些坑,线上能少熬几个夜。