tcp客户端
=========

作者:陈之敏 时间:2020年08月15日
关键字:csdk、RDA8910、二次开发、TCP客户端 ## 目录

`点击这里查看所有博文 <https://blog.csdn.net/weixin_44570083/article/details/104285283>`__

  最近听说有的小伙伴看了我的教程后,有一些问题都跑到官方的gitee上面去问去了。导致官方的人没搞懂问的是啥,小伙伴们也没能知道自己想要的答案。给大家造成了困扰,这里我说声抱歉。

  既然出现了这个问题,我这里就声明一下,本系列教程所涉及的内容(demo)不是官方的作品。我个人觉得官方的demo内容太多太全,往往都是把一个模块内所有的东西全部放在一起。这样的话对新手不是很友好,阅读起来也比较费劲。我就把官方的部分demo进行相关的简化,并推出教程这样的话可能会对新手朋友们有一定的帮助。

  有时候周末闲暇时间我也会加上一些我觉得好玩的模块在里面,这些可能在官方的demo都没有,比如cJSON、PAHO-MQTT、http-client。

  这就是官方的代码仓库。

.. code:: c

   git clone --recursive https://gitee.com/openLuat/Luat_CSDK_Air724U.git

  当然各位小伙伴在看本教程时,我建议还是使用我下面提供的仓库比较好,看完之后在迁移到官方的仓库⇧。

.. code:: c

   git clone --recursive https://gitee.com/chenxiahuaxu/RDA8910_CSDK.git

  再看本教程过程中如果遇到了问题,可以在本人的代码仓库下面评论。也可以在本人的\ `博客 <https://blog.csdn.net/weixin_44570083/article/details/104285283>`__\ 下面评论。我要是看到的话,并且这个问题在我的能力范围的话我会尽力解答的(非官方,不要对我要求太多哦,要求太多我可能就不管啦)。

一、前言
--------

  上篇博客我们简单的使用了下互联网的功能,主要是为了带大家熟悉一下联网流程,必须要根据设备反馈的信息进行相应的处理,才能接入互联网。本篇博客我们一起来试一试怎么使用tcp连接与服务器交换信息。

  关于tcp通讯的详解\ `点击这里 <https://blog.csdn.net/sinat_36629696/article/details/80740678>`__\ ,这位大神总结的很好,有兴趣的可以去看看底层原理。原理很复杂我这里也没什么好讲的,我只负责教会大家怎么用。

.. figure:: https://img-blog.csdnimg.cn/20200526200411431.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70
   :alt: 在这里插入图片描述

   在这里插入图片描述

  要记住一点就是tcp通讯是需要建立长连接的,有服务端和客户端的区别。服务端不会主动发起连接请求,只有客户端才会主动向服务器发起连接请求,建立连接后双方均可以主动发送数据。(\ ``注意:这里的服务端是指软件的状态,并不是指远程的服务器``\ )。

  由于我们使用的是4g模块,联网时基站给我们分配的ip不是公网ip。我又没有能力实现在模块上跑一套内网穿透的程序,所以就没有办法测试将模块作为tcp服务端使用的程序,关于tcp的例程只有这一个\ ``TcpClient``\ ,这是一个很大的遗憾。如果用的是wifi模块那就可以在局域网内测试TcpServer。
|在这里插入图片描述|

  lwip是瑞典计算机科学院(SICS)的Adam Dunkels
开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM
的占用。现在嵌入式设备联网所使用的socket库一般都是lwip库,所以不用担心在我这里学会了socket怎么用,去其他地方就不会用了,放心大胆的学,在哪都是一样的。
|image1| ## 二、编写测试程序 ## 2.1、了解本例程所用到的函数
  使用tcp副武需要包含\ ``#include "iot_socket.h""``\ 头文件,我们这里只用到了8个函数,分别是:
>/**设置网络状态回调函数 *@param indCb: 回调函数*\ @return TRUE: 成功
\*    FALSE: 失败 \**/

-  BOOL ``iot_network_set_cb`` (F_OPENAT_NETWORK_IND_CB indCb )

