Winsock accept返回套接字调用recv触发10038错误问题
10038错误产生原因
错误码10038(WSAENOTSOCK)的核心触发逻辑是自定义Sock类违反C++资源管理的三/五原则,未正确处理对象拷贝/赋值时的套接字句柄所有权,导致合法客户端套接字被提前关闭:
Accept方法按值返回Sock对象时,编译器生成的默认浅拷贝逻辑会让临时返回对象、外层赋值目标client对象的m_NativeID成员同时指向同一个合法客户端套接字句柄- 临时对象生命周期结束时会调用析构函数,执行
closesocket(m_NativeID)直接释放该客户端套接字,还会错误递减套接字计数 - 后续调用
client.Recv()时,client对象存储的套接字句柄已经被关闭失效,执行套接字操作自然触发10038错误;客户端PuTTY因为服务端主动断开连接,会直接关闭窗口。
代码里还存在4个会触发异常的隐患:
Accept方法参数为const SockAddr&类型,acceptAPI需要向该地址写入客户端地址信息,传const引用属于未定义行为;且传入的地址长度参数错误写为sizeof(sa),实际应为sizeof(sockaddr_in)Sock类的默认构造函数未初始化m_NativeID成员,初始值为随机垃圾值,对象析构时会随机关闭无效句柄Close方法调用closesocket后未将m_NativeID设为INVALID_SOCKET,后续对象析构时会重复关闭已经释放的句柄,触发未知错误SockAddr的IP参数为unsigned long的构造函数注释掉了ZeroMemory初始化逻辑,结构体预留字段为随机值,可能导致bind/connect调用异常
修复方案
按以下步骤修改代码即可解决问题:
- 修正Sock类的资源管理逻辑,避免句柄重复释放
- 禁止默认浅拷贝,实现移动语义保证同一时间一个套接字句柄仅被一个
Sock对象持有 - 所有构造函数中初始化
m_NativeID为INVALID_SOCKET - 析构、Close方法中先判断句柄有效性,关闭后立即将
m_NativeID设为INVALID_SOCKET
- 禁止默认浅拷贝,实现移动语义保证同一时间一个套接字句柄仅被一个
- 修正Accept方法签名
去掉参数的const限定,传入正确的地址长度值,保证acceptAPI可以正常写入客户端地址信息 - 补全SockAddr结构体初始化逻辑
恢复unsigned long参数构造函数中的ZeroMemory调用,清空结构体预留字段 - 清理Main.cpp中的冗余注释代码,将
void main()改为标准的int main()
关键修改代码示例
Sock.h 类定义修改
class Sock { public: Sock(); Sock(int af, int type, int protocol); Sock(const SOCKET& base); // 禁止浅拷贝 Sock(const Sock&) = delete; Sock& operator=(const Sock&) = delete; // 实现移动语义转移所有权 Sock(Sock&& other) noexcept; Sock& operator=(Sock&& other) noexcept; ~Sock(); int Bind(const SockAddr& sa); int Send(const char* buf, int len, int flags); int Recv(char* buf, int len, int flags); int Listen(); // 修改参数为非const引用 Nikson::Sock Accept(SockAddr& sa); int Connect(const SockAddr& sa); int Close(); int Shutdown(int how); SOCKET GetID(); private: // 初始化成员默认值 SOCKET m_NativeID = INVALID_SOCKET; static inline WSAData s_WSAData; static inline int s_SockCount=0; };
Sock.cpp 方法实现修改
// 新增移动构造 Nikson::Sock::Sock(Sock&& other) noexcept { if (s_SockCount == 0) { WORD ver = MAKEWORD(2, 2); if ( WSAStartup(ver, &s_WSAData)!= 0) { std::cout << "Cant Init winsock! Quiting" << std::endl; return; } } ++s_SockCount; // 转移句柄所有权 m_NativeID = other.m_NativeID; other.m_NativeID = INVALID_SOCKET; } // 新增移动赋值 Nikson::Sock& Nikson::Sock::operator=(Sock&& other) noexcept { if (this != &other) { // 先释放自身持有的句柄 if (m_NativeID != INVALID_SOCKET) { closesocket(m_NativeID); --s_SockCount; } // 转移新句柄 m_NativeID = other.m_NativeID; other.m_NativeID = INVALID_SOCKET; } return *this; } // 修正析构逻辑 Nikson::Sock::~Sock() { if (m_NativeID != INVALID_SOCKET) { closesocket(m_NativeID); m_NativeID = INVALID_SOCKET; } --s_SockCount; if (s_SockCount == 0) { WSACleanup(); } } // 修正Accept方法 Nikson::Sock Nikson::Sock::Accept(SockAddr& sa) { int size = sizeof(sockaddr_in); SOCKET clientSock = accept(m_NativeID,(sockaddr*)&sa.data,&size); return Nikson::Sock(clientSock); } // 修正Close方法 int Nikson::Sock::Close() { if (m_NativeID == INVALID_SOCKET) return 0; int res = closesocket(m_NativeID); m_NativeID = INVALID_SOCKET; return res; } // 所有原有构造函数开头补充m_NativeID初始化,比如默认构造: Nikson::Sock::Sock():m_NativeID(INVALID_SOCKET) { if (s_SockCount == 0) { WORD ver = MAKEWORD(2, 2); if ( WSAStartup(ver, &s_WSAData)!= 0) { std::cout << "Cant Init winsock! Quiting" << std::endl; return; } } ++s_SockCount; } // 其余两个构造函数也在初始化列表给m_NativeID赋初始值INVALID_SOCKET即可
SockAddr.cpp 修改
Nikson::SockAddr::SockAddr(int family, int port, unsigned long ip) { ZeroMemory(this, sizeof * this); // 恢复初始化逻辑 data.sin_family = family; data.sin_port = htons(port); data.sin_addr.S_un.S_addr = ip; }
修改完成后,Accept返回的临时对象会通过移动语义将套接字句柄所有权转移给外层client对象,临时对象析构时不会关闭有效套接字,后续recv调用可以正常执行,10038错误和客户端自动断开的问题都会解决。
内容的提问来源于stack exchange,提问作者Maykusa
相关产品推荐
相关产品推荐

