您现在的位置是:首页 >技术杂谈 >使用select实现TCP并发服务器模型网站首页技术杂谈

使用select实现TCP并发服务器模型

F.LASH. 2024-07-15 18:01:02
简介使用select实现TCP并发服务器模型


前言

本期主要分享的是对于select的使用,使用select实现TCP并发服务器模型,由于之前所用到的技术知识只能够支撑我们进行单个访问,但是有了select之后呢,我们就能够实现多用户进行访问;这也是非常符合客观需求的;


一、select是什么?

1.1 高级IO模型

(1)阻塞IO
	效率高,等待数据过程中不占用CPU资源	
(2)非阻塞IO
    能够解决多个文件描述符来数据的情况
	效率低,等待数据过程中CPU不断轮询所有文件描述符是否有数据	  
(3)异步IO
   当监听文件描述符有数据时,发送信号,收到信号接收数据
   只能应用在文件描述符比较少的情况下	
(4)多路复用IO
   select	   
   poll	   
   epoll

这次呢我们重点来使用一下select;

1.2 select实现

1.函数接口
	(1)select 
	   int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
	   功能:
			监听文件描述符集合
	   参数:
			nfds:最大文件描述符个数+1
			readfds:读文件描述符集合
			writefds:写文件描述符集合
			exceptfds:其余文件描述符集合
			timeout:设定超时时间
			NULL:永远等待,直到有数据发生
	   返回值:
			成功返回产生事件的文件描述符个数
			失败返回-1 	
2. void FD_CLR(int fd, fd_set *set);
	功能:将fd从文件描述符集合中移除		
	
3. int  FD_ISSET(int fd, fd_set *set);
	功能:判断fd是否仍在文件描述符集合中		
	
4. void FD_SET(int fd, fd_set *set);
	功能:将fd加入文件描述符集合			
	
5. void FD_ZERO(fd_set *set);
	功能:将文件描述符集合清0 

1.3 select缺点:

(1)select监听文件描述符个数上限为1024
(2)select监听的文件描述符集合在应用层,事件发时,内核数据需要传递给应用层,造成资源开销
(3)select需要手动检测产生事件的文件描述符
(4)select只能工作在水平触发模式(低速模式)

二、使用select实现TCP并发服务器模型

1.引入库

用到的头文件如下:

#ifndef __HEAD_H__
#define __HEAD_H__

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>

#endif

2.TCP服务器端

我在前面不做过多的文字陈述,大家看代码的时候一定得看注释(非常详细),认真分析,代码如下(示例):

#include "head.h"

int main(int argc, const char *argv[])
{
	int sockfd = 0;
	struct sockaddr_in recvaddr;
	int ret = 0;
	fd_set readfds;
	fd_set tmpfds;
	int maxfd;
	int i = 0;
	int confd = 0;
	char tmpbuff[1024] = {0};
	ssize_t nsize = 0;
	int nready = 0;

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		perror("fail to socket");
		return -1;
	}

	recvaddr.sin_family = AF_INET;
	recvaddr.sin_port = htons(50000);
	recvaddr.sin_addr.s_addr = inet_addr("192.168.209.128");

	ret = bind(sockfd, (struct sockaddr *)&recvaddr, sizeof(recvaddr));
	if (-1 == ret)
	{
		perror("fail to bind");
		return -1;
	}

	ret = listen(sockfd, 10);
	if (-1 == ret)
	{
		perror("fail to listen");
		return -1;
	}

	FD_ZERO(&readfds);			//清空文件描述符集合
	FD_SET(sockfd, &readfds);	//将sockfd加入到文件描述符集合中
	maxfd = sockfd;				//此时sockfd就是最大的文件描述符

	while (1)
	{
		tmpfds = readfds;		//每次循环最开始必须是所有目前已经加入的所有文件描述符
		nready = select(maxfd+1, &tmpfds, NULL, NULL, NULL);	//进行事件监听
		if (-1 == nready)
		{
			perror("fail to select");
			return -1;
		}

		if (FD_ISSET(sockfd, &tmpfds))		//判断是否有tcp通信文件描述符
		{
			confd = accept(sockfd, NULL, NULL);		//进行三次握手链接
			if (-1 == confd)						
			{
				perror("fail to accept");
				close(sockfd);						
				FD_CLR(sockfd, &readfds);
				break;								//出错后直接退出,因为sockfd只有一个,是server通信的文件描述符,而confd是不同的客户端请求就会对应产生一个
			}
			FD_SET(confd, &readfds);
			maxfd = maxfd > confd ? maxfd : confd;		//防止0,1,2三个文件描述符在前面关闭,confd去替补了,所以这里使用三目运算符对confd和maxfd进行判断
		}

		for (i = sockfd + 1; i <= maxfd;++i)		//只判断confd的事件,也就是客户端的事件(客户端用的是confd通信)
		{
			if (FD_ISSET(i, &tmpfds))
			{
				memset(tmpbuff, 0, sizeof(tmpbuff));
				nsize = recv(i, tmpbuff, sizeof(tmpbuff), 0);	//接收客户端消息
				if (-1 == nsize)
				{
					perror("fail to recv");
					close(i);
					FD_CLR(i, &readfds);
					continue;		//继续判断下一个
				}
				else if (0 == nsize)
				{
					close(i);
					FD_CLR(i, &readfds);
					continue;		//继续判断下一个
				}
				printf("recv = %s
", tmpbuff);						//打印来自于客户端的消息
				sprintf(tmpbuff, "%s----recv", tmpbuff);			//回复消息
				nsize = send(i, tmpbuff, strlen(tmpbuff), 0);
				if (-1 == nsize)
				{
					perror("fail to send");
					close(i);
					FD_CLR(i, &readfds);
					continue;										//发送完毕后继续轮询看别的客户端是否有事件发生
				}
			}
		}
	}

	close(confd);
	close(sockfd);

	return 0;
}

3. TCP服务器端

服务器端比较简单,就是发送消息给服务器等待解接收回复,select主要还是体现在server端;

#include "head.h"

int main(int argc, const char *argv[])
{
	int sockfd = 0;
	struct sockaddr_in senaddr;
	char tmpbuff[1024] = {0};
	int count = 0;
	ssize_t nsize = 0;
	int ret = 0;

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		perror("fail to spckfd");
		return -1;
	}

	senaddr.sin_family = AF_INET;
	senaddr.sin_port = htons(50000);
	senaddr.sin_addr.s_addr = inet_addr("192.168.209.128");

	ret = connect(sockfd, (struct sockaddr *)&senaddr, sizeof(senaddr));
	if (-1 == ret)
	{
		perror("fail to connect");
		return -1;
	}

	while (1)
	{
		memset(tmpbuff, 0, sizeof(tmpbuff));
		sprintf(tmpbuff, "hello ------->%d", count);
		count++;
		nsize = send(sockfd, tmpbuff, strlen(tmpbuff), 0);		//发送给服务器
		if (-1 == nsize)
		{
			perror("fail to send");
			return -1;
		}

		memset(tmpbuff, 0, sizeof(tmpbuff));
		nsize = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
		if (nsize <= 0)
		{
			break;
		}
		printf("%s
", tmpbuff);
	}

	close(sockfd);
	return 0;
}

3. 运行结果

最后,我们来看一下运行结果:
在这里插入图片描述


总结

本期分享的这个模型确实能够实现我们这个多任务并发,每当客户端有消息发来时,select监听到才会向下执行,这是它的优点;希望各位小伙伴一定手动练习起来哦!
最后,各位小伙伴们如果喜欢我的分享可以点赞收藏哦,你们的认可是我创作的动力,一起加油!

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。