https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
基于TCP/IP的网络编程有5种模型:
SELECT模型
事件选择模型
异步选择模型
重叠IO模型
完成端口模型
这次先讲第四种。
还是重叠IO模型,但是是基于完成例程的,例程可以理解为回调函数。
我们先把完成例程和事件通知两种重叠IO模型的思想厘清。
完成例程 | 事件通知 | |
---|---|---|
相同1 | 异步完成后调用;AcceptEx函数的使用 | 异步完成后调用;AcceptEx函数的使用 |
相同2 | 当客户端多次向服务器端发送数据(调用多次send),服务器会产生多个recv信号,但是在第一次接收消息的时候就会收完所有数据 | 当客户端多次向服务器端发送数据(调用多次send),服务器会产生多个recv信号,但是在第一次接收消息的时候就会收完所有数据 |
不同1 | 系统自动根据不同操作(WSASend、WSARecv等)完成后,自动调用对应的函数,WSASend、WSARecv有绑定回调函数 | 根据事件类型有不同的信号,然后编写不同的代码 |
不同2 | 系统根据具体事件自动调用回调函数,自动分类,性能好 | 在WSAWaitForMultipleEvents自己判断信号,执行顺序无法保证,循环次数和客户端数量正比,下标越大的客户端延迟越大 |
貌似完成例程由系统直接根据操作来调用相应函数,不像事件通知还要手工判断信号然后进行相应处理,完成例程少了判断信号的步骤。
简而言之就是完成例程代码逻辑上更加简单,性能也更好(系统帮你干活效率高)。
重叠IO模型:完成例程代码逻辑
代码逻辑其实和事件通知一样的,不同的是在完成例程中的WSASend、WSARecv函数要额外绑定回调函数,在执行完WSASend、WSARecv操作后,会系统会自动调用绑定的回调函数,AcceptEx还是和事件通知的一样,这个函数是没有绑定回调函数的功能的(名字看上去就和其他两个不一样,其实是因为在Accept操作完成后没有什么必要的后续操作,因此也就没有绑定回调函数):
1.创建事件(optional)、SOCKET数组,重叠结构体数组(根据下标来进行对应,相同下标是一组)
2.创建重叠IO模型使用的SOCKET:WSASocket
3.投递AcceptEx
3.1立即完成,此时有客户端连接
3.1.1对客户端套接字投递WSARecv
3.1.1.1有客户端消息,系统空闲,立即完成,自动调用回调函数,跳3.1.1
3.1.1.2无客户端消息,跳3.3
3.1.2根据需求对客户端套接字投递WSASend
3.1.2.1有消息要发送,系统空闲,立即完成,自动调用回调函数,跳3.1.2
3.1.2.2无消息要发送,跳3.3
3.1.3如果需要连接客户端跳3
3.2延迟完成,此时没有客户端连接,跳3.3
3.3循环等待信号(WSAWaitForMultipleEvents)只用等服务器的信号
3.3.1 没信号,等到有为止
3.3.2 有信号,先获取重叠结构上的信息(WSAGetOverlappedResult) 肯定是服务器的信号
3.3.2.1 如果有信号肯定是请求连接信号,跳转3
重叠IO模型:完成例程代码实现
回调函数介绍
既然伪代码差不多,因此具体代码实现和上一节差不多,这里只看改什么东西。
下看WSARecv
int WSAAPI WSARecv(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
最后一个参数,在事件通知中设置的是NULL,这里完成例程中要设置为要绑定的回调函数。
回调函数的定义为:
typedef
void
(CALLBACK * LPWSAOVERLAPPED_COMPLETION_ROUTINE)(
DWORD dwError,
DWORD cbTransferred,
LPWSAOVERLAPPED lpOverlapped,
DWORD dwFlags
);
从名字上可以推断这是一个回调函数指针。具体可以看这里:
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nc-winsock2-lpwsaoverlapped_completion_routine
稍微解释一下:
void:代表没有返回值
CALLBACK:代表这个函数是一个回调函数(具体的调用约定可以转到定义自己看),后面接一个自己起的函数名字。
参数1:错误码,TCP要额外判断10054的错误码,表示客户端点×退出
参数2:发送或者接收到的字节数,如果该值为0表示客户端正常退出
参数3:重叠结构
参数4:函数执行方式,这个和WSARecv、WSASend的参数5意思是一样的。
需要说一下,这个回调函数是由系统自动调用的,是执行完下面这个函数自动调用:
BOOL WSAAPI WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer,
BOOL fWait,
LPDWORD lpdwFlags
);
因此二者有很多联系,注意看下面代码中参数的对应关系。
回调函数 | WSAGetOverlappedResult |
---|---|
DWORD dwError | WSAGetOverlappedResult 的错误码就是回调函数产生的错误码 |
DWORD cbTransferred | LPDWORD lpcbTransfer |
DWORD dwFlags | LPDWORD lpdwFlags |
LPWSAOVERLAPPED lpOverlapped | LPWSAOVERLAPPED lpOverlapped |
在回调函数中的处理流程和之前的接收到信号后的流程差不多:
1.dwError ==10054,表示客户端点击×关闭窗口,需要删除客户端和对应重叠IO结构体;
2.cbTransferred == 0,表示客户端正常退出, 需要删除客户端和对应重叠IO结构体;
cbTransferred != 0,接收数据成功,处理接收到的数据
3.其他情况,发送数据成功?
回调函数的代码实现
这里只写接收数据的回调函数:RecvCallBack,当然要记得把这个名字放到WSARecv的最后一个参数那里,否则系统就不知道自动调用哪个回调函数。
void CALLBACK RecvCallBack(DWORD dwError,DWORD cbTransferred,LPWSAOVERLAPPED lpOverlapped,DWORD dwFlags)
{
int i = 0;//位置
//循环遍历重叠IO结构体数组,找到当前重叠IO结构体在数组中的位置
for(i; i < gi_count; i++)
{
//重叠IO结构体的事件句柄一样代表找到了
if(garr_olpAll[i].hEvent=lpOverlapped->hEvent)
{
break;
}
}
//无论是非正常dwError == 10054或者正常退出cbTransferred == 0都需要做相同操作
//需要删除客户端和对应重叠IO结构体,这一块的代码和事件通知是一样的
if (dwError == 10054 || cbTransferred == 0)
{
printf("客户端下线!");
//关闭客户端SOCKET和事件句柄
closesocket(garr_sockAll[i]);
WSACloseEvent(garr_olpAll[i].hEvent);
//从数组中删除客户端SOCKET和事件,这里思路用数组最后一位替换当前元素
garr_sockAll[i] = garr_sockAll[gi_count-1];
garr_olpAll[i] = garr_olpAll[gi_count-1];
gi_count--;//数组元素个数减一
printf("数组共有元素:%d\n",gi_count);
}
else//接收数据
{
printf("%s\n",wsabuff.buf);
memset(gc_recvbuff,0,MAX_RECV_LENGTH);//清空buff
//根据情况投递send
//跳3.1.1继续投递Recv
PostRecv(socketIndex);
}
}
发送数据的调用是按照需求来的,因此其对应的回调函数里面暂时没有写代码
等待循环的代码实现
这里不需要再对接收和发送进行判断(都放到回调函数中处理了),这里只需要对服务器SOCKET句柄对应的garr_olpAll[0]进行判断,不需要循环garr_olpAll数组所有元素,而且这里只会发生客户端请求连接信号,上节中判断客户端退出的功能已在recv的回调函数中处理,这里不需要再处理,将上节代码删除部分,变成:
while(1)
{
//这里只用查询服务器SOCKET是否有事件,如果有信号,必定是请求连接事件
//因此garr_olpAll的位置设置为0
int nRes=WSAWaitForMultipleEvents(1,&(garr_olpAll[0].hEvent), FALSE,WSA_INFINITE, TRUE);
if(nRes==WSA_WAIT_FAILED || nRes==WSA_WAIT_IO_COMPLETION)//查询失败或者超时
{
continue;
}
//信号置空
WSAResetEvent(garr_olpAll[0].hEvent);
printf("情况1:接受连接完成\n");
//执行成功,并连接成功
//走流程3.1的两种情况
//对连接上的客户端send消息
PostSend(gi_count);
//投递recv
PostRecv(gi_count);
gi_count++;//注意这里gi_count++的位置
//再次投递AcceptEx
PostAccept();
}
这里需要注意的是,由于只需要等待服务器事件,因此WSAWaitForMultipleEvents的第四个参数可设置为:WSA_INFINITE,反正没有别的SOCKET事件需要处理,就一直等到服务器有事件信号;最后一个参数要设置为TRUE,因为完成例程必须要设置TRUE才能生效。这个函数设置为TRUE后,WSAWaitForMultipleEvents这个函数和完成例程就进入异步执行模式,完成例程的回调函数执行完成就会返回WSA_WAIT_IO_COMPLETION,也就是当前的这个函数或者说完成例程已经排队等执行。
小结:WSAWaitForMultipleEvents最后一个参数设置为TRUE后,不但能够获得事件的信号通知,还能得到完成例程执行完毕的通知。由于使用WSA_INFINITE(无限等待,直到有信号为止),原来的WSA_WAIT_TIMEOUT就不需要了。
优化
回调函数优化
这里主要针对寻找重叠IO结构体位置的代码进行优化:
int i = 0;//位置
//循环遍历重叠IO结构体数组,找到当前重叠IO结构体在数组中的位置
for(i; i < gi_count; i++)
{
//重叠IO结构体的事件句柄一样代表找到了
if(garr_olpAll[i].hEvent==lpOverlapped->hEvent)
{
printf("RecvCallBack回调找到重叠IO结构体位置是:%d!\n",i);
break;
}
}
执行完上面的代码后,i就是当前重叠IO结构体在数组中的位置,但是如果数组里面的元素非常多,每次做这个循环效率就很低。可以看到lpOverlapped实际上是当前重叠IO结构体的地址(因为都是传址引用),整个数组的地址我们也知道,那么我们可以用减法直接算出当前重叠IO结构体的位置。
小例子:
假如一个数组有10个元素,每个元素是1个字节大小,第一个元素的地址是0,那么:
数组元素 | 第1个 | 第2个 | 第3个 | 第4个 | 第5个 | 第6个 | 第7个 | 第8个 | 第9个 | 第10个 |
---|---|---|---|---|---|---|---|---|---|---|
地址 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
如果当前元素的第7个,那么可以用第7个元素的地址减去第一个元素的地址:7-0=7得到当前是第几个元素。
如果元素大小即使不是1,上面的方法仍然适用,因此,上面的代码可以换成:
int i = lpOverlapped - &garr_olpAll[0];
时间复杂度从 O ( n ) O(n) O(n)变成 O ( 1 ) O(1) O(1)
投递函数递归转循环
递归调用虽然比较好理解,但是内存特别容易炸,玩过数据结构的塔罗牌的就知道。
反正我们的目标是不断的重复执行AcceptEx,因此每次执行成功(bRes == TRUE)就continue重复循环,如果是延迟完成或出错先退出循环。
//投递AcceptEx
int PostAccept()
{
while(1)
{
//客户端句柄加到数组里面,注意gi_count++的位置
garr_sockAll[gi_count]=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
garr_olpAll[gi_count].hEvent = WSACreateEvent();//事件初始化
char str[1024] = {0};
DWORD dwRecvCount = 0;
//AcceptEx涉及的SOCKET句柄和重叠事件结构体都是针对服务器的
BOOL bRes = AcceptEx(garr_sockAll[0],garr_sockAll[gi_count],str,0,sizeof(struct sockaddr_in)+16,
sizeof(struct sockaddr_in)+16,&dwRecvCount,&garr_olpAll[0]);
printf("PostAccept\n");
if (bRes == TRUE)
{
printf("PostAccept Success\n");
//PostSend(gi_count);
//执行成功,并连接成功
//走流程3.1的两种情况
//投递recv
PostRecv(gi_count);
gi_count++;//注意这里gi_count++的位置
//再次投递AcceptEx
//PostAccept();递归变循环
//return 0;
continue;
}
else
{
int acceptexerr = WSAGetLastError();
if (acceptexerr == ERROR_IO_PENDING)
{
//延迟处理
//return 0;
break;
}
else
{
//出错处理
printf("PostAccept出错,错误码是:%d\n",acceptexerr);
//return acceptexerr;
break;
}
}
}
return 0;
}
错误处理
错误 C2440 “=”: 无法从“const char [XXX]”转换为“char *”
原因是新版编译器中,不能直接把 const char* 赋值给 char*
简单粗暴解决:
找到项目属性,将下面选项设置为否即可。
完整代码(有bug)
Bug描述:开多个客户端,然后每个客户端发送多个消息服务器无回复
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <Winsock2.h>
#include <mswsock.h>
#include <string.h>
#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "Mswsock.lib")
#define MAX_COUNT 1024
#define MAX_RECV_COUNT 1024
SOCKET g_allSock[MAX_COUNT];
OVERLAPPED g_allOlp[MAX_COUNT];
int g_count;
WSABUF wsabuf;
//接收缓冲区
//char g_strRecv[MAX_RECV_COUNT];
int PostAccept();
int PostRecv(int index);
int PostSend(int index);
void Clear()
{
for (int i = 0; i < g_count; i++)
{
closesocket(g_allSock[i]);
WSACloseEvent(g_allOlp[i].hEvent);
}
}
BOOL WINAPI fun(DWORD dwCtrlType)
{
switch (dwCtrlType)
{
case CTRL_CLOSE_EVENT:
//释放所有socket
Clear();
break;
}
return TRUE;
}
int main(void)
{
wsabuf.len = MAX_RECV_COUNT;
wsabuf.buf = (char*)malloc(MAX_RECV_COUNT * sizeof(char));
memset(wsabuf.buf, 0, MAX_RECV_COUNT);
SYSTEM_INFO systemProcessorsCount;
GetSystemInfo(&systemProcessorsCount);
int nProcessorsCount = systemProcessorsCount.dwNumberOfProcessors;
SetConsoleCtrlHandler(fun, TRUE);
WORD wdVersion = MAKEWORD(2, 2);
WSADATA wdScokMsg;
int nRes = WSAStartup(wdVersion, &wdScokMsg);
if (0 != nRes)
{
switch (nRes)
{
case WSASYSNOTREADY:
printf("重启下电脑试试,或者检查网络库");
break;
case WSAVERNOTSUPPORTED:
printf("请更新网络库");
break;
case WSAEINPROGRESS:
printf("请重新启动");
break;
case WSAEPROCLIM:
printf("请尝试关掉不必要的软件,以为当前网络运行提供充足资源");
break;
}
return 0;
}
//校验版本
if (2 != HIBYTE(wdScokMsg.wVersion) || 2 != LOBYTE(wdScokMsg.wVersion))
{
//说明版本不对
//清理网络库
WSACleanup();
return 0;
}
SOCKET socketServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
//int a = WSAGetLastError();
if (INVALID_SOCKET == socketServer)
{
int a = WSAGetLastError();
//清理网络库
WSACleanup();
return 0;
}
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(9527);
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
//int a = ~0;
if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr *)&si, sizeof(si)))
{
//出错了
int a = WSAGetLastError();
//释放
closesocket(socketServer);
//清理网络库
WSACleanup();
return 0;
}
if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
{
//出错了
int a = WSAGetLastError();
//释放
closesocket(socketServer);
//清理网络库
WSACleanup();
return 0;
}
g_allSock[g_count] = socketServer;
g_allOlp[g_count].hEvent = WSACreateEvent();
g_count++;
if (0 != PostAccept())
{
Clear();
//清理网络库
WSACleanup();
return 0;
}
while (1)
{
int nRes = WSAWaitForMultipleEvents(1, &(g_allOlp[0].hEvent), FALSE, WSA_INFINITE, TRUE);
if (WSA_WAIT_FAILED == nRes || WSA_WAIT_IO_COMPLETION == nRes)
{
continue;
}
//信号置空
WSAResetEvent(g_allOlp[0].hEvent);
//PostSend(g_count);
printf("accept\n");
//接收链接完成了
//投递recv
PostRecv(g_count);
//根据情况投递send
//PostSend(g_count);
//客户端适量++
g_count++;
//投递accept
PostAccept();
}
Clear();
//清理网络库
WSACleanup();
system("pause");
return 0;
}
int PostAccept()
{
while (1)
{
g_allSock[g_count] = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
g_allOlp[g_count].hEvent = WSACreateEvent();
char str[1024] = { 0 };
DWORD dwRecvcount;
BOOL bRes = AcceptEx(g_allSock[0], g_allSock[g_count], str, 0, sizeof(struct sockaddr_in) + 16,
sizeof(struct sockaddr_in) + 16, &dwRecvcount, &g_allOlp[0]);
if (TRUE == bRes)
{
//立即完成了
//投递recv
PostRecv(g_count);
//根据情况投递send
//客户端适量++
g_count++;
//投递accept
//PostAccept();
continue;
}
else
{
int a = WSAGetLastError();
if (ERROR_IO_PENDING == a)
{
//延迟处理
break;
}
else
{
break;
}
}
}
return 0;
}
void CALLBACK RecvCall(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags)
{
//int i = 0;
//for (i; i < g_count; i++)
//{
// if (lpOverlapped->hEvent == g_allOlp[i].hEvent)
// {
// break;
// }
//}
int i = lpOverlapped - &g_allOlp[0];
if (10054 == dwError || 0 == cbTransferred)
{
//删除客户端
printf("close\n");
//客户端下线
//关闭
closesocket(g_allSock[i]);
WSACloseEvent(g_allOlp[i].hEvent);
//从数组中删掉
g_allSock[i] = g_allSock[g_count - 1];
g_allOlp[i] = g_allOlp[g_count - 1];
//个数减-1
g_count--;
}
else
{
printf("RecvCall%s\n", wsabuf.buf);
memset(wsabuf.buf, 0, MAX_RECV_COUNT);
//根据情况投递send
//对自己投递接收
PostRecv(i);
}
}
int PostRecv(int index)
{
DWORD dwRecvCount;
DWORD dwFlag = 0;
int nRes = WSARecv(g_allSock[index], &wsabuf, 1, &dwRecvCount, &dwFlag, &g_allOlp[index], RecvCall);
if (0 == nRes)
{
//立即完成的
//打印信息
printf("立即完成的%s\n", wsabuf.buf);
memset(wsabuf.buf, 0, MAX_RECV_COUNT);
PostSend(index);
//根据情况投递send
//对自己投递接收
PostRecv(index);
return 0;
}
else
{
int a = WSAGetLastError();
if (ERROR_IO_PENDING == a)
{
//延迟处理
return 0;
}
else
{
return a;
}
}
}
void CALLBACK SendCall(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags)
{
printf("send over\n");
memset(wsabuf.buf, 0, MAX_RECV_COUNT);
}
int PostSend(int index)
{
WSABUF wsasendbuf;
wsasendbuf.buf = "你好";
wsasendbuf.len = MAX_RECV_COUNT;
DWORD dwSendCount;
DWORD dwFlag = 0;
int nRes = WSASend(g_allSock[index], &wsasendbuf, 1, &dwSendCount, dwFlag, &g_allOlp[index], SendCall);
if (0 == nRes)
{
//立即完成的
//打印信息
printf("send成功\n");
return 0;
}
else
{
int a = WSAGetLastError();
if (ERROR_IO_PENDING == a)
{
//延迟处理
return 0;
}
else
{
return a;
}
}
}