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

转载自: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 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;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.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_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();
            }
            else
               iter++;
        }
    }
    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;
    else
        return 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;//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;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();
            }
            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::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于山西大同

结构层次及相互联系 (1)、工作线程:响应连接的IO投递返回并负责投递读请求,并将IO返回结果投递给处理线程,可设定参数决定工作线程数量; (2)、处理线程:处理线程调用回调函数将信息传递给应用层或协议栈,可设定参数决定工作处理数量; (3)、看守线程:响应Accept事件调用AcceptEx,检测连接和心跳超时 ,将信息投递给工作线程,模块仅有一个看守线程。 1. 技术要求 (1)、线程同步:Lock指令、临界段; (2)、主要Socket API:WSASend、WSARecv、AcceptEx、DisconnectEx; (3)、内存管理:连接池(句柄重用)、内存池; (4)、数据0拷贝:通过内置处理线程,上层应用可以避免自建线程池及复制数据的过程。同时提供GBuf内存分配功能,应用层获得分配地址及填充数据之后亦可直接投递给内核/驱动层; (5)、数据顺序同步:同一个连接同时只有一个处理线程响应其IO事件; (6)、IO请求投递:单投递读、多投递写; (7)、0缓冲读投递:可条件编译实现,以适用大规模连接要求。 (8)、超时机制:可设置空连接(连接不发送数据)超时时间以防止DOS攻击,也可设置心跳超时时间防止网络故障导致的现有连接成为虚连接避免耗尽系统资源。 (9)、接口技术:API、回调函数、客户句柄(客户连接句柄)。 (10)、主、被动发送:不使用HASH、MAP及LIST技术,即可提供安全可靠高效的客户连接句柄,以实现服务器端主被动发送数据功能; (11)、PerHandleData的回收不以IO投递的计数器或链表来做依据但仍能安全回收,同时尽量避免在高频的读写操作时做其他无关的操作以提高读写效率。 (12)、处理线程和工作线程有着良好分工界限,繁重的工作交给处理线程完成,工作线程工作量最大限度的减少,仅响应投递返回及读投递的操作; (13)、支持AWE,模块自动识别AWE是否开启(需手动开启),“否”则使用虚拟内存机制。 2. 功能要求 (1)、多IP多端口监听,每个监听可设置不同的回调函数,以高效的区别处理数据 (2)、可设置每秒最大的连接并发量和空连接(连接不发数据)超时时间以防止DOS攻击造成的服务瘫痪、具有心跳处理(防网络异常造成的虚连接)功能 (3)、不加协议的透明传输,可适用广泛的网络通讯环境 (4)、可现实主、被动发送数据,但不会因兼顾主动发送而额外增加降低效率的工作 (5)、内置处理线程,上层应用可不必自建线程池处理数据,所有IO事件按顺序调用回调函数并可以在回调函数内直接处理数据,不必担心多线程造成的接收数据乱序的问题。 (6)、高效率的数据对应关联机制,在初次连接并根据登录数据设置每个连接对应的宿主(Owner)之后,再接收的数据即可立即获得该连接对应的宿主,而不必再做额外的查询工作,并且模块内部采用的是指针关联方式,对于长连接、主动发送的服务器系统而言是高效率的。 (7)、可兼容IPv6 3. 注意事项 因硬件环境和应用环境不同,不合理的配置会出现效率及性能上的问题,因此以下情况出现时,请务必与作者联系以确保获得更好的参数配置: (1)、连接量超过1000个的。超过的应结合具体硬件配置和网络带宽等因素综合设定运行参数。 (2)、带宽使用率超过20%的。工作线程和处理线程数量的设置也是综合考虑数据吞吐量和数据处理负载的因素来设置的,过多的线程会在调度上浪费时间,同时也应该综合考虑线程优先级别来设置工作线程和处理线程数量,两者的设置也不一定能相等。 (3)、服务器端有主动发送需求的、短连接(含网络故障造成的连接断开)出现频率高的。 压力测试工具介绍: 一、 使用G-TcpClient模块 二、 可以设定间隔时间发起大规模长、短连接 三、 可以发起密集数据包,包括即时和定时发送,1M的光纤带宽最大可以达到100K/S(单向)以上,100M本地网最大可以达到10M/S(单向)以上 四、 数据发送仅由一个独立线程但当,每点击一次Connect就创建一个线程根据当前参数发起连接。 五、 测试前提:服务器接收客户端数据后立即原样返回给客户端
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值