SOCKET IOCP 一

  • 引言
  • 便于理解小帮手
  • Accept 方式的IOCP代码


引言

最近又有空了,但是最近看见很多关于Java netty的东西,大家想到最多的就是NIO,事实上netty在windows下我想应该使用的是select没有iocp(AIO),所以性能会和linux上有很大差距,但这并不妨碍老衲写几篇关于IO的文章。看这篇文章前可以先看看。

  • Windows和Linux IO模型简单介绍
  • IOCP 简单的完成端口读写文件

循序渐进的方式来看IOCP,如果我直接使用acceptex方式来讲IOCP,可能很多小伙伴会被完成键和OL结构绕晕,所以分2篇写,这篇使用accept监听,但收发使用IOCP

看了之前的文章会了解到,使用IOCP最起码需要2步
1 创建一个完成端口

  • CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

2 将刚才创建的完成端口与某个设备进行关联,并传入完成键和OL结构指针
完成键的理解小伙伴们可以将它理解为自定义的消息,比如linux中的信号,我可以自定义某个信号,windows窗口消息中的自定义消息
OL结构我想不必多说了,是异步IO必须存在的结构,里面包含了event对象等。

便于理解小帮手

在iocp传输文件的那一篇文章中,我们自定义了完成键和创建了ol结构,通过主动触发让它工作起来。但是在socket的操作中,我们不会单纯的传一个完成键和ol结构,为了方便理解,我这里说一下重点,不然除了学c/c++的同学,其他语言的同学可能会有点迷。
事实上我们设置的完成键和ol结构,为了节省开销,都是传入指针,HANDLE 本身也可以理解为对象指针。
所以在socket开发中,我们其实可以传入自己定义的结构,这样在接收时通过指针,就能转变为我们自己的结构,而不是单纯传一个数字(完成键)和ol结构指针。事实上我就是这么做的

Accept 方式的IOCP代码

// IOCP_SOCKET.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <iostream>
#include <WinSock2.h>
#include <Windows.h>

#include <vector>


using namespace std;

#pragma comment(lib, "Ws2_32.lib")		// Socket编程需用的库
#pragma comment(lib, "Kernel32.lib")	// IOCP需要用到的库

/**
 * 自己定义封装OL结构,加了一些东西
 **/
const int DataBuffSize = 2 * 1024;
typedef struct
{
	OVERLAPPED overlapped;
	WSABUF databuff;
	char buffer[DataBuffSize];
	int BufferLen;
	int operationType;
}PER_IO_OPERATEION_DATA, *LPPER_IO_OPERATION_DATA, *LPPER_IO_DATA, PER_IO_DATA;

/**
* 完成键,好让我们知道是哪个socket触发的
 **/
typedef struct
{
	SOCKET socket;
	SOCKADDR_STORAGE ClientAddr;
}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;

// 定义全局变量
const int DefaultPort = 8080;
vector < PER_HANDLE_DATA* > clientGroup;		// 记录客户端的向量组

HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);
DWORD WINAPI ServerWorkThread(LPVOID CompletionPortID);
DWORD WINAPI ServerSendThread(LPVOID IpParam);

