发 帖  
原厂入驻New

LiteOS通信模组教程05-LiteOS的SAL及socket编程实例

2020-2-26 09:33:58  414 单片机 物联网 华为云 小熊派BearPi Liteos
分享
0
本帖最后由 小熊派开源社区 于 2020-2-26 09:38 编辑

1. SAL套接字抽象层
SAL全称Socket Abstract Layer,即套接字抽象层,主要作用是对上层应用提供一层统一的 socket 编程接口,屏蔽底层网络硬件的差异。
LiteOS的SAL架构如下:
SAL的优势从图中一看即知:
无论底层使用以太网+LwIP协议栈组合,还是使用ESP8266/M26+AT框架组合,经过SAL套接字抽象层之后,对用户提供的接口都是统一的,极大的提高了程序的可移植性。
SAL框架的源码及其实现在SDK中的IoT_LINK_1.0.0\iot_link\network\tcpip目录:

除了sal文件夹之外,其余的文件夹分别对应着不同的sal实现,比如esp8266_socket对应的是基于AT框架和ESP8266的SAL实现。
SAL相关的头文件存放在IoT_LINK_1.0.0\iot_link\inc文件夹中,如图:

  • sal.h:SAL头文件,使用时需包含;
  • sal_imp.h:抽象接口定义头文件;
  • sal_types.h:socket编程中涉及到的类型定义;
  • sal_define.h:socket编程中涉及到的宏定义;
  • link_endian.h:socket编程中的大小端字节序转换函数定义;

2. Socket编程基础2.1. Socket概述
Socket称为套接字,本质上是一种文件描述符,所以socket通信的过程和操作文件的方法基本类似。
TCP/IP协议族的传输层中,分为有连接的,可靠的TCP传输方式,和无连接的,不可靠的UDP传输方式,所以Socket分为两种:
  • 流式Socket(SOCK_STREAM):提供可靠的、面向连接的通信流,使用TCP协议;
  • 数据报Socket(SOCK_DGRAM):提供一种无连接的服务,使用UDP协议;

2.2. Socket结构体
一个标准的Socket应该包括以下五部分:
  • 协议类型
  • 目的IP
  • 目的端口
  • 源ip
  • 源端口
SAL提供了两种socket的结构体用于存放数据,sockaddr结构体和sockaddr_in结构体,定义均在sal_types.h文件中。
sockaddr结构体的定义如下:
  1. struct sockaddr
  2. {
  3.     sa_family_t     sa_family;      /* address family, AF_xxx   */
  4.     char            sa_data[14];    /* 14 bytes of protocol address */
  5. };
复制代码
参数说明如下:
  • sa_family:地址族,一般为AF_INET,表示ipv4协议;
  • sa_data:包含了源ip、源端口、目的ip、目的端口;

sockaddr_in结构体的定义如下:
  1. struct sockaddr_in
  2. {
  3.     sa_family_t sin_family;             /* AF_INET */
  4.     in_port_t sin_port;                 /* Port number.  */
  5.     struct in_addr sin_addr;            /* Internet address.  */
  6.     unsigned char sin_zero[8];          /* Pad to size of `struct sockaddr'.  */
  7. };
复制代码
sockaddr结构体将所有的ip和端口信息都放在了sa_data中,不利用编程,而sockaddr_in结构体本质上和sockaddr结构体一样,但是将目的ip和目的端口分离出来,容易编程,所以一般在使用的时候有如下技巧:
使用sockaddr_in结构体赋值,作为参数传递时强制转换为sockaddr类型传递。

2.3. 字节序转换函数
在sockaddr_in结构体中填写sin_port和sin_addr这两个值时,需要注意:
  • in_port_t是uint16_t类型;
  • sin_addr是uint32_t类型;
这样就涉及到了两个转换问题:
  • ip地址的转换
ip地址通常是一个字符串,比如"192.168.1.100",但是此处需要转换为一个uint32_t类型的数据,SAL提供了一个转换函数,在之前提到的link_endian.h文件中。
  • 字节序的转换
