WindowssocketIO完成端口开发驾照理论考试系统实例
这一节我们讲解如何利用套接字完成端口开发驾照理论考试系统。
该系统由服务器和客户端两部分组成。
服务器负责对题库和学生信息的管理,主要包括以下功能:
1:试卷管理:从题库读取试卷和向客户端发送试卷。
2:客户端管理(CClientManager类)。从数据库读取学生信息,验证学生信息。
3:监视考生考试状态。
4:评分。保存学生考试状态和成绩。
客户端负责生成试卷,主要包括以下功能:
1:登录服务器。
2:生成试卷。
3:考试计时。考试时间结束考生停止答题。
主要步骤如下:
客户端成功连接服务器后,向服务器发送学号。服务器在收到学号后,会验证该学号是否存在于数据库中。如果存在且未登录过,则向客户端发送该学生姓名和试卷。否则,项客户端发送查无此人信息。
客户端在收到试卷后,向考生显示准备完成,是否开始答题。考生点击开始答题后,客户端向服务器发送开始答卷消息。考生开始答卷,客户端进行计时。考试结束后,客户端向服务器发送答题结果。服务器在收到答题结果后,对该考生的试卷进行评分,并将结果保存在数据库。
数据包设计:
为了保证客户端与服务器之间数据的正确发送与接收,在发送数据时,先发送包头后发送包体。包头指明包体的类型和长度。包头定义如下:
typedef struct _header
{
u_short type;//包类型。
u_short len;//包体长度。
}HEADER;
客户端发送的数据包类型包括考生状态和心跳包两种。在考试期间为了监控考生状态,客户端定期向服务器发送心跳包,服务器根据此心跳包判断客户端状态。如果由于意外导致连接断开,服务器在一段时间内没有收到心跳包则断定客户端断线。
客户端在发送包头后,发送考生状态,它包括以下几种:
登录:此时考生状态为考生学号。LOGIN
答卷:此时考生状态没有数据。DOING
交卷:此时考生状态为答题结果。DONE
断线:在服务器一段时间没有收到心跳包后,设置客户端为断线状态。DISCONN
服务器发送的数据包类型包括:
考生姓名和试卷。
服务器在验证考生学号后,向客户端发送该考生的姓名和试卷。
工作流程:
服务器启动后,调用InitSocket初始化监听套接字。读取数据库,将试卷读入内存,为发送做准备。调用启动服务函数,在启动服务函数内创建接受客户端请求线程和服务线程。创建监听线程使用WSAEventSelect模型管理请求套接字。服务线程可以有多个,循环调用GetQueuedCompletionStatus函数,检查是否有异步IO已完成。
当有套接字请求时,接受线程接受请求,并创建CClient对象,加入CClientManager管理的客户端链表。并调用CClient的AsyRecvHeader执行接收包头异步IO操作。
当有异步IO完成时,GetQueuedCompletionStatus函数返回,根据IO操作类型IOType决定执行何种操作。
如果是接收包头异步IO完成,根据包头指定的类型判断是心跳包,或是状态包。
如是状态包,则调用接受包体异步IO函数。在接收包体函数完成后,前两字节指定的状态,执行操作。如果状态是LOGIN,则是登录状态。包体长度为hdr.Len。前2字节为当前状态,两字节后为学号,长度为hdr.Len-2。接收到学号后,从数据库查询如果存在此学号,且此学号未登录则发送姓名。否则发送登录失败。GetQueuedCompletionStatus返回时,收到发送姓名异步IO完成后,发送试卷。客户端收到试卷后对试卷进行解析,生成试卷。
客户端向服务器发送考试开始包,考试开始,服务器更新考生状态为正在考试。客户端设置一个计时器,每10s向服务器发送一个心跳包,表明当前客户端处于在线状态,并未离线。服务器也定义一个计时器,它每隔60s触发一次,在响应函数内遍历所有连接的客户端,检查它发送心跳包的时间与当前时间差,如果大于60s则说明客户端已掉线。将此客户端从链表中删除。并更新对应考生状态为掉线。
考生做完所有试题后,点击交卷,客户端向服务器发送DONE包,包体部分为答案。服务器更新考生状态,并计算分数更新到列表控件和服务器。然后删除客户端,整个过程结束。
//客户端向服务器发送的包类型。
#define PULSE 107//心跳包。
#define REQUEST 108//请求包。
//服务器向客户端发送的包类型:
#define STUNAME 109//学生姓名。
#define PAPER 110//试卷包。
#define LOGINFAILED 111//登录失败。
考试系统的试题类型为选择题。它包括试题和答案两部分。题号与问题之间使用:分割。每道题有四个答案,每个答案之间用|分割。试题之间用<>分割。如
<1:问题|A:答案|B:答案|C:答案|D:答案><:问题2|A:答案|B:答案|C:答案|D:答案><:问题3|A:答案|B:答案|C:答案|D:答案><:问题4|A:答案|B:答案|C:答案|D:答案>
数据库设计
Access数据中存储了考生信息和试卷信息。此处定义了两张表,分别为StuInfo表和Paper表。。使用ODBC与数据库连接。由于本文更偏重与介绍IOCP的机制,因此对于数据库如何使用此处不再介绍。
StuInfo表包括Stu_ID,Stu_NO,Stu_Name,Stu_State,Stu_Grade字段。Stu_ID为主键。
Paper表包括PAP_ID,PAP_QUESTION,PAP_ANSWERA,PAP_ANSWERB,PAP_ANSWERC,PAP_ANSWERD,PAP_ANSWER。
服务器设计:
服务器负责发送试题和对考生考试信息进行管理。它包括多线程设计和界面设计。
多线程设计:
主线程启动后,创建一个接受客户端连接请求的线程和多个服务线程。服务线程使用套接字的IO完成端口模型对服务器的IO操作进行管理。监听线程使用套接字的WSAEventSelect模型实现对接受客户端请求进行管理。
当服务器退出时,主线程通知接受客户端线程和服务线程退出,然后主线程退出。
在接受客户端连接请求线程中,接受客户端的连接请求后,立即发出一个接受客户端数据包头的异步请求。在服务线程取出IO操作结果后,再发起另一个异步IO操作。
一:主线程:负责初始化界面、读取数据库、更新界面、更新数据库、创建完成端口和通知接受请求线程和服务线程退出。
二:接受客户端请求线程,利用WSAEventSelect模型实现对客户端请求的管理。主要任务为:
1:接受客户端连接请求。
2:将完成端口与套接字关联起来。
3:发起异步接收客户端数据操作。
三:服务线程,利用IO完成端口实现对IO操作管理。主要任务为:
1:管理客户端。
2:发送考生姓名和试卷。
3:接收客户端数据。
4:考试结束后,对考生答卷进行评分。
服务器程序是基于单文档的MFC应用程序。主要包括一下几个类:
1:CCServerView类:视图类,实现服务器的主要功能。
2:CClient类:实现与客户端通信功能。
3:CClientManager类,实现对连接的客户端进行管理功能。
4:CServerAddrDlg类:实现IP地址输入窗口。
CCServerView类:
OnInitialUpdate函数实现服务器初始化功能:初始化列表试图控件、更新列表试图控件和读取试卷。
OnStartService实现启动服务功能。它创建监听套接字、创建完成端口、创建监听事件对象和接受客户端请求线程和服务线程。
在服务器保存一定数量的习题。在OnInitialUpdate函数中会将这些试题读入到m_cPaper数组中。
CCServerView类的用户自定义部分为:
public:
static DWORD WINAPI AcceptClientThread(PVOID ppram);
static DWORD WINAPI ServiceThread(PVOID ppram);
bool InitSocket();//初始化套接字。
bool StartService();//开始服务。
bool StopService();//停止服务。
CString AuthenticateStuNo(char*);//验证学号。
bool ReadPaper();//从数据库读取试卷信息。
bool ReadStuInfo();//从数据库读取学生信息。
void ContructPaperBuffer();//构造
void UpdateClientState(CClient *pClient,USHORT state);
void Destroy();
bool SaveGradeIntoDB(CClient*pClient);//保存成绩到数据库。
bool IsAlreadyLogin(CString StuName);//判断考生是否已登录。
public:
HANDLE m_hIOCP;//完成端口句柄。
HANDLE m_hEvent;//监听套接字对应事件对象。
HANDLE m_h[SERVICE_NUMBER+1];//线程句柄。
SOCKET m_sListenSocket;//监听套接字。
bool m_IsRunning;//判断服务器是否运行。
CDatabase m_StuInfoDB;//考生信息数据库类。
CDatabase m_PaperDB;//试卷数据库类。
CListCtrl m_listCrtl;//列表控件。
PaperItem *m_pPaperArray;//试题数组。PaperItem定义马上介绍。
UINT m_numOfPaperItem;//试卷试题数量。
CString m_PaperBuff;//试卷缓冲区。
afx_msg void OnTimer(UINT_PTR nIDEvent);//计时器函数,用于检查心跳包
afx_msg void OnStartService();//服务器开始运行。
实现为:
// CIOCPDriverLisenceExamServerView 消息处理程序
void CIOCPDriverLisenceExamServerView::OnInitialUpdate()
{
CView::OnInitialUpdate();
// TODO: 在此添加专用代码和/或调用基类
m_hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);
CString sDriver = TEXT("MICROSOFT ACCESS DRIVER (*.mdb)");
CString sDsnStuInfo;
CString sFileStuInfo = TEXT("E://DriverLiscenceExam.mdb");//Change path here
sDsnStuInfo.Format(TEXT("ODBC;DRIVER={%s};DSN='';DBQ=%s"),sDriver,sFileStuInfo);
bool ret=m_StuInfoDB.Open(NULL,false,false,sDsnStuInfo);
if(!ret)
{
MessageBox(TEXT("连接数据库失败!"));
}
InitSocket();
ReadPaper();
ReadStuInfo();
ContructPaperBuffer();
}
bool CIOCPDriverLisenceExamServerView::InitSocket()
{
WSAData wsadata;
WSAStartup(MAKEWORD(2,2),&wsadata);
m_sListenSocket=socket(AF_INET,SOCK_STREAM,0);
if(m_sListenSocket==INVALID_SOCKET)
{
closesocket(m_sListenSocket);
WSACleanup();
return false;
}
m_hEvent=WSACreateEvent();
WSAEventSelect(m_sListenSocket,m_hEvent,FD_ACCEPT);//为监听套接字设置FD_ACCEPT事件。
SOCKADDR_IN addr;
addr.sin_family=AF_INET;
addr.sin_addr.S_un.S_addr=inet_addr("192.168.1.100");
addr.sin_port=htons(4000);
int ret=bind(m_sListenSocket,(SOCKADDR*)&addr,sizeof(addr));
if(ret==SOCKET_ERROR)
{
return false;
}
ret=listen(m_sListenSocket,1000);
if(ret==SOCKET_ERROR)
{
return false;
}
}
DWORD WINAPI CIOCPDriverLisenceExamServerView::AcceptClientThread( PVOID ppram )
{
CIOCPDriverLisenceExamServerView*pServer=(CIOCPDriverLisenceExamServerView*)ppram;
SOCKADDR_IN addr;
int len=sizeof(addr);
while(pServer->m_IsRunning)
{
int ret=WSAWaitForMultipleEvents(1,&pServer->m_hEvent,false,WSA_INFINITE,false);
if(ret==WSA_WAIT_TIMEOUT)
{
continue;
}
else
{
WSANETWORKEVENTS events;
int r=WSAEnumNetworkEvents(pServer->m_sListenSocket,pServer->m_hEvent,&events);//重置事件对象。
if(r==SOCKET_ERROR)
{
break;
}
if(events.lNetworkEvents&FD_ACCEPT)
{
if(events.iErrorCode[FD_ACCEPT_BIT]==0)//发生FD_ACCEPT网络事件。接受客户端请求。
{
SOCKET sAccept=WSAAccept(pServer->m_sListenSocket,(SOCKADDR*)&addr,&len,0,NULL);
CClient*pClient=new CClient(sAccept,pServer);
if(CreateIoCompletionPort((HANDLE)sAccept,pServer->m_hIOCP,(ULONG_PTR)pClient,0)==NULL)
return -1;
g_clientManager.addClient(pClient);
//调用接收数据异步IO。
if(!pClient->AsyRecvHeader())
g_clientManager.deleteClient(pClient);//接收数据失败后,将此客户端从链表删除。
}
}
}
}
return 0;
}
bool CIOCPDriverLisenceExamServerView::StartService()
{
m_IsRunning=true;
m_h[0]=CreateThread(NULL,0,AcceptClientThread,this,0,NULL);
for(int i=1;im_IsRunning)
{
bool ret=GetQueuedCompletionStatus(pServer->m_hIOCP,&transferred,(LPDWORD)&pClient,&lpoverlapped,WSA_INFINITE);
if(ret&&lpoverlapped&&pClient)//成功的异步IO完成。根据从lpoverlapped中得到的类型,进行操作。
{
IO_OPERATION_DATA*pIO=(IO_OPERATION_DATA*)lpoverlapped;
switch(pIO->IOType)
{
case IOReadHead:
{
pClient->AsyRecvHeaderCompleted();
}
break;
case IOReadBody:
{
pClient->AsyRecvBodyCompleted();
}
break;
case IOWritePaper:
{
//试卷发送完毕。不执行动作。
pServer->UpdateClientState(pClient,CClient::LOGIN);
}
break;
case IOWriteName:
{
pClient->AsySendPaper();
}
break;
case IOWriteUnLogin:
{
g_clientManager.deleteClient(pClient);
}
break;
default:
break;
}
}
}
return 0;
}
CString CIOCPDriverLisenceExamServerView::AuthenticateStuNo( char*pStuNo)
{
//TCHAR wStuNo[128];
//ZeroMemory(wStuNo,128);
//MultiByteToWideChar(CP_ACP,MB_COMPOSITE,pStuNo,strlen(pStuNo),wStuNo,128);//TCP是基于字节流的,而此程序是使用Unicode编码。因此需要转换。
CStuInfo StuInfo(&m_StuInfoDB);
if(StuInfo.IsOpen())
{
StuInfo.Close();
}
StuInfo.m_strFilter.Format(TEXT("StuNo='%s'"),pStuNo);
StuInfo.Open();
if(StuInfo.IsEOF())//数据库中未搜索到该用户。该账号不存在。
{
//MessageBox(TEXT("未找到该用户。"));
return CString();
}
else
{
//MessageBox(TEXT("找到该用户。"));
return CString(StuInfo.m_StuName);
}
}
bool CIOCPDriverLisenceExamServerView::ReadPaper()
{
CPaper paperRecordSet(&m_PaperDB);
if(paperRecordSet.IsOpen())
{
paperRecordSet.Close();
}
paperRecordSet.Open();
//long numInDB=paperRecordSet.GetRecordCount();
//paperRecordSet.GetRe
m_numOfPaperItem=0;
while(!paperRecordSet.IsEOF())
{
m_numOfPaperItem++;
paperRecordSet.MoveNext();
}
paperRecordSet.MoveFirst();
m_pPaperArray=new PaperItem[m_numOfPaperItem];
if(!m_pPaperArray)
return false;
for(int j=0;j",m_pPaperArray[i].PAP_ID,m_pPaperArray[i].PAP_QUESTION
,m_pPaperArray[i].PAP_ANS_A,m_pPaperArray[i].PAP_ANS_B,m_pPaperArray[i].PAP_ANS_C,m_pPaperArray[i].PAP_ANS_D);
m_PaperBuff+=temp;
}
}
void CIOCPDriverLisenceExamServerView::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
EnterCriticalSection(&g_clientManager.m_cs);
for(std::list::iterator iter=g_clientManager.m_ClientList.begin();iter!=g_clientManager.m_ClientList.end();)
{
CClient*pClient=(*iter);
if(pClient->m_state==CClient::DOING)
{
CTime CurTime=CTime::GetCurrentTime();
CTimeSpan spanTime(CurTime.GetDay()-pClient->m_time.GetDay(),
CurTime.GetHour()-pClient->m_time.GetHour(),
CurTime.GetMinute()-pClient->m_time.GetMinute(),CurTime.GetSecond()-pClient->m_time.GetSecond());
if(spanTime.GetMinutes()>1)
{
//设置考生为断线状态。
pClient->m_state=CClient::DISCONN;
//更新显示状态。
UpdateClientState(pClient,CClient::DISCONN);
//删除该客户端。
g_clientManager.deleteClient(pClient);
iter=g_clientManager.m_ClientList.begin();
}
else
iter++;
}
}
LeaveCriticalSection(&g_clientManager.m_cs);
CView::OnTimer(nIDEvent);
}
void CIOCPDriverLisenceExamServerView::UpdateClientState( CClient *pClient,USHORT state )
{
int i=0;
for(i=0;im_StuName==m_listCrtl.GetItemText(i,2))
{
break;
}
}
CString s;
switch(state)
{
case CClient::LOGIN:
s="已登录";
break;
case CClient::DOING:
s="正在答题";
break;
case CClient::DONE:
{
s="已交卷";
SaveGradeIntoDB(pClient);
}
break;
case CClient::DISCONN:
s="掉线";
break;
default:
s="未知状态";
break;
}
m_listCrtl.SetItemText(i,3,s);
}
void CIOCPDriverLisenceExamServerView::Destroy()
{
KillTimer(1);
}
void CIOCPDriverLisenceExamServerView::OnStartService()
{
// TODO: 在此添加命令处理程序代码
StartService();
}
bool CIOCPDriverLisenceExamServerView::SaveGradeIntoDB(CClient*pClient)
{
//在列表空间更新分数。
int i=0;
for(i=0;im_StuName==m_listCrtl.GetItemText(i,2))
{
//m_listCrtl.SetItemText(i,4);
break;
}
}
CString s;
s.Format("%d",pClient->m_grade);
m_listCrtl.SetItemText(i,4,s);
//在数据库更新分数。
CStuInfo StuInfo(&m_StuInfoDB);
if(StuInfo.IsOpen())
{
StuInfo.Close();
}
StuInfo.m_strFilter.Format(TEXT("StuNo='%s'"),pClient->m_StuNo);
StuInfo.Open();
if(!StuInfo.IsEOF())//数据库中未搜索到该用户。该账号不存在。
{
StuInfo.Edit();
StuInfo.m_StuGrade=pClient->m_grade;
StuInfo.m_StuState="已考试";
StuInfo.Update();
StuInfo.Close();
g_clientManager.deleteClient(pClient);
return true;
}
return false;
}
bool CIOCPDriverLisenceExamServerView::IsAlreadyLogin( CString StuName )
{
std::list::iterator iter;
for(iter=g_clientManager.m_ClientList.begin();iter!=g_clientManager.m_ClientList.end();iter++)
{
if((*iter)->m_StuName==StuName)
break;
}
if(iter==g_clientManager.m_ClientList.end())
return false;
else
return true;
}
CClient类。
CClient类来实现服务器与客户端的通信。类的构造函数为套接字和CCServerView*指针。有了CCServerView指针,CClient对象就可以调用相关函数修改考生信息。在析构函数中关闭套接字。
在该类中定义了一些与客户端通信的函数。类中有两个WSAOVERLAPPED扩展结构的变量,分别对应接收和发送数据异步操作。m_time表示接收到客户端心跳包的事件。
m_state表示客户端的当前状态。它是state枚举类型。定义如下:
enum state
{
LOGIN,//已登录状态。
DOING,//正在答题状态。
DONE,//已交卷状态。
DISCONN//故障掉线。
};
CClient有一个函数用以接收包头,接收包头后,根据包头的类型,包体长度,在调用不同的函数接收包体。这在使用Windowssocket开发中经常使用。这样的结构可以使程序结构变得更清晰。
由于接收和发送数据异步IO完成的时间不确定,每个异步IO函数,还分别对应一个完成函数。AsyRecvHeader和AsyRecvBody仅仅用于发出异步IO请求,而对应的完成函数用以处理在接收数据完成后的工作。这种编程方式要特别注意。
每一个异步IO请求对应着一个OVERLAPPED结构和一个完成键。我们常常会重新定义OVERLAPPED结构,并传给完成键对我们有用的信息,这样在GetQueuedCompletionStatus返回时就会得到更多的信息。
重新定义后的OVERLAPPED结构信息为:
typedef struct _IO_OPERATION_DATA
{
OVERLAPPED overlapped;
char buffer[BUFFER_SiZE];
byte IOType;//IO类型。用以得知哪种异步IO完成。收or发。
}IO_OPERATION_DATA;
在每个异步IO完成时,都可以根据IO类型得到是什么IO完成。一定要区分IO类型和包类型。这很重要。
IO类型有以下几种:
#define IOReadHead 100//接收包头完成,异步IO完成。
#define IOReadBody 101//接收包体完成,异步IO完成。
#define IOWriteUnLogin 103//登录失败。
#define IOWritePaper 104//发送试卷完成,异步IO完成。
#define IOExit 105//退出。
#define IOWriteName 106//发送姓名异步IO完成。
定义包头。服务器和客户端互相通信离不开包头。包头可以指定,数据包的类型和包体的长度。
定义如下:
typedef struct HEADER
{
short PacketType;//包类型。
short Len;//包体长度。
}HDR,*PHDR;
CClient类。几乎所有前面介绍过的socket程序都有一个CClient类,它用于执行与客户端通信的功能。也是在这个IOCP开发的驾照考试系统最重要的一个类。
声明如下:
#pragma once
#include"WinSock2.h"
#include"ctime"
class CIOCPDriverLisenceExamServerView;
#define BUFFER_SIZE 10240
#define IOReadHead 100
#define IOReadBody 101
#define IOWriteUnLogin 103
#define IOWritePaper 104
#define IOExit 105
#define IOWriteName 106
//客户端向服务器发送的包类型。
#define PULSE 107//心跳包。
#define REQUEST 108//请求包。
//服务器向客户端发送的包类型:
#define STUNAME 109//学生姓名。
#define PAPER 110//试卷包。
#define LOGINFAILED 111//登录失败。
typedef struct HEADER
{
short PacketType;
short Len;
}HDR,*PHDR;
typedef struct _IO_OPERATION_DATA
{
WSAOVERLAPPED overlapped;
char buffer[BUFFER_SIZE];
HDR hdr;
byte IOType;
}IO_OPERATION_DATA;
class CClient
{
public:
enum state//考生状态。
{
LOGIN,//已登录。
DOING,//正在答题。
DONE,//已交卷。
DISCONN,//因故障断开。
UNKNOW//原因不明。
};
CClient(SOCKET s,CIOCPDriverLisenceExamServerView*pDlg);
~CClient(void);
public:
bool AsyRecvHeader();//接收包头。
bool AsyRecvBody(int len);
void AsyRecvBodyCompleted();
bool AsySendName();
bool AsySendPaper();
bool AsySendFailedLoginMsg();
void AsyRecvHeaderCompleted();
void CalculateGrade();
public:
public:
SOCKET m_s;
IO_OPERATION_DATA m_IoRecv;//recv
IO_OPERATION_DATA m_IoSend;
USHORT m_state;//考生状态。
CTime m_time;//最近一次心跳包时间。
CIOCPDriverLisenceExamServerView*m_pServerView;//主窗口指针。
public:
CString m_StuNo;
//CString m_StuID;
CString m_StuName;
long m_grade;
CString m_Result;//答题结果。
};
CClient类实现为:
#include "StdAfx.h"
#include "Client.h"
#include"IOCPDriverLisenceExamServerView.h"
CClient::CClient( SOCKET s,CIOCPDriverLisenceExamServerView*pDlg )
{
m_s=s;
m_pServerView=pDlg;
m_time=CTime::GetCurrentTime();
}
CClient::~CClient(void)
{
closesocket(m_s);
}
//读取包头异步函数。
bool CClient::AsyRecvHeader()
{
WSABUF wsabuf;
ZeroMemory(&m_IoRecv,sizeof(IO_OPERATION_DATA));
m_IoRecv.IOType=IOReadHead;
wsabuf.buf=(char*)&m_IoRecv.hdr;
wsabuf.len=sizeof(HDR);
DWORD flag=0;
int ret=WSARecv(m_s,&wsabuf,1,NULL,&flag,&m_IoRecv.overlapped,NULL);
if(ret==SOCKET_ERROR)
{
int err=WSAGetLastError();
if(err!=WSA_IO_PENDING)
{
return false;
}
}
return true;
}
//包头接收完毕。
void CClient::AsyRecvHeaderCompleted()
{
if(m_IoRecv.hdr.PacketType==PULSE)//如果为心跳包。//客户端发送给服务器的包头的PacketType有两种。一种是心跳包,另外是REQUEST包。
//在数据的前两个字节会指定当前请求,有LOGIN,DOING ,DONE等。
{
m_time=CTime::GetCurrentTime();
AsyRecvHeader();
}
else //if(state==m_IoRecv.hdr.PacketType)//状态包。接着接收包体。
{
AsyRecvBody(m_IoRecv.hdr.Len);
}
}
//接收包体。
bool CClient::AsyRecvBody(int len)
{
WSABUF wsabuf;
//ZeroMemory(&m_IoRecv.buffer,BUFFER_SIZE);
ZeroMemory(&m_IoRecv,sizeof(IO_OPERATION_DATA));
m_IoRecv.IOType=IOReadBody;
wsabuf.buf=m_IoRecv.buffer;
wsabuf.len=len;//接收包体。
DWORD flag=0;
int ret=WSARecv(m_s,&wsabuf,1,NULL,&flag,&m_IoRecv.overlapped,NULL);
if(ret==SOCKET_ERROR)
{
int err=WSAGetLastError();
if(err!=WSA_IO_PENDING)
{
return false;
}
}
return true;
}
extern CClientManager g_clientManager;//全局的管理客户端类。
void CClient::AsyRecvBodyCompleted()
{
//根据包体的内容,确定是什么类型的请求包。是请求学生姓名还是请求发送试卷。
//前2个字节用于确定类型。
u_short type;
memcpy(&type,m_IoRecv.buffer,2);
switch(type)
{
case LOGIN://登录。
{
//获取学号。
char StuNo[128];
strcpy(StuNo,(m_IoRecv.buffer+2));
CString name=m_pServerView->AuthenticateStuNo(StuNo);
if(!name.IsEmpty()&&!m_pServerView->IsAlreadyLogin(name))//验证成功。
{
m_StuNo=StuNo;
m_StuName=name;
AsySendName();//在发送姓名异步IO完成收到通知后,再发送试卷。
AsyRecvHeader();
}
else//验证失败。
{
AsySendFailedLoginMsg();
}
}
break;
case DOING://答题状态。
{
m_state=type;
AsyRecvHeader();
}
break;
case DONE://交卷状态。
{
m_state=type;
//从包体中得到考生答案。
m_Result=m_IoRecv.buffer+2;
CalculateGrade();
}
break;
default:
break;
}
m_pServerView->UpdateClientState(this,type);
}
//发送考生姓名。
bool CClient::AsySendName()
{
ZeroMemory(&m_IoSend,sizeof(IO_OPERATION_DATA));
WSABUF wsabuf[2];
//char name[128];
//memset(name,0,128);
// WideCharToMultiByte(CP_ACP,WC_COMPOSITECHECK,m_StuName.GetString(),m_StuName.GetLength(),name,128,NULL,NULL);
m_IoSend.IOType=IOWriteName;
m_IoSend.hdr.Len=m_StuName.GetLength();
m_IoSend.hdr.PacketType=STUNAME;
//发送包头。
wsabuf[0].buf=(char*)&m_IoSend.hdr;
wsabuf[0].len=sizeof(HDR);
//送包体。
wsabuf[1].buf=m_StuName.GetBuffer();
wsabuf[1].len=m_StuName.GetLength();
DWORD flag=0;
int ret=WSASend(m_s,wsabuf,2,NULL,flag,&m_IoSend.overlapped,NULL);
if(ret==SOCKET_ERROR)
{
int err=WSAGetLastError();
if(err!=WSA_IO_PENDING)
{
return false;
}
}
return true;
}
//发送登录失败。
bool CClient::AsySendFailedLoginMsg()
{
ZeroMemory(&m_IoSend,sizeof(IO_OPERATION_DATA));
WSABUF wsabuf[2];
m_IoSend.IOType=IOWriteUnLogin;
m_IoSend.hdr.Len=0;
m_IoSend.hdr.PacketType=LOGINFAILED;
//发送包头。
wsabuf[0].buf=(char*)&m_IoSend.hdr;
wsabuf[0].len=sizeof(HDR);
//发送包体
//wsabuf[1].buf=""
int ret=WSASend(m_s,wsabuf,1,NULL,0,&m_IoSend.overlapped,NULL);
if(ret==SOCKET_ERROR)
{
int err=WSAGetLastError();
if(err!=WSA_IO_PENDING)
{
return false;
}
}
return true;
}
void CClient::CalculateGrade()
{
m_grade=0;
for(int i=0;im_numOfPaperItem;i++)
{
if(m_Result.GetAt(i)-'A'+1==m_pServerView->m_pPaperArray[i].PAP_ANSWER)
{
m_grade++;
}
}
}
//发送试卷。
bool CClient::AsySendPaper()
{
ZeroMemory(&m_IoSend,sizeof(m_IoSend));
WSABUF wsabuf[2];
//发送包头:
wsabuf[0].buf=(char*)&m_IoSend.hdr;
wsabuf[0].len=sizeof(HDR);
//发送包体。
wsabuf[1].buf=m_pServerView->m_PaperBuff.GetBuffer();
wsabuf[1].len=m_pServerView->m_PaperBuff.GetLength();
m_IoSend.IOType=IOWritePaper;
m_IoSend.hdr.PacketType=PAPER;
m_IoSend.hdr.Len=wsabuf[1].len;
int ret=WSASend(m_s,wsabuf,2,NULL,0,&m_IoSend.overlapped,NULL);
if(ret==SOCKET_ERROR)
{
int err=WSAGetLastError();
if(err!=WSA_IO_PENDING)
{
return false;
}
}
return true;
}
PaperItem为保存试题的结构体,其定义为:
typedef struct _PaperItem
{
LONG PAP_ID;
//CString PAP_NO;
CString PAP_QUESTION;
CString PAP_ANS_A;
CString PAP_ANS_B;
CString PAP_ANS_C;
CString PAP_ANS_D;
BYTE PAP_ANSWER;
}PaperItem;
CClientManager类:用以管理接受连接的客户端。内部是使用list实现,主要具有添加、删除、删除所有元素等成员函数。list的元素类型为CClient*类型,这样以后就可以对所有连接的客户端进行管理。注意,对list进行操作时,应该使用关键段或其他同步措施进行同步。
声明如下:
#pragma once
#include"Client.h"
//class CClient;
#include
class CClientManager
{
public:
CClientManager(void);
~CClientManager(void);
public:
std::listm_ClientList;
CRITICAL_SECTION m_cs;//用于线程互斥的关键段。防止同时对list进行修改。
public:
bool addClient(CClient*pClient);
bool deleteClient(CClient*pClient);
bool delteAllClient();
};
实现为:
#pragma once
#include "StdAfx.h"
#include "ClientManager.h"
CClientManager::CClientManager(void)
{
InitializeCriticalSection(&m_cs);
}
CClientManager::~CClientManager(void)
{
for(std::list::iterator iter=m_ClientList.begin();iter!=m_ClientList.end();iter++)
{
delete (*iter);
}
m_ClientList.clear();
}
bool CClientManager::addClient( CClient*pClient )
{
EnterCriticalSection(&m_cs);
m_ClientList.push_back(pClient);
LeaveCriticalSection(&m_cs);
return true;
}
bool CClientManager::deleteClient( CClient*pClient )
{
//std::list<::iterator iter=m_ClientList.find(pClient);
EnterCriticalSection(&m_cs);
delete pClient;
m_ClientList.remove(pClient);
LeaveCriticalSection(&m_cs);
return true;
}
bool CClientManager::delteAllClient()
{
EnterCriticalSection(&m_cs);
for(std::list::iterator iter=m_ClientList.begin();iter!=m_ClientList.end();iter++)
{
delete (*iter);
}
m_ClientList.clear();
LeaveCriticalSection(&m_cs);
return true;
}
OnTimer响应函数实现周期性的扫描所有已连接客户端,检查心跳包发送时间。
void CIOCPDriverLisenceExamServerView::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
EnterCriticalSection(&g_clientManager.m_cs);
for(std::list::iterator iter=g_clientManager.m_ClientList.begin();iter!=g_clientManager.m_ClientList.end();)
{
CClient*pClient=(*iter);
if(pClient->m_state==CClient::DOING)
{
CTime CurTime=CTime::GetCurrentTime();
CTimeSpan spanTime(CurTime.GetDay()-pClient->m_time.GetDay(),
CurTime.GetHour()-pClient->m_time.GetHour(),
CurTime.GetMinute()-pClient->m_time.GetMinute(),CurTime.GetSecond()-pClient->m_time.GetSecond());
if(spanTime.GetMinutes()>1)
{
//设置考生为断线状态。
pClient->m_state=CClient::DISCONN;
//更新显示状态。
UpdateClientState(pClient,CClient::DISCONN);
//删除该客户端。
g_clientManager.deleteClient(pClient);
iter=g_clientManager.m_ClientList.begin();
}
else
iter++;
}
}
LeaveCriticalSection(&g_clientManager.m_cs);
CView::OnTimer(nIDEvent);
}
服务器运行截图:
客户端设计:
由于客户端比较简单,因此采用最简单的阻塞模式实现。
CClientSocket类:用以实现与服务器的通信工作。
定义如下:
#pragma once
#define BUFFER_SIZE 10240
//服务器向客户端发送的包类型:
#define STUNAME 109//学生姓名。
#define PAPER 110//试卷包。
#define LOGINFAILED 111//登录失败。
enum state//考生状态。
{
LOGIN,//已登录。
DOING,//正在答题。
DONE,//已交卷。
DISCONN,//因故障断开。
UNKNOW//原因不明。
};
class CDriverLisenceClientDlg;
class CClientSocket
{
public:
CClientSocket(CDriverLisenceClientDlg*pDlg,DWORD IP ,short port);
~CClientSocket(void);
public:
bool ConnectToServer();//连接到服务器。
bool LoginServer();//登录。
bool RecvName();//接收姓名。
bool RecvPaper();//接收试卷。
bool SendStart();//通知服务器考试开始。
bool SendResult();//发送考试结果。
bool SendPulse();//发送心跳包。
public:
SOCKET m_s;//客户端套接字。
char *m_pRecvBuffer;//接收缓冲区。
char *m_pSendBuffer;//发送缓冲区。
DWORD m_ServerIP;//服务器IP。
SHORT m_ServerPort;//服务器端口。
CDriverLisenceClientDlg*m_pMainDlg;//主窗口指针。
};
实现为:
#include "StdAfx.h"
#include "ClientSocket.h"
#include"DriverLisenceClientDlg.h"
CClientSocket::CClientSocket(CDriverLisenceClientDlg*pDlg, DWORD IP ,short port )
{
// TODO: 在此添加控件通知处理程序代码
m_pMainDlg=pDlg;
m_pSendBuffer=new char[BUFFER_SIZE];
m_pRecvBuffer=new char[BUFFER_SIZE];
m_ServerIP=IP;
m_ServerPort=port;
WSAData wsadata;
WSAStartup(MAKEWORD(2,2),&wsadata);
m_s=socket(AF_INET,SOCK_STREAM,0);
if(m_s==INVALID_SOCKET)
{
closesocket(m_s);
WSACleanup();
return ;
}
}
CClientSocket::~CClientSocket(void)
{
closesocket(m_s);
}
//连接服务器。
bool CClientSocket::ConnectToServer()
{
SOCKADDR_IN addr;
addr.sin_family=AF_INET;
addr.sin_addr.S_un.S_addr=inet_addr("192.168.1.100");
addr.sin_port=htons(4000);
int len=sizeof(addr);
int ret=connect(m_s,(SOCKADDR*)&addr,len);
if(ret==SOCKET_ERROR)
{
return false;
}
const char chOpt=1;
ret=setsockopt(m_s,IPPROTO_TCP,TCP_NODELAY,&chOpt,sizeof(char));
if(ret==SOCKET_ERROR)
{
return false;
}
return true;
}
//登录到服务器。
bool CClientSocket::LoginServer()
{
memset(m_pSendBuffer,0,BUFFER_SIZE);
HDR hdr;
hdr.PacketType=REQUEST;
hdr.Len=m_pMainDlg->m_StuNo.GetLength()+2;
USHORT State=LOGIN;
int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。
if(ret==SOCKET_ERROR)
{
return false;
}
strncpy(m_pSendBuffer,(char*)&State,sizeof(USHORT));
strncpy(m_pSendBuffer+2,m_pMainDlg->m_StuNo.GetBuffer(),m_pMainDlg->m_StuNo.GetLength());//
ret=send(m_s,m_pSendBuffer,hdr.Len,0);//发送登陆信息包体。
if(ret==SOCKET_ERROR)
{
return false;
}
return true;
}
//接收姓名。
bool CClientSocket::RecvName()
{
HDR hdr;
int ret=recv(m_s,(char*)&hdr,sizeof(HDR),0);//接收包头。
if(ret==SOCKET_ERROR)
{
return false;
}
memset(m_pRecvBuffer,0,BUFFER_SIZE);
if(hdr.PacketType==STUNAME)
{
ret=recv(m_s,m_pRecvBuffer,hdr.Len,0);//接收姓名。
if(ret==SOCKET_ERROR)
{
return false;
}
m_pMainDlg->m_StuName=m_pRecvBuffer;
}
else if(hdr.PacketType==LOGINFAILED)
{
MessageBox(NULL,"考生号输入有误或该帐号已经登陆!","登录失败",MB_OK);
return false;
}
return true;
}
//接收试卷。
bool CClientSocket::RecvPaper()
{
HDR hdr;
int ret=recv(m_s,(char*)&hdr,sizeof(HDR),0);
if(ret==SOCKET_ERROR)
{
return false;
}
if(hdr.PacketType==PAPER)
{
memset(m_pRecvBuffer,0,BUFFER_SIZE);
ret=recv(m_s,m_pRecvBuffer,hdr.Len,0);
if(ret==SOCKET_ERROR)
{
return false;
}
m_pMainDlg->m_PaperBuffer=m_pRecvBuffer;
}
return true;
}
//发送开始答题消息。
bool CClientSocket::SendStart()
{
HDR hdr;
hdr.PacketType=REQUEST;
hdr.Len=sizeof(USHORT);
int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。
if(ret==SOCKET_ERROR)
{
return false;
}
memset(m_pSendBuffer,0,BUFFER_SIZE);
USHORT State=DOING;
strncpy(m_pSendBuffer,(char*)&State,sizeof(USHORT));//构造两字节的状态。为准备答题。
ret=send(m_s,m_pSendBuffer,sizeof(USHORT),0);//DOING状态。
if(ret==SOCKET_ERROR)
{
return false;
}
m_pMainDlg->SetTimer(1,10,NULL);
return true;
}
//发送答题结果。
bool CClientSocket::SendResult()
{
HDR hdr;
hdr.PacketType=REQUEST;
hdr.Len=sizeof(USHORT)+m_pMainDlg->m_result.GetLength();
int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。
if(ret==SOCKET_ERROR)
{
return false;
}
memset(m_pSendBuffer,0,BUFFER_SIZE);
USHORT State=DONE;
//strncpy(m_pSendBuffer,(char*)&State,sizeof(USHORT));//交卷状态。
// strncpy(m_pSendBuffer+sizeof(USHORT),m_pMainDlg->m_result.GetBuffer(),m_pMainDlg->m_result.GetLength());
ret=send(m_s,(char*)&State,sizeof(USHORT),0);
ret=send(m_s,m_pMainDlg->m_result.GetBuffer(),m_pMainDlg->m_result.GetLength(),0);//状态后为答题结果。。
if(ret==SOCKET_ERROR)
{
return false;
}
return true;
}
//发送心跳包。
bool CClientSocket::SendPulse()
{
HDR hdr;
hdr.PacketType=PULSE;
hdr.Len=0;
int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。
if(ret==SOCKET_ERROR)
{
return false;
}
return true;
}
DlgView类执行初始化答题窗口、生成试卷、发送心跳包
声明为:
public:
afx_msg void OnBnClickedBtnOk();//登录消息响应函数。
bool ParsePaperBuffer();//扫描试卷缓冲区,取出试题。
bool StartTheExam();//开始考试。
static DWORD WINAPI QuesationDlgThread(PVOID pparam);
public:
short m_port;//服务器端口。
DWORD m_ServerIP;//服务器IP。
CString m_StuNo;//考生号。
CString m_StuName;//从服务器接收的考生姓名。
std::listm_PaperItemList;//试题链表。每个节点存储一道试题。
CTimeSpan *m_pTimeRemained;//剩余时间。考试时间为60min.
CString m_PaperBuffer;//从服务器接收的试卷缓冲区。
CString m_result;//存储答题结果。
CClientSocket *m_pClientSocket;//CCClientSocket指针。
HANDLE m_hEvent;//事件对象,用于在上一题下一题时触发。
CQuestionDlg*m_pQuestionDlg;//答题窗口。
std::list::iterator m_IterCurQuestion;
bool m_IsThreadExit;//决定试题循环线程是否退出。
afx_msg void OnTimer(UINT_PTR nIDEvent);//考试时间计时。
};
类实现为:
//登录按钮消息响应函数。
void CDriverLisenceClientDlg::OnBnClickedBtnOk()
{
UpdateData();
m_pClientSocket=new CClientSocket(this,m_ServerIP,m_port);
bool ret=m_pClientSocket->ConnectToServer();
//ASSERT(ret);
if(!ret)
{
MessageBox("服务器未开启,登录失败!","服务器未开启");
return ;
}
ret=m_pClientSocket->LoginServer();
//ASSERT(ret);
if(!ret)
{
//MessageBox("帐号有误或重复登录,登录失败!","登录失败");
return ;
}
ret=m_pClientSocket->RecvName();
//ASSERT(ret);
if(!ret)
{
MessageBox("数据接收出现错误,请检查网络连接!!","数据接收出现错误");
return ;
}
ret=m_pClientSocket->SendStart();
if(!ret)
{
MessageBox("数据发送出现错误,请检查网络连接!!","数据接收出现错误");
return ;
}
ret=m_pClientSocket->RecvPaper();
//ASSERT(ret);
if(!ret)
{
MessageBox("数据接收出现错误,请检查网络连接!!","数据接收出现错误");
return ;
}
ParsePaperBuffer();
StartTheExam();
SetTimer(2,1000,NULL);
}
//扫描试题缓冲区,将试题存入试题链表内。
bool CDriverLisenceClientDlg::ParsePaperBuffer()
{
CPaperItem *pItem;
char c;
int i=0;
int count=0;
while(i!=m_PaperBuffer.GetLength())
{
c=m_PaperBuffer[i];
if(c=='<')
{
count=0;
pItem=new CPaperItem;
}
else if(c=='>')
{
m_PaperItemList.push_back(pItem);
}
else if(c=='|')
{
count++;
}
else
{
if(count==0)
{
pItem->m_Question+=c;
}
else if(count==1)
{
pItem->m_A+=c;
}
else if(count==2)
{
pItem->m_B+=c;
}
else if(count==3)
{
pItem->m_C+=c;
}
else
{
pItem->m_D+=c;
}
}
i++;
}
return true;
}
//开始考试。
bool CDriverLisenceClientDlg::StartTheExam()
{
ShowWindow(SW_HIDE);
DWORD num=m_PaperItemList.size();
m_pQuestionDlg=new CQuestionDlg;
bool ret=m_pQuestionDlg->Create(IDD_DLG_PAPER,this);
ret=m_pQuestionDlg->ShowWindow(SW_SHOW);
HANDLE hThread=CreateThread(NULL,0,QuesationDlgThread,this,0,NULL);
CloseHandle(hThread);
CString s;
s.Format(" 驾驶员理论考试! 试题数:%d",num);
//CString s;
m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_NOTIFICATION,s);
s.Format("姓名:%s",m_StuName);
m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_STUNAME,s);
s.Format("考号:%s",m_StuNo);
m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_STUNO,s);
//dlg.DoModal();
return true;
}
//在此线程循环内不断遍历试题,显示到对话框。
DWORD WINAPI CDriverLisenceClientDlg::QuesationDlgThread( PVOID pparam )
{
CDriverLisenceClientDlg*pDlg=(CDriverLisenceClientDlg*)pparam;
((CButton*)(pDlg->m_pQuestionDlg->GetDlgItem(IDC_BTN_HANDIN)))->EnableWindow(false);
pDlg->m_IsThreadExit=false;
//for(std::list::iterator iter=pDlg->m_PaperItemList.begin();iter!=pDlg->m_PaperItemList.end();iter=pDlg->m_IterCurQuestion)//iter++)
for(std::list::iterator iter=pDlg->m_PaperItemList.begin();!pDlg->m_IsThreadExit;iter=pDlg->m_IterCurQuestion)//iter++)
{
if(pDlg->m_PaperItemList.end()!=iter)
{
CPaperItem *pPaperItem=(*iter);
pDlg->m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_QUESTION,pPaperItem->m_Question);
pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_A,pPaperItem->m_A);
pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_B,pPaperItem->m_B);
pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_C,pPaperItem->m_C);
pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_D,pPaperItem->m_D);
pDlg->m_IterCurQuestion=iter;
}
WaitForSingleObject(pDlg->m_hEvent,INFINITE);//等待上一题或下一题触发事件消息。
}
return 0;
}
//发送心跳包。
void CDriverLisenceClientDlg::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
if(nIDEvent==1)
m_pClientSocket->SendPulse();
else if(nIDEvent==2)
{
CTimeSpan temp(0,0,0,1);
(*m_pTimeRemained)-=temp;
CString t;
t.Format("剩余时间:%d分%d秒",m_pTimeRemained->GetMinutes(),m_pTimeRemained->GetSeconds());
m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_TIMEREMAINED,t);
}
CDialogEx::OnTimer(nIDEvent);
}
客户端登录界面:
答题窗口: