【Linux】IO复用之select系统调用

select系统调用

函数原型解释

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
  • nfds:需要监控的文件描述符中最大的文件描述符值 + 1
  • readfds:可读事件对应的文件描述符集合
  • writefds:可写事件对应的文件描述符集合
  • exceptfds:异常事件对应的文件描述符集合
  • timeout:超时时间
  • 返回值:就绪(可读、可写和异常)文件描述符的总数

事件集合结构体 fd_set

fd_set内核源码:

typedef struct {
	unsigned long fds_bits [1024 / (8 * sizeof(unsigned long))];
} __kernel_fd_set;
typedef __kernel_fd_set		fd_set;

由内核源码可以看出,在64位Linux操作系统下fd_set实际上是一个unsigned long fds_bits [16]位图,该位图一共有1024个位,位上如果是1表示该号的文件描述符是需要监控的,位上如果是0表示该号的文件描述符是不需要监控的,每一位标记一个文件描述符

在这里插入图片描述

对于位图上面的每一位,系统需要逐位判断该位是否为1,可以根据第一个参数nfds来设定遍历上限,这样可以减少遍历次数

操作 fd_set 的接口

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
  • FD_CLR:将 set 位图上的第 fd 位置为 0
  • FD_ISSET:判断 set 位图上的第 fd 位是否为 1
  • FD_SET:将 set 位图上的第 fd 位置为 1
  • FD_ZERO:将 set 位图每一位都置为 0

fd_set 接口实现源码

#define	__FDMASK(d)	(1UL << ((d) % (8 * sizeof(unsigned long))))
#define	__FDELT(d)	((d) / (8 * sizeof(unsigned long)))

#define	__FD_SET(d, set)	((set)->fds_bits[__FDELT(d)] |= __FDMASK(d))
#define	__FD_CLR(d, set)	((set)->fds_bits[__FDELT(d)] &= ~__FDMASK(d))
#define	__FD_ISSET(d, set)	(((set)->fds_bits[__FDELT(d)] & __FDMASK(d)) != 0)
#define	__FD_ZERO(set)	\
  ((void) memset ((void *) (set), 0, sizeof (__kernel_fd_set)))

前三个接口由源码可知是通过将1进行移d个位后与原位图进行按位操作来实现的

最后一个接口由源码可知是通过使用memset方式将整个位图的二进制位置0的

超时时间结构体 timeval

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

若该参数传递NULL,表示阻塞监控,直到某个文件描述符就绪

若该参数传递0,表示非阻塞监控,select立即返回,即tv_sec=0 && tv_usec=0

若该参数传递其他,表示带有超时时间的监控,当超时时间达到还没有某个文件描述符就绪,select返回,如果在超时时间中有文件描述符就绪,

select使用解须知

当select运行完毕后,会将传入的三个事件集合参数中未就绪的文件描述符置为0,如果你再想监控原来的文件描述符就需要手动给再次加上,这样会导致非常的麻烦,我们可以传递一个拷贝文件描述符集合的临时变量进去,原位图不让select修改即可,下图中的问题便解决了

在这里插入图片描述

代码测试 select

select监控 0 号文件描述符

#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>

int main()
{
    fd_set readfds;
    FD_ZERO(&readfds);		// 清空位图
    FD_SET(0, &readfds);	// 将 0 号置为 1
    int ret = select(1, &readfds, NULL, NULL, NULL);
    if(ret < 0)
    {
        perror("select");
        return 1;
    }
    else printf("select ret : %d\n", ret);

    char buf[1024] = {0};
    read(0, buf, sizeof(buf));
    printf("buf:%s\n", buf);

    return 0;
}

输出:

gcc test.cpp && ./a.out
hello world!
select ret : 1
buf:hello world!

select监视新连接

