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

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

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

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

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

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

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

3:监视考生考试状态。

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

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

1:登录服务器。

2:生成试卷。

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

主要步骤如下:

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

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

数据包设计:

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

typedef struct _header

{

   u_short type;//包类型。

   u_short len;//包体长度。

}HEADER;


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

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

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

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

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

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

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

考生姓名和试卷。

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

工作流程:

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

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

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

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

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

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

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

//客户端向服务器发送的包类型。
#define PULSE          107//心跳包。
#define REQUEST          108//请求包。

//服务器向客户端发送的包类型:
#define STUNAME        109//学生姓名。
#define PAPER          110//试卷包。
#define LOGINFAILED    111//登录失败。


考试系统的试题类型为选择题。它包括试题和答案两部分。题号与问题之间使用:分割。每道题有四个答案,每个答案之间用|分割。试题之间用<>分割。如

<1:问题|A:答案|B:答案|C:答案|D:答案><:问题2|A:答案|B:答案|C:答案|D:答案><:问题3|A:答案|B:答案|C:答案|D:答案><:问题4|A:答案|B:答案|C:答案|D:答案>

数据库设计

Access数据中存储了考生信息和试卷信息。此处定义了两张表,分别为StuInfo表和Paper表。。使用ODBC与数据库连接。由于本文更偏重与介绍IOCP的机制,因此对于数据库如何使用此处不再介绍。

StuInfo表包括Stu_ID,Stu_NO,Stu_Name,Stu_State,Stu_Grade字段。Stu_ID为主键。

Paper表包括PAP_IDPAP_QUESTIONPAP_ANSWERAPAP_ANSWERBPAP_ANSWERCPAP_ANSWERDPAP_ANSWER。

服务器设计:

服务器负责发送试题和对考生考试信息进行管理。它包括多线程设计和界面设计。

多线程设计:

主线程启动后,创建一个接受客户端连接请求的线程和多个服务线程。服务线程使用套接字的IO完成端口模型对服务器的IO操作进行管理。监听线程使用套接字的WSAEventSelect模型实现对接受客户端请求进行管理。

当服务器退出时,主线程通知接受客户端线程和服务线程退出,然后主线程退出。

在接受客户端连接请求线程中,接受客户端的连接请求后,立即发出一个接受客户端数据包头的异步请求。在服务线程取出IO操作结果后,再发起另一个异步IO操作。

一:主线程:负责初始化界面、读取数据库、更新界面、更新数据库、创建完成端口和通知接受请求线程和服务线程退出。

二:接受客户端请求线程,利用WSAEventSelect模型实现对客户端请求的管理。主要任务为:

1:接受客户端连接请求。

2:将完成端口与套接字关联起来。

3:发起异步接收客户端数据操作。

三:服务线程,利用IO完成端口实现对IO操作管理。主要任务为:

1:管理客户端。

2:发送考生姓名和试卷。

3:接收客户端数据。

4:考试结束后,对考生答卷进行评分。

服务器程序是基于单文档的MFC应用程序。主要包括一下几个类:

1CCServerView类:视图类,实现服务器的主要功能。

2CClient类:实现与客户端通信功能。

3CClientManager类,实现对连接的客户端进行管理功能。

4CServerAddrDlg类:实现IP地址输入窗口。

CCServerView类:

OnInitialUpdate函数实现服务器初始化功能:初始化列表试图控件、更新列表试图控件和读取试卷。

OnStartService实现启动服务功能。它创建监听套接字、创建完成端口、创建监听事件对象和接受客户端请求线程和服务线程。

在服务器保存一定数量的习题。在OnInitialUpdate函数中会将这些试题读入到m_cPaper数组中。

CCServerView类的用户自定义部分为:

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

实现为:

// CIOCPDriverLisenceExamServerView 消息处理程序


void CIOCPDriverLisenceExamServerView::OnInitialUpdate()
{
	CView::OnInitialUpdate();

	// TODO: 在此添加专用代码和/或调用基类
	m_hIOCP=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);
	
	CString sDriver = TEXT("MICROSOFT ACCESS DRIVER (*.mdb)");
	CString sDsnStuInfo;
	CString sFileStuInfo = TEXT("E://DriverLiscenceExam.mdb");//Change path here
 
	sDsnStuInfo.Format(TEXT("ODBC;DRIVER={%s};DSN='';DBQ=%s"),sDriver,sFileStuInfo);

	bool ret=m_StuInfoDB.Open(NULL,false,false,sDsnStuInfo);
	if(!ret)
	{
		MessageBox(TEXT("连接数据库失败!"));
	}
	InitSocket();
	
	ReadPaper();
	ReadStuInfo();
	ContructPaperBuffer();
}

bool CIOCPDriverLisenceExamServerView::InitSocket()
{
	WSAData wsadata;
	WSAStartup(MAKEWORD(2,2),&wsadata);
	m_sListenSocket=socket(AF_INET,SOCK_STREAM,0);
	if(m_sListenSocket==INVALID_SOCKET)
	{
		closesocket(m_sListenSocket);
		WSACleanup();
		return false;
	}
	m_hEvent=WSACreateEvent();
	WSAEventSelect(m_sListenSocket,m_hEvent,FD_ACCEPT);//为监听套接字设置FD_ACCEPT事件。
	SOCKADDR_IN addr;
	addr.sin_family=AF_INET;
	addr.sin_addr.S_un.S_addr=inet_addr("192.168.1.100");
	addr.sin_port=htons(4000);
	int ret=bind(m_sListenSocket,(SOCKADDR*)&addr,sizeof(addr));
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	ret=listen(m_sListenSocket,1000);
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
}

