前言
温馨提示:题目和代码可以不看,直接拿代码去测试就明白了。
题目:
在编程实践2基础上,采用多线程技术和I0复用实现一个线程服务多个客户端,服务进程里面的多个线程服务大量客户端。要求:
- 源代码格式化良好并适当注释;
- 除上述明确要求的功能外,还要注意其它问题,比如线程互斥、服务程序支持线程数和每个线程支持客户端算的可配置性等;
3. 提交报告,报告中包括程序源代码和测试效果截图。
测试效果截图:
和编程实践2类似,我创建了三个文件夹,一个代表server端,另外两个代表client和client2两个客户端。
下面进行程序测试,看看客户端能否与服务端通信:
通信正常,接下来我要从客户端client上传文件test.txt文件,然后在client2端将被上传到服务端的test.txt下载到client2。
如下图是文件上传和下载之前各个文件夹中的文件。
客户端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)