SAL 套接字抽象层

143 阅读15分钟

SAL 套接字抽象层

1、SAL 简介

为了适配更多的网络协议栈

类型,避免系统对单一网络协议栈的依赖,RT-Thread 系统提供了一套SAL(套接字抽象层)组件,该组件完成对不同网络协议栈或网络实现接口的抽象并对上层提供一组标准的 BSD Socket API,这样开发者只需要关心和使用网络应用层提供的网络接口

,而无需关心底层具体网络协议栈类型和实现,极大的提高了系统的兼容性,方便开发者完成协议栈的适配和网络相关的开发。

SAL组件主要功能特点:

• 抽象、统一多种网络协议栈接口;
• 提供 Socket 层面的 TLS 加密传输特性;
• 支持标准 BSD Socket API;
• 统一的 FD 管理,便于使用 read/write poll/select 来操作网络功能;

1.1 SAL 网络框架

RT-Thread 的网络框架结构

如下所示:

图 : 网络框架图

最顶层是网络应用层,提供一套标准 BSD Socket API ,如 socket、connect 等函数,用于系统中大部分网络开发应用。

往下第二部分为 SAL 套接字抽象层,通过它 RT-Thread 系统能够适配下层不同的网络协议栈,并提供给上层统一的网络编程

接口,方便不同协议栈的接入。套接字抽象层为上层应用层提供接口有:accept、connect、send、recv 等。

第三部分为 netdev 网卡层,主要作用是解决多网卡情况设备网络连接和网络管理

相关问题,通过netdev 网卡层用户可以统一管理各个网卡信息和网络连接状态,并且可以使用统一的网卡调试命令

接口。

第四部分为协议栈层,该层包括几种常用的 TCP/IP 协议栈,例如嵌入式开发

中常用的轻型 TCP/IP 协议栈 lwIP 以及 RT-Thread 自主研发的 AT Socket 网络功能实现等。这些协议栈或网络功能实现直接和硬件接触,完成数据从网络层到传输层

的转化。

RT-Thread 的网络应用层提供的接口主要以标准 BSD Socket API 为主,这样能确保程序可以在 PC上编写、调试,然后再移植到 RT-Thread 操作系统上。

1.2 工作原理

SAL 组件工作原理的介绍主要分为如下三部分:

• 多协议栈接入与接口函数统一抽象功能;
• SAL TLS 加密传输功能;

1.2.1 多协议栈接入与接口函数统一抽象功能

对于不同的协议栈或网络功能实现,网络接口的名称可能各不相同,以 connect 连接函数

为例,lwIP协议栈中接口名称为 lwip_connect ,而 AT Socket 网络实现中接口名称为 at_connect。SAL 组件提供对不同协议栈或网络实现接口的抽象和统一,组件在 socket 创建时通过判断传入的协议簇

domain)类型来判断使用的协议栈或网络功能,完成 RT-Thread 系统中多协议的接入与使用。

目前 SAL 组件支持的协议栈或网络实现类型有:lwIP 协议栈、AT Socket 协议栈、WIZnet 硬件TCP/IP 协议栈。

int socket(int domain, int type, int protocol);

上述为标准 BSD Socket API 中 socket 创建函数的定义,domain 表示协议域

又称为协议簇(family),用于判断使用哪种协议栈或网络实现,AT Socket 协议栈使用的簇类型为 AF_AT,lwIP 协议栈使用协议簇类型有 AF_INET 等,WIZnet 协议栈使用的协议簇类型为 AF_WIZ

对于不同的软件包

,socket 传入的协议簇类型可能是固定的,不会随着 SAL 组件接入方式的不同而改变。为了动态适配不同协议栈或网络实现的接入,SAL 组件中对于每个协议栈或者网络实现提供两种协议簇类型匹配方式:主协议

簇类型和次协议簇类型。socket 创建时先判断传入协议簇类型是否存在已经支持的主协议类型,如果是则使用对应协议栈或网络实现,如果不是判断次协议簇类型是否支持。目前系统支持协议簇类型如下:

