IO複用,即IO multiplexing,是使用select/poll/epoll這一系列的多路選擇器,可使進程或線程能夠處理多個連接;實質上不是複用IO,而是多個IO複用進程或線程;
編程模型
(1)單進程IO複用服務器,它適合IO密集型的應用,不適合計算密集性的應用,因爲計算需要耗費CPU資源,使得其他IO連接無法得到服務;網絡延遲也可能會大一些,本來進行一次read系統調用可以獲得數據,現在需要一次poll和一次read才能獲得數據;
(2)IO複用服務器+多線程服務器,它(1)的基礎上,收到的計算請求不在主進程中進行處理,而是創建新的線程中進行處理;如果爲一個連接上的多個請求而創建多個線程處理,會有次序的問題,多個請求會分發給多個線程去計算,這樣就會佔滿全部的CPU;而且線程的創建數目是受限的,因此不宜創建過多的請求;
(3)在(2)的基礎上,同一個連接上的計算請求都發往同一個線程中去計算;但是這樣併發的連接數將受限於線程的數目;與(2)不同的是,一個含有很多請求的連接,只會固定的在同一個線程進行處理,只會佔用一個CPU,讓其它CPU可以用於其他連接的請求處理;
(4)IO複用服務器+計算線程池;將IO全部工作都放在一個線程中處理,其他的計算任務交給線程池;線程池的另一個作用可以執行阻塞操作,因爲有的操作可能沒有對應的非阻塞操作,或必須需要等待同步;
(5)在(4)的基礎上,IO複用只在一個線程中來處理,可以換成在多個線程進行IO處理,有一個主線程accept後分發連接給各個IO處理線程,小規模計算可以在當前線程中進行,否則放入線程池中去計算;
(6)在(1)的基礎上,使用多進程進行IO複用,各個工作進程相互獨立,便於升級;
特點
(1)IO複用幾乎要使用非阻塞IO,否則一旦線程在處理具體邏輯時阻塞在某系統調用,如read,那麼該線程的其他連接即使可讀或可寫,也無法得到服務;IO複用既然使用的是非阻塞IO,那麼不能保證每次讀寫都可以完全,那麼需要應用層緩衝;
(2)IO複用使得一個進程或線程,幾乎不處於阻塞狀態,高效利用CPU資源,避免爲每一個連接創建進程或線程,從而引起資源緊張,進程切換等不必要的開銷;
(3)IO複用可以處理鍵盤輸入和socket可讀的情況,可以簡化程序編寫的邏輯;
實現內容
(1)本節實現的網絡模型爲單進程IO複用服務器;
(2)實現的內容是一個echo服務器,由客戶端從鍵盤輸入相關內容,發送給服務器,然後由服務器收到後轉發至客戶端,客戶端打印至終端;
(3)服務器不主動斷開連接,而由客戶端從鍵盤獲得EOF或客戶端退出後,將會發送Reset報文至服務器,服務器也將會退出;
TcpServer服務端實現
TcpServer接口
class TcpServer final
{
public:
TcpServer(const TcpServer&) = delete;
TcpServer& operator=(const TcpServer&) = delete;
explicit TcpServer(const struct sockaddr_in& serverAddr);
~TcpServer();
void start();
static void signalHandle(int sig);
private:
void _serviceRead(int connfd, size_t i);
void _serviceWrite(int connfd, size_t i);
void _closeConnection(size_t& index);
void _newConnection();
const int _listenfd;
const struct sockaddr_in _serverAddress;
bool _started;
unsigned int _usercount;
std::vector<struct pollfd> _pollfds;
std::map<int, std::vector<char>> _buffers;
};
說明幾點:(1)TcpServer不具有值語義,具有對象語義,拷貝一個TcpSever並不能讓系統多一個一模一樣的TcpServer主進程服務器(包括所管理的孩子進程),假設拷貝了一份TcpServer,那麼系統中僅僅是多了對TcpServer本身數據的拷貝,而主進程和孩子進程仍然是不變的,因此對象拷貝是禁止的;繼承也是被禁止的,因爲TcpServer不具有多態語意,實在需要複用時,使用組合的方式;分別使用C++11新特性中delete和final來禁止拷貝和繼承;
(2)_listenfd作爲監聽描述符;_serverAddress作爲綁定的服務器端地址,需要在TcpServer顯式指定;_usercount表示當前poll需要IO複用的文件描述符號個數;
(3)_pollfds存放各文件描述符需要對應pollfd結構的集合;_buffers存放每一個連接描述符的對應的應用層緩衝;
(4)_serviceRead()對應連接讀可訪問的處理,而_serviceWrite()對應寫可訪問的處理;
(5)_newConnection()對應新連接到來的處理,_closeConnection()對應連接關閉;
構造函數
TcpServer::TcpServer(const struct sockaddr_in& serverAddr):
_listenfd(sockets::createBlockingSocket()),
_serverAddress(serverAddr),
_started(false),
_usercount(0)
{
assert(_listenfd >= 0);
sockets::setReuseAddr(_listenfd);
sockets::signal(SIGCHLD, &TcpServer::signalHandle);
_pollfds.resize(100);
}
說明幾點:
(1)初始化_listenfd監聽描述符,綁定_serverAddress服務器地址後;將服務器地址設置爲可重用,這樣bind一個正處於TIME_WAIT狀態上的連接會失敗,因此使用setReuseAddr()設置socket的SO_REUSEADDR選項;
(2) sockets::signal(SIGCHLD, &TcpServer::signalHandle);設置SIGCHLD信號處理程序;其中TcpServer::signalHandle爲靜態成員函數,這樣才能保證與普通函數具有相同的語義;
(3)_pollfds首先默認初始化爲100,當_usercount大於該大小時,將使用resize()以兩倍的速度增長;
服務器啓動
void TcpServer::start()
{
assert(!_started);
sockets::bind(_listenfd, _serverAddress);
sockets::listen(_listenfd);
_pollfds[0].fd = _listenfd;
_pollfds[0].events = POLLIN | POLLERR;
_pollfds[0].revents = 0;
printf("TcpServer start...\n");
++_usercount;
_started = true;
while (_started)
{
printf("poll wait...\n");
int ret = ::poll(&(*_pollfds.begin()), _usercount, -1);
if (ret < 0)
{
printf("failed in TcpServer::start, poll error :%s\n", strerror_r(errno, g_errorbuf, sizeof g_errorbuf));
continue;
}
for (size_t i = 0; i < _usercount; ++i)
{
if (_pollfds[i].fd == _listenfd && _pollfds[i].revents & POLLIN)
{
printf("server new conn\n");
_newConnection();
}
else if (_pollfds[i].revents & POLLERR)
{
int err = sockets::socketError(_pollfds[i].fd);
printf("socket fd[%d] error: %s\n", _pollfds[i].fd, strerror_r(err, g_errorbuf, sizeof g_errorbuf));
}
else if (_pollfds[i].revents & POLLRDHUP)
{
printf("a client fd[%d] conenction down\n", _pollfds[i].fd);
_closeConnection(i);
}
else if (_pollfds[i].revents & POLLIN)
{
printf("server conn can read\n");
int conn = _pollfds[i].fd;
_serviceRead(conn, i);
}
else if (_pollfds[i].revents & POLLOUT)
{
printf("server conn can write\n");
int conn = _pollfds[i].fd;
_serviceWrite(conn, i);
}
}
}
}
說明幾點:(1)首先構造監聽描述符 _pollfds[0].fd = _listenfd;_pollfds[0].events = POLLIN | POLLERR;用於可讀以及錯誤的監聽,不需要用於寫;
(2)當poll返回時,說明有相應的描述符已經準備好了;對於監聽描述符判斷,if (_pollfds[i].fd == _listenfd && _pollfds[i].revents & POLLIN)說明有新的連接到來,具體處理函數見下文;
(3)對於出錯的描述符,if (_pollfds[i].revents & POLLERR),使用getsocketopt獲取對應的出錯碼,並輸出;
(4)對於if (_pollfds[i].revents & POLLRDHUP),說明客戶端斷開了連接,此時觸發服務器斷開連接;具體處理函數見下文:
(5)對於連接描述符可讀,if (_pollfds[i].revents & POLLIN),說明客戶端發送的數據已經在Tcp的內核接收緩衝區,具體處理函數見下文;對於連接描述符可寫,if (_pollfds[i].revents & POLLOUT),說明Tcp的內核發送緩衝區可以讓我們存放對應的發送給客戶端的數據,具體處理函數見下文;(注,一般應用層要設置發送緩衝區和接收緩衝區,但是本程序爲了簡化相關邏輯,只使用一個緩衝區,讀寫只能互斥的訪問這個緩衝區,即緩衝區爲空時,連接可以寫入數據至緩衝區,否則要等到緩衝區的數據發送完爲空);
新連接處理
void TcpServer::_newConnection()
{
int connfd = sockets::accept(_listenfd, NULL); //so far, we will not concern client address
if (connfd >= 0)
{
if (_usercount == _pollfds.size())
_pollfds.resize(2 * _pollfds.size());
_pollfds[_usercount].fd = connfd;
_pollfds[_usercount].events = POLLIN | POLLERR | POLLRDHUP;
_pollfds[_usercount].revents = 0;
++_usercount;
sockets::setNonBlockingFd(connfd);
_buffers[connfd] = std::vector<char>(20);
}
else
{
printf("In TcpServer::_newConnection, accept error : %s\n", strerror_r(errno, g_errorbuf, sizeof g_errorbuf));
}
}
說明幾點:
(1)使用accept獲取新連接,將獲取的新連接,如果此時_pollfds已經放滿,那麼就擴大_pollfds的空間;
(2)設置對應的連接描述符的相關值, _pollfds[_usercount].events = POLLIN | POLLERR | POLLRDHUP;表明該連接是不關注寫可用的;
(3)並使用sockets::setNonBlockingFd(connfd);將該連接描述符connfd設置成非阻塞方式,加入到_pollfds中;將每一個新到連接的緩衝區設置成20個字節;
連接關閉
-
void TcpServer::_closeConnection(size_t& i) { sockets::close(_pollfds[i].fd); std::map<int, std::vector<char>>::iterator iter = _buffers.find(_pollfds[i].fd); if (iter != _buffers.end()) { _buffers.erase(iter); } _pollfds[i] = _pollfds[_usercount - 1]; --i; --_usercount; } <span style="font-size: 11.9999990463257px; line-height: 13.1999998092651px; font-family: Consolas, 'Courier New', Courier, mono, serif; background-color: inherit;"> </span>
說明幾點:
(1)使用close關閉對應的連接,並將該連接在map中對應映射的緩衝區清除;
(2)將該連接在_pollfds與最後一個連接的pollfd相對換,並遞減_usercount;
void TcpServer::_serviceRead(int connfd, size_t i)
{
char buf[20];
int n = sockets::read(connfd, buf, sizeof buf);
if (n <= 0)
return;
std::vector<char>& buffer = _buffers[connfd];
buffer.resize(n);
std::copy(buf, buf + n, buffer.begin());
_pollfds[i].events &= ~POLLIN;
_pollfds[i].events |= POLLOUT;
}
說明幾點:
(1)使用read先將客戶端發來的數據讀入緩衝中,並將該緩衝中的值拷貝到該連接所對應的緩衝區,必須使用引用到對應的連接緩衝區;
(2)由於該緩衝獨佔訪問,需要關閉讀關注,打開寫關注;否則若不關閉讀關注,那麼讀入的數據可能會擾亂對應的發送數據;
(3)爲了效率,其實可以不關閉讀關注,將讀寫訪問同步,每次將socket數據讀入到緩衝的尾部,但是這樣會造成整個緩衝區過大,而且寫指針指向的緩衝區的前部過大,可通過循環緩衝來解決;
寫服務
void TcpServer::_serviceWrite(int connfd, size_t i)
{
std::vector<char>& buffer = _buffers[connfd];
int n = sockets::write(connfd, &(*buffer.begin()), buffer.size());
if (n == static_cast<int>(buffer.size()))
{
_pollfds[i].events |= POLLIN;
_pollfds[i].events &= ~POLLOUT;
}
else if (n >= 0)
{
std::copy(buffer.begin() + n, buffer.end(), buffer.begin());
buffer.resize(buffer.size() - n);
}
else if (n < 0)
{
printf("In TcpServer::_serviceWrite, write error : %s\n", strerror_r(errno, g_errorbuf, sizeof g_errorbuf));
_pollfds[i].events |= POLLIN;
_pollfds[i].events &= ~POLLOUT;
}
}
說明幾點:
(1)使用write將該連接緩衝區(必須使用引用)的數據拷貝到對應的該連接的內核發送緩衝區,有可能不能完全寫入;
(2)若完全寫入,則關閉寫關注,打開讀關注;否則繼續關注寫,並移動剩餘的數據至緩衝區頭;若出錯,那就關閉寫關注,打開讀關注,因爲有可能還有讀數據需要處理;
(3)爲了效率,其實可以不關閉讀關注,將讀寫訪問同步,每次將socket數據讀入到緩衝的尾部,但是這樣會造成整個緩衝區過大,而且寫指針指向的緩衝區的前部過大,可通過循環緩衝來解決;
TcpClient客戶端的實現
與一個客戶一個進程的TcpClient客戶端一樣,不再贅述;