UDP/TCP是物联网通信中常用的一种基础通信协议,是TCP/IP协议的核心。其中TCP是面向连接、可实现端到端可靠数据包发送;UDP是无连接的,无超时重发机制,数据流传输不完全可靠,但传输速度比TCP更快。本文从使用流程、SDK demo测试、TCP测试示例和常见问题四个方面介绍了如何快速实现ML307A模组的UDP/TCP双向通信功能。
一、UDP/TCP通信示例流程
二、SDK demo测试
SDK本身有UDP/TCP测试示例,下面我们通过烧录demo固件进行测试演示。
2.1 连接服务器
(1) 模组上电开机,等待初始化完成。当串口打印”please input cmds:”后,通过串口输入:
CM:ASOCKET:OPEN:0
其中,OPEN后面的参数0代表测试TCP连接;如果配置其它非0值则代表测试UDP连接。
(2) 上述指令执行后,通过串口可以观察到模组开始运行TCP测试用例,日志如下:
__on_eloop_cmd_OPEN_recv_event type=0
sock(3) open
sock(3) open request success, wait connect...
sock(3) connect_ok
sock(3) recv_ind: recv_avail=38, recv_len=38, data=
221.178.126.121:31893 CONNECTED OK
其中,sock括号中的3代表socket id值。
2.2 向服务器上报数据
(1) 服务器连接成功后,通过串口输入:
CM:ASOCKET:SEND:3
其中,3代表上面创建的socket id,即向该socket发送数据。
(2) 上述指令执行后,通过串口可以观察到模组开始运行TCP测试用例,日志如下:
CM:ASOCKET:SEND:3
cm_test_asocket_cmd len=4
cmd[0]=CM,cmd[1]=ASOCKET,cmd[2]=SEND
__on_cmd_SEND sock=3
OK
please input cmds:
__on_eloop_cmd_SEND_recv_event sock=3
sock(3) send len=5
sock(3) send_ind
其中,send_len代表发送数据内容长度为5,因为demo调用的发送数据示例是cm_asocket_send(sock, "hello", 5, 0)。
(3) 通过TCP服务器观察模组上报的数据,内容如下:
2.3 服务器下发数据
(1) 通过TCP服务器给模组下发数据,数据内容为ABCD:
(2) 通过串口可以观察到模组打印服务器下发数据,内容如下:
sock(3) recv_ind: recv_avail=4, recv_len=4, data=ABCD
三、TCP 测试示例介绍
本小节通过建立TCP连接、向服务器发数据、接收服务器下行数据为例,进行代码举例说明。
3.1 建立TCP连接
(1) 创建socket
void cm_custom_tcp_test(void)
{
cm_demo_printf("tcp init begins:\n");
if (socket_id != -1)
{
return; //如果socket_id不等于-1,退出cm_custom_tcp_init函数
}
//用最原始的socket接口创建socket
socket_id = socket(AF_INET, SOCK_STREAM, 0);
if (socket_id == -1)
{
cm_demo_printf("sokcet_id create fail!\n");
return;
}
else
{
cm_demo_printf("socket_id%d create sucess!\n", socket_id);
}
- 配置TCP服务器IP和端口
struct sockaddr_in server_address;
memset(&server_address, 0, sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr("47.108.191.127"); //把参数cp所指的网络地址字符串转换成网络所使用的二进制数字
server_address.sin_port = htons(2027); //将一个无符号短整型数值转换为网络字节序,即大端模式
- 建立TCP连接
//用原始的connect接口创建tcp连接
ret = connect(socket_id, (struct sockaddr *)&server_address, sizeof(struct sockaddr));
if (ret != 0)
{
cm_demo_printf("[TCPCLIENT]tcp connect error\n");
}
else
{
cm_demo_printf("[TCPCLIENT]tcp connect ok\n");
}
- 运行结果
tcp init begins:
socket_id3 create sucess!
[TCPCLIENT]tcp connect ok
receiving data...
data received ...
socket3 Data Arrives:36
218.204.253.98:3940 CONNECTED OK
3.2 向TCP服务器上报数据
(1) 通过select阻塞检测TCP事件
//清零fd_set结构
memset(&tcp_readfds, 0, sizeof(tcp_readfds));
memset(&tcp_errorfds, 0, sizeof(tcp_errorfds));
memset(&tcp_writefds, 0, sizeof(tcp_writefds));
//设置socket到fd_set结构体
FD_SET(socket_id, &tcp_readfds);
FD_SET(socket_id, &tcp_errorfds);
FD_SET(socket_id, &tcp_writefds);
/* 重要!!!select接口为阻塞形式;
#define select(a,b,c,d,e) lwip_select(a,b,c,d,e)
int lwip_select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout);
* maxfdp1 指集合中所有文件描述符的范围,即所有文件描述符的最大值+1
* readfds、writefds、errorfds 指向文件描述符集合的指针,分别检测输入、输出是否就绪和异常情况是否发生
* timeout 是select()的超时时间,控制着select()的阻塞行为
*/
custom_tcp_result = select(socket_id + 1, &tcp_readfds, &tcp_writefds, &tcp_errorfds, NULL);
- 当检测到tcp_writefds,同时error没有错误,代表可以向服务器发送数据
if (custom_tcp_result > 0)
{
/*
* 针对TCP连接过程的三次握手,服务器是否有回复的ACK包进行检测
* 当检测到tcp_writefds,同时error没有错误,代表可以向服务器发送数据
* 当检测到tcp_writefds,如果error有错误,则代表连接失败
*/
if ((socket_id != -1) && FD_ISSET(socket_id, &tcp_writefds))
{
// getsocketopt的主要作用是去检测error有没有错误产生,传入的是error的地址,即将错误值赋给error
// 连接上不一定会触发该事件
getsockopt(socket_id, SOL_SOCKET, SO_ERROR, &error, (socklen_t *)&socket_code_size);
if (error == 0)
{
if (ret == 0 && osMessageQueueGet(custom_uart1_queue, queue_data_buffer, NULL, 0) == 0)
{
cm_custom_tcp_send(queue_data_buffer, strlen(queue_data_buffer));
memset(queue_data_buffer, 0, sizeof(queue_data_buffer));
}
}
}
其中,上述代码获取队列数据,队列数据通过串口3输入,并将数据通过cm_custom_tcp_send发出。
- cm_custom_tcp_send函数实现
// TCP发数据
void cm_custom_tcp_send(unsigned char *data_buf, int len)
{
int send_ret = 0;
send_ret = send(socket_id, data_buf, len, 0);
cm_demo_printf("send_ret = %d\n", send_ret);
}
- 运行结果
通过串口输入Hello,Word!
服务器打印[2023-5-13 20:47:081 Hello.Word!
3.3 接收TCP服务器下行数据
(1) 通过select阻塞检测TCP事件
//清零fd_set结构
memset(&tcp_readfds, 0, sizeof(tcp_readfds));
memset(&tcp_errorfds, 0, sizeof(tcp_errorfds));
memset(&tcp_writefds, 0, sizeof(tcp_writefds));
//设置socket到fd_set结构体
FD_SET(socket_id, &tcp_readfds);
FD_SET(socket_id, &tcp_errorfds);
FD_SET(socket_id, &tcp_writefds);
/* 重要!!!select接口为阻塞形式,#define select(a,b,c,d,e) lwip_select(a,b,c,d,e)
int lwip_select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout);
* maxfdp1 指集合中所有文件描述符的范围,即所有文件描述符的最大值+1
* readfds、writefds、errorfds 指向文件描述符集合的指针,分别检测输入、输出是否就绪和异常情况是否发生
* timeout 是select()的超时时间,控制着select()的阻塞行为
*/
custom_tcp_result = select(socket_id + 1, &tcp_readfds, &tcp_writefds, &tcp_errorfds, NULL);
- 当检测到tcp_readfds,进入cm_socket_receive_callback回调处理数据
if (custom_tcp_result > 0)
{
if ((socket_id != -1) && FD_ISSET(socket_id, &tcp_readfds))
{
cm_socket_receive_callback();
}
- cm_socket_recive_callback通过recvfrom接口处理下行数据
static void cm_socket_receive_callback()
{
/*
函数原型:int recvfrom(int sockfd, const void *buf, int len, unsigned int flags, const struct sockaddr *from, int fromlen)
传 入 值:sockfd 套接字描述符
buf 存放接收数据的缓冲区
len 接收数据的长度
flags 一般为0
from 发送方的IP地址和端口号信息
fromlen 地址长度
返 回 值:成功返回实际接收到的字节数;失败返回-1
*/
int data_len = 0;
struct sockaddr_in from;
int fromlen = sizeof(struct sockaddr_in);
//以非阻塞方式接收socket数据
tcp_recv_buf = recvfrom(socket_id, cm_custom_tcp_recv, 2048, MSG_TRUNC | MSG_DONTWAIT, (struct sockaddr *)&from, &fromlen);
if (tcp_recv_buf <= 0)
{
cm_demo_printf("tcp socket closed!\n");
}
//判断数据是否接收完毕
cm_demo_printf("receiving data...\n");
while (0 < tcp_recv_buf && tcp_recv_buf < 2048)
{
//没有读取到数据,recvfrom返回值为-1
data_len = recvfrom(socket_id, cm_custom_tcp_recv + tcp_recv_buf, 2048 - tcp_recv_buf, MSG_TRUNC | MSG_DONTWAIT, (struct sockaddr *)&from, &fromlen);
if (data_len <= 0)
{
osDelay(1);
data_len = recvfrom(socket_id, cm_custom_tcp_recv + tcp_recv_buf, 2048 - tcp_recv_buf, MSG_TRUNC | MSG_DONTWAIT, (struct sockaddr *)&from, &fromlen);
if (data_len <= 0)
{
break;
}
else
{
tcp_recv_buf += data_len;
}
}
else
{
tcp_recv_buf += data_len;
}
}
cm_demo_printf("data received ...\n");
cm_demo_printf("socket%d Data Arrives:%d\n", socket_id, tcp_recv_buf);
cm_uart_write(OPENCPU_MAIN_URAT, cm_custom_tcp_recv, tcp_recv_buf, 2000);
cm_demo_printf("\n");
memset(cm_custom_tcp_recv, 0, sizeof(cm_custom_tcp_recv));
}
其中,代码增加了数据双重判断以提高数据接收完整性。
- 运行结果
通过TCP服务器下发数据This is a TCP test!
模组主串口打印:
receiving data...
data received ...
socket3 Data Arrives:19
This is a TCP test!
四、常见问题
1、哪些情况会导致连接TCP服务器失败?
(1) 模组没有注册上4G网络时,发起TCP连接会失败;
(2) 专网卡默认只能连接定向IP和端口,使用专网卡连接其他TCP服务器会失败。
2、服务器下发2K数据,为什么模组会分2包打印?
TCP是以段为单位发送数据的。建立TCP连接后,有一个最大消息长度(MSS),如果应用层数据包超过MSS,就会把应用层数据拆分,分成两个段来发送。这种情况下,应用层要拼接这两个TCP包才能正确处理数据。一般TCP的MSS为1460字节。
3、选用select接口判断socket状态,为什么会出现其它线程无法执行现象,例如上行数据无法发送,或者串口线程无法接收数据?
select接口的第五个参数为timeout超时时间,如果设定为NULL(custom_tcp_result = select(socket_id + 1, &tcp_readfds, &tcp_writefds, &tcp_errorfds, NULL)),线程会快速重复遍历socket接收、发送和错误状态,这种情况下CPU会一直执行select接口,无法释放CPU资源,因此导致其他线程无法执行。针对此现象,可以通过在select执行完后增加osDelay系统延时解决。