最近了解了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)。
打开网络调试助手,进行如下的配置,这样我们的网络调试助手就成功配置成一个TCP服务器了,我们就可以进行后续的测试工作了。
点击网络调试助手连接,复位开发板,按下KEY1后,发现IDE竟然报错了!!!
发现的问题点:
最终定位在中断服务函数中,原来是中断服务函数执行在执行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服务器端正常响应了客户端发送的信息。
当然,以上代码又存在一个问题,就是数据的单向发送问题,如何才能让客户端也能收到服务端返回的数据呢,答案就是让客户端代码不断地监测是否有数据到达。为了实现这个功能,我在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 服务端发送消息,可以看到客户端也正常接收到了服务端的信息了。
##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)
网络调试助手.zip (483.81 KB)。
---------------------
作者:DKENNY
链接:https://bbs.21ic.com/icview-3374698-1-1.html
来源:21ic.com
此文章已获得原创/原创奖标签,著作权归21ic所有,任何人未经允许禁止转载。