LiteOS通訊模組教程05-LiteOS的SAL及socket編程實例

1. SAL套接字抽象層

SAL全稱Socket Abstract Layer,即套接字抽象層,主要做用是對上層應用提供一層統一的 socket 編程接口,屏蔽底層網絡硬件的差別。linux

LiteOS的SAL架構以下:macos

SAL網絡架構

SAL的優點從圖中一看即知:編程

不管底層使用以太網 LwIP協議棧組合,仍是使用ESP8266/M26 AT框架組合,通過SAL套接字抽象層以後,對用戶提供的接口都是統一的,極大的提升了程序的可移植性。服務器

SAL框架的源碼及其實如今SDK中的IoT_LINK_1.0.0\iot_link\network\tcpip目錄:網絡

SAL實現源碼

除了sal文件夾以外,其他的文件夾分別對應着不一樣的sal實現,好比esp8266_socket對應的是基於AT框架和ESP8266的SAL實現。架構

SAL相關的頭文件存放在IoT_LINK_1.0.0\iot_link\inc文件夾中,如圖:app

SAL頭文件

  • 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分爲兩種:dom

  • 流式Socket(SOCK_STREAM):提供可靠的、面向鏈接的通訊流,使用TCP協議;
  • 數據報Socket(SOCK_DGRAM):提供一種無鏈接的服務,使用UDP協議;

2.2. Socket結構體

一個標準的Socket應該包括如下五部分:socket

  • 協議類型
  • 目的IP
  • 目的端口
  • 源ip
  • 源端口

SAL提供了兩種socket的結構體用於存放數據,sockaddr結構體和sockaddr_in結構體,定義均在sal_types.h文件中。

sockaddr結構體的定義以下:

struct sockaddr
{
    sa_family_t     sa_family;      /* address family, AF_xxx   */
    char            sa_data[14];    /* 14 bytes of protocol address */
};

參數說明以下:

  • sa_family:地址族,通常爲AF_INET,表示IPv4協議;
  • sa_data:包含了源ip、源端口、目的ip、目的端口;

sockaddr_in結構體的定義以下:

struct sockaddr_in
{
    sa_family_t sin_family;             /* AF_INET */
    in_port_t sin_port;                 /* Port number.  */
    struct in_addr sin_addr;            /* Internet address.  */
    unsigned char sin_zero[8];          /* Pad to size of `struct sockaddr'.  */
};

sockaddr結構體將全部的ip和端口信息都放在了sa_data中,不利用編程,而sockaddr_in結構體本質上和sockaddr結構體同樣,可是將目的ip和目的端口分離出來,容易編程,因此通常在使用的時候有以下技巧:

使用sockaddr_in結構體賦值,做爲參數傳遞時強制轉換爲sockaddr類型傳遞

2.3. 字節序轉換函數

在sockaddr_in結構體中填寫sin_port和sin_addr這兩個值時,須要注意:

  • in_port_tuint16_t類型;
  • sin_addruint32_t類型;

這樣就涉及到了兩個轉換問題:

  • ip地址的轉換

ip地址一般是一個字符串,好比"192.168.1.100",可是此處須要轉換爲一個uint32_t類型的數據,SAL提供了一個轉換函數,在以前提到的link_endian.h文件中,函數以下:

  • 字節序的轉換

字節序分爲大端存儲和小端存儲,爲了保證統一性,屏蔽硬件差別,須要將ip地址和端口的值轉換爲網絡字節序,SAL提供了本地字節序和網絡字節序的互相轉換函數,在link_endian.h文件中,其中h表示host主機,n表示network網絡字節序

htonl(unsigned long int hostlong);
htons(unisgned short int hostshort);
ntohl(unsigned long int netlong);
ntohs(unsigned short int netshort);

3. AT框架和SAL配置及開啓

本實驗中咱們使用ESP8266 AT框架 SAL進行實驗,因此須要開啓使能AT框架和SAL。

3.1. AT框架開啓

關於AT框架具體的剖析,能夠閱讀上一篇教程[]()。