lwIP 协 议 栈: family = AF_INET、sec_family = AF_INET
AT Socket 协 议 栈: family = AF_AT、sec_family = AF_INET
WIZnet 硬 件 TCP/IP 协 议 栈: family = AF_WIZ、sec_family = AF_INET

SAL 组件主要作用是统一抽象底层 BSD Socket API 接口,下面以 connect 函数调用

流程为例说明SAL 组件函数

调用方式:

• connect:SAL 组件对外提供的抽象的 BSD Socket API,用于统一 fd 管理;
• sal_connect:SAL 组件中 connect 实现函数,用于调用底层协议栈注册的 operation 函数。
• lwip_connect:底层协议栈提供的层 connect 连接函数,在网卡初始化完成时注册到 SAL 组件中,最终调用的操作函数。

/* SAL 组 件 为 应 用 层 提 供 的 标 准 BSD Socket API */
int connect(int s, const struct sockaddr *name, socklen_t namelen)
{
/* 获 取 SAL 套 接 字 描 述 符 */
int socket = dfs_net_getsocket(s);
/* 通 过 SAL 套 接 字 描 述 符 执 行 sal_connect 函 数 */
return sal_connect(socket, name, namelen);
}
/* SAL 组 件 抽 象 函 数 接 口 实 现 */
int sal_connect(int socket, const struct sockaddr *name, socklen_t namelen)
{
struct sal_socket *sock;
struct sal_proto_family *pf;
int ret;
/* 检 查 SAL socket 结 构 体 是 否 正 常 */
SAL_SOCKET_OBJ_GET(sock, socket);
/* 检 查 当 前 socket 网 络 连 接 状 态 是 否 正 常 */
SAL_NETDEV_IS_COMMONICABLE(sock->netdev);
/* 检 查 当 前 socket 对 应 的 底 层 operation 函 数 是 否 正 常 */
SAL_NETDEV_SOCKETOPS_VALID(sock->netdev, pf, connect);
/* 执 行 底 层 注 册 的 connect operation 函 数 */
ret = pf->skt_ops->connect((int) sock->user_data, name, namelen);
#ifdef SAL_USING_TLS
if (ret >= 0 && SAL_SOCKOPS_PROTO_TLS_VALID(sock, connect))
{
if (proto_tls->ops->connect(sock->user_data_tls) < 0)
{
return -1;
}
return ret;
}
#endif
return ret;
}
/* lwIP 协 议 栈 函 数 底 层 connect 函 数 实 现 */
int lwip_connect(int socket, const struct sockaddr *name, socklen_t namelen)
{
...
}

1.2.2 SAL TLS 加密传输功能

1)SAL TLS 功能介绍

在 TCP、UDP 等协议数据传输时,由于数据包是明文的,所以很可能被其他人拦截并解析出信息,这给信息的安全传输带来很大的影响。为了解决此类问题,一般需要用户在应用层和传输层之间添加 SSL/TLS 协议。

TLS(Transport Layer Security,传输层安全协议

) 是建立在传输层 TCP 协议之上的协议,其前身是SSL(Secure Socket Layer,安全套接字层),主要作用是将应用层的报文进行非对称加密

后再由 TCP 协议进行传输,实现了数据的加密安全交互。

目前常用的 TLS 方式:MbedTLSOpenSSLs2n 等,但是对于不同的加密方式,需要使用其指定的加密接口和流程进行加密,对于部分应用层协议

的移植较为复杂。因此 SAL TLS 功能产生,主要作用是提供 Socket 层面的 TLS 加密传输特性,抽象多种 TLS 处理方式,提供统一的接口用于完成 TLS 数据交互。

2)SAL TLS 功能使用方式

使用流程如下:

• 配置开启任意网络协议栈支持(如 lwIP 协议栈);
• 配置开启 MbedTLS 软件包(目前只支持 MbedTLS 类型加密方式);
• 配置开启 SAL_TLS 功能支持(如下配置选项章节所示);