DWORD WINAPI CIOCPDriverLisenceExamServerView::AcceptClientThread( PVOID ppram )
{
	CIOCPDriverLisenceExamServerView*pServer=(CIOCPDriverLisenceExamServerView*)ppram;
	SOCKADDR_IN addr;
	int len=sizeof(addr);
	while(pServer->m_IsRunning)
	{
		int ret=WSAWaitForMultipleEvents(1,&pServer->m_hEvent,false,WSA_INFINITE,false);
		if(ret==WSA_WAIT_TIMEOUT)
		{
			continue;
		}
		else
		{
			WSANETWORKEVENTS events;
			int r=WSAEnumNetworkEvents(pServer->m_sListenSocket,pServer->m_hEvent,&events);//重置事件对象。
			if(r==SOCKET_ERROR)
			{
				break;
			}
			if(events.lNetworkEvents&FD_ACCEPT) 
			{
				if(events.iErrorCode[FD_ACCEPT_BIT]==0)//发生FD_ACCEPT网络事件。接受客户端请求。
				{
					SOCKET sAccept=WSAAccept(pServer->m_sListenSocket,(SOCKADDR*)&addr,&len,0,NULL);

					CClient*pClient=new CClient(sAccept,pServer);
					if(CreateIoCompletionPort((HANDLE)sAccept,pServer->m_hIOCP,(ULONG_PTR)pClient,0)==NULL)
						return -1;
					g_clientManager.addClient(pClient);
					//调用接收数据异步IO。
					if(!pClient->AsyRecvHeader())
						g_clientManager.deleteClient(pClient);//接收数据失败后,将此客户端从链表删除。

				}
			}
			
		}
		
	}
	return 0;
}

bool CIOCPDriverLisenceExamServerView::StartService()
{
	m_IsRunning=true;
	m_h[0]=CreateThread(NULL,0,AcceptClientThread,this,0,NULL);
	for(int i=1;im_IsRunning)
	{
		bool ret=GetQueuedCompletionStatus(pServer->m_hIOCP,&transferred,(LPDWORD)&pClient,&lpoverlapped,WSA_INFINITE);
		if(ret&&lpoverlapped&&pClient)//成功的异步IO完成。根据从lpoverlapped中得到的类型,进行操作。
		{
			IO_OPERATION_DATA*pIO=(IO_OPERATION_DATA*)lpoverlapped;
			switch(pIO->IOType)
			{
			case IOReadHead:
				{
					pClient->AsyRecvHeaderCompleted();
				}
				break;
			case IOReadBody:
				{
					pClient->AsyRecvBodyCompleted();
				}
				break;
			case IOWritePaper:
				{
					//试卷发送完毕。不执行动作。
					pServer->UpdateClientState(pClient,CClient::LOGIN);

				}
				break;
			case IOWriteName:
				{
					pClient->AsySendPaper();
				}
				break;
			case IOWriteUnLogin:
				{
					g_clientManager.deleteClient(pClient);
				}
				break;
			default:
				break;
			}
			
		}
	}
	return 0;
}

CString CIOCPDriverLisenceExamServerView::AuthenticateStuNo( char*pStuNo)
{
	//TCHAR wStuNo[128];
	//ZeroMemory(wStuNo,128);
	//MultiByteToWideChar(CP_ACP,MB_COMPOSITE,pStuNo,strlen(pStuNo),wStuNo,128);//TCP是基于字节流的,而此程序是使用Unicode编码。因此需要转换。
	CStuInfo StuInfo(&m_StuInfoDB);
	if(StuInfo.IsOpen())
	{
		StuInfo.Close();
	}
	StuInfo.m_strFilter.Format(TEXT("StuNo='%s'"),pStuNo);
	StuInfo.Open();
	if(StuInfo.IsEOF())//数据库中未搜索到该用户。该账号不存在。
	{
		//MessageBox(TEXT("未找到该用户。"));
		return CString();
	}
	else
	{
		//MessageBox(TEXT("找到该用户。"));

		return CString(StuInfo.m_StuName);
	}

	
}

bool CIOCPDriverLisenceExamServerView::ReadPaper()
{
	CPaper paperRecordSet(&m_PaperDB);
	if(paperRecordSet.IsOpen())
	{
		paperRecordSet.Close();
	}
	paperRecordSet.Open();
	//long numInDB=paperRecordSet.GetRecordCount();
	//paperRecordSet.GetRe
	
	m_numOfPaperItem=0;
	while(!paperRecordSet.IsEOF())
	{
		m_numOfPaperItem++;
		paperRecordSet.MoveNext();
	}
	paperRecordSet.MoveFirst();
	m_pPaperArray=new PaperItem[m_numOfPaperItem];
	if(!m_pPaperArray)
		return false;
	for(int j=0;j",m_pPaperArray[i].PAP_ID,m_pPaperArray[i].PAP_QUESTION
			,m_pPaperArray[i].PAP_ANS_A,m_pPaperArray[i].PAP_ANS_B,m_pPaperArray[i].PAP_ANS_C,m_pPaperArray[i].PAP_ANS_D);
		m_PaperBuff+=temp;
	}
}


