打印
[APM32F4]

APM32: 构建UDP RAW编程实例

[复制链接]
95|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
DKENNY|  楼主 | 2024-9-18 17:50 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 DKENNY 于 2024-9-19 13:21 编辑

#申请原创# @21小跑堂
前言
    在现代网络编程中,RAW API提供了灵活的方式来处理数据传输。本文旨在基于已有的TCP RAW例程,修改并实现一个UDP RAW例程。通过对RAW编程和UDP协议的深入探讨,我们将逐步展示如何在软件中实现这一功能。接下来的内容将涵盖RAW编程的基本概念、UDP的特点以及具体的软件实现步骤。

1. RAW编程
    在LWIP中,RAW编程指的是直接使用LWIP提供的低级API来实现网络协议的功能,而不是使用更高级别的协议栈接口(如TCP、UDP等)。RAW编程允许我们直接控制数据包的构建和发送,提供了最大的灵活性和控制能力,适合需要自定义协议或特定功能的应用。

1.1 RAW编程的特点
    - 1. 灵活性:我们可以根据具体需求构建和解析数据包,适合实现非标准协议或自定义数据格式。
    - 2. 低级接口:使用LWIP的原始API,我们可以手动管理数据包的发送和接收,包括数据的封装和解析。
    - 3. 性能:RAW编程可以减少协议栈的开销,适合对性能有严格要求的应用。
    - 4. 适用场景:通常用于实现简单的网络协议、传感器数据传输、控制命令等。

1.2 RAW编程示例
    以下是一个简单的LWIP RAW编程示例,演示如何创建一个RAW网络协议并接收数据包:
#include "lwip/opt.h"
#include "lwip/raw.h"
#include "lwip/ip.h"
#include "lwip/udp.h"
#include "lwip/tcpip.h"
#include "lwip/init.h"

struct raw_pcb *raw;

// 数据包接收回调函数
err_t raw_recv(void *arg, struct raw_pcb *pcb, struct pbuf *p, const ip_addr_t *addr) {
    // 处理接收到的数据包
    // 例如,打印数据
    printf("Received packet: %s\n", (char *)p->payload);
    pbuf_free(p);  // 释放pbuf
    return ERR_OK;
}

void init_raw() {
    // 初始化LWIP
    lwip_init();

    // 创建RAW PCB
    raw = raw_new(IP_PROTO_ICMP);  // 例如,使用ICMP协议
    if (raw != NULL) {
        raw_bind(raw, IP_ADDR_ANY);  // 绑定到任意IP地址
        raw_recv(raw, raw_recv, NULL);  // 注册接收回调
    }
}

int main() {
    // 初始化网络
    init_raw();

    // 主循环
    while (1) {
        // 处理LWIP任务
        sys_check_timeouts();
    }
}

1.3 总结
    RAW编程在LWIP中提供了对网络协议的底层访问,适合需要高度自定义的网络应用。通过直接操作数据包,我们能够实现特定的功能和协议。

2. RAW 编程接口 UDP 简介

2.1 UDP 协议
    UDP协议是TCP/IP协议栈中的一种传输层协议,它是一种简单的面向数据报的协议。在传输层,还有另一个重要的协议就是TCP。UDP不负责将数据包分组或组装,也不能对数据包进行排序,发送后无法确认数据是否安全完整地到达。尽管有这些缺点,UDP也有自己的优点,因为它不是连接型协议,所以占用的资源少,处理速度快。因此,UDP通常用于音频、视频和普通数据的传输。
    UDP 数据报结构如图所示。


    端口号用于标识发送和接收的进程,UDP协议通过端口号为不同的应用分配各自的数据传输通道。UDP和TCP协议都利用端口号来支持多个应用在同一时间内同时发送和接收数据,接收方则通过目标端口来获取信息。有些网络应用只能使用事先分配或注册的静态端口,而其他一些应用则可以使用未注册的动态端口。由于UDP报头用两个字节来存储端口号,因此端口号的有效范围是0到65535。通常,49151以上的端口号被视为动态端口。

    数据报的长度是指报头和数据部分加起来的总字节数。由于报头的长度是固定的,这个字段主要用来计算可变长度的数据部分,也就是数据负载。数据报的最大长度会因操作环境的不同而有所变化。理论上,包含报头的数据报最大可以达到65535字节。

    UDP协议通过报头中的校验和来确保数据的安全性。发送方会使用特定的算法计算出校验和,接收方在收到数据后也会进行重新计算。如果在传输过程中,数据报被第三方篡改或因线路噪音等原因损坏,发送方和接收方计算出的校验和就会不一致,这样UDP协议就能检测到错误。

2.2 LWIP 中 UDP 协议函数
    在LWIP源码中,udp.c和udp.h这两个文件与UDP协议相关,里面包含了许多函数。如果想深入了解UDP,可以查看这两个文件。udp.c 中与 UDP 报文处理有关的函数之间的关系如下图所示。


2.3 LWIP 中 RAW API 编程接口中与 UDP 相关的函数
    LWIP的RAW API编程方式采用回调机制。在初始化应用时,我们需要为内核中的不同事件注册相应的回调函数。当这些事件发生时,注册的回调函数就会被自动调用。下表给出了 UDP 的 RAW API 功能函数,我们使用这些函数来完成 UDP 的数据发送和接收。

RAW API函数
描述
udp_new
新建一个 UDP 的 PCB 块
udp_remove
将一个 PCB 控制块从链表中删除,并且释放这个控制块的内存
udp_bind
为 UDP 的 PCB 控制块绑定一个本地 IP 地址和端口号
udp_connect
连接到指定 IP 地址主机的指定端口上,其实就是设置 PCB 控制块
的 remote_ip 和 remote_port
udp_disconnect
断开连接,将控制块设置为非连接状态,其实就是清除 remote_ip
和 remote_port 字段
udp_send
通过一个 PCB 控制块发送数据
udp_recv
需要创建的一个回调函数,当接收到数据的时候被调用

    上面的表格只列出了编程时需要用到的一些函数,实际上与UDP相关的函数还有很多,这里就不一一列出。如果想了解更多UDP的其他函数,可以查看udp.c文件。

3. 软件设计
    我是在原来TCP例程的基础上修改的,这样就不用重新新建或者添加lwip协议栈到工程里了。

    3.1 确保LWIP开启了UDP功能,如果没有开启,需要手动在lwipopts添加如下代码。


    3.2 新建udp_c.h和udp_c.c文件。


    3.3 实现udp_connect函数
/**
* [url=home.php?mod=space&uid=247401]@brief[/url] Establishes a UDP client connection and sends a message.
*
* This function creates a new UDP protocol control block (PCB), assigns a destination IP address,
* and connects to the specified port. It sets a receive callback for incoming UDP packets and
* sends a message to the connected destination. If any errors occur during the process,
* the function cleans up by removing the UDP connection and freeing resources.
*
* @retval None
*/

void udp_client_connect(void)
{
    struct udp_pcb *upcb;
    struct pbuf *p;
    struct ip4_addr DestIPaddr;
    err_t err;

    /* Create a new UDP control block  */
    upcb = udp_new();
  
    if (upcb!=NULL)
    {
        /*assign destination IP address */
        IP4_ADDR( &DestIPaddr, COMP_IP_ADDR0, COMP_IP_ADDR1, COMP_IP_ADDR2, COMP_IP_ADDR3 );
  
        /* configure destination IP address and port */
        err= udp_connect(upcb, &DestIPaddr, COMP_PORT);
   
        if (err == ERR_OK)
        {
            /* Set a receive callback for the upcb */
            udp_recv(upcb, (udp_recv_fn)udp_receive_callback, NULL);

            sprintf((char*)data, "sending udp client message %d\r\nsending anything to disconnect!", message_count);
      
            /* allocate pbuf from pool*/
            p = pbuf_alloc(PBUF_TRANSPORT,strlen((char*)data), PBUF_POOL);
      
            if (p != NULL)
            {
                /* copy data to pbuf */
                pbuf_take(p, (char*)data, strlen((char*)data));
                  
                /* send udp data */
                udp_send(upcb, p);
               
                /* free pbuf */
                pbuf_free(p);
            }
            else
            {
                /* free the UDP connection, so we can accept new clients */
                udp_remove(upcb);
                printf("\n\r can not allocate pbuf ");
            }
        }
        else
        {
            /* free the UDP connection, so we can accept new clients */
            udp_remove(upcb);
            printf("\n\r can not connect udp pcb");
        }
    }
    else
    {
        printf("\n\r can not create udp pcb");
    }
}
   这个函数实现了一个UDP客户端连接的功能,具体步骤和功能分析如下:
    1. 创建UDP控制块
       - 使用 udp_new() 函数创建一个新的UDP协议控制块(PCB),用于管理UDP连接。
    2. 检查PCB创建成功
       - 如果 upcb 不为 NULL ,表示成功创建了UDP控制块,接下来进行连接配置。
    3. 设置目标IP地址
       - 使用 IP4_ADDR 宏设置目标IP地址,地址由四个部分( COMP_IP_ADDR0COMP_IP_ADDR3 )组成。
    4. 连接到目标地址和端口
       - 调用 udp_connect() 函数,将UDP PCB连接到指定的目标IP地址和端口。如果连接成功(返回 ERR_OK ),则继续执行。
    5. 设置接收回调函数
       - 使用 udp_recv() 函数为UDP PCB设置接收回调函数 udp_receive_callback ,以处理接收到的UDP数据包。
    6. 准备发送数据
       - 使用 sprintf() 格式化要发送的消息,内容为 sending udp client message 加上当前的 message_count
    7. 分配pbuf
       - 调用 pbuf_alloc() 从内存池中分配一个pbuf,用于存储要发送的数据。如果分配成功,继续执行。
    8. 复制数据到pbuf
       - 使用 pbuf_take() 将格式化后的数据复制到分配的pbuf中。
    9. 发送UDP数据
       - 调用 udp_send() 函数发送数据包。
    10. 释放pbuf
        - 发送完成后,使用 pbuf_free() 释放pbuf所占用的内存。
    11. 错误处理
        - 如果在任何步骤中出现错误(如无法分配pbuf或连接失败),都会调用 udp_remove() 函数释放UDP控制块,以便接受新的客户端连接,并打印相应的错误信息。

    总结:这个函数实现了一个UDP客户端的基本功能,包括创建连接、发送消息和处理接收数据的回调。它还包含了错误处理机制,以确保在出现问题时能够正确释放资源。

    3.4 实现receive callback函数