..

   /**创建socket @param domain: 仅支持AF_INET (IPV4 网络协议) @param
   type: 支持SOCK_STREAM/SOCK_DGRAM,分别表示TCP、UDP连接 @param
   protocol: 仅支持0\ @return >=0: socket描述符,用于后续操作     <0:
   创建socket失败\ @note 创建的socket不用后需要用close将其关闭**/

-  int ``socket``\ (int domain, int type, int protocol)

..

   /**本地字节顺序转化为网络字节顺序(16bits) @param n:
   本地字节书序数据\ @return 网络字节顺序数据**/

``htons``\ (n) ((n & 0xff) << 8) \| ((n & 0xff00) >> 8)

   /**将ip地址字符串转为数值,转化后的数值为网络字节顺序 @param cp:
   ip地址字符串,例如“192.168.1.1”\ @param addr: struct in_addr
   返回的ip地址数值 @return 1: 成功    0: 失败**/

``inet_aton``\ (cp, addr) ipaddr_aton(cp, (openat_ip_addr_t*)addr)

   /**建立和服务器端的连接 *@param socketfd:
   调用socket接口返回的socket描述符 @param addr: 指定服务器地址和端口
   @param addrlen: sizeof(struct openat_sockaddr)*\ @return 0: 表示成功
   \*    <0 表示有错误 \**/

-  int ``connect``\ (int socketfd, const struct openat_sockaddr \*addr,
   openat_socklen_t addrlen)

..

   /**关闭socket *@param fd: 调用socket接口返回的socket描述符*\ @return
   0: 表示成功 \*    <0 表示有错误 \**/

-  int ``close`` (int fd)

..

   /**发送数据 *@param socketfd: 调用socket接口返回的socket描述符 @param
   msg: 数据内容 @param len: 数据长度 @param flags:
   仅支持MSG_DONTWAIT/MSG_OOB,可以通过或来指定多个标志,一般为0*\ @return
   >=0: 实际发送的长度 \*    <0 发送错误 \**/

-  int ``send``\ (int socketfd,const void \*msg,size_t len,int flags)

..

   /**接收数据 *@param socketfd: 调用socket接口返回的socket描述符 @param
   buf: 用于存放数据的缓存 @param len: buf的长度 @param flags:
   仅支持MSG_DONTWAIT/MSG_PEEK/MSG_OOB,可以通过或来指定多个标志,一般为0*\ @return
   >0: 接收到的数据长度 *    =0: 对方已经断开连接*    <0: 读取错误
   \*@note
   当flags没有设置MSG_DONTWAIT,该函数会阻塞,直到有数据或者读取超时
   \**/

-  int ``recv``\ (int socketfd,void \*buf,size_t len,int flags)

2.2、编写主程序
---------------

  主程序负责注册网络回调函数,以及创建一个消息处理函数。

.. code:: c

       //系统休眠
       iot_os_sleep(10000);
       //注册网络状态回调函数
       iot_network_set_cb(NetWorkCb);
       //创建一个任务
       //TestTask_HANDLE =
       TestTask_HANDLE = iot_os_create_task(TestTask, NULL, 2048, 10, OPENAT_OS_CREATE_DEFAULT, "TestTask");
       return 0;

2.3、编写网络回调函数
---------------------

  消息回调函数负责通知系统消息,最好不要在其中处理复杂的动作。我这里将系统消息转存到全局变量中,然后再任务中定时查询并处理其他的逻辑。

.. code:: c

   static void NetWorkCb(E_OPENAT_NETWORK_STATE state)
   {
       NetWorkCbMessage = state;
   }

2.4、编写消息处理任务
---------------------

  在消息处理函数中,定时查询全局变量转存的网络状态,进行相应的处理。网络正常后调用\ ``TcpConnect``\ 建立套接字,同时连接远程服务端。执行完成后进行任务自毁。

