news 2026/3/25 0:12:29

网络编程之数据封拆包与http协议

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
网络编程之数据封拆包与http协议

一、数据的 “快递包装”:封包与拆包

你想给朋友发一条 “Hello” 消息,这条消息在网络里可不是直接裸奔的 —— 它会被层层 “包装”,到了对方那里再层层 “拆开”,这就是封包和拆包

以 OSI 模型为例,数据从应用层出发,每经过一层协议,就会加上对应的协议头(相当于快递的 “面单”):

  • 应用层的 “Hello” 先到传输层,加上 TCP/UDP 头;
  • 再到网络层,加上 IP 头;
  • 最后到网络接口层,加上 MAC 头。

等数据到达目标设备后,会从网络接口层开始,逐层拆掉协议头,最终把 “Hello” 交给应用层。

这里还要注意一个概念:MTU(最大传输单元),比如以太网的 MTU 是 1500 字节。如果数据超过这个大小,会被拆分成多个数据包传输,到了目标端再重组。

MTU 相关避坑实际开发中,如果发送的数据超过 MTU 且 IP 头的D位(不分片)设为 1,数据包会被路由器丢弃并返回 “ICMP 分片需要但不分片” 错误,这是新手排查网络不通的常见要点。

二、各层 “面单”:协议头详解

不同层的协议头,承担的 “快递信息” 不一样,咱们逐个看:

1. 网络接口层:MAC 头

MAC 头是数据在局域网内的 “身份证”,格式如下:

  • 目的地址 / 源地址:各占 6 字节,是设备的物理地址(网卡 MAC 地址);
  • 类型:2 字节,标识上层协议(比如 0x0800 代表 IP 协议);
  • FCS:4 字节,用于校验数据是否损坏。

另外,物理层还会给 MAC 帧加 8 字节的前同步码 + 帧开始定界符,帮接收方同步信号。

2. 网络层:IP 头

IP 头是数据在互联网中的 “导航地址”,核心字段包括:

  • 版本:比如 IPv4/IPv6;
  • 源地址 / 目的地址:各占 4 字节(IPv4),是设备的逻辑地址;
  • TTL(生存周期):数据每经过一个网络节点,TTL 减 1;如果减到 0,数据会被丢弃(防止无限循环);
  • IP FlagD标识是否允许分片,M标识是否是最后一片分片。

TTL 的实战意义ping 目标IP可查看 TTL 值(如 Windows 默认 128,Linux 默认 64),如果 TTL 值异常(比如远小于默认值),说明数据包经过了大量路由跳转,可能存在网络环路或跨网访问慢的问题。

3. 传输层:TCP 头与 UDP 头

传输层负责 “端到端” 的通信,常用协议是 TCP 和 UDP。

TCP 头(可靠传输的 “严谨面单”)

TCP 是面向连接、可靠的协议,头里的核心字段:

  • 源端口 / 目的端口:标识应用程序(比如 HTTP 用 80 端口);
  • 序列号 / 确认号:保证数据有序、不丢包;
  • TCP Flag(标志位)
    • SYN:请求建立连接;
    • ACK:确认收到数据;
    • FIN:请求断开连接;
    • PUSH:催促对方立刻处理数据;
    • RST:重置连接。

TCP 三次握手 / 四次挥手

  • 三次握手:SYN(客户端请求)→SYN+ACK(服务器应答)→ACK(客户端确认),确保双方收发能力正常;
  • 四次挥手:FIN+ACK(主动方请求断开)→ACK(被动方确认)→FIN+ACK(被动方请求断开)→ACK(主动方确认),因为 TCP 是全双工通信,需双向关闭。
UDP 头(高效传输的 “简易面单”)

UDP 是无连接、不可靠的协议,头很简单:只有源端口、目的端口、长度、校验和,优点是速度快,适合直播、视频通话等场景。

TCP/UDP 选型建议

  • 选 TCP:要求数据可靠(如文件传输、登录、支付);
  • 选 UDP:要求低延迟(如游戏、音视频),可在应用层自己实现重传 / 校验逻辑。