void CIOCPDriverLisenceExamServerView::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	EnterCriticalSection(&g_clientManager.m_cs);
	for(std::list::iterator iter=g_clientManager.m_ClientList.begin();iter!=g_clientManager.m_ClientList.end();)
	{
		CClient*pClient=(*iter);
		if(pClient->m_state==CClient::DOING)
		{
			CTime CurTime=CTime::GetCurrentTime();
			CTimeSpan spanTime(CurTime.GetDay()-pClient->m_time.GetDay(),
				CurTime.GetHour()-pClient->m_time.GetHour(),
				CurTime.GetMinute()-pClient->m_time.GetMinute(),CurTime.GetSecond()-pClient->m_time.GetSecond());
			if(spanTime.GetMinutes()>1)
			{
				//设置考生为断线状态。
				pClient->m_state=CClient::DISCONN;
				//更新显示状态。
				UpdateClientState(pClient,CClient::DISCONN);
				//删除该客户端。
				g_clientManager.deleteClient(pClient);
				iter=g_clientManager.m_ClientList.begin();
			}
			else
			   iter++;
		}
	}
	LeaveCriticalSection(&g_clientManager.m_cs);
	CView::OnTimer(nIDEvent);
}

void CIOCPDriverLisenceExamServerView::UpdateClientState( CClient *pClient,USHORT state )
{
	int i=0;
	for(i=0;im_StuName==m_listCrtl.GetItemText(i,2))
		{
			break;
		}
	}
	CString s;
	switch(state)
	{
	case CClient::LOGIN:
		s="已登录";
		break;
	case CClient::DOING:
		s="正在答题";
		break;
	case CClient::DONE:
		{
			s="已交卷";
			SaveGradeIntoDB(pClient);
		}
		break;
	case CClient::DISCONN:
		s="掉线";
		break;
	default:
		s="未知状态";
		break;
	}
	m_listCrtl.SetItemText(i,3,s);
}

void CIOCPDriverLisenceExamServerView::Destroy()
{
	KillTimer(1);
}


void CIOCPDriverLisenceExamServerView::OnStartService()
{
	// TODO: 在此添加命令处理程序代码
	StartService();
}

bool CIOCPDriverLisenceExamServerView::SaveGradeIntoDB(CClient*pClient)
{
	//在列表空间更新分数。
	int i=0;
	for(i=0;im_StuName==m_listCrtl.GetItemText(i,2))
		{
			//m_listCrtl.SetItemText(i,4);
			break;
		}
	}
	CString s;
	s.Format("%d",pClient->m_grade);
	m_listCrtl.SetItemText(i,4,s);
	//在数据库更新分数。
	CStuInfo StuInfo(&m_StuInfoDB);
	if(StuInfo.IsOpen())
	{
		StuInfo.Close();
	}
	StuInfo.m_strFilter.Format(TEXT("StuNo='%s'"),pClient->m_StuNo);
	StuInfo.Open();
	if(!StuInfo.IsEOF())//数据库中未搜索到该用户。该账号不存在。
	{
		StuInfo.Edit();
		StuInfo.m_StuGrade=pClient->m_grade;
		StuInfo.m_StuState="已考试";
		StuInfo.Update();
		StuInfo.Close();
		g_clientManager.deleteClient(pClient);
		return true;
	}
	
	return false;
}

bool CIOCPDriverLisenceExamServerView::IsAlreadyLogin( CString StuName )
{
	std::list::iterator iter;
	for(iter=g_clientManager.m_ClientList.begin();iter!=g_clientManager.m_ClientList.end();iter++)
	{
		if((*iter)->m_StuName==StuName)
			break;
	}
	if(iter==g_clientManager.m_ClientList.end())
		return false;
	else
		return true;
}



CClient类。

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

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

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

enum state

{

  LOGIN,//已登录状态。

  DOING,//正在答题状态。

  DONE,//已交卷状态。

  DISCONN//故障掉线。

};


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

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

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

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

typedef struct _IO_OPERATION_DATA

{

   OVERLAPPED overlapped;

   char buffer[BUFFER_SiZE];

   byte IOType;//IO类型。用以得知哪种异步IO完成。收or发。

}IO_OPERATION_DATA;


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

IO类型有以下几种:

    #define IOReadHead     100//接收包头完成,异步IO完成。

     #define IOReadBody     101//接收包体完成,异步IO完成。

     #define IOWriteUnLogin 103//登录失败。

     #define IOWritePaper   104//发送试卷完成,异步IO完成。

     #define IOExit         105//退出。

     #define IOWriteName    106//发送姓名异步IO完成。


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

定义如下:

typedef struct HEADER

{

   short PacketType;//包类型。

   short Len;//包体长度。

}HDR,*PHDR;


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

声明如下:

#pragma once
#include"WinSock2.h"
#include"ctime"

class CIOCPDriverLisenceExamServerView;

#define  BUFFER_SIZE 10240
#define IOReadHead     100
#define IOReadBody     101
#define IOWriteUnLogin 103
#define IOWritePaper   104
#define IOExit         105
#define IOWriteName    106
//客户端向服务器发送的包类型。
#define PULSE          107//心跳包。
#define REQUEST          108//请求包。

