ESP32学习笔记_WiFi(2)——TCP/UDP

摘要

本文阐述了 TCP 作为一种面向连接、可靠且具备滑动窗口流量控制与复杂拥塞控制机制的传输层协议,在 ESP32 平台上的实现与应用逻辑;而 UDP 在传输时不建立连接,具有高速、可广播等特性

参考资料
lwIP - ESP-IDF 编程指南
lwip/socket.h - esp-idf - Github
Michael_ee视频教程 - Bilibili

文章目录

    • [TCP & UDP](#TCP & UDP)
      • TCP
        • [TCP Client Example](#TCP Client Example)
        • [TCP Server Example](#TCP Server Example)
      • UDP
      • [Difference between TCP and UDP](#Difference between TCP and UDP)


TCP & UDP

TCP

TCP 是一种面向连接的、可靠的、基于字节流的传输层通信协议,在数据传输过程中,TCP 将应用层数据分割成报文段,并利用序列号(Sequence Number)和确认应答(ACK)机制确保数据的有序到达与完整性;若发生丢包或超时,协议栈会自动触发重传机制;TCP 集成了滑动窗口(Sliding Window)机制以实现流量控制,防止发送方溢出接收方的缓冲区,并结合慢启动、拥塞避免、快重传和快恢复等拥塞控制算法,根据网络带宽状况动态调节发送速率,从而在不可靠的互联网络中提供高可靠性的端到端数据传输服务

ESP32 使用的 TCP 协议基于 LwIP 协议栈,支持标准的 BSD Sockets API(一个跨平台的 Socket API)

Socket(套接字) 是计算机网络通信的核心接口与端点,它由IP地址(用于定位计算机)和端口号(用于定位具体的应用程序)共同组成,充当了数据传输的门户;当两个程序需要跨网络交流时,它们必须各自创建一个 Socket 并建立连接,依靠它作为标准化的 API 来完成所有数据的发送与接收
BSD Socket API 是一种常见的跨平台 TCP/IP 套接字 API,最初源于 UNIX 操作系统的伯克利标准发行版,现已标准化为 POSIX 规范的一部分;BSD 套接字有时也称 POSIX 套接字,或伯克利套接字
在 ESP-IDF 中,lwIP 支持 BSD 套接字 API 的所有常见用法,然而,并非所有操作都完全线程安全,因此多个线程同时进行读写可能需要额外的同步机制

Workflow

标准 Socket 程序:Initialize->Connect->Communicate->Disconnect
Client
Server
socket()
bind()
listen()
accept()
recv()
send()
close()
socket()
connect()
send()
recv()
close()

使用实例:protocol -> tcp_client、protocol -> tcp_server

这里要注意 esp32s3 只支持 2.4G 频段
Wi-Fi disconnected 201 表示未找到 AP,需要检查热点是否在 2.4G 频段

  • socket() 创建一个 socket 用于侦听/连接
  • bind() 绑定 socket 到一个端口
  • listen() 侦听是否有客户端发起连接
  • accept() 接受当前连接
  • send() 数据发送
  • recv() 数据接收
  • close() 关闭连接
  • connect() 连接到服务端

在 Server 示例中,一个 Server 只能同时与一个 Client 连接与数据传输,实际上,ESP-IDF/lwIP 支持多个 TCP 连接并发,若需要连接多个 Client,可以在一个 task 中使用非阻塞 socket + select()/poll() 多路复用多个已 accept 的 sock,或在每次 accept() 后为该客户端创建一个专用 task 做 recv/send,而主 task 只负责 listen/accept
在 TCP Server 和 Client 示例中,会使用 example_connect() 函数来快速建立一个 WiFi 连接,这个函数并不适用于真实项目中,仅用于 demo

TCP Client Example

headers

c 复制代码
#include "sdkconfig.h"
#include <string.h>
#include <unistd.h> // POSIX universal headers
#include <sys/socket.h> // POSIX socket headers(POSIX socket is based on BSD socket)
#include <errno.h> // Error codes definitions
#include <netdb.h> // struct addrinfo, network database operations
#include <arpa/inet.h> // IP address conversion functions
#include "esp_netif.h" // ESP32 network interface headers
#include "esp_log.h"
#if defined(CONFIG_EXAMPLE_SOCKET_IP_INPUT_STDIN)
#include "addr_from_stdin.h" // Mainly used by socket client examples which read IP address from stdin (if configured)
#endif

POSIX (可移植操作系统接口)是一层软件兼容接口层,在ESP32中通常封装在FreeRTOS之上;它允许开发者使用通用的、标准化的C语言函数(如用于文件操作的 open/read/write,用于多线程的 pthread)来替代特定硬件或RTOS专有的指令;这层接口的核心价值在于可移植性,意味着为Linux或UNIX编写的代码只需很少的修改甚至无需修改,就可以直接在ESP32上运行,极大地降低了跨平台开发的门槛
BSD SocketPOSIX 的关系可以概括为"起源"与"标准"的关系:BSD Socket是网络编程接口的鼻祖和事实标准,而 POSIX 则是后来将这套接口正式纳入规范的官方标准;在ESP32上编写网络代码时,使用的是符合POSIX标准的接口,但其核心语法和逻辑完全源自BSD Socket

sockaddr_in definition

定义目标服务器 IP 地址、端口、使用的协议

c 复制代码
struct sockaddr_in dest_addr; // Target server address structure (IPv4)
        inet_pton(AF_INET, host_ip, &dest_addr.sin_addr); // Convert the dotted decimal string `host_ip` to a binary IPv4 address.
        dest_addr.sin_family = AF_INET; // Address family = IPv4
        dest_addr.sin_port = htons(PORT); // Convert port number from host byte order to network byte order
        addr_family = AF_INET;
        ip_protocol = IPPROTO_IP; // 0, it means choose the protocol automatically based on the given socket type
        // "sin_" means "socket internet"

Socket create and transmit

c 复制代码
int sock = socket(addr_family, SOCK_STREAM, ip_protocol); // Create a socket, SOCK_STREAM means TCP
int err = connect(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr)); // Connect the server
err = send(sock, payload, strlen(payload), 0); // Send msg
int len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0); // Receive msg
shutdown(sock, 0); // Shutdown socket
// 0 - No more receptions
// 1 - No more transmissions
// 2 - No more receptions or transmissions
close(sock); // Close socket
TCP Server Example

variable declarations

c 复制代码
    char addr_str[128];
    int addr_family = (int)pvParameters; // Use the passed parameter to decide IPv4 or IPv6, here is IPv4
    int ip_protocol = 0;
    int keepAlive = 1; // Enable keepalive option
    int keepIdle = KEEPALIVE_IDLE; // Start keepalive after this period idle(seconds)
    int keepInterval = KEEPALIVE_INTERVAL; // Interval between keepalive probes(seconds)
    int keepCount = KEEPALIVE_COUNT; // When to drop connection after how many failed probes
    struct sockaddr_storage dest_addr; // Definition of the server address structure

#ifdef CONFIG_EXAMPLE_IPV4
    if (addr_family == AF_INET)
    {
        struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
        dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY); // Set local IP address to any(0.0.0.0 means all local interfaces), it means listen on all local interfaces
        dest_addr_ip4->sin_family = AF_INET;
        dest_addr_ip4->sin_port = htons(PORT);
        ip_protocol = IPPROTO_IP;
    }
#endif

Socket create and transmit

c 复制代码
int listen_sock = socket(addr_family, SOCK_STREAM, ip_protocol); // Create a socket
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // Enable address reuse, so we can bind a local ip and port when a previous connection is in TIME_WAIT state
// So that we can restart server quickly after shutdown() or close()

int err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr)); // Bind scoket to the port