.. code:: c

   static void TestTask(void *param)
   {
       bool NetLink = FALSE;
       while (NetLink == FALSE)
       {
           T_OPENAT_NETWORK_CONNECT networkparam = {0};
           switch (NetWorkCbMessage)
           {
           case OPENAT_NETWORK_DISCONNECT: //网络断开 表示GPRS网络不可用澹,无法进行数据连接,有可能可以打电话
               iot_debug_print("[socket] OPENAT_NETWORK_DISCONNECT");
               iot_os_sleep(10000);
               break;
           case OPENAT_NETWORK_READY: //网络已连接 表示GPRS网络可用,可以进行链路激活
               iot_debug_print("[socket] OPENAT_NETWORK_READY");
               memcpy(networkparam.apn, "CMNET", strlen("CMNET"));
               //建立网络连接,实际为pdp激活流程
               iot_network_connect(&networkparam);
               iot_os_sleep(500);
               break;
           case OPENAT_NETWORK_LINKED: //链路已经激活 PDP已经激活,可以通过socket接口建立数据连接
               iot_debug_print("[socket] OPENAT_NETWORK_LINKED");
               NetLink = TRUE;
               break;
           }
       }
       if (NetLink == TRUE)
       {
           TcpConnect();
       }
       iot_os_delete_task(TestTask_HANDLE);
   }

  最开始我是用任务通知做的,这样消息的实时性更高,不需要自己主动去查询消息。我也不知道为什么,如果用消息通知的话,加上这一行\ ``iot_network_connect(&networkparam);``\ 设备就重启。我以人格担保任务接收到的消息指针是对的,我还打印出来看过了,switch语句也进入了对应的分支。我就搞不懂为什么进行链路激活会出错。最后实在找不到原因,用全局变量转存,主动查询状态,避开了这个问题。
|image2|

2.5、编写TcpConnect
-------------------

  这是一个子程序,网络连接正常后在消息处理函数中被调用,只会执行一次。首先创建一个套接字,判断创建是否正常。然后初始化网络连接结构体,要注意的是需要将字符串格式的ip转化为网络序列的ip和主机字节port转化为网络字节顺序的端口,主要是因为\ ``大小端存储模式``\ 的字节顺序不一样。

  大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。

  小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。

.. code:: c

       //创建套接字
       socketfd = socket(OPENAT_AF_INET, OPENAT_SOCK_STREAM, 0);
       while (socketfd < 0)
       {
           iot_debug_print("[socket] create tcp socket error");
           iot_os_sleep(3000);
       }
       // 建立TCP链接
       struct openat_sockaddr_in tcp_server_addr = {0};
       //AF_INET 的目的就是使用 IPv4 进行通信
       tcp_server_addr.sin_family = OPENAT_AF_INET;
       //远端端口,主机字节顺序转变成网络字节顺序
       tcp_server_addr.sin_port = htons((unsigned short)TCP_SERVER_PORT);
       //字符串远端ip转化为网络序列ip
       inet_aton(TCP_SERVER_IP, &tcp_server_addr.sin_addr);
       iot_debug_print("[socket] tcp connect to addr %s", TCP_SERVER_IP);
       int connErr = connect(socketfd, (const struct openat_sockaddr *)&tcp_server_addr, sizeof(struct openat_sockaddr));
       if (connErr < 0)
       {
           iot_debug_print("[socket] tcp connect error %d", socket_errno(socketfd));
           close(socketfd);
       }
       iot_debug_print("[socket] tcp connect success");
       iot_os_create_task(SentTask, NULL, 2048, 10, OPENAT_OS_CREATE_DEFAULT, "SentTask");
       iot_os_create_task(RecvTask, NULL, 2048, 10, OPENAT_OS_CREATE_DEFAULT, "RecvTask");

2.6、编写发送任务
-----------------

  在发送任务中,定时对建立的套接字做循环发送字符串的动作,并进行相应的次数标记。

