
简介这是一份面向C网络编程初学者与需要快速验证HTTP请求流程的开发者的轻量示例代码聚焦在Windows环境下用C完成一次基础HTTP GET访问并附带请求耗时统计便于理解网络通信与性能度量的结合方式。压缩包仅2KB共3个文件由两个头文件和一个CPP源文件组成分别承担接口声明、计时器抽象与请求实现等职责结构精简适合直接阅读、编译和拆解学习。现有268人浏览学习说明其作为入门参考具有一定关注度。读者可从中获得一个可运行的最小实现骨架观察请求类如何组织、执行流程与响应获取如何衔接以及如何用计时器记录单次请求耗时也可据此补充超时、重定向、异常处理和HTTPS支持等扩展点作为进一步完善的起点适合用于课程示例、自学练手和轻量工具原型。1. 网页是别人家的文件C 想拿到它只差一次「问」和「答」很多人第一次听到「用 C 发 HTTP 请求」脑子里立刻浮现一大组网络库、一堆看不懂的报错。其实换个角度想就很朴素网页就是别人服务器上的一份文本文件你的程序想拿到它只需要隔着网络问一句「把 /index.html 给我」然后等对方把内容送回来。这句「怎么问、怎么收」就是 HTTP GET 要解决的全部问题。这份简单的 HTTP GET C 实现做的事情很基础——不依赖任何第三方库用系统自带的 socket 把请求发出去、把响应收回来。它适合刚学完 C 语法、想让自己的程序第一次「连上网」的新手。2. 先搞懂 GET 报文它其实只是一段有格式的纯文本2.1 三段式结构请求行、请求头、一个不能省的空行HTTP 协议最反直觉的一点是它规定的是「文本长什么样」而不是「二进制包怎么排」。你在终端里敲出来的字符串和真正发到网线上的字节除了换行必须是\r\n之外几乎一模一样。所以第一次学 GET别急着背函数名先把这段文本的形状记住。一个 HTTP/1.1 的 GET 请求固定由三部分组成顺序不能乱请求行GET 空格 路径 空格 协议版本请求头若干行名字: 值每行以\r\n结尾一个空行单独一个\r\n告诉服务器「头到此为止下面没有请求体」写成看得见的样子是这样GET /index.html HTTP/1.1 Host: example.com Connection: close最后那个空行是新手最容易漏掉的。漏了服务器就会一直等你发出「头结束」的信号表现是程序看起来卡死不返回而你对着代码找半天也找不到哪里写错了。Host这一行在 HTTP/1.1 里是强制的。因为一台服务器上可能挂着几十个站点服务器靠Host才知道你要的是哪一个。哪怕你直接用 IP 连过去这一行也得写值可以填一个域名凑数。Connection: close不是必须的但它对新手极其友好它告诉服务器「响应发完就把连接关掉」。这样你的接收循环只要等到recv返回 0就知道内容收完了不用去纠结长连接怎么判断边界。入门阶段先让它简单。2.2 亲手发一次把「玄学」变成「看得见」在写 C 之前先用系统自带的小工具发一次请求。这一步的意义在于你会亲眼看到所谓「网络请求」就是打出去一串字符然后回来一大串字符没有任何神秘的东西。# 把一段文本原样打到 example.com 的 80 端口 # printf 里的 \r\n 是真实的回车换行别图省事写成 \n printf GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n | nc example.com 80如果你机器上没有 nc用 curl 也能看到同样的东西# -v 会把发出去的请求行和请求头原样打出来 # 前缀 表示发送出去的内容前缀 表示接收回来的内容 curl -v http://example.com/跑完你会看到屏幕上刷出一大段 HTML。这时候再回头看 2.1 的三段式你会发现它真的就是那么几行字。我第一次跑通的时候有点意外——原来「联网」的门槛这么低低到只需要会拼字符串。如果你手上什么都没有也不用担心。这一步纯粹是为了建立直觉跳过它直接写代码也完全能跑只是后面出问题时你会少一个对照物。2.3 响应长什么样状态行、头、空行、正文服务器回给你的东西结构和请求几乎对称也是四段状态行例如HTTP/1.1 200 OK响应头若干行例如Content-Type: text/html、Content-Length: 1256一个空行响应正文可能是 HTML、JSON也可能是一张图片的原始字节状态码入门阶段记住四个就够用200 表示成功301 和 302 表示跳转到了别处404 表示路径不对500 表示服务器自己出错了。看到 404 先检查你请求的路径写没写对看到 500 一般是对方的问题不是你的事。这里有一个必须提前知道的点正文到底有多长是由响应头里的Content-Length决定的或者由Transfer-Encoding: chunked表示「分块发送、长度不固定」。这个细节直接决定了你接收数据时该循环还是只收一次。后面第 5 章讲「响应被截断」的坑根源就在这里。3. 动手前的准备一次 GET 在 socket 层面只对应四个动作3.1 域名变 IPgetaddrinfo 帮你查「电话簿」你写的是example.com但网络设备只认 IP 地址。这中间必须有一次「查号」的过程专业点叫 DNS 解析。C 里最省事的做法是调用getaddrinfo它会帮你查号、顺便把结果整理成一张「候选地址表」。为什么要给一张表而不是一个地址因为现在一个域名往往同时对应 IPv4 和 IPv6 多个地址有的通有的不通。稳妥的写法是遍历这张表挨个试着连第一个连上的就用它。这也是新手代码和自己写着玩的代码之间最明显的差距之一。// 声明两个变量hints 用来告诉函数我想要什么样的地址res 用来接结果 addrinfo hints{}, *res nullptr; hints.ai_family AF_UNSPEC; // 不限定 IPv4 还是 IPv6都给 hints.ai_socktype SOCK_STREAM; // 要 TCPHTTP 就跑在 TCP 上 // 参数依次是域名、端口字符串形式、筛选条件、结果指针的地址 int rc getaddrinfo(example.com, 80, hints, res);getaddrinfo返回 0 表示成功非 0 表示失败。失败最常见的原因是域名拼错或者你的机器压根没配好网络。用完这张表记得调用freeaddrinfo(res)释放它是函数内部帮你申请的内存你不放就漏了。3.2 socket / connect / send / recv四次调用各自的职责把这四个函数想成打电话的四个动作就好记了函数类比你要给它什么返回值怎么看socket买一部手机地址族、类型、协议返回一个整数句柄小于 0 就是失败connect拨号句柄、对方地址、地址长度返回 0 表示接通send说话句柄、数据指针、数据长度、标志位返回实际发出去的字节数recv听对方说句柄、缓冲区、缓冲区长度、标志位大于 0 是收到的字节数等于 0 是对面挂了小于 0 是出错send的返回值值得单独说一句它返回的是「这次实际发出去多少字节」不保证等于你给它的长度。发几行请求头这种几十字节的小数据通常一次就发完了但如果你以后要 POST 一个大文件就必须写个循环一直发到全部发完为止。入门阶段可以先用一次send心里要知道这里埋着一个隐患。recv的返回值 0 是新手最容易误判的地方。它不是「没收到数据」而是「对方已经关闭了连接不会再有数据了」。所以在Connection: close模式下循环条件写成while ((n recv(...)) 0)刚刚好。3.3 环境准备与最小前置知识清单这份实现是标准 POSIX socket 写法Linux 和 macOS 上开箱即用一条命令就能编译# -stdc17 打开现代 C 语法-o 指定输出的可执行文件名 g -stdc17 simple_get.cpp -o simple_get如果你在 Windows 上需要额外做两件事包含winsock2.h而不是sys/socket.h以及在程序开头调用一次WSAStartup、编译时链接ws2_32库。差别不大但少了这一步就会报一堆「找不到 socket」的错误第 5 章会专门讲。前置知识其实很少会写main函数、知道std::string怎么拼接、见过指针和结构体就够了。不需要懂多线程不需要懂模板也不需要提前学什么网络原理。我建议的顺序就是先照着抄一遍跑通再回头一行一行问「这句不写会怎样」。带着问题回头看比先看两小时理论有用得多。4. 第一个能跑的例子五十行 C 抓下一个网页4.1 完整代码从解析域名到打印响应把下面这段存成simple_get.cpp改掉host和path两个变量编译运行你就能在终端里看到网页源码。// simple_get.cpp —— 最小可跑的 HTTP GET 客户端Linux / macOS // 编译: g -stdc17 simple_get.cpp -o simple_get #include iostream #include string #include sys/types.h #include sys/socket.h #include netdb.h #include unistd.h int main() { const std::string host example.com; // 只写域名不要带 http:// const std::string path /; // 想抓哪个页面就改这里 const std::string port 80; // HTTP 默认端口是 80 // 步骤 1域名 - 候选地址表 addrinfo hints{}, *res nullptr; hints.ai_family AF_UNSPEC; // IPv4 / IPv6 都接受 hints.ai_socktype SOCK_STREAM; // TCP if (getaddrinfo(host.c_str(), port.c_str(), hints, res) ! 0) { std::cerr DNS 解析失败检查域名拼写\n; return 1; } // 步骤 2遍历候选地址连上第一个能连的 int fd -1; for (addrinfo* p res; p ! nullptr; p p-ai_next) { fd socket(p-ai_family, p-ai_socktype, p-ai_protocol); if (fd 0) continue; // 建句柄失败换下一个 if (connect(fd, p-ai_addr, p-ai_addrlen) 0) break; // 接通了 close(fd); // 没接通先关掉再试 fd -1; } freeaddrinfo(res); // 表用完必须释放 if (fd 0) { std::cerr 连接失败检查网络或端口\n; return 1; } // 步骤 3拼请求报文结尾的空行不能省 std::string req GET path HTTP/1.1\r\n Host: host \r\n Connection: close\r\n \r\n; send(fd, req.c_str(), req.size(), 0); // 步骤 4循环接收直到对方关闭连接 char buf[4096]; ssize_t n; while ((n recv(fd, buf, sizeof(buf), 0)) 0) { std::cout.write(buf, n); // 按字节原样输出不做任何转换 } close(fd); return 0; }4.2 逐段拆解每行在干什么、参数怎么改步骤 1 里那个hints结构体你可以当成一张「需求单」你勾选「我不挑地址族」「我要 TCP」函数就按这个要求去查。res是输出参数查到的结果挂在它上面。两个参数都是字符串形式端口写成80而不是80这是个容易写错的小地方。步骤 2 的循环是整份代码里最值得学的部分。它做了三件事建句柄、试连接、失败就关掉换下一个。少了这个循环程序在 IPv6 优先的环境里可能一次都连不上而你完全不知道为什么。fd初始设成-1是个小技巧循环结束后只要它还是-1就说明所有候选地址全失败了。步骤 3 是拼字符串。这里用了三个\r\n分别结束请求行、Host头、Connection头最后还有一个\r\n单独作为空行。数一下正好四个少一个服务器就不认。send的第三个参数是req.size()注意不是sizeof(req)后者算的是std::string对象本身的大小是个典型的翻车点。步骤 4 的buf大小 4096 是个折中值太小要循环很多次太大在栈上占空间。std::cout.write(buf, n)用的是write不是因为遇到\0就停了而 HTTP 正文里完全可能出现\0用错了会莫名其妙少一截内容。4.3 跑通之后先做三件小事第一件把path改成/index.html或者别的路径再跑一次。如果对方回的是 404说明你成功收到了错误页面这本身就是进步——你已经开始会读状态码了。第二件把std::cout.write(...)换成把内容写进文件用浏览器打开看看// 在 while 循环之前打开文件循环里写文件而不是写屏幕 std::ofstream out(page.html, std::ios::binary); // ...循环内改成 out.write(buf, n);第三件故意把Host那一行删掉再跑一次看看服务器回什么。亲手制造一次错误比看十遍「Host 是强制的」记得牢。这三件事做完你对 GET 的理解就从「抄过」变成「试过」了。5. 新手避坑五个最容易翻车的点5.1 连不上、连得慢三个高频故障坑一域名前面带了http://现象程序直接打印「DNS 解析失败」或者卡几秒后连不上。原因getaddrinfo要的是纯主机名http://example.com里的冒号和斜杠对它来说是非法字符。解决把协议头拆出去host只留example.com协议部分你在心里知道是 HTTP 就行代码里写死在请求行的HTTP/1.1里。坑二忘了Host头或者忘了结尾空行现象服务器回400 Bad Request或者程序跑完一直不退出。原因HTTP/1.1 规定必须有Host而结尾空行是「头部结束」的分隔符缺了服务器会继续等你发。解决把请求报文打印出来数\r\n的个数一个标准 GET 是四个也可以先用 2.2 的 curl 命令拿到一份正确样本逐字节对照。坑三Windows 上编译报一堆「未定义」现象undefined reference to socket、connect之类。原因Windows 的网络功能在单独的库里不链接就不会被带进来而且必须先初始化才能用。解决包含winsock2.hmain开头调用WSAStartup(MAKEWORD(2,2), wsaData)编译时加上-lws2_32。这三步缺一不可。5.2 收得不对两个隐蔽故障坑四响应被截断网页只拿到一半现象输出看着像是少了尾巴HTML 没有闭合标签。原因recv一次只保证返回「当前能拿到的一部分」不是全部。你如果只调用一次就完事剩下还在路上。解决一定要写循环条件是recv返回值大于 0配合Connection: close等到返回 0 就自然结束。坑五中文变成乱码或者程序一直不退出现象一屏幕上出现一堆看不懂的字符。原因响应正文可能被gzip压缩过你收到的是压缩后的字节直接打印当然乱。解决在请求头里加一行Accept-Encoding: identity明确告诉对方别压缩或者先把字节存成文件用浏览器打开浏览器会自己解压。现象二数据都收到了程序就是不结束。原因你没写Connection: close服务器默认保持长连接recv就一直等着下一次数据。解决加上这一行或者自己判断Content-Length收够了就主动close(fd)。6. 下一步学什么从「能抓到」到「抓得稳」的三条路跑通第一个例子之后最怕的是不知道往哪走。我给三条路线按你想起做什么来选不用全走。路线适合的场景要先补的东西大概会卡在哪加 HTTPS 支持想抓的站点全是 httpsTLS 加密库的基本用法、证书校验概念证书验证失败、握手报错换成熟客户端库想快速做个能用的工具库的接口文档、超时与重试设置库自己的配置项太多啃 HTTP 细节想搞明白协议本身分块传输、重定向、状态码语义chunked 解析容易写错我的建议是主要走第二条顺手了解第一条。原因很实在手写 socket 的价值在于让你知道底下发生了什么但真做东西时超时、重定向、连接复用、编码处理这些杂事成熟库已经替你处理好了没必要重复做一遍。至于分块传输那些细节等你真的遇到「响应少了一截」再回头看效率最高。具体动作可以这样排先给现在的代码加一个超时——connect在某些网络下会卡很久加个几秒的上限体验立刻不一样然后把「拼 URL、发请求、存文件」拆成三个小函数你会发现代码一下子清爽了最后再考虑要不要引入第三方库。有一点血泪经验从那以后我每次写网络相关的代码第一件事都是把发出去的原始报文打印出来看一眼而不是直接怀疑逻辑。十次里有八次问题就出在那几行字符串上——少一个\r\n、多一个http://、Host拼错一个字母。这个习惯比多学三个库都值钱。这份简单的 HTTP GET C 实现源码就是这个思路每个步骤分开注释变量名直白没有为了「显得专业」而绕弯子。你可以直接拿它当起点在上面改路径、改域名、加请求头改坏了再对照一遍。希望帮到你。本文还有配套的精品资源点击获取