发新帖本帖赏金 50.00元(功能说明)我要提问
返回列表
[APM32F4]

APM32: LWIP2.2 Socket编程应用

[复制链接]
2511|1
手机看帖
扫描二维码
随时随地手机跟帖
DKENNY|  楼主 | 2024-10-16 10:05 | 显示全部楼层 |阅读模式
本帖最后由 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 通信流程,可参考以下流程图。
Socket通信.png



3 Socket API
    下面是Socket通信常用的API介绍。
3.1 socket()
int socket(int domain, int type, int protocol);
   - 功能:创建一个新的套接字。
    - 参数
      - domain:协议簇(如 AF_INET)。
      - type:套接字类型(如 SOCK_STREAMSOCK_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()
int close(int sockfd);
   - 功能:关闭套接字,释放资源。
    - 参数
      - 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编程
92148892ee6f82049a8a074f061c28e6

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 实验现象
    打开网络调试助手和串口调试助手,使用网口线连接,现象如下。
290383bd0802c4e9cedac33e4d9afbf7


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)

使用特权

评论回复

打赏榜单

21小跑堂 打赏了 50.00 元 2024-10-25
理由:恭喜通过原创审核!期待您更多的原创作品~

评论
21小跑堂 2024-10-25 17:38 回复TA
深入探讨基于LWIP2.2的Socket编程,介绍Socket的概念和使用场景,并在APM32嵌入式平台上的应用方式。 
发新帖 本帖赏金 50.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

33

主题

57

帖子

6

粉丝