0
点赞
收藏
分享

微信扫一扫

TCP并发服务器(进程、线程、select)

DYBOY 2022-05-01 阅读 29

一个好的服务器,一般都是并发服务器(同一时刻可以响应多个客户端的请求)。并发服务器设计一般有多进程服务器、多线程服务器、I/O复用服务器等。

一、进程版本

在这里插入图片描述

优点:父子进程资源独立,某个进程结束,不会影响已有的进程,服务器更加稳定。
缺点:消耗资源大

demo:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/************************************************************************
函数名称: void main(int argc, char *argv[])
函数功能: 主函数,用进程建立一个TCP Echo Server
函数参数: 无
函数返回: 无
************************************************************************/


int main(int argc, char *argv[])
{
	unsigned short port = 8080; // 本地端口
	//1.创建tcp套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd < 0)
	{
		perror( "socket");
		exit( -1);
	}
		
	//配置本地网络信息
	struct sockaddr_in my_addr;
	bzero(&my_addr, sizeof(my_addr)); // 清空
	my_addr.sin_family = AF_INET; // IPv4
	my_addr.sin_port = htons(port); // 端口
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // ip

	//2.绑定bind
	int err_log = bind(sockfd, ( struct sockaddr*)&my_addr, sizeof(my_addr));
	if( err_log != 0)
	{
		perror( "binding");
		close(sockfd);
		exit( -1);
	}
	
	//3.监听listen
	err_log = listen(sockfd, 10);
	if(err_log != 0)
	{
		perror( "listen");
		close(sockfd);
		exit( -1);
	}
	
	while( 1) //主进程 循环等待客户端的连接
	{
		char cli_ip[INET_ADDRSTRLEN] = {0};		//INET_ADDRSTRLEN定义为16
		struct sockaddr_in client_addr;
		socklen_t cliaddr_len = sizeof(client_addr);
		// 取出客户端已完成的连接
		int connfd = accept(sockfd, ( struct sockaddr*)&client_addr, &cliaddr_len);
		if(connfd < 0)
		{
			perror( "accept");
			close(sockfd);
			exit( -1);
		}
		
		//创建fork
		pid_t pid = fork();
		if(pid < 0)
		{
			perror( "fork");
			_exit( -1);
		} 
		
		//子进程 接收客户端的信息,并发还给客户端
		else if( 0 == pid)   
		{ 
			/*关闭不需要的套接字可节省系统资源,
			  同时可避免父子进程共享这些套接字
 	          可能带来的不可预计的后果
			*/
		
		close(sockfd); // 关闭监听套接字,这个套接字是从父进程继承过来
		char recv_buf[ 1024] = { 0};
		int recv_len = 0;
		// 打印客户端的 ip 和端口
		memset(cli_ip, 0, sizeof(cli_ip)); // 清空
		inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
		printf( "----------------------------------------------\n");
		printf( "client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));
		
		// 接收数据
		while( (recv_len = recv(connfd, recv_buf, sizeof(recv_buf), 0)) > 0 )
		{
			printf( "recv_buf: %s\n", recv_buf); // 打印数据
			send(connfd, recv_buf, recv_len, 0); // 给客户端回数据
		}
		printf( "client_port %d closed!\n", ntohs(client_addr.sin_port));
		close(connfd); //关闭已连接套接字
		exit( 0);
		}
		
		// 父进程
		else if(pid > 0)
		{ 
		close(connfd); //关闭已连接套接字
		}
	}
	
	close(sockfd);
	return 0;

}

网络调试助手登录:
在这里插入图片描述

二、线程版本

在这里插入图片描述

优点:线程共享进程资源,资源开销小。
缺点:一旦主进程结束,所以进程都会结束,不稳定。

demo:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>						
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>					
#include <pthread.h>
 
/************************************************************************
函数名称:	void *client_fun(void *arg)
函数功能:	线程函数,处理客户信息
函数参数:	已连接套接字
函数返回:	无
************************************************************************/
void *client_fun(void *arg)
{
	int recv_len = 0;
	char recv_buf[1024] = "";	// 接收缓冲区
	int connfd = (int)arg; // 传过来的已连接套接字
 
	// 接收数据
	while((recv_len = recv(connfd, recv_buf, sizeof(recv_buf), 0)) > 0)
	{
		printf("recv_buf: %s\n", recv_buf); // 打印数据
		send(connfd, recv_buf, recv_len, 0); // 给客户端回数据
	}
	
	printf("client closed!\n");
	close(connfd);	//关闭已连接套接字
	
	return 	NULL;
}
 
//===============================================================
// 语法格式:	void main(void)
// 实现功能:	主函数,建立一个TCP并发服务器
// 入口参数:	无
// 出口参数:	无
//===============================================================
int main(int argc, char *argv[])
{
	int sockfd = 0;				// 套接字
	int connfd = 0;
	int err_log = 0;
	struct sockaddr_in my_addr;	// 服务器地址结构体
	unsigned short port = 8080; // 监听端口
	pthread_t thread_id;
	
	printf("TCP Server Started at port %d!\n", port);
	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);   // 创建TCP套接字
	if(sockfd < 0)
	{
		perror("socket error");
		exit(-1);
	}
	
	bzero(&my_addr, sizeof(my_addr));	   // 初始化服务器地址
	my_addr.sin_family = AF_INET;
	my_addr.sin_port   = htons(port);
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	
	printf("Binding server to port %d\n", port);
	
	// 绑定bind
	err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
	if(err_log != 0)
	{
		perror("bind");
		close(sockfd);		
		exit(-1);
	}
	
	// 监听listen
	err_log = listen(sockfd, 10);
	if( err_log != 0)
	{
		perror("listen");
		close(sockfd);		
		exit(-1);
	}
	
	printf("Waiting client...\n");
	
	while(1)
	{
		char cli_ip[INET_ADDRSTRLEN] = "";	   // 用于保存客户端IP地址
		struct sockaddr_in client_addr;		   // 用于保存客户端地址
		socklen_t cliaddr_len = sizeof(client_addr);   // 必须初始化!!!
		
		//获得一个已经建立的连接	
		connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);   							
		if(connfd < 0)
		{
			perror("accept this time");
			continue;
		}
		
		// 打印客户端的 ip 和端口
		inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
		printf("----------------------------------------------\n");
		printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));
		
		if(connfd > 0)
		{
			//由于同一个进程内的所有线程共享内存和变量,因此在传递参数时需作特殊处理,值传递。
			pthread_create(&thread_id, NULL, (void *)client_fun, (void *)connfd);  //创建线程
			pthread_detach(thread_id); // 线程分离,结束时自动回收资源
		}
	}
	
	close(sockfd);
	
	return 0;
}

