接下来为项目添加CClient类用于在套接字上实现数据发送和接收。
在VS2015左侧“解决方案资源管理器”中选中“IOCP_Server”项目,之后在右键菜单中选择“添加->类”,如图3所示。
图3 为项目添加类
之后,在添加类的对话框中选择添加C++类,之后点击“添加”按键,如图4所示。
图4 添加C++类
接下来将类名设置为“CClient”,其它选项自动生成即可,如图5所示。
图5 添加CClient类
最后,点击“完成”按键。这样就在项目中添加了一个名为CClient的类,该类对应的文件为Client.h和Client.cpp。
Client类中需要包含套接字变量以及客户端网络信息变量,因此在Client类的构造函数中需要对这两个变量进行初始化。
CClient::CClient(SOCKET s, SOCKADDR_IN addr)
{
m_s = s;
m_addr = addr;
}
其中m_s表示与客户端通信的套接字;m_addr表示客户端的网络信息。以上两个变量均为CClient类的private成员变量。
SOCKET m_s;
SOCKADDR_IN m_addr;
在CClient类的析构函数中,关闭与客户端通信的套接字。
CClient::~CClient()
{
closesocket(m_s);
}
在CClient类中自定义数据结构
typedef struct _io_operation_data
{
OVERLAPPED overlapped;
WSABUF dataBuf;
BYTE type;
}IO_OPERATION_DATA, *PIO_OPERATION_DATA;
该结构的成员包含了重叠I/O结构OVERAPPED、数据缓冲区结构WSABUF、用于保存发送/接收数据的缓冲区和长度、以及I/O操作类型标志等。
(1)OVERAPPED对象
OVERAPPED对象overlapped主要用在服务端发出重叠I/O请求,例如接收数据或者发送数据时使用。线程池中的服务线程在被激活时,即“2 相关API函数”中提到的GetQueuedCompletionStatus()函数返回时,该函数的第四个参数就保存了该OVERAPPED对象。在服务线程中,通过GetQueuedCompletionStatus()函数获取到的OVERAPPED对象,就可以得到自定义结构IO_OPERATION_DATA的指针,进而在服务线程中获取重叠I/O操作的类型。
(2)重叠I/O操作类型
在“1.2 完成端口模型基本原理”中提到,线程池中的服务线程获取消息队列中的完成通知,并在服务线程中处理套接字数据。但是服务线程只是知道有重叠I/O操作完成,但是无法知道完成的这个重叠I/O操作到底是读操作还是写操作。自定义结构IO_OPERATION_DATA中的成员变量type的作用就是指定重叠I/O操作到底是读操作还是写操作。
(3)缓冲区
重叠I/O操作的函数WSAWRecv()和WSASend()都需要WSABUF结构为缓冲区。因此,自定义结构IO_OPERATION_DATA中的WSABUF结构的对象dataBuf,用于指定重叠I/O操作的缓冲区。
为CClient类添加public权限的成员函数Send(),该函数的作用是发出发送数据的重叠I/O请求。
bool CClient::Send(char* sendBuf, int length)
{
DWORD flags = 0;
DWORD sendBytes = 0;
m_IO.type = WRITE;
m_IO.dataBuf.buf = sendBuf;
m_IO.dataBuf.len = length;
if (WSASend(m_s, &m_IO.dataBuf, 1, &sendBytes, flags, (LPOVERLAPPED)&(m_IO.overlapped), NULL) == SOCKET_ERROR)
{
return false;
}
return true;
}
在以上代码中,m_IO是定义结构IO_OPERATION_DATA的对象,将type定义为WRITE,即写操作,WRITE为自定义常量。
#define WRITE 1
之后,通过WSASend()函数发出发送数据的重叠I/O请求,m_IO.dataBuf中保存了要发送数据的缓冲区及缓冲区大小。
为CClient类添加public权限的成员函数Recv(),该函数的作用是发出接收数据的重叠I/O请求。
bool CClient::Recv()
{
DWORD flags = 0;
DWORD recvBytes = 0;
ZeroMemory(&m_IO, sizeof(IO_OPERATION_DATA));
m_IO.type = READ;
if (WSARecv(
m_s
, &m_IO.dataBuf
, 1
, &recvBytes
, &flags
, &m_IO.overlapped
, NULL
) == SOCKET_ERROR)
{
if (ERROR_IO_PENDING != WSAGetLastError())
{
return false;
}
}
return true;
}
在以上代码中,READ是自定义常量,表示重叠I/O操作是读操作,该变量定义如下
#define READ 0
当接收数据的重叠I/O操作完成之后,接收到的数据保存在m_IO.dataBuf中。在线程池中的服务线程此时会通过GetQueuedCompletionStatus()函数获取到重叠I/O结构的指针,即在WSARecv()函数中指定的&m_IO.overlapped。通过该指针,线程池中的服务线程会得到完成的重叠I/O操作的类型,即m_IO.type。
在通过重叠I/O操作接收到数据之后,数据保存在m_IO.dataBuf中。之后可以对接收到的数据进行操作。
为CClient定义public权限的成员变量Handle(),在该函数中,对m_IO.dataBuf进行处理。
通过“2 相关API函数”中提到的CreateIoCompletionPort()函数将完成端口与套接字关联。
在“3.6 接收客户端的连接”中提到,在while语句中,服务端调用WSAAccept()函数接收客户端的连接。如果没有客户端连接,WSAAccept()函数不会返回,主程序将会挂起;当有客户端连接,WSAAccept()函数返回一个新建的套接字sAccept,服务端使用这个新建的套接字sAccept与客户端进行通信。接下来,仍然在while语句中,使用CreateIoCompletionPort()函数将完成端口与套接字关联。
CClient* pClient = new CClient(sAccept, servAddr);
if (CreateIoCompletionPort((HANDLE)sAccept, hComPort, (DWORD)pClient, 0) == NULL)
{
return -1;
}
其中,pClient是“3.7 CClient类”中提到的CClient类的指针,该指针作为CreateIoCompletionPort()函数的第三个参数,即完成键使用。在线程池的服务线程中,只知道有重叠I/O操作完成,但是并不知道该重叠I/O操作是发生在哪个套接字上的。因此,将CClient类的指针pClient作为完成键,在线程池的服务线程中,可以通过GetQueuedCompletionStatus()函数获取这个完成键,即获取到了Client类的指针,也就是知道了完成重叠I/O操作的套接字的信息了。