Windows I/O模型之一:Select模型

1.概念理解

       在进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)

四种调用模式:

同步:所谓同步,就是在发出一个功能调用时,在没有得到结果前,该调用就不返回。也就是必须一件

一件做事,等前一件做完了才能做另一件。

例如在C/S模式的某个流程中,你服务器提交了某个请求,在服务器处理完毕返回结果期间客户端什么

也不能做。

异步:异步概念和同步相对。当一个异步过程调用发出后,调用者不会立刻得到结果。调用者在发出

调用后可以继续做自己的事,被调用者通过状态、通知来通知调用者,或者通过回调函数处理这个调用。

阻塞:阻塞调用是指调用结果返回前,当前线程会被挂起(当前线程处于非可执行状态,在这个状态下,

CPU不会给线程分配时间片,即线程暂停运行),函数只有在得到结果后才回返回。

非阻塞:非阻塞和阻塞的概念相对,是指不能立刻得到借过前,该函数不会阻塞当前进程,而回立刻返回。

区别:有人会把同步和阻塞调用等同起来,实际上他们是不同的,对于同步调用来说,很多时候当前调用

还是激活的,只是从逻辑上当前函数没有返回而已。阻塞的话当前线程会被挂起。

2.Select模型的原理和使用步骤

select(选择)模型是Winsock中最常见的I/O模型。之所以称其为“ select模型”,是由于它的“中心思想”

便是利用select函数,实现对 I/O的管理!利用select函数,我们判断套接字上是否存在数据,或者能否向一

个套接字写入数据。之所以要设计这个函数,唯一的目的便是防止应用程序在套接字处于锁定模式中时,在

一次I/O绑定调用(如send或recv)过程中,被迫进入“锁定”状态;同时防止在套接字处于非锁定模式中时,

产生WSAEWOULDBLOCK错误。除非满足事先用参数规定的条件,否则select函数会在进行I/O操作时锁定。

select的函数原型如下:

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

其中,第一个参数nfds会被忽略。之所以仍然要提供这个参数,只是为了保持与早期的Berkeley套接字应用程

序的兼容。大家可注意到三个 fd_set参数:一个用于检查可读性(readfds),一个用于检查可写性(writefds),

另一个用于例外数据( excepfds)。从根本上说,fdset数据类型代表着一系列特定套接字的集合。其中,

readfds集合包括符合下述任何一个条件的套接字:

■ 有数据可以读入。
■ 连接已经关闭、重设或中止。
■ 假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。
writefds集合包括符合下述任何一个条件的套接字:
■ 有数据可以发出。
■ 如果已完成了对一个非锁定连接调用的处理,连接就会成功。
最后,exceptfds集合包括符合下述任何一个条件的套接字:
■ 假如已完成了对一个非锁定连接调用的处理,连接尝试就会失败。
■ 有带外(out-of-band,OOB)数据可供读取。

例如,假定我们想测试一个套接字是否“可读”,必须将自己的套接字增添到readfds集合,再等待select函数

完成。select完成之后,必须判断自己的套接字是否仍为readfds集合的一部分。若答案是肯定的,便表明该套

接字“可读”,可立即着手从它上面读取数据。在三个参数中(readfds、writedfss和exceptfds),任何两个都

可以是空值(NULL);但是,至少有一个不能为空值!在任何不为空的集合中,必须包含至少一个套接字句柄;

否则, select函数便没有任何东西可以等待。最后一个参数timeout对应的是一个指针,它指向一个timeval结构,

用于决定select最多等待 I / O操作完成多久的时间。如 timeout是一个空指针,那么select调用会无限期地“锁定”

或停顿下去,直到至少有一个描述符符合指定的条件后结束。对timeval结构的定义如下:

struct timeval {
long tv_sec;
long tv_usec;

} ;

若将超时值设置为(0,0),表明select会立即返回,允许应用程序对 select操作进行“轮询”。出于对性能方面

的考虑,应避免这样的设置。select成功完成后,会在 fd_set结构中,返回刚好有未完成的I/O操作的所有套接字

句柄的总量。若超过timeval设定的时间,便会返回0。不管由于什么原因,假如select调用失败,都会返回SOCKET_ERROR。

用select对套接字进行监视之前,在自己的应用程序中,必须将套接字句柄分配给一个集合,设置好一个或全部

读、写以及例外 fd_set结构。将一个套接字分配给任何一个集合后,再来调用select,便可知道一个套接字上是

否正在发生上述的I/O活动。Winsock提供了下列宏操作,可用来针对I/O活动,对 fd_set进行处理与检查:

■ FD_CLR(s, *set):从s e t中删除套接字 s。
■ FD_ISSET(s, *set):检查 s是否s e t集合的一名成员;如答案是肯定的是,则返回 T R U E。
■ FD_SET(s, *set):将套接字 s加入集合 s e t。
■ F D _ Z E R O ( * s e t ):将s e t初始化成空集合。

例如,假定我们想知道是否可从一个套接字中安全地读取数据,同时不会陷于无休止的“锁定”状态,便可使用

FD_SET宏,将自己的套接字分配给fd_set集合,再来调用select。要想检测自己的套接字是否仍属 fd_read集合

的一部分,可使用FD_ISSET宏。采用下述步骤,便可完成用select操作一个或多个套接字句柄的全过程:

1) 使用FD_ZERO宏,初始化自己感兴趣的每一个fd_set。
2) 使用FD_SET宏,将套接字句柄分配给自己感兴趣的每个fd_set。
3) 调用select函数,然后等待在指定的fd_set集合中,I/O活动设置好一个或多个套接字句柄。
select完成后,会返回在所有fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。
4) 根据select的返回值,我们的应用程序便可判断出哪些套接字存在着尚未完成(待决)
的I/O操作—具体的方法是使用FD_ISSET宏,对每个fd_set集合进行检查。
5) 知道了每个集合中“待决”的I/O操作之后,对I/O进行处理,然后返回步骤 1 ),继续进

