分享

Windows socket之IOCP实例

 lpg2008 2013-12-11

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

 

这一节我们讲解如何利用套接字完成端口开发驾照理论考试系统。

该系统由服务器和客户端两部分组成。

 

    服务器负责对题库和学生信息的管理,主要包括以下功能:

      1:试卷管理:从题库读取试卷和向客户端发送试卷。

      2:客户端管理(CClientManager类)。从数据库读取学生信息,验证学生信息。

      3:监视考生考试状态。

      4:评分。保存学生考试状态和成绩。

    客户端负责生成试卷,主要包括以下功能:

      1:登录服务器。

      2:生成试卷。

      3:考试计时。考试时间结束考生停止答题。

 

主要步骤如下:

 

     客户端成功连接服务器后,向服务器发送学号。服务器在收到学号后,会验证该学号是否存在于数据库中。如果存在且未登录过,则向客户端发送该学生姓名和试卷。否则,项客户端发送查无此人信息。

     客户端在收到试卷后,向考生显示准备完成,是否开始答题。考生点击开始答题后,客户端向服务器发送开始答卷消息。考生开始答卷,客户端进行计时。考试结束后,客户端向服务器发送答题结果。服务器在收到答题结果后,对该考生的试卷进行评分,并将结果保存在数据库。

    数据包设计:

    为了保证客户端与服务器之间数据的正确发送与接收,在发送数据时,先发送包头后发送包体。包头指明包体的类型和长度。包头定义如下:

 

  1. typedef struct _header  
  2.   
  3. {  
  4.   
  5.    u_short type;//包类型。  
  6.   
  7.    u_short len;//包体长度。  
  8.   
  9. }HEADER;  


 

     客户端发送的数据包类型包括考生状态和心跳包两种。在考试期间为了监控考生状态,客户端定期向服务器发送心跳包,服务器根据此心跳包判断客户端状态。如果由于意外导致连接断开,服务器在一段时间内没有收到心跳包则断定客户端断线。

客户端在发送包头后,发送考生状态,它包括以下几种:

    登录:此时考生状态为考生学号。LOGIN

    答卷:此时考生状态没有数据。DOING

    交卷:此时考生状态为答题结果。DONE

    断线:在服务器一段时间没有收到心跳包后,设置客户端为断线状态。DISCONN

 

    服务器发送的数据包类型包括:

    考生姓名和试卷。

    服务器在验证考生学号后,向客户端发送该考生的姓名和试卷。

   

    工作流程:

    服务器启动后,调用InitSocket初始化监听套接字。读取数据库,将试卷读入内存,为发送做准备。调用启动服务函数,在启动服务函数内创建接受客户端请求线程和服务线程。创建监听线程使用WSAEventSelect模型管理请求套接字。服务线程可以有多个,循环调用GetQueuedCompletionStatus函数,检查是否有异步IO已完成。     

    当有套接字请求时,接受线程接受请求,并创建CClient对象,加入CClientManager管理的客户端链表。并调用CClientAsyRecvHeader执行接收包头异步IO操作。

    当有异步IO完成时,GetQueuedCompletionStatus函数返回,根据IO操作类型IOType决定执行何种操作。

    如果是接收包头异步IO完成,根据包头指定的类型判断是心跳包,或是状态包。

