I/O复用(一)-select (待完善)

本文详细介绍了select系统调用的工作原理,包括其参数含义、fd_set结构体、FD_SETSIZE限制以及在服务器开发中的基本使用流程。同时,文章指出在使用select时需要注意的两个关键点:readfds等集合的更新以及超时时间timeout的处理。

代码和知识有引用自 高性能服务器开发

select系统调用:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写、异常事件

select 函数API如下

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

参数含义:
nfds 被监听的文件描述符总数,通常被设置为select监听的所有文件描述符中的最大值加1(因为文件描述符是从0开始的)
readfds、writefds、exceptfds 对应需要监听的可读、可写、异常事件的fd集合
timeout select函数超时时间,不设置的话select函数会一直阻塞等待事件发生

其中readfds、writefds、exceptfds都为同个结构体fd_set,定义在/usr/include/sys/select.h中

/usr/include/bits/typesizes.h
#define __FD_SETSIZE            1024

/* The fd_set member is required to be an array of longs.  */
typedef long int __fd_mask;

/* It's easier to assume 8-bit bytes than to get CHAR_BIT.  */
#define __NFDBITS       (8 * (int) sizeof (__fd_mask))
#define __FD_ELT(d)     ((d) / __NFDBITS)
#define __FD_MASK(d)    ((__fd_mask) 1 << ((d) % __NFDBITS))

/* fd_set for select and pselect.  */
typedef struct
  {
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
  } fd_set;

/* Maximum number of file descriptors in `fd_set'.  */
#define FD_SETSIZE              __FD_SETSIZE

由此可见,fd_set就只有一个整型数组,该数组的每一位(bit)标记一个文件描述符。fd_set可以容纳的文件描述符数量由FD_SETSIZE决定。因此select在同时处理文件描述符的总量是受到限制的,在/usr/include/sys/select.h中还定义了一系列的宏来处理fd_set结构体的位:

#include <sys/select.h>
FD_ZERO(fd_set * fdset); 			//清除fdset的所有位
FD_SET(int fd,fd_set *fdset);		//设置fdset的位fd
FD_CLR(int fd,fd_set *fdset);    	//清除fdset的位fd
int FD_ISSET(int fd,fd_set * fdset) //验证fdset的位fd是否被设置
    

/* We don't use `memset' because this would require a prototype and
   the array isn't too big.  */
# define __FD_ZERO(set)  \
  do {                                                                        \
    unsigned int __i;                                                         \
    fd_set *__arr = (set);                                                    \
    for (__i = 0; __i < sizeof (fd_set) / sizeof (__fd_mask); ++__i)          \
      __FDS_BITS (__arr)[__i] = 0;                                            \
  } while (0)

#endif  /* GNU CC */

#define __FD_SET(d, set) \
  ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)))
#define __FD_CLR(d, set) \
  ((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d)))
#define __FD_ISSET(d, set) \
  ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0)

接下来是参数timeout,结构体形式为:

struct timeval 
{
	long    tv_sec;         /* seconds */
	long    tv_usec;        /* microseconds */
};

一般服务端实现的基础流程如下图

对应代码如下 

#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <iostream>
#include <string.h>
#include <vector>

#define INVALID_FD -1

int main(int argc,char * argv[])
{
    int listenfd = socket(AF_INET,SOCK_STREAM,0);
    if(listenfd == -1)
    {
        std::cout << "create socket err" << std::endl;
        return -1;
    }
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);
    if(bind(listenfd,(struct sockaddr *)&bindaddr,sizeof(bindaddr)) == -1)
    {
        std::cout << "bind err" << std::endl;
    }

    if(listen(listenfd,SOMAXCONN) == -1)
    {
        std::cout << "listen err" << std::endl;
    }

    // 存储客户端socket的数组
    std::vector<int> clientfds;
    int maxfd = listenfd;

    while(true)
    {
        fd_set readset;
        FD_ZERO(&readset);  //清除readset的所有位

        FD_SET(listenfd,&readset);  //设置readset的位fd

        int clientfdslength = clientfds.size();
        for(int i=0; i<clientfdslength; ++i)
        {
            if(clientfds[i] != INVALID_FD)
            {
                FD_SET(clientfds[i],&readset);
            }
        }

        timeval tm;
        tm.tv_sec = 1;
        tm.tv_usec = 0;

        // 只检测可读事件
        int ret = select(maxfd+1,&readset,NULL,NULL,&tm);
        if(ret == -1)
        {
            if(errno != EINTR)  //出错退出
                break;
        }
        else if (ret == 0)
        {
            continue; //超时重来
        }
        else
        {
            if(FD_ISSET(listenfd,&readset)) //检测到有事件
            {
                // 侦听sockct的可读事件,表明新的连接来到
                struct sockaddr_in clientaddr;
                socklen_t clientaddrlen = sizeof(clientaddr);
                // 接收客户端连接,生成新的对应客户端连接socket
                int clientfd = accept(listenfd,(struct sockaddr *)&clientaddr,&clientaddrlen);
                if(clientfd == -1)
                {
                    break;
                }
                // 只接收连接不调用recv收取数据
                std::cout << "accept a client connection, fd:" << clientfd << std::endl;
                clientfds.push_back(clientfd);
                // 记录最新的最大fd值作为下一轮循环中select的第一个参数
                if (clientfd > maxfd)
                {
                    maxfd = clientfd;
                }
            }
            else
            {
                // 做客户端的时候保证传送数据不超过63个字符
                char recvbuf[64];
                int clientfdslength = clientfds.size();
                for(int i=0; i<clientfdslength; ++i)
                {
                    if(clientfds[i] != INVALID_FD && FD_ISSET(clientfds[i],&readset))
                    {
                        memset(recvbuf,0,sizeof(recvbuf));
                        // 非侦听socket,则接收socket
                        int length = recv(clientfds[i],recvbuf,64,0);
                        if( length <= 0 && errno != EINTR)
                        {
                            std::cout << "recv data err,clientfd: " << clientfds[i] << std::endl;

                            close(clientfds[i]);
                            clientfds[i] = INVALID_FD;
                            continue;
                        }
                        std::cout << "clientfd:" << clientfds[i] << ", recv data:" << recvbuf << std::endl; 
                    }
                }
            }
            
        }

    }

    int clientlength = clientfds.size();
    for (int i=0; i< clientlength; ++i)
    {
        if(clientfds[i] != INVALID_FD)
        {
            close(clientfds[i]);
        }
    }
    
    close(listenfd);
    return 0;
}