// 开始主函数
int main()
{
	// 加载socket动态链接库
	WORD wVersionRequested = MAKEWORD(2, 2); // 请求2.2版本的WinSock库
	WSADATA wsaData;	// 接收Windows Socket的结构信息
	DWORD err = WSAStartup(wVersionRequested, &wsaData);

	if (0 != err) {	// 检查套接字库是否申请成功
		cerr << "Request Windows Socket Library Error!\n";
		system("pause");
		return -1;
	}
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {// 检查是否申请了所需版本的套接字库
		WSACleanup();
		cerr << "Request Windows Socket Version 2.2 Error!\n";
		system("pause");
		return -1;
	}

	// 创建IOCP的内核对象
		/**
		 * 需要用到的函数的原型:
		 * HANDLE WINAPI CreateIoCompletionPort(
		 *    __in   HANDLE FileHandle,		// 已经打开的文件句柄或者空句柄,一般是客户端的句柄
		 *    __in   HANDLE ExistingCompletionPort,	// 已经存在的IOCP句柄
		 *    __in   ULONG_PTR CompletionKey,	// 完成键,包含了指定I/O完成包的指定文件
		 *    __in   DWORD NumberOfConcurrentThreads // 真正并发同时执行最大线程数,一般推介是CPU核心数*2
		 * );
	 **/

	//创建一个完成端口
	HANDLE completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
	if (NULL == completionPort) {	// 创建IO内核对象失败
		cerr << "CreateIoCompletionPort failed. Error:" << GetLastError() << endl;
		system("pause");
		return -1;
	}

	// 创建IOCP线程--线程里面创建线程池

		// 确定处理器的核心数量
	SYSTEM_INFO mySysInfo;
	GetSystemInfo(&mySysInfo);

	// 基于处理器的核心数量创建线程
	long threadCount = mySysInfo.dwNumberOfProcessors==8 ? 8: 3 +(mySysInfo.dwNumberOfProcessors * 5 / 8)
	for (DWORD i = 0; i < threadCount; ++i) {
		// 创建服务器工作器线程,并将完成端口传递到该线程
		HANDLE ThreadHandle = CreateThread(NULL, 0, ServerWorkThread, completionPort, 0, NULL);
		if (NULL == ThreadHandle) {
			cerr << "Create Thread Handle failed. Error:" << GetLastError() << endl;
			system("pause");
			return -1;
		}
		CloseHandle(ThreadHandle);
	}

	// 建立流式套接字
	SOCKET srvSocket = socket(AF_INET, SOCK_STREAM, 0);

	// 绑定SOCKET到本机
	SOCKADDR_IN srvAddr;
	srvAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	srvAddr.sin_family = AF_INET;
	srvAddr.sin_port = htons(DefaultPort);
	int bindResult = bind(srvSocket, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR));
	if (SOCKET_ERROR == bindResult) {
		cerr << "Bind failed. Error:" << GetLastError() << endl;
		system("pause");
		return -1;
	}

	// 将SOCKET设置为监听模式
	int listenResult = listen(srvSocket, 1024);
	if (SOCKET_ERROR == listenResult) {
		cerr << "Listen failed. Error: " << GetLastError() << endl;
		system("pause");
		return -1;
	}


	// 开始处理IO数据
	cout << "本服务器已准备就绪,正在等待客户端的接入...\n";

	// 创建用于发送数据的线程,就是个客户端而已
	HANDLE sendThread = CreateThread(NULL, 0, ServerSendThread, 0, 0, NULL);

	while (true) {
		
		//同步Accept
		SOCKADDR_IN saRemote;
		int RemoteLen;
		SOCKET acceptSocket;

		// 接收连接,并分配完成端,这儿可以用AcceptEx()
		RemoteLen = sizeof(saRemote);
		acceptSocket = accept(srvSocket, (SOCKADDR*)&saRemote, &RemoteLen);
		if (SOCKET_ERROR == acceptSocket) {	// 接收客户端失败
			cerr << "Accept Socket Error: " << GetLastError() << endl;
			closesocket(acceptSocket);
			continue;
		}

		// 创建用来和套接字关联的单句柄数据信息结构
		PER_HANDLE_DATA * PerHandleData = NULL;
		PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));	// 在堆中为这个PerHandleData申请指定大小的内存
		PerHandleData->socket = acceptSocket;
		memcpy(&PerHandleData->ClientAddr, &saRemote, RemoteLen);
		clientGroup.push_back(PerHandleData);		// 将单个客户端数据指针放到客户端组中

		// 将接受套接字和完成端口关联
		CreateIoCompletionPort((HANDLE)(PerHandleData->socket), completionPort, (DWORD)PerHandleData, 0);


		// 开始在接受套接字上处理I/O使用重叠I/O机制
		// 在新建的套接字上投递一个或多个异步
		// WSARecv或WSASend请求,这些I/O请求完成后,工作者线程会为I/O请求提供服务
		// 单I/O操作数据(I/O重叠)
		LPPER_IO_OPERATION_DATA PerIoData = NULL;
		PerIoData = (LPPER_IO_OPERATION_DATA)GlobalAlloc(GPTR, sizeof(PER_IO_OPERATEION_DATA));
		ZeroMemory(&(PerIoData->overlapped), sizeof(OVERLAPPED));
		PerIoData->databuff.len = 1024;
		PerIoData->databuff.buf = PerIoData->buffer;
		PerIoData->operationType = 0;	// read

		DWORD RecvBytes;
		DWORD Flags = 0;
		WSARecv(PerHandleData->socket, &(PerIoData->databuff), 1, &RecvBytes, &Flags, &(PerIoData->overlapped), NULL);

		
		
	}

	system("pause");
	return 0;
}