在工程目錄下的.sdkconfig中手動配置開啓驅動框架(串口使用)和AT框架:

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:

makefile文件

makefile設置

3.2. SAL開啓

SAL默認是未開啓的,須要在工程目錄下的.sdkconfig中手動配置開啓:

使能esp8266實現的sal

其中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文件中便可看到:

SAL自動初始化

4. TCP Socket客戶端編程實例

4.1. TCP服務端的創建

在本實驗中,TCP Server使用網絡調試助手模擬,在本機8000端口開啓一個TCP服務器,如圖:

TCP服務端

4.2. SAL提供的Socket客戶端編程API

創建socket

API原型以下:

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原型以下:

int sal_connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

參數說明以下:

參數 說明
sockfd 建立成功的sockfd描述符
addr sockaddr結構體指針
addrlen sockaddr結構體長度

socket發送數據

API原型以下:

int sal_send(int sockfd,const void *buf,size_t len,int flags);

參數說明以下:

參數 說明
sockfd 建立成功的sockfd描述符
buf 發送數據
len 發送數據長度
flags 發送或接收標記,通常都設爲0

socket接收數據(非堵塞)

API原型以下:

int sal_recv(int sockfd,void *buf,size_t len,int flags);

參數說明以下:

參數 說明
sockfd 建立成功的sockfd描述符
buf 接收數據緩衝區
len 接收數據緩衝區長度
flags 發送或接收標記,通常都設爲0

關閉socket

API原型以下:

int sal_closesocket(int sockfd);

參數說明以下:

參數 說明
sockfd 建立成功的sockfd描述符

4.3. 基於SAL的TCP客戶端編程

打開以前建立的HelloWorld工程(若是沒有,能夠參考第一篇教程新建),建立下面的文件夾sal_test_demo,並在該文件夾中新建一個測試文件sal_tcp_demo.c

新建實驗文件

編輯如下內容:

注意,其中的server_ip和server_port應該是服務器的實際狀況相對應!
#include <osal.h>
#include <sal.h>

#define server_port 8000
#define server_ip   "192.168.0.101"

static int sal_tcp_demo_entry()
{
    int sockfd;

    /* 建立TCP socket */
    sockfd = sal_socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        printf("TCP Socket create fail.\r\n");
        return -1;
    }
    else
    {
        printf("TCP Socket create ok.\r\n");
    }

    /* 鏈接服務器 */
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    server_addr.sin_addr.s_addr = inet_addr(server_ip);
    while(-1 == sal_connect(sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)))
    {
        //鏈接失敗,則1s後自動重連
        printf("connect server fail, repeat...\r\n");
        osal_task_sleep(1000);
    }
    printf("connect server ok.\r\n");
    
    int nbytes;
    char buf[] = "hello server!";
    //發送數據到服務器
    nbytes = sal_send(sockfd, buf, sizeof(buf), 0);
    if(nbytes < 0)
    {
        printf("send dat %s fail.\r\n", buf);
        return -1;
    }
    else
    {
        printf("send [%d] bytes: %s.\r\n", nbytes , buf);
    }

    //等待接收服務器數據
    char recv_buf[50]={0};
    while( -1 == (nbytes = sal_recv(sockfd, recv_buf, 50, 0)));
    printf("recv [%d] bytes: %s.\r\n", nbytes, recv_buf);

    //關閉socket
    sal_closesocket(sockfd);
    printf("TCP socket closed.\r\n");

    return 0;
}

int standard_app_demo_main()
{
    osal_task_create("sal_tcp_demo",sal_tcp_demo_entry,NULL,0x800,NULL,12);
    return 0;
}

而後在user_demo.mk中添加文件路徑:

#example for sal_tcp_demo
    ifeq ($(CONFIG_USER_DEMO), "sal_tcp_demo")    
        user_demo_src  = ${wildcard $(TOP_DIR)/targets/STM32L431_BearPi/Demos/sal_test_demo/sal_tcp_demo.c}
    endif

位置以下:

添加位置

