使用select()实现非阻塞套接字

8
我将尝试使用select函数实现服务器和1个客户端(不再多)之间的非阻塞I/O通信,以实现流畅的通信(可以随时发送消息而不必等待对方发送)。我找到了一个带有一些代码的教程,并尝试将其适应到我的代码中。以下是我的代码 -
服务器
#define PORT "4950"
#define STDIN 0

struct sockaddr name;

void set_nonblock(int socket) {
    int flags;
    flags = fcntl(socket,F_GETFL,0);
    assert(flags != -1);
    fcntl(socket, F_SETFL, flags | O_NONBLOCK);
}


// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa) {
    if (sa->sa_family == AF_INET)
        return &(((struct sockaddr_in*)sa)->sin_addr);

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(int agrc, char** argv) {
    int status, sock, adrlen, new_sd;

    struct addrinfo hints;
    struct addrinfo *servinfo;  //will point to the results

    //store the connecting address and size
    struct sockaddr_storage their_addr;
    socklen_t their_addr_size;

    fd_set read_flags,write_flags; // the flag sets to be used
    struct timeval waitd;          // the max wait time for an event
    int sel;                      // holds return value for select();

    //socket infoS
    memset(&hints, 0, sizeof hints); //make sure the struct is empty
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM; //tcp
    hints.ai_flags = AI_PASSIVE;     //use local-host address

    //get server info, put into servinfo
    if ((status = getaddrinfo("127.0.0.1", PORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
        exit(1);
    }

    //make socket
    sock = socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol);
    if (sock < 0) {
        printf("\nserver socket failure %m", errno);
        exit(1);
    }

    //allow reuse of port
    int yes=1;
    if (setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
        perror("setsockopt");
        exit(1);
    }

    //unlink and bind
    unlink("127.0.0.1");
    if(bind (sock, servinfo->ai_addr, servinfo->ai_addrlen) < 0) {
        printf("\nBind error %m", errno);
        exit(1);
    }

    freeaddrinfo(servinfo);

    //listen
    if(listen(sock, 5) < 0) {
        printf("\nListen error %m", errno);
        exit(1);
    }
    their_addr_size = sizeof(their_addr);

    //accept
    new_sd = accept(sock, (struct sockaddr*)&their_addr, &their_addr_size);
    if( new_sd < 0) {
        printf("\nAccept error %m", errno);
        exit(1);
    }

    set_nonblock(new_sd);
    cout<<"\nSuccessful Connection!";

    char* in = new char[255];
    char* out = new char[255];
    int numSent;
    int numRead;


    while(1) {

        waitd.tv_sec = 10;
        FD_ZERO(&read_flags);
        FD_ZERO(&write_flags);
        FD_SET(new_sd, &read_flags);

        if(strlen(out) != 0)
            FD_SET(new_sd, &write_flags);

        sel = select(new_sd+1, &read_flags, &write_flags, (fd_set*)0, &waitd);
        if(sel < 0)
            continue;

        //socket ready for reading
        if(FD_ISSET(new_sd, &read_flags)) {
            FD_CLR(new_sd, &read_flags);

            memset(&in, 0, sizeof(in));

            if(recv(new_sd, in, sizeof(in), 0) <= 0) {
                close(new_sd);
                break;
            }
            else
                cout<<"\n"<<in;
        }   //end if ready for read

        //socket ready for writing
        if(FD_ISSET(new_sd, &write_flags)) {

            FD_CLR(new_sd, &write_flags);
            send(new_sd, out, strlen(out), 0);
            memset(&out, 0, sizeof(out));
        }
    }   //end while

    cout<<"\n\nExiting normally\n";
    return 0;
}

客户端(基本相同,只是去掉了accept调用)-
#define PORT "4950"

struct sockaddr name;

void set_nonblock(int socket) {
    int flags;
    flags = fcntl(socket,F_GETFL,0);
    assert(flags != -1);
    fcntl(socket, F_SETFL, flags | O_NONBLOCK);
}

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa) {
    if (sa->sa_family == AF_INET)
        return &(((struct sockaddr_in*)sa)->sin_addr);

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(int agrc, char** argv) {
    int status, sock, adrlen;

    struct addrinfo hints;
    struct addrinfo *servinfo;  //will point to the results

    fd_set read_flags,write_flags; // the flag sets to be used
    struct timeval waitd;          // the max wait time for an event
    int sel;                      // holds return value for select();

    memset(&hints, 0, sizeof hints); //make sure the struct is empty
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM; //tcp
    hints.ai_flags = AI_PASSIVE;     //use local-host address

    //get server info, put into servinfo
    if ((status = getaddrinfo("127.0.0.1", PORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
        exit(1);
    }

    //make socket
    sock = socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol);
    if (sock < 0) {
        printf("\nserver socket failure %m", errno);
        exit(1);
    }

    if(connect(sock, servinfo->ai_addr, servinfo->ai_addrlen) < 0) {
        printf("\nclient connection failure %m", errno);
        exit(1);
    }

    cout<<"\nSuccessful connection!";

    set_nonblock(sock);

    char* out = new char[255];
    char* in = new char[255];
    int numRead;
    int numSent;

    while(1) {

        waitd.tv_sec = 10;
        FD_ZERO(&read_flags);
        FD_ZERO(&write_flags);
        FD_SET(sock, &read_flags);

        if(strlen(out) != 0)
            FD_SET(sock, &write_flags);


        sel = select(sock+1, &read_flags, &write_flags, (fd_set*)0, &waitd);
        if(sel < 0)
            continue;

        //socket ready for reading
        if(FD_ISSET(sock, &read_flags)) {
            FD_CLR(sock, &read_flags);

            memset(&in, 0, sizeof(in));

            if(recv(sock, in, sizeof(in), 0) <= 0) {
                close(sock);
                break;
            }
            else
                cout<<"\n"<<in;
        }   //end if ready for read

        //socket ready for writing
        if(FD_ISSET(sock, &write_flags)) {
            FD_CLR(sock, &write_flags);
            send(sock, out, strlen(out), 0);
            memset(&out, 0, sizeof(out));
        }
    }   //end while

    cout<<"\n\nExiting normally\n";
    return 0;
}

问题在于我运行它们时,什么也不会发生。我可以在其中一个输入并按回车键,但另一个屏幕上没有任何显示(反之亦然)。这对我来说不是很有信息可供调试,而且这是我第一次尝试使用select,所以我想也许我只是不知道一些简单的东西。如果有任何错误或奇怪之处,请指出,感谢您的帮助。

3个回答

15

问题在于我运行它们时,没有任何反应。

真正的问题是多年来人们一直在复制 Beej 的代码,但并不理解它。这就是为什么我不太喜欢那个指南;它提供了大量的代码块,但并没有详细地解释它们。

你没有读取任何东西也没有发送任何东西;没有 fgets、scanf、cin 等等。这是我会做的:

FD_SET(sock, &read_flags);
FD_SET(STDIN_FILENO, &read_flags);

/* .. snip .. */

if(FD_ISSET(STDIN_FILENO, &read_flags)) {
    fgets(out, len, stdin);
}

这将监视stdin,并在有输入时从中读取;然后,在套接字可写时(FD_ISSET(sock, &write_flags)),它将发送缓冲区。


1
谢谢您的回复。是的,我最近几天一直在阅读Beej的教程,但仍然感到困难重重。我尝试将其添加到我的代码中,但现在每当我在该终端上键入任何内容时(在服务器上输入“hi”,服务器会发生段错误,客户端关闭良好,反之亦然),我都会收到一个分段错误。我尝试使用getline来确保它不仅仅是fgets的问题。GDB并没有真正帮助我-告诉我回溯只是#0 0x08048c93在main()中。很抱歉要问(此时有点绝望),但您知道可能是什么原因吗? - Sterling
@Sterling 使用-g编译。另外,作为入门,请这样声明outinchar out[255]; - cnicutar
奇怪...当我只改变那些声明时,程序不会出现“段错误”,但它直接通过 while 循环并退出。 - Sterling
@Sterling 尝试调试它。如果需要,添加 printf 行。在“select”之后添加一个 printf 行 printf("Select fired\n"); 等等。 - cnicutar
当然,他没有使用fgetscin等,但他确实调用了recv。为什么这样不够? - EntangledLoops
显示剩余2条评论

5
我现在已经成功地运行了这个程序。
服务器 -
#define PORT "4950"
#define STDIN 0

struct sockaddr name;

void set_nonblock(int socket) {
    int flags;
    flags = fcntl(socket,F_GETFL,0);
    assert(flags != -1);
    fcntl(socket, F_SETFL, flags | O_NONBLOCK);
}


// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa) {
    if (sa->sa_family == AF_INET)
        return &(((struct sockaddr_in*)sa)->sin_addr);

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}


int main(int agrc, char** argv) {
    int status, sock, adrlen, new_sd;

    struct addrinfo hints;
    struct addrinfo *servinfo;  //will point to the results

    //store the connecting address and size
    struct sockaddr_storage their_addr;
    socklen_t their_addr_size;

    fd_set read_flags,write_flags; // the flag sets to be used
    struct timeval waitd = {10, 0};          // the max wait time for an event
    int sel;                      // holds return value for select();


    //socket infoS
    memset(&hints, 0, sizeof hints); //make sure the struct is empty
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM; //tcp
    hints.ai_flags = AI_PASSIVE;     //use local-host address

    //get server info, put into servinfo
    if ((status = getaddrinfo("127.0.0.1", PORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
        exit(1);
    }

    //make socket
    sock = socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol);
    if (sock < 0) {
        printf("\nserver socket failure %m", errno);
        exit(1);
    }

    //allow reuse of port
    int yes=1;
    if (setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
        perror("setsockopt");
        exit(1);
    }

    //unlink and bind
    unlink("127.0.0.1");
    if(bind (sock, servinfo->ai_addr, servinfo->ai_addrlen) < 0) {
        printf("\nBind error %m", errno);
        exit(1);
    }

    freeaddrinfo(servinfo);

    //listen
    if(listen(sock, 5) < 0) {
        printf("\nListen error %m", errno);
        exit(1);
    }
    their_addr_size = sizeof(their_addr);

    //accept
    new_sd = accept(sock, (struct sockaddr*)&their_addr, &their_addr_size);
    if( new_sd < 0) {
        printf("\nAccept error %m", errno);
        exit(1);
    }

    //set non blocking
    set_nonblock(new_sd);
    cout<<"\nSuccessful Connection!\n";

    char in[255];
    char out[255];
    memset(&in, 0, 255);
    memset(&out, 0, 255);
    int numSent;
    int numRead;

    while(1) {

        FD_ZERO(&read_flags);
        FD_ZERO(&write_flags);
        FD_SET(new_sd, &read_flags);
        FD_SET(new_sd, &write_flags);
        FD_SET(STDIN_FILENO, &read_flags);
        FD_SET(STDIN_FILENO, &write_flags);

        sel = select(new_sd+1, &read_flags, &write_flags, (fd_set*)0, &waitd);

        //if an error with select
        if(sel < 0)
            continue;

        //socket ready for reading
        if(FD_ISSET(new_sd, &read_flags)) {

            //clear set
            FD_CLR(new_sd, &read_flags);

            memset(&in, 0, 255);

            numRead = recv(new_sd, in, 255, 0);
            if(numRead <= 0) {
                printf("\nClosing socket");
                close(new_sd);
                break;
            }
            else if(in[0] != '\0')
                cout<<"\nClient: "<<in;

        }   //end if ready for read

        //if stdin is ready to be read
        if(FD_ISSET(STDIN_FILENO, &read_flags))
            fgets(out, 255, stdin);


        //socket ready for writing
        if(FD_ISSET(new_sd, &write_flags)) {
            //printf("\nSocket ready for write");
            FD_CLR(new_sd, &write_flags);
            send(new_sd, out, 255, 0);
            memset(&out, 0, 255);
        }   //end if
    }   //end while

   cout<<"\n\nExiting normally\n";
    return 0;
}

客户端基本上是相同的...唯一的区别就是缺少监听和接受功能。

3
"我太累了,解释不清楚,所以我会贴出代码,如果未来有人需要这些信息,可以与上面的代码进行比较。" 这并不是编写有用答案的正确方式。-1 - Manu343726
在这个部分:FD_SET(new_sd, &read_flags); FD_SET(new_sd, &write_flags); FD_SET(STDIN_FILENO, &read_flags); FD_SET(STDIN_FILENO, &write_flags);前两个FD_SET被后两个覆盖了。为什么要这样做呢? - EntangledLoops
@EntangledLoops 我不同意 - 这四行代码将会把 new_sd 和 STDIN_FILENO 添加到 read_flags 和 write_flags 集合中。参考 fd_set man pagesFD_SET() 和 FD_CLR() 分别用于向集合中添加和移除给定的文件描述符。 - pepan
@pepan 你显然是正确的,因为现在我看它时,不记得去年十月份读它时在想什么了。对于一个很好的例子给予+1的支持。 - EntangledLoops

0

我只想补充一下,上面的例子在Linux上可能无法按预期工作。在Linux上,waitd可能会被select修改。因此,在Linux上,在select之前应该重写waitd。

请参阅select的man页面中的“timeout”部分。


这个问题不应该放在已接受答案的评论中吗? - fde-capu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接