Linux网络编程 5 - select模式的TCP服务器

        为了同时处理多个客户端的连接,上一篇介绍了利用多线程的方法实现,每个连接新建一个线程,然后各自去处理。这是最简单也是最容易想到的方式。客户端的连接存在,线程就存在。

        但是,对于每一个客户端,并不是时时刻刻都会向服务端发送消息的,随着客户端连接数量的增加,创建的线程也越来越多,系统在线程和进程之间切换的开销就会变得非常大。

        再者,如果客户端频繁的创建连接又断开,服务端就会随之创建或销毁线程,使得效率非常低。

        这时,就要用到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 <stdio.h>
#include <string.h>
#include <sys/types.h>  
#include <sys/socket.h>  
#include <netinet/in.h>  
#include <arpa/inet.h>  
#include <unistd.h>  
#include <iostream>
#include <sstream> 
#include <pthread.h>
#include <vector>
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<int>& 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<int>& 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<int>& 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<int> 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);
		}
	}  
} 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值