【问题标题】:How to set timeout in UDP socket with C/C++ in WINDOWS?如何在 WINDOWS 中使用 C/C++ 在 UDP 套接字中设置超时?
【发布时间】:2020-07-12 22:52:39
【问题描述】:

我一直试图在我的DatagramSocket WINDOWS 的 C/C++ 实现类中设置一个timeout,但我不能,我尝试了setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char *) &timeout, sizeof(struct timeval)); 像 linux 并使用 select 函数但没有延迟发生了,我做错了什么?

这是接口文件:

#ifndef __DatagramSocket_H__
#define __DatagramSocket_H__


#ifdef linux
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
#else
    // #include <ws2tcpip.h>
    #include <winsock2.h>
    // #pragma comment(lib,"ws2_32.lib") // Winsock Library, it only works in the microsoft compiler, MinGW ignores it
#endif

#include <iostream>
#include <strings.h>
#include <unistd.h>
#include <sys/time.h>

#include <errno.h>
#include "DatagramPacket.h"

class DatagramSocket {
public:
    DatagramSocket(uint16_t iport, const std::string & addr);
    ~DatagramSocket();

    void unbind();

    int send(DatagramPacket &);
    int receive(DatagramPacket &);

    void setTimeout(long, long);
    int receiveTimeout(DatagramPacket &, time_t, time_t);
private:
    sockaddr_in localAddress, remoteAddress;

    struct timeval timeout;
    bool timeout_set;
    #ifdef linux
        int s;
    #else
        SOCKET s;
        struct fd_set fds;
    #endif
};

#endif

DatagramPacket 定义为:

class DatagramPacket {
public:
    // data, len, ip, port
    DatagramPacket(char* , size_t, const string &, uint16_t );
    DatagramPacket(char* , size_t);
    DatagramPacket();
    ~DatagramPacket();
    string getAddress();
    char *getData();
    size_t getLength();
    uint16_t getPort();

    void setAddress(const string &);
    void setData(char* , size_t);
    void setLength(size_t);
    void setPort(uint16_t);

private:
    char *data;
    uint16_t port;
    string ip;
    size_t length;
};

DatagramSocket 类的成员函数定义:

DatagramSocket::DatagramSocket(uint16_t iport, const std::string &addr): timeout_set(false) {
    #ifdef _WIN32  // detect windows of 32 and 64 bits
        WSAData wsaData;
        WORD word = MAKEWORD(2, 2);
        if (WSAStartup(word, &wsaData) != 0) {
            std::cerr << "Server: WSAStartup failed with error: " << WSAGetLastError() << std::endl;
            exit(1);
        }
    #endif 

    s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    // bzero((char *)&localAddress, sizeof(localAddress));
    memset((char *) &localAddress, 0, sizeof(localAddress));
    localAddress.sin_family = AF_INET;
    localAddress.sin_addr.s_addr = inet_addr(addr.c_str());
    localAddress.sin_port = htons(iport);
    bind(s, (struct sockaddr *) &localAddress, sizeof(localAddress));
}

void DatagramSocket::setTimeout(long secs, long u_secs) {
    timeout = { 
        .tv_sec = secs, 
        .tv_usec = u_secs };  
    timeout_set = true;
    #ifdef __linux__
        setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char *) &timeout, sizeof(struct timeval));
    #else 
        FD_ZERO(&fds);
        FD_SET(s, &fds);
    #endif
}


int DatagramSocket::receiveTimeout(DatagramPacket &p, time_t secs, time_t u_secs) {
    int len = sizeof(remoteAddress);
    if (!timeout_set) setTimeout(secs, u_secs);  
    #ifdef _WIN32
        int sret = select(0, &fds, NULL, NULL, &timeout);
        if (sret == 0) {
            cout << "Timeout! " << sret << endl;
            return NULL;
        }
    #endif
    int n = recvfrom(s, p.getData(), p.getLength(), 0, (struct sockaddr *) &remoteAddress, &len);
    if (n < 0) {  // deal with errors 
        #ifdef __linux__
            if (errno == EWOULDBLOCK) 
                fprintf(stderr, "Timeout \n");
            else 
                fprintf(stderr, "Error in recvfrom. \n");
        #endif
        return n;
    }
    p.setPort(remoteAddress.sin_port);  
    p.setAddress(string(inet_ntoa(remoteAddress.sin_addr)));
    p.setLength(n);
    return n;
}

我包含了 ws2tcpip.h 和 winsock2.h 库,并使用 -lwsock32 标志进行了编译。我在 Windows 10 中使用 MinGW。我已经可以在 linux 中完成此任务,但在 Windows 中无法完成,我一直在互联网上寻找答案,但没有代码适合我。正如我所说,设置超时在 linux 中确实有效。

提前致谢。

【问题讨论】:

  • 我们看不到足够的代码来找出问题所在。我敢打赌你至少犯了这两个错误:1)你是否设置了套接字非阻塞?您必须这样做。 2)你每次都重置fds吗?当您调用select 时,fds 既是输入参数又是输出参数,因此您希望将副本传递给select,而不是原始参数。
  • 我用unsigned long mode = 0; ioctlsocket(s, FIONBIO, &amp;mode); 设置了套接字非阻塞,但它不起作用。感谢您的第二次警告。我添加了更多代码。
  • 你每次都重置fds吗?

标签: c++ c windows sockets udp


【解决方案1】:

