转载自:http://blog.csdn.net/ithzhang/article/details/8532711


Windows socket IO完成端口开发驾照理论考试系统实例

这一节我们讲解如何利用套接字完成端口开发驾照理论考试系统。
该系统由服务器和客户端两部分组成。

服务器负责对题库和学生信息的管理,主要包括以下功能: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  //登录失败。</span>

考试系统的试题类型为选择题。它包括试题和答案两部分。题号与问题之间使用:分割。每道题有四个答案,每个答案之间用|分割。试题之间用<>分割。如
<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 heresDsnStuInfo.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;i<SERVICE_NUMBER;i++){m_h[i]=CreateThread(NULL,0,ServiceThread,this,0,NULL);}//设置计时器。 客户端会每隔5秒,向服务器发送心跳包。计时器每个1分钟检查每个客户端。//看当前时间与客户端发送的最近一次心跳包是否大于1分钟。如大于说明客户端已断开。SetTimer(1,10000,NULL);return true;
}bool CIOCPDriverLisenceExamServerView::StopService()
{m_IsRunning=false;return true;
}DWORD WINAPI CIOCPDriverLisenceExamServerView::ServiceThread( PVOID ppram )
{CIOCPDriverLisenceExamServerView*pServer=(CIOCPDriverLisenceExamServerView*)ppram;
//  IO_OPERATION_DATA *pio_operation_data;LPOVERLAPPED lpoverlapped;CClient *pClient;DWORD transferred;while(pServer->m_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.GetRem_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_numOfPaperItem;j++){m_pPaperArray[j].PAP_ID=paperRecordSet.m_PAP_ID;m_pPaperArray[j].PAP_QUESTION=paperRecordSet.m_PAP_QUESTION;m_pPaperArray[j].PAP_ANS_A=paperRecordSet.m_PAP_ANS_A;m_pPaperArray[j].PAP_ANS_B=paperRecordSet.m_PAP_ANS_B;m_pPaperArray[j].PAP_ANS_C=paperRecordSet.m_PAP_ANS_C;m_pPaperArray[j].PAP_ANS_D=paperRecordSet.m_PAP_ANS_D;m_pPaperArray[j].PAP_ANSWER=paperRecordSet.m_PAP_ANSWER;paperRecordSet.MoveNext();}return true;
}int CIOCPDriverLisenceExamServerView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{if (CView::OnCreate(lpCreateStruct) == -1)return -1;// TODO:  在此添加您专用的创建代码CRect      rect; GetClientRect(&rect);m_listCrtl.Create(WS_CHILD|WS_VISIBLE|WS_BORDER|LVS_REPORT,rect,this,1); //m_listCrtl.SetBkColor(RGB(255,255,255)); //m_listCrtl.SetTextColor(RGB(0,0,0)); //m_listCrtl.SetTextBkColor(RGB(117,151,240)); m_listCrtl.SetExtendedStyle(LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES|LVS_EX_HEADERDRAGDROP); int     cxScreen     =     ::GetSystemMetrics(SM_CXSCREEN); //获得屏幕宽 int     cyScreen     =     ::GetSystemMetrics(SM_CYSCREEN); //获得屏幕高 rect.top=0;rect.bottom = cyScreen;rect.left = 0;rect.right =cxScreen ;//MoveWindow(rect);m_listCrtl.MoveWindow(rect); //}}控件跟随窗口大小变化m_listCrtl.InsertColumn(0,_T("考 生   ID"),LVCFMT_LEFT,100); m_listCrtl.InsertColumn(1,_T("考      号"),LVCFMT_LEFT,100); m_listCrtl.InsertColumn(2,_T("姓      名"),LVCFMT_LEFT,150); m_listCrtl.InsertColumn(3,_T("考 试 状 态"),LVCFMT_LEFT,100); m_listCrtl.InsertColumn(4,_T("考 试 成 绩"),LVCFMT_LEFT,100); return 0;
}bool CIOCPDriverLisenceExamServerView::ReadStuInfo()
{CStuInfo StuInfo(&m_StuInfoDB);if(StuInfo.IsOpen()){StuInfo.Close();}StuInfo.Open();int i=0;while(!StuInfo.IsEOF()){m_listCrtl.InsertItem(i,StuInfo.m_StuID);m_listCrtl.SetItemText(i,1,StuInfo.m_StuNo);m_listCrtl.SetItemText(i,2,StuInfo.m_StuName);m_listCrtl.SetItemText(i,3,StuInfo.m_StuState);i++;StuInfo.MoveNext();}return true;
}
//构造试题,准备发送。
void CIOCPDriverLisenceExamServerView::ContructPaperBuffer()
{for(int i=0;i<m_numOfPaperItem;i++){CString temp;temp.Format("<%d:%s|%s|%s|%s|%s>",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<CClient*>::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();}elseiter++;}}LeaveCriticalSection(&g_clientManager.m_cs);CView::OnTimer(nIDEvent);
}void CIOCPDriverLisenceExamServerView::UpdateClientState( CClient *pClient,USHORT state )
{int i=0;for(i=0;i<m_listCrtl.GetItemCount();i++){if(pClient->m_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;i<m_listCrtl.GetItemCount();i++){if(pClient->m_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<CClient*>::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;elsereturn true;
}

CClient类。

CClient类来实现服务器与客户端的通信。类的构造函数为套接字和CCServerView*指针。有了CCServerView指针,CClient对象就可以调用相关函数修改考生信息。在析构函数中关闭套接字。

在该类中定义了一些与客户端通信的函数。类中有两个WSAOVERLAPPED扩展结构的变量,分别对应接收和发送数据异步操作。m_time表示接收到客户端心跳包的事件。

m_state表示客户端的当前状态。它是state枚举类型。定义如下:

enum state
{LOGIN,//已登录状态。DOING,//正在答题状态。DONE,//已交卷状态。DISCONN//故障掉线。
};

CClient有一个函数用以接收包头,接收包头后,根据包头的类型,包体长度,在调用不同的函数接收包体。这在使用Windows socket开发中经常使用。这样的结构可以使程序结构变得更清晰。

由于接收和发送数据异步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;//recvIO_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;i<m_pServerView->m_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<list>
class CClientManager
{
public:CClientManager(void);~CClientManager(void);
public:std::list<CClient*>m_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<CClient*>::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<<CClient*>::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<CClient*>::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<CClient*>::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();}elseiter++;}}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::list<CPaperItem*>m_PaperItemList;//试题链表。每个节点存储一道试题。CTimeSpan *m_pTimeRemained;//剩余时间。考试时间为60min.CString m_PaperBuffer;//从服务器接收的试卷缓冲区。CString m_result;//存储答题结果。CClientSocket *m_pClientSocket;//CCClientSocket指针。HANDLE m_hEvent;//事件对象,用于在上一题下一题时触发。CQuestionDlg*m_pQuestionDlg;//答题窗口。std::list<CPaperItem*>::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<CPaperItem*>::iterator iter=pDlg->m_PaperItemList.begin();iter!=pDlg->m_PaperItemList.end();iter=pDlg->m_IterCurQuestion)//iter++)for(std::list<CPaperItem*>::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);
}

