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

mbedos TCP 客户端程序设计

[复制链接]
2071|2
手机看帖
扫描二维码
随时随地手机跟帖
DKENNY|  楼主 | 2024-4-29 17:14 | 显示全部楼层 |阅读模式
本帖最后由 DKENNY 于 2024-4-29 18:53 编辑

#申请原创# @21小跑堂

    最近了解了ETH的TCP相关知识,准备在mbedos上开发一个TCP Client Demo,这篇文章从TCP的定义以及后面具体的代码实现,记录了我在开发时遇到的一些问题以及经验。

##1、TCP应用基础知识介绍
    TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它在网络通信中扮演着至关重要的角色,常用于实现可靠的数据传输。

    1. **面向连接**:TCP是一种面向连接的协议,通信双方在传输数据之前必须先建立连接。连接的建立是通过三次握手来实现的,确保发送方和接收方都能够互相通信。

    2. **可靠性**:TCP提供可靠的数据传输服务,通过序列号、确认应答和重传机制等手段来确保数据的可靠性。如果数据包丢失、损坏或者乱序,TCP会进行重传,直到接收方正确接收到数据。

    3. **流量控制**:TCP使用滑动窗口机制进行流量控制,以防止发送方发送过多的数据导致接收方无法及时处理。接收方通过发送窗口大小来告知发送方可以接收的数据量,发送方根据窗口大小调整发送速率。

    4. **拥塞控制**:TCP具有拥塞控制机制,用于避免网络拥塞的发生。通过动态调整发送速率、检测丢包并进行重传、以及快速恢复等技术,TCP可以在网络拥塞时减少数据丢失和延迟,保持网络的稳定性。

    5. **面向字节流**
:TCP是一种面向字节流的协议,数据在连接上以字节流的形式进行传输。TCP并不关心应用层的消息边界,而是将数据划分为字节流进行传输,因此应用层需要自行处理消息的边界。

    6. **全双工通信**
:TCP连接是全双工的,即双方都可以同时发送和接收数据,实现了双向的通信。

    总之,TCP是一种可靠的、面向连接的通信协议,适用于构建各种类型的网络应用程序,如Web浏览器、电子邮件、文件传输等。

##2、mbedos TCP介绍

    mbedOS 是一款针对物联网设备的嵌入式操作系统,它提供了许多功能丰富的组件和库,其中包括 TCP/IP 协议栈,用于构建连接到网络的设备。在 mbedOS 中,TCP/IP 协议栈提供了 TCP 和 IP 层的实现,使得设备可以通过 TCP 协议进行可靠的数据通信。

    在 mbedOS 中使用 TCP 协议进行网络通信时,可以利用其提供的套接字 API,通过简单的函数调用来实现建立连接、发送和接收数据等操作。以下是在 mbedOS 中使用 TCP 协议的基本步骤:

    1. **创建套接字**:使用 `socket` 函数创建一个 TCP 套接字,该函数返回一个套接字描述符,用于后续的操作。

    2. **绑定套接字**:如果需要,可以使用 `bind` 函数将套接字绑定到特定的 IP 地址和端口上。

    3. **建立连接**:对于作为客户端的设备,使用 `connect` 函数连接到远程服务器;对于作为服务器的设备,使用 `listen` 函数监听连接请求,并使用 `accept` 函数接受连接。

    4. **发送和接收数据**:使用 `send` 和 `recv` 函数来发送和接收数据。发送数据时,将数据缓冲区和数据长度传递给 `send` 函数;接收数据时,指定接收缓冲区和最大接收长度给 `recv` 函数。

    5. **关闭连接**:通信完成后,使用 `close` 函数关闭套接字,释放资源。

    在使用 TCP 协议进行网络通信时,还需要注意处理各种可能出现的错误情况,如连接超时、数据发送失败等,以确保通信的稳定性和可靠性。同时,也可以利用 mbedOS 提供的其他功能,如事件驱动的网络堆栈、定时器等,来优化和增强 TCP 通信的性能和可靠性。

    开发使用相关API说明:https://os.mbed.com/docs/mbed-os/v6.16/apis/socket.html

##3、mbedos TCP客户端程序设计
    TCP 客户端程序的特点是随时可以根据应用的需要发起或结束连接,服务端实现无需知道客户端的存在,而一旦与服务端建立连接后,双方可以任意收发数据,所以非常适合那些需要远程监控的场合。

