WSAAsyncSelect模型接收数组包的问题

qq_40487922 2020-09-15 12:07:18
下面是WSAAsyncSelect的处理代码,但是总感觉别扭。
1.如果有无聊的人连接上来不发送数据也不关闭连接就永远不会有FD_CLOSE消息,导致存储lp_client_read_info的信息永远会保存在list里面。
2.用list保存组包缓冲区,这种方式合不合适呢,一般WSAAsyncSelect模式怎么保存SOCKET组包缓冲区的?

void DSocketServer::OnMessage(SOCKET sock, LPARAM lParam, HWND hWnd)
{
if (WSAGETSELECTERROR(lParam)) {
closesocket(sock);
return;
}
switch (WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT:
{
sockaddr_in clientAddr;
int addr_size = sizeof(sockaddr);
SOCKET clientSock = accept(m_socket_server, (sockaddr*)&clientAddr, &addr_size);
if (INVALID_SOCKET == clientSock)
{
globalUserLog.Error(TEXT("客户端套按字无效!错误码:%ld"), WSAGetLastError());
return;
}
char *clientIp = inet_ntoa(clientAddr.sin_addr);

// 读取数据的结构体,第一次进来先需要读取头信息,描述数据包的大小之类的信息
lp_client_read_info lpReadInfo = new client_read_info{0};
// 保存客户端的IP
_tcscpy_s(lpReadInfo->client_ip, 88, clientIp);
lpReadInfo->client_sock = clientSock;
// 应该接收的数据长度,h_info头信息的结构
lpReadInfo->data_len = sizeof(h_info);
// 接收数据的缓冲
lpReadInfo->data = new TCHAR[lpReadInfo->data_len]{0};
// 将结构体保存到一个list,它是一个类属性
push_client_info(lpReadInfo);
// 绑定READ和CLOSE消息
WSAAsyncSelect(clientSock, hWnd, WM_ASYNC_SOCKET, FD_READ | FD_CLOSE);
break;
}
case FD_READ:
{
WSAAsyncSelect(sock, hWnd, 0, 0);
// 根据SOCKET取出对应的lp_client_read_info结构
lp_client_read_info lpReadInfo = get_client_info(sock);
if (sock != lpReadInfo->client_sock)
{
return;
}

// 计算剩下需要读取的数据
UINT readBufLen = lpReadInfo->data_len - lpReadInfo->readed_len;
// 最多一次读取MAX_READ_BUF个字节
if (readBufLen > MAX_READ_BUF)
{
readBufLen = MAX_READ_BUF;
}

TCHAR tmpBuf[MAX_READ_BUF] = {0};
int recvLen = recv(sock, tmpBuf, readBufLen, 0);
if (SOCKET_ERROR == recvLen)
{
int wsaErrorNo = WSAGetLastError();
if (wsaErrorNo != WSAEWOULDBLOCK && wsaErrorNo != WSAEINTR)
{
switch (wsaErrorNo)
{
case WSAETIMEDOUT:
{
globalUserLog.Error(TEXT("读取数据超时,IP:%s"), lpReadInfo->client_ip);
}
break;
case WSAECONNRESET:
{
globalUserLog.Error(TEXT("远程主机强制断开连接,IP:%s"), lpReadInfo->client_ip);
}
break;
default:
{
globalUserLog.Error(TEXT("读取数据时发生未知网络错误,错误码:%d,IP:%s"), wsaErrorNo, lpReadInfo->client_ip);
}
break;
}
WSAAsyncSelect(sock, hWnd, WM_ASYNC_SOCKET, FD_CLOSE);
}
else
{
WSAAsyncSelect(sock, hWnd, WM_ASYNC_SOCKET, FD_READ);
}
}
else if (recvLen > 0 && recvLen + lpReadInfo->readed_len > lpReadInfo->data_len)
{
globalUserLog.Error(TEXT("数据可能被篡改,IP:%s"), lpReadInfo->client_ip);
WSAAsyncSelect(sock, hWnd, WM_ASYNC_SOCKET, FD_CLOSE);
}
else
{
// 读取到数据后保存到缓冲区
if (recvLen > 0)
{
memcpy_s(lpReadInfo->data + lpReadInfo->readed_len, lpReadInfo->data_len - lpReadInfo->readed_len, tmpBuf, recvLen);
lpReadInfo->readed_len += recvLen;
}
// 如果数据读取完成开始处理数据
if (lpReadInfo->data_len == lpReadInfo->readed_len)
{

// 判断组包后的数据是不是头数据包,如果是头数据包,再继续读实际要用的数据
if (!lpReadInfo->readed_header)
{
h_info header_info = {0};
memcpy_s(&header_info, sizeof(h_info), lpReadInfo->data, sizeof(h_info));
delete[] lpReadInfo->data;
lpReadInfo->data = nullptr;

// 标识为头数据包已经读完了
lpReadInfo->readed_header = TRUE;
lpReadInfo->data_len = ntohl(header_info.data_len);
lpReadInfo->origin_data_len = ntohl(header_info.origin_data_len);
lpReadInfo->readed_len = 0;
try
{
lpReadInfo->data = new TCHAR[lpReadInfo->data_len]{ 0 };
if (NULL == lpReadInfo->data)
{
WSAAsyncSelect(sock, hWnd, WM_ASYNC_SOCKET, FD_CLOSE);
globalUserLog.Error(TEXT("内存分配失败,分配大小:%ld"), lpReadInfo->data_len);
}
else
{
WSAAsyncSelect(sock, hWnd, WM_ASYNC_SOCKET, FD_READ);
}
}
catch (const std::bad_alloc &memEx)
{
WSAAsyncSelect(sock, hWnd, WM_ASYNC_SOCKET, FD_CLOSE);
globalUserLog.Error(TEXT("内存分配发生异常,分配大小:%ld"), lpReadInfo->data_len);
}
}
else
{
// 最后处理数据
EnterCriticalSection(&m_cs);
UINT ret = m_dataHandler.Handler(lpReadInfo->data,lpReadInfo->data_len, lpReadInfo->origin_data_len);
LeaveCriticalSection(&m_cs);
ret = htonl(ret);
int sendRet = send(sock, (const char*)&ret, sizeof(UINT), 0);
if (SOCKET_ERROR == sendRet)
{
globalUserLog.Error(TEXT("返回结果时发生未知网络错误,错误码:%d,IP:%s"), WSAGetLastError(), lpReadInfo->client_ip);
}
WSAAsyncSelect(sock, hWnd, WM_ASYNC_SOCKET, FD_CLOSE);
}
}
else
{
WSAAsyncSelect(sock, hWnd, WM_ASYNC_SOCKET, FD_READ);
}
}
}
break;
case FD_CLOSE:
{
WSAAsyncSelect(sock, hWnd, 0, 0);
// 移除sock对应的数据结构体
remove_client_info(sock);
closesocket(sock);
}
break;
}
}
...全文
139 3 打赏 收藏 转发到动态 举报
写回复
用AI写文章
3 条回复
切换为时间正序
请发表友善的回复…
发表回复
an_bachelor 2020-09-16
  • 打赏
  • 举报
