《嵌入式系统 – 玩转ART-Pi开发板(基于RT-Thread系统)》第9章 基于Select/Poll实现并发服务器(一)

本文主要是介绍《嵌入式系统 – 玩转ART-Pi开发板(基于RT-Thread系统)》第9章 基于Select/Poll实现并发服务器(一),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

开发环境:
RT-Thread版本:4.0.3
操作系统:Windows10
RT-Thread Studio版本:2.1.1
开发板MCU:STM32H750XB
LWIP:2.0.2

并发服务器支持多个客户端的同时连接,最大可接入的客户端数取决于内核控制块的个数。当使用Socket API时,要使服务器能够同时支持多个客户端的连接,必须引入多任务机制,为每个连接创建一个单独的任务来处理连接上的数据,多任务可以是多线程或者多进程,这是最常用的并发服务器设计。但是多线程/多进程消耗资源多,处理起来也比较复杂,本文将基于LWIP协议栈的Select/Poll机制实现并发服务器


9.1 IO模型概述

在具体讲解基于Select/Poll机制实现并发服务器之前,我们需要了解IO的相关概念,所谓IO就是,就是数据的读写,一般分为网络IO(本质就是socket读写)和磁盘IO

IO模型大致可以分为:同步阻塞、同步非阻塞、异步、信号驱动

在这里插入图片描述

可细分为5种I/O模型:

1)阻塞I/O,进程处于阻塞模式时,让出CPU,进入休眠状态;
2)非阻塞I/O,非阻塞模式的使用并不普遍,因为非阻塞模式会浪费大量的CPU资源;
3)I/O复用(select和poll),针对批量IP操作时,使用I/O多路复用,非常有好;
4)异步I/O(POSIX的aio_系列函数)
5)信号驱动I/O(SIGIO)

一个输入操作通常包括两个不同的阶段:

1)等待数据准备好;
2)从内核向进程复制数据;

对于一个套接字的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

9.1.1阻塞I/O

阻塞 I/O 模式是最普遍使用的 I/O 模式。一个套接字建立后所处于的模式就是阻塞 I/O 模式。(因为Linux系统默认的IO模式是阻塞模式)。对于一个 UDP 套接字来说,数据就绪的标志比较简单:

(1)已经收到了一整个数据报
(2)没有收到。

而 TCP 这个概念就比较复杂,需要附加一些其他的变量。

最流行的I/O模型是阻塞式I/O(blocking I/O) 模型,默认情况下,所有的套接字都是阻塞的。阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,CPU不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。以数据包套接字为例,如图。

在这里插入图片描述

进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区或者发生错误才返回。最常见的错误是系统调用被信号中断。我们说进程从调用recvfrom开始到它返回的整段时间内是被阻塞的,recvfrom成功返回后,进程开始处理数据报。

9.1.2非阻塞I/O

进程把一个套接口设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。

在这里插入图片描述

前三次调用recvfrom 时没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK 错误。第四次调用 recvfrom 时已有一个数据报准备好,它被复制到应用程序缓冲区,于是recvfrom 成功返回。我们接着处理数据。

当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读(称做 polling(轮询))。应用程序不停的 polling 内核来检查是否 I/O操作已经就绪。这将是一个极浪费 CPU资源的操作。这种模式使用中不是很普遍。
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

9.1.3 I/O复用

在使用 I/O 多路技术的时候,我们调用 select()函数和 poll()函数或epoll函数(2.6内核开始支持),在调用它们的时候阻塞,而不是我们来调用 recvfrom(或recv)的时候阻塞。主要可以调用select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听,可以等待多个描述符就绪。

I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

当我们调用 select函数阻塞的时候,select 函数等待数据报套接字进入读就绪状态。当select函数返回的时候, 也就是套接字可以读取数据的时候。 这时候我们就可以调用 recvfrom函数来将数据拷贝到我们的程序缓冲区中。

对于单个I/O操作,和阻塞模式相比较,select()和poll()或epoll并没有什么高级的地方。而且,在阻塞模式下只需要调用一个函数:读取或发送函数。在使用了多路复用技术后,我们需要调用两个函数了:先调用 select()函数或poll()函数,然后才能进行真正的读写。

多路复用的高级之处在于:它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

在这里插入图片描述

IO 多路技术一般在下面这些情况中被使用:

  • 当一个客户端需要同时处理多个文件描述符的输入输出操作的时候(一般来说是标准的输入输出和网络套接字),I/O 多路复用技术将会有机会得到使用。
  • 当程序需要同时进行多个套接字的操作的时候。
  • 如果一个 TCP 服务器程序同时处理正在侦听网络连接的套接字和已经连接好的套接字。
  • 如果一个服务器程序同时使用 TCP 和 UDP 协议。
  • 如果一个服务器同时使用多种服务并且每种服务可能使用不同的协议(比如 inetd就是这样的)。

