OneMO模组说|技术学堂-ML307A开发指南(三) OpenCPU UDP及TCP使用介绍

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);

    }
  1. 配置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);                       //将一个无符号短整型数值转换为网络字节序,即大端模式
  1. 建立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");

   }
  1. 运行结果

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);
  1. 当检测到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发出。

  1. 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);

}
  1. 运行结果

通过串口输入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);
  1. 当检测到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();

            }
  1. 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));

}

其中,代码增加了数据双重判断以提高数据接收完整性。

  1. 运行结果

通过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系统延时解决。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值