select模型

在Windows中所有的socket函数都是阻塞类型的,也就是说只有网络中有特定的事件发生时才会返回,在没有发生事件时会一直等待,虽说我们将它们设置为非阻塞状态,但是在对于服务器段而言,肯定会一直等待客户端的消息,也就是说即使设置为非阻塞状态,时间到了函数返回,但是程序不能结束,需要一个循环不断的侦听,特别是对于有多个客户端需要管理的时候,每一个与客户端通信的socket都需要一个侦听,这样管理起来非常麻烦,我们希望系统帮助我们管理,告诉我们有哪些socket现在可以操作。为了实现这个,我们可以使用select模型

select模型中需要一个结构体fd_set,该结构体是一个socket的集合,我们可以看到该结构体的定义:

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

从这个定义中可以看到,结构体中主要保存了一个socket的数组和一个保存数组的大小变量;
使用select模型主要使用函数select,该函数原型如下:

int select (
  int nfds,     //系统保留,无意义                      
  fd_set FAR * readfds,//可读的socket集合               
  fd_set FAR * writefds,//可写的socket集合
  fd_set FAR * exceptfds,//外带socket集合             
  const struct timeval FAR * timeout//该函数的超时值  
);

在程序中使用该函数前需要在特定的集合中放入需要检测的socket值,当发生某一时间导致该函数返回时,函数会将特定集合中未待决的socket全部剔除出去,保留待决套接字,比如在readfds集合中放入几个套接字并执行完成函数,那么留下的套接字都是可以从系统的相应缓冲区读数据的。通过遍历相应的集合我们知道如何对套接字做相应的操作;

select模型最多支持64个套接字,这个值由FD_SETSIZE宏定义的,我们可以修改这个宏的值,以便支持更多的套接字,修改时尽量不要在系统文件中修改,在我们的工程文件中修改,可用使用如下方式:

#ifdef FD_SETSIZE
#undef FD_SETSIZE
#endif

#define FD_SETSIZE   200

这段代码使得select模型支持200个套接字;虽然可以修改,但是这个数组太大,会消耗过多的系统资源,每次在遍历数组时总会从头到尾遍历,数组太大效率必然底下,所以最好不要修改这个值,处理大于64个套接字的情况下可以使用多线程的方式,多定义几个集合处理;
为了操作这个集合,Windows专门定义了一组宏,他们分别是:

FD_SET(fd, &set) //将fd套接字压入集合set中
FD_ISSET(fd, &set)//判断fd是否在set中
FD_ZERO(&set)//将集合set清零
FD_CLR(fd, &set)//将fd从集合set中删除

下面说一下服务端一个简单的select模型的编写
1)创建套接字,绑定、侦听;
2)等待客户端链接
3)将连接返回的套接字压入一个数组中保存
4)将数组的套接字填入集合中
5)调用select函数
6)检测特定集合中的套接字
7)进行读写操作
8)返回到第四步,等待客户端下一步请求

在编写时需要注意以下几点:
1)为了与多个客户端保持连接,需要一个数组保存与客户端连接的所有的socket,由于select函数只会执行一次,每次返回后需要再次将徐监控的套接字压入集合,调用select,以便进行下一次检测;所以一般将这一步写在一个死循环中
2)注意select是一个阻塞函数,所以为了可以支持多个客户端可以采用一些方法:第一种就是采用多线程的方式,每有一个客户端连接都需要将新开一个线程处理并调用select监控;另一种就是调用select对侦听套接字以及与客户端通信的套接字;为什么可以这样呢,这就要说到TCP/IP中的三次握手,首先一般由客户端发起链接,发送一条数据包到服务器,服务器接收到数据,发送一条确认信息给客户端,然后客户端再发送一条数据,这样就正式建立连接,所以在客户端与服务器建立连接时必然会发送数据,而服务器一定会收到数据,所以将侦听套接字放入到read集合中,当有客户端需要连接时自然会收到一条数据,这个时候select会返回,我们需要校验集合中的套接字是否是侦听套接字,如果是则表明有客户端需要连接;这样当客户端有请求select会返回,可以进行下一次的侦听,没有请求,会死锁在select函数上,但是对于所有客户端并没有太大的影响;
3)我们用数组存储所有的套接字时,每当有客户端链接,我们需要添加,而有客户端断开链接我们需要在数组中删除,并将下一个套接字添加进该位置,为了管理套接字数组,我们另外需要一个队列用来记录退出客户端的socket在数组中的位置,下一次有新的链接进来就将相应的套接字放到这个位置。
下面是一个简单的例子:

SOCKET g_sockArray[FD_SETSIZE] = { 0 };
int g_nCount = 0;

int _tmain(int argc, _TCHAR* argv[])
{
    WSAData wd;
    WSAStartup(MAKEWORD(2, 2), &wd);

    SOCKET sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    if (INVALID_SOCKET == sockListen)
    {
        cout << "创建套接字失败,错误码为:" << WSAGetLastError() << endl;
        closesocket(sockListen);
        WSACleanup();
        return 0;
    }
    SOCKADDR_IN SrvAddr = { AF_INET };
    SrvAddr.sin_port = htons(6666);
    SrvAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (SOCKET_ERROR == bind(sockListen, (SOCKADDR*)&SrvAddr, sizeof(SOCKADDR)))
    {
        cout << "绑定失败,错误码:" << WSAGetLastError() << endl;
        closesocket(sockListen);
        WSACleanup();
        return 0;
    }

    if (SOCKET_ERROR == listen(sockListen, 5))
    {
        cout << "侦听失败,错误码:" << WSAGetLastError() << endl;
        closesocket(sockListen);
        WSACleanup();
    }

    g_sockArray[g_nCount] = sockListen;
    g_nCount++;
    fd_set fdRead = { 0 };
    while (true)
    {
        FD_ZERO(&fdRead);
        for (int i = 0; i < g_nCount; i++)
        {
            FD_SET(g_sockArray[i], &fdRead);
        }

        select(0, &fdRead, NULL, NULL, NULL);

        for (int i = 0; i < g_nCount; i++)
        {
            if (FD_ISSET(g_sockArray[i], &fdRead))
            {
                if (sockListen == g_sockArray[i])
                {
                    SOCKET sockClient = accept(sockListen, NULL, NULL);
                    if (INVALID_SOCKET == sockClient)
                    {
                        cout << "连接失败, 错误码为:" << WSAGetLastError() << endl;
                        break;
                    }

                    cout << "有客户端链接进来" << endl;
                    if (g_nCount == FD_SETSIZE)
                    {
                        cout << "以达到服务器管理上限" << endl;
                        break;
                    }
                    if (NULL == g_pFirst)
                    {
                        g_sockArray[g_nCount] = sockClient;
                    }
                    else
                    {
                        int n = Pop();
                        g_sockArray[n] = sockClient;
                    }

                    g_nCount++;
                }
                else
                {
                    char szBuf[255] = "";
                    recv(g_sockArray[i], szBuf, 255, 0);
                    cout << "客户端发送消息:"<< szBuf << endl;

                    if (0 == strcmp(szBuf, "exit"))
                    {
                        Push(i);
                        closesocket(g_sockArray[i]);
                        cout << "与客户端链接断开" << endl;
                        g_nCount--;
                    }
                    break;
                }
            }
        }
    }

    WSACleanup();
    return 0;
}

上述代码中,每当检测到有待决套接字就处理,处理完一个后就不在继续检测了,我们知道在理论上select执行完成后,保留的是所有待决套接字,那么待决套接字可不可能有多个呢,我觉得这个基本上不可能,因为服务器端判定在某一时刻该套接字是否处于待决状态是在毫秒级别的,就算有几个客户端在某时刻毫秒不差的向服务器发送数据,那么我们还要考虑双方之间的距离(虽说光速很快可以忽略不计但是当单位是毫秒是应该还是有影响)以及网络状况,虽说在大规模的情况可能出现在毫秒级别上同时响应,但是我们的select只支持64个(超过64时需要另外开线程再创建一个相应的集合),在64个客户端中找到这样的两个客户端是不可能的,所以我们就假定每次只有一个待决套接字,使用break为了让其跳出循环,避免做无用功;

posted @ 2017-10-24 20:55  masimaro  阅读(273)  评论(0编辑  收藏  举报