You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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调用异常
修复方案

按以下步骤修改代码即可解决问题:

  1. 修正Sock类的资源管理逻辑,避免句柄重复释放
    • 禁止默认浅拷贝,实现移动语义保证同一时间一个套接字句柄仅被一个Sock对象持有
    • 所有构造函数中初始化m_NativeID为INVALID_SOCKET
    • 析构、Close方法中先判断句柄有效性,关闭后立即将m_NativeID设为INVALID_SOCKET
  2. 修正Accept方法签名
    去掉参数的const限定,传入正确的地址长度值,保证acceptAPI可以正常写入客户端地址信息
  3. 补全SockAddr结构体初始化逻辑
    恢复unsigned long参数构造函数中的ZeroMemory调用,清空结构体预留字段
  4. 清理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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 18:09:28