最後在.sdkconfig中配置選中該demo文件:

配置選中dmeo

而後編譯,下載,便可看到串口輸出(前提是確保TCP服務器已開啓):

串口輸出

在TCP服務端軟件也能夠看到:

服務端信息

在服務端發送數據,在串口能夠看到客戶端已接收:

串口輸出

5. UDP Socket客戶端編程實例

5.1. UDP服務端的創建

在本實驗中,UDP Server使用網絡調試助手模擬,在本機8000端口開啓一個UDP服務器,如圖:

UDP服務端

5.2. SAL提供的Socket客戶端編程API

鏈接服務器socket

API原型以下:

int sal_connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

參數說明以下:

參數 說明
sockfd 建立成功的sockfd描述符
addr sockaddr結構體指針
addrlen sockaddr結構體長度

發送數據

API原型以下:

int sal_sendto(int sockfd, const void *dataptr, size_t size, int flags,
    const struct sockaddr *to, socklen_t tolen);

參數說明以下:

參數 說明
sockfd 建立成功的sockfd描述符
dataptr 待發送的數據指針
size 發送包數據大小
flags 發送或接收標記,通常都設爲0
addr sockaddr結構體指針
addrlen sockaddr結構體長度

接收數據

API原型以下:

int sal_recvfrom(int sockfd, void *mem, size_t len, int flags,
      struct sockaddr *from, socklen_t *fromlen);

參數說明以下:

參數 說明
sockfd 建立成功的sockfd描述符
mem 接收緩衝區數據指針
size 接收數據大小
flags 發送或接收標記,通常都設爲0
addr sockaddr結構體指針
addrlen sockaddr結構體長度

關閉socket

API原型以下:

int sal_closesocket(int sockfd);

參數說明以下:

參數 說明
sockfd 建立成功的sockfd描述符

5.3. 基於SAL的UDP客戶端編程

打開以前建立的HelloWorld工程(若是沒有,能夠參考第一篇教程新建),建立下面的文件夾sal_test_demo,並在該文件夾中新建一個測試文件sal_udp_demo.c

新建實驗文件

編輯如下內容:

注意,其中的server_ip和server_port應該是服務器的實際狀況相對應!
#include <osal.h>
#include <sal.h>

#define server_port 8000
#define server_ip   "192.168.0.101"

static int sal_udp_demo_entry()
{
    int sockfd;

    /* 建立udp socket */
    sockfd = sal_socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        printf("udp Socket create fail.\r\n");
        return -1;
    }
    else
    {
        printf("udp Socket create ok.\r\n");
    }

    /* 服務端信息 */
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    server_addr.sin_addr.s_addr = inet_addr(server_ip);

    /* 發送數據到服務器 */
    int nbytes;
    char buf[] = "hello server!";
    nbytes = sal_sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
    if(nbytes < 0)
    {
        printf("send dat %s fail.\r\n", buf);
        return -1;
    }
    else
    {
        printf("send [%d] bytes: %s.\r\n", nbytes , buf);
    }

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

    /* 關閉socket */
    sal_closesocket(sockfd);
    printf("udp socket closed.\r\n");

    return 0;
}

int standard_app_demo_main()
{
    osal_task_create("sal_udp_demo",sal_udp_demo_entry,NULL,0x800,NULL,12);
    return 0;
}

而後在user_demo.mk中添加文件路徑:

#example for sal_udp_demo
    ifeq ($(CONFIG_USER_DEMO), "sal_udp_demo")    
        user_demo_src  = ${wildcard $(TOP_DIR)/targets/STM32L431_BearPi/Demos/sal_test_demo/sal_udp_demo.c}
    endif

位置以下:

添加位置

最後在.sdkconfig中配置選中該demo文件:

配置選中dmeo

而後編譯,下載,便可看到串口輸出(前提是確保UDP服務器已開啓):

串口輸出

在UDP服務端軟件也能夠看到:

服務端信息

在服務端發送數據,在串口能夠看到客戶端已接收:

串口輸出

相關文章
相關標籤/搜索