文章目录
1. 完成端口IOCP模型
一个完成端口实际上就是一个通知队列,操作系统把已经完成的重叠I/0请求的通知放到队列中。完成端口会充分利用Windows最复杂的内核对象来进行I/O的调度,属于异步IO,是用于C/S通信模式中性能最好的网络通信模型。
2. 完成端口IOCP模型的原理
完成端口创建几个线程(系统CPU的数目、避免线程上下文切换),等到用户请求的时候,就把这些请求都加入到一个公共消息队列中去,然后这几个线程就排队从消息队列中取出消息并加以处理,这种方式就很优雅的实现了异步通信和负载均衡的问题,因为它提供了一种机制来使用几个线程“公平的“处理来自于多个客户端的输入/输出,并且线程如果没事干的时候也会被系统挂起,不会占用CPU周期,这个关键的作为交换的消息队列,就是完成端口。
3. 使用完成端口的基本流程
1.调用CreateloCompletionPort()函数创建一个完成端口,第四个参数设为0,让完成端口上每个处理器一次只允许执行一个线程
2.根据系统中CPU核心的数量建立对应的Worker线程
3.一个用于监听的Socket,在指定的端口上监听连接请求
4.将接受的套接字绑定到完成端口
5.使用重叠I0,在套接字上投递一个或者多个WSARecv或者WSASend请求
6.在Worker线程使用GetQueuedCompletionStatus函数,它让Worker线程进入不占用CPU的睡眠状态,直到完成端口上出现了需要处理的网络操作或者超出了等待的时间限制为止。
7.重复5-6步骤
4. CreateloCompletionPort函数
HANDLE createloCompletionPort(
HANDLE FileHandle,//有效的文件句柄或INVALID_HANDLE_VALUE
HANDLE ExistingCompletionPort,//已经存在的完成端口,为NULL则新建一个IOCP
ULONG_PTR CompletionKey,//传送给处理函数的参数
DWORD NumberOfConcurrentThreads //有多少个线程在访问这个消息队列。当参数ExistingCompletionPort不为0的时候,系统忽略该参数,当该参数为0表示允许同时相等数目于处理器个数的线程访问该消息队列。
);
返回值:
- 成功返回一个IOCP的句柄
- 失败返回为NULL
5. GetQueuedCompletionStatus函数
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds
);
调用参数:
CompletionPort
:指定的IOCP,该值由CreateIoCompletionPort函数创建。
lpnumberofbytes
:一次完成后的I/O操作所传送数据的字节数。
lpcompletionkey
:当文件I/O操作完成后,用于存放与之关联的CK。
lpoverlapped
:为调用IOCP机制所引用的OVERLAPPED结构。
dwmilliseconds
:用于指定调用者等待CP的时间。
返回值:
调用成功,则返回非零数值,相关数据存于lpNumberOfBytes、lpCompletionKey、lpoverlapped变量中。失败则返回零值。
WSAOVERLAPPED结构
typedef struct _OVERLAPPEDPLUS
{
OVERLAPPED ol;
SOCKET s, sclient;
int OpCode;
WSABUF wbuf;
DWORD dwBytes, dwFlags;
}OVERLAPPEDPLUS;
调用函数,GetQueuedCompletionStatus(hIocp, &dwBytesXfered,(PULONG_PTR)&PerHandleKey, &Overlap, INFINITE);
最后一个参数为等待时间,为INFINITE时直至有信号返回。
6. WSARecv函数
在重叠模型中,接收数据就要靠它了,它的参数也比recv要多,因为要用到重叠结构嘛,它是这样定义的:
int WSARecv(
SOCKET s, // 当然是投递这个操作的套接字
,与Recv函数不同
// 这里需要一个由WSABUF结构构成的数组
DWORD dwBufferCount, // 数组中WSABUF结构的数量
LPDWORD lpNumberOfBytesRecvd, // 如果接收操作立即完成,这里会返回函数调用所接收到的字节数
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 完成例程中将会用到的参数,我们这里设置为 NULL
);
返回值:
WSA_IO_PENDING
: 最常见的返回值,这是说明我们的WSARecv操作成功了,但是I/O操作还没有完成,所以我们就需要绑定一个事件来通知我们操作何时完成
7. 服务端编程
#include <WinSock2.h>
#include <iostream>
using namespace std;
#pragma comment(lib,"Ws2_32.lib")
//工作者线程有一个参数,是指向完成端口的句柄
DWORD WINAPI WorkerThread(LPVOID CompletionPortId);
//自定义结构体方便WSARecv操作所需的参数
typedef struct _MY_WSAOVERLAPPED
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
DWORD NumberOfBytesRecvd;
DWORD Flags;
_MY_WSAOVERLAPPED()
{
Buffer.buf = new char[64]{ 0 };
Buffer.len = 64;
Flags = 0;//设置为0
NumberOfBytesRecvd = 0;
overlap.hEvent = NULL;//完成端口中不需要事件,置空
}
~_MY_WSAOVERLAPPED()
{
delete[]Buffer.buf;
Buffer.buf = NULL;
Buffer.len = 0;
}
} MY_WSAOVERLAPPED, *PMY_WSAOVERLAPPED;
int main()
{
// 初始化Winsock 2.2
WSAData wsaData;
if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
{
cout << "WSAStartup error:" << WSAGetLastError() << endl;
return -1;
}
//创建完成端口
HANDLE completionPort= CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (completionPort == NULL)
{
cout << "CreateIoCompletionPort error:" << WSAGetLastError() << endl;
return -1;
}
//获取CPU数量,为每个处理器创建一个线程
SYSTEM_INFO sysInfo;
::GetSystemInfo(&sysInfo);
for (int i = 0; i < (int)sysInfo.dwNumberOfProcessors; i++)
{
//完成端口作为线程参数传入线程处理函数
HANDLE h = CreateThread(NULL, 0, WorkerThread, completionPort, 0, NULL);
CloseHandle(h);
}
cout << "创建了" << sysInfo.dwNumberOfProcessors << "个工作线程!" << endl;
//创建监听套接字,注意WSA_FLAG_OVERLAPPED参数, 如果使用socket(),默认就是WSA_FLAG_OVERLAPPED
SOCKET sListen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (sListen == INVALID_SOCKET)
{
cout << "socket() Error: " << WSAGetLastError() << endl;
return -1;
}
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(8000);
sin.sin_addr.S_un.S_addr = htonl(ADDR_ANY);
//绑定到本机任意地址
if (SOCKET_ERROR == bind(sListen, (sockaddr*)&sin, sizeof(sin)))
{
cout << "bind error:" << WSAGetLastError() << endl;
return -1;
}
//监听套接字, Maximum queue length specifiable by listen
if (listen(sListen, SOMAXCONN) == SOCKET_ERROR)
{
cout << " listen() Error:" << WSAGetLastError() << endl;
return -1;
}
while (true)
{
// 接受连接 ,和accept函数类似
SOCKET sClient = WSAAccept(sListen, NULL, NULL, NULL, NULL);
if (sClient == INVALID_SOCKET)
{
cout << " accept() Error:" << WSAGetLastError() << endl;
break;
}
cout << sClient << " 进入聊天室!" << endl;
char tmp[64];
sprintf(tmp, "欢迎%d进入聊天室!", sClient);
send(sClient, tmp, strlen(tmp), 0);
//将接受的套接字sClient绑定到完成端口,一样的函数,这次就不是创建了,而是绑定
CreateIoCompletionPort((HANDLE)sClient, completionPort, (ULONG_PTR)sClient, 0);
PMY_WSAOVERLAPPED pOver = new MY_WSAOVERLAPPED;
//投递WSARecv请求
int ret = WSARecv(sClient, &pOver->Buffer, 1,
&pOver->NumberOfBytesRecvd, &pOver->Flags, &pOver->overlap, NULL);
if (ret == SOCKET_ERROR)
{
int err = GetLastError();
if (err == WSA_IO_PENDING)//请求投递成功
{
}
else
{
closesocket(sClient);
delete pOver;
cout << "WSARecv error" << err << endl;
}
}
}
//为了让WorkerThread退出,我们需要约定一个规则,即completionKey为空时退出
PostQueuedCompletionStatus(completionPort, 0, NULL, NULL);
//关闭完成端口
CloseHandle(completionPort);
//关闭套接字
closesocket(sListen);
// 最后应该做一些清除工作
if (WSACleanup() == SOCKET_ERROR)
{
cout << "WSACleanup 出错!" << endl;
}
}
DWORD WINAPI WorkerThread(LPVOID CompletionPortId)
{
HANDLE completionPort = (HANDLE)CompletionPortId;
DWORD dwByteTransferred;
SOCKET sClient=NULL;//用于获取参数指针
PMY_WSAOVERLAPPED pOver = NULL;
while (true)
{
bool b=GetQueuedCompletionStatus(completionPort, &dwByteTransferred,
(PULONG_PTR)&sClient, (LPOVERLAPPED*)&pOver, INFINITE);
//检测,一般completionKey为sClient,不会为NULL,否则退出线程
if (sClient == NULL)
{
return 0;
}
//IO操作完成,数据可直接使用
if (b && dwByteTransferred > 0)
{
//直接在WSABUF中获取数据
cout << sClient << " 说: " << pOver->Buffer.buf << endl;
//清空缓存数据
ZeroMemory(pOver->Buffer.buf, 64);
//继续投递WSARecv请求
//投递WSARecv请求
int ret = WSARecv(sClient, &pOver->Buffer, 1,
&pOver->NumberOfBytesRecvd, &pOver->Flags, &pOver->overlap, NULL);
if (ret == SOCKET_ERROR)
{
int err = GetLastError();
if (err == WSA_IO_PENDING)//请求投递成功
{
}
else
{
closesocket(sClient);
delete pOver;
cout << "WSARecv error" << err << endl;
}
}
}
else
{
cout << sClient << " 离开了!" << endl;
::closesocket(sClient);
delete pOver;//释放结构体
//cout << "GetQueuedCompletionStatus error: " << WSAGetLastError() << endl;
}
}
return 0;
}
8. 客户端编程
#include<winsock2.h>//winsock2的头文件
#include<iostream>
using namespace std;
//勿忘,链接dll的lib
#pragma comment(lib, "ws2_32.lib")
int main()
{
//加载winsock2的环境
WSADATA wd;
if (WSAStartup(MAKEWORD(2, 2), &wd) != 0)
{
cout << "WSAStartup error:" << GetLastError() << endl;
return 0;
}
//1.创建流式套接字
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET)
{
cout << "socket error:" << GetLastError() << endl;
return 0;
}
//2.链接服务器
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8000);
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
int len = sizeof(sockaddr_in);
if (connect(s, (SOCKADDR*)&addr, len) == SOCKET_ERROR)
{
cout << "connect error:" << GetLastError() << endl;
return 0;
}
//3接收服务端的消息
char buf[100] = { 0 };
recv(s, buf, 100, 0);
cout << buf << endl;
//3随时给服务端发消息
int ret = 0;
do
{
char buf[64] = { 0 };
cout << "请输入聊天内容:";
cin >> buf;
ret = send(s, buf, 64, 0);
} while (ret != SOCKET_ERROR&& ret != 0);
//4.关闭监听套接字
closesocket(s);
//清理winsock2的环境
WSACleanup();
return 0;
}