如是状态包,则调用接受包体异步IO函数。在接收包体函数完成后,前两字节指定的状态,执行操作。如果状态是LOGIN,则是登录状态。包体长度为hdr.Len。前2字节为当前状态,两字节后为学号,长度为hdr.Len-2。接收到学号后,从数据库查询如果存在此学号,且此学号未登录则发送姓名。否则发送登录失败。GetQueuedCompletionStatus返回时,收到发送姓名异步IO完成后,发送试卷。客户端收到试卷后对试卷进行解析,生成试卷。

    客户端向服务器发送考试开始包,考试开始,服务器更新考生状态为正在考试。客户端设置一个计时器,每10s向服务器发送一个心跳包,表明当前客户端处于在线状态,并未离线。服务器也定义一个计时器,它每隔60s触发一次,在响应函数内遍历所有连接的客户端,检查它发送心跳包的时间与当前时间差,如果大于60s则说明客户端已掉线。将此客户端从链表中删除。并更新对应考生状态为掉线。

    考生做完所有试题后,点击交卷,客户端向服务器发送DONE包,包体部分为答案。服务器更新考生状态,并计算分数更新到列表控件和服务器。然后删除客户端,整个过程结束。

 

 

  1. <span style="font-size:18px;">//客户端向服务器发送的包类型。  
  2. #define PULSE          107//心跳包。  
  3. #define REQUEST          108//请求包。  
  4.   
  5. //服务器向客户端发送的包类型:  
  6. #define STUNAME        109//学生姓名。  
  7. #define PAPER          110//试卷包。  
  8. #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_IDPAP_QUESTIONPAP_ANSWERAPAP_ANSWERBPAP_ANSWERCPAP_ANSWERDPAP_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类的用户自定义部分为:

 

  1. public:  
  2.     static DWORD WINAPI AcceptClientThread(PVOID ppram);  
  3.     static DWORD WINAPI ServiceThread(PVOID ppram);  
  4.     bool InitSocket();//初始化套接字。  
  5.     bool StartService();//开始服务。  
  6.     bool StopService();//停止服务。  
  7.     CString AuthenticateStuNo(char*);//验证学号。  
  8.     bool ReadPaper();//从数据库读取试卷信息。  
  9.     bool ReadStuInfo();//从数据库读取学生信息。  
  10.     void ContructPaperBuffer();//构造  
  11.     void UpdateClientState(CClient *pClient,USHORT state);  
  12.     void Destroy();  
  13.     bool SaveGradeIntoDB(CClient*pClient);//保存成绩到数据库。  
  14.     bool IsAlreadyLogin(CString StuName);//判断考生是否已登录。  
  15. public:  
  16.     HANDLE m_hIOCP;//完成端口句柄。  
  17.     HANDLE m_hEvent;//监听套接字对应事件对象。  
  18.     HANDLE m_h[SERVICE_NUMBER+1];//线程句柄。  
  19.     SOCKET m_sListenSocket;//监听套接字。  
  20.     bool m_IsRunning;//判断服务器是否运行。  
  21.     CDatabase m_StuInfoDB;//考生信息数据库类。  
  22.     CDatabase m_PaperDB;//试卷数据库类。  
  23.     CListCtrl m_listCrtl;//列表控件。  
  24.     PaperItem *m_pPaperArray;//试题数组。PaperItem定义马上介绍。  
  25.     UINT m_numOfPaperItem;//试卷试题数量。  
  26.     CString m_PaperBuff;//试卷缓冲区。      
  27.       
  28.     afx_msg void OnTimer(UINT_PTR nIDEvent);//计时器函数,用于检查心跳包  
  29.     afx_msg void OnStartService();//服务器开始运行。  

     实现为:

  1. // CIOCPDriverLisenceExamServerView 消息处理程序  
  2.   
  3.   
  4. void CIOCPDriverLisenceExamServerView::OnInitialUpdate()  
  5. {  
  6.     CView::OnInitialUpdate();  
  7.   
  8.     // TODO: 在此添加专用代码和/或调用基类  
  9.     m_hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);  
  10.       
  11.     CString sDriver = TEXT("MICROSOFT ACCESS DRIVER (*.mdb)");  
  12.     CString sDsnStuInfo;  
  13.     CString sFileStuInfo = TEXT("E://DriverLiscenceExam.mdb");//Change path here  
  14.    
  15.     sDsnStuInfo.Format(TEXT("ODBC;DRIVER={%s};DSN='';DBQ=%s"),sDriver,sFileStuInfo);  
  16.   
  17.     bool ret=m_StuInfoDB.Open(NULL,false,false,sDsnStuInfo);  
  18.     if(!ret)  
  19.     {  
  20.         MessageBox(TEXT("连接数据库失败!"));  
  21.     }  
  22.     InitSocket();  
  23.       
  24.     ReadPaper();  
  25.     ReadStuInfo();  
  26.     ContructPaperBuffer();  
  27. }  
  28.   
  29. bool CIOCPDriverLisenceExamServerView::InitSocket()  
  30. {  
  31.     WSAData wsadata;  
  32.     WSAStartup(MAKEWORD(2,2),&wsadata);  
  33.     m_sListenSocket=socket(AF_INET,SOCK_STREAM,0);  
  34.     if(m_sListenSocket==INVALID_SOCKET)  
  35.     {  
  36.         closesocket(m_sListenSocket);  
  37.         WSACleanup();  
  38.         return false;  
  39.     }  
  40.     m_hEvent=WSACreateEvent();  
  41.     WSAEventSelect(m_sListenSocket,m_hEvent,FD_ACCEPT);//为监听套接字设置FD_ACCEPT事件。  
  42.     SOCKADDR_IN addr;  
  43.     addr.sin_family=AF_INET;  
  44.     addr.sin_addr.S_un.S_addr=inet_addr("192.168.1.100");  
  45.     addr.sin_port=htons(4000);  
  46.     int ret=bind(m_sListenSocket,(SOCKADDR*)&addr,sizeof(addr));  
  47.     if(ret==SOCKET_ERROR)  
  48.     {  
  49.         return false;  
  50.     }  
  51.     ret=listen(m_sListenSocket,1000);  
  52.     if(ret==SOCKET_ERROR)  
  53.     {  
  54.         return false;  
  55.     }  
  56. }  
  57.   
  58. DWORD WINAPI CIOCPDriverLisenceExamServerView::AcceptClientThread( PVOID ppram )  
  59. {  
  60.     CIOCPDriverLisenceExamServerView*pServer=(CIOCPDriverLisenceExamServerView*)ppram;  
  61.     SOCKADDR_IN addr;  
  62.     int len=sizeof(addr);  
  63.     while(pServer->m_IsRunning)  
  64.     {  
  65.         int ret=WSAWaitForMultipleEvents(1,&pServer->m_hEvent,false,WSA_INFINITE,false);  
  66.         if(ret==WSA_WAIT_TIMEOUT)  
  67.         {  
  68.             continue;  
  69.         }  
  70.         else  
  71.         {  
  72.             WSANETWORKEVENTS events;  
  73.             int r=WSAEnumNetworkEvents(pServer->m_sListenSocket,pServer->m_hEvent,&events);//重置事件对象。  
  74.             if(r==SOCKET_ERROR)  
  75.             {  
  76.                 break;  
  77.             }  
  78.             if(events.lNetworkEvents&FD_ACCEPT)   
  79.             {  
  80.                 if(events.iErrorCode[FD_ACCEPT_BIT]==0)//发生FD_ACCEPT网络事件。接受客户端请求。  
  81.                 {  
  82.                     SOCKET sAccept=WSAAccept(pServer->m_sListenSocket,(SOCKADDR*)&addr,&len,0,NULL);  
  83.   
  84.                     CClient*pClient=new CClient(sAccept,pServer);  
  85.                     if(CreateIoCompletionPort((HANDLE)sAccept,pServer->m_hIOCP,(ULONG_PTR)pClient,0)==NULL)  
  86.                         return -1;  
  87.                     g_clientManager.addClient(pClient);  
  88.                     //调用接收数据异步IO。  
  89.                     if(!pClient->AsyRecvHeader())  
  90.                         g_clientManager.deleteClient(pClient);//接收数据失败后,将此客户端从链表删除。  
  91.   
  92.                 }  
  93.             }  
  94.               
  95.         }  
  96.           
  97.     }  
  98.     return 0;  
  99. }  
  100.   
  101. bool CIOCPDriverLisenceExamServerView::StartService()  
  102. {  
  103.     m_IsRunning=true;  
  104.     m_h[0]=CreateThread(NULL,0,AcceptClientThread,this,0,NULL);  
  105.     for(int i=1;i<SERVICE_NUMBER;i++)  
  106.     {  
  107.         m_h[i]=CreateThread(NULL,0,ServiceThread,this,0,NULL);  
  108.     }  
  109.     //设置计时器。 客户端会每隔5秒,向服务器发送心跳包。计时器每个1分钟检查每个客户端。  
  110.     //看当前时间与客户端发送的最近一次心跳包是否大于1分钟。如大于说明客户端已断开。  
  111.     SetTimer(1,10000,NULL);  
  112.     return true;  
  113. }  
  114.   
  115. bool CIOCPDriverLisenceExamServerView::StopService()  
  116. {  
  117.     m_IsRunning=false;  
  118.     return true;  
  119. }  
  120.   
  121. DWORD WINAPI CIOCPDriverLisenceExamServerView::ServiceThread( PVOID ppram )  
  122. {  
  123.     CIOCPDriverLisenceExamServerView*pServer=(CIOCPDriverLisenceExamServerView*)ppram;  
  124. //  IO_OPERATION_DATA *pio_operation_data;  
  125.     LPOVERLAPPED lpoverlapped;  
  126.     CClient *pClient;  
  127.     DWORD transferred;  
  128.     while(pServer->m_IsRunning)  
  129.     {  
  130.         bool ret=GetQueuedCompletionStatus(pServer->m_hIOCP,&transferred,(LPDWORD)&pClient,&lpoverlapped,WSA_INFINITE);  
  131.         if(ret&&lpoverlapped&&pClient)//成功的异步IO完成。根据从lpoverlapped中得到的类型,进行操作。  
  132.         {  
  133.             IO_OPERATION_DATA*pIO=(IO_OPERATION_DATA*)lpoverlapped;  
  134.             switch(pIO->IOType)  
  135.             {  
  136.             case IOReadHead:  
  137.                 {  
  138.                     pClient->AsyRecvHeaderCompleted();  
  139.                 }  
  140.                 break;  
  141.             case IOReadBody:  
  142.                 {  
  143.                     pClient->AsyRecvBodyCompleted();  
  144.                 }  
  145.                 break;  
  146.             case IOWritePaper:  
  147.                 {  
  148.                     //试卷发送完毕。不执行动作。  
  149.                     pServer->UpdateClientState(pClient,CClient::LOGIN);  
  150.   
  151.                 }  
  152.                 break;  
  153.             case IOWriteName:  
  154.                 {  
  155.                     pClient->AsySendPaper();  
  156.                 }  
  157.                 break;  
  158.             case IOWriteUnLogin:  
  159.                 {  
  160.                     g_clientManager.deleteClient(pClient);  
  161.                 }  
  162.                 break;  
  163.             default:  
  164.                 break;  
  165.             }  
  166.               
  167.         }  
  168.     }  
  169.     return 0;  
  170. }  
  171.   
  172. CString CIOCPDriverLisenceExamServerView::AuthenticateStuNo( char*pStuNo)  
  173. {  
  174.     //TCHAR wStuNo[128];  
  175.     //ZeroMemory(wStuNo,128);  
  176.     //MultiByteToWideChar(CP_ACP,MB_COMPOSITE,pStuNo,strlen(pStuNo),wStuNo,128);//TCP是基于字节流的,而此程序是使用Unicode编码。因此需要转换。  
  177.     CStuInfo StuInfo(&m_StuInfoDB);  
  178.     if(StuInfo.IsOpen())  
  179.     {  
  180.         StuInfo.Close();  
  181.     }  
  182.     StuInfo.m_strFilter.Format(TEXT("StuNo='%s'"),pStuNo);  
  183.     StuInfo.Open();  
  184.     if(StuInfo.IsEOF())//数据库中未搜索到该用户。该账号不存在。  
  185.     {  
  186.         //MessageBox(TEXT("未找到该用户。"));  
  187.         return CString();  
  188.     }  
  189.     else  
  190.     {  
  191.         //MessageBox(TEXT("找到该用户。"));  
  192.   
  193.         return CString(StuInfo.m_StuName);  
  194.     }  
  195.   
  196.       
  197. }  
  198.   
  199. bool CIOCPDriverLisenceExamServerView::ReadPaper()  
  200. {  
  201.     CPaper paperRecordSet(&m_PaperDB);  
  202.     if(paperRecordSet.IsOpen())  
  203.     {  
  204.         paperRecordSet.Close();  
  205.     }  
  206.     paperRecordSet.Open();  
  207.     //long numInDB=paperRecordSet.GetRecordCount();  
  208.     //paperRecordSet.GetRe  
  209.       
  210.     m_numOfPaperItem=0;  
  211.     while(!paperRecordSet.IsEOF())  
  212.     {  
  213.         m_numOfPaperItem++;  
  214.         paperRecordSet.MoveNext();  
  215.     }  
  216.     paperRecordSet.MoveFirst();  
  217.     m_pPaperArray=new PaperItem[m_numOfPaperItem];  
  218.     if(!m_pPaperArray)  
  219.         return false;  
  220.     for(int j=0;j<m_numOfPaperItem;j++)  
  221.     {  
  222.         m_pPaperArray[j].PAP_ID=paperRecordSet.m_PAP_ID;  
  223.         m_pPaperArray[j].PAP_QUESTION=paperRecordSet.m_PAP_QUESTION;  
  224.         m_pPaperArray[j].PAP_ANS_A=paperRecordSet.m_PAP_ANS_A;  
  225.         m_pPaperArray[j].PAP_ANS_B=paperRecordSet.m_PAP_ANS_B;  
  226.         m_pPaperArray[j].PAP_ANS_C=paperRecordSet.m_PAP_ANS_C;  
  227.         m_pPaperArray[j].PAP_ANS_D=paperRecordSet.m_PAP_ANS_D;  
  228.         m_pPaperArray[j].PAP_ANSWER=paperRecordSet.m_PAP_ANSWER;  
  229.         paperRecordSet.MoveNext();  
  230.     }  
  231.       
  232.     return true;  
  233. }  
  234.   
  235.   
  236. int CIOCPDriverLisenceExamServerView::OnCreate(LPCREATESTRUCT lpCreateStruct)  
  237. {  
  238.     if (CView::OnCreate(lpCreateStruct) == -1)  
  239.         return -1;  
  240.   
  241.     // TODO:  在此添加您专用的创建代码  
  242.     CRect      rect;   
  243.     GetClientRect(&rect);  
  244.     m_listCrtl.Create(WS_CHILD|WS_VISIBLE|WS_BORDER|LVS_REPORT,rect,this,1);   
  245.     //m_listCrtl.SetBkColor(RGB(255,255,255));   
  246.     //m_listCrtl.SetTextColor(RGB(0,0,0));   
  247.     //m_listCrtl.SetTextBkColor(RGB(117,151,240));   
  248.     m_listCrtl.SetExtendedStyle(LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES|LVS_EX_HEADERDRAGDROP);   
  249.     int     cxScreen     =     ::GetSystemMetrics(SM_CXSCREEN); //获得屏幕宽   
  250.     int     cyScreen     =     ::GetSystemMetrics(SM_CYSCREEN); //获得屏幕高   
  251.     rect.top=0;  
  252.     rect.bottom = cyScreen;  
  253.     rect.left = 0;  
  254.     rect.right =cxScreen ;  
  255.     //MoveWindow(rect);  
  256.     m_listCrtl.MoveWindow(rect);   
  257.     //}}控件跟随窗口大小变化  
  258.     m_listCrtl.InsertColumn(0,_T("考 生   ID"),LVCFMT_LEFT,100);   
  259.     m_listCrtl.InsertColumn(1,_T("考      号"),LVCFMT_LEFT,100);   
  260.     m_listCrtl.InsertColumn(2,_T("姓      名"),LVCFMT_LEFT,150);   
  261.     m_listCrtl.InsertColumn(3,_T("考 试 状 态"),LVCFMT_LEFT,100);   
  262.     m_listCrtl.InsertColumn(4,_T("考 试 成 绩"),LVCFMT_LEFT,100);   
  263.   
  264.     return 0;  
  265. }  
  266.   
  267. bool CIOCPDriverLisenceExamServerView::ReadStuInfo()  
  268. {  
  269.     CStuInfo StuInfo(&m_StuInfoDB);  
  270.     if(StuInfo.IsOpen())  
  271.     {  
  272.         StuInfo.Close();  
  273.     }  
  274.     StuInfo.Open();  
  275.     int i=0;  
  276.     while(!StuInfo.IsEOF())  
  277.     {  
  278.   
  279.   
  280.         m_listCrtl.InsertItem(i,StuInfo.m_StuID);  
  281.         m_listCrtl.SetItemText(i,1,StuInfo.m_StuNo);  
  282.         m_listCrtl.SetItemText(i,2,StuInfo.m_StuName);  
  283.         m_listCrtl.SetItemText(i,3,StuInfo.m_StuState);  
  284.         i++;  
  285.         StuInfo.MoveNext();  
  286.     }  
  287.     return true;  
  288. }  
  289. //构造试题,准备发送。  
  290. void CIOCPDriverLisenceExamServerView::ContructPaperBuffer()  
  291. {  
  292.     for(int i=0;i<m_numOfPaperItem;i++)  
  293.     {  
  294.         CString temp;  
  295.         temp.Format("<%d:%s|%s|%s|%s|%s>",m_pPaperArray[i].PAP_ID,m_pPaperArray[i].PAP_QUESTION  
  296.             ,m_pPaperArray[i].PAP_ANS_A,m_pPaperArray[i].PAP_ANS_B,m_pPaperArray[i].PAP_ANS_C,m_pPaperArray[i].PAP_ANS_D);  
  297.         m_PaperBuff+=temp;  
  298.     }  
  299. }  
  300.   
  301.   
  302. void CIOCPDriverLisenceExamServerView::OnTimer(UINT_PTR nIDEvent)  
  303. {  
  304.     // TODO: 在此添加消息处理程序代码和/或调用默认值  
  305.     EnterCriticalSection(&g_clientManager.m_cs);  
  306.     for(std::list<CClient*>::iterator iter=g_clientManager.m_ClientList.begin();iter!=g_clientManager.m_ClientList.end();)  
  307.     {  
  308.         CClient*pClient=(*iter);  
  309.         if(pClient->m_state==CClient::DOING)  
  310.         {  
  311.             CTime CurTime=CTime::GetCurrentTime();  
  312.             CTimeSpan spanTime(CurTime.GetDay()-pClient->m_time.GetDay(),  
  313.                 CurTime.GetHour()-pClient->m_time.GetHour(),  
  314.                 CurTime.GetMinute()-pClient->m_time.GetMinute(),CurTime.GetSecond()-pClient->m_time.GetSecond());  
  315.             if(spanTime.GetMinutes()>1)  
  316.             {  
  317.                 //设置考生为断线状态。  
  318.                 pClient->m_state=CClient::DISCONN;  
  319.                 //更新显示状态。  
  320.                 UpdateClientState(pClient,CClient::DISCONN);  
  321.                 //删除该客户端。  
  322.                 g_clientManager.deleteClient(pClient);  
  323.                 iter=g_clientManager.m_ClientList.begin();  
  324.             }  
  325.             else  
  326.                iter++;  
  327.         }  
  328.     }  
  329.     LeaveCriticalSection(&g_clientManager.m_cs);  
  330.     CView::OnTimer(nIDEvent);  
  331. }  
  332.   
  333. void CIOCPDriverLisenceExamServerView::UpdateClientState( CClient *pClient,USHORT state )  
  334. {  
  335.     int i=0;  
  336.     for(i=0;i<m_listCrtl.GetItemCount();i++)  
  337.     {  
  338.         if(pClient->m_StuName==m_listCrtl.GetItemText(i,2))  
  339.         {  
  340.             break;  
  341.         }  
  342.     }  
  343.     CString s;  
  344.     switch(state)  
  345.     {  
  346.     case CClient::LOGIN:  
  347.         s="已登录";  
  348.         break;  
  349.     case CClient::DOING:  
  350.         s="正在答题";  
  351.         break;  
  352.     case CClient::DONE:  
  353.         {  
  354.             s="已交卷";  
  355.             SaveGradeIntoDB(pClient);  
  356.         }  
  357.         break;  
  358.     case CClient::DISCONN:  
  359.         s="掉线";  
  360.         break;  
  361.     default:  
  362.         s="未知状态";  
  363.         break;  
  364.     }  
  365.     m_listCrtl.SetItemText(i,3,s);  
  366. }  
  367.   
  368. void CIOCPDriverLisenceExamServerView::Destroy()  
  369. {  
  370.     KillTimer(1);  
  371. }  
  372.   
  373.   
  374. void CIOCPDriverLisenceExamServerView::OnStartService()  
  375. {  
  376.     // TODO: 在此添加命令处理程序代码  
  377.     StartService();  
  378. }  
  379.   
  380. bool CIOCPDriverLisenceExamServerView::SaveGradeIntoDB(CClient*pClient)  
  381. {  
  382.     //在列表空间更新分数。  
  383.     int i=0;  
  384.     for(i=0;i<m_listCrtl.GetItemCount();i++)  
  385.     {  
  386.         if(pClient->m_StuName==m_listCrtl.GetItemText(i,2))  
  387.         {  
  388.             //m_listCrtl.SetItemText(i,4);  
  389.             break;  
  390.         }  
  391.     }  
  392.     CString s;  
  393.     s.Format("%d",pClient->m_grade);  
  394.     m_listCrtl.SetItemText(i,4,s);  
  395.     //在数据库更新分数。  
  396.     CStuInfo StuInfo(&m_StuInfoDB);  
  397.     if(StuInfo.IsOpen())  
  398.     {  
  399.         StuInfo.Close();  
  400.     }  
  401.     StuInfo.m_strFilter.Format(TEXT("StuNo='%s'"),pClient->m_StuNo);  
  402.     StuInfo.Open();  
  403.     if(!StuInfo.IsEOF())//数据库中未搜索到该用户。该账号不存在。  
  404.     {  
  405.         StuInfo.Edit();  
  406.         StuInfo.m_StuGrade=pClient->m_grade;  
  407.         StuInfo.m_StuState="已考试";  
  408.         StuInfo.Update();  
  409.         StuInfo.Close();  
  410.         g_clientManager.deleteClient(pClient);  
  411.         return true;  
  412.     }  
  413.       
  414.     return false;  
  415. }  
  416.   
  417. bool CIOCPDriverLisenceExamServerView::IsAlreadyLogin( CString StuName )  
  418. {  
  419.     std::list<CClient*>::iterator iter;  
  420.     for(iter=g_clientManager.m_ClientList.begin();iter!=g_clientManager.m_ClientList.end();iter++)  
  421.     {  
  422.         if((*iter)->m_StuName==StuName)  
  423.             break;  
  424.     }  
  425.     if(iter==g_clientManager.m_ClientList.end())  
  426.         return false;  
  427.     else  
  428.         return true;  
  429. }  


 


 

     CClient类。

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

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

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

  1. enum state  
  2.   
  3. {  
  4.   
  5.   LOGIN,//已登录状态。  
  6.   
  7.   DOING,//正在答题状态。  
  8.   
  9.   DONE,//已交卷状态。  
  10.   
  11.   DISCONN//故障掉线。  
  12.   
  13. };  


 

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

     由于接收和发送数据异步IO完成的时间不确定,每个异步IO函数,还分别对应一个完成函数。AsyRecvHeaderAsyRecvBody仅仅用于发出异步IO请求,而对应的完成函数用以处理在接收数据完成后的工作。这种编程方式要特别注意。

     每一个异步IO请求对应着一个OVERLAPPED结构和一个完成键。我们常常会重新定义OVERLAPPED结构,并传给完成键对我们有用的信息,这样在GetQueuedCompletionStatus返回时就会得到更多的信息。