三、抓包工具:Wireshark,网络的 “X 光机”

想直观看到这些协议头和数据,就得用Wireshark—— 它是网络抓包的神器,能帮你分析协议、调试网络程序。

Wireshark 使用步骤(以 Linux 为例):

  1. 终端输入sudo wireshark启动工具;
  2. 选择网络设备:本机通信选loopback,外网通信选ens33,不确定就选any
  3. 设置过滤条件:
    • 按 IP 过滤:ip.addr == 192.0.2.1
    • 按端口过滤:tcp.port == 80 || udp.port == 80
    • 按协议过滤:http(只看 HTTP 报文)、icmp(只看 ping 包)。

Wireshark 实战技巧

  • 双击数据包可展开 “分层协议头”,对照本文的协议头讲解,能直观看到每一层的字段值;
  • 右键数据包→“Follow TCP Stream” 可追踪完整的 HTTP 请求 / 响应链路,排查接口调用异常。

四、应用层:HTTP 协议,网页浏览的 “语言”

咱们平时刷网页,用的就是HTTP(超文本传输协议),它是基于 TCP 的应用层协议,负责把服务器的网页数据传给浏览器。

1. 基础概念

  • URL:统一资源定位符,是网页的 “地址”,格式是http://主机名:端口号/路径(HTTP 默认端口 80,HTTPS 默认 443,可省略),比如http://news.sohu.com/
  • HTML:超文本标记语言,是网页的 “内容格式”,浏览器收到 HTML 后会解析成咱们看到的网页样式。

2. HTTP 通信流程

HTTP 是 “请求 - 响应” 模式,流程是:

  1. 建立 TCP 连接:浏览器和服务器先通过 TCP 三次握手建立连接;
  2. 发送 HTTP 请求报文:浏览器把想要的资源(比如网页)以请求报文的形式发给服务器;
  3. 返回 HTTP 响应报文:服务器把资源打包成响应报文发给浏览器;
  4. 释放 TCP 连接:通信完成后,通过 TCP 四次挥手断开连接。

HTTP 状态码(新手必记)响应报文的第一行包含状态码,核心分类:

  • 2xx(成功):200 OK(请求成功);
  • 3xx(重定向):302 Found(临时重定向)、304 Not Modified(资源未修改,用缓存);
  • 4xx(客户端错误):404 Not Found(资源不存在)、403 Forbidden(权限不足);
  • 5xx(服务器错误):500 Internal Server Error(服务器内部错误)、503 Service Unavailable(服务器过载)。

3. HTTP 请求报文格式

HTTP 请求报文是 “纯文本” 格式,结构如下:

  • 请求行:包含方法(如GET/POST)、URL、HTTP版本,以回车换行(CRLF)结尾;
  • 首部行:多个 “字段名:值” 的组合(比如Host: news.sohu.com),每个首部行后加 CRLF;
  • 空行:一个单独的 CRLF,标识首部行结束;
  • 实体主体:可选,比如 POST 请求的参数会放在这里。

4. HTTP 核心方法:GET 与 POST 的完整区别

GET 和 POST 是 HTTP 最核心的两个请求方法,很多开发者容易混淆,咱们从协议层面、使用场景、安全性等维度讲透区别:

