SAL全稱Socket Abstract Layer,即套接字抽象層,主要做用是對上層應用提供一層統一的 socket 編程接口,屏蔽底層網絡硬件的差別。linux
LiteOS的SAL架構以下:macos
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
文件夾中,如圖:app
sal.h
:SAL頭文件,使用時需包含;sal_imp.h
:抽象接口定義頭文件;sal_types.h
:socket編程中涉及到的類型定義;sal_define.h
:socket編程中涉及到的宏定義;link_endian.h
:socket編程中的大小端字節序轉換函數定義;Socket稱爲套接字,本質上是一種文件描述符,因此socket通訊的過程和操做文件的方法基本相似。框架
TCP/IP協議族的傳輸層中,分爲有鏈接的,可靠的TCP傳輸方式,和無鏈接的,不可靠的UDP傳輸方式,因此Socket分爲兩種:dom
一個標準的Socket應該包括如下五部分:socket
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 */ };
參數說明以下:
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類型傳遞。
在sockaddr_in結構體中填寫sin_port和sin_addr這兩個值時,須要注意:
in_port_t
是uint16_t
類型;sin_addr
是uint32_t
類型;這樣就涉及到了兩個轉換問題:
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);
本實驗中咱們使用ESP8266 AT框架 SAL進行實驗,因此須要開啓使能AT框架和SAL。
關於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
:
SAL默認是未開啓的,須要在工程目錄下的.sdkconfig
中手動配置開啓:
其中CONFIG_TCPIP_ENABLE = y
須要本身添加,CONFIG_TCPIP_TYPE
宏定義的值目前支持,能夠根據本身的需求選擇:
注意:兩個宏定義必須同時存在且使能,SAL纔會生效。
使能了SAL以後,系統會自動進行初始化,在SDK目錄中的IoT_LINK_1.0.0\iot_link
下的link_main.c
文件中便可看到:
在本實驗中,TCP Server使用網絡調試助手模擬,在本機8000端口開啓一個TCP服務器,如圖:
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則表示失敗 |
API原型以下:
int sal_connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
參數說明以下:
參數 | 說明 |
---|---|
sockfd | 建立成功的sockfd描述符 |
addr | sockaddr結構體指針 |
addrlen | sockaddr結構體長度 |
API原型以下:
int sal_send(int sockfd,const void *buf,size_t len,int flags);
參數說明以下:
參數 | 說明 |
---|---|
sockfd | 建立成功的sockfd描述符 |
buf | 發送數據 |
len | 發送數據長度 |
flags | 發送或接收標記,通常都設爲0 |
API原型以下:
int sal_recv(int sockfd,void *buf,size_t len,int flags);
參數說明以下:
參數 | 說明 |
---|---|
sockfd | 建立成功的sockfd描述符 |
buf | 接收數據緩衝區 |
len | 接收數據緩衝區長度 |
flags | 發送或接收標記,通常都設爲0 |
API原型以下:
int sal_closesocket(int sockfd);
參數說明以下:
參數 | 說明 |
---|---|
sockfd | 建立成功的sockfd描述符 |
打開以前建立的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文件:
而後編譯,下載,便可看到串口輸出(前提是確保TCP服務器已開啓):
在TCP服務端軟件也能夠看到:
在服務端發送數據,在串口能夠看到客戶端已接收:
在本實驗中,UDP Server使用網絡調試助手模擬,在本機8000端口開啓一個UDP服務器,如圖:
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結構體長度 |
API原型以下:
int sal_closesocket(int sockfd);
參數說明以下:
參數 | 說明 |
---|---|
sockfd | 建立成功的sockfd描述符 |
打開以前建立的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文件:
而後編譯,下載,便可看到串口輸出(前提是確保UDP服務器已開啓):
在UDP服務端軟件也能夠看到:
在服務端發送數據,在串口能夠看到客戶端已接收: