美文网首页
Windows Select

Windows Select

作者: FakeCSer爱去网吧 | 来源:发表于2020-09-24 20:49 被阅读0次

select IO模型学习

  • 问题由来

    单进程单线程下,最普通的socket是阻塞连接的,即server与一个client连接后,不能与其他的client通信。

    如果采取多线程解决这个问题,每来一个client socket连接,就创建一个线程的话,浪费时间。

  • fd_set 一个数据类型,四个函数

    • 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;
      
    • FD_ZERO(fd_set*) ;将传入的套接字集合置空

    • FD_SET(fd, fd_set*) 将套接字fd放入套接字集合中

    • FD_CLR(fd, fd_set*) 将套接字集合中的套接字fd 去除掉

    • FD_ISSET(fd, fd_set*)查询套接字fd是否在套接字集合中

  • select

//VS2015 f12查找定义的结果  定义在 #include <WinSock2.h> 下
  int
  WSAAPI
  select(
      _In_ int nfds,
      _Inout_opt_ fd_set FAR * readfds,  //类型 _Inout_opt_ 表示输入输出参数
      _Inout_opt_ fd_set FAR * writefds,
      _Inout_opt_ fd_set FAR * exceptfds,
      _In_opt_ const struct timeval FAR * timeout
      );
  -----------------------------
  
  参数:
      
  nfds
  忽略。包含nfds参数只是为了与Berkeley套接字兼容。
  注意:这个参数在linux中要有特殊的意义和操作,表示最大的套接字内容,但是windows下可以直接用0
  
  readfds
  套接字集合指针类型,检查套接字集合的可读性
  可读性监视,可读性指有连接到来、有数据到来、连接已关闭、重置或终止
      
  writefds
  套接字集合指针类型,检查套接字集合的可写性
  1如果处理一个connect调用(非阻塞),连接成功。数据可以发送。

  exceptfds
  套接字集合指针类型,检查套接字集合错误
  
timeout
  select等待的最长时间,以TIMEVAL结构的形式提供。对于阻塞操作,请将timeout参数设置为null。
      
  返回值:
  返回状态发生的套接字的个数  失败返回SOCKET_ERROR

个人理解select函数的作用

  • 把想要监控状态的套接字集合传给select的相应部分(是否可读可写是否出错),然后内核会自动维护这个套接字集合:通过输入输出参数 (&)的方式。维护的结果就是,除了有期待状态发生的socket,其他的都会踢出集合。

重要:sockSer可读表示有链接到来,sockCli可读表示有数据到来

  • select缺点与epoll比较

    elect监控的文件描述符有上限
    每次调用都需要手动的设置文件描述符集合,使用非常不便
    每次调用都要把文件描述符从用户态拷贝到内核态,开销比较大
    当就绪的文件描述符好后,需要循环遍历来进行判断,效率不好

  • 与多进程多线程服务器的优缺点比较

    • 优点

      1)不需要建立多个线程、进程就可以实现一对多的通信。
      2)可以同时等待多个文件描述符,效率比起多进程多线程来说要高很多。
      3)select()的可移植性更好,在某些Unix系统上不支持poll()
      4)select() 对于超时值提供了更好的精度:微秒,而poll是毫秒

    • 缺点

      1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 ,循环次数有点多;
      2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 。
      3)select支持的文件描述符数量太小了,默认是1024;

  • 一个小实现,同时连接多个客户端并且读取各个客户端发来的消息
//*************服务器端*************************
//=========================================================头文件

#include <WinSock2.h>//是网络编程socket相关部分API
#include <WS2tcpip.h>//inet_ntop()
#include <iostream>
#include <sstream>//std::to_string
#include <cstring>
#pragma comment(lib,"ws2_32.lib")//这是链接API相关连的Ws2_32.lib静态库
using namespace std;

//==========================================================静态变量

const int BUF_SIZE = 2048;//缓冲区变量长度

                          //==========================================================全局变量

SOCKET sockSer;//定义用于监听的套接口
SOCKET sockCli;//定义用于连接客户端和收发信息的套接口
SOCKADDR_IN addrSer, addrCli;//定义两端的地址(IP地址加端口号)
int naddr = sizeof(SOCKADDR_IN);

char sendBuf[BUF_SIZE];//发送的字符串缓冲区
char recvBuf[BUF_SIZE];//接收内容缓冲区


