select IO模型学习
-
问题由来
单进程单线程下,最普通的socket是阻塞连接的,即server与一个client连接后,不能与其他的client通信。
如果采取多线程解决这个问题,每来一个client socket连接,就创建一个线程的话,浪费时间。
-
fd_set 一个数据类型,四个函数
-
fd_set 类型 一个套接字socket的数组
typedef struct fd_set { u_int fd_count; /* how many are SET? */ SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */ } fd_set;
FD_ZERO(fd_set*) ;
将传入的套接字集合置空FD_SET(fd, fd_set*)
将套接字fd放入套接字集合中FD_CLR(fd, fd_set*)
将套接字集合中的套接字fd 去除掉FD_ISSET(fd, fd_set*)
查询套接字fd是否在套接字集合中
-
select
//VS2015 f12查找定义的结果 定义在 #include <WinSock2.h> 下
int
WSAAPI
select(
_In_ int nfds,
_Inout_opt_ fd_set FAR * readfds, //类型 _Inout_opt_ 表示输入输出参数
_Inout_opt_ fd_set FAR * writefds,
_Inout_opt_ fd_set FAR * exceptfds,
_In_opt_ const struct timeval FAR * timeout
);
-----------------------------
参数:
nfds
忽略。包含nfds参数只是为了与Berkeley套接字兼容。
注意:这个参数在linux中要有特殊的意义和操作,表示最大的套接字内容,但是windows下可以直接用0
readfds
套接字集合指针类型,检查套接字集合的可读性
可读性监视,可读性指有连接到来、有数据到来、连接已关闭、重置或终止
writefds
套接字集合指针类型,检查套接字集合的可写性
1如果处理一个connect调用(非阻塞),连接成功。数据可以发送。
exceptfds
套接字集合指针类型,检查套接字集合错误
timeout
select等待的最长时间,以TIMEVAL结构的形式提供。对于阻塞操作,请将timeout参数设置为null。
返回值:
返回状态发生的套接字的个数 失败返回SOCKET_ERROR
个人理解select函数的作用
- 把想要监控状态的套接字集合传给select的相应部分(是否可读可写是否出错),然后内核会自动维护这个套接字集合:通过输入输出参数 (&)的方式。维护的结果就是,除了有期待状态发生的socket,其他的都会踢出集合。
重要:sockSer可读表示有链接到来,sockCli可读表示有数据到来
-
select缺点与epoll比较
elect监控的文件描述符有上限
每次调用都需要手动的设置文件描述符集合,使用非常不便
每次调用都要把文件描述符从用户态拷贝到内核态,开销比较大
当就绪的文件描述符好后,需要循环遍历来进行判断,效率不好 -
与多进程多线程服务器的优缺点比较
-
优点
1)不需要建立多个线程、进程就可以实现一对多的通信。
2)可以同时等待多个文件描述符,效率比起多进程多线程来说要高很多。
3)select()的可移植性更好,在某些Unix系统上不支持poll()
4)select() 对于超时值提供了更好的精度:微秒,而poll是毫秒 -
缺点
1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 ,循环次数有点多;
2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 。
3)select支持的文件描述符数量太小了,默认是1024;
-
- 一个小实现,同时连接多个客户端并且读取各个客户端发来的消息
//*************服务器端*************************
//=========================================================头文件
#include <WinSock2.h>//是网络编程socket相关部分API
#include <WS2tcpip.h>//inet_ntop()
#include <iostream>
#include <sstream>//std::to_string
#include <cstring>
#pragma comment(lib,"ws2_32.lib")//这是链接API相关连的Ws2_32.lib静态库
using namespace std;
//==========================================================静态变量
const int BUF_SIZE = 2048;//缓冲区变量长度
//==========================================================全局变量
SOCKET sockSer;//定义用于监听的套接口
SOCKET sockCli;//定义用于连接客户端和收发信息的套接口
SOCKADDR_IN addrSer, addrCli;//定义两端的地址(IP地址加端口号)
int naddr = sizeof(SOCKADDR_IN);
char sendBuf[BUF_SIZE];//发送的字符串缓冲区
char recvBuf[BUF_SIZE];//接收内容缓冲区
int main()
{
//加载socket库,WSAStartup(Sock版本,版本信息返回)
WSADATA wsadata;
if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
{
//输出出错信息
cout << "载入socket失败" << endl;
system("pause");//暂停一下给用户反映时间
return 0;
}
//else { cout << "载入socket成功" << endl; }
//创建socket
sockSer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//sockets(套接字)编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW);基于TCP的socket编程是采用的流式套接字。
//初始化SOCKET_IN各个成员变量
//addrSer.sin_addr.s_addr= inet_addr("202.98.105.139");
addrSer.sin_addr.S_un.S_addr = INADDR_ANY;
//inet_addr十进制ip转二进制ip
addrSer.sin_family = AF_INET;//指定协议簇
addrSer.sin_port = htons(20000);//htons()端口转换二进制
//绑定socket:bind(socket套接字,地址,)
if (bind(sockSer, (SOCKADDR*)&addrSer, sizeof(SOCKADDR)) == -1)
{
cout << "bind fail" << endl;
}
else { cout << "bind success" << endl; }
//监听请求listen(服务端套接字,等待队列最大容量(3-5))
if (listen(sockSer, 5) == -1)
{
cout << "listen fail" << endl;
}
else { cout << "listen success" << endl; }
/*
//正常一对一
sockCli = accept(sockSer, (SOCKADDR*)&addrCli, &naddr);
//请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字sockCli
if (sockCli != INVALID_SOCKET) {//连接成功
cout << "连接客户端成功!" << endl;
strncpy_s(sendBuf, "Hello",50);
send(sockCli, sendBuf, sizeof(sendBuf), 0);//发送数据到客户端
}
else { cout << "连接客户端失败!" << endl; }
system("pause");
*/
//allSocketsSet存放sockSer+所有的已连接的Client Sockets
fd_set allSocketsSet;
FD_ZERO(&allSocketsSet);
//先把sockSer放进去,如果sockSer可读(FD_SET(sockCli, &allSocketsSet);)表示有客户端连接
FD_SET(sockSer, &allSocketsSet);
while (true)
{
fd_set readSet;
//每次select前要存储已经连接的描述符
readSet = allSocketsSet;
int ret = select(0, &readSet, 0, 0, 0);
if (ret == SOCKET_ERROR)
{
cout << "select error" << endl;
break;
}
//不断等待客户端请求的到来,到来了就加allSocketsSet,之后跳出循环
if (FD_ISSET(sockSer, &readSet))
{//有客户端连接,就给他保存到allSocketsSet中
sockCli = accept(sockSer, (SOCKADDR*)&addrCli, &naddr);
if (sockCli == INVALID_SOCKET)
{
cout << "accept error" << endl;
break;
}
else
{
cout << "accept连接客户端成功!" << endl;
//将连接成功的sockCli放入allSocketsSet
FD_SET(sockCli, &allSocketsSet);
continue;
}
}
//没客户端到来,再检查allSocketsSet中的其他客户端的sockets是否可读,可读表示有数据到来。
for (u_int i = 0; i < allSocketsSet.fd_count; ++i)
{
SOCKET socket = allSocketsSet.fd_array[i];
if (FD_ISSET(socket, &readSet))
{//可读,有数据到来
int result = recv(socket, recvBuf, BUF_SIZE, 0);
//返回值分析
if (result == SOCKET_ERROR)
{//读的过程中出错
DWORD err = WSAGetLastError();
if (err == WSAECONNRESET) // 客户端的socket没有被正常关闭,即没有调用closesocket
{
cout << "客户端被强行关闭" << endl;
}
else
{
cout << "recv() error" << endl;
}
closesocket(socket);
FD_CLR(socket, &allSocketsSet);
//std::cout << "目前客户端的数量为:" << allSockSet.fd_count - 1 << std::endl;
cout << "目前客户端的数量为:" << std::to_string(allSocketsSet.fd_count - 1) << endl;
break;
}
else if (result == 0)
{//客户端的socket调用closesocket正常关闭
closesocket(socket);
FD_CLR(socket, &allSocketsSet);
//-1是因为减去sockSer
cout << "客户端已经退出,目前客户端的数量为:" << std::to_string(allSocketsSet.fd_count - 1) << endl;
break;
}
if (result > 0)
{//正常读取了结果
cout << recvBuf << endl;
/*
*
*
此处可添加对客户端发来消息的处理。
*
*/
}
}
}
}
closesocket(sockSer);
closesocket(sockCli);
return 0;
}