对比维度GETPOST
核心目的从服务器获取资源(只读操作)向服务器提交 / 修改资源(写操作)
参数传递方式参数拼接在 URL 后(?key1=val1&key2=val2参数放在请求体(实体主体)中
URL 可见性参数暴露在 URL 中,可被浏览器 / 服务器日志记录参数隐藏在请求体中,URL 不可见
数据长度限制受 URL 长度限制(不同浏览器 / 服务器限制不同,通常 2KB~8KB)无明确长度限制(由服务器配置决定)
数据类型限制仅支持 ASCII 字符,无法传输二进制数据(如文件)支持任意类型数据(文本、二进制、文件)
缓存机制可被浏览器缓存(默认会缓存响应结果)默认不缓存,需手动设置缓存头
安全性低(参数暴露,易被窃取)较高(参数隐藏,但未加密仍可抓包获取)
幂等性幂等(多次请求结果一致,无副作用)非幂等(多次提交可能产生副作用,如重复下单)
浏览器回退 / 刷新无副作用(刷新只是重新获取资源)可能触发重复提交(浏览器会提示 “确认重新提交”)
书签 / 收藏可收藏(URL 包含所有参数)不可收藏(参数在请求体中)
典型使用场景
  • GET:查询数据(如查天气、搜商品、获取文章)、分页请求、静态资源加载;
  • POST:登录 / 注册(传递账号密码)、提交表单(如订单、评论)、上传文件、支付操作。
关键提醒
  • POST 的 “安全性更高” 是相对的:它只是参数不暴露在 URL 中,抓包工具(如 Wireshark、Fiddler)仍能看到明文参数,敏感数据需配合 HTTPS 加密;
  • 不要用 GET 传递敏感信息(如密码、身份证号),哪怕对参数加密,也不建议。

新增:HTTPS 与 HTTP 的区别HTTP 是明文传输(抓包可直接看内容),HTTPS 是 “HTTP+SSL/TLS” 加密传输:

  1. 建立 TCP 连接后,先完成 SSL/TLS 握手(验证服务器证书、协商加密算法);
  2. 后续的 HTTP 报文都会被加密,即使抓包也只能看到乱码,大幅提升安全性;
  3. HTTPS 默认端口 443,HTTP 默认端口 80。

5. 网络编程必备:strstr 与 strchr 函数详解

在解析 HTTP 响应(如 JSON 格式的天气数据)时,我们会频繁用到strstrstrchr这两个 C 语言字符串处理函数,它们是解析文本数据的 “利器”。

(1)strchr:查找字符首次出现的位置
函数原型
char *strchr(const char *str, int c);
功能说明
  • 在字符串str从左到右查找字符c(注意:入参c是 int 类型,但实际是字符的 ASCII 值);
  • 找到则返回指向该字符的指针,未找到则返回NULL
  • 会匹配字符串结束符'\0'(如果c'\0',会返回指向字符串末尾的指针)。
示例(对应天气解析代码)
// 假设days指向"days":"2026-01-03" char *end = strchr(days, '"'); // 找到第二个双引号,返回指向该引号的指针 *end = '\0'; // 将引号替换为结束符,截取"days":"后的内容
(2)strstr:查找子字符串首次出现的位置
函数原型
char *strstr(const char *haystack, const char *needle);
功能说明
  • haystack:被查找的源字符串(“草堆”);
  • needle:要查找的子字符串(“针”);
  • 找到则返回指向子字符串起始位置的指针,未找到则返回NULL
  • 区分大小写,且子字符串为空时返回源字符串首地址。
示例(对应天气解析代码)
// 从HTTP响应buf中查找"days"字段的起始位置 char *days = strstr(buf, "days"); // 找到后,days指向buf中第一个"days"的首字符位置 days += 7; // 跳过"days":",定位到实际日期值(如2026-01-03)
核心区别与使用场景
函数查找目标返回值典型场景
strchr单个字符指向该字符的指针截取到某个字符为止的字符串
strstr子字符串(多个字符)指向子字符串的指针定位文本中的关键字段
注意事项
  • 两个函数都不会修改原字符串,只是返回指针;
  • 使用前必须判断返回值是否为NULL(避免空指针解引用崩溃);
  • 解析 JSON/HTTP 文本时,需结合字符串偏移(如days += 7)调整指针位置,跳过字段名和分隔符。

字符串解析避坑(新手常犯错误)

  1. 未判空:days = strstr(buf, "days");后直接days +=7,如果strstr返回NULL,会导致程序崩溃;
  2. 偏移值错误:比如把"days":"的偏移值算成 6(实际是 7),会导致解析出的字符串包含多余字符;
  3. 原字符串被修改:如果buf是只读内存(如常量字符串),*end = '\0'会触发段错误,需先拷贝到可写内存。

6. 实战案例 1:C 语言实现 HTTP GET 请求

结合strstrstrchr,我们实现一个完整的 HTTP GET 请求程序,调用天气 API 并解析数据:

#include <arpa/inet.h> #include <netinet/in.h> #include <netinet/ip.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> /* See NOTES */ #include <time.h> #include <unistd.h> typedef struct sockaddr *(SA); int main(int argc, char **argv) { // 1. 创建TCP套接字(AF_INET=IPv4,SOCK_STREAM=TCP) int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { perror("socket"); // 错误提示 return 1; } // 2. 配置服务器地址信息 struct sockaddr_in ser; bzero(&ser, sizeof(ser)); // 清空结构体 ser.sin_family = AF_INET; // IPv4协议 ser.sin_port = htons(80); // 转换为网络字节序的80端口(HTTP默认端口) // 天气API服务器的IP地址 ser.sin_addr.s_addr = inet_addr("8.129.233.227"); // 3. 建立TCP连接(三次握手) int ret = connect(sockfd, (SA)&ser, sizeof(ser)); if (-1 == ret) { perror("connect"); return 1; } // 4. 构造HTTP GET请求报文(符合HTTP协议格式) char *http_cmd[7] = {NULL}; // 请求行:GET方法 + 接口路径 + HTTP/1.1版本(参数拼接在URL后) http_cmd[0] = "GET " "/?app=weather.today&cityNm=%E8%A5%BF%E5%AE%89&appkey=36397&sign=" "41451f2bb7b779f8366f4312f18dfdab&format=json HTTP/1.1\r\n"; http_cmd[1] = "Host: api.k780.com\r\n"; // 主机名(必须字段) http_cmd[2] = "Connection: keep-alive\r\n"; // 保持连接 // 模拟浏览器的User-Agent http_cmd[3] = "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 " "Safari/537.36\r\n"; // 接受的响应格式 http_cmd[4] = "Accept: " "text/html,application/xhtml+xml,application/xml;q=0.9,image/" "avif,image/webp,image/apng,*/*;q=0.8,application/" "signed-exchange;v=b3;q=0.7\r\n"; http_cmd[5] = "Accept-Encoding: gzip, deflate\r\n"; // 接受的编码方式 http_cmd[6] = "Accept-Language: zh-CN,zh;q=0.9\r\n\r\n"; // 空行(标识首部结束) // 5. 发送HTTP请求报文到服务器 int i = 0; for (i = 0; i < 7; i++) { send(sockfd, http_cmd[i], strlen(http_cmd[i]), 0); } // 6. 接收服务器的HTTP响应报文 char buf[1024] = {0}; recv(sockfd, buf, sizeof(buf), 0); printf("完整响应报文:\n%s\n", buf); fflush(stdout); // 7. 解析响应中的天气数据(核心:strstr + strchr) char *days = NULL; char *week = NULL; char *name = NULL; char *temp = NULL; char *wea = NULL; char *end = NULL; // 第一步:用strstr定位关键字段的起始位置 days = strstr(buf, "days"); // 查找"days"字段 week = strstr(days, "week"); // 基于days继续查找"week"字段 name = strstr(week, "citynm"); // 查找城市名称字段 temp = strstr(week, "temperature"); // 查找温度字段 wea = strstr(temp, "weather"); // 查找天气状况字段 // 判空:避免strstr返回NULL导致后续操作崩溃 if (days == NULL || week == NULL || name == NULL || temp == NULL || wea == NULL) { printf("解析数据失败:未找到指定字段\n"); close(sockfd); return 1; } // 第二步:用strchr找到字段值的结束位置(双引号),并截取字符串 days += 7; // 跳过"days":"(7个字符) end = strchr(days, '"'); // 找到值后的双引号 *end = '\0'; // 替换为结束符,完成截取 week += 7; // 跳过"week":" end = strchr(week, '"'); *end = '\0'; name += 9; // 跳过"citynm":" end = strchr(name, '"'); *end = '\0'; temp += 14; // 跳过"temperature":" end = strchr(temp, '"'); *end = '\0'; wea += 10; // 跳过"weather":" end = strchr(wea, '"'); *end = '\0'; // 打印解析后的天气信息 printf("\n解析后的天气信息:\n"); printf("日期:%s | 星期:%s | 城市:%s | 温度:%s | 天气:%s\n", days, week, name, temp, wea); // 8. 关闭套接字(四次挥手) close(sockfd); return 0; }
代码运行说明:
  1. 编译:在 Linux 系统下,执行gcc http_get_weather.c -o http_get_weather
  2. 运行:执行./http_get_weather,即可看到服务器返回的 HTTP 响应,以及解析后的西安天气数据;
  3. 核心逻辑
    • 通过socket()创建 TCP 套接字,对应传输层的 TCP 协议;
    • 通过connect()建立 TCP 连接,对应 TCP 三次握手;
    • 构造符合 HTTP 协议格式的 GET 请求报文(参数拼在 URL 后);
    • strstr定位 JSON 字段,strchr截取字段值,完成数据解析。

代码优化建议(新手进阶)

  1. 原代码recv只接收一次数据,若响应超过 1024 字节会截断,可改为循环recv直到接收完所有数据;
  2. 解析 JSON 用strstr/strchr仅适用于简单场景,复杂 JSON 建议用cJSON库(开源、易用);
  3. 增加超时机制:用setsockopt设置套接字超时,避免connect/recv阻塞卡死。

7. 实战案例 2:C 语言实现 HTTP POST 请求

为了对比 GET/POST 的差异,咱们再写一个 POST 版本的请求示例(以模拟登录为例):

#include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> typedef struct sockaddr *(SA); int main() { // 1. 创建TCP套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket error"); return 1; } // 2. 配置服务器地址 struct sockaddr_in ser_addr; bzero(&ser_addr, sizeof(ser_addr)); ser_addr.sin_family = AF_INET; ser_addr.sin_port = htons(80); // 模拟测试服务器(可替换为实际接口地址) ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 3. 建立TCP连接 if (connect(sockfd, (SA)&ser_addr, sizeof(ser_addr)) == -1) { perror("connect error"); close(sockfd); return 1; } // 4. 构造POST请求参数(放在请求体中) char *post_data = "username=test&password=123456"; int data_len = strlen(post_data); // 5. 构造HTTP POST请求报文 char request[1024]; snprintf(request, sizeof(request), "POST /login HTTP/1.1\r\n" "Host: 127.0.0.1\r\n" "Content-Type: application/x-www-form-urlencoded\r\n" "Content-Length: %d\r\n" "Connection: close\r\n" "\r\n" "%s", data_len, post_data); // 6. 发送POST请求 send(sockfd, request, strlen(request), 0); // 7. 接收响应并简单解析(演示strstr用法) char buf[1024] = {0}; recv(sockfd, buf, sizeof(buf)-1, 0); printf("POST响应结果:\n%s\n", buf); // 解析响应中的"status"字段 char *status = strstr(buf, "status"); if (status != NULL) { status += 8; // 跳过"status":" char *end = strchr(status, '"'); if (end != NULL) { *end = '\0'; printf("登录状态:%s\n", status); } } // 8. 关闭连接 close(sockfd); return 0; }
POST 请求核心差异点:
  • 请求行是POST /login HTTP/1.1,无 URL 参数;
  • 新增Content-Type:标识请求体的数据格式(application/x-www-form-urlencoded是表单默认格式);
  • 新增Content-Length:告诉服务器请求体的长度;
  • 参数放在请求体中,而非 URL 后;
  • 同样可用strstr/strchr解析响应数据。

五、新手常见问题与解决方案

1. 运行 HTTP 请求代码提示 “connect: Connection refused”

  • 原因:目标服务器未启动、端口未开放,或 IP / 端口填写错误;
  • 解决:
    1. ping 目标IP测试网络连通性;
    2. telnet 目标IP 端口测试端口是否开放(如telnet 8.129.233.227 80);
    3. 确认服务器是否正常运行,防火墙是否放行该端口。

2. 解析数据时程序崩溃

  • 原因:strstr/strchr返回NULL后直接操作指针;
  • 解决:所有strstr/strchr调用后必须判空,如:
    if (days == NULL) { printf("未找到days字段\n"); return 1; }

3. 接收的响应报文乱码

  • 原因:服务器返回 gzip 压缩数据,而代码未解压;
  • 解决:
    1. 请求头去掉Accept-Encoding: gzip, deflate,让服务器返回明文;
    2. 或引入zlib库解压 gzip 数据。

从数据的 “封包拆包”,到各层协议头的 “分工”,再到 Wireshark 抓包、HTTP 协议的 GET/POST 方法,以及 C 语言字符串解析函数的使用 —— 网络通信的逻辑其实就是 “包装 - 传输 - 拆包 - 解析” 的完整链路。

新手学习网络编程的核心是 “理论 + 实战”:先理解分层协议的作用,再用 Wireshark 抓包看实际报文,最后手写代码实现 HTTP 请求,遇到问题时从 “网络连通性→端口→协议格式→数据解析” 逐步排查,就能快速掌握核心知识点。


总结

  1. 网络数据传输的核心是分层封包 / 拆包,MTU、TTL、TCP 三次握手 / 四次挥手是排查网络问题的关键;
  2. strchr(查单个字符)和strstr(查子串)是 C 语言解析文本的核心工具,使用前必须判空,避免空指针崩溃;
  3. GET/POST 的核心区别是语义(GET 读、POST 写),而非参数位置,敏感数据需用 HTTPS 加密;
  4. 实战中需注意:HTTP 报文格式要严格(CRLF 换行、空行分隔首部和体)、响应数据可能截断、端口 / IP 错误会导致连接失败。
  5. 新手排查网络问题的思路:先测连通性(ping)→ 再测端口(telnet)→ 抓包看报文(Wireshark)→ 检查代码逻辑。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/16 19:51:53

Sonic数字人项目使用Filebeat收集日志文件

Sonic数字人项目使用Filebeat收集日志文件 在AI生成内容&#xff08;AIGC&#xff09;浪潮席卷各行各业的今天&#xff0c;数字人技术正从实验室走向产线。尤其在虚拟主播、在线教育、电商直播等场景中&#xff0c;如何快速、低成本地生成高质量口型同步视频&#xff0c;成为企…

作者头像 李华
网站建设 2026/3/17 20:35:23

Sonic模型能否支持生成对抗网络?增强真实性

Sonic模型能否支持生成对抗网络&#xff1f;增强真实性 在虚拟主播、数字客服和在线教育日益普及的今天&#xff0c;用户对“会说话的面孔”不再满足于简单的口型摆动&#xff0c;而是期待更自然的表情、更精准的语音同步&#xff0c;乃至接近真人的情感表达。正是在这一需求驱…

作者头像 李华
网站建设 2026/3/15 18:09:08

站在实验室窗边盯着示波器波形的时候,突然发现MMC的电压电流相位终于对齐了。这种微妙的同步感就像乐队的弦乐组突然找准了调,忍不住想把调试过程记录成文

模块化多电平换流器&#xff08;MMC&#xff09;仿真。 采用cps-spwm&#xff08;载波相移调制&#xff09;的mmc调制技术&#xff0c;有子模块的电容电压平衡策略。 通过结果可以看出来电压电流的相位补偿一致了。 提供总结pdf和参考文献。咱们先来点硬核的——MATLAB里生成相…

作者头像 李华
网站建设 2026/3/22 14:57:55

Sonic模型是否支持多人物同时说话?当前局限性说明

Sonic模型是否支持多人物同时说话&#xff1f;当前局限性说明 在数字人技术快速渗透短视频、直播带货和在线教育的今天&#xff0c;越来越多的内容创作者开始依赖AI驱动的“会说话头像”来提升生产效率。其中&#xff0c;由腾讯与浙江大学联合推出的Sonic模型因其轻量高效、高…

作者头像 李华
网站建设 2026/3/23 12:24:04

5步搞定Unity游戏翻译:XUnity Auto Translator完整指南

5步搞定Unity游戏翻译&#xff1a;XUnity Auto Translator完整指南 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 想要让Unity游戏突破语言障碍&#xff0c;面向全球玩家吗&#xff1f;XUnity Auto Tran…

作者头像 李华