重新定义后的OVERLAPPED结构信息为:

 

  1. typedef struct _IO_OPERATION_DATA  
  2.   
  3. {  
  4.   
  5.    OVERLAPPED overlapped;  
  6.   
  7.    char buffer[BUFFER_SiZE];  
  8.   
  9.    byte IOType;//IO类型。用以得知哪种异步IO完成。收or发。  
  10.   
  11. }IO_OPERATION_DATA;  


 

     在每个异步IO完成时,都可以根据IO类型得到是什么IO完成。一定要区分IO类型和包类型。这很重要。

     IO类型有以下几种:

 

  1. #define IOReadHead     100//接收包头完成,异步IO完成。  
  2.   
  3.  #define IOReadBody     101//接收包体完成,异步IO完成。  
  4.   
  5.  #define IOWriteUnLogin 103//登录失败。  
  6.   
  7.  #define IOWritePaper   104//发送试卷完成,异步IO完成。  
  8.   
  9.  #define IOExit         105//退出。  
  10.   
  11.  #define IOWriteName    106//发送姓名异步IO完成。  


 

     定义包头。服务器和客户端互相通信离不开包头。包头可以指定,数据包的类型和包体的长度。

定义如下:

 

  1. typedef struct HEADER  
  2.   
  3. {  
  4.   
  5.    short PacketType;//包类型。  
  6.   
  7.    short Len;//包体长度。  
  8.   
  9. }HDR,*PHDR;  


 

   CClient类。几乎所有前面介绍过的socket程序都有一个CClient类,它用于执行与客户端通信的功能。也是在这个IOCP开发的驾照考试系统最重要的一个类。

声明如下:

 

  1. #pragma once  
  2. #include"WinSock2.h"  
  3. #include"ctime"  
  4.   
  5. class CIOCPDriverLisenceExamServerView;  
  6.   
  7. #define  BUFFER_SIZE 10240  
  8. #define IOReadHead     100  
  9. #define IOReadBody     101  
  10. #define IOWriteUnLogin 103  
  11. #define IOWritePaper   104  
  12. #define IOExit         105  
  13. #define IOWriteName    106  
  14. //客户端向服务器发送的包类型。  
  15. #define PULSE          107//心跳包。  
  16. #define REQUEST          108//请求包。  
  17.   
  18. //服务器向客户端发送的包类型:  
  19. #define STUNAME        109//学生姓名。  
  20. #define PAPER          110//试卷包。  
  21. #define LOGINFAILED    111//登录失败。  
  22.   
  23.   
  24. typedef struct HEADER  
  25. {  
  26.     short PacketType;  
  27.     short Len;  
  28. }HDR,*PHDR;  
  29. typedef struct _IO_OPERATION_DATA  
  30. {  
  31.     WSAOVERLAPPED overlapped;  
  32.     char buffer[BUFFER_SIZE];  
  33.     HDR hdr;  
  34.     byte IOType;  
  35.   
  36. }IO_OPERATION_DATA;  
  37. class CClient  
  38. {  
  39. public:  
  40.     enum state//考生状态。  
  41.     {  
  42.         LOGIN,//已登录。  
  43.         DOING,//正在答题。  
  44.         DONE,//已交卷。  
  45.         DISCONN,//因故障断开。  
  46.         UNKNOW//原因不明。  
  47.     };  
  48.       
  49.     CClient(SOCKET s,CIOCPDriverLisenceExamServerView*pDlg);  
  50.     ~CClient(void);  
  51. public:  
  52.     bool AsyRecvHeader();//接收包头。  
  53.     bool AsyRecvBody(int len);  
  54.     void AsyRecvBodyCompleted();  
  55.   
  56.     bool AsySendName();  
  57.     bool AsySendPaper();  
  58.     bool AsySendFailedLoginMsg();  
  59.       
  60.     void AsyRecvHeaderCompleted();  
  61.     void CalculateGrade();  
  62. public:  
  63.       
  64. public:  
  65.     SOCKET m_s;  
  66.     IO_OPERATION_DATA m_IoRecv;//recv  
  67.     IO_OPERATION_DATA m_IoSend;  
  68.     USHORT m_state;//考生状态。  
  69.     CTime m_time;//最近一次心跳包时间。  
  70.     CIOCPDriverLisenceExamServerView*m_pServerView;//主窗口指针。  
  71. public:  
  72.     CString m_StuNo;  
  73.     //CString m_StuID;  
  74.     CString m_StuName;  
  75.     long m_grade;  
  76.     CString m_Result;//答题结果。  
  77. };  

 

