目录
IO多路复用模型中的Select是一种常用的同步IO模型,它允许单个线程监视多个文件句柄(在网络编程中,这些文件句柄通常是socket),并在一个或多个句柄就绪时进行相应的读写操作。以下是关于Select模型的详细解析:
一、基本概念
- IO多路复用:一种同步IO模型,通过单个线程监视多个文件句柄(如socket),当某个句柄就绪时,进行读写操作。它减少了系统开销,避免了为每个连接创建独立线程的需要。
- Select模型:IO多路复用的一种实现方式,通过select系统调用实现。
二、工作原理
- 初始化:创建一个文件描述符集合,用于存放需要监视的文件描述符(socket)。
- 监视:调用select函数,将文件描述符集合传递给内核,让内核监视这些文件描述符的状态变化(如可读、可写、异常)。
- 阻塞等待:select函数会阻塞当前线程,直到一个或多个文件描述符就绪,或者超时发生。
- 处理事件:当select函数返回时,检查文件描述符集合,找出哪些文件描述符就绪,并进行相应的读写操作。
三、select函数
select函数的原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 参数说明:
nfds
:待测试的文件描述符集合中最大文件描述符加1(因为文件描述符是从0开始的)。readfds
:指向可读文件描述符集合的指针。writefds
:指向可写文件描述符集合的指针。exceptfds
:指向异常条件文件描述符集合的指针。timeout
:指定等待的最长时间,如果设置为NULL,则永久等待。
- 返回值:返回就绪的文件描述符数量,如果超时则返回0,如果出错则返回-1。
四、select Qt示例
tcpserver.h
#ifndef TCPSERVER_H
#define TCPSERVER_H
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <list>
#include <map>
// //sock--包大小,缓冲区,偏移量
struct stru_pack{
int m_nPackSize;
char *m_pszbuf;
int m_noffset;
};
class TCPServer
{
public:
TCPServer();
~TCPServer();
public:
bool initNetWork(const char* szip ="127.0.0.1",short nport = 1234);
void unInitNetWork(const char *szerr = "");
bool sendData(SOCKET sock,const char* szbuf,int nlen);
void recvData();
public:
static DWORD WINAPI ThreadAccept(LPVOID lpvoid);
static DWORD WINAPI ThreadRecv(LPVOID lpvoid);
private:
SOCKET m_socklisten;
std::list<HANDLE> m_lstThread;
bool m_bFlagQuit;
public:
std::list<SOCKET> m_lstSocket;
fd_set m_fdsets;
std::map<SOCKET,stru_pack*> m_mapSocketToPack;
};
#endif // TCPSERVER_H
tcpserver.cpp
#include "tcpserver.h"
TCPServer::TCPServer()
{
m_socklisten = 0;
m_bFlagQuit = true;
FD_ZERO(&m_fdsets);
}
TCPServer::~TCPServer()
{
}
bool TCPServer::initNetWork(const char *szip, short nport)
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0) {
/* Tell the user that we could not find a usable */
/* Winsock DLL. */
printf("WSAStartup failed with error: %d\n", err);
return false;
}
/* Confirm that the WinSock DLL supports 2.2.*/
/* Note that if the DLL supports versions greater */
/* than 2.2 in addition to 2.2, it will still return */
/* 2.2 in wVersion since that is the version we */
/* requested. */
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
/* Tell the user that we could not find a usable */
/* WinSock DLL. */
unInitNetWork("Could not find a usable version of Winsock.dll\n");
return false;
}
else
printf("The Winsock 2.2 dll was found okay\n");
m_socklisten = socket(AF_INET,SOCK_STREAM,0);
if(INVALID_SOCKET == m_socklisten){
unInitNetWork("socket err\n");
return false;
}
sockaddr_in addrserver;
addrserver.sin_family = AF_INET;
addrserver.sin_port = htons(nport);
addrserver.sin_addr.s_addr = 0;
if(SOCKET_ERROR == bind(m_socklisten,(const struct sockaddr*)&addrserver,sizeof(addrserver))){
unInitNetWork("bind err\n");
return false;
}
if(SOCKET_ERROR == listen(m_socklisten,10)){
unInitNetWork("listen err\n");
return false;
}
//创建线程
HANDLE hThread = CreateThread(0,0,&ThreadAccept,this,0,0);
if(hThread)
m_lstThread.push_back(hThread);
//单线程轮询接收所有的客户端
hThread = CreateThread(0,0,&ThreadRecv,this,0,0);
if(hThread){
m_lstThread.push_back(hThread);
}
return true;
}
DWORD TCPServer::ThreadAccept(LPVOID lpvoid)
{
TCPServer *pthis = (TCPServer*)lpvoid;
sockaddr_in addrclient;
int nsize = sizeof(addrclient);
u_long iMode = 1;
while(pthis->m_bFlagQuit){
SOCKET sockWaiter = accept(pthis->m_socklisten,(struct sockaddr*)&addrclient,&nsize);
printf("client ip:%s nport:%d\n",inet_ntoa(addrclient.sin_addr),addrclient.sin_port);
//加入到集合内
FD_SET(sockWaiter,&pthis->m_fdsets);
//将sockWaiter设置为非阻塞
//ioctlsocket(sockWaiter, FIONBIO, &iMode);
//将sockWaiter加入链表中
// pthis->m_lstSocket.push_back(sockWaiter);
}
return 0;
}
DWORD TCPServer::ThreadRecv(LPVOID lpvoid)
{
TCPServer *pthis = (TCPServer*)lpvoid;
pthis->recvData();
return 0;
}
void TCPServer::recvData()
{
int nPackSize;
int nRecvNum;
char *pszbuf = NULL;
SOCKET sockWaiter;
fd_set fdtemp;
TIMEVAL tv;
tv.tv_sec =0;
tv.tv_usec = 100;
while(m_bFlagQuit){
//接收包大小
fdtemp = m_fdsets;
//将集合交给select查看
select(0,&fdtemp,0,0,&tv);
//校验是否发生网络事件
for(int i =0; i < m_fdsets.fd_count;i++){
if(FD_ISSET(m_fdsets.fd_array[i],&fdtemp)){
sockWaiter = m_fdsets.fd_array[i];
//sock--包大小,缓冲区,偏移量
stru_pack *p = m_mapSocketToPack[sockWaiter];
if(!p){
//第一次接收 --接收包大小
nRecvNum = recv(sockWaiter,(char*)&nPackSize,sizeof(int),0);
if(nRecvNum<=0){
//客户端下载
if(GetLastError() == 10054){
closesocket(sockWaiter);
//将sockwaiter 从fd_set集合移除
FD_CLR(sockWaiter,&m_fdsets);
}
continue;
}
p = new stru_pack;
p->m_nPackSize = nPackSize;
p->m_pszbuf = new char[nPackSize];
p->m_noffset = 0;
m_mapSocketToPack[sockWaiter] = p;
}else{
//第n次接收的包内容
nRecvNum = recv(sockWaiter,p->m_pszbuf+p->m_noffset,p->m_nPackSize,0);
p->m_noffset += nRecvNum;
p->m_nPackSize-=nRecvNum;
if(p->m_nPackSize ==0){
printf("%s\n",pszbuf);//处理数据
delete p;
m_mapSocketToPack[sockWaiter] = NULL;
}
}
}
}
// Sleep(100);
}
}
void TCPServer::unInitNetWork(const char *szerr )
{
m_bFlagQuit = false;
for (auto& ite:m_lstThread) {
if(WAIT_TIMEOUT == WaitForSingleObject(ite,100))
TerminateThread(ite,-1);
if(ite){
CloseHandle(ite);
ite = NULL;
}
}
m_lstThread.clear();
printf(szerr);
if(m_socklisten){
closesocket(m_socklisten);
m_socklisten = 0;
}
WSACleanup();
}
bool TCPServer::sendData(SOCKET sock, const char *szbuf, int nlen)
{
if(!szbuf || nlen <=0)
return false;
//粘包
//发送包大小
if(send(sock,(char*)&nlen,sizeof(int),0)<=0)
return false;
//发送包内容
if(send(sock,szbuf,nlen,0)<=0)
return false;
return true;
}
main.cpp
#include <QCoreApplication>
#include "network/tcpserver.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
TCPServer ts;
if(ts.initNetWork())
printf("server is running......\n");
else
printf("server err\n");
return a.exec();
}
五、select模型的优缺点
优点:
- 单线程处理多个连接:减少了线程创建和切换的开销。
- 跨平台:几乎支持所有平台。
缺点:
- 文件描述符限制:在32位系统上,单个select调用能够监视的文件描述符数量有限(通常是1024个),虽然可以通过修改宏定义来扩展,但扩展后性能会下降。
- 性能问题:每次调用select都需要遍历整个文件描述符集合,随着文件描述符数量的增加,性能会下降。
- 数据拷贝:在select返回后,需要将就绪的文件描述符从内核空间拷贝到用户空间,增加了开销。
六、应用场景
Select模型适用于连接数不是特别多(不超过文件描述符限制)的场景。对于需要处理大量连接的应用,可能需要考虑使用poll或epoll等更高效的IO多路复用模型。
七、总结
Select是IO多路复用模型的一种实现方式,它通过单个线程监视多个文件描述符的状态变化,并在就绪时进行相应的读写操作。虽然存在文件描述符限制和性能问题,但在连接数不是特别多的情况下,它仍然是一种有效的解决方案。