err = listen(listen_sock, 1); // Listen for incoming connections
// When this API is called, the tcp stack will set this socket as server socket

/* --- */

int sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len); // Accept a connection and store the client address info
// This is a blocking call, so if no connections are incoming, will wait here

setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(int)); // Enable keepalive
setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(int)); // Start keepalive after this period idle
setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(int)); // Interval between keepalive probes
setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(int)); // When to drop connection after how many failed probes

inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr, addr_str, sizeof(addr_str) - 1); // Convert the client ip address from binary to dotted decimal string

do_retransmit(sock); // Process data from client

shutdown(sock, 0); // Shutdown socket
close(sock); // Close socket

/* --- */

close(listen_sock); // Close the listening socket
  • listen_socksocket() 系统调用创建,并通过 bind() 绑定至本地特定的 IP 地址与端口,确立服务端的寻址入口;随后 listen() 函数将其从主动状态转换为被动监听模式,使其内核控制块开始维护未完成连接队列(SYN 队列)和已完成连接队列(Accept 队列);该 scoket的生命周期贯穿服务端的主进程,其核心职能仅限于连接管理(Connection Management),即处理 TCP 三次握手请求,并通过 accept() 派发新的文件描述符,本身不参与应用层数据的传输
  • sock 是由 accept() 系统调用在三次握手成功后返回的全新描述符,代表一个完全建立的 TCP 连接会话(Session),它是内核中五元组(源 IP、源端口、目的 IP、目的端口、协议)的具体句柄,专门用于当前特定客户端与服务端之间的全双工数据传输,所有的 recv()send() 操作均作用于该套接字;其生命周期是瞬态的,仅存在于会话期间,一旦会话结束或发生链路错误,程序需调用 close() 将其销毁以释放系统资源,且不同客户端的 sock 相互隔离,互不干涉

