从零开始学习嵌入式P33----网络基础之HTTP

从零开始学习嵌入式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
  • 协议httphttps(加密版 HTTP);
  • 主机 :域名或 IP。域名靠 DNS 解析成 IP------api.k780.com 解析后可能是 8.163.63.15
  • 端口 :可以省略,省略时用默认端口------HTTP 默认 80,HTTPS 默认 443
  • 路径 :服务器上资源的位置;? 后面是参数 ,格式为 键=值,多个参数用 & 连接。

4. 一次完整的 HTTP 通信

从输入网址到看到内容,背后发生了六件事:

  1. 解析 URL,拆出协议、主机、端口、路径;
  2. 根据主机名做 DNS 解析得到 IP,与 IP + 端口建立 TCP 连接(三次握手);
  3. 客户端按照 HTTP 格式拼好请求报文,通过 TCP 连接发给服务器;
  4. 服务器按照 HTTP 格式拼好响应报文,通过 TCP 连接发回来;
  5. 通信完毕,断开 TCP 连接(四次挥手);
  6. 客户端解析响应内容(浏览器渲染网页,我们的程序解析 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 需要在平台注册获取 appkeysign,把下面两个宏换成自己的):

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;
}

这段代码里四个细节值得停下来看:

  1. 每个 \r\n 一个都不能少 :请求行末尾、每条请求头末尾、以及最后标志头部结束的空行 ,少了任何一个,服务器就会认为请求没收完,一直在那等------程序就卡死在后面的 recv 上;
  2. Host 头必须带 :HTTP/1.1 的强制要求,漏了服务器直接回 400
  3. User-Agent 伪装成浏览器:很多 Web 服务器会检查这个头,不像浏览器的请求可能被拒绝或返回不同内容;
  4. 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 返回 0break:服务器按约定(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,所以不会覆盖还没读的数据):

  1. strtol(src, &src, 16):按十六进制 读出块长度,src 自动停在长度行之后;
  2. 跳过 \r\n,若长度为 0------这是 chunked 的结束块,收工;
  3. memmove(dst, src, chunksize):把这一块的数据挪到前面去(用 memmove 而不是 memcpy,因为源和目标可能重叠);
  4. 跳过这块数据末尾的 \r\n,回到第 1 步读下一块;
  5. 全部处理完在 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.ccJSON.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;
}

防御性编程的三道防线都值得学:

  1. cJSON_Parse 返回 NULL------正文根本不是合法 JSON(比如服务器回了错误页),直接报错;
  2. 检查 success 字段------API 层面的失败(密钥错误、请求超限等)会返回 {"success":"0","msg":"..."},把 msg 打出来方便排查;
  3. 取每个字段前先 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(把城市名、appkeysign 填进参数)→ 发请求 → 收响应 → 关连接 → 剥响应头 → 看到 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: closerecv 返回 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 一路走到了应用层,已经具备了让嵌入式设备"上网冲浪"的完整能力。

相关推荐
励志不掉头发的内向程序员1 小时前
【LibreCAD 2D架构】从鼠标点击到屏幕像素:LibreCAD绘图架构全链路解析之Action与命令系统
linux·开发语言·c++·qt·学习·系统架构
小道士写程序1 小时前
自然语言处理NLP - 第8章 词向量与神经网络:从手工特征到学习表示
神经网络·学习·自然语言处理
2401_862880821 小时前
Linux应用层开发 --- HTTP
linux·运维·http
zbyyd2 小时前
Linux 网络编程:IO 多路复用
linux·运维·c语言·网络
SKH.2 小时前
网络(3)TCP通信
网络·网络协议·tcp/ip
μθημα2 小时前
Kubernetes 微服务网络实践:Service 与 Ingress 从入门到灰度发布
网络·微服务·kubernetes
晊晌_h2 小时前
嵌入式从0到精通——Linux 网络通信|TCP、HTTP 网络编程
linux·c语言·网络
MetaLite2 小时前
HTTPS之外的响应加密-GCM序列化与处理器顺序
网络协议·http·https
姚不倒2 小时前
Tengine 实战:主动健康检查 + 动态 Upstream 配置
运维·网络·nginx