多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接。
通过三个例子,来举例:
例一例二是在windows平台下的,例三兼容多平台。
1、无select的socket编程
#define FD_SETSIZE 1024
#include <WinSock2.h>
#include <iostream>
#include <vector>
#define PORT 6666
#define IPSTR "127.0.0.1"
int main()
{
int iRet = 0;
WORD vr = (2, 2);
WSADATA dta;
iRet = WSAStartup(vr, &dta);
/*错误处理*/
SOCKET sock = INVALID_SOCKET;
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
/*错误处理*/
sockaddr_in sin = {};
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
iRet = bind(sock, (sockaddr*)&sin, sizeof(sockaddr_in));
/*错误处理*/
iRet = listen(sock, 5);
/*错误处理*/
SOCKET csock = INVALID_SOCKET;
//普通连接
sockaddr_in csin = {};
int addrlen = sizeof(sockaddr_in);
csock = accept(sock, (sockaddr*)&csin, &addrlen);
while (1)
{
char buf[256];
iRet = recv(csock, buf, sizeof(buf), 0); //阻塞位置
std::cout << buf << std::endl;
}
closesocket(sock);
WSACleanup();
}
在普通连接中,会在iRet = recv(csock, buf, sizeof(buf), 0)处阻塞。
2、select
#define FD_SETSIZE 1024
#include <WinSock2.h>
#include <iostream>
#include <vector>
#define PORT 6666
#define IPSTR "127.0.0.1"
int main()
{
int iRet = 0;
WORD vr = (2, 2);
WSADATA dta;
iRet = WSAStartup(vr, &dta);
/*错误处理*/
SOCKET sock = INVALID_SOCKET;
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
/*错误处理*/
sockaddr_in sin = {};
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
iRet = bind(sock, (sockaddr*)&sin, sizeof(sockaddr_in));
/*错误处理*/
iRet = listen(sock, 5);
/*错误处理*/
SOCKET csock = INVALID_SOCKET;
//普通select连接
fd_set fdSocket;
FD_ZERO(&fdSocket);
FD_SET(sock, &fdSocket);
SOCKET maxfd = sock;
while (1)
{
fd_set fdRead = fdSocket; //每次循环时都从新设置select监控信号集
timeval val = { 0,10 };
iRet = select(maxfd +1, &fdRead, 0, 0, &val); //为了兼容其他系统
if (iRet < 0)
{
std::cout << "select出错" << std::endl;
break;
}
if (iRet > 0)
{
if (FD_ISSET(sock, &fdRead)) //说明有新的客户端链接请求
{
if (fdRead.fd_count < FD_SETSIZE) {
sockaddr_in csin = {};
int addrlen = sizeof(sockaddr_in);
csock = accept(sock, (sockaddr*)&csin, &addrlen);
std::cout << "客户端连接:" << csock << std::endl;
FD_SET(csock,&fdSocket);
}
else
{
std::cout << "超出连接范围" << std::endl;
}
}
else
{
for (size_t i = 0; i < fdSocket.fd_count; i++)
{
if (FD_ISSET(fdSocket.fd_array[i], &fdRead))
{
char buf[256];
iRet = recv(fdSocket.fd_array[i], buf, sizeof(buf), 0);
if (iRet <= 0)
{
closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
}
else
{
std::cout << "来自" << fdSocket.fd_array[i] << ":" << buf << std::endl;
}
}
}
}
}
}
closesocket(sock);
WSACleanup();
}
几个主要函数和结构:
WINSOCK_API_LINKAGE
int
WSAAPI
select(
_In_ int nfds,
_Inout_opt_ fd_set FAR * readfds,
_Inout_opt_ fd_set FAR * writefds,
_Inout_opt_ fd_set FAR * exceptfds,
_In_opt_ const struct timeval FAR * timeout
);
返回值
int 若有就绪描述符返回其数目,若超时则为0,若出错则为-1
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
timeval 置为NULL,即为阻塞。
void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
在win32中,select的参一nfds没有意义,可以直接设置为0,
但在linux系统中,参数1代表被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的,为了之后可以多平台兼容,所以设置了maxfd+1;
另外,在win32中FD_SETSIZE的初始值为64,可以在开头添加#define FD_SETSIZE 1024开修改。
maxfd:起初服务端的监听socket的即为最大文件描述符,之后通过比较新加的csock来确定新的maxfd。
fd_set结构包括fd的数量以及由socket组成的集合,
2、select方式二
#ifdef _WIN32
#define FD_SETSIZE 1024
#include <WinSock2.h>
#else
#include<unistd.h> //uni std
#include<arpa/inet.h>
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif
#include <iostream>
#include <vector>
#define PORT 6666
#define IPSTR "127.0.0.1"
int main()
{
int iRet = 0;
#ifdef _WIN32
WORD vr = (2, 2);
WSADATA dta;
iRet = WSAStartup(vr, &dta);
#endif
/*错误处理*/
SOCKET sock = INVALID_SOCKET;
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
/*错误处理*/
sockaddr_in sin = {};
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT);
#ifdef _WIN32
sin.sin_addr.S_un.S_addr = INADDR_ANY;
#else
sin.sin_addr.s_addr = INADDR_ANY;
#endif
iRet = bind(sock, (sockaddr*)&sin, sizeof(sockaddr_in));
/*错误处理*/
iRet = listen(sock, 5);
/*错误处理*/
SOCKET csock = INVALID_SOCKET;
std::vector<SOCKET> client;
fd_set fdSocket;
FD_ZERO(&fdSocket);
FD_SET(sock, &fdSocket);
int maxfd = sock;
int maxi = -1;
while (1)
{
fd_set fdRead = fdSocket;
timeval val = { 0,10 };
iRet = select(sock + 1, &fdRead, 0, 0, &val);
if (iRet < 0)
{
std::cout << "select出错" << std::endl;
break;
}
if (iRet > 0)
{
if (FD_ISSET(sock, &fdRead))
{
sockaddr_in csin = {};
int addrlen = sizeof(sockaddr_in);
#ifdef _WIN32
csock = accept(sock, (sockaddr*)&csin, &addrlen);
#else
csock = accept(sock, (sockaddr*)&csin, (socklen_t *)&addrlen);
#endif
std::cout << "客户端连接:" << csock << std::endl;
client.push_back(csock);
if (client.size() == FD_SETSIZE)
{
std::cout << "超过连接限制" << std::endl;
break;
}
FD_SET(csock,&fdSocket);
if (maxfd < csock)
{
maxfd = csock;
}
if (maxi < client.size())
{
maxi = client.size();
}
}
for (auto it = client.begin(); it != client.end(); it++)
{
if (FD_ISSET(*it, &fdRead))
{
char buf[256];
iRet = recv(*it, buf, sizeof(buf), 0);
if (iRet == 0)
{
#ifdef _WIN32
closesocket(*it);
#else
close(*it);
#endif
it = client.erase(it);
std::cout << *it << "断开连接" << std::endl;
}
else if(iRet > 0)
{
std::cout << "来自" << *it << ":" << buf << std::endl;
}
}
}
}
}
#ifdef _WIN32
closesocket(sock);
WSACleanup();
#else
close(sock);
#endif
}
多用于类unix系统
在linux中:
文件描述符:
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。
程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。
select机制的问题
1、每次调用select,都需要把fd_set
集合从用户态拷贝到内核态,如果fd_set
集合很大时,那这个开销也很大
2、同时每次调用select都需要在内核遍历传递进来的所有fd_set
,如果fd_set
集合很大时,那这个开销也很大
3、为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set
集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)