三、select版本

进程方式和线程方式可以实现并发服务,其实里面调用send,read,accept函数都会导致阻塞。而linux的select函数可以使我们在程序中同时监听多个文件描述符的读写状态。程序会停在select这里等待,直到被监视的文件描述符有某一个或多个发生了状态改变。select()的机制中提供一fd_set的数据结构,实际上是一long类型的数组, 每一个数组元素都能与一打开的文件描述符(不管是Socket描述符,还是其他 文件或命名管道或设备描述符)建立联系, 当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。

API:

FD_ZERO(fd_set *)          清空一个文件描述符集合;

FD_SET(int ,fd_set *)      将一个文件描述符添加到一个指定的文件描述符集合中;

FD_CLR(int ,fd_set*)       将一个给定的文件描述符从集合中删除;

FD_ISSET(int ,fd_set* )    检查集合中指定的文件描述符是否可以读写。

select函数

int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);

参数1:maxfdp是一个整数,是指集合所以文件描述符的范围,即所以文件描述符的最大值+1;

参数2:fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符。我们是要监视这些文件描述符的变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读;如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

参数3:fd_set *writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

参数4:fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常文件。

参数5:struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

返回值
在这里插入图片描述
demo:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
 
#define MAX_LISTEN 5
#define PORT 8080
#define IP "192.168.199.208"
 
