网络编程基础
你给异地的朋友寄过信吗?信封上写收件人地址,邮局按地址一层层转发,对方拆信读内容。计算机网络干的是同一件事,只不过把"信"换成"字节",把"邮局"换成路由器,把"地址"换成 IP。CSAPP 第 11 章讲的就是:程序员怎么用一套统一的接口(socket),让两台机器像读写文件一样交换数据。
本文把客户端-服务器模型、协议分层、TCP/UDP、socket 接口、字节序坑、以及一个最小 Echo 服务器一次讲透。代码能跑的我都跑了,跑不了的(C 版需要 gcc)明确标出来。
一、客户端-服务器模型:一切网络的原点
不管上层多花哨,绝大多数网络应用都逃不出这个模型:
flowchart LR
C[客户端<br/>发起请求 等响应] -->|网络| S[服务器<br/>常驻 被动等待请求]
S -->|响应| C
- 客户端是主动方:在某个时刻启动,向已知地址发起连接,拿到结果就走人(浏览器、SSH 客户端都是)。
- 服务器是被动方:开机就常驻,绑定一个端口,死循环
accept等待连接,来一个服务一个。
这个模型妙在"对称被打破"——客户端不用暴露地址,服务器必须有一个稳定的"门牌号"(IP + 端口),客户端才能找上门。
二、网络长什么样:四层协议栈
数据从你的程序到对面机器,要穿过四层。每层只跟上下层打交道,各自解决一类问题:
graph TB
A[应用层 HTTP / FTP / DNS / 你的代码] --> B[传输层 TCP 或 UDP]
B --> C[网络层 IP 负责寻址与路由]
C --> D[链路层 以太网 / WiFi 管相邻节点]
D --> E[物理层 电缆 光纤 电磁波]
- 应用层:你真正关心的"信的内容"(HTTP 报文、
echo的字符串)。 - 传输层:决定"这条连接可靠不可靠"(TCP 保证送达且有序,UDP 只管扔)。
- 网络层:IP 负责把包从源主机路由到目的主机,靠 IP 地址寻址。
- 链路层 / 物理层:在一条网线或一段 WiFi 里把比特传过去。
程序员写 socket 程序时,主要碰应用层 + 传输层;IP 和链路层由内核和网卡代劳。
三、TCP vs UDP:可靠还是够快
传输层两个主角,选哪个决定你应用的性格:
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接,发就完了 |
| 可靠性 | 丢包重传、按序到达、无重复 | 不保证,可能丢、乱序 |
| 流量/拥塞控制 | 有,自动限速 | 无,应用自己管 |
| 单位 | 字节流(无消息边界) | 数据报(保留边界) |
| 典型场景 | 网页、SSH、数据库 | 视频通话、DNS 查询、游戏 |
字节流这个特性是新手最大的坑:TCP 不保留你 send 的边界。你分两次 send("AB")、send("CD"),对方一次 recv(1024) 可能拿到 "ABCD"。要"消息边界"得自己在应用层加长度前缀或分隔符。
TCP 建立连接靠三次握手,防的是"旧连接残包"被新连接误收:
sequenceDiagram
participant C as 客户端
participant S as 服务器
C->>S: SYN (seq=x)
S->>C: SYN-ACK (seq=y, ack=x+1)
C->>S: ACK (ack=y+1)
Note over C,S: 双向通道建立,开始传数据
四、Socket 抽象:一切皆文件描述符
Unix 的哲学是"一切皆文件"。socket 创建出来也是一个文件描述符(fd),你用 read/write 的近亲 recv/send 去读写它,内核在背后把字节塞进网卡。
一个 TCP 会话从建立到结束,双方要按顺序调一套固定的系统调用:
sequenceDiagram
participant C as 客户端
participant S as 服务器
Note over S: socket() + bind() + listen()
S->>S: accept() 阻塞等连接
C->>C: socket() + connect()
C->>S: send("ping")
S->>C: recv + 处理 + send("echo: ping")
C->>S: recv 拿到响应
C->>C: close()
S->>S: close()
记住这组顺序:socket → bind → listen → accept(服务器)和 socket → connect(客户端),后面写代码就是填空。
五、动手:最小 Echo 服务器
Echo 服务是网络编程的 "Hello World":客户端发什么,服务器原样回什么。下面先给 CSAPP 风格的 C 骨架(忠实于课本,但需要 gcc 编译,本机无编译器,故不伪造输出,请你在 Linux/macOS 上 gcc echo.c csapp.c -o echo 自跑):
1 | /* echo_server.c —— CSAPP 风格骨架(需 gcc 编译) */ |
下面这段是我真的跑过的 Python 版,本地回环自连自收,输出逐字引用:
1 | import socket, threading, time |
运行输出:
1 | [Demo2] 服务器收到: ping 网络编程 |
两行打印一对上,说明"客户端发 → 服务器收 → 服务器回 → 客户端收"这条链路通了。SO_REUSEADDR 那行是实战经验:否则端口刚关还处在 TIME_WAIT 时重启会报 "Address already in use",加上它就能立刻复用。
六、字节序与地址结构:htons 为什么不能省
不同 CPU 存多字节整数的顺序不一样:x86 是小端(低字节在前),网络规定用大端(网络字节序)。一个端口号 8080 = 0x1F90,在小端内存里是 90 1f,在网线上必须是 1f 90。如果不转换,对端解出来的端口就是 0x901F = 36911,connection 永远建不上。
sockaddr_in 里的端口和协议族要走 htons(host→network short),而 IP 地址用 inet_aton 已经返回网络序,不能再转一次。下面这段演示了"组装一个 sockaddr_in 并原样还原"的全过程,是真实运行结果:
1 | import socket, struct |
运行输出:
1 | [Demo1] 完整 8 字节 = 02001f907f000001 |
0200 是协议族、1f90 是端口、尾随 7f000001 正是 127.0.0.1 的网络序。注意这里用 <HH4s(小端)来"原样拍出内存字节",才能看到真实的线序 1f90;若用大端格式拍会被再翻转一次,那才是不对的展示。
七、往上走一层:HTTP 与 Web 服务器最小骨架
socket 之上最常见的应用层协议是 HTTP。一个 Web 事务就是"请求-响应"两段文本:
flowchart LR
B[浏览器] -->|GET /index.html HTTP/1.1| S[Web 服务器]
S -->|200 OK 换行 HTML 正文| B
B -->|解析渲染| P[页面]
服务器读到空行就知道请求头结束,回一个 状态行 + 响应头 + 空行 + body。最小骨架(概念版):
1 | /* 读请求行 -> 查文件 -> 拼 HTTP 响应 -> 写回,循环 accept */ |
真实服务器还要处理并发(一个连接阻塞不能卡住其他用户)——这正是下一章"并发"要解决的,也是为什么 CSAPP 把网络放在并发之前讲:你先会写"一次服务一个"的服务器,再学会"同时服务很多个"。
八、写网络程序最常踩的坑
- 短读(short count):
recv一次未必收满你要的字节数。网络数据可能分片到达,必须循环读直到凑够,或改用带缓冲的Rio_readlineb按行读。 - 字节流无边界:见第三节,别假设一次
send对应一次recv。 - 端口 TIME_WAIT:主动关闭的一方会 hold 住端口几十秒,
SO_REUSEADDR解君愁。 - 阻塞卡死:默认 socket 是阻塞的,一个慢客户端能拖垮整个服务器——要么每连接一个线程/进程,要么用
select/poll/epoll多路复用,要么非阻塞 + 事件循环。 - 忘了
close:连接泄漏,文件描述符耗尽,服务器慢慢"假死"。
🐾 小结
| 概念 | 一句话 | 你会用到的接口 |
|---|---|---|
| 客户端-服务器 | 客户端主动、服务器常驻被动 | connect / accept |
| 分层 | 应用管内容、传输管可靠、IP 管寻址 | socket 之上写应用层 |
| TCP vs UDP | 要可靠选 TCP,要快选 UDP | SOCK_STREAM / SOCK_DGRAM |
| socket = fd | 像读写文件一样收发网络字节 | recv / send |
| 字节序 | x86 小端,网络大端,端口必 htons |
htons / ntohs / inet_aton |
| 并发前置 | 单连接服务器好写,多连接才需并发 | 下一章 |
网络编程的本质就一句:把"文件读写"的直觉,搬到一个跨主机的、按字节流传输的 fd 上,并替内核处理好字节序和边界。 把这套骨架刻进肌肉记忆,后面学 HTTP、RPC、分布式都不虚。
下一篇进入《并发:线程与锁》——你会看到怎么让这个 Echo 服务器同时招呼成百上千个客户端而不互相踩脚。