#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main()
{
  // 1. 创建监听套接字
  int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
  if(listen_sockfd < 0)
  {
    perror("socket");
    return 0;
  }
  // 2. 绑定地址信息:IP 端口 协议
  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_port = htons(19999);
  addr.sin_addr.s_addr = inet_addr("0.0.0.0");
  int ret = bind(listen_sockfd, (struct sockaddr*)&addr, sizeof(addr));
  if(ret < 0)                                                            
  {
    perror("bind");
    return 0;
  }
  // 3. 监听
  listen(listen_sockfd, 2);
  // 4. 创建一个select监视位图,监控"监听套接字文件描述符"
  fd_set readfds;
  FD_ZERO(&readfds);
  FD_SET(listen_sockfd, &readfds);
  while(1)
  {
    // 设置select监听位图属性:非阻塞监控
    struct timeval tv;
    tv.tv_sec  = 2;
    tv.tv_usec = 0;
    fd_set tmp = readfds;
    int ret    = select(listen_sockfd + 1, &tmp, NULL, NULL, &tv);
    if(ret < 0)
    {
      perror("select");
      return 0;
    }
    else if(ret == 1 &&  FD_ISSET(listen_sockfd, &tmp) == 1)
    {
      // 来了一个新连接
      int new_sock = accept(listen_sockfd, NULL, NULL);
      if(new_sock < 0)
      {
        perror("accept");
        continue;
      }
      printf("accept a connection : sockfd[%d]\n", new_sock);
    }
    else printf("No connections\n");
  } // end while 

  return 0;
}

输出现象:使用其他电脑telnet ip port即可产生以下现象

[gongruiyang@localhost select]$ gcc seledemo.c -o main && ./main
No connections
accept a connection : sockfd[4]
No connections
No connections
No connections
accept a connection : sockfd[5]
No connections
^C

每来一个telnet连接都会为该连接分配一个文件描述符

封装select类和TCP类

select_svr.hpp

#pragma once 
#include <sys/select.h>
#include <unistd.h>
#include <iostream>
#include <sys/socket.h>
#include <algorithm>
#include <vector>
#include "tcp_server.hpp"

using namespace std;

#define SELECT_ERR (-1)
#define TIMEOUT (-2)                                                

class SelectSvr{
    public:
    SelectSvr()
    {
        FD_ZERO(&readfds_);
        nfds_ = -1;
    }
    ~SelectSvr(){}

    // 添加文件描述符
    void addFD(int fd)
    {
        FD_SET(fd, &readfds_);
        nfds_ = max(nfds_, fd);
    }

    // 删除文件描述符
    void delFD(int fd)
    {
        FD_CLR(fd, &readfds_);
        if(fd < nfds_)
            return ;
        for(int i = nfds_; i >= 0; i--)
        {
            if(FD_ISSET(i, &readfds_) == 1)
            {
                nfds_ = i;
                break;
            }
        }
    }

    // 监控
    int selectWait(struct timeval* tv, vector<TcpSvr>& vec)
    {
        fd_set tmp = readfds_;
        int ret = select(nfds_ + 1, &tmp, NULL, NULL, tv);
        if(ret < 0)
        {
            perror("select");
            return SELECT_ERR;
        }
        else if(ret == 0)
        {
            cout << "Time Out ..." << endl;
            return TIMEOUT;
        }

        for(int i = 0; i <= nfds_; i++)
        {
            if(FD_ISSET(i, &readfds_) == 1)
            {
                TcpSvr ts;
                ts.setSocket(i);
                vec.push_back(ts);
            }
        }
        return ret;
    }
    private:
    // 读事件集合
    fd_set readfds_;
    // 轮询范围:最大文件描述符值
    int nfds_;
};

tcp_server.hpp

#pragma once          
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <string.h>

using namespace std;

#define TCPSUCCESS 0
#define TCPERROR (-1)
#define SOCKETINIT (-2)
#define PEERSHUTDOWN (-3)

class TcpSvr{
    public:
    TcpSvr()
    {
        socket_fd = SOCKETINIT;;
    }

    ~TcpSvr(){}

    int CreateSocket()
    {
        socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(socket_fd < 0)
        {
            perror("socket");
            return TCPERROR;
        }
        return TCPSUCCESS;
    }

    int Bind(const string& ip, const uint16_t port)
    {
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        int ret = bind(socket_fd, (struct sockaddr*)&addr, sizeof(addr));
        if(ret < 0)
        {
            perror("bind");
            return TCPERROR;
        }
        return TCPSUCCESS;
    }