配置完成之后,只要在 socket 创建时传入的 protocol 类型使用 PROTOCOL_TLSPROTOCOL_DTLS ,即可使用标准 BSD Socket API 接口,完成 TLS 连接的建立和数据的收发。示例代码如下所示:

#include <stdio.h>
#include <string.h>
#include <rtthread.h>
#include <sys/socket.h>
#include <netdb.h>
/* RT-Thread 官 网, 支 持 TLS 功 能 */
#define SAL_TLS_HOST "www.rt-thread.org"
#define SAL_TLS_PORT 443
#define SAL_TLS_BUFSZ 1024
static const char *send_data = "GET /download/rt-thread.txt HTTP/1.1\r\n"
"Host: www.rt-thread.org\r\n"
"User-Agent: rtthread/4.0.1 rtt\r\n\r\n";
void sal_tls_test(void)
{
int ret, i;
char *recv_data;
struct hostent *host;
int sock = -1, bytes_received;
struct sockaddr_in server_addr;
/* 通 过 函 数 入 口 参 数url获 得host地 址 (如 果 是 域 名, 会 做 域 名 解 析) */
host = gethostbyname(SAL_TLS_HOST);
recv_data = rt_calloc(1, SAL_TLS_BUFSZ);
if (recv_data == RT_NULL)
{
rt_kprintf("No memory\n");
return;
}
/* 创 建 一 个socket, 类 型 是SOCKET_STREAM,TCP 协议, TLS 类 型 */
if ((sock = socket(AF_INET, SOCK_STREAM, PROTOCOL_TLS)) < 0)
{
rt_kprintf("Socket error\n");
goto __exit;
}
/* 初 始 化 预 连 接 的 服 务 端 地 址 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SAL_TLS_PORT);
server_addr.sin_addr = *((struct in_addr *)host->h_addr);
rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) < 0)
{
rt_kprintf("Connect fail!\n");
goto __exit;
}
/* 发 送 数 据 到 socket 连 接 */
ret = send(sock, send_data, strlen(send_data), 0);
if (ret <= 0)
{
rt_kprintf("send error,close the socket.\n");
goto __exit;
}
/* 接 收 并 打 印 响 应 的 数 据, 使 用 加 密 数 据 传 输 */
bytes_received = recv(sock, recv_data, SAL_TLS_BUFSZ - 1, 0);
if (bytes_received <= 0)
{
rt_kprintf("received error,close the socket.\n");
goto __exit;
}
rt_kprintf("recv data:\n");
for (i = 0; i < bytes_received; i++)
{
rt_kprintf("%c", recv_data[i]);
}
__exit:
if (recv_data)
rt_free(recv_data);
if (sock >= 0)
closesocket(sock);
}
#ifdef FINSH_USING_MSH
#include <finsh.h>
MSH_CMD_EXPORT(sal_tls_test, SAL TLS function test);
#endif /* FINSH_USING_MSH */

1.3 配置选项

当我们使用 SAL 组件时需要在 rtconfig.h 中定义如下宏定义

目前 SAL 抽象层支持 lwIP 协议栈、AT Socket 协议栈和 WIZnet 硬件 TCP/IP 协议栈,系统中开启SAL 需要至少开启一种协议栈支持。

上面配置选项可以直接在 rtconfig.h 文件中添加使用,也可以通过组件包管理工具 ENV 配置选项加入,ENV 工具中具体配置路径如下:

RT-Thread Components --->
Network --->
Socket abstraction layer --->
[*] Enable socket abstraction layer
protocol stack implement --->
[ ] Support lwIP stack
[ ] Support AT Commands stack
[ ] Support MbedTLS protocol
[*] Enable BSD socket operated by file system API

配置完成可以通过 scons 命令重新生成功能,完成 SAL 组件的添加。

2、 初始化

配置开启 SAL 选项之后,需要在启动时对它进行初始化,开启 SAL 功能,如果程序中已经使用了组件自动初始化,则不再需要额外进行单独的初始化,否则需要在初始化任务中调用如下函数:

int sal_init(void);

该初始化函数主要是对 SAL 组件进行初始,支持组件重复初始化判断,完成对组件中使用的互斥锁

等资源的初始化。SAL 组件中没有创建新的线程,这也意味着 SAL 组件资源占用极小,目前 SAL 组件资源占用为 ROM 2.8KRAM 0.6K

3、BSD Socket API 介绍

SAL 组件抽象出标准 BSD Socket API 接口,如下是对常用网络接口的介绍:

3.1 创建套接字(socket

int socket(int domain, int type, int protocol);

该函数用于根据指定的地址族、数据类型和协议来分配一个套接字描述符及其所用的资源。

domain / 协议族类型:

• AF_INET:IPv4
• AF_INET6:IPv6

type / 协议类型:

• SOCK_STREAM:流套接字
• SOCK_DGRAM:数据报套接字
• SOCK_RAW:原始套接字

3.2 绑定套接字(bind

int bind(int s, const struct sockaddr *name, socklen_t namelen);

该函数用于将端口号

和 IP 地址绑定带指定套接字上。

SAL 组件依赖 netdev 组件,当使用 bind() 函数时,可以通过 netdev 网卡名称获取网卡对象中 IP地址信息,用于将创建的 Socket 套接字绑定到指定的网卡对象。下面示例完成通过传入的网卡名称绑定该网卡 IP 地址并和服务器进行连接的过程:

#include <rtthread.h>
#include <arpa/inet.h>
#include <netdev.h>
#define SERVER_HOST "192.168.1.123"
#define SERVER_PORT 1234
static int bing_test(int argc, char **argv)
{
struct sockaddr_in client_addr;
struct sockaddr_in server_addr;
struct netdev *netdev = RT_NULL;
int sockfd = -1;
if (argc != 2)
{
rt_kprintf("bind_test [netdev_name] --bind network interface device by name
.\n");
return -RT_ERROR;
}
/* 通 过 名 称 获 取 netdev 网 卡 对 象 */
netdev = netdev_get_by_name(argv[1]);
if (netdev == RT_NULL)
{
rt_kprintf("get network interface device(%s) failed.\n", argv[1]);
return -RT_ERROR;
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
rt_kprintf("Socket create failed.\n");
return -RT_ERROR;
}
/* 初 始 化 需 要 绑 定 的 客 户 端 地 址 */
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(8080);
/* 获 取 网 卡 对 象 中 IP 地 址 信 息 */
client_addr.sin_addr.s_addr = netdev->ip_addr.addr;
rt_memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));
if (bind(sockfd, (struct sockaddr *)&client_addr, sizeof(struct sockaddr)) < 0)
{
rt_kprintf("socket bind failed.\n");
closesocket(sockfd);
return -RT_ERROR;
}
rt_kprintf("socket bind network interface device(%s) success!\n", netdev->name);
/* 初 始 化 预 连 接 的 服 务 端 地 址 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_HOST);
rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
/* 连 接 到 服 务 端 */
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) <
0)
{
rt_kprintf("socket connect failed!\n");
closesocket(sockfd);
return -RT_ERROR;
}
else
{
rt_kprintf("socket connect success!\n");
}
/* 关 闭 连 接 */
closesocket(sockfd);
return RT_EOK;
}
#ifdef FINSH_USING_MSH
#include <finsh.h>
MSH_CMD_EXPORT(bing_test, bind network interface device test);
#endif /* FINSH_USING_MSH */