//服务器向客户端发送的包类型:
#define STUNAME        109//学生姓名。
#define PAPER          110//试卷包。
#define LOGINFAILED    111//登录失败。


typedef struct HEADER
{
	short PacketType;
	short Len;
}HDR,*PHDR;
typedef struct _IO_OPERATION_DATA
{
	WSAOVERLAPPED overlapped;
	char buffer[BUFFER_SIZE];
	HDR hdr;
	byte IOType;

}IO_OPERATION_DATA;
class CClient
{
public:
	enum state//考生状态。
	{
		LOGIN,//已登录。
		DOING,//正在答题。
		DONE,//已交卷。
		DISCONN,//因故障断开。
		UNKNOW//原因不明。
	};
	
	CClient(SOCKET s,CIOCPDriverLisenceExamServerView*pDlg);
	~CClient(void);
public:
	bool AsyRecvHeader();//接收包头。
	bool AsyRecvBody(int len);
	void AsyRecvBodyCompleted();

	bool AsySendName();
	bool AsySendPaper();
	bool AsySendFailedLoginMsg();
	
	void AsyRecvHeaderCompleted();
	void CalculateGrade();
public:
	
public:
	SOCKET m_s;
	IO_OPERATION_DATA m_IoRecv;//recv
	IO_OPERATION_DATA m_IoSend;
	USHORT m_state;//考生状态。
	CTime m_time;//最近一次心跳包时间。
	CIOCPDriverLisenceExamServerView*m_pServerView;//主窗口指针。
public:
	CString m_StuNo;
	//CString m_StuID;
	CString m_StuName;
	long m_grade;
	CString m_Result;//答题结果。
};

CClient类实现为:

#include "StdAfx.h"
#include "Client.h"
#include"IOCPDriverLisenceExamServerView.h"
CClient::CClient( SOCKET s,CIOCPDriverLisenceExamServerView*pDlg )
{
	m_s=s;
	m_pServerView=pDlg;
	m_time=CTime::GetCurrentTime();
}


CClient::~CClient(void)
{
	closesocket(m_s);
}
//读取包头异步函数。
bool CClient::AsyRecvHeader()
{

	WSABUF wsabuf;
	ZeroMemory(&m_IoRecv,sizeof(IO_OPERATION_DATA));
	m_IoRecv.IOType=IOReadHead;
	wsabuf.buf=(char*)&m_IoRecv.hdr;
	wsabuf.len=sizeof(HDR);
	DWORD flag=0;
	int ret=WSARecv(m_s,&wsabuf,1,NULL,&flag,&m_IoRecv.overlapped,NULL);
	if(ret==SOCKET_ERROR)
	{
		int err=WSAGetLastError();
		if(err!=WSA_IO_PENDING)
		{
			return false;
		}
	}
	return true;
}
//包头接收完毕。
void CClient::AsyRecvHeaderCompleted()
{
	if(m_IoRecv.hdr.PacketType==PULSE)//如果为心跳包。//客户端发送给服务器的包头的PacketType有两种。一种是心跳包,另外是REQUEST包。
		//在数据的前两个字节会指定当前请求,有LOGIN,DOING ,DONE等。
	{
		m_time=CTime::GetCurrentTime();
		AsyRecvHeader();
	}
	else //if(state==m_IoRecv.hdr.PacketType)//状态包。接着接收包体。
	{
		AsyRecvBody(m_IoRecv.hdr.Len);
	}
}
//接收包体。
bool CClient::AsyRecvBody(int len)
{
	WSABUF wsabuf;
	//ZeroMemory(&m_IoRecv.buffer,BUFFER_SIZE);
	ZeroMemory(&m_IoRecv,sizeof(IO_OPERATION_DATA));
	m_IoRecv.IOType=IOReadBody;
	wsabuf.buf=m_IoRecv.buffer;
	wsabuf.len=len;//接收包体。
	DWORD flag=0;
	int ret=WSARecv(m_s,&wsabuf,1,NULL,&flag,&m_IoRecv.overlapped,NULL);
	if(ret==SOCKET_ERROR)
	{
		int err=WSAGetLastError();
		if(err!=WSA_IO_PENDING)
		{
			return false;
		}
	}
	return true;
}
extern CClientManager g_clientManager;//全局的管理客户端类。
void CClient::AsyRecvBodyCompleted()
{
	//根据包体的内容,确定是什么类型的请求包。是请求学生姓名还是请求发送试卷。
	//前2个字节用于确定类型。
	u_short type;
	memcpy(&type,m_IoRecv.buffer,2);
	switch(type)
	{
	case LOGIN://登录。
		{
			//获取学号。
			char StuNo[128];
			strcpy(StuNo,(m_IoRecv.buffer+2));
			
			CString name=m_pServerView->AuthenticateStuNo(StuNo);
			if(!name.IsEmpty()&&!m_pServerView->IsAlreadyLogin(name))//验证成功。
			{
					m_StuNo=StuNo;
					m_StuName=name;
					AsySendName();//在发送姓名异步IO完成收到通知后,再发送试卷。
					AsyRecvHeader();
				
			}
			else//验证失败。
			{
				AsySendFailedLoginMsg();
			}
		}
		break;
	case DOING://答题状态。
		{
			m_state=type;
			AsyRecvHeader();
		}
		break;
	case DONE://交卷状态。
		{

			m_state=type;
			//从包体中得到考生答案。
			m_Result=m_IoRecv.buffer+2;
			CalculateGrade();
			
		}
		break;
	default:
		break;
	}
	m_pServerView->UpdateClientState(this,type);
}
//发送考生姓名。
bool CClient::AsySendName()
{
	ZeroMemory(&m_IoSend,sizeof(IO_OPERATION_DATA));
	
	WSABUF wsabuf[2];
	//char name[128];
	//memset(name,0,128);
//	WideCharToMultiByte(CP_ACP,WC_COMPOSITECHECK,m_StuName.GetString(),m_StuName.GetLength(),name,128,NULL,NULL);
	m_IoSend.IOType=IOWriteName;
	m_IoSend.hdr.Len=m_StuName.GetLength();
	m_IoSend.hdr.PacketType=STUNAME;
	//发送包头。
	wsabuf[0].buf=(char*)&m_IoSend.hdr;
	wsabuf[0].len=sizeof(HDR);
	//送包体。
	wsabuf[1].buf=m_StuName.GetBuffer();
	wsabuf[1].len=m_StuName.GetLength();
	DWORD flag=0;
	int ret=WSASend(m_s,wsabuf,2,NULL,flag,&m_IoSend.overlapped,NULL);
	if(ret==SOCKET_ERROR)
	{
		int err=WSAGetLastError();
		if(err!=WSA_IO_PENDING)
		{
			return false;
		}
	}
	return true;
}
//发送登录失败。
bool CClient::AsySendFailedLoginMsg()
{
	ZeroMemory(&m_IoSend,sizeof(IO_OPERATION_DATA));
	WSABUF wsabuf[2];
	
	m_IoSend.IOType=IOWriteUnLogin;
	m_IoSend.hdr.Len=0;
	m_IoSend.hdr.PacketType=LOGINFAILED;
	//发送包头。
	wsabuf[0].buf=(char*)&m_IoSend.hdr;
	wsabuf[0].len=sizeof(HDR);
	//发送包体
	//wsabuf[1].buf=""
	int ret=WSASend(m_s,wsabuf,1,NULL,0,&m_IoSend.overlapped,NULL);
	if(ret==SOCKET_ERROR)
	{
		int err=WSAGetLastError();
		if(err!=WSA_IO_PENDING)
		{
			return false;
		}
	}
	return true;
}