.. code:: c

       uint8 num = 0;
       int len = 0;
       char data[512] = {0};
       while (1)
       {
           if (socketfd >= 0)
           {
               len = sprintf(data, "RDA8910 Sent:%d", num);
               data[len] = '\0';
               iot_debug_print(data);
               if (len > 0)
               {
                   // TCP 发送数据
                   len = send(socketfd, data, len + 1, 0);
                   if (len < 0)
                   {
                       iot_debug_print("[socket] tcp send data False");
                   }
                   else
                   {
                       iot_debug_print("[socket] tcp send data Len = %d", len);
                       num += 1;
                   }
               }
           }
           iot_os_sleep(3000);
       }

2.7、编写接收任务
-----------------

  在接收任务中,将接收的数据打印在日志中显示,recv函数会陷入阻塞状态,直到接收到数据。程序中提供的测试服务端自带回环功能,会将接收的的数据原封不动返回。所以我们接收到的数据就是自己发送的数据。

.. code:: c

       int len = 0;
       unsigned char data[512] = {0};
       while (1)
       {
           if (socketfd >= 0)
           {
               // TCP 接受数据
               len = recv(socketfd, data, sizeof(data), 0);
               if (len < 0)
               {
                   iot_debug_print("[socket] tcp send data False");
               }
               else
               {
                   iot_debug_print("[socket] tcp Recv data result = %s", data);
               }
           }
       }

三、编译并下载程序
------------------

  完整代码在这,自取。