在 Windows 上,SO_RCVTIMEO 采用 DWORD 指定毫秒,而不是像其他平台上的 timeval 结构:

void DatagramSocket::setTimeout(long secs, long u_secs) {
    timeout = { 
        .tv_sec = secs, 
        .tv_usec = u_secs };  
    timeout_set = true;

    #ifdef _WIN32
    DWORD dw = (secs * 1000) + ((u_secs + 999) / 1000);
    setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char *) &dw, sizeof(dw));
    #else
    setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char *) &timeout, sizeof(struct timeval));
    #endif
}

另外,请注意SO_RCVTIMEO 仅适用于阻塞读取,不适用于非阻塞读取。您显示的代码没有将套接字置于非阻塞模式,因此如果您使用SO_RCVTIMEO,则使用select() 确实没有意义:

int DatagramSocket::receiveTimeout(DatagramPacket &p, time_t secs, time_t u_secs) {
    setTimeout(secs, u_secs);  
    int len = sizeof(remoteAddress);
    int n = recvfrom(s, p.getData(), p.getLength(), 0, (struct sockaddr *) &remoteAddress, &len);
    if (n < 0) {  // deal with errors 
        #ifdef _WIN32
        if (WSAGetLastError() == WSAETIMEDOUT) 
        #else
        if (errno == EAGAIN || errno == EWOULDBLOCK)
        #endif
            std::cout << "Timeout!" << std::endl;
        else
            std::cerr << "Error in recvfrom." << std::endl;
        return -1;
    }
    p.setPort(remoteAddress.sin_port);  
    p.setAddress(string(inet_ntoa(remoteAddress.sin_addr)));
    p.setLength(n);
    return n;
}

关于select() 本身,它会修改输出时传递的fd_set(s),因此每次调用select() 时都需要重置fds 变量。因此,如果您要使用select(),那么您应该将select() 变量本地移动到receiveTimeout(),并且不要使用SO_RCVTIMEO

int DatagramSocket::receiveTimeout(DatagramPacket &p, time_t secs, time_t u_secs) {
    timeval timeout = { 
        .tv_sec = secs, 
        .tv_usec = u_secs };  

    fd_set fds;
    FD_ZERO(&fds);
    FD_SET(s, &fds);

    int nfds;
    #ifdef _WIN32
    nfds = 0;
    #else
    nfds = s + 1;
    #endif

    int sret = select(nfds, &fds, NULL, NULL, &timeout);
    if (sret <= 0) {
        if (sret == 0) {
            std::cout << "Timeout!" << std::endl;
        else
            std::cerr << "Error in select." << std::endl;
        return -1;
    }

    int len = sizeof(remoteAddress);
    int n = recvfrom(s, p.getData(), p.getLength(), 0, (struct sockaddr *) &remoteAddress, &len);
    if (n < 0) {  // deal with errors 
        std::cerr << "Error in recvfrom." << std::endl;
        return -1;
    }
    p.setPort(remoteAddress.sin_port);  
    p.setAddress(string(inet_ntoa(remoteAddress.sin_addr)));
    p.setLength(n);
    return n;
}

【讨论】:

  • 嘿,我尝试了你的两个实现,但没有成功。我调试了您使用 setsockopt 的代码,我意识到如果我调用 setTimeout 函数并不重要,recvfrom 在 Windows 中不会阻塞,但在 linux 中会阻塞。我试图通过键入 `u_long mode = 0; 来强制套接字处于阻塞模式。 ioctlsocket(s, FIONBIO, &mode);` 但 recvfrom 仍然没有阻塞。
  • @ÁngelLópezManríquez 套接字默认在所有平台上以阻塞模式创建。只有非阻塞模式需要显式启用。也就是说,recvfrom() 将在 Windows 上以阻塞模式阻塞,如果不是,则必须发生错误。 n 被设置为什么?如果 -1,WSAGetLastError() 返回什么?
  • 我预料到了,但正如我所说,它不会阻塞。在 Windows 中,recvfrom 返回 -1,WSAGetLastError 返回 10054。我没有运行服务器,我只是运行客户端来强制超时。如果我运行服务器然后客户端一切正常,当然。我调用了 receiveTimeout 7 次,我收到了错误代码 10054 的消息“recvfrom 错误”7 次,没有任何延迟。
  • @ÁngelLópezManríquez 错误 10054 是 WSAECONNRESET。根据 MSDN 上的recvfrom() documentation:“WSAECONNRESET:远程端执行硬关闭或异常关闭重置了虚拟电路。应用程序应关闭套接字;它不再可用。在 UDP-数据报套接字此错误表示先前的发送操作导致 ICMP 端口不可达消息
  • @ÁngelLópezManríquez 换句话说,您必须将 UDP 套接字 connect()'ed 到远程对等方,然后尝试使用该“连接”套接字向该对等方发送数据,但对等方没有t 可达,因此recvfrom() 拒绝在“已连接”套接字上侦听来自该对等方的数据包。如果您想继续与其他对等方一起使用套接字,您将不得不“断开”套接字(调用connect()sockaddr 填充 0)。否则,创建一个新的套接字。
猜你喜欢
  • 2012-11-12
  • 2012-04-10
  • 1970-01-01
  • 2020-07-16
  • 2011-05-10
  • 1970-01-01
  • 2014-05-18
  • 2010-09-18
相关资源
最近更新 更多