客户端登录界面:

答题窗口:

对程序流程不是很清楚的话,对上面这些东西会觉得晕乎乎的。如果想要非常彻底的对IOCP及程序流程有更深的理解,推荐自己动手实现这样一个系统。在动手过程中会遇到很多困难,但解决这些困难会不断加深你的理解。当你最终靠着自己努力实现完成后,你会对先前迷惑的东西恍然大悟的。相信我!!

以上实例参考自《精通Windows socekt网络开发–基于Visual C++实现》但做不少的修改。这个示例更偏重于对IOCP的讲解,因此没有对程序逻辑太过重视。程序有一处已知的bug:虽然实现了上一题下一题功能,但是必须按顺序一次做完。回来修改不起任何作用,还会导致答案混乱。虽知道解决方法但不做修改。客户端由于实现时非常简单,只是草草做了下。有很多不太合适的地方。特此提示。

By ithzhang 转载请注明出处。
2013.1.13于山西大同

Windows socket之IOCP实例----IOCP开发驾照理论考试系统相关推荐

  1. java实现模拟考试系统,基于jsp的驾照模拟考试系统a-JavaEE实现驾照模拟考试系统a - java项目源码...

    基于jsp+servlet+pojo+mysql实现一个javaee/javaweb的驾照模拟考试系统a, 该项目可用各类java课程设计大作业中, 驾照模拟考试系统a的系统架构分为前后台两部分, 最 ...

  2. java实现模拟考试系统,Java Web应用开发—— 在线模拟考试系统-毕业设计

    Java Web应用开发-- 在线模拟考试系统-毕业设计 毕毕 业业 设设 计(论计(论 文)文) 题题 目目 Java Web 应用开发 在线模拟考试系统的设计与实现 学学 院院 计算机学院 专专 ...

  3. 基于Java与MySQL开发的在线考试系统

    1 概述 1.1 简介 在Exam++考试系统第一版的基础上,我们对ExamStack V2.0进行了大量代码重构,同时也对数据模型做了部分调整.为了减小学员考试交卷时大量并发带来的系统风险,我们尝试 ...

  4. Web前端中级认证php,1+x 证书 Web 前端开发中级理论考试 (试卷 6 )

    1+x 证书 Web 前端开发中级理论考试 (试卷 6 ) 1+x 证书 web 前端开发中级理论考试(试卷 6 ) 官方 QQ 群 1+x 证书 Web 前端开发初级对应课程分析 http://bl ...

  5. 下面关于html的描述正确的一项是,1 x 证书 Web 前端开发初级理论考试试卷五(2)...

    1+x 证书 web 前端开发初级对应课程分析 http://blog.zh66.club/index.php/archives/194/ 1+X 证书 Web 前端开发中级对应课程分析 http:/ ...

  6. xslt 标签取集合第一条数据_1+x证书Web前端开发中级理论考试(试卷1)

    2019年下半年 Web前端开发中级 理论考试 (考试时间19:00-20:30 共150分钟,测试卷1) 本试卷共3道大题,满分100分. 请在指定位置作答. 一.单选题(每小题2分,共30小题,共 ...

  7. 基于SSM整合的驾照在线考试系统

    基于SSM整合的驾照在线考试系统 项目描述 主要功能有: 1.用户功能模块 用户注册登 用户可以通过用户名邮箱注册网站,并且通过注册的用户登陆网站. 2.随机练习 从题库中随机取出指定数量的题目供学员 ...

  8. 1+x证书Web前端开发中级理论考试(试卷1)

    2019年下半年 Web前端开发中级 理论考试 (考试时间19:00-20:30 共150分钟,测试卷1) 本试卷共3道大题,满分100分. 请在指定位置作答. 一.单选题(每小题2分,共30小题,共 ...

  9. 驾照理论考试android应用

    驾驶员理论考试系统 ◇软件简介:适用于A.B.C类的驾照类型,共850题.其中通用725题,客车专用57题,货车专用68题. ◇章节练习:按章节做练习 ◇顺序练习:有记忆功能.可以记住上次考试的题号. ...

  10. 驾照理论考试速成软件 MFC源代码下载

    驾照理论考试速成,模访一个软件做成的 数据库是mssql2005,在JzData.rar文件中,附加即可; 如果你使用的是sql 2000的数据库,请将debug目录下的access数据库jzdata ...

最新文章

  1. 白春礼:探究物质世界奥秘的一把金钥匙——纪念元素周期表发表150周年
  2. DSX2-5000 CH测试结果使用福禄克LinkWare Live软件的好处
  3. 宇宙最強的IDE - Visual Studio 25岁生日快乐
  4. 转 java synchronized详解
  5. Job单线程运行的方案
  6. C# WinForm开发系列
  7. python处理wps表格数据匹配_wps2个excel表格数据匹配-Wps表格怎么自动匹配我需要的数值...
  8. 10块钱闯荡深圳,如今身价935亿,超越李彦宏,他竟如此低调……
  9. 2021年美妆护肤行业电商营销报告
  10. 微信公众平台学习笔记
  11. Python+pyqt4 山寨美图秀秀的美肤功能
  12. Android权限 - AppOps介绍
  13. EDI X12 997 功能性确认 Functional Acknowledgment
  14. 远程调试工具之weinre
  15. 华为云计算HCIE学习笔记-FusionStorage
  16. matlab使用杂谈4-偏微分方程求解之pdede函数使用
  17. Python的P图大法,你值得拥有!
  18. 移动机器人 | 手势识别
  19. 安装2019Pycharm最新版本-详细教程--激活码
  20. 影视感悟专题---2、《大染坊》

热门文章

  1. 关于 MySQL bug 91418 一些看法
  2. Intel公布史上最强Atom;Xilinx发布射频级模拟技术│IoT黑板报
  3. Gym 100015A
  4. c语言将时速转换成配速,配速与时速换算(跑步配速和时速换算)
  5. 西安音乐学院人计算机学院,我校在“中国大学生计算机设计大赛”中获佳绩
  6. 第五课:BCD计数器设计与验证
  7. 常见几种操作系统简介
  8. Layui 数据表格合并单元格
  9. 使用LIS2DH12三轴加速度传感器检测震动与倾斜角度
  10. 当你在搜索框按下enter的一刹那发生了什么?