Linux网络编程实践三(多线程+io复用select)

 前言

        温馨提示:题目和代码可以不看,直接拿代码去测试就明白了。

题目:

在编程实践2基础上,采用多线程技术和I0复用实现一个线程服务多个客户端,服务进程里面的多个线程服务大量客户端。要求:

  1. 源代码格式化良好并适当注释;
  2. 除上述明确要求的功能外,还要注意其它问题,比如线程互斥、服务程序支持线程数和每个线程支持客户端算的可配置性等;

    3. 提交报告,报告中包括程序源代码和测试效果截图。

测试效果截图:

        和编程实践2类似,我创建了三个文件夹,一个代表server端,另外两个代表client和client2两个客户端。

        下面进行程序测试,看看客户端能否与服务端通信:

  通信正常,接下来我要从客户端client上传文件test.txt文件,然后在client2端将被上传到服务端的test.txt下载到client2。

扫描二维码关注公众号,回复: 15204632 查看本文章

        如下图是文件上传和下载之前各个文件夹中的文件。

        客户端Client开始上传文件:

        上传成功,client客户端退出连接并在client2客户端下载test.txt文件:

        下载成功,可以看到server和client2都出现了test.txt文件。

        接下来测试服务端可接入的客户端数:

        手动测试太繁琐,没测出上限。

        所以我修改了一下客户端的代码,让客户端用不断创建线程去与服务端连接(不进行通信),然后编译运行,发现都是在创建到3400个线程左右程序停止。

        也可以用ulimit -a命令查看线程栈大小,如下图,默认栈大小为8192(8MB),所以可以创建的线程也可以大致估算。同时,可以用ps -a查看当前进程,用ps -T -p pid查看进程pid下的线程。

        程序测试就到这里。

程序源代码:

服务端代码:

#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <fcntl.h>
#define PORT 8888	/*侦听端口地址*/
#define BACKLOG 32	/*侦听队列长度*/

#define Maxline 1024	/*最大读取文件行数*/

char file_name[105];	/*设置一个全局变量用来保存文件名*/
int ss,sc;	/*ss为服务器的socket描述符,sc为客户端的socket描述符*/
struct sockaddr_in server_addr;		/*服务器地址结构*/
struct sockaddr_in client_addr;		/*客户端地址结构*/
socklen_t len = sizeof(client_addr);	/*客户端地址结构长度*/
char ipstr[128],ip[128];	/*用于输出ip*/
int sum_client=0;	/*记录接入的客户端个数*/
pthread_mutex_t mutex; /*互斥区*/
fd_set fds;	/*文件描述符集合*/

void get_filename(char buffer[]);	/*从buffer字符串中提取出文件名*/
void upload_file(char buffer[],int s);		/*文件上传函数*/
void download_file(char buffer[],int s);	/*接收文件函数*/
void process_conn_server(int s);  /*服务端与客户端通信函数*/
void *start_routine(void *arg);	 /*线程处理函数*/
void *server_init(void *arg);	/*进行服务器的相关操作,包括select*/

int main(int argc, char *argv[])
{
	int err;	/*返回值*/
	pthread_mutex_init(&mutex,NULL);	/*初始化互斥区*/
	pthread_t threadid = 0;
	err = pthread_create(&threadid,NULL,server_init,NULL);/*创建线程,再进行服务端的设置*/
	if(err != 0){
		printf("创建线程出错\n");
		return 0;
	}
	else pthread_join(pthread_self(),NULL);
	while(1)sleep(10);	/*防止主线程关闭*/
	pthread_mutex_destroy(&mutex);	/*销毁互斥*/
	return 0;
}

void *server_init(void *arg){	/*进行服务器的相关操作,包括select*/
	int err;	/*返回值*/
	/*建立一个流式套接字*/
	ss = socket(AF_INET, SOCK_STREAM, 0);
	if(ss < 0){	/*出错*/
		printf("socket error\n");
		return NULL;	
	}
	
	/*设置服务器地址*/
	bzero(&server_addr, sizeof(server_addr));	/*清零*/
	server_addr.sin_family = AF_INET;	/*协议族*/
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);	/*本地地址*/
	server_addr.sin_port = htons(PORT);	/*服务器端口*/
	
	/*绑定地址结构到套接字描述符*/
	err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if(err < 0){	/*出错*/
		printf("bind error\n");
		exit(-1);
		//return NULL;	
	}
	
	/*设置侦听*/
	err = listen(ss, BACKLOG);
	if(err < 0){	/*出错*/
		printf("listen error\n");
		return NULL;	
	}
	while(1){
		FD_ZERO(&fds);		//描述符集合初始化
		FD_SET(ss,&fds);
		
		struct timeval timeout = {30, 0};	//超时时间设置为30s
		
		err = select(ss+1,&fds,NULL,NULL,&timeout);
		if(err == -1){
			printf("select error\n");
			return NULL;
		}
		else if(err == 0){
			printf("没有接收到请求(超时)\n");
			continue;
		}
		else{
			if(FD_ISSET(ss,&fds)){
				int size=sizeof(client_addr);
				bzero(&client_addr,size);//清空客户端
				
				sc = accept(ss,(struct sockaddr*)&client_addr,&size);
				if(sc < 0){//出错
					continue;//结束本次循环
				}
            			strcpy(ip,inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,ipstr,sizeof(ipstr)));
            			int port = ntohs(client_addr.sin_port);
            			printf("新的客户端接入,ip为:%s ,端口号为:%d \n",ip,port);/*输出接如服务端的客户端ip和端口*/
            			sum_client++;
            			printf("当前连接的客户端数(也是线程的个数)为:%d\n",sum_client);
				pthread_t pth;
				err = pthread_create(&pth,NULL,start_routine,NULL);
				if(err != 0){
					printf("创建子线程出错\n");
					FD_CLR(ss,&fds);
					return NULL;
				}
				else pthread_join(pthread_self(),NULL);
				
			}
			
		}
		
	}
}

void *start_routine(void *arg){
	printf("正在通信的线程ID为: %lu\n",pthread_self());
	process_conn_server(sc);	/*调用通信函数*/
}

void process_conn_server(int s)
{
	ssize_t size = 0;
	char buffer[1024];	/*数据的缓冲区*/
	char message[1024];	/*用来传递消息*/
	
	while(1){/*循环处理过程*/
		memset(buffer,0,1024);	/*清空缓冲区*/	
		size = recvfrom(s,buffer,sizeof(buffer),0,(struct sockaddr*)&client_addr,&len);/*从套接字中读取数据放到缓冲区buffer中*/
		if(size == 0){/*没有数据*/
            		strcpy(ip,inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,ipstr,sizeof(ipstr)));
            		int port = ntohs(client_addr.sin_port);
            		sum_client--;
                        printf("客户端关闭连接,其ip为:%s,端口号为:%d;\n",ip,port);
                        printf("因客户端关闭连接,ID为:%lu的线程关闭\n",pthread_self());
                        printf("当前连接的客户端数为:%d\n",sum_client);
			return;	
		}
		if(strstr(buffer,"上传文件")!=NULL){	/*收到客户端上传文件的请求*/
			strcpy(message,"服务器端开始接收文件...");
			sendto(s,message,sizeof(message),0,(struct sockaddr*)&client_addr,sizeof(client_addr));/*发给客户端*/
			printf("%s\n接收中...\n",message);
			upload_file(buffer, s);	/*调用函数进行文件接收*/
			sleep(1);	/*用1s时间来进行缓冲*/
		}
		else if(strstr(buffer,"下载文件")!=NULL){	/*收到客户端下载文件的请求*/
			printf("开始上传文件...\n");
			size = recvfrom(s,message,sizeof(message),0,(struct sockaddr*)&client_addr,&len);/*从客户端读取数据*/
			printf("%s\n上传中....\n",message);
			download_file(buffer,s);		/*调用函数进行文件上传*/
			sleep(1);	/*用1s时间来进行缓冲*/
		}
		else {   /*不上传、下载文件则进行消息交互*/
			/*构建响应字符,为接收到客户端字节的数量*/
			sprintf(buffer, "收到你的消息啦!\n");
			sendto(s,buffer,sizeof(buffer),0,(struct sockaddr*)&client_addr,sizeof(client_addr));/*发给客户端*/
		}
		
	}	
}

void get_filename(char buffer[]){	/*从buffer字符串中提取出文件名*/
	memset(file_name,0,strlen(file_name));	/*因为file_name是全局变量,所以需要先清零*/
	int j=0;
	for(int i=0;i<strlen(buffer);i++)
	if((buffer[i]>='a'&&buffer[i]<='z')||(buffer[i]>='A'&&buffer[i]<='Z')||(buffer[i]>='0'&&buffer[i]<='9')||(buffer[i]=='.'))
		file_name[j++]=buffer[i];
}

void download_file(char buffer[],int s){		/*服务端接受文件函数*/
	get_filename(buffer);	/*取出文件名*/

	int fd = open(file_name,O_RDONLY);	//打开文件
	if(fd < 0){
		char message2[] = "打开文件失败!上传失败!";
		printf("%s\n",message2);
		/*将打开文件失败的消息传送到客户端,防止客户端卡死*/
		sendto(s,message2,sizeof(message2),0,(struct sockaddr*)&client_addr,sizeof(client_addr));
		return ;
	}
	char message[1024]={0};
	while(read(fd,message,1000)){//每次读取1000大小的内容
		/*将读取到的文件内容上传的客户端*/
		sendto(s,message,sizeof(message),0,(struct sockaddr*)&client_addr,sizeof(client_addr));
		sleep(1);	/*每上传一行就暂停1秒,防止客户端来不及接收消息*/
	}
	strcpy(message,"上传结束");
	/*将上传结束的消息传递给客户端*/
	sendto(s,message,sizeof(message),0,(struct sockaddr*)&client_addr,sizeof(client_addr));
	printf("\n上传成功\n");
	close(fd);
	return;	
}

void upload_file(char buffer[],int s){	/*服务端上传文件函数*/
	get_filename(buffer);	/*获取文件名*/

	int fd = open(file_name,O_WRONLY|O_CREAT,S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
	/*打开文件,如果文件不存在会新建一个,同时设置参数,防止open文件加权限使文件无法打开*/
	if(fd < 0){
		printf("打开文件失败!接收失败!\n");
		/*因为此时客户端处于持续发送消息状态,所以把消息传回客户端的话可能会有bug,所以不将消息回传*/
		return;
	}
	size_t size=0;
	char message[1024]={0};		/*创建一个字符串用来传递消息*/
	while(1){
		size = recvfrom(s,message,sizeof(message),0,(struct sockaddr*)&client_addr,&len);
		if(size < 0){		/*没有数据*/
			printf("接收失败!\n");
			return;
		}
		if(strstr(message,"打开文件失败") != NULL){	/*接受到客户端打开文件失败的消息*/
			printf("客户端打开文件失败!接收失败!\n");
			return;
		}
		
		//printf("接收中...\n");
		if(strstr(message,"上传结束")!=NULL) break;	/*接收到上传结束的消息就退出死循环*/
		write(fd,message,1000);
	}
	printf("接收成功!\n");
	close(fd);/*关闭文件*/
	return;
}

客户端代码:

#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#define PORT 8888	/*侦听端口地址*/
#define Maxline 1024	/*最大读取文件行数*/

char file_name[105];	/*设置一个全局变量用来保存文件名*/
struct sockaddr_in server_addr;		/*服务器地址结构*/
socklen_t len = sizeof(server_addr);	/*服务端地址结构长度*/

void get_filename(char buffer[]);/*从buffer字符串中提取出文件名*/
void upload_file(char buffer[],int s);		/*文件上传函数*/
void download_file(char buffer[],int s);	/*接收文件函数*/
void process_conn_client(int s);   		/*客户端通信函数*/

int main(int argc, char *argv[])
{
	int s;		/*s为socket描述符*/
	s = socket(AF_INET, SOCK_STREAM, 0); 	/*建立一个流式套接字 */
	if(s < 0){	/*出错*/
		printf("socket error\n");
		return -1;
	}	
	
	/*设置服务器地址*/
	bzero(&server_addr, sizeof(server_addr));	/*清零*/
	server_addr.sin_family = AF_INET;	/*协议族*/
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);	/*本地地址*/
	server_addr.sin_port = htons(PORT);	/*服务器端口*/
	
	/*将用户输入的字符串类型的IP地址转为整型*/
	inet_pton(AF_INET, /*argv[1]*/"127.0.0.1", &server_addr.sin_addr);	
	/*连接服务器*/
	printf("直接输入:上传文件+文件名 即可上传文件\n");
	printf("直接输入:下载文件+文件名 即可下载文件\n");
	printf("其他无特殊情况则保持与服务端进行消息交互\n");
	connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
	process_conn_client(s);		/*客户端处理过程*/
	close(s);	/*关闭连接*/
	return 0;
}


void get_filename(char buffer[]){	/*从buffer字符串中提取出文件名*/
	memset(file_name,0,strlen(file_name));	/*因为file_name是全局变量,所以需要先清零*/
	int j=0;
	for(int i=0;i<strlen(buffer);i++)
		if((buffer[i]>='a'&&buffer[i]<='z')||(buffer[i]>='A'&&buffer[i]<='Z')||(buffer[i]>='0'&&buffer[i]<='9')||(buffer[i]=='.'))
			file_name[j++]=buffer[i];
}

void upload_file(char buffer[],int s){		/*文件上传函数*/
	get_filename(buffer);	/*取出文件名*/

	int fd = open(file_name,O_RDONLY);/*打开文件*/
	if(fd < 0){
		char message2[] = "打开文件失败!上传失败!";
		printf("%s\n",message2);
		/*将打开文件失败的消息传送到服务端,防止服务端卡死*/
		sendto(s,message2,sizeof(message2),0,(struct sockaddr*)&server_addr,sizeof(server_addr));/*发给客户端*/
		return ;
	}
	char message[1024]={0};

	while(read(fd,message,1000)){//每次读1000大小的文件内容
		/*将读取到的文件内容上传的服务端*/
		sendto(s,message,sizeof(message),0,(struct sockaddr*)&server_addr,sizeof(server_addr));/*发给客户端*/
		sleep(1);	/*每上传一行就暂停1秒,防止服务端来不及接收消息*/
	}
	strcpy(message,"上传结束");
	/*将上传结束的消息传递给服务端*/
	sendto(s,message,sizeof(message),0,(struct sockaddr*)&server_addr,sizeof(server_addr));/*发给客户端*/
	printf("\n上传成功\n");
	close(fd);
	return;	
}

void download_file(char buffer[],int s){	/*接收文件*/
	get_filename(buffer);	/*获取文件名*/

	int fd = open(file_name,O_WRONLY|O_CREAT,S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
	/*打开文件,如果文件不存在会新建一个,同时设置参数,防止open文件加权限使文件无法打开*/
	if(fd < 0){
		printf("打开文件失败!接收失败!\n");
		/*因为此时服务端处于持续发送消息状态,所以把消息传回服务端的话可能会有bug,所以不将消息回传*/
		return;
	}
	size_t size=0;
	char message[1024]={0};		/*创建一个字符串用来传递消息*/
	while(1){
		size = recvfrom(s,message,sizeof(message),0,(struct sockaddr*)&server_addr,&len);
		if(size < 0){		/*没有数据*/
			printf("接收失败!\n");
			return;
		}
		if(strstr(message,"打开文件失败") != NULL){	/*接受到客户端打开文件失败的消息*/
			printf("服务端打开文件失败!接收失败!\n");
			return;
		}
		if(strstr(message,"收到你的消息啦!")!=NULL){
			/*经运行检测发现有额外输入文件的数据,这里进行隐形处理*/
			continue;
		}
		
		//printf("接收中...\n");
		if(strstr(message,"上传结束")!=NULL) break;	/*接收到上传结束的消息就退出死循环*/
		write(fd,message,1000);
	}
	printf("接收成功!\n");
	close(fd);/*关闭文件*/
	return;
}

void process_conn_client(int s)
{
	ssize_t size = 0;
	char buffer[1024];	/*数据的缓冲区*/
	char message[1024];	/*用来传递消息*/
	
	while(1){	/*循环处理过程*/
		/*从标准输入中读取数据放到缓冲区buffer中*/
		size = read(0, buffer, 1024);
		if(size > 0){	/*读到数据*/
			sendto(s,buffer,sizeof(buffer),0,(struct sockaddr*)&server_addr,sizeof(server_addr));/*发给服务器*/
			if(strstr(buffer,"上传文件")!=NULL){	/*上传文件*/
				printf("开始上传文件...\n");
				size = recvfrom(s,message,sizeof(message),0,(struct sockaddr*)&server_addr,&len);/*从服务器读取数据*/
				printf("%s\n上传中....\n",message);
				upload_file(buffer, s);		/*调用对应函数进行上传*/
				sleep(1);	/*用1s时间来进行缓冲*/
			}
			else if(strstr(buffer,"下载文件")!=NULL){	/*接收文件*/
				strcpy(message,"客户端开始下载文件...");
				sendto(s,message,sizeof(message),0,(struct sockaddr*)&server_addr,sizeof(server_addr));/*发给服务端*/
				printf("%s\n下载中...\n",message);
				download_file(buffer,s);	/*调用接收文件的函数*/
				sleep(1);	/*用1s时间来进行缓冲*/
			}	
			else {
				size = recvfrom(s,buffer,sizeof(buffer),0,(struct sockaddr*)&server_addr,&len);/*从服务器读取数据*/
				printf("%s\n",buffer);
			}
		}
	}	
}

代码存在的bug和不足

        1、还是上一篇博客的问题,使用了fopen、fgets、fprintf这些读取文本文件的函数,而文件并不只是文本文件,应该使用read和write这些函数读取文件(已解决)

        2、应该使用sendto、recvfrom这些比较好的函数进行同行(已解决)

        3、代码测试结果展示太潦草了,竟然将文字解释写在图片里面,这是一个不好的习惯,应该截个图,然后在图的后面进行解释说明

        4、线程互斥使用错误,线程互斥的概念没有弄清楚。应该是多线程访问同一个资源的时候才进行互斥,简单的说,要有共享资源才需要用到线程互斥。而我看到题目有要求使用线程互斥,不管三七二十一就用了线程互斥。(当然啦,上面的源代码我已经去掉线程互斥了,防止同一时间只能有一个线程进行通信)(已解决)

        5、没有必要专门创建一个主线程来处理,直接就进行服务端的初始化就可以。(这种方式我试过,不过因为不熟悉select的原因,造成会创建多余线程的结果,所以我才这样子做。)

        上面标注已解决的说明代码中已经进行了修改。如果没解决可能是我记混了,可以评论或者私信我。

结语

        这一次的编程实践我是急着做出来的,尽管老师说截止时间不限(这学期完成就行),因为平时时间不多,然后之前的实践也没花多少时间,所以我就想着快点做出来然后交了,免得后面麻烦,没想到一做就是快一周的时间(平时还要上课,写其他作业),然后终于在周末结束的时候做出来了,然后赶紧写报告交了,因为还有其他实验要做。

        结果我竟然是第一个交的,然后下一周的第一次Linux网络编程课老师在最后竟然拿我的报告来点评,当场进行评价,揪出一堆错误(大部分是上面提到过的)。我当场社死。这时候我才发现原来我遇到的问题书后面的章节都有说,但是因为惯性思维,我一直把目光放在布置这个实践的那个章节。所以说思想还是太僵化了,不够灵活。

        课后我赶紧找老师解释了一下老师没理解的问题,然后也和老师聊了一下,后面继续加油吧(xiangbailanle)

猜你喜欢

转载自blog.csdn.net/xiexieyuchen/article/details/129759725