字节序分为大端存储和小端存储,为了保证统一性,屏蔽硬件差异,需要将ip地址和端口的值转换为网络字节序,SAL提供了本地字节序和网络字节序的互相转换函数,在link_endian.h文件中,其中h表示host主机,n表示network网络字节序:
  1. htonl(unsigned long int hostlong);
  2. htons(unisgned short int hostshort);
  3. ntohl(unsigned long int netlong);
  4. ntohs(unsigned short int netshort);
复制代码
3. AT框架和SAL配置及开启
本实验中我们使用ESP8266+AT框架+SAL进行实验,所以需要开启使能AT框架和SAL。

3.1. AT框架开启
关于AT框架具体的剖析,可以阅读上一篇教程。
在工程目录下的.sdkconfig中手动配置开启驱动框架(串口使用)和AT框架:

实验中使用的是ESP8266,所以还需要配置路由器的SSID和PASSWD,在SDK目录中的IoT_LINK_1.0.0\iot_link\network\tcpip\esp8266_socket目录下, 打开esp8266_socket_imp.h文件:
在其中设置ESP8266连接的热点名称和密码,这里我的设置如下:

最后,需要修改同文件夹下的esp8266_socket_imp.mk文件,将图中标出的两处TOP_DIR改为SDK_DIR:

3.2. SAL开启
SAL默认是未开启的,需要在工程目录下的.sdkconfig中手动配置开启:

其中CONFIG_TCPIP_ENABLE = y需要自己添加,CONFIG_TCPIP_TYPE宏定义的值目前支持,可以根据自己的需求选择:
  • "lwip_socket"
  • "linux_socket"
  • "macos_socket"
  • "esp8266_socket"
  • "none"
注意:两个宏定义必须同时存在且使能,SAL才会生效。

3.3. SAL自动初始化
使能了SAL之后,系统会自动进行初始化,在SDK目录中的IoT_LINK_1.0.0\iot_link下的link_main.c文件中即可看到:

4. TCP Socket客户端编程实例

4.1. TCP服务端的建立
在本实验中,TCP Server使用网络调试助手模拟,在本机8000端口开启一个TCP服务器,如图:

4.2. SAL提供的Socket客户端编程API

建立socket
API原型如下:
  1. int sal_socket(int domain, int type, int protocol);
复制代码
参数说明如下:
参数
说明
常用值
domain
协议或地址族
AF_INET,表示IPv4
type
socket类型
SOCK_STREAM,表示TCP

SOCK_DGRAM,表示UDP
protocol
使用的协议号
0,表示使用默认协议号
返回值
socket描述符
int类型值,-1则表示失败

连接服务器socket
API原型如下:
  1. int sal_connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
复制代码
参数说明如下:
参数
说明
sockfd
创建成功的sockfd描述符
addr
sockaddr结构体指针
addrlen
sockaddr结构体长度

socket发送数据
API原型如下:
  1. int sal_send(int sockfd,const void *buf,size_t len,int flags);
复制代码
参数说明如下:
参数
说明
sockfd
创建成功的sockfd描述符
buf
发送数据
len
发送数据长度
flags
发送或接收标记,一般都设为0

socket接收数据(非堵塞)
API原型如下:
  1. int sal_recv(int sockfd,void *buf,size_t len,int flags);
复制代码
参数说明如下:
参数
说明
sockfd
创建成功的sockfd描述符
buf
接收数据缓冲区
len
接收数据缓冲区长度
flags
发送或接收标记,一般都设为0

关闭socket
API原型如下:
  1. int sal_closesocket(int sockfd);
复制代码
参数说明如下:
参数
说明
sockfd
创建成功的sockfd描述符

4.3. 基于SAL的TCP客户端编程
打开之前创建的HelloWorld工程(如果没有,可以参考第一篇教程新建),创建下面的文件夹sal_test_demo,并在该文件夹中新建一个测试文件sal_tcp_demo.c:

