本帖最后由 DKENNY 于 2024-10-16 13:20 编辑
#申请原创# @21小跑堂
前言
在现代网络应用开发中,Socket编程是实现计算机之间高效通信的基础。本文将深入探讨基于LWIP2.2的Socket编程,特别是在APM32嵌入式平台上的应用。Socket作为网络通信的核心工具,允许开发者通过IP地址和端口号建立连接,实现数据的发送和接收。我们将介绍Socket的基本概念、编程流程以及在LWIP中如何使用Socket API进行网络通信。
1 什么是Socket?
Socket 就是一个让计算机之间可以通过网络互相交流的工具。可以这么理解,它就是网络中的一个“插座”,通过这个插座,程序就能发送和接收信息。每个 Socket 代表着一个网络连接,通常由一个 IP 地址(类似于计算机的地址)和一个端口号(类似于房间号)组成。这样,信息就能准确地找到目的地。
1.1 Socket 的基本概念
- IP地址:标识网络中某一设备的地址。
- 端口号:标识设备上某一特定服务或应用程序的入口。
- 套接字:在编程中,Socket 是一个抽象的概念,通常用一个整数来表示,程序通过这个整数来进行网络操作。
1.2 Socket 编程
Socket 编程是指使用 Socket 接口进行网络通信的编程方式。它允许开发者创建网络应用程序,比如网页浏览器、聊天程序、文件传输工具等。Socket 编程通常涉及以下几个步骤:
1. 创建 Socket:使用系统提供的 API 创建一个 Socket。
2. 绑定:将 Socket 绑定到一个特定的 IP 地址和端口号,以便接收数据。
3. 监听(对于服务器):在服务器端,Socket 需要监听来自客户端的连接请求。
4. 接受连接(对于服务器):服务器接受客户端的连接请求,建立连接。
5. 发送和接收数据:通过已建立的连接,进行数据的发送和接收。
6. 关闭连接:完成通信后,关闭 Socket 以释放资源。
1.3 应用场景
- Web 服务器:处理来自浏览器的请求。
- 聊天应用:实现实时消息传递。
- 文件传输:在网络中传输文件。
总之,Socket 编程是网络应用开发的基础,能够实现不同设备之间的高效通信。
2 LWIP中的Socket
Socket 类型:
- 流式 Socket(SOCK_STREAM):用于 TCP 连接,提供可靠的、面向连接的通信。
- 数据报 Socket(SOCK_DGRAM):用于 UDP 连接,提供无连接的、不可靠的通信。
在 LwIP 中,Socket API 是建立在 NETCONN API 之上的。系统最多可以提供 MEMP_NUM_NETCONN 个netconn 连接结构,因此可用的 Socket 套接字数量也相应限制为这个数值。为了更好地封装 netconn,LwIP 定义了一个名为 lwip_sock 的套接字结构体(可以称之为 Socket 连接结构)。每个 lwip_sock 内部都包含一个指向 netconn 的指针,从而实现了对 netconn 的进一步封装。
那么,如何找到 lwip_sock 这个结构体呢?LwIP 定义了一个类型为 lwip_sock 的 sockets 数组,通过套接字可以直接索引并访问这个结构体。这也是为什么套接字被表示为一个整数的原因。lwip_sock 结构体相对简单,因为它主要依赖于 netconn 的实现。
#define NUM_SOCKETS MEMP_NUM_NETCONN
/** 包含用于套接字的所有内部指针和状态 */
struct lwip_sock {
/** 套接字当前是基于 netconn 构建的,每个套接字都有一个 netconn */
struct netconn *conn;
/** 上一次读取时留下的数据 */
union lwip_sock_lastdata lastdata;
#if LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL
/** 接收到数据的次数,由 event_callback() 设置,由接收和选择函数测试 */
s16_t rcvevent;
/** 数据被确认的次数(释放发送缓冲区),由 event_callback() 设置,由选择测试 */
u16_t sendevent;
/** 此套接字发生的错误,由 event_callback() 设置,经过选择测试 */
u16_t errevent;
/** 使用选择等待此套接字的线程数量计数器 */
SELWAIT_T select_waiting;
#endif /* LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL */
#if LWIP_NETCONN_FULLDUPLEX
/* 使用 struct lwip_sock 的线程数量计数器(不是 'int') */
u8_t fd_used;
/* 待处理关闭/删除操作的状态 */
u8_t fd_free_pending;
#define LWIP_SOCK_FD_FREE_TCP 1
#define LWIP_SOCK_FD_FREE_FREE 2
#endif
};
Socket 通信流程,可参考以下流程图。
3 Socket API
下面是Socket通信常用的API介绍。
3.1 socket()
int socket(int domain, int type, int protocol);
- 功能:创建一个新的套接字。
- 参数:
- domain:协议簇(如 AF_INET)。
- type:套接字类型(如 SOCK_STREAM、SOCK_DGRAM)。
- protocol:协议(通常为0,表示自动选择)。
- 返回值:成功时返回套接字描述符,失败时返回 -1。
3.2 bind()
int bind(int sockfd, const struct sockaddr *addr, socklen_taddrlen);
- 功能:将套接字绑定到一个地址(IP和端口)。
- 参数:
- sockfd:套接字描述符。
- addr:指向 sockaddr 结构的指针,包含要绑定的地址。
- addrlen:地址结构的长度。
- 返回值:成功时返回 0,失败时返回 -1。
3.3 listen()
int listen(int sockfd, int backlog);
- 功能:将套接字设置为被动监听状态,等待连接请求。
- 参数:
- sockfd:套接字描述符。
- backlog:等待连接的最大数量。
- 返回值:成功时返回 0,失败时返回 -1。
3.4 accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接受一个连接请求,返回新的套接字用于与客户端通信。
- 参数:
- sockfd:监听套接字描述符。
- addr:指向 sockaddr 结构的指针,用于存储客户端地址。
- addrlen:指向地址长度的指针。
- 返回值:成功时返回新的套接字描述符,失败时返回 -1。
3.5 connect()
int connect(int sockfd, const struct sockaddr *addr, socklen_taddrlen);
- 功能:连接到指定的服务器。
- 参数:
- sockfd:套接字描述符。
- addr:指向 sockaddr 结构的指针,包含服务器地址。
- addrlen:地址结构的长度。
- 返回值:成功时返回 0,失败时返回 -1。
3.6 send()
ssize_tsend(int sockfd, const void *buf, size_tlen, int flags);
- 功能:向连接的套接字发送数据。
- 参数:
- sockfd:套接字描述符。
- buf:指向要发送数据的缓冲区。
- len:要发送的数据长度。
- flags:发送选项(通常为0)。
- 返回值:成功时返回发送的字节数,失败时返回 -1。
3.7 recv()
ssize_trecv(int sockfd, void *buf, size_tlen, int flags);
- 功能:从连接的套接字接收数据。
- 参数:
- sockfd:套接字描述符。
- buf:指向接收数据的缓冲区。
- len:缓冲区的大小。
- flags:接收选项(通常为0)。
- 返回值:成功时返回接收到的字节数,失败时返回 -1。
3.8 close()
- 功能:关闭套接字,释放资源。
- 参数:
- sockfd:套接字描述符。
- 返回值:成功时返回 0,失败时返回 -1。
3.9 setsockopt()
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_toptlen);
- 功能:设置套接字选项。
- 参数:
- sockfd:套接字描述符。
- level:选项级别(如 SOL_SOCKET)。
- optname:选项名称(如 SO_REUSEADDR)。
- optval:指向选项值的指针。
- optlen:选项值的长度。
- 返回值:成功时返回 0,失败时返回 -1。
3.10 getsockopt()
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
- 功能:获取套接字选项。
- 参数:
- sockfd:套接字描述符。
- level:选项级别(如 SOL_SOCKET)。
- optname:选项名称。
- optval:指向存储选项值的缓冲区。
- optlen:指向选项值长度的指针。
- 返回值:成功时返回 0,失败时返回 -1。
3.11 总结
LWIP的Socket API提供了创建、绑定、监听、连接、发送和接收数据的基本功能,适用于嵌入式系统的网络编程。通过这些API,我们可以实现网络通信的各种需求。
4 Socket 编程实例
这次依旧实现一个简单的UDP的回显功能。由于我们已经实现了LWIP NETCONN UDP编程实例,所以我们直接沿用这个例程。
4.1 开启LWIP Socket编程
4.2 修改udp_ehco.c。
#include "FreeRTOS.h"
#include "lwip/opt.h"
#include "task.h"
#include "lwip/sockets.h"
#include "lwip/api.h"
#include "lwip/sys.h"
#include <stdbool.h>
#define UDPECHO_THREAD_PRIO ( tskIDLE_PRIORITY + 5 ) // 定义UDP回显线程的优先级
#define PORT 6000 // 定义UDP服务器监听的端口号
#define RECV_DATA (1024) // 定义接收数据的缓冲区大小
/*-----------------------------------------------------------------------------------*/
static void udpecho_thread(void *arg) // UDP回显线程函数
{
int sock = -1; // 套接字描述符
char *recv_data = NULL; // 接收数据的缓冲区
struct sockaddr_in udp_addr, seraddr; // UDP地址结构
int recv_data_len; // 接收数据的长度
socklen_t addrlen = sizeof(seraddr); // 初始化地址长度
bool success = true; // 变量用于跟踪成功状态
// 分配接收数据缓冲区
recv_data = (char *)pvPortMalloc(RECV_DATA);
if (recv_data == NULL) // 检查内存分配是否成功
{
printf("No memory\r\n"); // 打印内存不足信息
success = false; // 设置成功状态为false
}
// 创建UDP套接字
if (success)
{
sock = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP套接字
if (sock < 0) // 检查套接字创建是否成功
{
printf("Socket error\r\n"); // 打印套接字错误信息
success = false; // 设置成功状态为false
}
}
// 绑定套接字
if (success)
{
udp_addr.sin_family = AF_INET; // 设置地址族为IPv4
udp_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用接口
udp_addr.sin_port = htons(PORT); // 设置端口号
memset(&(udp_addr.sin_zero), 0, sizeof(udp_addr.sin_zero)); // 清零结构体的剩余部分
if (bind(sock, (struct sockaddr *)&udp_addr, sizeof(struct sockaddr)) == -1) // 绑定套接字
{
printf("Unable to bind\r\n"); // 打印绑定失败信息
success = false; // 设置成功状态为false
}
}
// 主循环
while (success)
{
recv_data_len = recvfrom(sock, recv_data, RECV_DATA, 0, (struct sockaddr*)&seraddr, &addrlen); // 接收数据
if (recv_data_len < 0) // 检查接收是否成功
{
printf("Receive error\r\n"); // 打印接收错误信息
break; // 退出循环
}
/* 显示发送者的IP地址 */
printf("receive from %s\r\n", inet_ntoa(seraddr.sin_addr)); // 打印发送者IP地址
/* 显示发送者发送的字符串 */
printf("receive data: %s\r\n\r\n", recv_data); // 打印接收到的数据
/* 将字符串返回给发送者 */
sendto(sock, recv_data, recv_data_len, 0, (struct sockaddr*)&seraddr, addrlen); // 发送回显数据
}
// 清理资源
if (sock >= 0) // 检查套接字是否有效
closesocket(sock); // 关闭套接字
if (recv_data) // 检查接收数据缓冲区是否有效
free(recv_data); // 释放接收数据缓冲区
}
/*-----------------------------------------------------------------------------------*/
void udpecho_init(void) // 初始化UDP回显功能
{
sys_thread_new("udpecho_thread", udpecho_thread, NULL, DEFAULT_THREAD_STACKSIZE, UDPECHO_THREAD_PRIO ); // 创建UDP回显线程
}
以下是对代码功能的详细分析:
主要功能
- 1. 创建UDP套接字:创建一个UDP套接字,用于接收和发送数据。
- 2. 绑定套接字:将套接字绑定到指定的端口(6000),以便接收来自该端口的数据。
- 3. 接收数据:在主循环中,服务器不断接收来自客户端的数据。
- 4. 回显数据:接收到数据后,服务器将其打印到控制台,并将相同的数据返回给发送者(回显功能)。
- 5. 资源管理:在退出时,清理分配的资源,包括关闭套接字和释放内存。
4.3 实验现象
打开网络调试助手和串口调试助手,使用网口线连接,现象如下。
5.总结
以上只实现了UDP的一个简单实例,TCP的实现也不是太过于复杂,有兴趣的小伙伴可以尝试尝试。以下是我针对Socket编程的TCP和UDP配置流程做了一个简单的梳理表格。
步骤
| TCP编程流程描述
| UDP编程流程描述
| 1. 初始化LWIP
| 初始化LWIP协议栈,设置内存池和网络接口,确保LWIP能够正常工作。
| 同样初始化LWIP协议栈,设置内存池和网络接口,准备进行UDP通信。
| 2. 创建Socket
| 创建一个TCP Socket,指定协议族(如IPv4)和Socket类型(SOCK_STREAM)
| 创建一个UDP Socket,指定协议族(如IPv4)和Socket类型(SOCK_DGRAM)
| 3. 绑定Socket
| 将Socket绑定到特定的IP地址和端口号,以便接收来自该地址和端口的连接请求。
| 将Socket绑定到特定的IP地址和端口号,以便接收来自该地址和端口的数据报。
| 4. 监听连接
| 设置Socket为监听状态,准备接受客户端的连接请求。
| 不需要监听,因为UDP是无连接的协议,直接发送和接收数据报。
| 5. 接受连接
| 使用接受函数来接受客户端的连接请求,创建一个新的Socket用于与客户端通信。
| 不需要接受连接,直接使用绑定的Socket进行数据的发送和接收。
| 6. 发送和接收数据
| 通过已建立的连接进行数据的发送和接收,双方可以进行多次数据交互。
| 直接使用Socket发送和接收数据报,数据报是独立的,不需要建立连接。
| 7. 关闭Socket
| 在完成通信后,关闭Socket,释放资源,并进行四次挥手以正常终止连接。
| 在完成数据传输后,关闭Socket,释放资源。UDP不需要进行连接终止的过程。
| 8. 清理资源
| 清理LWIP使用的资源,包括释放内存和关闭网络接口,确保系统稳定。
| 同样清理LWIP使用的资源,确保没有内存泄漏和其他潜在问题。
|
附件是相关Demo, LWIP Socket UDP Demo。
UDP_SOCKET.zip
(6.71 MB)
|
深入探讨基于LWIP2.2的Socket编程,介绍Socket的概念和使用场景,并在APM32嵌入式平台上的应用方式。