void CClient::CalculateGrade()
{
	m_grade=0;
	for(int i=0;im_numOfPaperItem;i++)
	{
		if(m_Result.GetAt(i)-'A'+1==m_pServerView->m_pPaperArray[i].PAP_ANSWER)
		{
			m_grade++;
		}
	}

}
//发送试卷。
bool CClient::AsySendPaper()
{
	ZeroMemory(&m_IoSend,sizeof(m_IoSend));
	WSABUF wsabuf[2];
	//发送包头:
	
	wsabuf[0].buf=(char*)&m_IoSend.hdr;
	wsabuf[0].len=sizeof(HDR);
	//发送包体。
	wsabuf[1].buf=m_pServerView->m_PaperBuff.GetBuffer();
	wsabuf[1].len=m_pServerView->m_PaperBuff.GetLength();
	m_IoSend.IOType=IOWritePaper;
	m_IoSend.hdr.PacketType=PAPER;
	m_IoSend.hdr.Len=wsabuf[1].len;
	int ret=WSASend(m_s,wsabuf,2,NULL,0,&m_IoSend.overlapped,NULL);
	if(ret==SOCKET_ERROR)
	{
		int err=WSAGetLastError();
		if(err!=WSA_IO_PENDING)
		{
			return false;
		}
	}
	return true;
}



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

typedef struct _PaperItem
{
	LONG PAP_ID;
	//CString PAP_NO;
	CString PAP_QUESTION;
	CString PAP_ANS_A;
	CString PAP_ANS_B;
	CString PAP_ANS_C;
	CString PAP_ANS_D;
	BYTE    PAP_ANSWER;
}PaperItem;


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

声明如下:

#pragma once
#include"Client.h"
//class CClient;
#include
class CClientManager
{
public:
	CClientManager(void);
	~CClientManager(void);
public:
	std::listm_ClientList;
	CRITICAL_SECTION m_cs;//用于线程互斥的关键段。防止同时对list进行修改。
public:
	bool addClient(CClient*pClient);
	bool deleteClient(CClient*pClient);
	bool delteAllClient();
};

实现为:

#pragma once
#include "StdAfx.h"
#include "ClientManager.h"


CClientManager::CClientManager(void)
{
	InitializeCriticalSection(&m_cs);
}


CClientManager::~CClientManager(void)
{
	for(std::list::iterator iter=m_ClientList.begin();iter!=m_ClientList.end();iter++)
	{
		delete (*iter);
	}
	m_ClientList.clear();
}
bool CClientManager::addClient( CClient*pClient )
{
	EnterCriticalSection(&m_cs);
	m_ClientList.push_back(pClient);
	LeaveCriticalSection(&m_cs);
	return true;
}

bool CClientManager::deleteClient( CClient*pClient )
{
	//std::list<::iterator iter=m_ClientList.find(pClient);
	EnterCriticalSection(&m_cs);
	delete pClient;
	m_ClientList.remove(pClient);
	LeaveCriticalSection(&m_cs);
	return true;
}