1、准备
    ① mbed studio开发环境
    ② 一根usb数据线
    ③ 一根网口线
    ④ APM32F407IG-Tiny板
    ⑤ 网络调试助手

2、mbed studio的开发环境的搭建,这里就不介绍了,可以参考论坛其他大佬关于这部分的内容,我们直接上源码。
    我在写这部分代码的时候,经过了多次修改,也是发现了一些问题,并总结出了一些经验。

第一次代码编写:
#include "mbed.h"
#include "EthernetInterface.h"

//声明以太网接口
EthernetInterface eth;
//声明开发板ip地址
SocketAddress ip_address;
//声明开发板子网掩码
SocketAddress netmask_address;
//声明开发板网关
SocketAddress gateway_address;

//声明TCP连接
TCPSocket client;

//声明TCP服务器ip地址及端口
SocketAddress server_address;

DigitalOut led2(LED2);
InterruptIn button(KEY1);

void button_handler()
{
    char buf[100] = "Button was pressed!";
    client.send(buf, sizeof buf);
}

int main()
{
    //设置开发板ip信息
    ip_address.set_ip_address("192.168.10.2");
    netmask_address.set_ip_address("255.255.255.0");
    gateway_address.set_ip_address("192.168.74.254");

    //绑定到ETH接口
    eth.set_network(ip_address, netmask_address, gateway_address);

    //若eth成功获取到静态ip,打印ip信息
    if(eth.connect() < 0)
    {
        printf("setting static ip error!\r\n");
    }
    else
    {
        SocketAddress ip;
        eth.get_ip_address(&ip);
        printf("ip address: %s\r\n",ip.get_ip_address());
    }

    //绑定ip地址到tcp连接
    client.open(ð);

    //设置tcl服务器ip信息
    server_address.set_ip_address("192.168.10.100");
    server_address.set_port(8080);

    //开发板连接TCP服务器,并打印连接的信息
    if (client.connect(server_address) < 0)
    {
        printf("Connect server failed!\r\n");
    }
    else
    {
        printf("Connect server successed!\r\n");
    }

    button.fall(&button_handler);

    while(true)
    {
        led2 = !led2;
        ThisThread::sleep_for(1s);
    }

    return 0;
}
代码实现功能点:

    这段代码实现了一个基于mbedOS的嵌入式系统的简单TCP客户端程序。

    1. 引入必要的头文件和库:代码中使用了 `#include "mbed.h"` 和 `#include "EthernetInterface.h"` 来引入mbedOS和EthernetInterface的相关库。

    2. 定义变量
       - `EthernetInterface eth;`:声明了一个以太网接口对象 `eth`,用于管理以太网连接。
       - `SocketAddress ip_address;`、`SocketAddress netmask_address;`、`SocketAddress gateway_address;`:声明了三个套接字地址对象,分别用于存储开发板的IP地址、子网掩码和网关地址。
       - `TCPSocket client;`:声明了一个TCP套接字对象 `client`,用于与服务器建立连接并进行通信。
       - `SocketAddress server_address;`:声明了一个套接字地址对象 `server_address`,用于存储服务器的IP地址和端口号。
       - `DigitalOut led2(LED2);`、`InterruptIn button(KEY1);`:声明了一个LED控制对象 `led2` 和一个按键中断对象 `button`,用于控制LED和响应按键事件。

    3. 定义按键中断处理函数:`void button_handler()`,当按键被按下时,向服务器发送一个包含文本 "Button was pressed!" 的数据包。

    4. 主函数 `int main()`
       - 设置开发板的IP地址、子网掩码和网关地址,并绑定到以太网接口。
       - 如果以太网接口成功连接到网络,则打印开发板的IP地址。
       - 打开TCP连接,并设置服务器的IP地址和端口号。
       - 尝试连接到服务器,如果连接失败则打印连接失败信息,否则打印连接成功信息。
       - 注册按键中断处理函数。
       - 在主循环中,通过轮询方式交替改变LED的状态,并使用 `ThisThread::sleep_for()` 函数使程序休眠1秒钟。

    总体而言,这段代码的功能是初始化以太网接口,连接到网络,然后建立一个TCP连接到指定的服务器,并在按键被按下时向服务器发送数据,同时在循环中交替改变LED的状态。

    下载代码到开发板,用以太网线连接开发板和PC端,打开控制面板,设置以太网接口的静态的ip地址为TCP Server的IP(192.168.10.100)。