编辑以下内容:
注意,其中的server_ip和server_port应该是服务器的实际情况相对应!
  1. #include <osal.h>
  2. #include <sal.h>

  3. #define server_port 8000
  4. #define server_ip   "192.168.0.101"

  5. static int sal_tcp_demo_entry()
  6. {
  7.     int sockfd;

  8.     /* 创建TCP socket */
  9.     sockfd = sal_socket(AF_INET, SOCK_STREAM, 0);
  10.     IF(sockfd < 0)
  11.     {
  12.         printf("TCP Socket create fail.\r\n");
  13.         return -1;
  14.     }
  15.     else
  16.     {
  17.         printf("TCP Socket create ok.\r\n");
  18.     }

  19.     /* 连接服务器 */
  20.     struct sockaddr_in server_addr;
  21.     server_addr.sin_family = AF_INET;
  22.     server_addr.sin_port = htons(server_port);
  23.     server_addr.sin_addr.s_addr = inet_addr(server_ip);
  24.     while(-1 == sal_connect(sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)))
  25.     {
  26.         //连接失败,则1s后自动重连
  27.         printf("connect server fail, repeat...\r\n");
  28.         osal_task_sleep(1000);
  29.     }
  30.     printf("connect server ok.\r\n");
  31.    
  32.     int nbytes;
  33.     char buf[] = "hello server!";
  34.     //发送数据到服务器
  35.     nbytes = sal_send(sockfd, buf, sizeof(buf), 0);
  36.     if(nbytes < 0)
  37.     {
  38.         printf("send dat %s fail.\r\n", buf);
  39.         return -1;
  40.     }
  41.     else
  42.     {
  43.         printf("send [%d] bytes: %s.\r\n", nbytes , buf);
  44.     }

  45.     //等待接收服务器数据
  46.     char recv_buf[50]={0};
  47.     while( -1 == (nbytes = sal_recv(sockfd, recv_buf, 50, 0)));
  48.     printf("recv [%d] bytes: %s.\r\n", nbytes, recv_buf);

  49.     //关闭socket
  50.     sal_closesocket(sockfd);
  51.     printf("TCP socket closed.\r\n");

  52.     return 0;
  53. }

  54. int standard_app_demo_main()
  55. {
  56.     osal_task_create("sal_tcp_demo",sal_tcp_demo_entry,NULL,0x800,NULL,12);
  57.     return 0;
  58. }
复制代码
然后在user_demo.mk中添加文件路径:
        
  1. #example for sal_tcp_demo
  2.         ifeq ($(CONFIG_USER_DEMO), "sal_tcp_demo")        
  3.                 user_demo_src  = ${wildcard $(TOP_DIR)/targets/STM32L431_BearPi/Demos/sal_test_demo/sal_tcp_demo.c}
  4.         endif
复制代码

位置如下:

最后在.sdkconfig中配置选中该demo文件:

然后编译,下载,即可看到串口输出(前提是确保TCP服务器已开启):

在TCP服务端软件也可以看到:

在服务端发送数据,在串口可以看到客户端已接收:

5. UDP Socket客户端编程实例

5.1. UDP服务端的建立
在本实验中,UDP Server使用网络调试助手模拟,在本机8000端口开启一个UDP服务器,如图:
5.2. SAL提供的Socket客户端编程API

连接服务器socket
API原型如下:
  1. int sal_connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
复制代码
参数说明如下:
参数
说明
sockfd
创建成功的sockfd描述符
addr
sockaddr结构体指针
addrlen
sockaddr结构体长度

发送数据
API原型如下:
  1. int sal_sendto(int sockfd, const void *dataptr, size_t size, int flags,
  2.     const struct sockaddr *to, socklen_t tolen);
复制代码
参数说明如下:
参数
说明
sockfd
创建成功的sockfd描述符
dataptr
待发送的数据指针
size
发送包数据大小
flags
发送或接收标记,一般都设为0
addr
sockaddr结构体指针
addrlen
sockaddr结构体长度