CClient类实现为:

 

  1. #include "StdAfx.h"  
  2. #include "Client.h"  
  3. #include"IOCPDriverLisenceExamServerView.h"  
  4. CClient::CClient( SOCKET s,CIOCPDriverLisenceExamServerView*pDlg )  
  5. {  
  6.     m_s=s;  
  7.     m_pServerView=pDlg;  
  8.     m_time=CTime::GetCurrentTime();  
  9. }  
  10.   
  11.   
  12. CClient::~CClient(void)  
  13. {  
  14.     closesocket(m_s);  
  15. }  
  16. //读取包头异步函数。  
  17. bool CClient::AsyRecvHeader()  
  18. {  
  19.   
  20.     WSABUF wsabuf;  
  21.     ZeroMemory(&m_IoRecv,sizeof(IO_OPERATION_DATA));  
  22.     m_IoRecv.IOType=IOReadHead;  
  23.     wsabuf.buf=(char*)&m_IoRecv.hdr;  
  24.     wsabuf.len=sizeof(HDR);  
  25.     DWORD flag=0;  
  26.     int ret=WSARecv(m_s,&wsabuf,1,NULL,&flag,&m_IoRecv.overlapped,NULL);  
  27.     if(ret==SOCKET_ERROR)  
  28.     {  
  29.         int err=WSAGetLastError();  
  30.         if(err!=WSA_IO_PENDING)  
  31.         {  
  32.             return false;  
  33.         }  
  34.     }  
  35.     return true;  
  36. }  
  37. //包头接收完毕。  
  38. void CClient::AsyRecvHeaderCompleted()  
  39. {  
  40.     if(m_IoRecv.hdr.PacketType==PULSE)//如果为心跳包。//客户端发送给服务器的包头的PacketType有两种。一种是心跳包,另外是REQUEST包。  
  41.         //在数据的前两个字节会指定当前请求,有LOGIN,DOING ,DONE等。  
  42.     {  
  43.         m_time=CTime::GetCurrentTime();  
  44.         AsyRecvHeader();  
  45.     }  
  46.     else //if(state==m_IoRecv.hdr.PacketType)//状态包。接着接收包体。  
  47.     {  
  48.         AsyRecvBody(m_IoRecv.hdr.Len);  
  49.     }  
  50. }  
  51. //接收包体。  
  52. bool CClient::AsyRecvBody(int len)  
  53. {  
  54.     WSABUF wsabuf;  
  55.     //ZeroMemory(&m_IoRecv.buffer,BUFFER_SIZE);  
  56.     ZeroMemory(&m_IoRecv,sizeof(IO_OPERATION_DATA));  
  57.     m_IoRecv.IOType=IOReadBody;  
  58.     wsabuf.buf=m_IoRecv.buffer;  
  59.     wsabuf.len=len;//接收包体。  
  60.     DWORD flag=0;  
  61.     int ret=WSARecv(m_s,&wsabuf,1,NULL,&flag,&m_IoRecv.overlapped,NULL);  
  62.     if(ret==SOCKET_ERROR)  
  63.     {  
  64.         int err=WSAGetLastError();  
  65.         if(err!=WSA_IO_PENDING)  
  66.         {  
  67.             return false;  
  68.         }  
  69.     }  
  70.     return true;  
  71. }  
  72. extern CClientManager g_clientManager;//全局的管理客户端类。  
  73. void CClient::AsyRecvBodyCompleted()  
  74. {  
  75.     //根据包体的内容,确定是什么类型的请求包。是请求学生姓名还是请求发送试卷。  
  76.     //前2个字节用于确定类型。  
  77.     u_short type;  
  78.     memcpy(&type,m_IoRecv.buffer,2);  
  79.     switch(type)  
  80.     {  
  81.     case LOGIN://登录。  
  82.         {  
  83.             //获取学号。  
  84.             char StuNo[128];  
  85.             strcpy(StuNo,(m_IoRecv.buffer+2));  
  86.               
  87.             CString name=m_pServerView->AuthenticateStuNo(StuNo);  
  88.             if(!name.IsEmpty()&&!m_pServerView->IsAlreadyLogin(name))//验证成功。  
  89.             {  
  90.                     m_StuNo=StuNo;  
  91.                     m_StuName=name;  
  92.                     AsySendName();//在发送姓名异步IO完成收到通知后,再发送试卷。  
  93.                     AsyRecvHeader();  
  94.                   
  95.             }  
  96.             else//验证失败。  
  97.             {  
  98.                 AsySendFailedLoginMsg();  
  99.             }  
  100.         }  
  101.         break;  
  102.     case DOING://答题状态。  
  103.         {  
  104.             m_state=type;  
  105.             AsyRecvHeader();  
  106.         }  
  107.         break;  
  108.     case DONE://交卷状态。  
  109.         {  
  110.   
  111.             m_state=type;  
  112.             //从包体中得到考生答案。  
  113.             m_Result=m_IoRecv.buffer+2;  
  114.             CalculateGrade();  
  115.               
  116.         }  
  117.         break;  
  118.     default:  
  119.         break;  
  120.     }  
  121.     m_pServerView->UpdateClientState(this,type);  
  122. }  
  123. //发送考生姓名。  
  124. bool CClient::AsySendName()  
  125. {  
  126.     ZeroMemory(&m_IoSend,sizeof(IO_OPERATION_DATA));  
  127.       
  128.     WSABUF wsabuf[2];  
  129.     //char name[128];  
  130.     //memset(name,0,128);  
  131. //  WideCharToMultiByte(CP_ACP,WC_COMPOSITECHECK,m_StuName.GetString(),m_StuName.GetLength(),name,128,NULL,NULL);  
  132.     m_IoSend.IOType=IOWriteName;  
  133.     m_IoSend.hdr.Len=m_StuName.GetLength();  
  134.     m_IoSend.hdr.PacketType=STUNAME;  
  135.     //发送包头。  
  136.     wsabuf[0].buf=(char*)&m_IoSend.hdr;  
  137.     wsabuf[0].len=sizeof(HDR);  
  138.     //送包体。  
  139.     wsabuf[1].buf=m_StuName.GetBuffer();  
  140.     wsabuf[1].len=m_StuName.GetLength();  
  141.     DWORD flag=0;  
  142.     int ret=WSASend(m_s,wsabuf,2,NULL,flag,&m_IoSend.overlapped,NULL);  
  143.     if(ret==SOCKET_ERROR)  
  144.     {  
  145.         int err=WSAGetLastError();  
  146.         if(err!=WSA_IO_PENDING)  
  147.         {  
  148.             return false;  
  149.         }  
  150.     }  
  151.     return true;  
  152. }  
  153. //发送登录失败。  
  154. bool CClient::AsySendFailedLoginMsg()  
  155. {  
  156.     ZeroMemory(&m_IoSend,sizeof(IO_OPERATION_DATA));  
  157.     WSABUF wsabuf[2];  
  158.       
  159.     m_IoSend.IOType=IOWriteUnLogin;  
  160.     m_IoSend.hdr.Len=0;  
  161.     m_IoSend.hdr.PacketType=LOGINFAILED;  
  162.     //发送包头。  
  163.     wsabuf[0].buf=(char*)&m_IoSend.hdr;  
  164.     wsabuf[0].len=sizeof(HDR);  
  165.     //发送包体  
  166.     //wsabuf[1].buf=""  
  167.     int ret=WSASend(m_s,wsabuf,1,NULL,0,&m_IoSend.overlapped,NULL);  
  168.     if(ret==SOCKET_ERROR)  
  169.     {  
  170.         int err=WSAGetLastError();  
  171.         if(err!=WSA_IO_PENDING)  
  172.         {  
  173.             return false;  
  174.         }  
  175.     }  
  176.     return true;  
  177. }  
  178.   
  179. void CClient::CalculateGrade()  
  180. {  
  181.     m_grade=0;  
  182.     for(int i=0;i<m_pServerView->m_numOfPaperItem;i++)  
  183.     {  
  184.         if(m_Result.GetAt(i)-'A'+1==m_pServerView->m_pPaperArray[i].PAP_ANSWER)  
  185.         {  
  186.             m_grade++;  
  187.         }  
  188.     }  
  189.   
  190. }  
  191. //发送试卷。  
  192. bool CClient::AsySendPaper()  
  193. {  
  194.     ZeroMemory(&m_IoSend,sizeof(m_IoSend));  
  195.     WSABUF wsabuf[2];  
  196.     //发送包头:  
  197.       
  198.     wsabuf[0].buf=(char*)&m_IoSend.hdr;  
  199.     wsabuf[0].len=sizeof(HDR);  
  200.     //发送包体。  
  201.     wsabuf[1].buf=m_pServerView->m_PaperBuff.GetBuffer();  
  202.     wsabuf[1].len=m_pServerView->m_PaperBuff.GetLength();  
  203.     m_IoSend.IOType=IOWritePaper;  
  204.     m_IoSend.hdr.PacketType=PAPER;  
  205.     m_IoSend.hdr.Len=wsabuf[1].len;  
  206.     int ret=WSASend(m_s,wsabuf,2,NULL,0,&m_IoSend.overlapped,NULL);  
  207.     if(ret==SOCKET_ERROR)  
  208.     {  
  209.         int err=WSAGetLastError();  
  210.         if(err!=WSA_IO_PENDING)  
  211.         {  
  212.             return false;  
  213.         }  
  214.     }  
  215.     return true;  
  216. }  