9.1.4异步I/O模型

异步I/O(asynchronous I/O)有POSIX规范定义。后来演变成当前POSIX规范的各种早期标准定义的实时函数中存在的差异已经取得一致。一般地说,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到我们自己的缓冲区)完成后通知我们。这种模型与前与前面介绍的信号驱动模型的主要区别在于:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。

在这里插入图片描述

9.1.5信号驱动I/O模型

我们也可以用信号,让内核在描述字就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动I/O(signal-driven I/O)。

我们首先开启套接口的信号驱动I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用立即发回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已经准备好待处理,也可以立即通知主循环,让它读取数据报。

无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间,进程不被阻塞。主循环可以继续执行,只要不时等待来自信号处理函数的通知:既可以是数据已经准备好被处理,也可以是数据报已准备好被读取。

在这里插入图片描述

9.1.6各种模型的比较

各种模型的比较如下图所示,可以看出,前4种模型的主要区别在于第一阶段,因为它们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区起见,进程阻塞与recvfrom 调用,相反。异步I/O模型在这两个阶段都需要处理,从而不同于其他四种模型。

在这里插入图片描述

同步I/O与异步I/O对比
POSIX把这两个术语定义如下:

  • 同步I/O操作(synchronous I/O operation)导致请求进程阻塞,直到I/O操作完成。
  • 异步I/O(asynchronous I/O operation)不导致请求进程阻塞。

根据上述定义,我们前4种模型----阻塞I/O模型、非阻塞I/O模型、I/O复用模型和信号去驱动I/O模型都是同步I/O模型,因为其中真正的I/O操作(recvfrom)将阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配

本文的要将的I/O复用,本质就是select/poll机制。因此,其他IO有兴趣可以去了解。




9.2 RT-thread的网络架构

RT-Thread 的网络框架结构如下所示:

在这里插入图片描述

最顶层是网络应用层,提供一套标准 BSD Socket API ,如 socket、connect 等函数,用于系统中大部分网络开发应用, BSD Socket API已经是网络套接字的事实上的抽象标准。使用BSD Socket API编写应用,不会依赖具体的操作系统,但是底层的具体实现是依赖操作系统的。

第二部分为 SAL 套接字抽象层,通过它 RT-Thread 系统能够适配下层不同的网络协议栈,并提供给上层统一的网络编程接口,方便不同协议栈的接入。套接字抽象层为上层应用层提供接口有:accept、connect、send、recv 等。

第三部分为 netdev 网卡层,主要作用是解决多网卡情况设备网络连接和网络管理相关问题,通过 netdev 网卡层用户可以统一管理各个网卡信息和网络连接状态,并且可以使用统一的网卡调试命令接口。

第四部分为协议栈层,该层包括几种常用的 TCP/IP 协议栈,例如嵌入式开发中常用的轻型 TCP/IP 协议栈 lwIP 以及 RT-Thread 自主研发的 AT Socket 网络功能实现等。这些协议栈或网络功能实现直接和硬件接触,完成数据从网络层到传输层的转化。

最后一层为硬件层,ETH是唯一的有线网络接入方式,其余都是无线网络接入方式,LTE模组,Cat模组,NB-IOT模组这些依赖基站运营商的入网方式,例如 SIM800,EC25,AIR720,L610,N58,M5311 等,这些不同厂家,不同工作频率的模组均可以通过 NET 组件入网;WIFI 这种无需运营商直接提供的网络的入网方式,例如 ESP8266,W60x,AP6212,rw007等。

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

RT-Thread对于不同的协议栈或网络功能实现,网络接口的名称可能各不相同,以 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 接口,下面以 bind函数调用流程为例说明 SAL 组件函数调用方式:

  • bind:SAL 组件对外提供的抽象的 BSD Socket API,用于统一 fd 管理;
  • sal_bind:SAL 组件中 bind实现函数,用于指定端口和网卡(当存在多个网卡的时候)。
  • lwip_bind:底层协议栈提供的层 bind连接函数,在网卡初始化完成时注册到 SAL 组件中,最终调用的操作函数。