但是select也有几个点需要注意:

1. 看代码在循环里面,会先进行清除readset的所有位以及设置readset的位fd这两步操作,为何不在循环外面做?这是因为select 函数调用前后会修改 readfds、writefds 和 exceptfds 这三个集合中的内容。所以清零再重新设置,就可以复用该位。

2. 在循环中也可以看出需要修改函数超时时间timeout的结构,这也是因为在select函数调用后也会将timeout结构体的内容重置。

该服务器代码实现功能:以重叠I/0模式接收客户端的连接,接收客户端发送的数据,并告知客户端已成功接收信息。以下是服务器端代码,请完善并完整输出服务器代码 及客户端代码: #define _WINSOCK_DEPRECATED_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS #include "stdafx.h" #include <WINSOCK2.H> #include <iostream> #pragma comment(lib,"WS2_32.lib") int _tmain(int argc, _TCHAR* argv[]) { // ------------------------------ // 声明和初始化变量 WSABUF DataBuf; // 发送和接收数据的缓冲区结构体 char buffer[DATA_BUFSIZE]; // 缓冲区结构体DataBuf中 DWORD EventTotal = 0, // 记录事件对象数组中的数据 RecvBytes = 0, // 接收的字节数 Flags = 0, // 标识位 BytesTransferred = 0; // 在读、写操作中实际传输的字节数 // 数组对象数组 WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS]; WSAOVERLAPPED AcceptOverlapped; // 重叠结构体 SOCKET ListenSocket, AcceptSocket; // 监听Socket和与客户端进行通信的Socket // ------------------------------ // 初始化Windows Sockets WSADATA wsaData; WSAStartup(MAKEWORD(2,2), &wsaData); // ------------------------------ // 创建监听Socket,并将其绑定到本地IP地址和端口 ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); u_short port = 9990; char* ip; sockaddr_in service; service.sin_family = AF_INET; service.sin_port = htons(port); hostent* thisHost; thisHost = gethostbyname(""); ip = inet_ntoa(*(struct in_addr *)thisHost->h_addr_list); service.sin_addr.s_addr = inet_addr(ip); bind(ListenSocket, (SOCKADDR *) &service, sizeof(SOCKADDR)); // ------------------------------ // 开始监听 listen(ListenSocket, 1); printf("Listening...\n"); // ------------------------------ // 接收连接请求 AcceptSocket = accept(ListenSocket, NULL, NULL); printf("Client Accepted...\n"); // 创建事件对象,建立重叠结构 EventArray[EventTotal] = WSACreateEvent(); ZeroMemory(buffer, DATA_BUFSIZE); ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));// 初始化重叠结构 AcceptOverlapped.hEvent = EventArray[EventTotal]; // 设置重叠结构中的hEvent字段 DataBuf.len = DATA_BUFSIZE; // 设置缓冲区 DataBuf.buf = buffer; EventTotal++; // 事件对象总数加1 // 处理在Socket上接收到的数据 while (1) { DWORD Index; // 保存处于授信状态的事件对象句柄 // ------------------------------ // 调用WSARecv()函数在AcceptSocket Socket上以重叠I/O方式接收数据 // 保存到DataBuf缓冲区中 if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL) == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) printf("Error occured at WSARecv()\n"); } // ------------------------------ // 等待完成的重叠 I/O 调用 Index = WSAWaitForMultipleEvents(EventTotal, EventArray, FALSE, WSA_INFINITE, FALSE); // ------------------------------ // 决定重叠事件的状态 WSAGetOverlappedResult(AcceptSocket, &AcceptOverlapped, &BytesTransferred, FALSE, &Flags); // ------------------------------ // 如果连接已经关闭,则关闭 AcceptSocket Socket if (BytesTransferred == 0) { printf("Closing Socket %d\n", AcceptSocket); closesocket(AcceptSocket); WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0]); return -1; } // ------------------------------ // 如果有数据到达,则将收到的数据发送回客户端 if (WSASend(AcceptSocket, &DataBuf, 1, &RecvBytes, Flags, &AcceptOverlapped, NULL) == SOCKET_ERROR) printf("WSASend() is busted\n"); // ------------------------------ // 重置已授信的事件对象 WSAResetEvent(EventArray[Index - WSA_WAIT_EVENT_0]); // ------------------------------ // 重置 Flags 变量和重叠结构 Flags = 0; ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); ZeroMemory(buffer, DATA_BUFSIZE); AcceptOverlapped.hEvent = EventArray[Index - WSA_WAIT_EVENT_0]; // ------------------------------ // 重置缓冲区 DataBuf.len = DATA_BUFSIZE; DataBuf.buf = buffer; } }
最新发布
12-24
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值