为了同时处理多个客户端的连接,上一篇介绍了利用多线程的方法实现,每个连接新建一个线程,然后各自去处理。这是最简单也是最容易想到的方式。客户端的连接存在,线程就存在。
但是,对于每一个客户端,并不是时时刻刻都会向服务端发送消息的,随着客户端连接数量的增加,创建的线程也越来越多,系统在线程和进程之间切换的开销就会变得非常大。
再者,如果客户端频繁的创建连接又断开,服务端就会随之创建或销毁线程,使得效率非常低。
这时,就要用到select模式了:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
参数说明:
1. int nfds,所有文件描述符中最大值加1
2. fd_set可以理解为文件描述符的集合,它有下面几种操作:
void FD_CLR(int fd, fd_set *set); 从文件描述符集合set中删除一个fd
int FD_ISSET(int fd, fd_set *set); 判断fd是否在集合中
void FD_SET(int fd, fd_set *set); 在集合中添加一个fd
void FD_ZERO(fd_set *set); 清空文件描述符集合
3. readfds 想要检测的可读的文件描述符集合,writefds想要检测的可写的文件描述符集合,exceptfds想要检测的可能异常的文件描述符集合,一般情况下,想要从socket接收数据时,只需要readfds即可。
4. struct timeval *timeout,select的超时时间,可以传入三种值:
<1> timeout == NULL,select将处于阻塞状态,一直等到所监视的fd_set中有可读可写或异常的变化
<2> 传入一个timeval结构的时间值,但是时间为0,则select变成一个非阻塞函数,不管fd_set是否有变化,都立刻返回
<3> 传入一个非0的timeval的值,则select在这个时间内阻塞,如果有fd变化,则立刻返回,且返回值大于0,没有fd变化,则select等到超时后,返回值为0。需要注意的是,每次select会减小timeout的值,所以每次select之前,都需要设置timeout的值。
5. 在socket的服务端编程时,readfds为服务端监听的fd + 所有客户端的fd的集合。当有fd可读时使用FD_ISSET判断:
如果是服务端自己的socket可读,则表示有客户端发起了连接,需要调用accept接受连接
如果是客户端的socket可读,则表示客户端有数据发送过来,需要调用recv接收数据
select模式的服务端代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
//获取客户端 ip:port 格式的字符串
string getpeeraddrstr(int sockfd)
{
struct sockaddr_in addr = {0};
unsigned int size = sizeof(addr);
getpeername(sockfd, (struct sockaddr*)&addr, &size);
stringstream ssaddr;
ssaddr << inet_ntoa(addr.sin_addr) << ":" << ntohs(addr.sin_port);
return ssaddr.str();
}
//入参 cltsockets 客户端socket的集合
//入参 svrfd 服务端绑定的集合
//出参 maxfd 服务端和客户端中最大的socket
//出参 readset 可读的fdset
//根据2个入参获取2个出参 即select需要的两个参数
void select_fdset(const vector& cltsockets, int& svrfd, int& maxfd, fd_set& readset)
{
FD_ZERO(&readset);
for(unsigned int i = 0; i < cltsockets.size(); i++)
{
if(-1 != cltsockets[i])
{
FD_SET(cltsockets[i], &readset);
maxfd = cltsockets[i] > maxfd ? cltsockets[i] : maxfd;
}
}
if(-1 != svrfd)
{
FD_SET(svrfd, &readset);
maxfd = svrfd > maxfd ? svrfd : maxfd;
}
}
//如果是服务端的socket可读 则调用accept接收连接
void select_server(int svrfd, vector& cltsockets, const fd_set& readset)
{
if(-1 != svrfd && FD_ISSET(svrfd, &readset))
{
struct sockaddr_in cltaddr = {0};
unsigned int addrlen = sizeof(cltaddr);
//服务端阻塞等待客户端的连接
int cltfd = accept(svrfd, (struct sockaddr*)&cltaddr, &addrlen);
if(-1 == cltfd)
{
perror("accept failed");
return;
}
cout << "connect accept: " << getpeeraddrstr(cltfd) << endl;
cltsockets.push_back(cltfd);
}
}
//如果是客户端的socket可读 则接收客户端发来的消息 并打印
void select_client(vector& cltsockets, const fd_set& readset)
{
for(unsigned int i = 0; i < cltsockets.size(); i++)
{
if(-1 != cltsockets[i] && FD_ISSET(cltsockets[i], &readset))
{
char buffer[1024] = {0};
int recvlen = 0;
string straddr = getpeeraddrstr(cltsockets[i]);
recvlen = recv(cltsockets[i], buffer, sizeof(buffer), 0);
if(0 < recvlen) //接收成功
{
cout << "recv from " << straddr << " " << buffer;
}
else
{
cout << "client " << straddr <<" closed" << endl;
close(cltsockets[i]);
cltsockets[i] = -1;
break;
}
}
}
}
int main()
{
int opt = 1;
int svrfd = -1;
unsigned short svrport = 9999;
struct sockaddr_in svraddr = {0};
vector cltsockets; //保存客户端的socket集合
fd_set readset; //可读的socket集合
struct timeval tv; //select等待时间
int maxfd = -1; //select的socket范围 是服务端客户端所有socket中最大的socket值
//创建socket
svrfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == svrfd)
{
perror("socket failed");
return -1;
}
//设置地址重用
setsockopt(svrfd, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));
//绑定ip地址和端口
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(svrport); //服务端绑定端口
svraddr.sin_addr.s_addr = htonl(INADDR_ANY); //服务端绑定任意IP
if(-1 == bind(svrfd, (struct sockaddr*)&svraddr, sizeof(svraddr)))
{
perror("bind failed");
close(svrfd);
return -1;
}
//开始监听
if(-1 == listen(svrfd, 10))
{
perror("listen failed");
close(svrfd);
return -1;
}
while(1)
{
tv.tv_sec = 1;
tv.tv_usec = 0;
//根据客户端和服务端所有socket的集合 获取select的两个参数
select_fdset(cltsockets, svrfd, maxfd, readset);
//开始select 最多等待tv时长
int ret = select(maxfd+1, &readset, NULL, NULL, &tv);
if(ret < 0)
{
perror("select error");
break;
}
else if(0 == ret)
{
//select的tv时间到 继续select
continue;
}
else
{
select_server(svrfd, cltsockets, readset);
select_client(cltsockets, readset);
}
}
}