回复
引用 2 楼 qq_40487922 的回复:
[quote=引用 1 楼 陈仲甫 的回复:]可以在你的业务协议中规定一个失活时间,多久没活动自动RST TCP协议层面也可以通过不打开keepalive选项等它超时自己断开 但逻辑上应该做在业务协议里面 因为这是你业务规则的一部分
感谢。。因为我是第一次写异步网络。 我现在用的方式用list保存组包信息,READ的时候再根据SOCKET从list中取出组包信息继续读取。正常是用这种方式来作的吗?[/quote]其实这个API我没怎么直接用过 因为CAsyncSocket为它提供了更简单的接口 而需要高并发的场景它又不够用 组包的问题 没有收完整的临时数据可以暂存任何地方 只需考虑到方便高效地根据包ID读写这个包的数据(和相关属性 比如接收时间) 跟存储使用的容器/数据结构没有特别的联系
qq_40487922 2020-09-15
  • 打赏
  • 举报
回复
引用 1 楼 陈仲甫 的回复:
可以在你的业务协议中规定一个失活时间,多久没活动自动RST TCP协议层面也可以通过不打开keepalive选项等它超时自己断开 但逻辑上应该做在业务协议里面 因为这是你业务规则的一部分
感谢。。因为我是第一次写异步网络。 我现在用的方式用list保存组包信息,READ的时候再根据SOCKET从list中取出组包信息继续读取。正常是用这种方式来作的吗?
an_bachelor 2020-09-15
  • 打赏
  • 举报
