1. Socket基础回顾:TCP/IP协议栈、Socket是什么、Socket类型(TCP/UDP)、Socket工作流程、Socket API概览

好,咱们正式开始。第一章,我打算把Socket最基础的东西再过一遍。你可能会想:“这些东西我都知道,还用讲?” 嗯,我当年也这么想。但后来在项目里踩了不少坑,才发现很多问题就出在最基础的理解上。所以,这一章咱们不赶进度,把地基夯实了。

1.1 TCP/IP协议栈:数据是怎么“包”起来的?

Socket编程,说白了就是操作TCP/IP协议栈。你得先明白数据从你的应用程序到网线,中间经历了什么。

我个人习惯把TCP/IP协议栈想象成一个“俄罗斯套娃”的过程。数据从应用层下来,每一层都会给它套上一个“头”(头部信息)。

TCP/IP 协议栈数据封装过程 应用层 (HTTP/FTP/自定义协议) 原始数据 (Data) 传输层 (TCP/UDP) TCP头 / UDP头 + 数据 网络层 (IP) IP头 + TCP头/UDP头 + 数据 网络接口层 (以太网/网卡驱动) 以太网帧头 + IP头 + TCP头/UDP头 + 数据 + 帧尾 封装方向 ↓ 数据发送 每层加头部 ↓ 接收端解封装 ↑ 数据接收 每层去掉头部

你看这张图,数据从上往下走,每经过一层就加一个头。接收端反过来,从下往上走,每层剥掉一个头。这就是封装和解封装。

我的经验: 调试网络问题时,我经常用 Wireshark 抓包。你看到的就是这些“套娃”的原始模样。能看懂每一层的头部,你就能定位 90% 的网络问题。

1.2 Socket是什么?别把它想得太玄乎

Socket,中文叫“套接字”。很多教材把它讲得很抽象。其实,你可以把它理解成“网络世界的文件描述符”。

在Linux里,一切皆文件。你打开一个本地文件,得到一个文件描述符(fd),然后读写它。Socket也一样:你创建一个Socket,得到一个fd,然后通过它跟另一台机器上的程序通信。

我记得刚入行时,有个老大哥跟我说:“Socket就是两个程序之间的电话线。” 一端拨号(connect),另一端接听(accept),然后两边对着话筒说话(send/recv)。这个比喻我一直用到现在。

1.3 Socket类型:TCP vs UDP,你该怎么选?

Socket主要分两种:流式Socket(SOCK_STREAM) 和 数据报Socket(SOCK_DGRAM)。它们背后对应的就是TCP和UDP协议。

很多新手会纠结:“我该用TCP还是UDP?” 我建议你先看业务场景。下面这个表,是我自己总结的,你直接拿去用。

特性 TCP (SOCK_STREAM) UDP (SOCK_DGRAM)
连接 面向连接(先建立连接再通信) 无连接(直接发数据)
可靠性 可靠传输(有确认、重传机制) 不可靠(发出去就不管了)
数据边界 无边界(流式,可能粘包) 有边界(每个包独立)
速度 相对较慢(有握手和确认开销) 快(无握手,无确认)
典型应用 HTTP、FTP、邮件、数据库连接 DNS、视频直播、在线游戏、VoIP
避坑指南: 我曾经在一个视频监控项目里,为了追求“实时性”选了UDP。结果上线后,画面经常花屏、卡顿。后来发现,UDP虽然快,但丢包了不会重传。对于视频流,少量丢包可以接受,但如果网络质量差,丢包率超过5%,画面就完全没法看了。最后我们还是在UDP之上自己加了一层简单的确认重传机制。所以,选型时别只看“快”,要看你的业务能不能容忍丢包。

1.4 Socket工作流程:一次完整的“握手”

咱们以最常用的TCP Socket为例,走一遍完整的工作流程。这就像两个人打电话,有固定的步骤。

TCP Socket 工作流程 服务器端 1. socket() 创建Socket 2. bind() 绑定IP和端口 3. listen() 监听连接 4. accept() 阻塞等待客户端 5. recv() / send() 收发数据 6. close() 关闭连接 客户端 1. socket() 创建Socket 2. connect() 连接服务器 3. send() / recv() 收发数据 4. close() 关闭连接 三次握手 数据双向传输 四次挥手 注意:服务器端的 accept() 是阻塞的,直到有客户端连接进来才会返回。

流程很清晰,对吧?服务器是“被动方”,先启动,等着别人来连。客户端是“主动方”,去连服务器。这里面有几个关键点,我单独拎出来说说。

1.5 Socket API概览:常用的几个函数

API不用死记硬背,用多了自然就熟了。我把最核心的几个列出来,你有个印象就行。

函数 作用 关键参数
socket() 创建一个Socket domain (AF_INET), type (SOCK_STREAM), protocol (0)
bind() 将Socket绑定到本地IP和端口 sockfd, sockaddr结构体, addrlen
listen() 将Socket设为监听模式 sockfd, backlog (最大等待连接数)
accept() 接受一个客户端连接 sockfd, 客户端sockaddr, addrlen
connect() 客户端向服务器发起连接 sockfd, 服务器sockaddr, addrlen
send() 发送数据 sockfd, buf, len, flags
recv() 接收数据 sockfd, buf, len, flags
close() 关闭Socket sockfd

咱们来看一个最简单的TCP回射服务器(Echo Server)的代码片段,感受一下这些API是怎么串起来的。

// 服务器端核心代码 (C语言风格)
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);

// 1. 创建Socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);

// 2. 绑定IP和端口
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
server_addr.sin_port = htons(8080);       // 端口8080
bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

// 3. 开始监听
listen(server_fd, 5); // 最大等待队列长度为5

// 4. 接受客户端连接 (阻塞)
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);

// 5. 收发数据
char buffer[1024] = {0};
recv(client_fd, buffer, sizeof(buffer), 0);
send(client_fd, buffer, strlen(buffer), 0); // 原样返回

// 6. 关闭连接
close(client_fd);
close(server_fd);

注意: 上面的代码只是为了演示流程,完全没有做错误处理。在实际项目中,每个API调用后都必须检查返回值。比如 socket() 返回 -1 表示创建失败,accept() 返回 -1 表示接受失败。忽略这些检查,是线上事故最常见的根源之一。

嗯,基础就这些。你可能会觉得:“就这?太简单了。” 别急,这只是开胃菜。后面的章节,咱们会把这些基础放到真实的企业级场景里,看看它们是怎么被玩出花样的。比如,高并发下怎么处理成千上万个连接?粘包问题怎么解决?这些才是真正的挑战。