接收数据
API原型如下:
  1. int sal_recvfrom(int sockfd, void *mem, size_t len, int flags,
  2.       struct sockaddr *from, socklen_t *fromlen);
复制代码
参数说明如下:
参数
说明
sockfd
创建成功的sockfd描述符
mem
接收缓冲区数据指针
size
接收数据大小
flags
发送或接收标记,一般都设为0
addr
sockaddr结构体指针
addrlen
sockaddr结构体长度

关闭socket
API原型如下:
  1. int sal_closesocket(int sockfd);
复制代码
参数说明如下:
参数
说明
sockfd
创建成功的sockfd描述符

5.3. 基于SAL的UDP客户端编程
打开之前创建的HelloWorld工程(如果没有,可以参考第一篇教程新建),创建下面的文件夹sal_test_demo,并在该文件夹中新建一个测试文件sal_udp_demo.c:

编辑以下内容:
注意,其中的server_ip和server_port应该是服务器的实际情况相对应!
  1. #include <osal.h>
  2. #include <sal.h>

  3. #define server_port 8000
  4. #define server_ip   "192.168.0.101"

  5. static int sal_udp_demo_entry()
  6. {
  7.     int sockfd;

  8.     /* 创建udp socket */
  9.     sockfd = sal_socket(AF_INET, SOCK_DGRAM, 0);
  10.     if(sockfd < 0)
  11.     {
  12.         printf("udp Socket create fail.\r\n");
  13.         return -1;
  14.     }
  15.     else
  16.     {
  17.         printf("udp Socket create ok.\r\n");
  18.     }

  19.     /* 服务端信息 */
  20.     struct sockaddr_in server_addr;
  21.     server_addr.sin_family = AF_INET;
  22.     server_addr.sin_port = htons(server_port);
  23.     server_addr.sin_addr.s_addr = inet_addr(server_ip);

  24.     /* 发送数据到服务器 */
  25.     int nbytes;
  26.     char buf[] = "hello server!";
  27.     nbytes = sal_sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
  28.     if(nbytes < 0)
  29.     {
  30.         printf("send dat %s fail.\r\n", buf);
  31.         return -1;
  32.     }
  33.     else
  34.     {
  35.         printf("send [%d] bytes: %s.\r\n", nbytes , buf);
  36.     }

  37.     /* 等待接收服务器数据 */
  38.     char recv_buf[50]={0};
  39.     while( -1 == (nbytes = sal_recvfrom(sockfd, recv_buf, 50, 0,  (struct sockaddr*)&server_addr, sizeof(struct sockaddr))));
  40.     printf("recv [%d] bytes: %s.\r\n", nbytes, recv_buf);

  41.     /* 关闭socket */
  42.     sal_closesocket(sockfd);
  43.     printf("udp socket closed.\r\n");

  44.     return 0;
  45. }

  46. int standard_app_demo_main()
  47. {
  48.     osal_task_create("sal_udp_demo",sal_udp_demo_entry,NULL,0x800,NULL,12);
  49.     return 0;
  50. }
复制代码
然后在user_demo.mk中添加文件路径:
        
  1. #example for sal_udp_demo
  2.         ifeq ($(CONFIG_USER_DEMO), "sal_udp_demo")        
  3.                 user_demo_src  = ${wildcard $(TOP_DIR)/targets/STM32L431_BearPi/Demos/sal_test_demo/sal_udp_demo.c}
  4.         endif
复制代码

位置如下:

最后在.sdkconfig中配置选中该demo文件:

然后编译,下载,即可看到串口输出(前提是确保UDP服务器已开启):

在UDP服务端软件也可以看到:

在服务端发送数据,在串口可以看到客户端已接收:






关注“小熊派开源社区”微信公众号,回复“ 通信模组  ”获取 工具和文档  。
-------------------------------------END--------------------------------------

评论

高级模式
您需要登录后才可以回帖 登录 | 注册

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
发经验
关闭

站长推荐 上一条 /7 下一条

快速回复 返回顶部 返回列表