从零开始学习嵌入式P33----网络基础之HTTP
上一篇我们啃下了 TCP:三次握手建立连接,序号确认号保证可靠,
send/recv收发字节流。但 TCP 只管"把字节安全送到",至于这些字节是网页、图片还是 JSON 数据,它一概不关心------约定数据格式和含义,是应用层协议的事。HTTP 就是应用层最出名的协议:浏览器访问网页、手机 App 请求数据、开发板查询天气,背后都是它。这一篇我们就把 HTTP 拆开看:先说清它在协议栈里的位置、C/S 与 B/S 两种结构、URL 的格式和一次完整通信的六个步骤,再细看请求报文和响应报文的格式(请求行、请求头、状态行、状态码、chunked 编码),最后写一个实战------不用浏览器、不用现成库,纯用上一篇的 TCP 编程接口手写一个 HTTP 客户端,访问天气 API,把返回的 JSON 数据解析出来显示。
本篇目标
学完这一篇,我们应该能够:
- 说清 HTTP 在 TCP/IP 协议栈中的位置,知道它是跑在 TCP 之上的应用层协议;
- 说清 C/S 和 B/S 两种网络结构的区别;
- 说清 URL 的格式,知道 HTTP 默认端口 80、HTTPS 默认端口 443;
- 说清一次完整 HTTP 通信的六个步骤;
- 看懂 HTTP 请求报文和响应报文的格式,分清请求行、请求头、空行、请求体;
- 说清 GET 和 POST 的区别,认识常见的状态码和响应头;
- 会用 TCP 套接字手写 HTTP 请求,循环接收完整响应,并剥离响应头拿到正文;
- 会处理
chunked分块传输编码,会用 cJSON 解析 JSON 数据。
一、HTTP 是什么
1. 应用层协议,跑在 TCP 之上
回顾一下 TCP/IP 协议栈的分层:
应用层 HTTP HTTPS TFTP FTP DNS TELNET SMTP ...
传输层 TCP UDP
网络层 IP(IPv4 / IPv6) ARP / RARP
链路层 以太网、Wi-Fi ...
HTTP 全称 超文本传输协议 (HyperText Transfer Protocol),工作在应用层 ,底层基于 TCP------这就是说,上一篇学的 socket / connect / send / recv 就是我们和 HTTP 服务器打交道的全部武器。TCP 负责把字节可靠送达,HTTP 负责规定这些字节的格式:客户端怎么"点菜"(请求报文),服务器怎么"上菜"(响应报文)。
2. C/S 与 B/S
基于网络通信的程序有两种常见结构:
- C/S(Client/Server,客户端/服务器):要在设备上安装专门的客户端软件,比如 QQ、微信。前面写的 UDP 聊天、TCP 聊天都是 C/S;
- B/S(Browser/Server,浏览器/服务器):不需要安装专门软件,打开浏览器输入网址就能用,比如各种网页版应用。
HTTP 天然就是为 B/S 设计的:浏览器是通用的 HTTP 客户端,任何设备只要有浏览器就能访问服务器。但 HTTP 客户端不只有浏览器------这一篇的实战里,我们自己写的 C 程序就是一个 HTTP 客户端。
3. URL:网络资源的地址
访问一个网络资源,得先说清"它在哪、怎么拿"。这就是 URL(统一资源定位符)干的事,格式为:
<协议>://<主机>:<端口>/<路径>
例如:
http://api.k780.com:80/?app=weather.today&weaid=北京&format=json
- 协议 :
http或https(加密版 HTTP); - 主机 :域名或 IP。域名靠 DNS 解析成 IP------
api.k780.com解析后可能是8.163.63.15; - 端口 :可以省略,省略时用默认端口------HTTP 默认 80,HTTPS 默认 443;
- 路径 :服务器上资源的位置;
?后面是参数 ,格式为键=值,多个参数用&连接。
4. 一次完整的 HTTP 通信
从输入网址到看到内容,背后发生了六件事:
- 解析 URL,拆出协议、主机、端口、路径;
- 根据主机名做 DNS 解析得到 IP,与 IP + 端口建立 TCP 连接(三次握手);
- 客户端按照 HTTP 格式拼好请求报文,通过 TCP 连接发给服务器;
- 服务器按照 HTTP 格式拼好响应报文,通过 TCP 连接发回来;
- 通信完毕,断开 TCP 连接(四次挥手);
- 客户端解析响应内容(浏览器渲染网页,我们的程序解析 JSON)。
注意第 2 步和第 5 步:HTTP 本身不管连接,连接是 TCP 的事。所以手写 HTTP 客户端的流程就是------先当一回 TCP 客户端,连接建好后再按 HTTP 的格式说话。
二、HTTP 报文格式
HTTP 报文是纯文本 ,这是它最大的特点------不像 TCP、IP 包头那样要按字节抠位段,HTTP 报文直接用肉眼就能看懂,用 printf 就能拼出来。
1. 请求报文:请求行 + 请求头 + 空行 + 请求体
一个典型的 GET 请求长这样:
http
GET /?app=weather.today&weaid=beijing&format=json HTTP/1.1\r\n
Host: api.k780.com\r\n
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0\r\n
Accept: application/json\r\n
Connection: close\r\n
\r\n
四个部分:
- 请求行 (第一行):
请求方法 路径 HTTP/版本,三个字段用空格隔开; - 请求头 (第二行起):每行一条,格式为
字段名: 值,向服务器补充说明情况; - 空行 :单独一个
\r\n,标志请求头结束------少了它服务器会一直傻等; - 请求体(空行之后):GET 请求一般没有请求体;POST 提交的表单、JSON 数据放在这里。
每一行(包括最后的空行)都以 \r\n 结尾------这是 HTTP 的硬性规定,不是随手敲的回车。
常见请求头:
| 请求头 | 含义 |
|---|---|
Host |
目标主机域名。HTTP/1.1 必填------一个 IP 上可能跑着多个网站,靠它区分 |
User-Agent |
客户端身份(浏览器型号等),有些服务器会拒绝不像浏览器的请求 |
Accept |
告诉服务器"我能接受什么类型的数据",如 application/json |
Connection |
close 表示服务器发完响应就关连接;keep-alive 表示保持连接复用 |
Content-Length |
请求体的字节数(POST 用) |
Content-Type |
请求体的数据类型(POST 用) |
2. GET 与 POST
HTTP 定义了一批请求方法,最常用的两个:
- GET :获取资源。参数直接拼在 URL 的
?后面,请求体为空------查天气、看新闻、下载图片都是 GET; - POST / PUT :向服务器提交数据。参数放在请求体里,不暴露在 URL 中------登录、注册、上传文件用它们。
两者最直观的区别:GET 的参数写在 URL 里(能在地址栏看到、有长度限制),POST 的参数写在请求体里(不在地址栏显示、大小基本不限)。
3. 响应报文:状态行 + 响应头 + 空行 + 响应体
服务器回过来的响应和请求结构对称:
http
HTTP/1.1 200 OK\r\n
Content-Type: application/json;charset=utf-8\r\n
Transfer-Encoding: chunked\r\n
Connection: close\r\n
\r\n
{"success":"1","result":{"citynm":"北京","temperature_curr":"5℃",...}}
- 状态行 (第一行):
HTTP/版本 状态码 状态描述; - 响应头 :每行一条
字段名: 值; - 空行 :
\r\n\r\n之前的都是头部,之后的都是正文------找正文就是找\r\n\r\n; - 响应体:真正的内容(网页 HTML、图片二进制、JSON 数据)。
4. 状态码:三位数字的门道
状态码是服务器对这次请求的"判决",按首位数字分五类:
| 分类 | 含义 | 常见例子 |
|---|---|---|
| 1xx | 提示信息,请求已收到,继续处理 | 100 Continue |
| 2xx | 成功 | 200 OK |
| 3xx | 重定向,资源在别处 | 301 永久搬家、302 临时跳转、304 缓存有效 |
| 4xx | 客户端的错 | 400 请求格式错、403 没权限、404 资源不存在 |
| 5xx | 服务器的错 | 500 服务器内部错误、502 网关错误、503 服务不可用 |
写客户端程序时,第一件事就是看状态码是不是 200------不是的话,响应体里大概率不是想要的数据,直接解析会出错。
5. Content-Length 与 chunked:正文有多长
服务器怎么告诉客户端"正文到哪里结束"?两种方式:
-
Content-Length: 数字:直接写明正文字节数,客户端收够这个数就停; -
Transfer-Encoding: chunked(分块传输):服务器边生成边发,不知道总长度,于是把正文切成一块一块发,每块开头一行十六进制的块长度 ,最后以长度为0的块收尾:1f\r\n
{"success":"1","result":{"ci\r\n
26\r\n
tynm":"北京","temperature":...}\r\n
0\r\n
\r\n
遇到 chunked 编码,客户端必须把块长度行剥掉、把各块拼起来,才能拿到干净的正文------直接把带块长度的数据拿去解析 JSON,必炸。本节的实战会正面处理这个问题。
如果请求头里写了 Connection: close,还有一个更省事的办法判断数据收完没有:服务器发完就关连接,recv 返回 0 就是结束信号------上一篇文件传输用的正是这招。
三、实战:手写 HTTP 客户端获取天气
目标:不用浏览器,纯用 TCP 套接字写一个命令行程序,输入城市名,访问天气 API(api.k780.com,IP 8.163.63.15,端口 80),把返回的 JSON 天气数据解析出来显示。
整体流程完全对应第一节讲的六步:
建立 TCP 连接(connect)
→ 拼接并发送 HTTP 请求报文(send)
→ 循环接收完整响应(recv 直到返回 0)
→ 按 \r\n\r\n 剥离响应头,找到正文
→ 若是 chunked 编码,剥掉块长度还原正文
→ 用 cJSON 解析 JSON,格式化输出
→ 关闭连接
先在 head.h 里定义服务器地址和访问凭证(天气 API 需要在平台注册获取 appkey 和 sign,把下面两个宏换成自己的):
c
#define USE_KEY "79721"
#define USE_SIGN "1d3b3409befb2fdded82c95f965ff5e2"
#define NOWAPI_ADDR "8.163.63.15"
#define NOWAPI_PORT 80
1. 建立 TCP 连接
HTTP 基于 TCP,所以第一步就是上一篇的老套路------socket + connect:
c
int CreatTcpConnection(char *pIp,int port)
{
struct sockaddr_in sendaddr;
int sockfd = 0;
int ret = 0;
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == sockfd)
{
perror("socket");
return -1;
}
sendaddr.sin_family = AF_INET;
sendaddr.sin_port = htons(NOWAPI_PORT);
sendaddr.sin_addr.s_addr = inet_addr(NOWAPI_ADDR);
ret = connect(sockfd,(struct sockaddr *)&sendaddr,sizeof(sendaddr));
if(-1 == ret)
{
perror("connect");
return -1;
}
return sockfd;
}
connect 成功,三次握手完成------六步里的第 1、2 步就走完了。接下来就是按 HTTP 的格式说话。
2. 拼接并发送 HTTP 请求
HTTP 报文是纯文本,用 snprintf 像拼字符串一样拼出来即可:
c
int SendHttpRequest(int sockfd, char *pUrl)
{
char tmpbuff[4096] = {0};
ssize_t nret = 0;
/* 不声明 gzip 压缩,直接接收明文 JSON;用 Connection: close 便于一次收完 */
snprintf(tmpbuff, sizeof(tmpbuff),
"GET %s HTTP/1.1\r\n"
"Host: api.k780.com\r\n"
"User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0\r\n"
"Accept: application/json\r\n"
"Connection: close\r\n"
"\r\n", pUrl);
nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
if (-1 == nret)
{
perror("fail to send");
return -1;
}
return 0;
}
这段代码里四个细节值得停下来看:
- 每个
\r\n一个都不能少 :请求行末尾、每条请求头末尾、以及最后标志头部结束的空行 ,少了任何一个,服务器就会认为请求没收完,一直在那等------程序就卡死在后面的recv上; Host头必须带 :HTTP/1.1 的强制要求,漏了服务器直接回400;User-Agent伪装成浏览器:很多 Web 服务器会检查这个头,不像浏览器的请求可能被拒绝或返回不同内容;Connection: close是本程序的关键设计 :告诉服务器"发完响应就关连接",这样我们只要循环recv到返回0就确定收完了,不用去解析Content-Length算字节数------这是手写简单 HTTP 客户端的经典技巧。
3. 循环接收完整响应
TCP 是字节流,一次 recv 不保证收完整个响应(上一篇的粘包教训)。循环收,直到对方关闭连接:
c
int RecvHttpRespone(int sockfd, char *precvbuff, int maxlen)
{
ssize_t nret = 0;
int total = 0;
/* 循环接收,直到对端关闭连接(Connection: close)或缓冲区满 */
while (total < maxlen - 1)
{
nret = recv(sockfd, precvbuff + total, maxlen - 1 - total, 0);
if (-1 == nret)
{
perror("fail to recv");
return -1;
}
if (0 == nret)
{
break;
}
total += nret;
}
precvbuff[total] = '\0';
return total;
}
三个要点:
- 写入位置是
precvbuff + total,剩余空间是maxlen - 1 - total------追加着收,不能每次都从缓冲区开头写,否则前一次的数据被覆盖; recv返回0时break:服务器按约定(Connection: close)发完就关了连接,数据收全了;- 循环退出后手动补
'\0'------后面要用字符串函数处理这份响应,recv可不会帮我们加结束符。
4. 剥离响应头,找到正文
响应报文里头部和正文之间隔着一个空行,所以找正文就是找 \r\n\r\n:
c
/* 跳过 HTTP 响应头,返回正文起始位置 */
char *GetHttpBody(char *pResponse)
{
char *pbody = strstr(pResponse, "\r\n\r\n");
if (NULL == pbody)
{
return NULL;
}
return pbody + 4;
}
strstr 找到 \r\n\r\n 的位置,指针加 4 跳过这四个字符,指向的就是正文第一个字节。要是跳过这一步、直接把整个响应(连状态行带响应头)丢给 JSON 解析器,解析必然失败。
5. 处理 chunked 分块编码
如果响应头里有 Transfer-Encoding: chunked,正文中还混着一行行的十六进制块长度,得先剥掉:
c
/* 若响应采用 chunked 传输编码,则将正文还原为完整数据 */
void StripChunkedEncoding(char *pbody)
{
char *src = pbody;
char *dst = pbody;
long chunksize = 0;
while (1)
{
chunksize = strtol(src, &src, 16);
while ('\r' == *src || '\n' == *src)
{
src++;
}
if (chunksize <= 0)
{
break;
}
memmove(dst, src, chunksize);
dst += chunksize;
src += chunksize;
while ('\r' == *src || '\n' == *src)
{
src++;
}
}
*dst = '\0';
}
逻辑是"读长度 → 搬数据 → 再读长度"的循环,原地压缩(dst 永远追不上 src,所以不会覆盖还没读的数据):
strtol(src, &src, 16):按十六进制 读出块长度,src自动停在长度行之后;- 跳过
\r\n,若长度为0------这是 chunked 的结束块,收工; memmove(dst, src, chunksize):把这一块的数据挪到前面去(用memmove而不是memcpy,因为源和目标可能重叠);- 跳过这块数据末尾的
\r\n,回到第 1 步读下一块; - 全部处理完在
dst处补'\0'。
做完这一步,正文才是干干净净的 JSON 字符串。
6. 用 cJSON 解析 JSON
天气 API 返回的是 JSON 格式:
json
{"success":"1","result":{"citynm":"北京","days":"2024-01-15","week":"星期一","weather":"晴","temperature_curr":"5℃","temperature":"-6℃~6℃","humidity":"湿度 20%","wind":"北风","winp":"3级","aqi":"45"}}
JSON 是按 {"键":值} 组织键值对的文本格式,手写解析太痛苦,直接用轻量级的 cJSON 库------它只有 cJSON.c 和 cJSON.h 两个文件,拷进工程一起编译就行,非常适合嵌入式。用法就两步:cJSON_Parse 把字符串解析成对象,cJSON_GetObjectItem 按键名取值。
c
/* 从 JSON 对象中取字符串字段 */
const char *GetJsonString(cJSON *pObj, const char *pKey)
{
cJSON *pitem = cJSON_GetObjectItem(pObj, pKey);
if (cJSON_IsString(pitem) && NULL != pitem->valuestring)
{
return pitem->valuestring;
}
return "未知";
}
/* 解析天气 JSON 并以中文输出 */
int PrintWeatherInfo(char *pJson)
{
cJSON *proot = cJSON_Parse(pJson);
if (NULL == proot)
{
fprintf(stderr, "JSON 解析失败\n");
return -1;
}
cJSON *psuccess = cJSON_GetObjectItem(proot, "success");
if (!cJSON_IsString(psuccess) || 0 != strcmp(psuccess->valuestring, "1"))
{
cJSON *pmsg = cJSON_GetObjectItem(proot, "msg");
fprintf(stderr, "查询失败:%s\n",
cJSON_IsString(pmsg) ? pmsg->valuestring : "未知原因");
cJSON_Delete(proot);
return -1;
}
cJSON *presult = cJSON_GetObjectItem(proot, "result");
if (NULL == presult)
{
fprintf(stderr, "返回数据中没有 result 字段\n");
cJSON_Delete(proot);
return -1;
}
printf("========== 天气信息 ==========\n");
printf("城市:%s\n", GetJsonString(presult, "citynm"));
printf("日期:%s %s\n", GetJsonString(presult, "days"),
GetJsonString(presult, "week"));
printf("天气:%s\n", GetJsonString(presult, "weather"));
printf("当前气温:%s\n", GetJsonString(presult, "temperature_curr"));
printf("今日气温:%s\n", GetJsonString(presult, "temperature"));
printf("湿度:%s\n", GetJsonString(presult, "humidity"));
printf("风向风力:%s %s\n", GetJsonString(presult, "wind"),
GetJsonString(presult, "winp"));
printf("空气质量指数:%s\n", GetJsonString(presult, "aqi"));
printf("==============================\n");
cJSON_Delete(proot);
return 0;
}
防御性编程的三道防线都值得学:
cJSON_Parse返回NULL------正文根本不是合法 JSON(比如服务器回了错误页),直接报错;- 检查
success字段------API 层面的失败(密钥错误、请求超限等)会返回{"success":"0","msg":"..."},把msg打出来方便排查; - 取每个字段前先
cJSON_IsString验证类型------字段不存在时cJSON_GetObjectItem返回NULL,不做检查就取valuestring会段错误。
用完记得 cJSON_Delete(proot) 释放------cJSON_Parse 内部申请了内存。
7. main 函数:把六步串起来
c
int main(void)
{
char Cityname[1024] = {0};
printf("请输入城市:\n");
scanf("%1023s",Cityname);
int sockfd = 0;
char Url[2048] = {0};
char recvbuff[16384] = {0};
char *pbody = NULL;
sockfd = CreatTcpConnection(NOWAPI_ADDR,NOWAPI_PORT);
if(-1 == sockfd)
{
fprintf(stderr,"creattcpconnection failed\n");
return -1;
}
snprintf(Url,sizeof(Url),"/?app=weather.today&weaid=%s&appkey=%s&sign=%s&format=json",
Cityname, USE_KEY, USE_SIGN);
if(-1 == SendHttpRequest(sockfd, Url))
{
close(sockfd);
return -1;
}
if(-1 == RecvHttpRespone(sockfd, recvbuff, sizeof(recvbuff)))
{
close(sockfd);
return -1;
}
close(sockfd);
pbody = GetHttpBody(recvbuff);
if (NULL == pbody)
{
fprintf(stderr, "响应格式错误\n");
return -1;
}
if (NULL != strstr(recvbuff, "chunked"))
{
StripChunkedEncoding(pbody);
}
return PrintWeatherInfo(pbody);
}
main 就是一张流程清单:建连接 → 拼 URL(把城市名、appkey、sign 填进参数)→ 发请求 → 收响应 → 关连接 → 剥响应头 → 看到 chunked 字样就还原分块 → 解析输出。scanf 的 %1023s 限定宽度,防止输入超出缓冲区------老规矩。
编译运行(cJSON 的两个文件一起编进来):
bash
gcc weather.c cJson.c -o weather
./weather
请输入城市:
beijing
========== 天气信息 ==========
城市:北京
日期:2024-01-15 星期一
天气:晴
当前气温:5℃
今日气温:-6℃~6℃
湿度:湿度 20%
风向风力:北风 3级
空气质量指数:45
==============================
一个不到两百行的 C 程序,干的事和浏览器访问网页一模一样------HTTP 客户端没有魔法,就是 TCP 连接加上按格式拼文本。
四、常见错误
1. 请求报文少 \r\n,程序卡死在 recv
c
snprintf(tmpbuff, sizeof(tmpbuff),
"GET %s HTTP/1.1\r\n"
"Host: api.k780.com\r\n", pUrl); // 错:没有结尾的空行
HTTP 服务器靠空行 判断请求头结束。少了最后那个单独的 \r\n,服务器就一直等请求头的后续,而我们的程序发完就在 recv 上等响应------两边互等,双双卡死 。排查这类问题,先把拼出来的报文 printf 出来,数清楚每个 \r\n。
2. 忘记 Host 头,收到 400 Bad Request
虚拟主机技术让一个 IP 能跑多个网站,HTTP/1.1 规定请求必须带 Host 头说明要访问哪个。漏掉它,守规矩的服务器直接回 400。
3. 只 recv 一次就当收完了
TCP 是字节流,一次 recv 可能只收到响应的一部分。必须循环接收 ,直到 recv 返回 0(配合 Connection: close)或者收够 Content-Length 声明的字节数。
4. 把整个响应当 JSON 直接解析
响应报文 = 状态行 + 响应头 + 空行 + 正文。不先用 \r\n\r\n 定位正文就把整个缓冲区丢给 cJSON_Parse,解析器看到 HTTP/1.1 200 OK 就失败了。
5. 忽略 chunked 编码,JSON 里混入块长度
分块传输的正文里每隔一段就插着一行十六进制长度,不剥掉就解析 JSON 必炸。稳妥的做法:检查响应头里有没有 Transfer-Encoding: chunked,有就先还原再解析。
6. URL 里直接放特殊字符
URL 中空格、中文、&、?、= 等字符有特殊含义或不允许出现,规范做法是 URL 编码 (% + 十六进制 ASCII 码)。本例演示用拼音或英文城市名最省心;真要传中文,需要先编码再拼进 URL。
7. 解析 JSON 前不做任何检查
c
cJSON *pitem = cJSON_GetObjectItem(proot, "result");
printf("%s\n", pitem->valuestring); // 危险:字段不存在时 pitem 是 NULL
字段不存在返回 NULL,服务器出错返回的 JSON 结构可能完全不一样。每个取值动作前先判断指针和类型(cJSON_IsString),否则段错误等着你。
五、HTTP 速查
| 要素 | 格式 / 要点 |
|---|---|
| URL | <协议>://<主机>:<端口>/<路径>?参数1=值1&参数2=值2 |
| 默认端口 | HTTP 80,HTTPS 443 |
| 通信六步 | 解析 URL → 建 TCP 连接 → 发请求 → 收响应 → 断开连接 → 解析内容 |
| 请求报文 | 请求行(方法 路径 HTTP/版本)+ 请求头 + 空行 + 请求体 |
| 响应报文 | 状态行(HTTP/版本 状态码 描述)+ 响应头 + 空行 + 响应体 |
| 行结束符 | 每行 \r\n,头部结束用单独的空行 \r\n |
| 找正文 | strstr(response, "\r\n\r\n") + 4 |
| GET vs POST | GET 参数在 URL、无请求体;POST 参数在请求体 |
| 判断收完 | Connection: close 时 recv 返回 0;或按 Content-Length 计数 |
| chunked | 每块:十六进制长度行 + 数据 + \r\n,以 0\r\n\r\n 结束 |
| 常见状态码 | 含义 |
|---|---|
200 OK |
成功 |
301 / 302 |
永久 / 临时重定向 |
304 |
缓存有效,内容没变化 |
400 |
请求格式错误(检查 Host 头、\r\n) |
403 / 404 |
无权限 / 资源不存在 |
500 / 502 / 503 |
服务器内部错误 / 网关错误 / 服务不可用 |
| 易混淆点 | 结论 |
|---|---|
| HTTP vs TCP | HTTP 是应用层协议(规定数据格式),TCP 是传输层协议(负责可靠传输);HTTP 跑在 TCP 之上 |
| C/S vs B/S | C/S 要装专用客户端;B/S 用浏览器即可 |
Content-Length vs chunked |
前者直接写明正文长度;后者分块发送,每块带十六进制长度,0 块结束 |
| 请求头结束 vs 报文结束 | 头部结束看空行 \r\n;整个报文结束看连接关闭或长度字段 |
请求里的 \r\n |
是协议硬性规定的行结束符,少一个都可能卡死或 400 |
小结
这一篇我们从传输层走上了应用层。先看懂 HTTP 的定位:它是跑在 TCP 之上 的应用层协议,用纯文本约定客户端和服务器怎么对话;C/S 要装客户端,B/S 靠浏览器;URL 按 <协议>://<主机>:<端口>/<路径> 定位资源,HTTP 默认 80 端口;一次通信六步走------解析 URL、建 TCP 连接、发请求、收响应、断开连接、解析内容。再看报文格式:请求报文是"请求行 + 请求头 + 空行 + 请求体",响应报文是"状态行 + 响应头 + 空行 + 响应体",行与行之间全靠 \r\n;GET 把参数放在 URL 里,POST 把参数放在请求体里;状态码首位定成败,200 才是成功。最后的天气查询实战把一切都落了地:connect 建 TCP 连接,snprintf 拼出 GET 请求,Connection: close 配合循环 recv 收完整个响应,strstr 按 \r\n\r\n 剥离响应头,strtol + memmove 还原 chunked 编码,最后用 cJSON 解析 JSON 输出------整套流程没有调用任何 HTTP 库,因为 HTTP 客户端的本质就是 TCP 连接加上按格式拼文本。至此,网络编程从 UDP、TCP 一路走到了应用层,已经具备了让嵌入式设备"上网冲浪"的完整能力。