.. code:: c

   /*
    * @Author: your name
    * @Date: 2020-05-19 14:05:32
    * @LastEditTime: 2020-05-26 19:30:56
    * @LastEditors: Please set LastEditors
    * @Description: In User Settings Edit
    * @FilePath: \RDA8910_CSDK\USER\user_main.c
    */

   #include "string.h"
   #include "cs_types.h"

   #include "osi_log.h"
   #include "osi_api.h"

   #include "am_openat.h"
   #include "am_openat_vat.h"
   #include "am_openat_common.h"

   #include "iot_debug.h"
   #include "iot_uart.h"
   #include "iot_os.h"
   #include "iot_gpio.h"
   #include "iot_pmd.h"
   #include "iot_adc.h"
   #include "iot_vat.h"
   #include "iot_network.h"
   #include "iot_socket.h"

   //Tcp Client Demo

   #define TCP_SERVER_IP "121.40.198.143"
   #define TCP_SERVER_PORT 12415

   HANDLE TestTask_HANDLE = NULL;
   uint8 NetWorkCbMessage = 0;
   int socketfd = -1;

   static void SentTask(void *param)
   {
       uint8 num = 0;
       int len = 0;
       char data[512] = {0};
       while (1)
       {
           if (socketfd >= 0)
           {
               len = sprintf(data, "RDA8910 Sent:%d", num);
               data[len] = '\0';
               iot_debug_print(data);
               if (len > 0)
               {
                   // TCP 发送数据
                   len = send(socketfd, data, len + 1, 0);
                   if (len < 0)
                   {
                       iot_debug_print("[socket] tcp send data False");
                   }
                   else
                   {
                       iot_debug_print("[socket] tcp send data Len = %d", len);
                       num += 1;
                   }
               }
           }
           iot_os_sleep(3000);
       }
   }

   static void RecvTask(void *param)
   {
       int len = 0;
       unsigned char data[512] = {0};
       while (1)
       {
           if (socketfd >= 0)
           {
               // TCP 接受数据
               len = recv(socketfd, data, sizeof(data), 0);
               if (len < 0)
               {
                   iot_debug_print("[socket] tcp send data False");
               }
               else
               {
                   iot_debug_print("[socket] tcp Recv data result = %s", data);
               }
           }
       }
   }
   static void TcpConnect()
   {
       //创建套接字
       socketfd = socket(OPENAT_AF_INET, OPENAT_SOCK_STREAM, 0);
       while (socketfd < 0)
       {
           iot_debug_print("[socket] create tcp socket error");
           iot_os_sleep(3000);
       }
       // 建立TCP链接
       struct openat_sockaddr_in tcp_server_addr = {0};
       //AF_INET 的目的就是使用 IPv4 进行通信
       tcp_server_addr.sin_family = OPENAT_AF_INET;
       //远端端口,主机字节顺序转变成网络字节顺序
       tcp_server_addr.sin_port = htons((unsigned short)TCP_SERVER_PORT);
       //字符串远端ip转化为网络序列ip
       inet_aton(TCP_SERVER_IP, &tcp_server_addr.sin_addr);
       iot_debug_print("[socket] tcp connect to addr %s", TCP_SERVER_IP);
       int connErr = connect(socketfd, (const struct openat_sockaddr *)&tcp_server_addr, sizeof(struct openat_sockaddr));
       if (connErr < 0)
       {
           iot_debug_print("[socket] tcp connect error %d", socket_errno(socketfd));
           close(socketfd);
       }
       iot_debug_print("[socket] tcp connect success");
       iot_os_create_task(SentTask, NULL, 2048, 10, OPENAT_OS_CREATE_DEFAULT, "SentTask");
       iot_os_create_task(RecvTask, NULL, 2048, 10, OPENAT_OS_CREATE_DEFAULT, "RecvTask");
   }
   static void TestTask(void *param)
   {
       bool NetLink = FALSE;
       while (NetLink == FALSE)
       {
           T_OPENAT_NETWORK_CONNECT networkparam = {0};
           switch (NetWorkCbMessage)
           {
           case OPENAT_NETWORK_DISCONNECT: //网络断开 表示GPRS网络不可用澹,无法进行数据连接,有可能可以打电话
               iot_debug_print("[socket] OPENAT_NETWORK_DISCONNECT");
               iot_os_sleep(10000);
               break;
           case OPENAT_NETWORK_READY: //网络已连接 表示GPRS网络可用,可以进行链路激活
               iot_debug_print("[socket] OPENAT_NETWORK_READY");
               memcpy(networkparam.apn, "CMNET", strlen("CMNET"));
               //建立网络连接,实际为pdp激活流程
               iot_network_connect(&networkparam);
               iot_os_sleep(500);
               break;
           case OPENAT_NETWORK_LINKED: //链路已经激活 PDP已经激活,可以通过socket接口建立数据连接
               iot_debug_print("[socket] OPENAT_NETWORK_LINKED");
               NetLink = TRUE;
               break;
           }
       }
       if (NetLink == TRUE)
       {
           TcpConnect();
       }
       iot_os_delete_task(TestTask_HANDLE);
   }
   static void NetWorkCb(E_OPENAT_NETWORK_STATE state)
   {
       NetWorkCbMessage = state;
   }
   //main函数
   int appimg_enter(void *param)
   {
       //系统休眠
       iot_os_sleep(10000);
       //注册网络状态回调函数
       iot_network_set_cb(NetWorkCb);
       //创建一个任务
       //TestTask_HANDLE =
       TestTask_HANDLE = iot_os_create_task(TestTask, NULL, 2048, 10, OPENAT_OS_CREATE_DEFAULT, "TestTask");
       return 0;
   }

   //退出提示
   void appimg_exit(void)
   {
       OSI_LOGI(0, "application image exit");
   }

  查看输出,发现接收到的数据和发送的数据一致,没有任何问题。 |image3|

   不会下载的\ `点击这里 <https://blog.csdn.net/weixin_44570083/article/details/104285283>`__\ ,进去查看我的\ ``RDA8910 CSDK二次开发入门教程``\ 专题第一篇博文\ ``1、RDA8910CSDK二次开发:环境搭建``\ 里面讲了怎么下载
   这里只是我的学习笔记,拿出来给大家分享,欢迎大家批评指正,本篇教程到此结束

.. |在这里插入图片描述| image:: https://img-blog.csdnimg.cn/20200526200256514.png
.. |image1| image:: https://img-blog.csdnimg.cn/20200526200214752.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70
.. |image2| image:: https://img-blog.csdnimg.cn/20200526202250497.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70
.. |image3| image:: https://img-blog.csdnimg.cn/20200526204603206.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDU3MDA4Mw==,size_16,color_FFFFFF,t_70