深入linux网络编程(二):异步阻塞IO —— select

作者:[email protected] 新浪微博@孙雨润 新浪博客 CSDN博客日期:2012年11月17日

1. 异步阻塞IO

当从一个描述符读,写到另一个描述符时,可以在下列形式的循环中使用阻塞IO:

while ((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0)
    if (write(STDOUT_FILENO, buf, n) != n) 
        err_sys("write error");

这种形式的阻塞IO随处可见,但如果必须从两个描述符读呢?这种方式就可能导致长时间阻塞在其中一个描述符上,而另一个描述符虽然有很多数据却不能及时处理。

一种直观的解决方法是这样的:将两个描述符都设置为非阻塞,对第一个描述符发送read,如果有数据则读取并处理,如果没有则立即返回,对第二个描述符做同样处理。此后等待若干秒,再度去第一个描述符。这种形式的循环称为轮询(polling),缺点非常明显:大部分时间是无数据可读的,但是仍然花费时间反复执行read

异步阻塞IO基于这样一个想法:先构建一张有关描述符的列表,然后使用统一的阻塞函数,当任何socket有事件通知时跳出阻塞状态。

深入linux网络编程(二):异步阻塞IO —— select_第1张图片

2. select

select就是这样一个统一的阻塞函数。

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

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);

select()可以监视多个文件描述符,直到其中的一个或一些文件描述符的IO操作准备就绪,所谓就绪是指IO操作的请求不会被阻塞。

2.1 timeout参数

这个参数表示愿意阻塞等待的时间。

  • tvptr == NULL

    永远等待,如果捕捉到一个信号则中断,当所指定的描述符中的一个或多个已经准备好或捕捉到一个信号返回,后者返回-1,errno为EINTR

  • tvptr->tv_sec == 0 && tvptr->tv_usec == 0

    完全不等待,退化为轮询polling

  • tvptr->tv_sec != 0 || tvptr->tv_usec != 0

    等待指定时间,当制定的描述符之一已经准备好,或指定的时间值已经超过则返回,后者返回0。与第一种情况一样,可以捕捉信号。

2.2 readfds writefds exceptfds

这Sanger参数是指向描述符集的指针,说明了我们关心的可读可写或处于一场条件的各个描述符。这个描述符集使用位图的方式存储每个fd的状态。

这三个参数任意一个或者全部都可以是空指针,表示对相应状态不关心。如果都是空指针,则select提供了比sleep更精确的计时器。

2.3 FD_CLR FD_ISSET FD_SET FD_ZERO

这些宏或者参数负责对描述符集的结构做一些操作:清除、测试、置位、全部清除。

2.4 返回值

  • -1:表示出错,不修改其中任何一个描述符

  • 0: 描述符没有准备好,且超时

  • 正数:表示已经准备好的描述符个数,是三个描述符集中已准备好的描述符之和。

2.5 区别select的阻塞与文件描述符的阻塞

注意一个描述符阻塞与否并不影响select是否阻塞,

3. 完整C++代码示例

网上有很多select的源码,不过不是无法编译,就是以C语言方式全部写在main函数中,看得眼花缭乱,还经常看到循环层层嵌套。这里本人以C++的方式实现了一个完全正确的、可读性好的版本。希望能够帮助大家快速理清select的思路和用法。

猛击此处下载源码。

3.1 stdafx.h

预编译头,使头文件整洁,同时提高编译速度。

#ifndef STDAFX_H
#define STDAFX_H

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#endif // STDAFX_H

3.2 select.h

#ifndef SELECT_H
#define SELECT_H

#include <vector>

class SelectServer
{
public:
    SelectServer();
    bool init();
    bool _listen(uint32_t port);
    bool pulse();
    void broadcast(const std::string& data);

protected:
    /// Callback functions to be override
    virtual void onNewConnection(const sockaddr_in& clientAddr) {(void)clientAddr;}
    virtual void onClientData(int clientFd, const std::string& data){(void)clientFd; (void)data;}
    virtual void onClientBroken(int errCode) {(void)errCode;}

private:
    bool _accept();
    bool _receive(int clientFd);
    bool removeClient(int clientFd);

    fd_set m_masterFdSet;                       /** master file descriptor list */
    fd_set m_readFdSet;                         /** temp file descriptor list for select() */
    sockaddr_in m_serverAddr;                   /** server address */
    int m_maxFd;                                /** maximum file descriptor number */
    int m_listenerSocket;                       /** listening socket descriptor */
    char buf[1024];                             /** buffer for client data */
};

#endif // SELECT_H

3.3 select.cpp

#include "stdafx.h"
#include "selectserver.h"

using namespace std;

SelectServer::SelectServer()
{
    FD_ZERO(&m_masterFdSet);
    FD_ZERO(&m_readFdSet);
}