PaperItem为保存试题的结构体,其定义为:

 

  1. typedef struct _PaperItem  
  2. {  
  3.     LONG PAP_ID;  
  4.     //CString PAP_NO;  
  5.     CString PAP_QUESTION;  
  6.     CString PAP_ANS_A;  
  7.     CString PAP_ANS_B;  
  8.     CString PAP_ANS_C;  
  9.     CString PAP_ANS_D;  
  10.     BYTE    PAP_ANSWER;  
  11. }PaperItem;  


 

CClientManager类:用以管理接受连接的客户端。内部是使用list实现,主要具有添加、删除、删除所有元素等成员函数。list的元素类型为CClient*类型,这样以后就可以对所有连接的客户端进行管理。注意,对list进行操作时,应该使用关键段或其他同步措施进行同步。

声明如下:

 

  1. #pragma once  
  2. #include"Client.h"  
  3. //class CClient;  
  4. #include<list>  
  5. class CClientManager  
  6. {  
  7. public:  
  8.     CClientManager(void);  
  9.     ~CClientManager(void);  
  10. public:  
  11.     std::list<CClient*>m_ClientList;  
  12.     CRITICAL_SECTION m_cs;//用于线程互斥的关键段。防止同时对list进行修改。  
  13. public:  
  14.     bool addClient(CClient*pClient);  
  15.     bool deleteClient(CClient*pClient);  
  16.     bool delteAllClient();  
  17. };  

实现为:

  1. #pragma once  
  2. #include "StdAfx.h"  
  3. #include "ClientManager.h"  
  4.   
  5.   
  6. CClientManager::CClientManager(void)  
  7. {  
  8.     InitializeCriticalSection(&m_cs);  
  9. }  
  10.   
  11.   
  12. CClientManager::~CClientManager(void)  
  13. {  
  14.     for(std::list<CClient*>::iterator iter=m_ClientList.begin();iter!=m_ClientList.end();iter++)  
  15.     {  
  16.         delete (*iter);  
  17.     }  
  18.     m_ClientList.clear();  
  19. }  
  20. bool CClientManager::addClient( CClient*pClient )  
  21. {  
  22.     EnterCriticalSection(&m_cs);  
  23.     m_ClientList.push_back(pClient);  
  24.     LeaveCriticalSection(&m_cs);  
  25.     return true;  
  26. }  
  27.   
  28. bool CClientManager::deleteClient( CClient*pClient )  
  29. {  
  30.     //std::list<<CClient*>::iterator iter=m_ClientList.find(pClient);  
  31.     EnterCriticalSection(&m_cs);  
  32.     delete pClient;  
  33.     m_ClientList.remove(pClient);  
  34.     LeaveCriticalSection(&m_cs);  
  35.     return true;  
  36. }  
  37.   
  38. bool CClientManager::delteAllClient()  
  39. {  
  40.     EnterCriticalSection(&m_cs);  
  41.     for(std::list<CClient*>::iterator iter=m_ClientList.begin();iter!=m_ClientList.end();iter++)  
  42.     {  
  43.         delete (*iter);  
  44.     }  
  45.     m_ClientList.clear();  
  46.     LeaveCriticalSection(&m_cs);  
  47.     return true;  
  48. }  

 

OnTimer响应函数实现周期性的扫描所有已连接客户端,检查心跳包发送时间。

  1. void CIOCPDriverLisenceExamServerView::OnTimer(UINT_PTR nIDEvent)  
  2. {  
  3.     // TODO: 在此添加消息处理程序代码和/或调用默认值  
  4.     EnterCriticalSection(&g_clientManager.m_cs);  
  5.     for(std::list<CClient*>::iterator iter=g_clientManager.m_ClientList.begin();iter!=g_clientManager.m_ClientList.end();)  
  6.     {  
  7.         CClient*pClient=(*iter);  
  8.         if(pClient->m_state==CClient::DOING)  
  9.         {  
  10.             CTime CurTime=CTime::GetCurrentTime();  
  11.             CTimeSpan spanTime(CurTime.GetDay()-pClient->m_time.GetDay(),  
  12.                 CurTime.GetHour()-pClient->m_time.GetHour(),  
  13.                 CurTime.GetMinute()-pClient->m_time.GetMinute(),CurTime.GetSecond()-pClient->m_time.GetSecond());  
  14.             if(spanTime.GetMinutes()>1)  
  15.             {  
  16.                 //设置考生为断线状态。  
  17.                 pClient->m_state=CClient::DISCONN;  
  18.                 //更新显示状态。  
  19.                 UpdateClientState(pClient,CClient::DISCONN);  
  20.                 //删除该客户端。  
  21.                 g_clientManager.deleteClient(pClient);  
  22.                 iter=g_clientManager.m_ClientList.begin();  
  23.             }  
  24.             else  
  25.                iter++;  
  26.         }  
  27.     }  
  28.     LeaveCriticalSection(&g_clientManager.m_cs);  
  29.     CView::OnTimer(nIDEvent);  
  30. }  


 

服务器运行截图:


   

客户端设计:

由于客户端比较简单,因此采用最简单的阻塞模式实现。

CClientSocket类:用以实现与服务器的通信工作。

定义如下:

  1. #pragma once  
  2. #define BUFFER_SIZE 10240  
  3. //服务器向客户端发送的包类型:  
  4. #define STUNAME        109//学生姓名。  
  5. #define PAPER          110//试卷包。  
  6. #define LOGINFAILED    111//登录失败。  
  7. enum state//考生状态。  
  8. {  
  9.     LOGIN,//已登录。  
  10.     DOING,//正在答题。  
  11.     DONE,//已交卷。  
  12.     DISCONN,//因故障断开。  
  13.     UNKNOW//原因不明。  
  14. };  
  15. class CDriverLisenceClientDlg;  
  16. class CClientSocket  
  17. {  
  18. public:  
  19.     CClientSocket(CDriverLisenceClientDlg*pDlg,DWORD IP ,short port);  
  20.     ~CClientSocket(void);  
  21. public:  
  22.     bool ConnectToServer();//连接到服务器。  
  23.     bool LoginServer();//登录。  
  24.     bool RecvName();//接收姓名。  
  25.     bool RecvPaper();//接收试卷。  
  26.     bool SendStart();//通知服务器考试开始。  
  27.     bool SendResult();//发送考试结果。  
  28.     bool SendPulse();//发送心跳包。  
  29. public:  
  30.     SOCKET m_s;//客户端套接字。  
  31.     char *m_pRecvBuffer;//接收缓冲区。  
  32.     char *m_pSendBuffer;//发送缓冲区。  
  33.     DWORD m_ServerIP;//服务器IP。  
  34.     SHORT m_ServerPort;//服务器端口。  
  35.     CDriverLisenceClientDlg*m_pMainDlg;//主窗口指针。  
  36. };  