3.3 监听套接字(listen

int listen(int s, int backlog);

该函数用于 TCP 服务器监听指定套接字连接。

3.4 接收连接(accept

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

当应用程序监听来自其他主机的连接时,使用 accept() 函数初始化连接,accept() 为每个连接创立新的套接字并从监听队列中移除这个连接。

3.5 建立连接(connect

int connect(int s, const struct sockaddr *name, socklen_t namelen);

该函数用于建立与指定 socket 的连接。

3.6 TCP 数据发送(send

int send(int s, const void *dataptr, size_t size, int flags);

该函数常用于 TCP 连接发送数据。

3.7 TCP 数据接收(recv

int recv(int s, void *mem, size_t len, int flags);

该函数用于 TCP 连接接收数据。

3.8 UDP 数据发送(sendto

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

该函数用于 UDP 连接发送数据。

3.9 UDP 数据接收(recvfrom

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

该函数用于 UDP 连接接收数据。

3.10 关闭套接字(closesocket

int closesocket(int s);

该函数用于关闭连接,释放资源。

3.11 按设置关闭套接字(shutdown

int shutdown(int s, int how);

该函数提供更多的权限控制套接字的关闭过程。

how / 套接字控制的方式:

• 0:停止接收当前数据,并拒绝以后的数据接收;
• 1:停止发送数据,并丢弃未发送的数据;
• 2:停止接收和发送数据。

3.12 设置套接字选项(setsockopt

int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);

该函数用于设置套接字模式,修改套接字配置选项。

level / 协议栈配置选项:

• SOL_SOCKET:套接字层
• IPPROTO_TCP:TCP 层
• IPPROTO_IP:IP 层

optname / 需要设置的选项名 :

• SO_KEEPALIVE:设置保持连接选项
• SO_RCVTIMEO:设置套接字数据接收超时
• SO_SNDTIMEO:设置套接数据发送超时

3.13 获取套接字选项(getsockopt

int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen);

该函数用于获取套接字配置选项。

3.14 获取远端地址信息(getpeername

int getpeername(int s, struct sockaddr *name, socklen_t *namelen);

该函数用于获取与套接字相连的远端地址信息。

3.15 获取本地地址信息(getsockname

int getsockname(int s, struct sockaddr *name, socklen_t *namelen);

该函数用于获取本地套接字地址信息。

3.16 配置套接字参数(ioctlsocket

int ioctlsocket(int s, long cmd, void *arg);

该函数用于设置套接字控制模式。

cmd 支持下列命令

• FIONBIO:开启或关闭套接字的非阻塞模式,arg 参数 1 为开启非阻塞,0 为关闭非阻塞。

4、 网络协议栈接入方式

网络协议栈或网络功能实现的接入,主要是对协议簇结构体的初始化和注册处理, 并且添加到 SAL 组件中协议簇列表中,协议簇结构体定义如下:

/* network interface socket opreations */
struct sal_socket_ops
{
int (*socket) (int domain, int type, int protocol);
int (*closesocket)(int s);
int (*bind) (int s, const struct sockaddr *name, socklen_t namelen);
int (*listen) (int s, int backlog);
int (*connect) (int s, const struct sockaddr *name, socklen_t namelen);
int (*accept) (int s, struct sockaddr *addr, socklen_t *addrlen);
int (*sendto) (int s, const void *data, size_t size, int flags, const struct
sockaddr *to, socklen_t tolen);
int (*recvfrom) (int s, void *mem, size_t len, int flags, struct sockaddr *
from, socklen_t *fromlen);
int (*getsockopt) (int s, int level, int optname, void *optval, socklen_t *
optlen);
int (*setsockopt) (int s, int level, int optname, const void *optval, socklen_t
optlen);
int (*shutdown) (int s, int how);
int (*getpeername)(int s, struct sockaddr *name, socklen_t *namelen);
int (*getsockname)(int s, struct sockaddr *name, socklen_t *namelen);
int (*ioctlsocket)(int s, long cmd, void *arg);
#ifdef SAL_USING_POSIX
int (*poll) (struct dfs_fd *file, struct rt_pollreq *req);
#endif
};
/* sal network database name resolving */
struct sal_netdb_ops
{
struct hostent* (*gethostbyname) (const char *name);
int (*gethostbyname_r)(const char *name, struct hostent *ret, char *
buf, size_t buflen, struct hostent **result, int *h_errnop);
int (*getaddrinfo) (const char *nodename, const char *servname,
const struct addrinfo *hints, struct addrinfo **res);
void (*freeaddrinfo) (struct addrinfo *ai);
};
/* 协 议 簇 结 构 体 定 义 */
struct sal_proto_family
{
int family; /* primary protocol families type
*/
int sec_family; /* secondary protocol families type
*/
const struct sal_socket_ops *skt_ops; /* socket opreations */
const struct sal_netdb_ops *netdb_ops; /* network database opreations */
};

• family:每个协议栈支持的主协议簇类型,例如 lwIP 的为 AF_INET ,AT Socket 为 AF_AT,WIZnet为 AF_WIZ。
• sec_family:每个协议栈支持的次协议簇类型,用于支持单个协议栈或网络实现时,匹配软件包中其他类型的协议簇类型。
• skt_ops:定义 socket 相关执行函数,如 connect、send、recv 等,每种协议簇都有一组不同的实现方式。
• netdb_ops:定义非 socket 相关执行函数,如 gethostbyname、getaddrinfo、freeaddrinfo 等,每种协议簇都有一组不同的实现方式。

以下为 AT Socket 网络实现的接入注册流程,开发者可参考实现其他的协议栈或网络实现的接入:

#include <rtthread.h>
#include <netdb.h>
#include <sal.h> /* SAL 组 件 结 构 体 存 放 头 文 件 */
#include <at_socket.h> /* AT Socket 相 关 头 文 件 */
#include <af_inet.h>
#include <netdev.h> /* 网 卡 功 能 相 关 头 文 件 */
#ifdef SAL_USING_POSIX
#include <dfs_poll.h> /* poll 函 数 实 现 相 关 头 文 件 */
#endif
#ifdef SAL_USING_AT
/* 自 定 义 的 poll 执 行 函 数, 用 于 poll 中 处 理 接 收 的 事 件 */
static int at_poll(struct dfs_fd *file, struct rt_pollreq *req)
{
int mask = 0;
struct at_socket *sock;
struct socket *sal_sock;
sal_sock = sal_get_socket((int) file->data);
if(!sal_sock)
{
return -1;
}
sock = at_get_socket((int)sal_sock->user_data);
if (sock != NULL)
{
rt_base_t level;
rt_poll_add(&sock->wait_head, req);
level = rt_hw_interrupt_disable();
if (sock->rcvevent)
{
mask |= POLLIN;
}
if (sock->sendevent)
{
mask |= POLLOUT;
}
if (sock->errevent)
{
mask |= POLLERR;
}
rt_hw_interrupt_enable(level);
}
return mask;
}
#endif
/* 定 义 和 赋 值 Socket 执 行 函 数,SAL 组 件 执 行 相 关 函 数 时 调 用 该 注 册 的 底 层 函 数 */
static const struct proto_ops at_inet_stream_ops =
{
at_socket,
at_closesocket,
at_bind,
NULL,
at_connect,
NULL,
at_sendto,
at_recvfrom,
at_getsockopt,
at_setsockopt,
at_shutdown,
NULL,
NULL,
NULL,
#ifdef SAL_USING_POSIX
at_poll,
#else
NULL,
#endif /* SAL_USING_POSIX */
};
static const struct sal_netdb_ops at_netdb_ops =
{
at_gethostbyname,
NULL,
at_getaddrinfo,
at_freeaddrinfo,
};
/* 定 义 和 赋 值 AT Socket 协 议 簇 结 构 体 */
static const struct sal_proto_family at_inet_family =
{
AF_AT,
AF_INET,
&at_socket_ops,
&at_netdb_ops,
};
/* 用 于 设 置 网 卡 设 备 中 协 议 簇 相 关 信 息 */
int sal_at_netdev_set_pf_info(struct netdev *netdev)
{
RT_ASSERT(netdev);
netdev->sal_user_data = (void *) &at_inet_family;
return 0;
}
#endif /* SAL_USING_AT */