Fork me on GitHub

Winsock IO模型之select模型

     之所以称其为select模型是因为它主要是使用select函数来管理I/O的。这个模型的设计源于UNIX系统,目的是允许那些想要避免在套接字调用上阻塞的应用程序有能力管理多个套接字。

    int select(

         int nfds,                                                 // 忽略,仅是为了与Berkeley套接字兼容

         fd_set* readfds,                                  // 指向一个套接字集合,用来检查其可读性

         fd_set* writefds,                                 // 指向一个套接字集合,用来检查其可写性

         fd_set* exceptfds,                              // 指向一个套接字集合,用来检查错误

         const struct timeval* timeout           // 指定此函数等待的最长时间,如果为NULL,则最长时间为无限大

    );

 

 Select模型是最常见的I/O模型。

使用 int select( int nfds , fd_set FAR* readfds , fd_set FAR* writefds,fd_set FAR* exceptfds,const struct timeval FAR * timeout ) ;

函数来检查你要调用的Socket套接字是否已经有了需要处理的数据。

select包含三个Socket队列,分别代表: readfds ,检查可读性,writefds,检查可写性,exceptfds,例外数据。 timeout是select函数的返回时间。

例如,我们想要检查一个套接字是否有数据需要接收,我们可以把套接字句柄加入可读性检查队列中,然后调用select,如果,该套接字没有数据需要接收, select函数会把该套接字从可读性检查队列中删除掉,所以我们只要检查该套接字句柄是否还存在于可读性队列中,就可以知道到底有没有数据需要接收了。

timeout参数控制select完成的时间。若timeout参数为空指针,则select将一直阻塞到有一个描述字满足条件,否则的话,timeout指向一个timeval结构,其中指定了select调用在返回前等待多长时间。如果timeval为{0,0},则select立即返回,这可用于探询所选套接口的状态,如果处于这种状态,则select调用可认为是非阻塞的,且一切适用于非阻塞调用的假设都适用于它,举例来说,阻塞钩子函数不应被调用,且WINDOWS套接口实现不应yield。

    函数调用成功,返回发生网络事件的所有套接字数量的总和。如果超过了时间限制返回0,失败则返回SOCKET_ERROR。

    typedef struct fd_set {

                  u_int fd_count;                                       // 下面数组的大小

                  SOCKET fd_array[FD_SETSIZE];       // 套接字句柄数组

     } fd_set;

  • FD_ZERO(*set)                            初始化set为空集合。集合在使用前应该总是清空
  • FD_CLR(s, *set)                          从set移除套接字s
  • FD_ISSET(s, *set)                       检查s是不是set的成员,如果是返回TRUE
  • FD_SET(s, *set)                           添加套接字到集合
 
    typedef struct timeval{
                  long tv_sec;                        // 指示等待多少秒
                  long tv_usec;                      // 指示等待多少毫秒
    } timeval;
 
下面给出使用select模式的例子,运行后在4567端口监听,接受客户端连接请求,打印出接收到的数据。(原来单线程也可以管理多个套接字)

 
//////////////////////////////////////////////////////////  
// initsock.h文件  

#include <winsock2.h>  
#pragma comment(lib, "WS2_32")  // 链接到WS2_32.lib  

class CInitSock
{
public:
    CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
    {
        // 初始化WS2_32.dll  
        WSADATA wsaData;
        WORD sockVersion = MAKEWORD(minorVer, majorVer);
        if (::WSAStartup(sockVersion, &wsaData) != 0)
        {
            exit(0);
        }
    }
    ~CInitSock()
    {
        ::WSACleanup();
    }
};


//////////////////////////////////////////////////////  
// select.cpp文件  


#include "../common/initsock.h"  
#include <stdio.h>  

CInitSock theSock;      // 初始化Winsock库  
int main()
{
    USHORT nPort = 4567;    // 此服务器监听的端口号  

    // 创建监听套节字  
    SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(nPort);
    sin.sin_addr.S_un.S_addr = INADDR_ANY;
    // 绑定套节字到本地机器  
    if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
    {
        printf(" Failed bind() \n");
        return -1;
    }
    // 进入监听模式  
    ::listen(sListen, 5);

    // 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);
                            printf("接收到连接(%s)\n", ::inet_ntoa(addrRemote.sin_addr));
                        }
                        else
                        {
                            printf(" Too much connections! \n");
                            continue;
                        }
                    }
                    else
                    {
                        char szText[256];
                        int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
                        if (nRecv > 0)                        // (2)可读  
                        {
                            szText[nRecv] = '\0';
                            printf("接收到数据:%s \n", szText);
                        }
                        else                                // (3)连接关闭、重启或者中断  
                        {
                            ::closesocket(fdSocket.fd_array[i]);
                            FD_CLR(fdSocket.fd_array[i], &fdSocket);
                        }
                    }
                }
            }
        }
        else
        {
            printf(" Failed select() \n");
            break;
        }
    }
    return 0;
}

大体流程: 

//创建服务器套接字 
socket 
//绑定本地地址 
bind 
//进入监听模式 
listen 
//select模式 
//构造fd_set集合 
fd_set fdSocket;    
//清空fd_set集合并将服务器套接字加入 
FD_ZERO(&fdSocket);   
FD_SET(sListen, &fdSocket);   
//循环 
//复制一份fd_set集合并放入select中检测 
fd_set fdRead = fdSocket;   
int nRet = ::select(0, &fdRead, NULL, NULL, NULL);   
//根据select结果进行相应处理(accept read ) 


总结:通过select模式,实现了监听socket的accept和客户端的read之间,以及各个客户端之间的read,可以不用一直阻塞在那,而是在有相应事件的时候再进行阻塞处理,把accept和read两个长阻塞转化为select一个长阻塞。

     使用select的好处是程序能够在单个线程内同时处理多个套接字连接,这避免了阻塞模式下的线程膨胀问题。但是,添加到fd_set结构的套接字数量是有限制的,默认情况下,最大值是FD_SETSIZE,它在winsock2.h文件中定义为64。为了增加套接字数量,应用程序可以将FD_SETSIZE定义为更大的值(这个定义必须在包含winsock2.h之前出现)。不过,自定义的值也不能超过Winsock下层提供者的限制(通常是1024)。
    另外,FD_SETSIZE的值太大的话,服务器性能就会受到影响。例如有1000个套接字,那么在调用select之前就不得不设置这1000个套接字,select返回之后,又必须检查这1000个套接字。
posted @ 2016-06-26 21:32  ranjiewen  阅读(752)  评论(0编辑  收藏  举报