bool SelectServer::init()
{
    /** get the listener */
    if((m_listenerSocket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        cerr << "Server-socket() error, fail to get the listener Socket" << endl;
        return false;
    }
    /** "address already in use" error message */
    uint32_t yes = 0;
    if(setsockopt(m_listenerSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) {
        cerr << "Server-setsockopt() error, address already in use!" << endl;
        return false;
    }
    return true;
}

bool SelectServer::_listen(uint32_t port)
{
    m_serverAddr.sin_family = AF_INET;
    m_serverAddr.sin_addr.s_addr = INADDR_ANY;
    m_serverAddr.sin_port = htons(port);
    memset(&(m_serverAddr.sin_zero), '\0', 8);

    if(bind(m_listenerSocket, (struct sockaddr *)&m_serverAddr, sizeof(m_serverAddr)) == -1){
        cerr << "Server-bind() error lol!" << endl;
        return false;
    }
    if(listen(m_listenerSocket, 10) == -1) {
        cerr << "Server-listen() error lol!" << endl;
        return false;
    }

    FD_SET(m_listenerSocket, &m_masterFdSet);      /** add the listener to the master set */
    m_maxFd = m_listenerSocket; /** keep track of the biggest file descriptor, so far, it's this one*/
    return true;
}

bool SelectServer::_accept()
{
    sockaddr_in clientAddr;
    uint32_t clientAddrLen = sizeof (clientAddr);
    int newClientFd = accept(m_listenerSocket, (struct sockaddr *)&clientAddr, (socklen_t*)&clientAddrLen);
    cout << "_accept called...newClientFd is " << newClientFd << endl;
    if (newClientFd == -1) {
        cerr << "Server-accept() error lol!" << endl;
        return false;
    }
    FD_SET(newClientFd, &m_masterFdSet);            /** add to master set */
    m_maxFd = (newClientFd > m_maxFd) ? newClientFd : m_maxFd;  /** keep track of the maximum */
    onNewConnection(clientAddr);
    return true;
}

bool SelectServer::_receive(int clientFd)
{
    uint32_t nbytes = recv(clientFd, buf, sizeof(buf), 0);
    cout << "_receive called...nbytes is " << nbytes << " buf is " << buf << endl;
    string data(buf, nbytes);
    onClientData(clientFd, data);
    if (nbytes > 0) {   /** we got some data from a client*/
        broadcast(data);
    } else {
        cout << "socket " << clientFd << " has sth wrong since nbytes == " << nbytes  << endl;
        removeClient(clientFd);
    }
    return true;
}

bool SelectServer::removeClient(int clientFd)
{
    onClientBroken(1);
    close(clientFd);
    FD_CLR(clientFd, &m_masterFdSet);
    return true;
}

bool SelectServer::pulse()
{
    m_readFdSet = m_masterFdSet;
    if(select(m_maxFd + 1, &m_readFdSet, NULL, NULL, NULL) == -1) {
        cerr << "Server-select() error lol!" << endl;
        return false;
    }

    /** run through the existing connections looking for data to be read*/
    for(int i = 0; i <= m_maxFd; ++i) {
        if(FD_ISSET(i, &m_readFdSet)) { /** we got one... */
            if(i == m_listenerSocket) {
                return _accept();
            } else {
                return _receive(i);
            }
        }
    }
    return true;
}

void SelectServer::broadcast(const string &data)
{
    for(int i = 0; i <= m_maxFd; i++) {
        /** send to everyone except the listener and ourselves */
        if(FD_ISSET(i, &m_masterFdSet) && (i != m_listenerSocket)) {
            if(send(i, data.c_str(), data.size(), 0) == -1) {
                cerr << "send() to " << i << " error lol!" << endl;
            }
        }
    }
}

3.4 main.cpp

#include "stdafx.h"
#include "selectserver.h"

using namespace std;

int main(int argc, char *argv[])
{
    uint16_t port = 2020;
    if (argc >= 2) {
        port = atoi(argv[1]);
    }

    SelectServer server;
    do {
        if (!server.init()) {
            cout << "server.init() fail" << endl;
        }
        cout << "server.init() ok" << endl;
        if (!server._listen(port)) {
            cout << "server._listen() fail" << endl;
        }
        cout << "server._listen() ok on " << port << endl;
        while (server.pulse()) {
            usleep(1000);
        }
    } while (false);

    return 0;
}

3.5 qmake工程文件select.pro

######################################################################
# Automatically generated by qmake (2.01a) Fri Nov 16 18:01:16 2012
######################################################################

TEMPLATE = app
TARGET = 
DEPENDPATH += .
INCLUDEPATH += .
CONFIG -= qt
PRECOMPILED_HEADER += stdafx.h

# Input
SOURCES += main.cpp selectserver.cpp

HEADERS += selectserver.h
  • 如果这篇文章对您有帮助,请到CSDN博客留言;
  • 转载请注明:来自雨润的技术博客 http://blog.csdn.net/sunyurun

你可能感兴趣的:(C++,linux,socket,网络,select)