image001.png

    打开网络调试助手,进行如下的配置,这样我们的网络调试助手就成功配置成一个TCP服务器了,我们就可以进行后续的测试工作了。

image003.png

    点击网络调试助手连接,复位开发板,按下KEY1后,发现IDE竟然报错了!!!

image005.png

发现的问题点:
    最终定位在中断服务函数中,原来是中断服务函数执行在执行client.send()函数时凉了。

问题原因:
    在嵌入式系统中,中断服务函数(ISR)应该尽量保持简短和高效,因为它们会在中断发生时立即执行,而且在执行期间,其他中断可能会被屏蔽,因此长时间运行的操作可能会引起问题。

    在这个特定的情况下,`client.send()` 函数是一个涉及网络通信的操作,可能需要一段时间来完成,尤其是在网络速度较慢或者网络状况不佳的情况下。如果在中断服务函数中调用`client.send()`函数,那么当按钮按下时,就会触发网络通信操作,这可能会导致以下问题:

    1. **延迟和响应性问题:** 在网络通信过程中,中断服务函数会被阻塞,导致延迟。如果延迟太长,用户可能会感觉到按钮响应速度慢或者不一致。

    2. **资源竞争和冲突:** 在中断服务函数中进行网络通信操作可能会导致资源竞争和冲突,因为网络通信可能需要访问共享资源,比如缓冲区或者网络接口。如果其他部分的代码也在访问这些资源,可能会导致不可预测的行为。

    3. **栈溢出:** 中断服务函数通常在一个单独的栈上执行,如果在中断服务函数中进行了过多的操作或者递归调用,可能会导致栈溢出问题,从而使系统崩溃。

    原来是中断服务函数中最好只执行必要且尽可能快速的操作,为了解决这个问题,我在中断服务函数中设置了一个标志位,然后给发送的代码创建了一个线程,在线程中执行client.send()函数。


代码调整:
#include "mbed.h"
#include "EthernetInterface.h"
#include <cstdint>

//声明以太网接口
EthernetInterface eth;
//声明开发板ip地址
SocketAddress ip_address;
//声明开发板子网掩码
SocketAddress netmask_address;
//声明开发板网关
SocketAddress gateway_address;

//声明TCP连接
TCPSocket client;

//声明TCP服务器ip地址及端口
SocketAddress server_address;

DigitalOut led2(LED2);
DigitalOut led3(LED3);
InterruptIn button(KEY1);

uint32_t press_index = 0;
Queue<uint32_t,255> queue;
Thread thread;

void button_handler()
{
    // char buf[100] = "Button was pressed!";
    // client.send(buf, sizeof buf);
    press_index++;
    queue.put(&press_index);
}

void send_thread()
{
    while(true)
    {
        osEvent evt = queue.get();
        if(evt.status == osEventMessage)
        {
            char buf[100] = "Button is pressed!\r\n";
            led3 = !led3;
            client.send(buf, sizeof buf);
        }
    }
}

int main()
{
    //设置开发板ip信息
    ip_address.set_ip_address("192.168.10.2");
    netmask_address.set_ip_address("255.255.255.0");
    gateway_address.set_ip_address("192.168.74.254");

    //绑定到ETH接口
    eth.set_network(ip_address, netmask_address, gateway_address);

    //若eth成功获取到静态ip,打印ip信息
    if(eth.connect() < 0)
    {
        printf("setting static ip error!\r\n");
    }
    else
    {
        SocketAddress ip;
        eth.get_ip_address(&ip);
        printf("ip address: %s\r\n",ip.get_ip_address());
    }

    //绑定ip地址到tcp连接
    client.open(ð);

    //设置tcl服务器ip信息
    server_address.set_ip_address("192.168.10.100");
    server_address.set_port(8080);

    //开发板连接TCP服务器,并打印连接的信息
    if (client.connect(server_address) < 0)
    {
        printf("Connect server failed!\r\n");
    }
    else
    {
        printf("Connect server successed!\r\n");
    }

    button.fall(&button_handler);
    thread.start(&send_thread);

    while(true)
    {
        led2 = !led2;
        ThisThread::sleep_for(1s);
    }

    return 0;
}


