1. Socket 基础概念:什么是 Socket、Socket 与 TCP/IP 的关系、Socket 通信模型
做 SRE 这些年,我处理过不少 Socket 相关的问题。说实话,很多故障的根因,最后都绕回到对 Socket 基础的理解上。这一节,我们就先把地基打牢。
1.1 什么是 Socket?
Socket,中文叫「套接字」。说白了,它就是操作系统提供的一个编程接口,让两个程序能在网络上通信。你想想看,网络通信的细节其实很复杂——数据怎么分包、怎么路由、怎么保证不丢包……这些底层逻辑全在内核里。而 Socket 就是内核给你的一扇门,你只要打开这扇门,往里扔数据,或者从里面取数据,剩下的内核帮你搞定。
我个人习惯把 Socket 理解成「网络世界的文件描述符」。为什么这么说?因为在 Linux 里,Socket 和文件的操作方式几乎一样——你用 socket() 创建,用 read()/write() 读写,最后用 close() 关闭。这种设计让程序员上手非常快。
核心要点:Socket 是应用层与传输层之间的抽象层。它屏蔽了网络协议的复杂性,让开发者只需要关心「发什么」和「收什么」。
1.2 Socket 与 TCP/IP 的关系
这个问题我经常被问到。很多人搞不清 Socket 和 TCP/IP 到底谁是谁。其实很简单:TCP/IP 是一套协议栈,而 Socket 是这套协议栈的 API 封装。
举个例子。TCP/IP 协议栈从上到下分四层:应用层、传输层、网络层、链路层。Socket 就坐在应用层和传输层之间。当你调用 send() 时,Socket 把数据交给 TCP 协议,TCP 负责分段、重传、流量控制,然后交给 IP 层去路由,最后通过网卡发出去。
我记得有一次排查线上延迟问题,发现应用层调用 send() 后迟迟没有返回。当时我第一反应是网络问题,结果抓包一看,数据根本没出本机。最后定位到是 Socket 发送缓冲区满了,TCP 的流量控制机制在起作用。你看,不理解 Socket 和 TCP 的关系,这种问题根本无从下手。
| 层次 | 职责 | Socket 的角色 |
|---|---|---|
| 应用层 | HTTP、FTP、自定义协议 | 通过 Socket API 收发数据 |
| 传输层 | TCP/UDP 协议 | Socket 绑定端口,管理连接状态 |
| 网络层 | IP 协议,路由寻址 | Socket 不直接参与,由内核处理 |
| 链路层 | MAC 地址,物理传输 | Socket 完全透明 |
一个小技巧:当你用 ss -tlnp 查看 Socket 状态时,看到的 ESTAB、TIME_WAIT 这些状态,其实是 TCP 协议的状态机在 Socket 层面的映射。搞懂 TCP 状态迁移图,排查 Socket 问题就成功了一半。
1.3 Socket 通信模型
Socket 通信模型,说白了就是「谁等谁、谁找谁」。最经典的模型是客户端-服务器模型,也叫 C/S 模型。我画了一张图,你看一眼就明白了。
这个模型里,服务器是「被动方」,客户端是「主动方」。服务器先启动,创建 Socket,绑定端口,然后进入监听状态。客户端后启动,创建 Socket,然后主动去连接服务器。连接建立后,两边就可以互相发数据了。
这里有个细节很多人会忽略:服务器端的 accept() 会返回一个新的 Socket 描述符。原来的监听 Socket 继续等新连接,新 Socket 专门处理这个客户端的通信。我见过不少新手写服务器代码时,直接用监听 Socket 收发数据,结果导致后续连接全部失败。
我曾经踩过的坑:有一次线上服务突然拒绝连接,排查发现是监听 Socket 的 backlog 队列满了。backlog 是 listen() 的第二个参数,控制未完成三次握手的连接队列长度。默认值通常只有 128,在高并发场景下很容易被打满。后来我把它调到了 1024,问题解决。
1.4 Socket 的类型
Socket 不只有 TCP 这一种。根据传输层协议的不同,主要分三类:
- 流式 Socket(SOCK_STREAM):基于 TCP,面向连接,可靠,有序。适合 HTTP、数据库连接等场景。
- 数据报 Socket(SOCK_DGRAM):基于 UDP,无连接,不可靠,但速度快。适合 DNS 查询、视频流等场景。
- 原始 Socket(SOCK_RAW):直接操作 IP 层,可以自定义协议头。一般用于网络诊断工具,比如 ping 就是基于原始 Socket 实现的。
我个人在 SRE 工作中,90% 的问题都出在流式 Socket 上。UDP 的问题相对少,但一旦出问题往往更难排查,因为无连接的特性导致你很难追踪状态。
1.5 一个简单的代码示例
光说不练假把式。下面是一个最简的 TCP 服务器和客户端代码,帮你把上面的流程串起来。
// 服务器端(server.c)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, 128);
int client_fd = accept(server_fd, NULL, NULL);
// 这里就可以用 client_fd 收发数据了
close(client_fd);
close(server_fd);
return 0;
}
// 客户端(client.c)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
// 连接建立成功,可以收发数据了
close(sock);
return 0;
}
生产环境小贴士:上面的代码为了演示去掉了错误处理。但在生产环境里,socket()、bind()、connect() 每一步都可能失败。我建议你养成习惯:每次系统调用后都检查返回值,至少打一条日志。很多线上故障,就是因为忽略了这些返回值导致的。