select返回后,它会修改每个fd_set结构,删除那些不存在待决 I / O操作的套接字句柄。这正是我们在上述的步

骤 ( 4 )中,为何要使用FD_ISSET宏来判断一个特定的套接字是否仍在集合中的原因。

3.参考代码

// Select.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <WinSock2.h>
#include <iostream>
using namespace std;

#include <stdio.h>

#pragma comment(lib,"ws2_32.lib")

#define PORT 8000
#define MSGSIZE 255
#define SRV_IP "127.0.0.1"

int g_nSockConn = 0;//请求连接的数目

//FD_SETSIZE是在winsocket2.h头文件里定义的,这里windows默认最大为64
//在包含winsocket2.h头文件前使用宏定义可以修改这个值


struct ClientInfo
{
    SOCKET sockClient;
    SOCKADDR_IN addrClient;
};

ClientInfo g_Client[FD_SETSIZE];

DWORD WINAPI WorkThread(LPVOID lpParameter);

int _tmain(int argc, _TCHAR* argv[])
{//基本步骤就不解释了,网络编程基础那篇博客里讲的很详细了
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2,2),&wsaData);

    SOCKET sockListen = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

    SOCKADDR_IN addrSrv;
    addrSrv.sin_addr.S_un.S_addr = inet_addr(SRV_IP);
    addrSrv.sin_family = AF_INET;
    addrSrv.sin_port = htons(PORT);

    bind(sockListen,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));

    listen(sockListen,64);

    DWORD dwThreadIDRecv = 0;
    DWORD dwThreadIDWrite = 0;

    HANDLE hand = CreateThread(NULL,0, WorkThread,NULL,0,&dwThreadIDRecv);//用来处理手法消息的进程
    if (hand == NULL)
    {
        cout<<"Create work thread failed ";
        getchar();
        return -1;
    }

    SOCKET sockClient;
    SOCKADDR_IN addrClient;
    int nLenAddrClient = sizeof(SOCKADDR);//这里用0初试化找了半天才找出错误

    while (true)
    {
        sockClient = accept(sockListen,(SOCKADDR*)&addrClient,&nLenAddrClient);//第三个参数一定要按照addrClient大小初始化
        //输出连接者的地址信息
        //cout<<inet_ntoa(addrClient.sin_addr)<<":"<<ntohs(addrClient.sin_port)<<"has connect !"<<endl;

        if (sockClient != INVALID_SOCKET)
        {
            g_Client[g_nSockConn].addrClient = addrClient;//保存连接端地址信息
            g_Client[g_nSockConn].sockClient = sockClient;//加入连接者队列
            g_nSockConn++;
        }


    }

    closesocket(sockListen);
    WSACleanup();

    return 0;
}

DWORD WINAPI WorkThread(LPVOID lpParameter)
{
    FD_SET fdRead;
    int nRet = 0;//记录发送或者接受的字节数
    TIMEVAL tv;//设置超时等待时间
    tv.tv_sec = 1;
    tv.tv_usec = 0;
    char buf[MSGSIZE] = "";

    while (true)
    {
        FD_ZERO(&fdRead);
        for (int i = 0;i < g_nSockConn;i++)
        {
            FD_SET(g_Client[i].sockClient,&fdRead);
        }

        //只处理read事件,不过后面还是会有读写消息发送的
        nRet = select(0,&fdRead,NULL,NULL,&tv);

        if (nRet == 0)
        {//没有连接或者没有读事件
            continue;
        }

        for (int i = 0;i < g_nSockConn;i++)
        {
            if (FD_ISSET(g_Client[i].sockClient,&fdRead))
            {
                nRet = recv(g_Client[i].sockClient,buf,sizeof(buf),0);

                if (nRet == 0 || (nRet == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
                {
                    cout<<"Client "<<inet_ntoa(g_Client[i].addrClient.sin_addr)<<"closed"<<endl;
                    closesocket(g_Client[i].sockClient);

                    if (i < g_nSockConn-1)
                    {
                        //将失效的sockClient剔除,用数组的最后一个补上去
                        g_Client[i--].sockClient = g_Client[--g_nSockConn].sockClient;
                    }
                }
                else
                {
                    cout<<inet_ntoa(g_Client[i].addrClient.sin_addr)<<": "<<endl;
                    cout<<buf<<endl;
                    cout<<"Server:"<<endl;
                    //gets(buf);
                    strcpy(buf,"Hello!");
                    nRet = send(g_Client[i].sockClient,buf,strlen(buf)+1,0);
                }
            }
        }
    }
    return 0;
}

服务器的主要步骤:

1.创建监听套接字,绑定,监听

2.创建工作者线程

3.创建一个套接字组,用来存放当前所有活动的客户端套接字,没accept一个连接就更新一次数组

4.接收客户端的连接,因为没有重新定义FD_SIZE宏,服务器最多支持64个并发连接。最好是记录下连接数,不要无条件的接受连接

工作线程

工作线程是一个死循环,依次循环完成的动作是:

1.将当前客户端套接字加入到fd_read集中

2.调用select函数

3.用FD_ISSET查看时候套接字还在读集中,如果是就接收数据。如果接收的数据长度为0,或者发生WSAECONNRESET错误,,则

   表示客户端套接字主动关闭,我们要释放这个套接字资源,调整我们的套接字数组(让下一个补上)。上面还有个nRet==0的判断,

   就是因为select函数会立即返回,连接数为0会陷入死循环。

原文地址:https://www.cnblogs.com/Mr-Zhong/p/4160988.html