实现为:

  1. #include "StdAfx.h"  
  2. #include "ClientSocket.h"  
  3.   
  4. #include"DriverLisenceClientDlg.h"  
  5. CClientSocket::CClientSocket(CDriverLisenceClientDlg*pDlg, DWORD IP ,short port )  
  6. {  
  7.     // TODO: 在此添加控件通知处理程序代码  
  8.     m_pMainDlg=pDlg;  
  9.     m_pSendBuffer=new char[BUFFER_SIZE];  
  10.     m_pRecvBuffer=new char[BUFFER_SIZE];  
  11.     m_ServerIP=IP;  
  12.     m_ServerPort=port;  
  13.     WSAData wsadata;  
  14.     WSAStartup(MAKEWORD(2,2),&wsadata);  
  15.     m_s=socket(AF_INET,SOCK_STREAM,0);  
  16.   
  17.     if(m_s==INVALID_SOCKET)  
  18.     {  
  19.         closesocket(m_s);  
  20.         WSACleanup();  
  21.         return ;  
  22.     }  
  23. }  
  24.   
  25.   
  26. CClientSocket::~CClientSocket(void)  
  27. {  
  28.     closesocket(m_s);  
  29. }  
  30. //连接服务器。  
  31. bool CClientSocket::ConnectToServer()  
  32. {  
  33.     SOCKADDR_IN addr;  
  34.     addr.sin_family=AF_INET;  
  35.     addr.sin_addr.S_un.S_addr=inet_addr("192.168.1.100");  
  36.     addr.sin_port=htons(4000);  
  37.     int len=sizeof(addr);  
  38.     int ret=connect(m_s,(SOCKADDR*)&addr,len);  
  39.     if(ret==SOCKET_ERROR)  
  40.     {  
  41.         return false;  
  42.     }  
  43.     const char chOpt=1;  
  44.     ret=setsockopt(m_s,IPPROTO_TCP,TCP_NODELAY,&chOpt,sizeof(char));  
  45.     if(ret==SOCKET_ERROR)  
  46.     {  
  47.         return false;  
  48.     }  
  49.     return true;  
  50. }  
  51. //登录到服务器。  
  52. bool CClientSocket::LoginServer()  
  53. {  
  54.     memset(m_pSendBuffer,0,BUFFER_SIZE);  
  55.     HDR hdr;  
  56.     hdr.PacketType=REQUEST;  
  57.     hdr.Len=m_pMainDlg->m_StuNo.GetLength()+2;  
  58.     USHORT State=LOGIN;  
  59.     int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。  
  60.     if(ret==SOCKET_ERROR)  
  61.     {  
  62.         return false;  
  63.     }  
  64.     strncpy(m_pSendBuffer,(char*)&State,sizeof(USHORT));  
  65.     strncpy(m_pSendBuffer+2,m_pMainDlg->m_StuNo.GetBuffer(),m_pMainDlg->m_StuNo.GetLength());//  
  66.     ret=send(m_s,m_pSendBuffer,hdr.Len,0);//发送登陆信息包体。  
  67.     if(ret==SOCKET_ERROR)  
  68.     {  
  69.         return false;  
  70.     }  
  71.     return true;  
  72. }  
  73. //接收姓名。  
  74. bool CClientSocket::RecvName()  
  75. {  
  76.     HDR hdr;  
  77.     int ret=recv(m_s,(char*)&hdr,sizeof(HDR),0);//接收包头。  
  78.     if(ret==SOCKET_ERROR)  
  79.     {  
  80.         return false;  
  81.     }  
  82.     memset(m_pRecvBuffer,0,BUFFER_SIZE);  
  83.     if(hdr.PacketType==STUNAME)  
  84.     {  
  85.         ret=recv(m_s,m_pRecvBuffer,hdr.Len,0);//接收姓名。  
  86.         if(ret==SOCKET_ERROR)  
  87.         {  
  88.             return false;  
  89.         }  
  90.         m_pMainDlg->m_StuName=m_pRecvBuffer;  
  91.   
  92.     }  
  93.     else if(hdr.PacketType==LOGINFAILED)  
  94.     {  
  95.         MessageBox(NULL,"考生号输入有误或该账号已经登陆!","登录失败",MB_OK);  
  96.         return false;  
  97.     }  
  98.   
  99.     return true;  
  100. }  
  101. //接收试卷。  
  102. bool CClientSocket::RecvPaper()  
  103. {  
  104.     HDR hdr;  
  105.     int ret=recv(m_s,(char*)&hdr,sizeof(HDR),0);  
  106.     if(ret==SOCKET_ERROR)  
  107.     {  
  108.         return false;  
  109.     }  
  110.     if(hdr.PacketType==PAPER)  
  111.     {  
  112.         memset(m_pRecvBuffer,0,BUFFER_SIZE);  
  113.         ret=recv(m_s,m_pRecvBuffer,hdr.Len,0);  
  114.         if(ret==SOCKET_ERROR)  
  115.         {  
  116.             return false;  
  117.         }  
  118.         m_pMainDlg->m_PaperBuffer=m_pRecvBuffer;  
  119.     }  
  120.     return true;  
  121.       
  122. }  
  123. //发送开始答题消息。  
  124. bool CClientSocket::SendStart()  
  125. {  
  126.     HDR hdr;  
  127.     hdr.PacketType=REQUEST;  
  128.     hdr.Len=sizeof(USHORT);  
  129.       
  130.     int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。  
  131.     if(ret==SOCKET_ERROR)  
  132.     {  
  133.         return false;  
  134.     }  
  135.     memset(m_pSendBuffer,0,BUFFER_SIZE);  
  136.     USHORT State=DOING;  
  137.     strncpy(m_pSendBuffer,(char*)&State,sizeof(USHORT));//构造两字节的状态。为准备答题。  
  138.        
  139.     ret=send(m_s,m_pSendBuffer,sizeof(USHORT),0);//DOING状态。  
  140.     if(ret==SOCKET_ERROR)  
  141.     {  
  142.         return false;  
  143.     }  
  144.     m_pMainDlg->SetTimer(1,10,NULL);  
  145.     return true;  
  146. }  
  147. //发送答题结果。  
  148. bool CClientSocket::SendResult()  
  149. {  
  150.     HDR hdr;  
  151.     hdr.PacketType=REQUEST;  
  152.     hdr.Len=sizeof(USHORT)+m_pMainDlg->m_result.GetLength();  
  153.   
  154.     int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。  
  155.     if(ret==SOCKET_ERROR)  
  156.     {  
  157.         return false;  
  158.     }  
  159.     memset(m_pSendBuffer,0,BUFFER_SIZE);  
  160.     USHORT State=DONE;  
  161.     //strncpy(m_pSendBuffer,(char*)&State,sizeof(USHORT));//交卷状态。  
  162. //  strncpy(m_pSendBuffer+sizeof(USHORT),m_pMainDlg->m_result.GetBuffer(),m_pMainDlg->m_result.GetLength());  
  163.     ret=send(m_s,(char*)&State,sizeof(USHORT),0);  
  164.     ret=send(m_s,m_pMainDlg->m_result.GetBuffer(),m_pMainDlg->m_result.GetLength(),0);//状态后为答题结果。。  
  165.     if(ret==SOCKET_ERROR)  
  166.     {  
  167.         return false;  
  168.     }  
  169.     return true;  
  170. }  
  171. //发送心跳包。  
  172. bool CClientSocket::SendPulse()  
  173. {  
  174.     HDR hdr;  
  175.     hdr.PacketType=PULSE;  
  176.     hdr.Len=0;  
  177.   
  178.     int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。  
  179.     if(ret==SOCKET_ERROR)  
  180.     {  
  181.         return false;  
  182.     }  
  183.     return true;  
  184. }  


      DlgView类执行初始化答题窗口、生成试卷、发送心跳包

   声明为:

 

  1. public:  
  2.     afx_msg void OnBnClickedBtnOk();//登录消息响应函数。  
  3.     bool ParsePaperBuffer();//扫描试卷缓冲区,取出试题。  
  4.     bool StartTheExam();//开始考试。  
  5.     static DWORD WINAPI QuesationDlgThread(PVOID pparam);  
  6. public:  
  7.   
  8.     short m_port;//服务器端口。  
  9.     DWORD m_ServerIP;//服务器IP。  
  10.     CString m_StuNo;//考生号。  
  11.     CString m_StuName;//从服务器接收的考生姓名。  
  12.     std::list<CPaperItem*>m_PaperItemList;//试题链表。每个节点存储一道试题。  
  13.     CTimeSpan *m_pTimeRemained;//剩余时间。考试时间为60min.  
  14.     CString m_PaperBuffer;//从服务器接收的试卷缓冲区。  
  15.     CString m_result;//存储答题结果。  
  16.     CClientSocket *m_pClientSocket;//CCClientSocket指针。  
  17.     HANDLE m_hEvent;//事件对象,用于在上一题下一题时触发。  
  18.     CQuestionDlg*m_pQuestionDlg;//答题窗口。  
  19.     std::list<CPaperItem*>::iterator m_IterCurQuestion;  
  20.     bool m_IsThreadExit;//决定试题循环线程是否退出。  
  21.     afx_msg void OnTimer(UINT_PTR nIDEvent);//考试时间计时。  
  22. };  