int main()
{
	int conn_fd;
	
	//1.socket创建套接字
	int sock_fd = socket(AF_INET,SOCK_STREAM,0);
	if (sock_fd < 0) {
		perror("create socket failed");
		exit(1);
	}
 
	struct sockaddr_in addr_client;
	int client_size = sizeof(struct sockaddr_in);
 
	//2.bind绑定服务端的IP和端口
	struct sockaddr_in addr_serv;
	memset(&addr_serv, 0, sizeof(addr_serv));
	addr_serv.sin_family = AF_INET;
	addr_serv.sin_port = htons(PORT);
	addr_serv.sin_addr.s_addr = inet_addr(IP);
	if (bind(sock_fd,(struct sockaddr *)&addr_serv,sizeof(struct sockaddr_in)) < 0) {
		perror("bind error");
		exit(1);
	}
 
	//3.listen监听
	if (listen(sock_fd,MAX_LISTEN) < 0) {
		perror("listen failed");
		exit(1);
	}
 
	int recv_num;			//收到的数据长度
	int send_num;			//发送的数据长度
	char recv_buf[100];		//收到的数据
	char send_buf[100];		//发送的数据
 
	//用一个数组记录描述符的状态
	int i;
	int ready;
	int max_fd;
	
	int client[FD_SETSIZE];
	for (i = 0;i < FD_SETSIZE;i ++) {
		client[i] = -1;
	}
 
	fd_set readset;		//文件描述符集合
	max_fd = sock_fd;
 
	//最大可用描述符的个数,一般受操作系统内核的设置影响,我的环境下这个值是1024
	printf("max fd num %d\n",FD_SETSIZE);
 
	while (1) {
 
		//重置监听的描述符
		FD_ZERO(&readset);						//清空文件描述符集合
		FD_SET(sock_fd,&readset);				//将一个文件描述符添加到一个指定的文件描述符集合中
		for (i = 0;i < FD_SETSIZE;i ++) {
			if (client[i] == 1) {
				FD_SET(i, &readset);
			}
		}
 
		//开始监听描述符,是异步的,不会阻塞
		ready = select(max_fd+1, &readset, NULL, NULL, NULL);
		
		//可用描述符如果是创建连接描述符,则创建一个新的连接
		if (FD_ISSET(sock_fd, &readset)) {
			conn_fd = accept(sock_fd, (struct sockaddr *)&addr_client, &client_size);
			if (conn_fd < 0) {
				perror("accept failed");
				exit(1);
			}
 
			FD_SET(conn_fd, &readset);
			FD_CLR(sock_fd, &readset);
 
			if (conn_fd > max_fd) {
				max_fd = conn_fd;
			}
			client[conn_fd] = 1;	
		}
		
		//检查所有的描述符,查看可读的是哪个,针对它进行IO读写
		for (i = 0; i < FD_SETSIZE; i ++) {
			if (FD_ISSET(i, &readset)) {
 
				recv_num = recv(i, recv_buf, sizeof(recv_buf), 0);
				if (recv_num <= 0) {
					FD_CLR(i, &readset);	//将文件描述符从集合中清除
					client[i] = -1;
				}
				recv_buf[recv_num] = '\0';
				memset(send_buf,0,sizeof(send_buf));
				sprintf(send_buf, "server proc got %d bytes\n", recv_num);
				send_num = send(i, send_buf, strlen(send_buf), 0);
				if (send_num <= 0) {
					FD_CLR(i, &readset);
					client[i] = -1;
				}
			}
		}
	}
		
	close(sock_fd);
	return 0;
}
举报

相关推荐

0 条评论