目录
- 用 C 写 HTTP 客户端:为什么 select 还要配合非阻塞 IO
用 C 写 HTTP 客户端:为什么 select 还要配合非阻塞 IO
学习代码:http/http_request.c
HTTP 项目表面上是写一个 GET/POST 客户端,本质上是在练 TCP 客户端的完整流程:域名解析、建立连接、构造请求、发送数据、循环接收响应。和浏览器相比,自己用 C 写一遍会发现,HTTP 文本格式很清楚,但网络 IO 的边界情况一点都不少。
一个最基础的 GET 请求可以这样构造:
snprintf(request,sizeof(request),"GET %s HTTP/1.1\r\n""Host: %s\r\n""User-Agent: SimpleCClient/1.0\r\n""Accept: */*\r\n""Connection: close\r\n""\r\n",path,host);这里要注意\r\n,HTTP 请求行和请求头都用回车换行结束,头部结束后还要有一个空行。Connection: close的作用是告诉服务器响应完成后关闭连接,这样客户端可以通过recv返回 0 判断对端关闭,简化读取逻辑。
项目里更关键的是select接收模板。刚开始我以为select返回可读之后,recv就一定不会阻塞。后来看笔记才明白:select只能说明“当时看起来可读”,它不是永久保证。多线程抢读、连接异常、协议状态变化,都可能让后续阻塞调用卡住。因此更稳妥的方式是把 fd 设置成非阻塞:
intflags=fcntl(sockfd,F_GETFL,0);fcntl(sockfd,F_SETFL,flags|O_NONBLOCK);同时,每轮select前都要重新设置fd_set和timeval:
FD_ZERO(&read_fds);FD_SET(sock,&read_fds);tv.tv_sec=timeout_sec;tv.tv_usec=0;ret=select(sock+1,&read_fds,NULL,NULL,&tv);这是一个很容易踩的坑。因为select会修改fd_set,有些系统还会把timeval改成剩余时间。如果循环里不重置,后面的判断就可能被污染,出现空转或漏读。
我的理解是,HTTP 客户端练的不只是协议文本,更是“可靠地读完不确定长度的数据”。网络编程里,读到一半、超时、对端关闭、暂时无数据都是正常情况。代码必须把这些状态写清楚,程序才不会在真实网络环境里表现得忽好忽坏。
学习链接: https://github.com/0voice