说明:裸机 / MCU 环境一般不会自己手写 TLS 底层,TLS 用 mbedtls/wolfssl 库 ;HTTP/MQTT 是应用层协议,C 写应用层解析。
推荐学习路线:
- 先看懂裸 TCP → HTTP(明文)
- 再引入 mbedtls 封装 TCP,实现 HTTPS
- 裸 TCP 实现 MQTT(明文)
- mbedtls 封装 TCP,实现 MQTTs(加密)
环境建议:Linux PC 编译测试(方便,不用烧 MCU),代码可移植到 STM32/ESP32。依赖:
mbedtls库,安装:sudo apt install libmbedtls-dev
基础概念区分
表格
| 协议 | 底层 | 安全 | 端口 |
|---|---|---|---|
| HTTP | TCP 明文 | 无加密 | 80 |
| HTTPS | TCP + TLS | TLS 传输加密 | 443 |
| MQTT | TCP 明文 | 无加密 | 1883 |
| MQTTs | TCP + TLS | TLS 传输加密 | 8883 |
重点:HTTPS / MQTTs 不是协议本身加密,是在 TCP 和应用层之间套一层 TLS。应用层报文格式和明文版本完全一样,只是 TCP 传输的数据被 TLS 加密。
1. C 最简 HTTP Client(明文 TCP,Linux)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define BUF_SIZE 1024
int create_tcp_socket(const char *host, int port)
{
struct hostent *he = gethostbyname(host);
if (!he) return -1;
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
memcpy(&server_addr.sin_addr.s_addr, he->h_addr, he->h_length);
if (connect(fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
close(fd);
return -1;
}
return fd;
}
int main(void)
{
int sockfd = create_tcp_socket("httpbin.org", 80);
if (sockfd < 0)
{
perror("connect fail");
return 1;
}
// HTTP GET 请求
char req[] = "GET /get HTTP/1.1\r\nHost: httpbin.org\r\nConnection:close\r\n\r\n";
send(sockfd, req, strlen(req), 0);
char buf[BUF_SIZE];
ssize_t n;
while ((n = recv(sockfd, buf, BUF_SIZE - 1, 0)) > 0)
{
buf[n] = 0;
printf("%s", buf);
}
close(sockfd);
return 0;
}
编译:gcc http.c -o http
原理:TCP 连接后直接发送 HTTP 文本请求,接收返回。全部明文。
2. HTTPS Client(TCP+mbedtls TLS 封装)
HTTPS = TCP + TLS 握手,握手成功后读写的是 TLS 加密流,上层 HTTP 报文和上面完全不变。
#include <stdio.h>
#include <string.h>
#include <mbedtls/net_sockets.h>
#include <mbedtls/ssl.h>
#include <mbedtls/entropy.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/error.h>
#define BUF_SIZE 1024
int main(void)
{
mbedtls_net_context net_ctx;
mbedtls_ssl_context ssl;
mbedtls_ssl_config ssl_cfg;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_net_init(&net_ctx);
mbedtls_ssl_init(&ssl);
mbedtls_ssl_config_init(&ssl_cfg);
mbedtls_entropy_init(&entropy);
mbedtls_ctr_drbg_init(&ctr_drbg);
// 随机数种子
mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0);
mbedtls_ssl_config_defaults(&ssl_cfg,
MBEDTLS_SSL_IS_CLIENT,
MBEDTLS_SSL_TRANSPORT_STREAM,
MBEDTLS_SSL_PRESET_DEFAULT);
mbedtls_ssl_conf_rng(&ssl_cfg, mbedtls_ctr_drbg_random, &ctr_drbg);
// 生产环境必须开启证书校验!这里学习临时关闭
mbedtls_ssl_conf_authmode(&ssl_cfg, MBEDTLS_SSL_VERIFY_NONE);
mbedtls_ssl_setup(&ssl, &ssl_cfg);
// 连接服务器 443端口
int ret = mbedtls_net_connect(&net_ctx, "httpbin.org", "443", MBEDTLS_NET_PROTO_TCP);
if(ret != 0) { printf("net connect fail\n"); goto cleanup; }
mbedtls_ssl_set_bio(&ssl, &net_ctx, mbedtls_net_send, mbedtls_net_recv, NULL);
printf("TLS handshaking...\n");
ret = mbedtls_ssl_handshake(&ssl);
if(ret != 0)
{
char err_buf[1024];
mbedtls_strerror(ret, err_buf, sizeof(err_buf));
printf("handshake err: %s\n", err_buf);
goto cleanup;
}
printf("TLS handshake ok\n");
// HTTP GET,报文和明文HTTP完全一样!
char req[] = "GET /get HTTP/1.1\r\nHost: httpbin.org\r\nConnection:close\r\n\r\n";
mbedtls_ssl_write(&ssl, (unsigned char*)req, strlen(req));
unsigned char buf[BUF_SIZE];
while(1)
{
ret = mbedtls_ssl_read(&ssl, buf, BUF_SIZE-1);
if(ret <=0) break;
buf[ret] = 0;
printf("%s", buf);
}
cleanup:
mbedtls_ssl_close_notify(&ssl);
mbedtls_net_free(&net_ctx);
mbedtls_ssl_free(&ssl);
mbedtls_ssl_config_free(&ssl_cfg);
mbedtls_ctr_drbg_free(&ctr_drbg);
mbedtls_entropy_free(&entropy);
return 0;
}
编译:gcc https.c -o https -lmbedtls -lmbedx509 -lmbedcrypto
关键点:
- TLS 握手成功后,
mbedtls_ssl_write/read替代原生send/recv- HTTP 请求报文和明文 HTTP 一模一样,只是底层传输被 TLS 加密
生产环境:不能关闭证书校验,需要加载 CA 根证书,防止中间人攻击
3. MQTT 明文 C 客户端(极简,自己实现 MQTT 报文)
MQTT 是二进制协议,不是文本。核心报文:CONNECT、SUBSCRIBE、PUBLISH。
下面极简示例,只演示 CONNECT + PUBLISH(发布一条消息到 1883 端口公共 MQTT broker test.mosquitto.org)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
int create_tcp_socket(const char *host, int port)
{
struct hostent *he = gethostbyname(host);
if (!he) return -1;
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
memcpy(&addr.sin_addr.s_addr, he->h_addr, he->h_length);
if(connect(fd,(struct sockaddr*)&addr,sizeof(addr))<0) { close(fd); return -1; }
return fd;
}
// 构造MQTT CONNECT报文
void build_mqtt_connect(unsigned char *pkt, int *len)
{
unsigned char client_id[] = "test_client_001";
int cli_len = sizeof(client_id)-1;
int var_len = 2 + cli_len;
*len = 2 + var_len;
pkt[0] = 0x10; // CONNECT
pkt[1] = var_len;
pkt[2] = 0; pkt[3] = 4;
memcpy(pkt+4, "MQTT",4);
pkt[8] = 0x02; // clean session
pkt[9] = 0; pkt[10] = 60; // keepalive
pkt[11] = (cli_len>>8)&0xff;
pkt[12] = cli_len &0xff;
memcpy(pkt+13, client_id, cli_len);
}
int main(void)
{
int fd = create_tcp_socket("test.mosquitto.org",1883);
if(fd<0) { perror("tcp connect");return 1; }
unsigned char pkt[128];
int pkt_len;
build_mqtt_connect(pkt, &pkt_len);
send(fd, pkt, pkt_len, 0);
unsigned char resp[64];
recv(fd, resp, sizeof(resp),0);
printf("CONNECT ACK rc=%d\n", resp[3]);
// PUBLISH 报文,发送消息
unsigned char pub[] = {
0x30, 0x11, // PUBLISH
0x00,0x04, // topic len
't','e','s','t', // topic "test"
'h','e','l','l','o',' ','m','q','t','t'
};
send(fd, pub, sizeof(pub),0);
sleep(1);
close(fd);
return 0;
}
学习重点:MQTT 是二进制头部 + 可变长度编码,比 HTTP 复杂。
实际项目不会手写完整 MQTT 协议栈,常用:paho-mqtt C 库。
paho-mqtt(推荐,工业标准 MQTT C 库)
paho-mqtt 本身支持:明文 MQTT;对接 mbedtls/wolfssl 后直接 MQTTs。
- MQTT:paho 直接用原生 socket
- MQTTs:paho 注册 mbedtls 的读写回调,底层走 TLS,上层 MQTT 报文完全不变
4. MQTTs(MQTT over TLS)
逻辑和 HTTPS 完全一致:
- TCP 连接 broker 8883 端口
- mbedtls TLS 握手(校验证书)
- TLS 握手成功,用
mbedtls_ssl_write/read收发 MQTT 二进制报文
MQTT 的 CONNECT/PUBLISH/SUBSCRIBE 报文和明文 MQTT 完全一样,只是 TCP 链路被 TLS 加密。
两种学习路径建议
路径 A(由浅入深,推荐)
- 原生 socket TCP → HTTP(看懂文本请求响应)
- 原生 socket TCP → MQTT(看懂二进制报文)
- mbedtls TLS 封装 socket → HTTPS
- mbedtls TLS 封装 socket → MQTTs
路径 B(工程化,项目用)
- HTTP/HTTPS:使用
curl库(libcurl),不用自己解析 HTTP - MQTT/MQTTs:使用 paho-mqtt + mbedtls,不用手写 MQTT 协议解析
重要安全要点(和你前面 OTA 关联)
- HTTPS/MQTTs 的 TLS 主要解决传输加密 + 身份认证,防止中间人抓包篡改。
- TLS 只能保护链路;固件本身下载到 flash 后仍是明文,所以 OTA 还要额外做固件 AES 加密 + 数字签名(就是上一节我们聊的 OTA 加密)。
- 证书校验不要关闭,关闭 TLS 等于没有安全,中间人攻击可以劫持。
- MCU 资源紧张时,优先 TLS1.2,TLS1.3 握手更快,但库体积更大。
常见坑
- MQTT 可变长度剩余长度编码很容易写错,手写协议栈容易 bug,学习可以写,项目尽量 paho。
- TLS 握手超时、证书过期、域名不匹配。
- 网络缓冲区过小,分包读写 TLS 流,需要处理粘包。
- MCU 上内存不足,mbedtls 栈 / 堆配置不够,握手失败。
HTTP / HTTPS / MQTT / MQTTs 协议栈分层框图
分层逻辑:应用层 ↔ TLS 层(可选) ↔ TCP 传输层 ↔ IP 网络层 ↔ 链路层
核心一句话:HTTPS/MQTTs 只是在【应用层和 TCP 之间插入 TLS 安全层】,上层应用报文格式完全不变。
┌─────────────────────────────────────────────────────────────┐
│ 应用层 Application │
│ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌────────┐│
│ │ HTTP │ │ HTTPS │ │ MQTT │ │ MQTTs ││
│ │(明文文本)│ │HTTP报文 │ │(二进制)│ │MQTT报文││
│ └──────────┘ └────┬─────┘ └───┬────┘ └───┬────┘│
└───────────────────────────┼──────────────────┼───────────────┘
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ TLS 安全层(可选) │
│ 明文HTTP 无TLS │ HTTPS启用TLS1.2/1.3 │
│ 明文MQTT 无TLS │ MQTTs启用TLS1.2/1.3 │
│ 功能:握手、证书校验、加密、完整性校验、防中间人攻击 │
└───────────────────────────┬──────────────────┬───────────────┘
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ 传输层 Transport TCP │
│ 可靠字节流、连接、重传、流控 │
│端口:HTTP:80 HTTPS:443 MQTT:1883 MQTTs:8883 │
└───────────────────────────┬──────────────────┬───────────────┘
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ 网络层 IP │
│ IP寻址、路由 │
└───────────────────────────┬──────────────────┬───────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 链路层 + 物理层(以太网/WiFi) │
└─────────────────────────────────────────────────────────────┘
简化对比表(配套框图,方便学习)
表格
| 协议 | TLS 层 | 传输层 | 报文类型 | 端口 | 安全特点 |
|---|---|---|---|---|---|
| HTTP | ❌ 无 | TCP | 文本 | 80 | 全部明文,抓包可读 |
| HTTPS | ✅ 有 | TCP | 文本 | 443 | HTTP 报文被 TLS 加密传输 |
| MQTT | ❌ 无 | TCP | 二进制 | 1883 | 二进制明文,抓包可解析 payload |
| MQTTs | ✅ 有 | TCP | 二进制 | 8883 | MQTT 报文被 TLS 加密传输 |
关键知识点(结合你前面 OTA 场景)
- TLS 只加密 TCP 链路上传输的数据
数据到达设备,TLS 解密后,给到上层 HTTP/MQTT 的仍然是明文应用报文。
→ 所以 OTA 下载固件就算用 HTTPS/MQTTs,固件写到 Flash 还是明文,必须额外做固件镜像加密 + 数字签名。 - 上层协议逻辑不变:
HTTPS 的 HTTP 请求内容,和明文 HTTP 一模一样;MQTTs 的 CONNECT/PUBLISH 包和明文 MQTT 完全相同,TLS 只做透明加解密。 - TLS 职责:
- 服务器身份校验(CA 证书,防中间人劫持)
- 链路数据加密
- 报文完整性校验
设备端代码视角分层(对应 C 代码)
应用代码
├─ http_client() / mqtt_client() 【构造HTTP/MQTT报文】
└─ 底层读写接口
├─ 明文:send() / recv() 直接调用socket
└─ TLS加密:mbedtls_ssl_write() / mbedtls_ssl_read()
↓ mbedtls内部实现TLS握手、加解密
↓ 底层仍然调用socket send/recv