bool CClientManager::delteAllClient()
{
	EnterCriticalSection(&m_cs);
	for(std::list::iterator iter=m_ClientList.begin();iter!=m_ClientList.end();iter++)
	{
		delete (*iter);
	}
	m_ClientList.clear();
	LeaveCriticalSection(&m_cs);
	return true;
}

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

void CIOCPDriverLisenceExamServerView::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	EnterCriticalSection(&g_clientManager.m_cs);
	for(std::list::iterator iter=g_clientManager.m_ClientList.begin();iter!=g_clientManager.m_ClientList.end();)
	{
		CClient*pClient=(*iter);
		if(pClient->m_state==CClient::DOING)
		{
			CTime CurTime=CTime::GetCurrentTime();
			CTimeSpan spanTime(CurTime.GetDay()-pClient->m_time.GetDay(),
				CurTime.GetHour()-pClient->m_time.GetHour(),
				CurTime.GetMinute()-pClient->m_time.GetMinute(),CurTime.GetSecond()-pClient->m_time.GetSecond());
			if(spanTime.GetMinutes()>1)
			{
				//设置考生为断线状态。
				pClient->m_state=CClient::DISCONN;
				//更新显示状态。
				UpdateClientState(pClient,CClient::DISCONN);
				//删除该客户端。
				g_clientManager.deleteClient(pClient);
				iter=g_clientManager.m_ClientList.begin();
			}
			else
			   iter++;
		}
	}
	LeaveCriticalSection(&g_clientManager.m_cs);
	CView::OnTimer(nIDEvent);
}


服务器运行截图:


客户端设计:

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

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

定义如下:

#pragma once
#define BUFFER_SIZE 10240
//服务器向客户端发送的包类型:
#define STUNAME        109//学生姓名。
#define PAPER          110//试卷包。
#define LOGINFAILED    111//登录失败。
enum state//考生状态。
{
	LOGIN,//已登录。
	DOING,//正在答题。
	DONE,//已交卷。
	DISCONN,//因故障断开。
	UNKNOW//原因不明。
};
class CDriverLisenceClientDlg;
class CClientSocket
{
public:
	CClientSocket(CDriverLisenceClientDlg*pDlg,DWORD IP ,short port);
	~CClientSocket(void);
public:
	bool ConnectToServer();//连接到服务器。
	bool LoginServer();//登录。
	bool RecvName();//接收姓名。
	bool RecvPaper();//接收试卷。
	bool SendStart();//通知服务器考试开始。
	bool SendResult();//发送考试结果。
	bool SendPulse();//发送心跳包。
public:
	SOCKET m_s;//客户端套接字。
	char *m_pRecvBuffer;//接收缓冲区。
	char *m_pSendBuffer;//发送缓冲区。
	DWORD m_ServerIP;//服务器IP。
	SHORT m_ServerPort;//服务器端口。
	CDriverLisenceClientDlg*m_pMainDlg;//主窗口指针。
};


实现为:

#include "StdAfx.h"
#include "ClientSocket.h"

#include"DriverLisenceClientDlg.h"
CClientSocket::CClientSocket(CDriverLisenceClientDlg*pDlg, DWORD IP ,short port )
{
	// TODO: 在此添加控件通知处理程序代码
	m_pMainDlg=pDlg;
	m_pSendBuffer=new char[BUFFER_SIZE];
	m_pRecvBuffer=new char[BUFFER_SIZE];
	m_ServerIP=IP;
	m_ServerPort=port;
	WSAData wsadata;
	WSAStartup(MAKEWORD(2,2),&wsadata);
	m_s=socket(AF_INET,SOCK_STREAM,0);

	if(m_s==INVALID_SOCKET)
	{
		closesocket(m_s);
		WSACleanup();
		return ;
	}
}


