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发数据,内核帮你完成剩下的封装、路由、传输工作。
我画了一张图,把数据封装和解封装的过程展示出来,这样更直观:
我的经验:排查网络问题时,我习惯先定位问题出在协议栈的哪一层。比如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的代码很简单,但参数的含义要清楚:
#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继续操作,直接崩了。