UDP

UDP 是一种无连接的、面向报文的 传输层协议,它不维护连接状态,发送端在数据发出前无需与接收端建立握手,直接将应用层数据封装成数据报发送

UDP 提供"尽力而为"(Best-Effort)的交付服务,不保证数据包的到达、顺序或无重复,且不具备内置的重传、流量控制或拥塞控制机制,由于其头部开销极小(仅 8 字节)且无连接建立时延,UDP 具有极高的传输效率和确定性的时序特征,适用于对实时性要求极高但对少量丢包不敏感的应用场景

多人在线游戏通常优先采用 UDP 协议,利用其无连接、低头部开销和无拥塞控制的特性来最小化端到端延迟;然而由于 UDP 遵循"尽力而为"(Best-Effort)交付原则且缺乏 TCP 的自动重传请求和序列纠正机制,物理链路中的拥塞或信号衰减导致的丢包会被直接暴露给应用层,导致状态同步数据缺失

这就是为什么在打 CS 时,丢包率一高就会出现跑步漂移和自己能动,但是对面和队友站住不动的现象

Workflow
Client
Server
socket()
bind()
recvfrom()
sendto()
close()
socket()
sendto()
recvfrom()
close()

sendto()recvfrom() 在使用时需要传入目标 ip 和端口

Difference between TCP and UDP

  • TCP 是基于连接的数据传输协议,UDP 没有建立连接(TCP 在数据交换之前建立了连接,send 和 recv 无需包含 ip 地址和端口,而 UDP 没有建立连接,在数据交换时,recvfrom 和 sendto 需要包含 ip 地址和端口)
  • TCP 的数据传输是可靠的,而 UDP 的发送方不能保证接收方一定能够接收到数据,是不可靠的
  • 由于 TCP 存在建立连接和数据校验的过程,速度较慢,而 UDP 速度较快
  • 由于 TCP 需要建立连接,只能用于点对点传输,而 UDP 可以进行广播和组播
  • TCP 常用于 email、ftp、http/https 等,UDP 常用于实时音视频传输等
相关推荐
煎蛋学姐2 小时前
SSM学习互助平台网站8f554(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
学习·ssm 框架·javaweb 开发·书籍分类
AI_零食2 小时前
鸿蒙跨端框架 Flutter 学习 Day 4:异步编程基础——Future 与非阻塞执行的物理真相
学习·flutter·harmonyos
QiZhang | UESTC2 小时前
学习日记day64
学习
爱吃生蚝的于勒2 小时前
【Linux】零基础学习命名管道-共享内存
android·linux·运维·服务器·c语言·c++·学习
阿拉伯柠檬3 小时前
网络层协议IP(二)
linux·网络·网络协议·tcp/ip·面试
简叙生活3 小时前
【CES直击:从“屏幕依赖”到“真实对话”,Lookee如何用声网技术重构英语学习?
学习·ces
又是进步的一天3 小时前
Kubernetes 证书体系与 OpenSSL 命令学习
学习·容器·kubernetes
栗少3 小时前
Three.js快速入门
学习
想进部的张同学3 小时前
RK3588 Docker 中部署 GStreamer + MPP 并固化镜像(完整踩坑实录)
学习