尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

CSAPP网络编程:从socket到HTTP的完整学习笔记

发布时间:2026/9/19 12:10:07

资讯中心
01
ARTICLE

CSAPP网络编程:从socket到HTTP的完整学习笔记

CSAPP网络编程:从socket到HTTP的完整学习笔记
1. 开端一句“8.20 搁置9.5 继续”背后的网络编程学习我在计划表里给 CSAPP 第11章网络编程留的时间是 8.20结果当天被项目上的事打断连书都没翻开。标题里那个“搁置”不是矫情是真的没时间。到了 9.5 我重新打开这一章把 socket、connect、accept 这些概念从头又理了一遍才慢慢把之前零零碎碎的知识点串成一条线。这篇文章就当是我这段“拖延-重启”的记录把关键知识点重新整理一遍也把调试时踩过的坑写出来给后面读这章的人少走点弯路。CSAPP 这章和以往几章很不一样它不教你写业务代码也不追求让你立刻能写出高并发服务器。它把“网络”这件事拆成操作系统层面的概念然后告诉你 socket 编程为什么是这套固定流程。如果你之前只是用过 Python 的requests库或者调过 Java 的ServerSocket但不知道底层发生了什么这章就是补那块短板的。这章适合三类人看第一类是正在补操作系统和体系结构的学生想把“文件描述符、进程、信号”和网络串起来第二类是只写过应用层网络调用、没碰过 C socket 的开发者想搞清楚连接建立和数据传输的本质第三类是准备做 CSAPP 配套实验的读者需要把第11章的接口都用熟练。下面我按自己的学习顺序把这章的核心内容拆开讲。1.1 客户端-服务器模型几乎所有网络程序的地基第11章开头先讲了客户端-服务器模型很多人会觉得这是废话但恰恰是理解整章的前提。在一个网络应用里服务器进程先启动创建 socket 后绑定地址然后进入监听状态等待客户端来连接。客户端进程则是主动发起连接的一方连接建立后双方都能读写数据。这个模型并不是唯一形态但几乎你见过的所有主流网络应用都能套进去。你打开浏览器访问网页浏览器是客户端Web 服务器是服务端你发邮件邮件客户端是客户端邮件服务器是服务端你在终端里敲mysql -h 192.168.1.10mysql 命令是客户端MySQL 守护进程是服务端。为什么 CSAPP 要把这个模型说得那么重因为后面的所有 API、所有状态转换、所有边界情况都是围绕“谁先启动”“谁在等待”“连接建立之后怎么通信”来展开的。你如果只是背了 socket、bind、listen、accept、connect 这几个函数名却说不清哪一步发生在客户端、哪一步发生在服务端那这一章基本等于白读。我还踩过一个典型的理解错误以为accept是“接受连接请求并完成通信”的入口实则accept只是从已完成连接的队列里取出一个连接真正的三次握手是内核在调用listen之后就开始处理的。这一点后面我还会细说。1.2 IP、端口、协议栈面试官最爱问的三个词网络编程绕不开 IP、端口、协议栈。我第一次读这章的时候这些词都能背但一问“端口到底有什么用”就开始含糊。其实可以这样理解IP 负责把数据包送到某台主机端口负责把数据包交给这台主机上的某个进程。一台服务器上可能同时跑着 Web 服务、SSH 服务、数据库服务如果只有 IP系统根本不知道该把网络数据递给谁。端口号是个 16 位整数范围是 0 到 65535其中 0 到 1023 通常是系统预留端口比如 HTTP 用 80HTTPS 用 443SSH 用 22。我们自己写实验代码时一般用 8000 到 50000 之间的高位端口避免和系统服务冲突。协议栈这个词听起来吓人其实就是一个分层模型应用层、传输层、网络层、链路层。应用层数据往下传递时每一层都会加上自己的头部接收方再一层层剥掉头部。第11章重点在传输层尤其是 TCP 和 UDP 的差异。维度TCPUDP连接性面向连接需要建立连接无连接直接发数据报可靠性可靠有确认、重传、排序不可靠丢包不负责传输单位字节流数据报有边界典型场景HTTP、文件传输、远程登录DNS、音视频实时传输、游戏状态同步CSAPP 选择 TCP 讲网络编程是因为它和文件描述符的读写模型最契合。你调用read和write操作 TCP 连接时就像操作一个管道内核帮你处理了拆包和重组你只需要关心字节流的边界。1.3 Socket 的真相它就是一个能读能写的文件这一章最重要的认知转变就是把 socket 当成文件描述符来理解。Unix 世界里“一切皆文件”socket 也不例外。socket()系统调用返回的是一个非负整数它和open()返回的文件描述符地位相同都可以传给read、write、close使用。我在读第10章的时候对文件描述符已经很熟了但看到第11章 socket 时才真正理解这套抽象的力量。内核维护了一张文件描述符表每个 fd 都指向一个打开的文件表项socket 在文件表项里有自己的内核读写缓冲区。你往 socket 里写数据数据进入内核发送缓冲区由 TCP 协议栈负责分包和发送你从 socket 里读数据读的是内核接收缓冲区里已经到达的字节流。这个模型解释了为什么read在 socket 上可能返回 0表示对端关闭了连接也解释了为什么write在 socket 上可能触发SIGPIPE信号因为对端已经关闭再写就是往一个没有接收方的管道里灌水系统会默认终止进程。理解了这个你再看后面的代码就会明白网络编程写服务端本质上就是管理一堆 fd监听新的连接、读写已有连接、关闭不再使用的连接。2. Socket 编程的核心套路从 socket() 到 accept()2.1 服务端的固定流程牢记 listenfd 和 connfd 的区别CSAPP 第11章把服务端流程总结得很清晰先socket创建监听 fd再bind绑定地址然后listen进入监听状态之后循环accept拿出已完成的连接。这里有一个关键点我之前一直没意识到listenfd和connfd是两种完全不同的 fd。listenfd是整个服务器生命周期的“门口接应员”它只负责等待新的连接请求永远不会用来收发业务数据。accept每次返回一个新的connfd这个新 fd 才是具体某一次客户端连接的“直达通道”我们在这个 conndf 上read和write最终服务完再把它close掉。为什么要有这个区分因为一个服务器往往要服务多个客户端而listenfd可以一直存在继续接收新的连接。你如果不开新线程、不 fork 子进程只用一个进程循环 accept那么同一时刻只能处理一个连接的完整生命周期这就是“迭代服务器”它能跑但效率低而且还存在“连接饿死”的问题。服务端调listen(sockfd, backlog)时backlog参数表示内核里已完成连接队列的长度我在测试代码里通常写 5。客户端发起连接后三次握手由内核完成完成后的连接先放到这个队列里等accept来取。队列满了之后新的连接请求会被内核丢弃或延后处理。所以这个数值不能随便填 0否则本地用并发测试工具压一下就会出现大量连接失败。2.2 客户端的 connect为什么客户端一般不 bind客户端这边流程要简单很多创建 socket调用connect发起连接连接成功后直接读写最后关闭。客户端的connect会指定服务端的 IP 和端口同时内核会为这个 socket 自动分配一个临时端口作为本次连接的本端端口。这也是为什么客户端代码里通常看不到bind。你可能会问服务端必须 bind因为客户端需要通过固定 IP 和端口找到它客户端不 bind是因为内核自动选端口就够了不需要给外部暴露固定地址。如果多个客户端进程同时连接同一个服务器内核会给每个客户端 socket 分配不同的临时端口连接四元组(源IP, 源端口, 目的IP, 目的端口)也就各不相同服务器就能区分不同连接。在实验环境里如果connect返回Connection refused先别急着怀疑代码检查服务器进程是否真的在监听那个端口。我用netstat -tlnp查端口时经常发现自己刚才的服务端进程没起来或者端口号写错了客户端连到另一个端口上。2.3 字节序和地址结构新手必踩的两块砖先讲字节序。不同 CPU 存放多字节整数的顺序不同x86 是小端而网络传输规定统一用大端也就是“网络字节序”。调用socket编程接口时端口号、IP 地址都要从主机字节序转换成网络字节序反方向则要转换回来。CSAPP 给的四个函数是htonl、htons、ntohl、ntohs分别处理 long 和 short 类型的字节序转换。实际代码里端口一般用htons(PORT)因为端口号是 16 位IPv4 地址用htonl(INADDR_ANY)表示绑定任意本地地址。我早期没写htons在笔记本小端环境下端口号被翻转导致客户端去连一个完全不对的端口查了很久才找到原因。再讲地址结构。你在 C 里看到struct sockaddr_in和struct sockaddr时别慌。sockaddr是通用地址结构所有协议的地址都能往里塞sockaddr_in是 IPv4 的专用结构字段更明确。调用bind、connect、accept时都要把sockaddr_in*强转成sockaddr*传参这是历史遗留的接口设计现在依然这么用。struct sockaddr_in里有三个字段最容易写错sin_family要写AF_INETsin_port要转字节序sin_addr.s_addr要写 IP 地址的网络字节序值。用inet_pton把点分十进制字符串转成二进制地址比老式inet_addr更安全也更容易处理错误返回值。3. 动手实现一个可跑的 Echo 服务端和客户端3.1 服务端代码把上面流程落成 C 代码看懂了流程还是要落实到代码。我刚开始在 CSAPP 原书配套代码里看到了很多封装函数比如open_clientfd、open_listenfd它们封装了getaddrinfo等细节。我这里为了讲清楚底层先写一版不依赖封装的原始 echo 服务器方便对照上面说的每一步。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #define PORT 8888 #define MAXLINE 4096 int main() { int listenfd, connfd; socklen_t clientlen; struct sockaddr_in serveraddr, clientaddr; char buf[MAXLINE]; listenfd socket(AF_INET, SOCK_STREAM, 0); if (listenfd 0) { perror(socket); return 1; } memset(serveraddr, 0, sizeof(serveraddr)); serveraddr.sin_family AF_INET; serveraddr.sin_addr.s_addr htonl(INADDR_ANY); serveraddr.sin_port htons(PORT); if (bind(listenfd, (struct sockaddr *)serveraddr, sizeof(serveraddr)) 0) { perror(bind); return 1; } if (listen(listenfd, 5) 0) { perror(listen); return 1; } printf(server started at port %d\n, PORT); while (1) { clientlen sizeof(clientaddr); connfd accept(listenfd, (struct sockaddr *)clientaddr, clientlen); if (connfd 0) { perror(accept); continue; } printf(client connected: %s\n, inet_ntoa(clientaddr.sin_addr)); ssize_t n; while ((n read(connfd, buf, MAXLINE)) 0) { write(STDOUT_FILENO, buf, n); write(connfd, buf, n); } close(connfd); } return 0; }这段代码的服务端循环是迭代式的同一时间只能服务一个客户端。read返回值有三种情况大于 0 表示读到了字节等于 0 表示对端关闭循环结束小于 0 表示出错其中EINTR是被信号中断严格代码里应该继续重读我这里是简化版。注意我bind地址用了INADDR_ANY意思是绑定本机所有网卡上的 8888 端口。这样你用127.0.0.1或者本机局域网 IP 都能连上来。如果只绑定127.0.0.1外部主机就访问不到了。3.2 客户端代码建立连接后按行收发客户端写法相对简单核心是connect成功之后用read/write直接通信。下面这段代码接收命令行传入的服务端 IP然后从标准输入读一行发送给服务器再读回服务器的输出。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #define PORT 8888 #define MAXLINE 4096 int main(int argc, char *argv[]) { if (argc ! 2) { fprintf(stderr, usage: %s server_ip\n, argv[0]); return 1; } int clientfd socket(AF_INET, SOCK_STREAM, 0); if (clientfd 0) { perror(socket); return 1; } struct sockaddr_in serveraddr; memset(serveraddr, 0, sizeof(serveraddr)); serveraddr.sin_family AF_INET; serveraddr.sin_port htons(PORT); if (inet_pton(AF_INET, argv[1], serveraddr.sin_addr) 0) { perror(inet_pton); return 1; } if (connect(clientfd, (struct sockaddr *)serveraddr, sizeof(serveraddr)) 0) { perror(connect); return 1; } char buf[MAXLINE]; printf(input: ); while (fgets(buf, MAXLINE, stdin) ! NULL) { write(clientfd, buf, strlen(buf)); ssize_t n read(clientfd, buf, MAXLINE); if (n 0) { write(STDOUT_FILENO, buf, n); } printf(input: ); } close(clientfd); return 0; }这段代码只做了一次read如果服务器返回的数据超过了一次read能读到的量就会截断。实际项目中要循环读直到读到预期长度或连接关闭。这也就是 CSAPP 里强调 RIORobust I/O包的原因它保证不论收到多少数据都能按你的需求读满或读到 EOF。实验代码里建议直接用书上的 RIO 封装别自己裸写read循环容易漏边界。3.3 用 telnet 验证服务端教科书式的测试方法代码写完后最直接的验证方式是用系统自带的telnet。把服务端启动起来在另一个终端执行telnet 127.0.0.1 8888连上之后你会看到服务端打印了客户端 IP然后你在 telnet 里敲任意字符串回车服务端立刻把同样内容回显过来。这说明 TCP 连接已经建立数据链路是通的。测试完 telnet再跑一遍自己写的客户端gcc -stdc11 echoclient.c -o echoclient ./echoclient 127.0.0.1输入hello csapp如果看到回显说明两端程序都工作正常。此时不要急着收工再多做两个测试一个是在客户端直接CtrlD结束输入观察服务端是否正常处理read返回 0另一个是连续启动多个 telnet 客户端观察迭代服务器是否只能排队服务。第二个测试能直观感受“迭代模型”的缺陷也为后面理解并发模型做铺垫。3.4 我实测中遇到的三个运行问题第一个问题是端口被占用。服务端还没退出又重新启动时bind经常报Address already in use。原因是上一个服务端进程还占着端口或者 TCP 的 TIME_WAIT 状态还没结束。解决办法是先用ps或netstat -tlnp查端口把旧进程杀掉或者在代码里设置SO_REUSEADDR选项。第二个问题是客户端connect返回Connection refused。这个坑我一开始还怪代码后来才发现服务端根本没跑起来或者端口不一致。排查时先netstat -tlnp | grep 8888确认 listen 状态的进程存在再检查客户端代码里的端口号。第三个问题是程序正常关闭时服务端偶尔会收到SIGPIPE信号导致进程退出。原因是对端已经关闭但服务端还继续write。CSAPP 里也提到过这个处理方式要么在write前做好状态判断要么忽略SIGPIPE信号或者用send函数的MSG_NOSIGNAL标志。我自己实验时经常在客户端异常退出后看到这种问题属于必踩项。注意写网络代码时一定要对系统调用的返回值做检查。不要图省事直接忽略accept、read、write的返回值否则排错时的成本会翻倍。4. 从 Echo 到 HTTP理解第11章真正的考点4.1 为什么经历了 Echo 还要学 HTTP 解析Echo 服务器只能证明 socket 流程通了但真正有意义的网络程序是解析协议。第11章后半部分花了不少篇幅讲 HTTP因为 HTTP 是最直观的文本协议你用 telnet 都能手敲请求。一个最简单 HTTP 请求长这样GET /index.html HTTP/1.1 Host: localhost:8888 User-Agent: curl/8.5.0 Connection: close服务器收到后要按行解析请求行再解析头部然后根据请求的 URI 决定是返回静态文件还是交给动态逻辑。这个过程和 echo 的核心区别是echo 不需要理解数据内容直接回显HTTP 服务器必须解析语义做路径映射、内容类型判断、响应头构造。CSAPP 里给了一个极简 Web 服务器示例叫 Tiny代码很短但完整展示了静态和动态内容返回的流程。我读这一段时收获最大的是不要把 HTTP 想得太玄它就是一套约定好格式的文本对话。服务器不需要理解浏览器的“意图”它只需要按照规范把请求解析出来按照规范把响应构造回去。4.2 一个极简 HTTP 请求解析过程解析 HTTP 请求行的常规做法是先用fgets读一行然后用sscanf拆出三个字段方法、URI、版本号。比如char method[MAXLINE], uri[MAXLINE], version[MAXLINE]; if (fgets(buf, MAXLINE, fp) NULL) { return; } sscanf(buf, %s %s %s, method, uri, version);这只是最粗糙版本但要完成 CSAPP 的在线作业或实验通常还需要处理几个细节判断方法是不是GETURI是否需要做 URL 解码比如%20要还原成空格还要判断 URI 是否含查询字符串如果有需要拆成路径和参数两部分。响应构造也要注意格式。一个最简单的成功响应是HTTP/1.1 200 OK Content-Type: text/html Content-Length: 137 Connection: close html.../html头部字段和正文之间必须有一个空行。Content-Length必须和正文实际字节数一致否则浏览器会一直等待或直接报错。很多新手第一次写 HTTP 服务器都栽在空行上要么多敲了一个\n要么少敲了一个\r\n。我在实验里用的都是\r\n因为 HTTP 规范里行结束符是 CRLF。虽然很多服务器对\n也能容忍但规范就是规范照着写才不会出现跨平台、跨客户端解析异常。4.3 并发模型一个 socket 服务端如何服务多个客户端CSAPP 第11章后半部分很大一部分在讲并发因为迭代服务器太弱了。如果你用浏览器发一个请求浏览器会创建多个连接来下载网页里的静态资源迭代服务器只能一个接一个处理前面一个阻塞后面的全部排队。书中提到了三种并发方式基于进程、基于线程、基于 I/O 多路复用。基于进程的模型最简单accept返回一个connfd后fork一个子进程去服务这个连接。子进程要close(listenfd)父进程要close(connfd)不然 fd 引用计数会乱。父进程还要处理SIGCHLD信号回收僵尸进程否则系统里会堆积一堆僵尸子进程。基于线程的模型思路类似用pthread_create为每个连接创建一个现场服务线程。要注意设置线程分离或者在主线程里回收否则线程也会变成“僵尸”多个线程共享全局变量时还要加锁否则会有竞态。基于 I/O 多路复用的模型不用为每个连接创建新进程或线程而是用一个select或poll同时监听多个 fd哪个 fd 可读就处理哪个。CSAPP 重点展示了select版本它更接近现代高并发服务器的实现思路不过代码细节也更繁琐。并发模型优点缺点适合场景多进程简单隔离性好创建开销大进程数有限并发量不高的服务多线程开销较低共享数据方便需要同步线程安全问题多中等并发业务较重I/O 多路复用单线程可管大量 fd节省资源编程复杂度高不适合阻塞任务长连接多、IO 密集型我个人的学习建议是先把多进程版本跑通再去看多线程版本最后才啃select。一上来就啃事件驱动很容易被 fd 集合的操作搞晕。5. 记录一次拖延了半个月的“回归学习”5.1 搁置期过后如何快速找回状态从 8.20 到 9.5中间隔了将近三周。重新打开书的时候我之前记得的 socket 流程已经糊了。我用的恢复方法是“三步走”第一步只看第11章的几张关键图把 client-server 状态转换重新画一遍第二步不看资料默写socket、bind、listen、accept、connect的调用顺序第三步直接打开旧的实验代码跑一遍再改一个地方比如把迭代服务器改成能处理多次连接的版本。这样做比从头读一遍更省时间因为核心记忆还在缺的是“重新激活”。如果你也碰到计划中断的情况不要逼自己从第一页重新开始那样很容易产生厌烦情绪。挑一个小的可运行 demo先跑起来再往里加功能状态会恢复得很快。搁置期最大的问题不是知识忘了而是心理上的“畏难”。一旦你开始动手改代码那种“不知道从哪开始”的焦虑感就会消失。我拖到 9.5 才打开其实就是因为总觉得要腾出一个完整下午来学结果一直等不到那样的时间。后来我改变策略只要求自己先完成第一步“画流程图”花二十分钟就进入状态了。5.2 资料与工具清单哪些真正有用第11章官方配套代码和讲义肯定要看但要搭配手册使用。我在写 socket 代码时每次都打开man 2 socket、man 2 bind、man 2 connect这几个手册页把参数和返回值认真过一遍比自己硬记牢固得多。调试工具方面telnet虽然老但用来测试 TCP 文本协议特别方便。curl是 HTTP 调试神器可以手动指定请求头、查看响应头。端口排查用netstat -tlnp和ss -tlnp如果某个端口被占用lsof -i:8888能查到具体进程。抓包工具我用的是 tcpdump 和 Wireshark。第一次看我写的服务器和别人写的服务器交互时抓包能让你看到三次握手、数据包确认、四次挥手网络编程一下子从抽象变具体。观察 TCP 状态迁移时netstat的输出也能帮你看到LISTEN、ESTABLISHED、TIME_WAIT这些状态这是只看代码永远学不到的东西。工具用途使用时机telnet手工连接 TCP 端口模拟客户端测试服务端是否在监听curl发送 HTTP 请求测试 Web 服务器响应netstat / ss查看端口和连接状态排错、查占用lsof查看进程打开的 fd查端口归属tcpdump / Wireshark抓包分析协议细节深入理解三次握手、关闭流程5.3 给刚开始读这章的人三个建议第一个建议不要背 API把流程画成图。第11章所有接口都围绕着“连接建立、数据传输、连接关闭”这条主线你把这个主线理解透了API 只是一个查手册的问题。第二个建议认真做一次实验。CSAPP 配套的网络实验会要求你实现一个并发的 HTTP 服务做完之后你对accept、动态请求、URL 解析、响应头构造这些知识点的记忆深度远超过读十遍书。如果时间紧至少也要把书上的 Tiny 服务器代码手动敲一遍敲的过程中会不得不思考很多“昨天看的时候以为懂、今天一写就卡住”的细节。第三个建议先跑通再优化。我第一次写服务端时总想一口气把所有边界情况都处理好结果代码越写越乱。后来换了个思路先实现“能跑、能收发数据”再逐步处理EINTR、多进程回收、大文件读取这些进阶问题。每完成一个小版本就测试一轮这样进步最明显。我在实际把第11章啃下来的过程中最真实的体会是网络编程没有那么多玄机它的难度主要来自“接口多、状态多、边界条件多”。但只要你能静下心把一次完整的 echo 通信从socket()到close()全部走通再往 HTTP 解析和并发模型上延伸这章就算真正入门了。至于剩下的细节都是靠一次次 debug 和抓包积累出来的经验。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。