1. Socket基础回顾:TCP/IP协议栈、Socket通信模型、客户端/服务器架构、Socket类型与协议族

好,咱们正式开始。第一章我打算带大家快速过一遍Socket的基础知识。别觉得基础就简单,我见过太多人在高并发场景下栽跟头,最后发现根子就在这些基础概念上没吃透。说白了,地基不稳,楼盖得再高也是危房。

1.1 TCP/IP协议栈——数据是怎么跑起来的?

Socket编程,本质上是在跟TCP/IP协议栈打交道。你写的每一行代码,最终都会落到这个协议栈上。

我个人习惯把TCP/IP协议栈分成四层来理解,从上到下分别是:

  • 应用层:HTTP、FTP、SMTP……你直接接触的协议都在这一层。我们写的Socket代码,也属于应用层。
  • 传输层:TCP和UDP的老家。负责端到端的通信,帮你把数据从进程A送到进程B。
  • 网络层:IP协议的地盘。负责寻址和路由,决定数据包下一跳该往哪走。
  • 网络接口层:网卡、驱动、物理介质。把数据变成电信号发出去。

数据从应用层往下传,每经过一层,都会加上该层的头部信息。这个过程叫封装。接收端反过来,一层层剥开头部,叫解封装。

核心要点:Socket是应用层与传输层之间的抽象接口。你通过Socket发数据,内核帮你完成剩下的封装、路由、传输工作。

我画了一张图,把数据封装和解封装的过程展示出来,这样更直观:

TCP/IP协议栈数据封装与解封装过程 发送端 应用层 用户数据 传输层 TCP头部 + 数据 网络层 IP头部 + TCP + 数据 网络接口层 帧头部 + IP + ... 物理网络传输 比特流在网线/光纤中传输 接收端 网络接口层 去掉帧头部 网络层 去掉IP头部 传输层 去掉TCP头部 应用层 原始用户数据

我的经验:排查网络问题时,我习惯先定位问题出在协议栈的哪一层。比如connect超时,多半是网络层或传输层的问题;而数据发出去收不到响应,可能是应用层协议没对好。一层层往上查,效率最高。

1.2 Socket通信模型——一个文件描述符的故事

Socket到底是什么?很多人背过定义:它是网络通信的端点。但我觉得更直白的理解是——Socket就是一个文件描述符。

在Linux里,一切皆文件。你打开一个普通文件,得到一个fd;你创建一个Socket,内核也返回一个fd。你对这个fd做read/write,就是在收发网络数据。内核帮你把数据从用户态拷贝到内核态,再通过协议栈发出去。

Socket通信的基本流程,我总结成下面这张表:

步骤 服务端 客户端
1 socket() — 创建Socket socket() — 创建Socket
2 bind() — 绑定地址和端口 —
3 listen() — 进入监听状态 —
4 accept() — 等待客户端连接 connect() — 发起连接
5 recv()/send() — 收发数据 send()/recv() — 收发数据
6 close() — 关闭连接 close() — 关闭连接

嗯,这里要注意:bind()这一步,客户端通常不需要显式调用。内核会在你调用connect()时自动分配一个临时端口。但服务端必须bind,不然客户端怎么知道连谁?

避坑指南:我曾经遇到过一个线上问题,服务端重启后报"Address already in use"。原因是上次的Socket没有完全关闭,端口还被占用着。解决方案是在bind()之前设置SO_REUSEADDR选项。这个坑,几乎每个新手都会踩一次。

1.3 客户端/服务器架构——谁主动,谁被动?

客户端/服务器架构,说白了就是谁主动发起请求,谁就是客户端;谁被动等待请求,谁就是服务器。

这个模型有几个关键特征:

  • 不对称性:服务器通常运行在固定地址上,7×24小时待命。客户端按需启动,用完就关。
  • 请求-响应模式:客户端先说话,服务器后回应。当然也有服务器主动推送的场景,但那是在连接建立之后的事。
  • 并发处理:一个服务器要同时服务成千上万个客户端。这就是咱们这门课的核心——怎么让服务器扛住高并发。

我见过不少刚入行的朋友,写了个单线程的echo server,测了三个客户端没问题,就以为大功告成了。结果线上流量一上来,直接卡死。为什么?因为单线程模型下,一个请求没处理完,后面的全得排队等着。

核心认知:客户端/服务器架构本身不复杂,复杂的是在保证正确性的前提下,把并发能力做到极致。后面的章节,我们会一步步拆解这个问题。

1.4 Socket类型与协议族——选对工具做对事

Socket编程里,有两个概念你必须搞清楚:协议族和Socket类型。

协议族决定了你用什么网络协议栈。最常用的两个:

  • AF_INET:IPv4协议族。绝大多数场景都在用。
  • AF_INET6:IPv6协议族。IPv6迟早要全面铺开,建议新项目直接支持。

Socket类型决定了数据的传输方式。主流的有两种:

类型 协议 特点 典型场景
SOCK_STREAM TCP 面向连接、可靠、有序、无边界 HTTP、FTP、数据库连接
SOCK_DGRAM UDP 无连接、不可靠、无序、有边界 DNS、视频直播、游戏状态同步

你想想看,SOCK_STREAM就像打电话——先拨号建立连接,然后一句一句说,顺序不会乱。而SOCK_DGRAM就像寄明信片——写好地址扔邮筒,能不能到、什么时候到,全看运气。

选哪个?看业务需求。需要可靠传输,选TCP;对实时性要求高、能容忍丢包,选UDP。我做过一个游戏服务器项目,玩家的位置同步用的UDP,但登录和支付走的是TCP。各取所长嘛。

下面这张图把Socket类型和协议族的关系梳理清楚了:

Socket类型与协议族关系图 协议族 (AF_INET) 协议族 (AF_INET6) SOCK_STREAM SOCK_DGRAM TCP Socket UDP Socket 组合规则 • AF_INET + SOCK_STREAM → TCP Socket(可靠、有序、面向连接) • AF_INET + SOCK_DGRAM → UDP Socket(不可靠、无序、无连接) • AF_INET6 同理,只是地址长度和格式不同

创建Socket的代码很简单,但参数的含义要清楚:

#include <sys/socket.h>

// 创建一个TCP Socket
int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);

// 创建一个UDP Socket
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);

第三个参数protocol通常传0,表示让内核根据前两个参数自动选择默认协议。SOCK_STREAM默认对应TCP,SOCK_DGRAM默认对应UDP。

小技巧:我习惯在创建Socket后,立刻检查返回值。如果返回-1,说明创建失败,需要看errno。很多线上问题就是资源耗尽导致socket()失败,但代码里没做检查,后面拿着无效的fd继续操作,直接崩了。