BSD Sockets
概述
Zephyr 提供了 BSD Sockets API(POSIX 标准的一部分)的一个子集实现。该 API 允许复用现有的编程经验,并将现有简单的网络应用移植到 Zephyr。
以下是指导 Zephyr 的 BSD Sockets 兼容 API 实现的关键要求和概念:
开销最小,与其他 Zephyr 子系统的要求类似。
默认采用命名空间,以避免与 libc 或其他 POSIX 兼容库中可能存在的
close()等知名名称冲突。如果通过CONFIG_POSIX_API启用,它还会暴露 POSIX 兼容 API。
BSD Sockets 兼容 API 通过 CONFIG_NET_SOCKETS 配置选项启用,并实现以下操作: socket()、close()、recv()、recvfrom()、send()、sendto()、connect()、bind()、listen()、accept()、fcntl() (用于设置非阻塞模式)、getsockopt()、setsockopt()、poll()、select()、getaddrinfo()、getnameinfo()。
基于上述命名空间要求,这些操作默认以带 zsock_ 前缀的函数形式暴露,例如 zsock_socket() 和 zsock_close()。如果定义了配置选项 CONFIG_POSIX_API,所有函数还会以不带前缀的别名形式暴露。这包括 close() 和 fcntl() 之类的函数(它们可能与 libc 或其他库中的函数冲突,例如与文件系统库冲突)。
上述设计要求的另一个推论是,Zephyr API 会尽可能积极地利用 POSIX API 的短读/短写特性(以降低复杂度和开销)。POSIX 允许 recv() 和 send() 之类的调用实际处理(接收或发送)的数据少于用户请求的数据量(在 SOCK_STREAM 类型的 socket 上)。例如,调用 recv(sock, 1000, 0) 可能返回 100,表示只读取了 100 字节(短读),应用需要重试调用以接收剩余的 900 字节。
BSD Sockets API 使用文件描述符表示 socket。文件描述符是小整数,从零开始连续分配,在 socket、文件、特殊设备(如 stdin/stdout)等之间共享。在内部,有一个将文件描述符映射到内部对象指针的表。即使 POSIX 子系统的其余部分(文件系统、stdin/stdout)未启用,BSD Sockets API 也会使用该文件描述符表。
Zephyr 支持多种类型的 BSD socket,下表总结了可用的 socket 类型:
地址族 |
类型 |
协议 |
描述 |
|---|---|---|---|
AF_INET |
SOCK_DGRAM |
IPPROTO_UDP |
如果设置了 |
IPPROTO_DTLS_1_x |
如果设置了 |
||
SOCK_STREAM |
IPPROTO_TCP |
如果设置了 |
|
IPPROTO_TLS_1_x |
如果设置了 |
||
SOCK_RAW |
IPPROTO_IP |
如果设置了 |
|
AF_PACKET |
SOCK_DGRAM |
ETH_P_ALL |
如果设置了 |
SOCK_RAW |
ETH_P_ALL |
如果设置了 |
|
AF_CAN |
SOCK_RAW |
CAN_RAW |
如果设置了 |
请参阅 Echo server (advanced) 和 Echo client (advanced) 示例应用,了解如何创建基于 BSD socket 的简单服务器或客户端应用。
IPv4 和 IPv6 socket 选项
Zephyr 通过 zsock_setsockopt() 和 zsock_getsockopt(),在 NET_IPPROTO_IP 协议级别(IPv4)和 NET_IPPROTO_IPV6 协议级别(IPv6)支持 IP 级 socket 选项。选项的可用性可能取决于 Kconfig 设置和 socket 地址族。
IPv4 选项
IPv6 选项
QUIC 协议栈在 DPLPMTUD 探测期间会在内部使用 ZSOCK_IP_DONTFRAG 和 ZSOCK_IPV6_DONTFRAG 选项(参见 路径 MTU 发现(DPLPMTUD))。
安全 socket
Zephyr 提供了标准 POSIX socket API 的扩展,允许创建和配置使用 TLS 协议类型的 socket,从而实现安全通信。实现所需的安全函数由 Mbed TLS 库提供。安全 socket 实现允许通过标准 socket 调用使用 TLS 和 DTLS 两种协议。有关支持的安全协议版本,请参见 net_ip_protocol_secure 类型。
要启用安全 socket,请设置 CONFIG_NET_SOCKETS_SOCKOPT_TLS 选项。要启用 DTLS 支持,请使用 CONFIG_NET_SOCKETS_ENABLE_DTLS 选项。
TLS 凭据子系统
TLS 凭据必须先注册到系统中,然后才能用于安全 socket。更多信息请参见 tls_credential_add()。
当特定 TLS 凭据注册到系统时,会为其分配一个 sec_tag_t 类型的数值,称为标签。之后在通过 socket 选项配置安全 socket 时,可以使用该值来引用此凭据。
可以在系统中注册以下 TLS 凭据类型:
TLS_CREDENTIAL_CA_CERTIFICATETLS_CREDENTIAL_PUBLIC_CERTIFICATETLS_CREDENTIAL_PRIVATE_KEYTLS_CREDENTIAL_PSKTLS_CREDENTIAL_PSK_ID
CA 证书的注册示例(在 ca_certificate 数组中提供)如下所示:
ret = tls_credential_add(CA_CERTIFICATE_TAG, TLS_CREDENTIAL_CA_CERTIFICATE,
ca_certificate, sizeof(ca_certificate));
默认支持 DER 格式的证书。可以在 Mbed TLS 设置中启用 PEM 支持。
安全 socket 的创建
可以通过指定安全协议类型来创建安全 socket,例如:
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TLS_1_2);
创建安全 socket 时指定的协议版本表示该 TLS 会话使用的最低 TLS 版本。
创建后,可以使用 socket 选项对其进行配置。例如,可以设置 CA 证书和主机名:
sec_tag_t sec_tag_opt[] = {
CA_CERTIFICATE_TAG,
};
ret = setsockopt(sock, SOL_TLS, TLS_SEC_TAG_LIST,
sec_tag_opt, sizeof(sec_tag_opt));
char host[] = "google.com";
ret = setsockopt(sock, SOL_TLS, TLS_HOSTNAME, host, sizeof(host));
配置完成后,该 socket 就可以像普通 TCP socket 一样使用。
备注
由于 mbed TLS 内部数据缓冲以及 mbedtls_ssl_write() 函数的要求,当非阻塞的 zsock_send() 返回 EAGAIN 时,后续对 zsock_send() 的调用应包含与原始调用相同的数据。
Zephyr 中有多个示例使用安全 socket 进行通信。示例用法请参见 echo-server 示例应用 或 HTTP GET 示例应用。
安全 socket 选项
安全 socket 提供以下用于 socket 管理的选项:
socket 卸载
Zephyr 允许注册自定义 socket 实现(称为卸载 socket)。这样可以将提供外部 IP 协议栈并暴露类 socket API 的设备无缝集成进来。
socket 卸载可以通过 CONFIG_NET_SOCKETS_OFFLOAD 选项启用。想要注册新 socket 实现的网络驱动应使用 NET_SOCKET_OFFLOAD_REGISTER 宏。该宏接受以下参数:
socket_namesocket 实现的任意名称。
priosocket 实现的优先级。优先级越高,创建新 socket 时该实现被处理的顺序越靠前。数值越小表示优先级越高。
_family卸载 socket 所实现的 socket 地址族。
AF_UNSPEC表示任意地址族。
_is_supported过滤函数,用于验证卸载 socket 实现是否支持特定的 socket 地址族、类型和协议。
_handler与
socket()API 兼容的函数,用于创建卸载 socket。
每个卸载 socket 实现还应实现一组 socket API,这些 API 在 socket_op_vtable 结构体中指定。
为创建 socket 而注册的函数应使用 zvfs_reserve_fd() 函数分配新的文件描述符。特定于某个卸载 socket 实现创建过程的任何附加操作,都应在分配文件描述符之后执行。最后,如果卸载 socket 创建成功,应使用 zvfs_finalize_typed_fd() 或 zvfs_finalize_fd() 函数完成文件描述符的最终化处理。finalize 函数允许为卸载 socket 注册一个实现 socket API 的 socket_op_vtable 结构体,并可附加一个可选的 socket 上下文数据指针。
最后,当卸载网络接口初始化时,应使用 net_if_socket_offload_set() 函数指明该接口已卸载。该函数会在网络接口上注册用于创建卸载 socket 的函数(与 NET_SOCKET_OFFLOAD_REGISTER 中提供的函数相同)。
卸载 socket 的创建
当应用使用 socket() 函数创建新 socket 时,网络协议栈会遍历所有已注册的 socket 实现(原生和卸载)。优先级较高的 socket 实现会先被处理。对于每个已注册的 socket 实现,会验证地址族,如果匹配(或者该 socket 注册为 AF_UNSPEC),则调用相应的 _is_supported 函数来验证其余 socket 参数。第一个满足 socket 要求的实现(即 _is_supported 返回 true)将使用其 _handler 函数创建新 socket。
以上说明了 socket 优先级的重要性。如果多个 socket 实现支持相同的 socket 地址族/类型/协议组合,系统处理的第一个实现将创建 socket。因此,为应作为系统默认的实现赋予最高优先级非常重要。
原生 socket 实现的 socket 优先级通过 Kconfig 配置。使用 CONFIG_NET_SOCKETS_TLS_PRIORITY 设置原生 TLS socket 的优先级。使用 CONFIG_NET_SOCKETS_PRIORITY_DEFAULT 设置其余原生 socket 的优先级。
处理多个卸载接口
由于 socket() 函数不允许指定 socket 应使用哪个网络接口,因此当存在多个支持相同 socket 类型的卸载 socket 实现时,无法选择特定的实现。当系统中同时存在原生和卸载 socket 时,也会出现同样的问题。
为了解决此问题,引入了一种特殊的 socket 实现(称为 socket 调度器)。该模块的唯一目的是将 socket 的创建推迟到对 socket 执行第一个操作时。这样就留出了使用 SO_BINDTODEVICE socket 选项将 socket 绑定到特定网络接口(从而绑定到卸载 socket 实现)的机会。可以通过 CONFIG_NET_SOCKETS_OFFLOAD_DISPATCHER Kconfig 选项启用 socket 调度器。
启用后,应用可以使用 setsockopt() 函数指定要使用的网络接口:
/* A "dispatcher" socket is created */
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
struct ifreq ifreq = {
.ifr_name = "SimpleLink"
};
/* The socket is "dispatched" to a particular network interface
* (offloaded or not).
*/
setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, &ifreq, sizeof(ifreq));
类似地,如果原生和卸载 socket 都支持 TLS,可以使用 TLS_NATIVE socket 选项来指示应创建原生 TLS socket。随后,可以将底层 socket 绑定到特定的网络接口:
/* A "dispatcher" socket is created */
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TLS_1_2);
int tls_native = 1;
/* The socket is "dispatched" to a native TLS socket implmeentation.
* The underlying socket is a "dispatcher" socket now.
*/
setsockopt(sock, SOL_TLS, TLS_NATIVE, &tls_native, sizeof(tls_native));
struct ifreq ifreq = {
.ifr_name = "SimpleLink"
};
/* The underlying socket is "dispatched" to a particular network interface
* (offloaded or not).
*/
setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, &ifreq, sizeof(ifreq));
如果未对 socket 使用 SO_BINDTODEVICE socket 选项,则在首次调用 socket API 时,将根据默认优先级和过滤规则分派该 socket。