CClientSocket::~CClientSocket(void)
{
	closesocket(m_s);
}
//连接服务器。
bool CClientSocket::ConnectToServer()
{
	SOCKADDR_IN addr;
	addr.sin_family=AF_INET;
	addr.sin_addr.S_un.S_addr=inet_addr("192.168.1.100");
	addr.sin_port=htons(4000);
	int len=sizeof(addr);
	int ret=connect(m_s,(SOCKADDR*)&addr,len);
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	const char chOpt=1;
	ret=setsockopt(m_s,IPPROTO_TCP,TCP_NODELAY,&chOpt,sizeof(char));
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	return true;
}
//登录到服务器。
bool CClientSocket::LoginServer()
{
	memset(m_pSendBuffer,0,BUFFER_SIZE);
	HDR hdr;
	hdr.PacketType=REQUEST;
	hdr.Len=m_pMainDlg->m_StuNo.GetLength()+2;
	USHORT State=LOGIN;
	int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	strncpy(m_pSendBuffer,(char*)&State,sizeof(USHORT));
	strncpy(m_pSendBuffer+2,m_pMainDlg->m_StuNo.GetBuffer(),m_pMainDlg->m_StuNo.GetLength());//
	ret=send(m_s,m_pSendBuffer,hdr.Len,0);//发送登陆信息包体。
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	return true;
}
//接收姓名。
bool CClientSocket::RecvName()
{
	HDR hdr;
	int ret=recv(m_s,(char*)&hdr,sizeof(HDR),0);//接收包头。
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	memset(m_pRecvBuffer,0,BUFFER_SIZE);
	if(hdr.PacketType==STUNAME)
	{
		ret=recv(m_s,m_pRecvBuffer,hdr.Len,0);//接收姓名。
		if(ret==SOCKET_ERROR)
		{
			return false;
		}
		m_pMainDlg->m_StuName=m_pRecvBuffer;

	}
	else if(hdr.PacketType==LOGINFAILED)
	{
		MessageBox(NULL,"考生号输入有误或该帐号已经登陆!","登录失败",MB_OK);
		return false;
	}

	return true;
}
//接收试卷。
bool CClientSocket::RecvPaper()
{
	HDR hdr;
	int ret=recv(m_s,(char*)&hdr,sizeof(HDR),0);
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	if(hdr.PacketType==PAPER)
	{
		memset(m_pRecvBuffer,0,BUFFER_SIZE);
		ret=recv(m_s,m_pRecvBuffer,hdr.Len,0);
		if(ret==SOCKET_ERROR)
		{
			return false;
		}
		m_pMainDlg->m_PaperBuffer=m_pRecvBuffer;
	}
	return true;
	
}
//发送开始答题消息。
bool CClientSocket::SendStart()
{
	HDR hdr;
	hdr.PacketType=REQUEST;
	hdr.Len=sizeof(USHORT);
	
	int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	memset(m_pSendBuffer,0,BUFFER_SIZE);
	USHORT State=DOING;
	strncpy(m_pSendBuffer,(char*)&State,sizeof(USHORT));//构造两字节的状态。为准备答题。
	 
	ret=send(m_s,m_pSendBuffer,sizeof(USHORT),0);//DOING状态。
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	m_pMainDlg->SetTimer(1,10,NULL);
	return true;
}
//发送答题结果。
bool CClientSocket::SendResult()
{
	HDR hdr;
	hdr.PacketType=REQUEST;
	hdr.Len=sizeof(USHORT)+m_pMainDlg->m_result.GetLength();

	int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	memset(m_pSendBuffer,0,BUFFER_SIZE);
	USHORT State=DONE;
	//strncpy(m_pSendBuffer,(char*)&State,sizeof(USHORT));//交卷状态。
//	strncpy(m_pSendBuffer+sizeof(USHORT),m_pMainDlg->m_result.GetBuffer(),m_pMainDlg->m_result.GetLength());
	ret=send(m_s,(char*)&State,sizeof(USHORT),0);
	ret=send(m_s,m_pMainDlg->m_result.GetBuffer(),m_pMainDlg->m_result.GetLength(),0);//状态后为答题结果。。
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	return true;
}
//发送心跳包。
bool CClientSocket::SendPulse()
{
	HDR hdr;
	hdr.PacketType=PULSE;
	hdr.Len=0;

	int ret=send(m_s,(char*)&hdr,sizeof(hdr),0);//发送包头。
	if(ret==SOCKET_ERROR)
	{
		return false;
	}
	return true;
}


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

声明为:

public:
	afx_msg void OnBnClickedBtnOk();//登录消息响应函数。
	bool ParsePaperBuffer();//扫描试卷缓冲区,取出试题。
	bool StartTheExam();//开始考试。
	static DWORD WINAPI QuesationDlgThread(PVOID pparam);
public:

	short m_port;//服务器端口。
	DWORD m_ServerIP;//服务器IP。
	CString m_StuNo;//考生号。
	CString m_StuName;//从服务器接收的考生姓名。
	std::listm_PaperItemList;//试题链表。每个节点存储一道试题。
	CTimeSpan *m_pTimeRemained;//剩余时间。考试时间为60min.
	CString m_PaperBuffer;//从服务器接收的试卷缓冲区。
	CString m_result;//存储答题结果。
	CClientSocket *m_pClientSocket;//CCClientSocket指针。
	HANDLE m_hEvent;//事件对象,用于在上一题下一题时触发。
	CQuestionDlg*m_pQuestionDlg;//答题窗口。
	std::list::iterator m_IterCurQuestion;
	bool m_IsThreadExit;//决定试题循环线程是否退出。
	afx_msg void OnTimer(UINT_PTR nIDEvent);//考试时间计时。
};

类实现为:

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

