windows socket网络编程五:重叠IO模型

分析

在解决了select模型本身的同步阻塞问题后,我们还要处理send、recv、accept的执行阻塞。
我们socket的操作本质上都是字符串的拷贝复制,重叠IO是windows提供的一种异步读写文件的机制,将读的指令以及我们的buffer投给操作系统,然后函数直接返回,操作系统独立开个线程,将数据复制进咱们的buffer,数据复制期间,我们就可以去做其他事,即读写过程变成了异步,可以同时投递多个读写操作。

事件通知

重叠IO结构体

typedef struct _WSAOVERLAPPED {
  DWORD    Internal;
  DWORD    InternalHigh;
  DWORD    Offset;
  DWORD    OffsetHigh;
  WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;

前四个成员系统使用,我们不需要直接使用。
操作完成hEvent就会被置成有信号。

创建支持重叠IO的socket

SOCKET WSAAPI WSASocketW(
  int                 af,
  int                 type,
  int                 protocol,
  LPWSAPROTOCOL_INFOW lpProtocolInfo,
  GROUP               g,
  DWORD               dwFlags
);

功能:创建一个SOCKET
参数:

  • af — 地址族(和socket一样)
  • type — 套接字类型(和socket一样)
  • protocol — 协议类型(和socket一样)
  • lpProtocolInfo — 要创建的套接字的特征(这次用不到 用到查文档
  • g — 创建新的套接字和新的套接字组时,现有的套接字组ID或要执行的适当操作。
    含义
    0不执行组操作
    SG_UNCONSTRAINED_GROUP创建一个不受限制的套接字组,并使新的套接字成为第一个成员。
    SG_CONSTRAINED_GROUP创建一个受约束的套接字组,并使新的套接字成为第一个成员。
  • dwFlags — 套接字属性
    含义
    WSA_FLAG_OVERLAPPED创建一个支持重叠I / O操作的套接字。
    WSA_FLAG_MULTIPOINT_C_ROOT创建一个在多点会话中将为c_root的套接字。
    WSA_FLAG_MULTIPOINT_C_LEAF创建一个在多点会话中将为c_leaf的套接字。
    WSA_FLAG_MULTIPOINT_D_ROOT创建一个在多点会话中为d_root的套接字。
    WSA_FLAG_MULTIPOINT_D_LEAF创建一个在多点会话中为d_leaf的套接字。
    WSA_FLAG_ACCESS_SYSTEM_SECURITY创建一个套接字,使它能够在包含安全访问控制列表(SACL)而不是仅随意访问控制列表(DACL)的套接字上设置安全描述符。
    WSA_FLAG_NO_HANDLE_INHERIT创建一个不可继承的套接字。

返回值:

  • 如果成功,返回socket。
  • 如果失败,返回INVALID_SOCKET。

代码:

// 创建socket
SOCKET socketServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
if (INVALID_SOCKET == socketServer)
{
	printf("创建WSASocket失败 error:%d\n", WSAGetLastError());
	WSACleanup();
	return -1;
}

投递异步Accept

BOOL AcceptEx(
  SOCKET       sListenSocket,
  SOCKET       sAcceptSocket,
  PVOID        lpOutputBuffer,
  DWORD        dwReceiveDataLength,
  DWORD        dwLocalAddressLength,
  DWORD        dwRemoteAddressLength,
  LPDWORD      lpdwBytesReceived,
  LPOVERLAPPED lpOverlapped
);

功能:接受一个新的连接,返回本地和远程地址,并且接收由所述客户端应用程序发送的数据的第一个块。
参数:

  • sListenSocket — 服务器socket
  • sAcceptSocket — 链接服务器的客户端的socket(要调用WSASocket创建)
  • lpOutputBuffer — 缓冲区的指针,接收在新连接上发送的第一个数据(客户端第一次send,由这个函数接收。第二次以后就由WSARecv接收)
  • dwReceiveDataLength — 用于缓冲区开头的实际接收数据的字节数。设置0取消了3的功能。
  • dwLocalAddressLength — 为本地地址信息保留的字节数。此值必须至少比使用的传输协议的最大地址长度长16个字节。
  • dwRemoteAddressLength — 为远程地址信息保留的字节数。该值必须比使用的传输协议的最大地址长度至少多16个字节。不能为零。
  • lpdwBytesReceived — 指向接收到的字节数的DWORD的指针
  • lpOverlapped — 重叠结构

返回值:

  • 如果成功,返回TRUE。
  • 如果失败,返回FALSE。

WSAGetLastError返回错误:

  • 如果是ERROR_IO_PENDING,则操作已成功启动并且仍在进行中。
  • 如果是WSAECONNRESET,则表明存在传入连接,但随后在接受呼叫之前被远程对等方终止。

代码:

int PostAccept()
{
	g_sock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);

	char str[1024] = { 0 };
	DWORD dwRecvcount;
	if (TRUE == AcceptEx(g_allSock[0], g_sock, str, 0, sizeof(struct sockaddr_in) + 16,
		sizeof(struct sockaddr_in) + 16, &dwRecvcount, &g_allOlp[0]))	// 立即完成
	{
		printf("accept\n");
		g_allSock[g_count] = g_sock;
		g_allOlp[g_count].hEvent = WSACreateEvent();
		PostRecv(g_count);	// 投递recv
		++g_count;			// 客户端数量++
		PostAccept();		// 投递accept
		return 0;
	}
	else
	{
		int result = WSAGetLastError();
		if (ERROR_IO_PENDING == result)	// 延迟处理
		{
			return 0;
		}
		else
		{
			return result;
		}
	}
}

投递异步Recv

int WSAAPI WSARecv(
  SOCKET                             s,
  LPWSABUF                           lpBuffers,
  DWORD                              dwBufferCount,
  LPDWORD                            lpNumberOfBytesRecvd,
  LPDWORD                            lpFlags,
  LPWSAOVERLAPPED                    lpOverlapped,
  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

功能:投递异步接收信息
参数:

  • s — 客户端socket
  • lpBuffers — 接收后的信息存储buffer
    typedef struct _WSABUF {
        ULONG len;     /* the length of the buffer */
        _Field_size_bytes_(len) CHAR FAR *buf; /* the pointer to the buffer */
    } WSABUF, FAR * LPWSABUF;
    
  • dwBufferCount — lpBuffers数组中WSABUF结构的数量
  • lpNumberOfBytesRecvd — 接收成功的话,这里装着成功接收到的字节数,参数6重叠结构不为NULL的时候,此参数可以设置成NULL。
  • lpFlags — 指向用于修改WSARecv函数调用行为的标志的指针
    含义
    MSG_PEEK窥视传入的数据。数据被复制到缓冲区中,但不会从输入队列中删除。
    MSG_OOB处理OOB数据
    MSG_PARTIAL此次接收到的数据是客户端发来的一部分,接下来接收下一部分
    MSG_PUSH_IMMEDIATE通知传送尽快完成
    MSG_WAITALL呼叫者提供的缓冲区已满或连接已关闭或请求已取消或发生错误才把数据发送出去
  • lpOverlapped — 重叠结构
  • lpCompletionRoutine — 回调函数

返回值:

  • 如果成功,返回0。
  • 如果失败,返回SOCKET_ERROR。

WSAGetLastError返回错误:

  • 如果是ERROR_IO_PENDING,则操作已成功启动并且仍在进行中。
  • 如果是WSAECONNRESET,则表明存在传入连接,但随后在接受呼叫之前被远程对等方终止。

代码:

int PostRecv(int index)
{
	WSABUF wsabuf;
	wsabuf.buf = g_strRecv[index];
	wsabuf.len = MAX_RECV_COUNT;

	DWORD dwRecvCount; 
	DWORD dwFlag = 0;
	if (0 == WSARecv(g_allSock[index], &wsabuf, 1, &dwRecvCount, &dwFlag, &g_allOlp[index], NULL))	// 立即完成
	{
		// 接收到客户端消息 
		printf("Client Data : %s \n", wsabuf.buf);
		memset(g_strRecv[index], 0, MAX_RECV_COUNT);
		PostSend(index);	// 给客户回信
		PostRecv(index);	// 对自己投递接收
		return 0;
	}
	else
	{
		int result = WSAGetLastError();
		if (ERROR_IO_PENDING == result)	// 延迟处理
		{
			return 0;
		}
		else
		{
			return result;
		}
	}
}

投递异步Send

int WSAAPI WSASend(
  SOCKET                             s,
  LPWSABUF                           lpBuffers,
  DWORD                              dwBufferCount,
  LPDWORD                            lpNumberOfBytesSent,
  DWORD                              dwFlags,
  LPWSAOVERLAPPED                    lpOverlapped,
  LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

功能:投递异步发送信息
参数:

  • s — 客户端socket
  • lpBuffers — 发送的信息存储buffer
  • dwBufferCount — lpBuffers数组中WSABUF结构的数量
  • lpNumberOfBytesSent — 接收成功的话,这里装着成发送的字节数,参数6重叠结构不为NULL的时候,此参数可以设置成NULL。
  • dwFlags — 用于修改WSASend函数调用行为的标志 。
    含义
    MSG_DONTROUTE指定不应对数据进行路由。Windows套接字服务提供者可以选择忽略此标志。
    MSG_OOB处理OOB数据
    MSG_PARTIAL此次接收到的数据是客户端发来的一部分,接下来接收下一部分
  • lpOverlapped — 重叠结构
  • lpCompletionRoutine — 回调函数

返回值:

  • 如果成功,返回0。
  • 如果失败,返回SOCKET_ERROR。

WSAGetLastError返回错误:

  • 如果是ERROR_IO_PENDING,则操作已成功启动并且仍在进行中。
  • 如果是WSAECONNRESET,则表明存在传入连接,但随后在接受呼叫之前被远程对等方终止。

代码:

int PostSend(int index)
{
	WSABUF wsabuf;
	wsabuf.buf = "ok";
	wsabuf.len = MAX_RECV_COUNT;

	DWORD dwSendCount;
	DWORD dwFlag = 0;
	if (0 == WSASend(g_allSock[index], &wsabuf, 1, &dwSendCount, dwFlag, &g_allOlp[index], NULL))	// 立即完成
	{
		return 0;
	}
	else
	{
		int result = WSAGetLastError();
		if (ERROR_IO_PENDING == result)	// 延迟处理
		{
			return 0;
		}
		else
		{
			return result;
		}
	}
}

询问事件

采用一个一个询问

for (int i = 0; i < g_count; ++i)
{
	// 询问事件
	int result = WSAWaitForMultipleEvents(1, &(g_allOlp[i].hEvent), FALSE, 0, FALSE);
	if (WSA_WAIT_FAILED == result || WSA_WAIT_TIMEOUT == result)
	{
		continue;
	}
	。。。
}

获取重叠信号

BOOL WSAAPI WSAGetOverlappedResult(
  SOCKET          s,
  LPWSAOVERLAPPED lpOverlapped,
  LPDWORD         lpcbTransfer,
  BOOL            fWait,
  LPDWORD         lpdwFlags
);

功能:获取对应socket上的具体重叠io情况
参数:

  • s — socket
  • lpOverlapped — 重叠结构
  • lpcbTransfer — 由发送或者接收到的实际字节数(0表示客户端下线)
  • fWait — 指定函数是否应等待挂起的重叠操作完成。如果为TRUE,则在操作完成之前函数不会返回。如果为FALSE并且该操作仍未完成,则该函数返回FALSE,而 WSAGetLastError函数返回WSA_IO_INCOMPLETE。仅当重叠操作选择了基于事件的完成通知时,才能将fWait参数设置为TRUE。
  • lpdwFlags — 接收一个或多个补充完成状态的标志(装着WSARecv的参数5 lpflags)

返回值:

  • 如果成功,返回TRUE。
  • 如果失败,返回FALSE。(WSAGetLastError() == 10054,客户端强制退出)

代码:

// 有信号
DWORD dwState;
DWORD dwFlag;
BOOL bFlag = WSAGetOverlappedResult(g_allSock[i], &g_allOlp[i], &dwState, TRUE, &dwFlag);
WSAResetEvent(g_allOlp[i].hEvent);	// 信号置空

分类处理

客户端异常下线
代码:

if (FALSE == bFlag)
{
	int result = WSAGetLastError();
	if (10054 == result)
	{
		printf("客户端异常下线\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
		--i;
		// 个数减-1
		--g_count;
	}
	continue;
}

接收链接成功
代码:

if (0 == i)	// accept
{
	printf("accept\n");
	g_allSock[g_count] = g_sock;
	g_allOlp[g_count].hEvent = WSACreateEvent();
	PostRecv(g_count);	// 投递recv
	++g_count;			// 客户端数量++
	PostAccept();		// 投递accept
	continue;
}

接收数据为0客户端下线
代码:

if (0 == dwState)
{
	printf("客户端正常下线\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
	--i;
	// 个数减-1
	--g_count;
	continue;
}

send或recv成功
代码:

if (0 != dwState)	// 发送 或者 接收成功了
{
	if (g_strRecv[i][0] != 0)	// recv
	{
		// 接收到客户端消息 
		printf("Client Data : %s \n", g_strRecv[i]);
		memset(g_strRecv[i], 0, MAX_RECV_COUNT);
		
		PostSend(i);	// 给客户回信
		PostRecv(i);	// 对自己投递接收
	}
}

运行结果

在这里插入图片描述

完成例程

投递异步Accept

代码:

int PostAccept()
{
	while (1)
	{
		g_sock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);

		char str[1024] = { 0 };
		DWORD dwRecvcount;
		if (TRUE == AcceptEx(g_allSock[0], g_sock, str, 0, sizeof(struct sockaddr_in) + 16,
			sizeof(struct sockaddr_in) + 16, &dwRecvcount, &g_allOlp[0]))	// 立即完成
		{
			printf("accept\n");
			g_allSock[g_count] = g_sock;
			g_allOlp[g_count].hEvent = WSACreateEvent();
			PostRecv(g_count);	// 投递recv
			++g_count;			// 客户端数量++
			continue;
		}
		else
		{
			break;
		}
	}
	return 0;
}

投递异步Recv

和上面使用不同的是用回调函数来处理相关逻辑。
回调函数参数类型:

LPWSAOVERLAPPED_COMPLETION_ROUTINE LpwsaoverlappedCompletionRoutine;

void LpwsaoverlappedCompletionRoutine(
  DWORD dwError,				// 错误码
  DWORD cbTransferred,			// 发送或者接收到的字节数
  LPWSAOVERLAPPED lpOverlapped,	// 重叠结构
  DWORD dwFlags					// 函数执行的方式(WSARecv参数5)
)
{...}

可以看到和上面WSAGetOverlappedResult是对应的,所以处理逻辑也对应。

代码:

void CALLBACK RecvCall(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags)
{
	if (10054 == dwError)
	{
		printf("客户端异常下线\n");
	}
	else if (0 == cbTransferred)
	{
		printf("客户端正常下线\n");
	}

	int i = lpOverlapped - &g_allOlp[0];

	if (10054 == dwError || 0 == cbTransferred)
	{
		// 关闭
		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("Client Data : %s \n", g_strRecv[i]);
		memset(g_strRecv[i], 0, MAX_RECV_COUNT);
		PostSend(i);	// 给客户回信
		PostRecv(i);	// 对自己投递接收
	}
}

int PostRecv(int index)
{
	WSABUF wsabuf;
	wsabuf.buf = g_strRecv[index];
	wsabuf.len = MAX_RECV_COUNT;

	DWORD dwRecvCount;
	DWORD dwFlag = 0;
	if (0 == WSARecv(g_allSock[index], &wsabuf, 1, &dwRecvCount, &dwFlag, &g_allOlp[index], RecvCall))	// 立即完成
	{
		// 接收到客户端消息 
		printf("Client Data : %s \n", wsabuf.buf);
		memset(g_strRecv, 0, MAX_RECV_COUNT);
		PostSend(index);	// 给客户回信
		PostRecv(index);	// 对自己投递接收
		return 0;
	}
	else
	{
		int result = WSAGetLastError();
		if (ERROR_IO_PENDING == result)	// 延迟处理
		{
			return 0;
		}
		else
		{
			return result;
		}
	}
}

投递异步Send

和上面使用不同的是用回调函数来处理相关逻辑。

代码:

void CALLBACK SendCall(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags)
{
	
}

int PostSend(int index)
{
	WSABUF wsabuf;
	wsabuf.buf = "ok";
	wsabuf.len = MAX_RECV_COUNT;

	DWORD dwSendCount;
	DWORD dwFlag = 0;
	if (0 == WSASend(g_allSock[index], &wsabuf, 1, &dwSendCount, dwFlag, &g_allOlp[index], SendCall))	// 立即完成
	{
		return 0;
	}
	else
	{
		int result = WSAGetLastError();
		if (ERROR_IO_PENDING == result)	// 延迟处理
		{
			return 0;
		}
		else
		{
			return result;
		}
	}
}

分类处理

因为recv和send相关消息都在回调函数中处理完成了,所以分类处理其实只有accept相关内容。

代码:

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);	// 信号置空

	printf("accept\n");
	g_allSock[g_count] = g_sock;
	g_allOlp[g_count].hEvent = WSACreateEvent();
	PostRecv(g_count);	// 投递recv
	++g_count;			// 客户端数量++
	PostAccept();		// 投递accept
}

运行结果

在这里插入图片描述

模型流程图

在这里插入图片描述
可以看到我们的send、recv、accept的执行阻塞问题也解决了。

源码链接

百度云链接:https://pan.baidu.com/s/1xBOiSADlAG2gO1TC6BBO_A
提取码:sxbd

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值