基于IOCP机制的考试身份认证系统设计与实现
2021-01-23漆艺
漆 艺
(四川广播电视大学,四川 成都 610000)
一、引言
国家开放大学期末考试是全国性的统一考试,其重要性不言而喻。由于办学性质的特殊性,每年的6月和12月,由国家开放大学统一安排,各分部安排合适场地,组织进行期末考试。以四川分部为例,全省范围内共有150多个考点。为了加强考风考纪的建设,打击违规替考等不正之风,需要在进入考场之前,对考生进行“实人+实名+实证”的验证。而传统的人工核验易受到人为因素干扰,且无法防止证件作假的情况,由此背景下,亟需设计一套考场身份认证系统,辅助考场规范管理,严肃考风考纪。
由于考试入场事件集中在考前30至15分钟,因此在这段时间内,首先需要系统在响应规模、并发处理能力、数据交互量,以及自身资源消耗等方面,有较高要求。其次,为了方便扩充和演进,系统最好是“scalable”系统,即通过提升硬件能力(增加内存或CPU数量)而达到提升性能的目的。
二、系统实现的关键技术
(一)重叠I/O结构(OVERLAPPED I/O Structure)
在计算机系统中,设备I/O相对于其他操作是比较慢的,于是在多线程结构中通常采用异步I/O的方式进行设备I/O操作。从本质上讲,重叠I/O模型也是一种异步I/O模型,它的核心是一个重叠数据结构:
typedef struct _OVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;
从Winsock 2开始,重叠I/O便集成到了新的WinSock函数中。通过使用重叠结构,应用程序可以一次投递一个或多个异步I/O请求。要想在一个套接字上使用重叠I/O模型来处理网络数据通信,必须使用WSA_FLAG_OVERLAPPED这个标志来创建套接字。如下所示:
WSASocket(AF_INET,SOCK_STEAM,0,NULL,0 WSA_FLAG_OVERLAPPED);
应用程序有多种方法获取重叠I/O请求操作完成的通知,如等待事件对象置信,或使用完成例程等,但它们都有一个共同的缺点,即为每一个I/O都开了一个线程,当同时有成千上万个请求发生时,系统会深陷于线程上下文切换的泥潭里。为此,Windows引入了更为先进的完成端口模型IOCP,用线程池来解决这个问题。
(二)IOCP模型
IOCP(Input/Output Completion Port,I/O完成端口)是一种非常适合C/S模式的网络服务器模型,主要针对数据吞吐量和连接并发量而设计。自从Windows NT 3.5支持IOCP模型后,它被广泛应用于各类高性能网络服务器中。以往无论是event对象或是APCs,都十分紧密的和线程绑在一起,一旦接入的客户端数量过多,系统中会有很多的线程并行的运行,Windows内核会花费大量的时间进行上下文切换,而无力去执行线程体,从而导致效率低下。
IOCP模型可以克服这种“一个Client一个线程”的问题,它提供了工作者(I/O Worker)线程的概念,让应用程序在线程池中创建有限数量的服务器线程,而这个线程池的调用由Windows系统来维护,由系统内核帮忙调度去完成多客户端的I/O请求。从原理上看,IOCP类似于通知队列,当一个重叠I/O完成后,会被加入到队列中,操作系统从线程池中唤醒一个线程来处理。在此期间,线程会一直被挂起,而不会占用CPU时间。
(三)IOCP的关键步骤
大体上讲,使用IOCP需要遵循如下几个关键步骤:
1.创建完成端口对象
调用CreateIoCompletionPort()函数,创建一个完成端口对象,用它面向任意数量的套接字句柄,管理多个I/O请求。
HANDLE CreateIoCompletionPort(
HANDLE File Handle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads);
在创建完成端口时,需注意一下Number Of Concurrent Threads参数,它定义了在一个完成端口上,同时允许执行的线程数量。理想情况下,每个CPU负责一个线程的运行,这样可以让CPU尽可能的忙碌而不至于出现“过饱和”的情况。在一般情况下,将该参数设为0,表明系统内安装了多少个处理器,便允许同时运行多少个线程。即:
m_hIOCompletionPort=CreateIoCompletionPort(I NVALID_HANDLE_VALUE, NULL, 0, 0);
2.创建I/O工作者(I/O Worker)线程
成功创建一个完成端口后,再根据系统中的CPU个数,建立对应数量的工作者线程,这些线程是专门用来和客户端通信的。由于在创建完成端口时,已经指定了同一时刻最多允许的线程数等于CPU个数,因此,工作者线程的数量不能小于CPU个数,否则会出现CPU“空闲”的情况。最好是工作者线程数略大于CPU个数,以便在某些工作者线程被挂起时,能充分发挥系统的潜力。对此,MSDN的建议是:
工作者线程数 = CPU个数 * 2
3.建立监听线程,投递异步I/O请求
在监听套接字上调用异步AcceptEx()函数。AcceptEx是一个特殊的winsock 1.1扩展函数,它最初设计的宗旨是为了在Windows NT上使用WIN32的重叠I/O机制。AcceptEx的定义如下:
其中,第一个参数sListenSocket指定的是一个监听套接字,第二个参数sAcceptSocket指定的是另一个套接字,负责“接受”连接请求。因此,在调用AcceptEx之前,需要事先准备好套接字,这有区别于传统的accept函数。值得注意的是,由于AcceptEx函数是用于接收新的连接,在应用程序运行的过程中会非常频繁的调用,所以需要使用WSAIcotl函数将AcceptEx加载到内存,以减少函数调用带来的消耗。
第三个参数lpOutputBuffer指向一块内存区,当AcceptEx成功时,里面会保存客户端第一次发送的数据、sever的地址和client的地址,是一个非常重要的参数。
第四个参数dwReceiveDataLength用于存放数据的空间大小。如果为0,则AccepEx会立即返回,不会等待数据到来。因此,通常将该参数设成:
sizeof(lpOutputBuffer)(实参的实际空间大小) -2*(sizeof sockaddr_in +16)
4.扫描完成端口队列,处理网络请求
当有客户端连入的时候,再次调用CreateIo-CompletionPort()函数,把新连入的Socket与完成端口绑定。客户端连入之后,可以在Socket上提交一个网络请求,此时,在工作者线程里需要调用GetQueuedCompletionStatus()函数,扫描完成端口队列里是否有网络通信的请求存在。如果有,则将这个请求从完成端口队列中取回来,然后继续执行本线程中后面的处理代码,处理完毕之后,再继续投递下一个网络通信请求,如此循环。
三、系统设计与实现
(一)系统总体架构
系统采用C/S架构,客户端部署在各考场的入口处,考生入场时,采集身份证信息、人脸特征信息,在本地完成身份认证和核验,然后将验证数据实时上传,并存入数据库中。系统拓扑图如图1所示。
图1 系统拓扑图
网络协议采用面向连接的传输控制协议TCP,保证服务器端无差错的接收客户端发送的字节流,确保系统的可靠性和传输数据的正确性。
系统选择“公有云+私有云”混合部署方式。关键的数据服务器放在省校中心机房内,应用服务、数据分析等业务部署在公有云环境中,通过VPN获取考生数据,并将最终的分析结果上传至省校数据服务器,最大限度保证数据的安全性和可靠性。系统部署方式如图2所示。
图2 系统部署方式
(二)客户端设计
客户端由主机(客户端软件)、身份证读卡器、摄像头等组成,利用身份证读卡器,读出身份证信息,同时采集持证人人脸特征进行人脸核验,然后和组考数据进行比对,确认考生的真实身份。客户端的工作流程如图3所示。
客户端软件的工作流程为:
1.客户端软件控制身份证读卡器,对考生身份信息进行采集。
2.客户端软件控制摄像头,提取刷证人的人脸特征,同证件照进行比对。
3.同组考信息进行比对,判断身份、考试场次是否一致。
4.判定结果进入消息队列,同时保存本地数据库备查。
5.连接线程轮询消息队列,一旦发现有数据,即启动发送线程,将数据发送至Server。
(三)服务器端设计
服务器端的核心功能是,将接收到的客户端数据按照定义的格式进行解析,重组成独立的身份验证信息存入数据库中。使用IOCP模型后,服务器端代码架构得以简化,服务器端工作流程如图4所示。
图4 服务器端工作流程图
需要注意的是:
1.使用WSASocket函数
在为AcceptEx提前准备套接字时,需使用WSASocket函数。它是Windows专门用于支持异步操作的API,和通用的网络编程接口socket不同之处在于:WSASocket的发送操作和接收操作都可以被重叠使用,形成缓冲区,而socket只能在发送后等待消息返回才可以做下一步操作。
2.不要在工作者线程中处理数据
在调用GetQueuedCompletionStatus()所在的工作者线程里,最好不要做过多的数据处理操作。IOCP工作者线程池设计的目的是为了充分发挥CPU的性能,如果在工作者线程里进行过多的数据处理,会影响服务器的接收效率。最好的方法是设计一个I/O数据队列,工作者线程接收到数据后直接入队,单独使用一个I/O处理线程进行数据处理。
3.I/O数据队列的设计
服务器端的I/O数据队列不建议采用STL的数据结构。当客户端数量较多时(大于500),网络数据传输峰值较大,使用STL的数据结构会使占用内存成倍数增长,从而导致系统崩溃。最好根据数据包的有效数据格式,设计为多级队列Q1、Q2…Qn,每个队列的大小固定,当前一个队列Qi数据满了之后,新到的数据由Qi+1入队,同时启动I/O线程将Qi的数据一次性写入数据库,依次循环。I/O数据队列的工作流程如图5所示。
图5 I/O数据队列工作流程
(四) 数据包设计
数据包格式由消息头+源地址+帧号+数据长度+数据体+校验位+消息尾组成。除数据体外,其他字段均为定长,可降低数据包结构的复杂度。数据包格式定义如图6所示。
图6 数据包格式定义
在本系统的应用场景下,消息体长度基本固定,数据包的长度远远小于MSS,但从通用性的角度考虑,数据包的设计需尽可能的完整,以便后续系统功能的扩充。
四、系统性能测试及分析
(一)测试环境
使用两台Inter(R) Core(TM) i7-8700 CPU3.2G、8G内存的PC机,操作系统为Windows 10 64位。在其中一台安装客户端,另一台安装服务器端。
为了能模拟大量客户,在客户端程序中增加一个DEBUG模式。该模式下,客户端会同时启动N个线程来连接服务器,并向服务器发送标准测试数据。每个线程发送M条数据,每条数据发送间隔为1秒,总数据量为N * M。服务器端接收数据,并实时写入数据库系统。
(二)测试结果及分析
客户端在DEBUG模式下,分别启动100、200、500、1000个线程,每个线程发送的标准测试数据分别为1000、500,200,100条,总数量均为10万条。测试结果如表1所示。
表1 服务器测试结果对比
从测试结果可以看出,CPU的占用率一直稳定在8%左右,并未出现较大的增长。另外,内存的消耗量与客户端连接数量的增加成正比,且呈现线性缓慢增长,表现出了优良的稳定性。
五、结语
基于IOCP模型设计了一套通用的考场身份认证系统,详细的阐述了IOCP模型的关键技术点,同时给出了具体的设计思路和实现方法,最后针对服务器端关心的两个重要指标:CPU占用率和内存消耗,对系统进行了对比测试,结果证明了使用该模型的性能优势。另外,基于本系统封装的网络通信模块,经过简单修改即可移植到在线考试系统,具有一定的通用性。