void CDriverLisenceClientDlg::OnBnClickedBtnOk()
{
	UpdateData();
	m_pClientSocket=new CClientSocket(this,m_ServerIP,m_port);
	bool ret=m_pClientSocket->ConnectToServer();
	//ASSERT(ret);
	if(!ret)
	{
		MessageBox("服务器未开启,登录失败!","服务器未开启");
		return ;
	}
	ret=m_pClientSocket->LoginServer();
	//ASSERT(ret);
	if(!ret)
	{
		//MessageBox("帐号有误或重复登录,登录失败!","登录失败");
		return ;
	}
	ret=m_pClientSocket->RecvName();
	//ASSERT(ret);
	if(!ret)
	{
		MessageBox("数据接收出现错误,请检查网络连接!!","数据接收出现错误");
		return ;
	}
	ret=m_pClientSocket->SendStart();
	if(!ret)
	{
		MessageBox("数据发送出现错误,请检查网络连接!!","数据接收出现错误");
		return ;
	}
	ret=m_pClientSocket->RecvPaper();
	//ASSERT(ret);
	if(!ret)
	{
		MessageBox("数据接收出现错误,请检查网络连接!!","数据接收出现错误");
		return ;
	}
	ParsePaperBuffer();
	StartTheExam();
	SetTimer(2,1000,NULL);
}
//扫描试题缓冲区,将试题存入试题链表内。
bool CDriverLisenceClientDlg::ParsePaperBuffer()
{
	CPaperItem *pItem;
	char c;
	int i=0;
	int count=0;
	while(i!=m_PaperBuffer.GetLength())
	{
		
		c=m_PaperBuffer[i];
		if(c=='<')
		{
			count=0;
			pItem=new CPaperItem;
		}
		else if(c=='>')
		{
			m_PaperItemList.push_back(pItem);
		}
		else if(c=='|')
		{
			
			count++;
		}
		else
		{
			if(count==0)
			{
				pItem->m_Question+=c;
			}
			else if(count==1)
			{
				pItem->m_A+=c;
			}
			else if(count==2)
			{
				pItem->m_B+=c;
			}
			else if(count==3)
			{
				pItem->m_C+=c;
			}
			else
			{
				pItem->m_D+=c;
			}
		}
		i++;
	}
	return true;
}
//开始考试。
bool CDriverLisenceClientDlg::StartTheExam()
{
	ShowWindow(SW_HIDE);
	DWORD num=m_PaperItemList.size();

	m_pQuestionDlg=new CQuestionDlg;
	bool ret=m_pQuestionDlg->Create(IDD_DLG_PAPER,this);
	ret=m_pQuestionDlg->ShowWindow(SW_SHOW);
	HANDLE hThread=CreateThread(NULL,0,QuesationDlgThread,this,0,NULL);
	CloseHandle(hThread);
	CString s;
	s.Format("              驾驶员理论考试!      试题数:%d",num);
	//CString s;
	m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_NOTIFICATION,s);
	s.Format("姓名:%s",m_StuName);
	m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_STUNAME,s);
	s.Format("考号:%s",m_StuNo);
	m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_STUNO,s);
	//dlg.DoModal();
	
	return true;
}
//在此线程循环内不断遍历试题,显示到对话框。
DWORD WINAPI CDriverLisenceClientDlg::QuesationDlgThread( PVOID pparam )
{
	CDriverLisenceClientDlg*pDlg=(CDriverLisenceClientDlg*)pparam;
	((CButton*)(pDlg->m_pQuestionDlg->GetDlgItem(IDC_BTN_HANDIN)))->EnableWindow(false);
	pDlg->m_IsThreadExit=false;
	//for(std::list::iterator iter=pDlg->m_PaperItemList.begin();iter!=pDlg->m_PaperItemList.end();iter=pDlg->m_IterCurQuestion)//iter++)
	for(std::list::iterator iter=pDlg->m_PaperItemList.begin();!pDlg->m_IsThreadExit;iter=pDlg->m_IterCurQuestion)//iter++)
	{
		if(pDlg->m_PaperItemList.end()!=iter)
		{
			CPaperItem *pPaperItem=(*iter);
			pDlg->m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_QUESTION,pPaperItem->m_Question);
			pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_A,pPaperItem->m_A);
			pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_B,pPaperItem->m_B);
			pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_C,pPaperItem->m_C);
			pDlg->m_pQuestionDlg->SetDlgItemText(IDC_RADIO_ANS_D,pPaperItem->m_D);
			pDlg->m_IterCurQuestion=iter;
		}
		WaitForSingleObject(pDlg->m_hEvent,INFINITE);//等待上一题或下一题触发事件消息。
		
	}
	
	
	return 0;
}

//发送心跳包。
void CDriverLisenceClientDlg::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	if(nIDEvent==1)
	  m_pClientSocket->SendPulse();
	else if(nIDEvent==2)
	{
		CTimeSpan temp(0,0,0,1);
		(*m_pTimeRemained)-=temp;
		CString t;
		t.Format("剩余时间:%d分%d秒",m_pTimeRemained->GetMinutes(),m_pTimeRemained->GetSeconds());
		m_pQuestionDlg->SetDlgItemTextA(IDC_STATIC_TIMEREMAINED,t);
	}
	CDialogEx::OnTimer(nIDEvent);
}

客户端登录界面:



答题窗口:


对程序流程不是很清楚的话,对上面这些东西会觉得晕乎乎的。如果想要非常彻底的对IOCP及程序流程有更深的理解,推荐自己动手实现这样一个系统。在动手过程中会遇到很多困难,但解决这些困难会不断加深你的理解。当你最终靠着自己努力实现完成后,你会对先前迷惑的东西恍然大悟的。相信我!!
以上实例参考自《精通Windows socekt网络开发--基于Visual C++实现》但做不少的修改。这个示例更偏重于对IOCP的讲解,因此没有对程序逻辑太过重视。程序有一处已知的bug:虽然实现了上一题下一题功能,但是必须按顺序一次做完。回来修改不起任何作用,还会导致答案混乱。虽知道解决方法但不做修改。客户端由于实现时非常简单,只是草草做了下。有很多不太合适的地方。特此提示。
By ithzhang 转载请注明出处。
2013.1.13于山西大同

你可能感兴趣的:(Windows socket之IOCP实例----IOCP开发驾照理论考试系统)