    int Listen(int backlog = 5)
    {
        int ret = listen(socket_fd, backlog);
        if(ret < 0)
        {
            perror("listen");
            return TCPERROR;
        }
        return TCPSUCCESS;
    }

    int Accept(TcpSvr* ts, struct sockaddr_in* peer_addr, socklen_t* peer_addrlen)
    {
        int new_socket = accept(socket_fd, (struct sockaddr*)peer_addr, peer_addrlen);
        if(new_socket < 0)
        {
            perror("accept");
            return TCPERROR;
        }
        ts->setSocket(new_socket);
        return TCPSUCCESS;
    }

    int Send(const string& inputData)
    {
        ssize_t ret = send(socket_fd, inputData.c_str(), inputData.size(), 0);
        if(ret < 0)
        {
            perror("send");
            return TCPERROR;
        }
        return TCPSUCCESS;
    }

    int Recv(string& outputData)
    {
        char buf[1024];
        ssize_t ret = recv(socket_fd, buf, sizeof(buf) - 1, 0);
        if(ret < 0)
        {
            perror("recv");
            return TCPERROR;
        }
        else if(ret == 0)
            return PEERSHUTDOWN;
        outputData.assign(buf, strlen(buf));
        return TCPSUCCESS;
    }

    int getSockFd()
    {
        return socket_fd;
    }

    void setSocket(int sockfd)
    {
        socket_fd = sockfd;
    }
    private:
    int socket_fd;
};

main.cpp

#include "tcp_server.hpp"                                                                 
#include "select_svr.hpp"
#define CHECK(val) if(val==TCPERROR){return -1;}
using namespace std;
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        cout << "usage : ./tcpsvr [ip] [port]" << endl;
        return -1;
    }

    // 创建监听服务器类实例
    TcpSvr ts;
    string ip = argv[1];
    u_int16_t port = atoi(argv[2]);
    CHECK(ts.CreateSocket());
    CHECK(ts.Bind(ip, port));
    CHECK(ts.Listen());

    // 创建监控类类实例
    SelectSvr selectSvr;

    // 监控服务类实例
    selectSvr.addFD(ts.getSockFd());

    while(1)
    {
        struct timeval tv;
        tv.tv_sec  = 2;
        tv.tv_usec = 0;
        vector<TcpSvr> vec;
        vec.clear();
        int ret = selectSvr.selectWait(&tv, vec);

        if(ret == TIMEOUT)
            continue;
        else if(ret == SELECT_ERR)
            return SELECT_ERR;

        for(size_t i = 0; i < vec.size(); i++)
        {
            if(vec[i].getSockFd() == ts.getSockFd())
            {
                // 监听套接字已经就绪,可以接受新连接了
                TcpSvr new_ts;
                struct sockaddr_in peer_addr;
                socklen_t peer_addrlen = sizeof(peer_addr);
                ts.Accept(&new_ts, &peer_addr, &peer_addrlen);
                cout << "new socket is "  << new_ts.getSockFd()             << endl;
                cout << "peer ip is"      << inet_ntoa(peer_addr.sin_addr)  << endl;
                cout << "peer port is "   << ntohs(peer_addr.sin_port)      << endl;
                selectSvr.addFD(new_ts.getSockFd());  // 将新连接的fd放入监控位图中
            }
            else
            {
                // 取出新连接的套接字
                TcpSvr newConTcoSvr = vec[i];
                // 接受数据
                string dataFromPeer;
                dataFromPeer.clear();
                int ret = newConTcoSvr.Recv(dataFromPeer);
                if(ret == TCPERROR || ret == PEERSHUTDOWN)
                {
                    // 释放文件描述符 + 从监控中移除该文件描述符
                    int newConFD = newConTcoSvr.getSockFd();
                    close(newConFD);
                    selectSvr.delFD(newConFD);
                    continue;
                }
                cout << "peer data : " << dataFromPeer << endl;

                string dataResp;
                cin >> dataResp;
                newConTcoSvr.Send(dataResp);
            }
        }
    }

    close(ts.getSockFd());
    return 0;
}
  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值