回复
可以在你的业务协议中规定一个失活时间,多久没活动自动RST
TCP协议层面也可以通过不打开keepalive选项等它超时自己断开 但逻辑上应该做在业务协议里面 因为这是你业务规则的一部分
服务器概要设计说明全文共5页,当前为第1页。服务器概要设计说明全文共5页,当前为第1页。 服务器概要设计说明全文共5页,当前为第1页。 服务器概要设计说明全文共5页,当前为第1页。 目录 功能概述 2 网络通信层 3 连接生命周期的管理 3 接口 3 异步IO缓冲内存池 3 本地数据与字节流数据的互相转换 4 信令和通信数据结构 5 伪代码定义 5 命令管理 7 数据有效性检测 8 文件传输通道 9 日志 10 功能概述 服务器主要业务功能是连接物管和终端,为社区物管和管理中心提供管理功能,使其能够统一管理终端。 服务器的功能模块括: 数据管理,数据括房屋数据、住户数据、配租数据、门禁卡数据、终端配置数据等; 状态管理,服务器需要维持物管和终端的连接,保持连接状态的可增删改查; 命令管理,物管和终端之间的交互命令有确认机制,命令通过服务器传递,服务器需要保证命令传递的可靠性; 数据有效性检测,服务器需要定期检测一些数据的有效性,括配租数据是否〔临近〕到期、门禁卡白名单数据与终端定期交换等; 文件传输通道,括软件版本升级、数据文件传输等; 日志。 网络通信层 通信层负责业务命令和数据的发送接收。由于物管、终端和服务器之间命令和数据需要精确送达,所有业务都采用TCP来实现。 IOCP模型是Windows服务器开发中性能最好的非阻塞异步IO模型,所以通信层采用IOCP模型构建。Windows下有五种非阻塞I/O模型:选择〔Select〕、异步选择〔WSAAsyncSelect〕、事件选择〔WSAEventSelect〕、重叠I/O〔Overlapped I/O〕和完成端服务器概要设计说明全文共5页,当前为第2页。服务器概要设计说明全文共5页,当前为第2页。口〔Completion Port>。Select是同步IO模型,同时处理的任务有限〔上限1024〕,不符合处理成千上万连接的要求;WSAAsyncSelect也是同步IO模型,以接收Windows消息为基础,不符合服务器控制台程序要求;WSAEventSelect也是同步IO模型,需要创建与连接数等同的事件内核对象,资源未能高效利用,也排除在外;上面三种IO模型其实是一回事,都是类select模型,适合开发小型服务器或者客户端程序,而不适合需要接受成千上万连接的服务器程序。Overlapped I/O是异步IO模型,但是它需要程序员关心线程池的实现和调度〔类似Linux下面的epoll模型,但是epoll是同步IO模型〕;而IOCP克服了上面四种模型的缺点,对实现XX接数的服务器有可靠的性能和较少的资源占用,而且伸缩性比较强,占用资源数跟连接数量相关,甚至可以用在客户端程序上面。 服务器概要设计说明全文共5页,当前为第2页。 服务器概要设计说明全文共5页,当前为第2页。 连接生命周期的管理 C++语言没有对象回收〔GC〕机制,生命周期的管理和防止内存泄露需要程序自己实现,而一条连接从产生后到销毁的过程中会有多个线程同时对其进行操作,同时读写甚至同时关闭,对象的多线程同步也需要程序实现。这里采用智能指针〔shared_ptr, stl_c++11〕来管理连接的生命周期,通信层维护各个连接在内存中唯一一份数据,同时提供引用计数,统计当前该数据被外界使用情况,当外界没有角色再需要该数据时〔引用计数减到0〕,通信层会删除这份数据,同时表明该连接生命周期终止。 接口 数据接口采用handle/body手法,连接的handle采用整形数据,body采用C++对象封装连接数据,数据含SOCKET句柄、连接状态和当前接收缓存〔业务层〕等。连接生命周期反映到handle上表现为该handle是否为有效。发送内存采用智能指针〔unique_ptr, stl_c++11〕进行传递,这里用到了智能指针对数据和数据析构的封装,发送完成之后直接调用其删除器〔deleter〕进行内存的删除,这样上下层之间就避免了一次内存拷贝。 回调接口为C++接口〔纯虚函数〕。 异步IO缓冲内存池 由于系统层和stl层容器都实现了小内存内存池,所以程序将不再实现自己的内存池,发送缓冲内存完全动态分配,接收缓冲内存每个连接有一份,也通过动态分配而来。 本地数据与字节流数据的互相转换 本地数据转 为字节流数据时,根据本地数据大小构造字节流对象,然后将本地数据逐字节填入流中,可变数组先填入数组大小再逐个填充数组内容。 字节流数据转换为本地数据时,根据字节流中标识的大小动态构造本地数据,构造时使用智能指针〔unique_ptr, stl_c++11〕管理数据,加上C++多态特性,可以大大简化内存的管理。 服务器概要设计说明全文共5页,当前为第3页。服务器概要设计说明全文共5页,当前为第3页。信令和通信数据结构 服务器概要

18,356

社区成员

发帖
与我相关
我的任务
社区描述
VC/MFC 网络编程
c++c语言开发语言 技术论坛(原bbs)
社区管理员
  • 网络编程
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