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

[APM32F4] mbedos TCP 客户端程序设计

[复制链接]
7763|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的开发环境的搭建,这里就不介绍了,可以参考论坛其他大佬关于这部分的内容,我们直接上源码。
    我在写这部分代码的时候,经过了多次修改,也是发现了一些问题,并总结出了一些经验。

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

  3. //声明以太网接口
  4. EthernetInterface eth;
  5. //声明开发板ip地址
  6. SocketAddress ip_address;
  7. //声明开发板子网掩码
  8. SocketAddress netmask_address;
  9. //声明开发板网关
  10. SocketAddress gateway_address;

  11. //声明TCP连接
  12. TCPSocket client;

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

  15. DigitalOut led2(LED2);
  16. InterruptIn button(KEY1);

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

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

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

  30.     //若eth成功获取到静态ip,打印ip信息
  31.     if(eth.connect() < 0)
  32.     {
  33.         printf("setting static ip error!\r\n");
  34.     }
  35.     else
  36.     {
  37.         SocketAddress ip;
  38.         eth.get_ip_address(&ip);
  39.         printf("ip address: %s\r\n",ip.get_ip_address());
  40.     }

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

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

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

  55.     button.fall(&button_handler);

  56.     while(true)
  57.     {
  58.         led2 = !led2;
  59.         ThisThread::sleep_for(1s);
  60.     }

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

    这段代码实现了一个基于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()函数。


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

  4. //声明以太网接口
  5. EthernetInterface eth;
  6. //声明开发板ip地址
  7. SocketAddress ip_address;
  8. //声明开发板子网掩码
  9. SocketAddress netmask_address;
  10. //声明开发板网关
  11. SocketAddress gateway_address;

  12. //声明TCP连接
  13. TCPSocket client;

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

  16. DigitalOut led2(LED2);
  17. DigitalOut led3(LED3);
  18. InterruptIn button(KEY1);

  19. uint32_t press_index = 0;
  20. Queue<uint32_t,255> queue;
  21. Thread thread;

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

  29. void send_thread()
  30. {
  31.     while(true)
  32.     {
  33.         osEvent evt = queue.get();
  34.         if(evt.status == osEventMessage)
  35.         {
  36.             char buf[100] = "Button is pressed!\r\n";
  37.             led3 = !led3;
  38.             client.send(buf, sizeof buf);
  39.         }
  40.     }
  41. }

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

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

  50.     //若eth成功获取到静态ip,打印ip信息
  51.     if(eth.connect() < 0)
  52.     {
  53.         printf("setting static ip error!\r\n");
  54.     }
  55.     else
  56.     {
  57.         SocketAddress ip;
  58.         eth.get_ip_address(&ip);
  59.         printf("ip address: %s\r\n",ip.get_ip_address());
  60.     }

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

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

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

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

  77.     while(true)
  78.     {
  79.         led2 = !led2;
  80.         ThisThread::sleep_for(1s);
  81.     }

  82.     return 0;
  83. }


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

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

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

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

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


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

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


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

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

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

  9.     //若eth成功获取到静态ip,打印ip信息
  10.     if(eth.connect() < 0)
  11.     {
  12.         printf("setting static ip error!\r\n");
  13.     }
  14.     else
  15.     {
  16.         SocketAddress ip;
  17.         eth.get_ip_address(&ip);
  18.         printf("ip address: %s\r\n",ip.get_ip_address());
  19.     }

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

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

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

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

  36.     while(true)
  37.     {
  38.         // led2 = !led2;
  39.         // ThisThread::sleep_for(1s);
  40.         char buf[1024];
  41.         for (int i = 0; i < 1024; i++)
  42.         {
  43.             buf[i] = '\0';
  44.         }
  45.         int count = client.recv(buf, sizeof(buf));
  46.         if (count > 0)
  47.         {
  48.             led2 = !led2;
  49.             printf("%s",buf);
  50.         }
  51.         ThisThread::yield();
  52.     }
  53.     client.close();

  54.     return 0;
  55. }


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

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

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

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

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

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


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

  4. //声明以太网接口
  5. EthernetInterface eth;
  6. //声明开发板ip地址
  7. SocketAddress ip_address;
  8. //声明开发板子网掩码
  9. SocketAddress netmask_address;
  10. //声明开发板网关
  11. SocketAddress gateway_address;

  12. //声明TCP连接
  13. TCPSocket client;

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

  16. DigitalOut led2(LED2);
  17. DigitalOut led3(LED3);
  18. InterruptIn button(KEY1);

  19. uint32_t press_index = 0;
  20. Queue<uint32_t,255> queue;
  21. Thread thread;

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

  29. void send_thread()
  30. {
  31.     while(true)
  32.     {
  33.         osEvent evt = queue.get();
  34.         if(evt.status == osEventMessage)
  35.         {
  36.             char buf[100] = "Button is pressed!\r\n";
  37.             led3 = !led3;
  38.             client.send(buf, sizeof buf);
  39.         }
  40.     }
  41. }

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

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

  50.     //若eth成功获取到静态ip,打印ip信息
  51.     if(eth.connect() < 0)
  52.     {
  53.         printf("setting static ip error!\r\n");
  54.     }
  55.     else
  56.     {
  57.         SocketAddress ip;
  58.         eth.get_ip_address(&ip);
  59.         printf("ip address: %s\r\n",ip.get_ip_address());
  60.     }

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

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

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

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

  77.     while(true)
  78.     {
  79.         // led2 = !led2;
  80.         // ThisThread::sleep_for(1s);
  81.         char buf[1024];
  82.         for (int i = 0; i < 1024; i++)
  83.         {
  84.             buf[i] = '\0';
  85.         }
  86.         int count = client.recv(buf, sizeof(buf));
  87.         if (count > 0)
  88.         {
  89.             led2 = !led2;
  90.             printf("%s",buf);
  91.         }
  92.         ThisThread::yield();
  93.     }
  94.     client.close();

  95.     return 0;
  96. }

    下载代码到开发板,再次复位,在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, 下载次数: 5)

网络调试助手.zip (483.81 KB, 下载次数: 5)




打赏榜单

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

评论

结构清晰,步骤明确,详略得当,对开发过程的总结较为完整,问题定位准确,完成mbedos TCP 客户端程序设计  发表于 2024-4-30 15:56
szt1993 发表于 2024-5-23 18:01 | 显示全部楼层
TCP 客户端程序设计是标准库函数进行的开发?
您需要登录后才可以回帖 登录 | 注册

本版积分规则

60

主题

108

帖子

17

粉丝
快速回复 在线客服 返回列表 返回顶部