在上一次代码的基础上,主要做了以下修改:

    1. 引入了 `Queue` 和 `Thread` 类,用于在中断服务函数中将按键事件发送到一个线程进行处理。

    2. 修改了 `button_handler()` 函数,将按键事件发送的次数存储到 `press_index` 变量中,并将其放入队列中。

    3. 定义了一个新的线程 `send_thread()`,该线程不断从队列中获取按键事件,并向服务器发送数据。

    4. 在主函数中启动了 `send_thread()` 线程。


    这些修改的目的是将网络通信操作从中断服务函数中移出,避免在中断服务函数中进行长时间的操作,以提高系统的响应性和稳定性。通过将按键事件发送到一个线程中处理,可以保证网络通信操作在主线程中进行,从而避免了在中断服务函数中进行网络通信操作可能带来的问题。

    之后我再次下载代码,复位开发板,按下KEY1,发现TCP服务器端正常响应了客户端发送的信息。
image007.png


    当然,以上代码又存在一个问题,就是数据的单向发送问题,如何才能让客户端也能收到服务端返回的数据呢,答案就是让客户端代码不断地监测是否有数据到达。为了实现这个功能,我在main函数中添加了这部分的数据解析。

int main()
{
    //设置开发板ip信息
    ip_address.set_ip_address("192.168.10.2");
    netmask_address.set_ip_address("255.255.255.0");
    gateway_address.set_ip_address("192.168.74.254");

    //绑定到ETH接口
    eth.set_network(ip_address, netmask_address, gateway_address);

    //若eth成功获取到静态ip,打印ip信息
    if(eth.connect() < 0)
    {
        printf("setting static ip error!\r\n");
    }
    else
    {
        SocketAddress ip;
        eth.get_ip_address(&ip);
        printf("ip address: %s\r\n",ip.get_ip_address());
    }

    //绑定ip地址到tcp连接
    client.open(ð);

    //设置tcl服务器ip信息
    server_address.set_ip_address("192.168.10.100");
    server_address.set_port(8080);

    //开发板连接TCP服务器,并打印连接的信息
    if (client.connect(server_address) < 0)
    {
        printf("Connect server failed!\r\n");
    }
    else
    {
        printf("Connect server successed!\r\n");
    }

    button.fall(&button_handler);
    thread.start(&send_thread);

    while(true)
    {
        // led2 = !led2;
        // ThisThread::sleep_for(1s);
        char buf[1024];
        for (int i = 0; i < 1024; i++)
        {
            buf[i] = '\0';
        }
        int count = client.recv(buf, sizeof(buf));
        if (count > 0)
        {
            led2 = !led2;
            printf("%s",buf);
        }
        ThisThread::yield();
    }
    client.close();

    return 0;
}


在这个修改后的 `main()` 函数中,主要做了以下几个修改:

    1. 注释掉了之前的 LED 控制代码:`led2 = !led2;` 和 `ThisThread::sleep_for(1s);`。

    2. 添加了一个循环用于接收来自服务器的数据。在循环中使用 `client.recv()` 函数接收数据,并将其存储在 `buf` 缓冲区中。接收到的数据长度存储在 `count` 变量中。

    3. 如果接收到的数据长度大于 0,则切换 LED2 状态并打印接收到的数据。这个操作是为了在接收到数据时提供一种视觉指示。

    4. 在循环的末尾调用了 `ThisThread::yield()` 函数。这个函数的作用是让出当前线程的执行权,以便其他线程有机会执行。这样做可以确保主线程不会独占 CPU 资源,以便其他任务(比如网络通信)能够及时处理。

    5. 在循环结束后调用了 `client.close()` 函数,关闭了与服务器的连接。这个操作是为了确保在程序退出之前关闭网络连接,以释放资源并避免网络连接泄漏。


经过多次修改后,最终代码如下:
#include "mbed.h"
#include "EthernetInterface.h"
#include <cstdint>

//声明以太网接口
EthernetInterface eth;
//声明开发板ip地址
SocketAddress ip_address;
//声明开发板子网掩码
SocketAddress netmask_address;
//声明开发板网关
SocketAddress gateway_address;

//声明TCP连接
TCPSocket client;

//声明TCP服务器ip地址及端口
SocketAddress server_address;