/**
* [url=home.php?mod=space&uid=247401]@brief[/url] UDP receive callback function
*
* This function is called when a UDP packet is received. It increments the message count,
* frees the received pbuf, and removes the UDP connection to allow new clients to connect.
*
* @param arg User-defined argument passed to the callback function.
* @param upcb Pointer to the current UDP connection's protocol control block (PCB).
* @param p Pointer to the received data packet's pbuf.
* @param addr Pointer to the sender's IP address.
* @param port Sender's port number.
*
* @retval Returns 0 to indicate successful processing.
*/

udp_recv_fn udp_receive_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, struct ip4_addr *addr, u16_t port)
{

    /*increment message count */
    message_count++;
  
    /* Free receive pbuf */
    pbuf_free(p);
  
    /* free the UDP connection, so we can accept new clients */
    udp_remove(upcb);
   
    flag = 0;
    printf("\n\rDisconnect TCP server \r\n");

    return 0;
}
   这个函数实现了一个UDP接收回调函数的功能,具体分析如下:
    1. 函数定义
       - 函数名为 udp_receive_callback ,类型为 udp_recv_fn ,表示这是一个UDP接收回调函数。
    2. 参数说明
       -  void *arg :用户自定义的参数,可以在设置回调时传递给函数。
       -  struct udp_pcb *upcb :指向当前UDP连接的协议控制块(PCB),用于管理该连接。
       -  struct pbuf *p :指向接收到的数据包的pbuf,包含了UDP数据。
       -  struct ip4_addr *addr :指向发送者的IP地址。
       -  u16_t port :发送者的端口号。
    3. 功能实现
       - 消息计数递增:调用 message_count++,每当接收到UDP数据包时,消息计数器增加1。这可以用于跟踪接收到的消息数量。
       - 释放接收的pbuf:调用 pbuf_free(p) ,释放接收到的数据包所占用的内存,防止内存泄漏。
       - 移除UDP连接:调用 udp_remove(upcb) ,释放当前的UDP控制块(PCB),以便允许新的客户端连接。这意味着在处理完接收到的数据后,当前的UDP连接不再有效。
    4. 返回值
       - 函数返回0,表示成功处理了接收到的UDP数据包。

    总结:这个函数的主要功能是处理接收到的UDP数据包。它在接收到数据时增加消息计数,释放接收到的数据包的内存,并移除UDP连接以便接受新的客户端连接。该回调函数确保了资源的有效管理和UDP连接的动态处理。

    3.5  在udp_c.h中声明udp_client_connect函数。
#ifndef __UDP_C_H__
#define __UDP_C_H__
void udp_client_connect(void);
#endif /* __UDP_ECHOCLIENT_H__ */

    3.6 修改main函数


    3.7 设置IP地址,我这里关闭了DHCP动态分配地址(开启也可),使用静态ip地址。




    3.8 如果使用静态ip,连接以太网线后,需修改本机ip地址。


    3.9 现象
    例程的现象及实现的功能可参考如下图。


    当然,上面的例程只是参考例程,并没有对数据做处理,我们实际应用时,可以直接修改receive callback,进一步对数据进行分析,提取我们想要的数据,过滤掉那些无用的数据。

    通过本文的探讨,我们成功地在已有的TCP RAW例程基础上,修改并实现了一个UDP RAW例程。我们首先介绍了RAW编程的基本概念,分析了UDP协议的特点,最后详细展示了软件实现的过程。具体的例程在附件,有需要的可自行下载。


附件:UDP_RAW例程
UDP_Template.zip (9.59 MB)



使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

29

主题

52

帖子

6

粉丝