• 通信编程:Select 模型通信


    非阻塞模式

    Winsock 可以在阻塞和非阻塞模式下执行 I/O 操作,套接字创建时默认工作在阻塞模式下。也就是说当某个操作不能执行时,程序会先阻塞,等待操作可以被执行时才继续程序。例如对 recv 函数的调用会使程序进入等待状态,直到接收到数据才返回。
    阻塞套接字的好处是使用简单,但是当需要处理多个套接字连接时,就必须创建多个线程,给编程带来了许多不便。所以实际开发中使用最多的还是非阻塞模式,它使用起来比较复杂,但是处理发送和接收数据或者管理连接的 Winsock 调用将会立即返回,效率很高。
    不过如果系统输入缓冲区中没有待处理的数据,那么对 recv 的调用将返回 WSAEWOULDBLOCK 错误。关键的问题在于如何确定套接字什么时候可读/可写,如果需要不断调用函数去测试的话,程序的性能势必会受到影响,解决的办法就是使用 Windows 提供的不同的 I/O 模型。

    Select 模型

    select 模型的设计源于 UNIX 系统,主要实现的原理是 IO 多路复用。select 模型的优势是程序能够在单个线程内同时处理多个套接字连接,这避免了阻塞模式下的线程膨胀问题。但是添加到 fd_set 结构的套接字数量是有限制的,如果能能添加的 socket 太多的话,服务器性能就会受到影响。

    select 函数

    模型通过使用 select 函数来管理 I/O,函数可以确定一个或者多个套接字的状态。如果套接字上没有网络事件发生,便进入等待状态,以便执行同步 I/O。

    int
    WSAAPI
    select(
        _In_ int nfds,
        _Inout_opt_ fd_set FAR * readfds,
        _Inout_opt_ fd_set FAR * writefds,
        _Inout_opt_ fd_set FAR * exceptfds,
        _In_opt_ const struct timeval FAR * timeout
        );
    

    函数调用成功返回发生网络事件的所有 socket 数量的综合,超过时间限制就返回 0.

    参数 说明
    nfds 忽略,为了与 Berkeley 套接字兼容
    readfds 指向一个套接字集合,用来检查其可读性
    writefds 指向一个套接字集合,用来检查其可写性
    exceptfds 指向一个套接字集合,用来检查错误
    timeout 指定此函数等待的最长时间,为 NULL 时最长时间为无限大

    套接字集合

    fd_set 结构是 socket 集合,它可以把多个套接字连在一起,select 函数可以测试这个集合中哪些套接字有事件发生。

    typedef struct fd_set {
            u_int fd_count;               /* how many are SET? */
            SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
    } fd_set;
    

    WINSOCK 定义了 4 个操作 fd_set 的宏。

    功能
    FD_ZERO(*set) 初始化 set 为空集合,集合在使用前应该总是清空
    FD_CLR(s, *set) 从 set 移除套接字 s
    FD_ISSET(s, *set) 检查 s 是不是 set 的成员,如果是返回 TRUE
    FD_SET(s, *set) 添加套接字到集合

    网络事件

    传递给 select 函数的 3 个 fd_set 结构分别用于为了检查可读性(readfds)检查可写性(writefds)检查错误(exceptfds)。当我们想要测试某个 socket 的某种状态是,就把它放入对应的 fd_set 中,等待 select 函数返回。select 函数调用完成后,若 socket 还在 fd_set 中,就说明该 socket 满足可读、可写或者出错了。

    设置超时

    timeout 是 timeval 结构的指针,它指定了 select 函数等待的最长时间。

    /*
     * Structure used in select() call, taken from the BSD file sys/time.h.
     */
    struct timeval {
            long    tv_sec;         /* seconds */
            long    tv_usec;        /* and microseconds */
    };
    
    参数 说明
    tv_sec 等待多少秒
    tv_usec 等待多少毫秒

    如果 timeout 设为 NULL,select 将会无限阻塞。

    Select 模型样例

    注意无论是客户端还是服务器,都需要包含头文件 initsock.h 来载入 Winsock。

    功能设计

    模拟实现 TCP 协议通信过程,要求编程实现服务器端与客户端之间双向数据传递。也就是在一条 TCP 连接中,客户端和服务器相互发送一条数据即可。

    服务器

    使用 Select 模型实现的服务器需要按照如图所示的步骤进行编程,具体编码如下所示。

    #include "initsock.h"
    #include <iostream>
    using namespace std;
    
    CInitSock theSock;      // 初始化Winsock库
    int main()
    {
        // 创建监听套接字
        SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        sockaddr_in sin;
        sin.sin_family = AF_INET;
        sin.sin_port = htons(4567);
        sin.sin_addr.S_un.S_addr = INADDR_ANY;
        // 绑定套接字到本地机器
        if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
        {
            cout << " Failed bind()" << endl;
            return -1;
        }
        // 进入监听模式
        if (::listen(sListen, 5) == SOCKET_ERROR)
        {
            cout << " Failed listen()" << endl;
            return 0;
        }
        cout << "服务器已启动监听,可以接收连接!" << endl;
    
        // select模型处理过程
        // 1)初始化一个套接字集合fdSocket,添加监听套接字句柄到这个集合
        fd_set fdSocket;        // 所有可用套接字集合
        FD_ZERO(&fdSocket);
        FD_SET(sListen, &fdSocket);
        while (TRUE)
        {
            // 2)将fdSocket集合的一个拷贝fdRead传递给select函数,
            // 当有事件发生时,select函数移除fdRead集合中没有未决I/O操作的套接字句柄,然后返回。
            fd_set fdRead = fdSocket;
            int nRet = ::select(0, &fdRead, NULL, NULL, NULL);
            if (nRet > 0)
            {
                // 3)通过将原来fdSocket集合与select处理过的fdRead集合比较,
                // 确定都有哪些套接字有未决I/O,并进一步处理这些I/O。
                for (int i = 0; i < (int)fdSocket.fd_count; i++)
                {
                    if (FD_ISSET(fdSocket.fd_array[i], &fdRead))
                    {
                        if (fdSocket.fd_array[i] == sListen)    // (1)监听套接字接收到新连接
                        {
                            if (fdSocket.fd_count < FD_SETSIZE)
                            {
                                sockaddr_in addrRemote;
                                int nAddrLen = sizeof(addrRemote);
                                //接收客户端的连接请求
                                SOCKET sNew = ::accept(sListen, (SOCKADDR*)&addrRemote, &nAddrLen);
                                FD_SET(sNew, &fdSocket);
                                cout << "
    与主机" << ::inet_ntoa(addrRemote.sin_addr) << "建立连接" << endl;
                            }
                            else
                            {
                                cout << " Too much connections!" << endl;
                                continue;
                            }
                        }
                        else
                        {
                            char szText[256];
                            int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
                            if (nRecv > 0)    // (2)可读
                            {
                                //接收数据
                                szText[nRecv] = '';
                                cout << "  接收到数据:" << szText << endl;
                                //发送数据
                                char result[20];
                                char sendText[] = "你好,客户端!";
                                if(::send(fdSocket.fd_array[i], sendText, strlen(sendText), 0) > 0)
                                {
                                    cout << "  向客户端发送数据:" << sendText << endl;
                                }
                            }
                            else    // (3)连接关闭、重启或者中断
                            {
                                ::closesocket(fdSocket.fd_array[i]);
                                FD_CLR(fdSocket.fd_array[i], &fdSocket);
                            }
                        }
                    }
                }
            }
            else
            {
                cout << " Failed select()" << endl;
                break;
            }
        }
        return 0;
    }
    

    客户端

    #include "InitSock.h"
    #include <iostream>
    using namespace std;
    
    CInitSock initSock;     // 初始化Winsock库
    
    int main()
    {
        // 创建套节字
        SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (s == INVALID_SOCKET)
        {
            cout << " Failed socket()" << endl;
            return 0;
        }
    
        // 也可以在这里调用bind函数绑定一个本地地址
        // 否则系统将会自动安排
        char address[20] = "127.0.0.1";
        // 填写远程地址信息
        sockaddr_in servAddr;
        servAddr.sin_family = AF_INET;
        servAddr.sin_port = htons(4567);
        // 注意,这里要填写服务器程序(TCPServer程序)所在机器的IP地址
        // 如果你的计算机没有联网,直接使用127.0.0.1即可
        servAddr.sin_addr.S_un.S_addr = inet_addr(address);
    
        if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
        {
            cout << " Failed connect() " << endl;
            return 0;
        }
        else 
        {
            cout << "与服务器 " << address << "建立连接" << endl;
        }
    
        char szText[] = "你好,服务器!";
        if (::send(s, szText, strlen(szText), 0) > 0)
        {
            cout << "  发送数据:" << szText << endl;
        }
    
        // 接收数据
        char buff[256];
        int nRecv = ::recv(s, buff, 256, 0);
        if (nRecv > 0)
        {
            buff[nRecv] = '';
            cout << "  接收到数据:" << buff << endl;
        }
        
        // 关闭套节字
        ::closesocket(s);
        return 0;
    }
    

    运行效果

    参考资料

    《Windows 网络与通信编程》,陈香凝 王烨阳 陈婷婷 张铮 编著,人民邮电出版社
    UNIX再学习 -- 函数 select、poll、epoll

  • 相关阅读:
    2020-2021-1 20209305 《Linux内核原理与分析》第九周作业
    2020-2021-1 20209305 《Linux内核原理与分析》第八周作业
    2020-2021-1 20209305 《Linux内核原理与分析》第七周作业
    2020-2021-1 20209305 《Linux内核原理与分析》第六周作业
    2020-2021-1 20209305 《Linux内核原理与分析》第五周作业
    2020-2021-1 20209305 《Linux内核原理与分析》第四周作业
    2020-2021-1 20209305 《Linux内核原理与分析》第三周作业
    2020-2021-1 20209305 《Linux内核原理与分析》第二周作业
    2020-2021-1 20209309《Linux内核原理与分析》第十二周作业
    2020-2021-1 20209309《Linux内核原理与分析》第十一周作业
  • 原文地址:https://www.cnblogs.com/linfangnan/p/15408406.html
Copyright © 2020-2023  润新知