/* SAL 组件为应用层提供的标准 BSD Socket API */
int bind(int s, const struct sockaddr *name, socklen_t namelen)
{/* 获取 SAL 套接字描述符 */
int socket = dfs_net_getsocket(s);/* 通过 SAL 套接字描述符执行 sal_bind 函数 */return sal_bind(socket, name, namelen);
}
/* SAL 组件抽象函数接口实现 */
int sal_bind(int socket, const struct sockaddr *name, socklen_t namelen)
{struct sal_socket *sock;struct sal_proto_family *pf;ip_addr_t input_ipaddr;RT_ASSERT(name);/* get the socket object by socket descriptor */SAL_SOCKET_OBJ_GET(sock, socket);/* bind network interface by ip address */sal_sockaddr_to_ipaddr(name, &input_ipaddr);/* check input ipaddr is default netdev ipaddr */if (!ip_addr_isany_val(input_ipaddr)){struct sal_proto_family *input_pf = RT_NULL, *local_pf = RT_NULL;struct netdev *new_netdev = RT_NULL;new_netdev = netdev_get_by_ipaddr(&input_ipaddr);if (new_netdev == RT_NULL){return -1;}/* get input and local ip address proto_family */SAL_NETDEV_SOCKETOPS_VALID(sock->netdev, local_pf, bind);SAL_NETDEV_SOCKETOPS_VALID(new_netdev, input_pf, bind);/* check the network interface protocol family type */if (input_pf->family != local_pf->family){int new_socket = -1;/* protocol family is different, close old socket and create new socket by input ip address */local_pf->skt_ops->closesocket(socket);new_socket = input_pf->skt_ops->socket(input_pf->family, sock->type, sock->protocol);if (new_socket < 0){return -1;}sock->netdev = new_netdev;sock->user_data = (void *) new_socket;}}/* check and get protocol families by the network interface device */SAL_NETDEV_SOCKETOPS_VALID(sock->netdev, pf, bind);return pf->skt_ops->bind((int) sock->user_data, name, namelen);
}
/* lwIP 协议栈函数底层 bind 函数实现 */
int lwip_bind(int s, const struct sockaddr *name, socklen_t namelen)
{struct lwip_sock *sock;ip_addr_t local_addr;u16_t local_port;err_t err;sock = get_socket(s);if (!sock) {return -1;}if (!SOCK_ADDR_TYPE_MATCH(name, sock)) {/* sockaddr does not match socket type (IPv4/IPv6) */sock_set_errno(sock, err_to_errno(ERR_VAL));return -1;}/* check size, family and alignment of 'name' */LWIP_ERROR("lwip_bind: invalid address", (IS_SOCK_ADDR_LEN_VALID(namelen) &&IS_SOCK_ADDR_TYPE_VALID(name) && IS_SOCK_ADDR_ALIGNED(name)),sock_set_errno(sock, err_to_errno(ERR_ARG)); return -1;);LWIP_UNUSED_ARG(namelen);SOCKADDR_TO_IPADDR_PORT(name, &local_addr, local_port);LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_bind(%d, addr=", s));ip_addr_debug_print_val(SOCKETS_DEBUG, local_addr);LWIP_DEBUGF(SOCKETS_DEBUG, (" port=%"U16_F")\n", local_port));#if LWIP_IPV4 && LWIP_IPV6/* Dual-stack: Unmap IPv4 mapped IPv6 addresses */if (IP_IS_V6_VAL(local_addr) && ip6_addr_isipv4mappedipv6(ip_2_ip6(&local_addr))) {unmap_ipv4_mapped_ipv6(ip_2_ip4(&local_addr), ip_2_ip6(&local_addr));IP_SET_TYPE_VAL(local_addr, IPADDR_TYPE_V4);}
#endif /* LWIP_IPV4 && LWIP_IPV6 */err = netconn_bind(sock->conn, &local_addr, local_port);if (err != ERR_OK) {LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_bind(%d) failed, err=%d\n", s, err));sock_set_errno(sock, err_to_errno(err));return -1;}LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_bind(%d) succeeded\n", s));sock_set_errno(sock, 0);return 0;  
}

ART-Pi有两种常用的联网方式,一个是板载的WiFi模块AP6212,这个模块自带蓝牙;另一个是工业扩展板的网口,使用的芯片是LAN8720A。关于多网卡的使用和自动切换在前面的章节有所讲解。本文主要讲解如何使用Select/Poll机制来实现并发服务器

RT-Thread网络组件




欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书


欢迎订阅我的微信公众号

在这里插入图片描述

这篇关于《嵌入式系统 – 玩转ART-Pi开发板(基于RT-Thread系统)》第9章 基于Select/Poll实现并发服务器(一)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1120831

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

基于人工智能的图像分类系统

目录 引言项目背景环境准备 硬件要求软件安装与配置系统设计 系统架构关键技术代码示例 数据预处理模型训练模型预测应用场景结论 1. 引言 图像分类是计算机视觉中的一个重要任务,目标是自动识别图像中的对象类别。通过卷积神经网络(CNN)等深度学习技术,我们可以构建高效的图像分类系统,广泛应用于自动驾驶、医疗影像诊断、监控分析等领域。本文将介绍如何构建一个基于人工智能的图像分类系统,包括环境

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time