CMU 15-213 CSAPP:网络编程与 Web 服务器的底层构建(Network)

发布时间:2026/7/23 17:50:57
CMU 15-213 CSAPP:网络编程与 Web 服务器的底层构建(Network) 写在前面前面我们探讨了内存、汇编、异常控制流和本地 I/O现在我们要把视线投向外部世界。网络编程其实并不神秘在 Unix 的“万物皆文件”哲学下网络连接不过就是一个特殊的文件描述符Socket fd。在这篇笔记中我们将从底层的以太网聊起一步步搭建起 Socket 管道最后亲手解剖一个真实的 Web 服务器是如何运作的。Lec 21 Network Programming Part 1服务端 客户端 事务 (Client-Server Transaction)几乎所有的网络应用都是基于 Client-Server 模型的。Server (服务端):管理资源Resource提供服务Service。它像一台自动售货机永远在被动等待并响应请求。Client (客户端):主动发起请求获取服务。注意Client 和 Server 是进程 (Process)的概念而不是机器的概念。一台物理主机上可以同时跑着成百上千个 Client 和 Server 进程比如亚马逊的服务器群。最底层以太网段管理在最底层的硬件层面以太网Ethernet负责把数据从一台机器搬到另一台机器。MAC 地址:每块网卡出厂时都有一个全球唯一的 48 位地址如00:16:EA:54:54:E6。Hub (集线器) 与数据帧 (Frames):在早期的以太网中主机把数据切成小块Frames发给 HubHub 会极其“无脑”地把这个 Frame 复制并广播给所有连在上面的端口。所有的网卡都会收到这个 Frame但只有发现 MAC 地址和自己匹配的网卡才会把它接收进内存。因特网应用的硬件软件管理TCP/IP 协议栈是构建在底层局域网之上的跨网络抽象它对上层应用屏蔽了底层物理网络的复杂性。IPv4 IPv6IPv4:32 位地址约 43 亿个。最初设计时没人能想到互联网会这么庞大导致地址早已枯竭。由于 NAT (网络地址转换) 技术的续命目前世界上绝大多数的流量依然跑在 IPv4 上。IPv6:128 位地址。号称“能给地球上的每一粒沙子分配一个 IP”。在现代网络编程中我们应该尽量编写协议无关 (Protocol-Independent)的代码让系统自己去适配 IPv4 还是 IPv6。DNS (域名解析系统)人类记不住 IP 地址只能记住域名如www.cmu.edu。DNS 就是互联网的电话本。它是一个庞大的分布式数据库从未命名的根节点开始逐层向下解析.edu - cmu - www最终将域名转化为机器认识的 IP 地址。著名的 Port (端口) 和 Service Name一台主机只有一个 IP但上面可能跑着 Web 服务、邮件服务和 SSH 服务。为了区分数据包该交给哪个进程引入了端口 (Port)的概念16位0~65535。IP 地址找大楼 端口号找房间 唯一的通信端点Socket。知名端口Well-known ports通常被保留定义在 Linux 的/etc/services文件中Echo:7SSH:22Email (SMTP):25Web (HTTP):80Socket Interface (核心打通管道)这是网络编程的灵魂理解 Socket 最好的方法是**“打电话模型”**。1.socket()建立管道 —— 【买一部手机】创建一个 Socket 文件描述符 (fd)。参数domain(IPv4/IPv6),type(一般是流式SOCK_STREAM即 TCP),protocol(一般为 0)。2.bind()绑定端口 —— 【去营业厅办一个手机号】服务端专用。内核把你的 Socket fd 和一个具体的本地 IP 及端口号绑定。告诉系统“如果有发往 80 端口的数据包请交给我。”3.listen()监听端口 —— 【开机调大铃声准备接听】服务端专用。默认创建的 Socket 是主动去连别人的Client 视角。调用listen会把这个 Socket 转换成被动监听模式。backlog: 这是一个“候诊室”的大小。如果并发连接太多服务器处理不过来最多允许多少个连接在队列里排队。4.accept()服务端接受请求 —— 【接起电话开启通话子频道】服务端阻塞在这里直到有客户端连进来。灵魂设定accept会返回一个全新的 Socket fd (connfd) 用来专门和这个客户进行数据收发而原来的监听 fd (listenfd) 继续保持监听状态等待下一个客户进来。5.connect()客户端发起连接 —— 【拨打号码】客户端主动向 Server 发起连接引发 TCP 三次握手。成功后clientfd就可以自由地read/write交流了。现代的地址转换神器getaddrinfo以前写网络代码要自己查 DNS 把域名转成 IP还要手工处理字节序转换大端转小端极其痛苦。现在我们使用getaddrinfo。它是线程安全的可重入不仅通吃 IPv4 和 IPv6而且直接吐出一个链表里面包含了创建 Socket 需要的所有参数。常见套路顺着链表遍历挨个尝试socket()和connect()或bind()直到成功为止。Lec 22 Network Programming Part IISocket Address Structures (令人抓狂的 C 语言强转)C 语言没有面向对象的多态。网络 API 要求传入一个通用的sockaddr结构但我们实际操作时用的是专属的sockaddr_in针对 IPv4。所以你在代码里会看到满天飞的强制类型转换(struct sockaddr *)。这纯粹是历史遗留的丑陋设计习惯就好。Socket Helper封装open_listenfd为了不每次都写一堆繁琐的 Socket 初始化代码CSAPP 提供了一个极为好用的 Helper 包装函数。核心避坑指南 (SO_REUSEADDR):留意幻灯片里的setsockopt行。如果你杀掉一个服务端程序然后马上重启系统常常会报错“Address already in use (地址已被占用)”。因为操作系统会把刚才的端口保持在TIME_WAIT状态一段时间以防残留包。加上SO_REUSEADDR标志可以消除这个恶心的错误允许你立即重启绑定同一端口。迭代 Echo Server (Iterative Server)这是一个最基础的服务端架构。while(1)循环里不断地accept把收到的数据原封不动发回去Echo。它的致命弱点因为是串行Iterative的一次只能处理一个客户。如果黑客 A 连上之后故意不发数据也不挂断整个服务器就完全阻塞挂死了其他任何人都连不进来。这就是为什么后面必然要学习**并发服务器多进程/多线程/I/O 多路复用**的原因。Web (HTTP 原理初探)我们在浏览器里看到的五彩斑斓的网页本质上都是文本和二进制字节流。通过 MIME 扩展类型如text/html,image/jpeg服务器告诉浏览器发过去的数据到底该如何渲染。访问网页其实就是访问服务器硬盘上的一个文件并把它读过来。HTTP 请求 (Requests) 响应 (Responses)HTTP 是一个无状态的纯文本协议非常容易阅读。请求行Method URI Version例如GET /index.html HTTP/1.1响应行Version Status Code Status Msg例如HTTP/1.1 200 OK或HTTP/1.1 404 Not Found在头部之后会跟着一个空行\r\n空行之后就是真正的网页内容体Body了。小操作TINY Web Server 的逻辑实现一个简单的 Web 服务器其实只需这几步建立 Socket 等待连接accept。从连接中读取客户端发来的文本请求。把字符串切开提取出 Method (GET) 和 URI。判断是静态内容返回一个写死的 HTML 文件还是动态内容执行一个程序把程序的输出返回给客户端。CGI 与dup2的终极黑魔法 (Serving Dynamic Content)如果是动态内容客户端怎么传参数给服务端计算服务端又怎么把算好的结果送回给远方的客户端传参参数被塞在 URI 的问号?后面用分隔。空格会被转义为%20。计算服务器收到请求后调用fork()产生一个子进程。子进程把解析出的参数设置到操作系统的环境变量中然后用我们学过的execve()加载一个独立的 C 语言可执行程序CGI 脚本去运行。那个 C 程序直接用getenv就能拿到参数进行计算最为惊艳的设计 (dup2移花接木)那个独立运行的 C 语言计算程序并不知道什么是网络它只是单纯地计算完毕然后用printf()打印结果。printf()默认是输出到STDOUT(终端显示器) 的。但是在执行execve之前父进程巧妙地调用了dup2(fd, STDOUT_FILENO)。这一招瞬间将标准输出的管道重定向到了网络的 Socket fd上于是那个无辜的子程序调用printf(计算结果是 5)时屏幕上什么也没有这串字符顺着网线飞跃大洋直接出现在了客户端的浏览器屏幕上这就是 Unix 底层抽象的顶级魅力