简介:网络编程的起点往往从理解socket开始,它本质上是Unix一切皆文件哲学下的文件描述符,通过bind、listen、accept、connect等函数即可建立一条完整链路。TCP协议中的三次握手、滑动窗口与四次挥手,决定了应用层如何判断连接状态、处理粘包拆包以及优雅关闭。掌握这些基础后,多线程并发模型让TCP聊天室得以实现,而进一步解析HTTP文本协议,则能构建出响应浏览器的静态服务器。这种从字节流到文本协议的进阶路径,既覆盖了进程并发、互斥锁、广播等核心工程问题,也涉及Content-Length、状态码、目录穿越防护等实际细节。无论是准备面试项目,还是想系统补齐C语言网络编程能力,都可以从TCP聊天室走向HTTP服务器的完整实践中获得扎实的训练。
1. 从TCP聊天室到HTTP服务器:C语言网络编程的一条完整链路
大多数C语言学习者在指针和内存管理之后,遇到的第一个断层就是网络编程。原因很简单:前面的代码是单机程序,而socket一出现,就要同时面对协议、并发和阻塞这三件事。《C语言网络编程:从TCP聊天室到HTTP服务器搭建指南》正好把这条链路补完整——先写一个多线程TCP聊天室,把连接建立、消息收发、客户端管理跑通;再往上抽象一层,用同样的socket功底解析HTTP请求、构造响应,搭出一个能响应浏览器的HTTP服务器。文本协议和字节流协议各来一遍,Linux网络编程的地基就算站稳了。适合工作一到三年、已经写过C但没系统做过网络应用的研发,也适合准备面试前用一两天突击完整项目的同学。
2. Socket编程基础:七个函数撑起一个TCP连接
2.1 先理解socket是什么:不是协议,是文件描述符
Unix的哲学是一切皆文件。socket在Linux里就是一个可读写的文件描述符,网络收发最终落在read/write上,这和读写普通文件没有本质区别。创建socket时通过三个参数决定它的行为:domain指定协议族,type指定流式还是数据报,protocol指定具体协议。
| 参数 | 常用取值 | 含义 | 典型场景 |
|---|---|---|---|
| domain | AF_INET | IPv4地址族 | 绝大多数局域网与公网通信 |
| domain | AF_INET6 | IPv6地址族 | 需要IPv6地址的场景 |
| type | SOCK_STREAM | 可靠字节流,有序不重复 | TCP |
| type | SOCK_DGRAM | 不可靠数据报,保留消息边界 | DNS查询、音视频实时传输 |
| protocol | 0 | 由内核根据前两项选默认协议 | 最不容易出错的写法 |
protocol传0是推荐做法,让内核根据domain和type自动选择默认协议。C语言网络编程中更绕不开的是sockaddr_in这个结构体,sin_family填地址族,sin_port填端口,sin_addr填IP。这里有一个新手必踩的坑:端口和IP都是主机字节序,赋值前必须用htons和htonl转成网络字节序。很多第一次写TCP通信的人没有转换端口,程序跑起来能编译能连接,但连的永远不是自己以为的那个端口。
2.2 bind-listen-accept与connect的调用语义
理解了socket本质后,服务端的调用链就非常清晰了。bind把套接字和本机某个地址端口绑定,listen把主动套接字变成被动监听,accept从内核已完成握手的队列里取出一个连接并返回一个新的文件描述符,后续收发都用这个新fd。connect则是客户端发起握手的入口。
// server.c —— 单连接的TCP服务器骨架 #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #define PORT 8888 int main() { int listen_fd, conn_fd; struct sockaddr_in addr; char buf[1024] = {0}; const char *msg = "hello from server"; // 1. 创建IPv4流式套接字,protocol传0让内核选TCP listen_fd = socket(AF_INET, SOCK_STREAM, 0); // 2. 绑定通配地址:8888,INADDR_ANY表示监听所有网卡 addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(PORT); bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)); // 3. 进入监听,backlog为3 listen(listen_fd, 3); // 4. 接受连接,返回专门用于通信的fd conn_fd = accept(listen_fd, NULL, NULL); read(conn_fd, buf, sizeof(buf)); write(conn_fd, msg, strlen(msg)); close(conn_fd); close(listen_fd); return 0; }对应的客户端核心代码只有四步:
// client.c —— 客户端核心调用链 int sock = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv; serv.sin_family = AF_INET; serv.sin_port = htons(8888); inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr); connect(sock, (struct sockaddr *)&serv, sizeof(serv)); send(sock, "hello", 5, 0); read(sock, buf, sizeof(buf));这段代码里最值得理解的是accept的语义。accept返回的conn_fd才是真正收发数据的套接字,listen_fd继续留在监听状态,所以一个进程才能同时服务多个连接。listen的backlog参数是内核为尚未被accept的连接准备的队列长度,不是最大连接数。文档里示例服务器backlog只填3,在实际项目中这个值往往要放大到几十甚至几百,否则连接洪峰时会看到握手成功但应用层迟迟accept不到。
为了让你看清调用链,上面的骨架省略了所有的错误判断。实际项目中socket、bind、listen、accept、connect的返回值必须逐一检查,任何一个返回负值都要用perror输出原因。文档里完整示例对每个系统调用都做了判断,那是正确姿势。
2.3 字节序、地址转换与一个必踩的坑
收发数据前的地址处理有三个函数要记住:htons把16位端口转网络字节序,htonl把32位IP转网络字节序,inet_pton把"192.168.1.10"这样的点分十进制转成二进制形式。反过来读地址用ntohs和ntohl。bind时sin_addr.s_addr填INADDR_ANY表示监听所有网卡;想限制在某个网卡就填具体IP。
| 函数 | 方向 | 用途 |
|---|---|---|
| htons / htonl | 主机序转网络序 | 端口、IP地址赋值 |
| ntohs / ntohl | 网络序转主机序 | 打印对端端口、地址 |
| inet_pton | 字符串转二进制IP | 客户端connect前使用 |
| inet_ntop | 二进制IP转字符串 | 打印对端IP |
提示:服务器端如果在bind之前不设置SO_REUSEADDR,服务停止后立刻重启经常报"Address already in use"。原因是主动关闭方的TIME_WAIT状态还占着端口。setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))能解决这个问题,文档的初始化代码里已经带了这一行。
3. TCP机制落地:三次握手、滑动窗口与连接关闭
3.1 三次握手:内核里发生、代码里可见的链路
TCP三次握手对应用层来说是透明的,connect返回成功只说明客户端收到了服务器的SYN+ACK并回送了ACK,连接已经建立。但内核给应用层留了几个观察入口。用ss命令能实时看到连接状态迁移:
watch -n 1 ss -tn启动服务器后执行这一行,再从客户端发起connect,能在列表里看到SYN_SENT短暂出现然后变成ESTABLISHED的过程。如果连接一直停在SYN_SENT,说明服务器端的listen队列已满或防火墙丢弃了包。
这段状态观察对理解listen的backlog很有帮助。握手期间,客户端的连接处于内核维护的半连接队列和全连接队列中,accept只是从全连接队列里取走已经完成握手的连接。队列长度不够时,客户端表现为连接建立变慢或失败,而服务器端进程完全没有感知。排查时可以看`ss -tnl`输出里Recv-Q列的大小,如果长期接近backlog值,就该调大listen的第二个参数。 ### 3.2 四次挥手与read返回0的判断逻辑 断开连接的判断是网络编程里最容易写错的地方。四次挥手结束后,主动关闭方会进入TIME_WAIT状态,持续2MSL,这个状态在ss的输出里能直接看到。TIME_WAIT的意义是保证最后一个ACK能到达对端,同时让旧连接的报文在网络中消亡。服务器重启时报端口占用,绝大多数情况下就是之前进程的TIME_WAIT还没清掉,SO_REUSEADDR因此成了服务器端代码的标配。 聊天室场景里,客户端下线时服务器怎么知道?关键就在read的返回值: ```c ssize_t n = read(fd, buf, sizeof(buf)); if (n == 0) { // 对端正常关闭:收到了FIN,四次挥手完成 close(fd); remove_client(fd); } else if (n < 0) { if (errno == EINTR) { continue; // 被信号打断,不是错误,重试即可 } perror("read"); close(fd); // 网络异常或对端强制关闭,按断开处理 remove_client(fd); }read返回0表示读到EOF,即对端发来了FIN并且本地协议栈完成了关闭流程。EINTR表示读取被信号打断,这是正常情况,直接重试。其他负值返回值才有意义,比如ECONNRESET表示对端异常关闭。很多初学资料只教了recv返回0和-1的处理,没提EINTR这个分支,在高并发信号频繁的进程里这就成了隐藏的断开误判。
| TCP状态 | 出现场景 | 应用层表现 |
|---|---|---|
| SYN_SENT | connect后未收到SYN+ACK | connect阻塞中 |
| ESTABLISHED | 三次握手完成 | 可正常read/write |
| FIN_WAIT_2 | 主动关闭方已收到对端ACK | 半关闭状态 |
| TIME_WAIT | 主动关闭方等待2MSL | 端口暂时不可复用 |
3.3 滑动窗口、超时重传与应用层看到的现象
TCP的可靠传输由序列号、确认应答、滑动窗口和超时重传共同保证。滑动窗口决定发送方在没有收到ACK前能连续发多少数据,窗口大小由接收方的剩余缓冲区决定。这在应用层最直接的表现是:send返回成功只表示数据进了本地的内核发送缓冲区,既不表示对端内核收到了,更不表示对端应用读到了。
这个认知对两类程序特别重要。第一类是聊天室这类实时程序,如果TCP_NODELAY没开,小报文会被Nagle算法合并,消息在局域网里也能感受到几十毫秒延迟;第二类是HTTP服务器,响应头里的Content-Length就是应用层用来告诉对端“这次响应有多少字节”的机制。套接字层面的超时重传和拥塞控制由内核完成,应用层能干预的是SO_SNDTIMEO、SO_RCVTIMEO这类超时参数,以及用tcpdump抓包验证协议行为:
sudo tcpdump -i lo -nn 'tcp port 8888' -c 20抓包结果里能看到三次握手的SYN、SYN-ACK、ACK三个包,以及挥手阶段的FIN和ACK。这比查文档更能建立对TCP的直觉。文档里给出的TCP头部结构体解析代码也值得跑一遍,配合tcpdump导出的hex数据,可以亲手把源端口、序列号、标志位逐个对应上。
4. 多线程TCP聊天室:广播模型与并发边界
4.1 模型选择:一连接一线程在什么范围够用
TCP聊天室的核心需求是多个客户端实时互发消息,这个需求天然有两条实现路线:一连接一线程,或者select/epoll事件驱动。文档采用的是前者,也是理解网络并发最直接的模型。每个客户端accept之后创建一个pthread线程,线程里循环read,收到消息就遍历客户端列表广播。这个模型在连接数几十以内非常稳,代码逻辑直观,调试也容易。连接数到几百上千后,线程上下文切换的开销和每个线程默认8MB栈空间的虚拟内存占用会成为瓶颈,那时才有必要换epoll。聊天室这种广播型应用真正的热点在“遍历所有客户端发消息”的循环上,这跟epoll并不能互相替代。
4.2 服务器端:客户端数组、广播与互斥锁
服务器端需要维护一个全局客户端fd数组,每个连接对应一个线程。广播时多个线程同时操作这个数组,必须用互斥锁保护。下面是可编译运行的服务器完整实现:
// chat_server.c —— 多线程TCP聊天室服务器 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <sys/socket.h> #include <netinet/in.h> #define PORT 8888 #define MAX_CLIENTS 32 #define BUFFER_SIZE 1024 int client_fds[MAX_CLIENTS]; int client_count = 0; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 广播:发给除发送者外的所有客户端 static void broadcast(int sender_fd, const char *msg, size_t len) { pthread_mutex_lock(&lock); for (int i = 0; i < client_count; i++) { if (client_fds[i] != sender_fd) { send(client_fds[i], msg, len, 0); } } pthread_mutex_unlock(&lock); } // 从数组中移除一个客户端,用最后一个元素覆盖,避免搬移 static void remove_client(int fd) { pthread_mutex_lock(&lock); for (int i = 0; i < client_count; i++) { if (client_fds[i] == fd) { client_fds[i] = client_fds[client_count - 1]; client_count--; break; } } pthread_mutex_unlock(&lock); } // 每个客户端一个线程的执行体 static void *client_handler(void *arg) { int fd = *(int *)arg; free(arg); char buf[BUFFER_SIZE]; ssize_t n; while ((n = read(fd, buf, sizeof(buf))) > 0) { broadcast(fd, buf, n); // 收到什么就广播什么 } close(fd); remove_client(fd); return NULL; } int main() { int listen_fd = socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in addr = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_ANY), .sin_port = htons(PORT) }; bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)); listen(listen_fd, 8); printf("chat server listening on %d\n", PORT); while (1) { struct sockaddr_in peer; socklen_t len = sizeof(peer); int *fd = malloc(sizeof(int)); // 每个连接独立分配内存 *fd = accept(listen_fd, (struct sockaddr *)&peer, &len); pthread_mutex_lock(&lock); if (client_count < MAX_CLIENTS) { client_fds[client_count++] = *fd; pthread_mutex_unlock(&lock); pthread_t tid; pthread_create(&tid, NULL, client_handler, fd); pthread_detach(tid); // 线程退出后自动回收资源 } else { pthread_mutex_unlock(&lock); const char *msg = "server full\n"; send(*fd, msg, strlen(msg), 0); close(*fd); free(fd); } } }这段代码有几个值得注意的设计。第一个是线程参数的内存管理,传给client_handler的fd指针必须malloc,因为accept循环会不断覆盖栈上变量,如果传&fd这样的栈地址,所有线程拿到的可能是同一个被改写的值。第二个是remove_client用最后一个元素覆盖待删除元素,把数组压缩成本降到O(1)。第三个是pthread_detach,让线程退出时自动释放资源,避免join遗漏造成的僵尸线程。
4.3 客户端:收发双线程与fgets的换行问题
客户端的结构比服务器简单:连接建立后,启动一个接收线程专门read并打印消息,主线程循环fgets读键盘输入再send。完整实现如下:
// chat_client.c —— 一个接收线程负责收,主线程负责发 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8888 #define BUFFER_SIZE 1024 static void *recv_thread(void *arg) { int fd = *(int *)arg; char buf[BUFFER_SIZE]; ssize_t n; while ((n = read(fd, buf, sizeof(buf) - 1)) > 0) { buf[n] = '\0'; printf("recv: %s\n", buf); } printf("connection closed\n"); return NULL; } int main() { int fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv = { .sin_family = AF_INET, .sin_port = htons(PORT) }; inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr); if (connect(fd, (struct sockaddr *)&serv, sizeof(serv)) < 0) { perror("connect"); exit(EXIT_FAILURE); } pthread_t tid; pthread_create(&tid, NULL, recv_thread, &fd); char line[BUFFER_SIZE]; while (fgets(line, sizeof(line), stdin) != NULL) { line[strcspn(line, "\r\n")] = '\0'; // 去掉fgets带回的换行符 send(fd, line, strlen(line), 0); } close(fd); return 0; }这里有个容易被忽略的细节:fgets会把用户按回车产生的换行符一起读进缓冲区,如果不去掉,服务器广播的消息里会带多余的\n,客户端收到后打印时会多出空行。用strcspn(line, "\r\n")找到第一个换行符的位置并替换成字符串结束符,是最简洁的处理方式。recv_thread里用sizeof(buf) - 1作为read长度,是因为后面要把buf当字符串打印,必须留一个位置给'\0'。
4.4 编译运行与两个必踩的坑
编译时必须要带-pthread选项,否则链接阶段找不到pthread_create:
gcc -o chat_server chat_server.c -pthread gcc -o chat_client chat_client.c -pthread运行起来后开两个终端各跑一个客户端,一边输入字符,另一边能看到消息,基本功能就通了。用ss -tlnp | grep 8888可以确认服务器监听了TCP端口。
这个流程里有两个坑,都是广播型服务器的典型事故现场。第一个是SIGPIPE。当服务器往一个已经关闭的客户端fd上send时,内核会向进程发送SIGPIPE信号,默认动作是直接终止进程。这意味着某个客户端异常退出后,下一次广播就可能让整个聊天室进程崩溃。解决方法是在main里加一行signal(SIGPIPE, SIG_IGN);忽略该信号,让send返回-1而不是杀死进程。
第二个坑和第一个联动:read返回0之后要立即把该fd从广播列表里移除。如果不移,这个关闭的fd还留在client_fds数组中,下一个客户端的消息会触发一次对它的send,进程就算不崩也会反复出现无效的系统调用。正因为这个原因,remove_client必须放在read循环外、线程结束前,而不是等到下一次广播时才处理。
5. HTTP服务器:把socket字节流解析成请求-响应
5.1 先从字节流里切出HTTP报文
HTTP/1.1是一个基于TCP的文本协议。对比聊天室就知道差别在哪里:聊天室的消息没有明确边界,服务器读到什么就广播什么;HTTP则必须从字节流里切出一个完整请求。请求行以\r\n结尾,每个头字段也是\r\n结尾,头与body之间有一个空行。TCP是字节流,一次read可能只读到了半个请求,也可能一次读到了好几个请求,这就是常说的粘包和拆包。服务器第一件要做的事是攒数据、找边界,而不是收到多少就解析多少。
判断边界有两个依据。如果请求里有Content-Length,要等body收满该长度再处理;对于GET请求,请求行加头字段到空行结束就是完整请求。为了省事,第一个版本的HTTP服务器可以直接要求所有请求走短连接,即头部Connection: close。
5.2 请求解析:sscanf的边界与逐行解析
拿到完整请求报文后,第一步是解析请求行。请求行格式是METHOD SP PATH SP VERSION\r\n,比如GET /index.html HTTP/1.1。用sscanf可以快速取出三个字段:
// parse_request —— 解析请求行,并校验方法和路径 // 返回200表示可继续处理,501表示方法不支持,403表示路径非法,-1表示报文不完整 int parse_request(char *req, char *method, size_t mlen, char *path, size_t plen) { char *line_end = strstr(req, "\r\n"); if (line_end == NULL) return -1; // 数据还没收完整 *line_end = '\0'; // 把请求行从报文中截出来 if (sscanf(req, "%15s %255s", method, path) != 2) return -1; if (strcmp(method, "GET") != 0) return 501; if (strlen(path) >= plen) return 403; if (strstr(path, "..")) return 403; // 目录穿越拦截 return 200; }sscanf的格式字符串里写的是%15s和%255s,这是刻意为之。因为s转换说明会自动在末尾补'\0',所以宽度必须比缓冲区大小小1。%15s配合char method[16]才能保证不越界。很多从PHP或Java转过来的开发者第一次写C会忽略这个细节,直接用%s,一旦收到超长请求行就直接缓冲区溢出。strstr找到的是第一个\r\n的位置,把它替换成'\0'后,请求行就独立成串了。方法不是GET时返回501,在响应阶段会构造对应的错误报文。路径里出现..直接拒绝,这是防止利用../../etc/passwd这类输入读取服务器任意文件的最基本防线。
请求头是Key: Value结构,用strtok按\r\n逐行切分即可。第一个版本的服务器可以只解析Host头,其他全部跳过。但要注意:如果后续要实现keep-alive,必须知道每个请求到底在哪里结束,Content-Length和Connection两个头就必须认真解析,否则同一个TCP连接上的后续请求会串包。
5.3 响应构造:状态行、Content-Length与Connection
HTTP响应比请求简单,状态行加响应头加空行加body,四段拼起来:
// send_response —— 拼装并发送一个HTTP响应 void send_response(int fd, int status, const char *ctype, const char *body, int body_len) { char header[512]; int n = snprintf(header, sizeof(header), "HTTP/1.1 %d %s\r\n" "Content-Type: %s\r\n" "Content-Length: %d\r\n" "Connection: close\r\n" "\r\n", status, status == 200 ? "OK" : "Not Found", ctype, body_len); send(fd, header, n, 0); send(fd, body, body_len, 0); }这里Content-Length必须和body的实际字节数严格一致。填大了,客户端会一直等待剩余数据直到超时;填小了,客户端只显示截断的内容。Connection: close让服务器响应完就关闭连接,这是新手HTTP服务器最省心的策略。如果要支持HTTP连接复用,也就是常说的keep-alive长连接,服务器就得在响应后不关闭fd,回到读取循环继续解析下一个请求,同时请求处理逻辑要能在同一个连接上区分多个请求的边界。聊天室里的长连接短连接取舍在这里同样成立:短连接实现简单,但每个请求都要重新走一遍三次握手,性能明显差一截。
5.4 完整流程与curl验证
服务器主流程紧凑且直观:accept一个连接,read请求,解析,根据路径读文件或返回错误码,发送响应,关闭连接。
// 静态文件服务的核心逻辑 char path[256]; // ... 假设已从请求中解析出path,例如 /index.html FILE *fp = fopen(path + 1, "rb"); // 跳过开头的/ if (fp == NULL) { send_response(fd, 404, "text/html", "<h1>404 Not Found</h1>", 22); return; } char body[4096]; int body_len = fread(body, 1, sizeof(body), fp); fclose(fp); send_response(fd, 200, "text/html", body, body_len);调试HTTP服务器不要用浏览器,浏览器会缓存、会并发拉起多个连接、还会自动请求favicon.ico,干扰对协议行为的判断。curl是更合适的验证工具:
curl -v http://127.0.0.1:8080/ curl -v http://127.0.0.1:8080/not_existcurl -v会把请求和响应的原始报文逐行打印出来,包括请求行、响应头、Content-Length等关键字段。看到响应头与body与预期一致后,再进浏览器验证渲染效果。
5.5 状态码与错误处理
| 状态码 | 短语 | 触发场景 |
|---|---|---|
| 200 | OK | 请求资源存在并成功返回 |
| 301 | Moved Permanently | 永久重定向 |
| 404 | Not Found | 请求资源不存在 |
| 501 | Not Implemented | 收到不支持的方法,比如POST、DELETE |
错误处理的优先级可以这样排:先判断方法支持不支持,再判断路径合法不合法,最后判断文件存在不存在。文档里对每个错误响应的构造都写了单独的发送逻辑,这比在所有分支里复制粘贴send调用要整洁得多。
6. 性能优化、安全加固与抓包验证
6.1 广播开销与多路复用怎么选
聊天室到HTTP服务器这个量级,多线程完全够用,不必一上来就上epoll。真正需要优化的是广播本身的复杂度:每来一条消息就遍历全部客户端发送一次,客户端多起来后这是O(n)的热点路径。可以先判断这条消息的业务价值,再决定是否广播;空消息直接丢弃,心跳包只更新时间戳不进广播循环。如果需要支撑更高连接数,常见做法是把select替换成epoll,用水平触发模式保持语义一致,再把每个客户端的写缓冲改成应用层队列,避免在慢客户端上阻塞整个事件循环。
6.2 三个安全习惯
第一个是限制请求长度。HTTP请求行和头字段总长度超过4KB直接回414,不为超长请求分配大缓冲区,这是对付畸形报文的基本姿势。第二个是严格校验路径,strstr(path, "..")这种检查虽然简单,但能挡住最常见的目录穿越。更稳妥的做法是先把路径归一化,再确认其前缀是web根目录。第三个是编译期加防护:
gcc -fstack-protector-all -D_FORTIFY_SOURCE=2 -O2 -o http_server http_server.c -pthread-fstack-protector-all在函数栈中插入金丝雀值,检测到栈溢出立即终止;_FORTIFY_SOURCE在编译期和运行期同时检查常见的缓冲区操作。
6.3 tcpdump与快速压测
抓包验证协议行为是最不费力的排错手段。HTTP服务器起在8080端口时,本地回环抓包只需要一条命令:
sudo tcpdump -i lo -nn 'tcp port 8080' -c 12能看到TCP握手三包、HTTP请求报文、响应报文和挥手四包,协议栈每一个状态迁移都摆在眼前。压测不必上重量级工具,shell一行就能验证多连接并发的基础能力:
seq 100 | xargs -P 20 -I{} curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/这条命令用20个并发进程各发5个请求,输出的200数量等于成功响应的数量。如果出现000或连接失败,再用tcpdump回看是握手失败、队列溢出还是响应超时。压测结果先看http_code分布,再看耗时分布,最后才看服务器端日志,这个顺序能最快定位瓶颈在协议栈、accept循环还是业务处理。
本文还有配套的精品资源,点击获取