// 开始服务工作线程函数
DWORD WINAPI ServerWorkThread(LPVOID IpParam)
{
	HANDLE CompletionPort = (HANDLE)IpParam;
	DWORD BytesTransferred;
	LPOVERLAPPED IpOverlapped;
	LPPER_HANDLE_DATA PerHandleData = NULL;
	LPPER_IO_DATA PerIoData = NULL;
	DWORD RecvBytes;
	DWORD Flags = 0;
	BOOL bRet = false;

	while (true) {
		bRet = GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (PULONG_PTR)&PerHandleData, (LPOVERLAPPED*)&IpOverlapped, INFINITE);
		
		if (bRet == 0) {
			int lastError = GetLastError();
			closesocket(PerHandleData->socket);
			GlobalFree(PerHandleData);
			
			WaitForSingleObject(hMutex, INFINITE);
			cerr << "GetQueuedCompletionStatus Error: " << lastError << endl;
			ReleaseMutex(hMutex);
			continue;
		}

		//这行事实上可以不要,因为我们自己封装的头部就是ol结构,只是为了规范
		PerIoData = (LPPER_IO_DATA)CONTAINING_RECORD(IpOverlapped, PER_IO_DATA, overlapped);

		// 检查在套接字上是否有错误发生
		if (0 == BytesTransferred) {
			closesocket(PerHandleData->socket);
			GlobalFree(PerHandleData);
			GlobalFree(PerIoData);
			continue;
		}

		// 开始数据处理,接收来自客户端的数据
		WaitForSingleObject(hMutex, INFINITE);
		cout << "A Client says: " << PerIoData->databuff.buf << endl;
		ReleaseMutex(hMutex);

		// 为下一个重叠调用建立单I/O操作数据
		ZeroMemory(&(PerIoData->overlapped), sizeof(OVERLAPPED)); // 清空内存
		PerIoData->databuff.len = 1024;
		PerIoData->databuff.buf = PerIoData->buffer;
		PerIoData->operationType = 0;	// read
		WSARecv(PerHandleData->socket, &(PerIoData->databuff), 1, &RecvBytes, &Flags, &(PerIoData->overlapped), NULL);
	}

	return 0;
}


// 发送信息的线程执行函数
DWORD WINAPI ServerSendThread(LPVOID IpParam)
{
	while (1) {
		char talk[200];
		cin.getline(talk, 200);
		int len;
		for (len = 0; talk[len] != '\0'; ++len) {
			// 找出这个字符组的长度
		}
		talk[len] = '\n';
		talk[++len] = '\0';
		printf("I Say:");
		cout << talk;
		WaitForSingleObject(hMutex, INFINITE);
		for (UINT i = 0; i < clientGroup.size(); ++i) {
			send(clientGroup[i]->socket, talk, 200, 0);	// 发送信息
		}
		ReleaseMutex(hMutex);
	}
	return 0;
}