类实现为:

//登录按钮消息响应函数。

  1. void CDriverLisenceClientDlg::OnBnClickedBtnOk()  
  2. {  
  3.     UpdateData();  
  4.     m_pClientSocket=new CClientSocket(this,m_ServerIP,m_port);  
  5.     bool ret=m_pClientSocket->ConnectToServer();  
  6.     //ASSERT(ret);  
  7.     if(!ret)  
  8.     {  
  9.         MessageBox("服务器未开启,登录失败!","服务器未开启");  
  10.         return ;  
  11.     }  
  12.     ret=m_pClientSocket->LoginServer();  
  13.     //ASSERT(ret);  
  14.     if(!ret)  
  15.     {  
  16.         //MessageBox("账号有误或重复登录,登录失败!","登录失败");  
  17.         return ;  
  18.     }  
  19.     ret=m_pClientSocket->RecvName();  
  20.     //ASSERT(ret);  
  21.     if(!ret)  
  22.     {  
  23.         MessageBox("数据接收出现错误,请检查网络连接!!","数据接收出现错误");  
  24.         return ;  
  25.     }  
  26.     ret=m_pClientSocket->SendStart();  
  27.     if(!ret)  
  28.     {  
  29.         MessageBox("数据发送出现错误,请检查网络连接!!","数据接收出现错误");  
  30.         return ;  
  31.     }  
  32.     ret=m_pClientSocket->RecvPaper();  
  33.     //ASSERT(ret);  
  34.     if(!ret)  
  35.     {  
  36.         MessageBox("数据接收出现错误,请检查网络连接!!","数据接收出现错误");  
  37.         return ;  
  38.     }  
  39.     ParsePaperBuffer();  
  40.     StartTheExam();  
  41.     SetTimer(2,1000,NULL);  
  42. }  
  43. //扫描试题缓冲区,将试题存入试题链表内。  
  44. bool CDriverLisenceClientDlg::ParsePaperBuffer()  
  45. {  
  46.     CPaperItem *pItem;  
  47.     char c;  
  48.     int i=0;  
  49.     int count=0;  
  50.     while(i!=m_PaperBuffer.GetLength())  
  51.     {  
  52.           
  53.         c=m_PaperBuffer[i];  
  54.         if(c=='<')  
  55.         {  
  56.             count=0;  
  57.             pItem=new CPaperItem;  
  58.         }  
  59.         else if(c=='>')  
  60.         {  
  61.             m_PaperItemList.push_back(pItem);  
  62.         }  
  63.         else if(c=='|')  
  64.         {  
  65.               
  66.             count++;  
  67.         }  
  68.         else  
  69.         {  
  70.             if(count==0)  
  71.             {  
  72.                 pItem->m_Question+=c;  
  73.             }  
  74.             else if(count==1)  
  75.             {  
  76.                 pItem->m_A+=c;  
  77.             }  
  78.             else if(count==2)  
  79.             {  
  80.                 pItem->m_B+=c;  
  81.             }  
  82.             else if(count==3)  
  83.             {  
  84.                 pItem->m_C+=c;  
  85.             }  
  86.             else  
  87.             {  
  88.                 pItem->m_D+=c;  
  89.             }  
  90.         }  
  91.         i++;  
  92.     }  
  93.     return true;  
  94. }  
  95. //开始考试。  
  96. bool CDriverLisenceClientDlg::StartTheExam()  
  97. {  
  98.     ShowWindow(SW_HIDE);  
  99.     DWORD num=m_PaperItemList.size();  
  100.   
  101.     m_pQuestionDlg=new CQuestionDlg;  
  102.     bool ret=m_pQuestionDlg->Create(IDD_DLG_PAPER,this);  
  103.     ret=m_pQuestionDlg->ShowWindow(SW_SHOW);  
  104.     HANDLE hThread=CreateThread(NULL,0,QuesationDlgThread,this,0,NULL);  
  105.     CloseHandle(hThread);  
  106.     CString s;  
  107.     s.Format("              驾驶员理论考试!      试题数:%d",num);  
  108.     //CString s;  
  109.     m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_NOTIFICATION,s);  
  110.     s.Format("姓名:%s",m_StuName);  
  111.     m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_STUNAME,s);  
  112.     s.Format("考号:%s",m_StuNo);  
  113.     m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_STUNO,s);  
  114.     //dlg.DoModal();  
  115.       
  116.     return true;  
  117. }  
  118. //在此线程循环内不断遍历试题,显示到对话框。  
  119. DWORD WINAPI CDriverLisenceClientDlg::QuesationDlgThread( PVOID pparam )  
  120. {  
  121.     CDriverLisenceClientDlg*pDlg=(CDriverLisenceClientDlg*)pparam;  
  122.     ((CButton*)(pDlg->m_pQuestionDlg->GetDlgItem(IDC_BTN_HANDIN)))->EnableWindow(false);  
  123.     pDlg->m_IsThreadExit=false;  
  124.     //for(std::list<CPaperItem*>::iterator iter=pDlg->m_PaperItemList.begin();iter!=pDlg->m_PaperItemList.end();iter=pDlg->m_IterCurQuestion)//iter++)  
  125.     for(std::list<CPaperItem*>::iterator iter=pDlg->m_PaperItemList.begin();!pDlg->m_IsThreadExit;iter=pDlg->m_IterCurQuestion)//iter++)  
  126.     {  
  127.         if(pDlg->m_PaperItemList.end()!=iter)  
  128.         {  
  129.             CPaperItem *pPaperItem=(*iter);  
  130.             pDlg->m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_QUESTION,pPaperItem->m_Question);  
  131.             pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_A,pPaperItem->m_A);  
  132.             pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_B,pPaperItem->m_B);  
  133.             pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_C,pPaperItem->m_C);  
  134.             pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_D,pPaperItem->m_D);  
  135.             pDlg->m_IterCurQuestion=iter;  
  136.         }  
  137.         WaitForSingleObject(pDlg->m_hEvent,INFINITE);//等待上一题或下一题触发事件消息。  
  138.           
  139.     }  
  140.       
  141.       
  142.     return 0;  
  143. }  
  144.   
  145. //发送心跳包。  
  146. void CDriverLisenceClientDlg::OnTimer(UINT_PTR nIDEvent)  
  147. {  
  148.     // TODO: 在此添加消息处理程序代码和/或调用默认值  
  149.     if(nIDEvent==1)  
  150.       m_pClientSocket->SendPulse();  
  151.     else if(nIDEvent==2)  
  152.     {  
  153.         CTimeSpan temp(0,0,0,1);  
  154.         (*m_pTimeRemained)-=temp;  
  155.         CString t;  
  156.         t.Format("剩余时间:%d分%d秒",m_pTimeRemained->GetMinutes(),m_pTimeRemained->GetSeconds());  
  157.         m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_TIMEREMAINED,t);  
  158.     }  
  159.     CDialogEx::OnTimer(nIDEvent);  
  160. }  

客户端登录界面:



答题窗口:

 


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约