DigitalOut led2(LED2);
DigitalOut led3(LED3);
InterruptIn button(KEY1);

uint32_t press_index = 0;
Queue<uint32_t,255> queue;
Thread thread;

void button_handler()
{
    // char buf[100] = "Button was pressed!";
    // client.send(buf, sizeof buf);
    press_index++;
    queue.put(&press_index);
}

void send_thread()
{
    while(true)
    {
        osEvent evt = queue.get();
        if(evt.status == osEventMessage)
        {
            char buf[100] = "Button is pressed!\r\n";
            led3 = !led3;
            client.send(buf, sizeof buf);
        }
    }
}

int main()
{
    //设置开发板ip信息
    ip_address.set_ip_address("192.168.10.2");
    netmask_address.set_ip_address("255.255.255.0");
    gateway_address.set_ip_address("192.168.74.254");

    //绑定到ETH接口
    eth.set_network(ip_address, netmask_address, gateway_address);

    //若eth成功获取到静态ip,打印ip信息
    if(eth.connect() < 0)
    {
        printf("setting static ip error!\r\n");
    }
    else
    {
        SocketAddress ip;
        eth.get_ip_address(&ip);
        printf("ip address: %s\r\n",ip.get_ip_address());
    }

    //绑定ip地址到tcp连接
    client.open(ð);

    //设置tcl服务器ip信息
    server_address.set_ip_address("192.168.10.100");
    server_address.set_port(8080);

    //开发板连接TCP服务器,并打印连接的信息
    if (client.connect(server_address) < 0)
    {
        printf("Connect server failed!\r\n");
    }
    else
    {
        printf("Connect server successed!\r\n");
    }

    button.fall(&button_handler);
    thread.start(&send_thread);

    while(true)
    {
        // led2 = !led2;
        // ThisThread::sleep_for(1s);
        char buf[1024];
        for (int i = 0; i < 1024; i++)
        {
            buf[i] = '\0';
        }
        int count = client.recv(buf, sizeof(buf));
        if (count > 0)
        {
            led2 = !led2;
            printf("%s",buf);
        }
        ThisThread::yield();
    }
    client.close();

    return 0;
}

    下载代码到开发板,再次复位,在TCP 服务端发送消息,可以看到客户端也正常接收到了服务端的信息了。

image009.png

##4、问题总结

    在编写中断服务函数时,应避免使用或调用一些阻塞或超长延时函数,例如我们如果在中断服务函数中使用printf()函数,就可能出现下列的一系列情况。

    1. **资源竞争:** printf函数通常需要使用系统的I/O资源,如串口或文件,但在中断服务函数中,这些资源可能已经被其他部分的代码占用,如果在中断服务函数中调用printf,可能会造成资源竞争,甚至死锁。

    2. **执行时间不确定性:** printf函数可能会花费较长的时间来执行,因为它涉及到格式化字符串、系统调用等操作。在中断服务函数中执行时间不确定会导致中断响应时间过长,影响系统的实时性能。

    3. **堆栈溢出风险:** printf函数通常会使用堆栈来存储临时变量和函数调用信息,而在中断服务函数中,堆栈空间可能比较有限,如果printf函数在中断服务函数中使用过多的堆栈空间,可能会导致堆栈溢出。

    因此,为了保证中断服务函数的可靠性、实时性和效率,一般建议在中断服务函数中尽量避免使用这类函数,可以使用轻量级的日志记录方式或者设置标志位,在主程序中再进行相应的输出或日志记录。


    以上就是本次使用Mbed studio开发TCP Client的全部内容,附件TCP Client 的Main.cpp 文件,如有问题,欢迎讨论。

附件:

main源码.zip (1.15 KB)

使用特权

评论回复

打赏榜单

21小跑堂 打赏了 100.00 元 2024-04-30
理由:恭喜通过原创审核!期待您更多的原创作品~您已符合蓝v达人的审核标准,升级蓝v达人可解锁更高的打赏哦~

评论
21小跑堂 2024-4-30 15:56 回复TA
结构清晰,步骤明确,详略得当,对开发过程的总结较为完整,问题定位准确,完成mbedos TCP 客户端程序设计 
丙丁先生| | 2024-5-13 15:02 | 显示全部楼层

使用特权

评论回复
发新帖 本帖赏金 100.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

15

主题

28

帖子

4

粉丝