int main()
{

    //加载socket库,WSAStartup(Sock版本,版本信息返回)
    WSADATA wsadata;
    if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
    {
        //输出出错信息
        cout << "载入socket失败" << endl;

        system("pause");//暂停一下给用户反映时间
        return 0;
    }
    //else { cout << "载入socket成功" << endl; }


    //创建socket
    sockSer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    //sockets(套接字)编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW);基于TCP的socket编程是采用的流式套接字。


    //初始化SOCKET_IN各个成员变量
    //addrSer.sin_addr.s_addr= inet_addr("202.98.105.139");
    addrSer.sin_addr.S_un.S_addr = INADDR_ANY;
    //inet_addr十进制ip转二进制ip
    addrSer.sin_family = AF_INET;//指定协议簇
    addrSer.sin_port = htons(20000);//htons()端口转换二进制


                                    //绑定socket:bind(socket套接字,地址,)
    if (bind(sockSer, (SOCKADDR*)&addrSer, sizeof(SOCKADDR)) == -1)
    {
        cout << "bind fail" << endl;
    }
    else { cout << "bind success" << endl; }


    //监听请求listen(服务端套接字,等待队列最大容量(3-5))
    if (listen(sockSer, 5) == -1)
    {
        cout << "listen fail" << endl;
    }
    else { cout << "listen success" << endl; }


    /*
    //正常一对一
    sockCli = accept(sockSer, (SOCKADDR*)&addrCli, &naddr);

    //请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字sockCli

    if (sockCli != INVALID_SOCKET) {//连接成功
        cout << "连接客户端成功!" << endl;

        strncpy_s(sendBuf, "Hello",50);
        send(sockCli, sendBuf, sizeof(sendBuf), 0);//发送数据到客户端

    }
    else { cout << "连接客户端失败!" << endl; }
    
    system("pause");
    */



    //allSocketsSet存放sockSer+所有的已连接的Client Sockets
    fd_set allSocketsSet;
    FD_ZERO(&allSocketsSet);

    //先把sockSer放进去,如果sockSer可读(FD_SET(sockCli, &allSocketsSet);)表示有客户端连接
    FD_SET(sockSer, &allSocketsSet);

    while (true)
    { 
    
        fd_set readSet;
        //每次select前要存储已经连接的描述符
        readSet = allSocketsSet;
        int ret = select(0, &readSet, 0, 0, 0);
        if (ret == SOCKET_ERROR)
        {
            cout << "select error" << endl;
            break;
        }

//不断等待客户端请求的到来,到来了就加allSocketsSet,之后跳出循环
        if (FD_ISSET(sockSer, &readSet))
        {//有客户端连接,就给他保存到allSocketsSet中

            sockCli = accept(sockSer, (SOCKADDR*)&addrCli, &naddr);
            if (sockCli == INVALID_SOCKET)
            {
                cout << "accept error" << endl;
                break;
            }
            else 
            {
                cout << "accept连接客户端成功!" << endl;
                //将连接成功的sockCli放入allSocketsSet
                FD_SET(sockCli, &allSocketsSet);
                continue;
            }
        }
//没客户端到来,再检查allSocketsSet中的其他客户端的sockets是否可读,可读表示有数据到来。
        for (u_int i = 0; i < allSocketsSet.fd_count; ++i)
        {
            SOCKET socket = allSocketsSet.fd_array[i];
            if (FD_ISSET(socket, &readSet))
            {//可读,有数据到来
                int result = recv(socket, recvBuf, BUF_SIZE, 0);
                //返回值分析
                if (result == SOCKET_ERROR)
                {//读的过程中出错
                    DWORD err = WSAGetLastError();
                    if (err == WSAECONNRESET)       // 客户端的socket没有被正常关闭,即没有调用closesocket
                    {
                        cout << "客户端被强行关闭" << endl;
                    }
                    else
                    {
                        cout << "recv() error" << endl;
                    }

                    closesocket(socket);
                    FD_CLR(socket, &allSocketsSet);

                    //std::cout << "目前客户端的数量为:" << allSockSet.fd_count - 1 << std::endl;
                    cout << "目前客户端的数量为:" << std::to_string(allSocketsSet.fd_count - 1) << endl;
                    break;
                }
                else if (result == 0)
                {//客户端的socket调用closesocket正常关闭
                    closesocket(socket);
                    FD_CLR(socket, &allSocketsSet);
                    //-1是因为减去sockSer
                    cout << "客户端已经退出,目前客户端的数量为:" << std::to_string(allSocketsSet.fd_count - 1) << endl;
                    break;
                }
                if (result > 0)
                {//正常读取了结果
                    cout << recvBuf << endl;
                    
                    /*
                    *
                    *
                    此处可添加对客户端发来消息的处理。
                    *
                    */
                }
            }

        }
    }
    closesocket(sockSer);
    closesocket(sockCli);
    return 0;
}

相关文章

网友评论

      本文标题:Windows Select

      本文链接:https://www.haomeiwen.com/subject/vrmtuktx.html