ftp云盘(3.0)

一、功能描述

比上一版本:
增加信号量互斥与同步,get put传输大容量文件(分批传输),服务器可以正常退出(垃圾清理),可以远距离外网访问

下一版本优化:
CRC校验,账号密码登录

  1. 客户端(树莓派)
    1. 对服务器的操作
      1. 实现指令 ls,rm,cd,pwd
      2. 获取,上传文件(get,put)
      3. 获取主服务器和副服务器pid
    2. 对客户端本机的操作
      1. lls,lrm,lcd,lpwd
    3. 退出,断开服务器(quit)
  2. 服务器(虚拟机)
    1. 处理客户端的命令ls,rm,cd,pwd,get,put,pid
  3. 详细描述(特点难点)
    1. 支持多台客户端同时接入,每接入一个客户端,fork一个进程去管理
    2. 支持一次性上传下载大容量文件
    3. 主、副服务器协同处理数据
      1. 主服务器,获取客户端指令放入共享内存,等待副服务器处理,完成后数据传回客户端
      2. 副服务器,拿出共享内存指令,处理,完成后放回共享内存
      3. 主、副服务器之间,涉及到临界资源的竞争(使用信号量解决竞争和同步问题)
    4. 服务器的退出,使用信号传入,使其退出

二、遇到的问题和未解决的问题

  1. 信号量,客户端退出后,信号量值错乱了

    1. 解决:semop函数的sem_flag参数
      1. 0代表阻塞调用
      2. IPC_NOWAIT代表非阻塞调用
      3. SEM_UNDO,在进程结束时,相应的操作将被取消
        进程没有释放共享资源就退出时,内核将代为释放。
    2. SEM_UNDO,使信号量值变错乱
      sem_flag参数设置为0解决
  2. 资源竞争问题,共享内存的竞争

    1. 信号量的同步与互斥
    2. 使用3个信号量
      1. 一个互斥共享内存,同一时间只能一个进程访问,一次访问处理一次数据
      2. 另外2个,实现同步问题
        1. 放入共享内存,v1操作通知副服务器去处理
        2. 处理完成,v2操作通知主服务器
  3. 文件超出共享内存大小(传输大文件)

    1. 分批次传输
    2. 文件拆分开,分多次传输
  4. 外网传输数据错乱

    1. 错乱后清空socket接收缓存区
    2. 设置缓存区大小,和发送接收大小一致
    3. 禁用Nagle算法
    4. 重传机制
  5. 未解决bug

    1. 客户端cd改变服务端位置后,其他客户端的位置也随之改变
    2. 客户端ip不对
    3. put不能连续使用

三、代码流程图及进程任务

  1. 服务器
    1. 主服务器负责(接收命令,放入共享内存等待处理)
      1. 启动副服务器
      2. 客户端接入,fork进程去管理(quit命令退出结束进程)
      3. 接收网络数据,放入共享内存1,等待数据处理读共享内存2
    2. 副服务器负责(拿出共享内存命令,处理)
      1. 读取共享内存1,处理命令,数据放入共享内存2
  2. 客户端
    1. 等待键盘输入命令
    2. 判断本机还是远程操控命令
    3. 发送命令,等待数据传回,输出
  3. quit进程
    1. 退出服务器使用
    2. 客户端使用pid命令,查看主、副服务器进程pid
    3. 给quit进程分别传入,副服务器pid和主服务器pid
      在这里插入图片描述

四、为什么使用副服务器

  1. 一是为了巩固练习前面所学知识,项目需要的知识
    1. 网络编程
    2. Linux进程
    3. Linux文件
    4. Linux进程通信
      1. 共享内存
      2. 信号量
      3. 信号
    5. 只有线程没有用到
  2. 二是代码的可维护
    1. 代码分离,模块化,便于维护
  3. 多进程的好处
    1. 提高性能
      1. 并行处理,减少等待时间,提高服务器响应速度和处理能力
    2. 稳定性
      1. 一个进程出问题不会影响其他进程

五、内网穿透(实现远程访问)

5.1、Linux安装配置花生壳内网穿透

  1. 下载

    wget “https://down.oray.com/hsk/linux/phddns_5.2.0_amd64.deb” -O phddns_5.2.0_amd64.deb

  2. 安装

    dpkg -i phddns_5.2.0_amd64.deb

  3. 安装完会给,SN和密码admin,还有配置连接

    http://b.oray.com

  4. 登录上去使用SN登录,然后用已注册花生壳账号扫码激活

  5. 然后映射:TCP,外网域名,内网ip和端口号

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mfm9JUtm-1679578323784)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1679320276890.png)]

  6. 点击诊断获取外网ip

  7. 可以使用外网ip访问服务器了

5.2、外网传输遇到的问题(数据错乱)

5.2.1、概念

原文链接:https://blog.csdn.net/u011146511/article/details/64905331

  1. 缓存区类型

    1. 全缓存,当填满标准I/O缓存后才进行实际I/O操作
    2. 行缓存,当在输入和输出中遇到换行符时,执行真正的I/O操作
    3. 不带缓存,也就是不进行缓冲
  2. socket缓存区

    1. 创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
    2. write()/send(),先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器,一旦将数据写入到缓冲区,函数就可以成功返回
    3. TCP协议独立于 write()/send() 函数,不是写入就会立即发送,取决于当时的网络情况、当前线程是否空闲等诸多因素

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-01L17Spi-1679578323786)(C:\Users\戴尔\AppData\Roaming\Typora\typora-user-images\1679471163273.png)]

  3. 数据的**“粘包”**问题

    1. 客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性
    2. read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理

5.2.2、解决方法

  1. 减少发送数据大小

    1. 减小传输结构体data数据大小,避免丢包
  2. 设置缓存区大小与发送数据大小一致

    1. 可以通过socketopt函数设置

      int recvbuf_size = 1024;//接收缓存大小
      int sendbuf_size = 1024;//发送缓存大小
      if(setsockopt(s_fd, SOL_SOCKET, SO_RCVBUF, &recvbuf_size, sizeof(recvbuf_size)) != 0){
          perror("setsockopt SO_RCVBUF");
      }
      if(setsockopt(s_fd, SOL_SOCKET, SO_SNDBUF, &sendbuf_size, sizeof(sendbuf_size)) != 0){
          perror("setsockopt SO_SNDBUF");
      }
      
  3. 禁用Nagle算法

    1. Nagle算法会将数据合并

    2. socketopt函数禁用

      int enable = 1;
      if(setsockopt(s_fd, IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(enable)) != 0){
          perror("setsockopt TCP_NODELAY");
      }
      
  4. 清空socket接收缓存区数据

    1. 知晓缓存区中数据的多少
      1. 直接读出来就可以
    2. 不知道缓存区中数据的大小
      1. close一次socket
      2. 使用recv来读取,需要等待到超时
    3. 我是使用的recv+select清理缓存的
      1. select判断是否可读
      2. recv读取套接字实现清空缓存
    /******************************************************
      清缓存区函数
      清除tcp接收缓存区,100ms
      参数:
      网络描述符
     *******************************************************/
    int qk(int sockfd)
    {
    	int n = 0;
    	char buf[1024]; //读取大小
    	fd_set read_fds; // 定义fd_set类型的变量,用来存储需要监控的文件描述符
    	FD_ZERO(&read_fds); // 将fd_set变量清空
    	FD_SET(sockfd, &read_fds); // 将套接字加入到fd_set变量中
    	struct timeval timeout;
    	timeout.tv_sec = 0; // 设置select函数的超时时间为秒
    	timeout.tv_usec = 100000;	//us
    	while (1) {
    		// 调用select函数,检查套接字是否可读
    		int ret = select(sockfd + 1, &read_fds, NULL, NULL, &timeout);
    		if (ret < 0) { // select函数调用失败
    			perror("select");
    			return -1;
    		}
    		else if (ret == 0) { // select函数超时,说明清空完毕
    			//printf("No data available within 5 seconds.\n");
    			return 0;
    		}
    		else { // select函数返回套接字可读
    			if (FD_ISSET(sockfd, &read_fds)) { // 检查套接字是否可读
    				n = recv(sockfd, buf, sizeof(struct Mcc) , MSG_DONTWAIT); // 读取套接字中的数据,并设置为非阻塞模式
    				if (n < 0) { // 读取数据失败
    					perror("recv");
    					return -1;
    				}
    				else if (n == 0) { // 服务器关闭了连接
    					printf("Connection closed by the server.\n");
    					return -1;
    				}
    				else { // 读取数据成功
    					// ignore the received data
    				}
    			}
    		}
    	}
    }
    
  5. 重传机制

    1. 数据结构体,加入校验位,校验位内容本次传输命令的int型

    2. get,put,加入剩余传输文件长度和上次差值比较,不等于最大传输证明丢包,通知重发

      最后一次传输,不在丢包范畴

原文链接: linux socket清空缓存

六、代码部分展示

主服务器

/*********************************************2023.3.19******************************************************
  Linux文件服务站
  环境Ubuntu
  功能:服务器
  处理客户端的命令ls,rm,cd,pwd,get,put,pid
  使用:
  1.编译./a.sh
  2.运行./myser 127.0.0.1 8888
  3.结束服务器./quit ser1pid号 serpid号(客户端发送命令pid获取)
 **************************************************************************************************************/
#include "ser.h"
int s_fd;	//网络套接字

/******************************************************
  信号处理函数
  功能:退出主服务器进程
 *******************************************************/
void handler(int signum)
{
	switch(signum){
		case 2:
			close(s_fd);	//关闭网络描述符
			printf("ser: s_fd close success...\n");
			break;
		default:break;
	}
	printf("ser: quit\n");
	exit(0);
}

int main(int argc,char **argv)
{
	if(argc != 3){  			//判断输入参数是否正确
		printf("usage: command listen_port\n");
		goto Exit;
	}

	int c_fd;	//网络描述符
	int jieshou;	//等待连接用
	int pid,pid2;	//进程,一个运行ser1,一个等待连接对接客户端
	char *clientip=NULL;	//客户端ip

	struct sockaddr_in addr;	//信息,ip端口号
	struct sockaddr_in c_addr;

	memset(&c_addr,0,sizeof(struct sockaddr_in));	//初始化网络信息
	memset(&addr,0,sizeof(struct sockaddr_in));
	addr.sin_family = AF_INET;				//初始化协议ip端口号
	addr.sin_port = htons(atoi(argv[2]));
	inet_aton(argv[1],&addr.sin_addr);

	signal(SIGINT,handler);	//信号
	printf("ser: ser pid = %d\n", getpid());	//ser的pid
	s_fd = socket(AF_INET,SOCK_STREAM,0);		//创建套接字
	if(s_fd < 0){
		perror("socket");
		goto Exit;
	}
	
	if(socket_init(s_fd) != 0){		//设置socket缓冲区,禁用Nagle算法
		perror("socket");
		goto Sfd;
	}
	if(bind(s_fd,(struct sockaddr *)&addr,sizeof(struct sockaddr_in)) < 0){	//添加信息
		perror("bind");
		goto Sfd;
	}
	if(listen(s_fd,10) < 0){		//监听网络
		perror("listen");
		goto Sfd;
	}

	pid = fork();	//创建进程
	if(pid < 0){
		perror("fork error!");
		goto Sfd;
	}else if(pid == 0){	//运行副服务器
		system("./ser1");
		printf("ser: ser1 quit\n");
		exit(0);
	}else{
		jieshou = sizeof(struct sockaddr_in);
		while(1){
			c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&jieshou);	//等待连接
			if(c_fd < 0){
				perror("accept");
				goto Sfd;
			}else{
				printf("------------------------------------------------\n");
				clientip = inet_ntoa(c_addr.sin_addr);
				printf("ser: client IP:%s\n",clientip);	//打印客户端ip地址
				pid2 = fork();						//创建进程处理客户端
				if(pid2 < 0){
					perror("fork error!");
					goto Cfd;
				}else if(pid2 == 0){			
					serv_client(c_fd, clientip);	//接受数据处理
					close(c_fd);	//关闭通道
					printf("ser: client IP:%s quit\n",clientip);
					exit(0);
				}
			}
		}	

		close(s_fd);
	}

	return 0;
Cfd:
	close(c_fd);
Sfd:
	close(s_fd);
Exit:
	return -1;
}

副服务器

/*********************************************2023.3.19******************************************************
  副服务器
  环境Ubuntu
  任务:
  1.创建共享内存
  2.创建信号量
  3.处理共享内存里命令
 **************************************************************************************************************/
#include "ser.h"

struct Mcc *shmatadd1;	//共享内存连接地址
struct Mcc *shmatadd2;
int sem_id;	//信号量id
int shm_id1;	//共享内存id
int shm_id2;

/******************************************************
  信号处理函数
  功能:
  1.删除信号量,共享内存,退出结束进程
 *******************************************************/
void handler(int signum)
{
	switch(signum){
		case 2:
			shmdt(shmatadd1);		//断开连接
			shmdt(shmatadd2);
			shmctl(shm_id1,IPC_RMID,0);	//销毁共享内存
			shmctl(shm_id2,IPC_RMID,0);
			semctl(sem_id,0,IPC_RMID);		//销毁信号量
			semctl(sem_id,1,IPC_RMID);
			semctl(sem_id,2,IPC_RMID);
			printf("ser1: shm sem clean up success...\n");
			break;
		default:break;
	}
	printf("ser1: SIGINI quit\n");
	exit(0);
}

int main(int argc, char **argv)
{
	struct Mcc buf;
	signal(SIGINT,handler);//信号
	printf("ser1: pid = %d\n", getpid());

	if(Connect_sem_init(&sem_id, 'c', 3) != 0){	//连接创建信号量
		goto Exit;
	}
	printf("ser1: sem success...\n");

	if(Connect_shm(&shm_id1, &shmatadd1, 'a') != 0){	//连接创建共享内存
		goto Shm1;
	}
	if(Connect_shm(&shm_id2, &shmatadd2, 'b') != 0){
		goto Shm2;
	}
	printf("ser1: shm success...\n");

	while(1){
		memset(&buf,0,sizeof(buf));
		//printf("ser1: mem0=%d,mem1=%d,mem2=%d\n",semctl(sem_id,0,GETVAL),semctl(sem_id,1,GETVAL),semctl(sem_id,2,GETVAL));//信号量值
		pGetKey(sem_id, 1);	//信号量同步
		memcpy(&buf,shmatadd1,sizeof(buf));	//拿出共享内存1命令
		ser1_handle(&buf);				//处理命令
		memcpy(shmatadd2,&buf,sizeof(buf));	//数据放入共享内存2
		vPutBackKey(sem_id, 2);	//同步
	}

	shmdt(shmatadd1);		//断开连接
	shmdt(shmatadd2);
	shmctl(shm_id1,IPC_RMID,0);	//销毁
	shmctl(shm_id2,IPC_RMID,0);
	semctl(sem_id,0,IPC_RMID);
	semctl(sem_id,1,IPC_RMID);
	semctl(sem_id,2,IPC_RMID);
	printf("ser1: quit\n");
	return 0;

Shm2:
	shmctl(shm_id2,IPC_RMID,0);	
Shm1:
	shmctl(shm_id1,IPC_RMID,0);
	semctl(sem_id,0,IPC_RMID);
	semctl(sem_id,1,IPC_RMID);
	semctl(sem_id,2,IPC_RMID);
Exit:
	printf("ser1: quit -1\n");
	return -1;
}

客户端

/*********************************************2023.3.19******************************************************
  Linux文件服务站
  环境Ubuntu
  功能:客户端
  用户输入命令,传入服务器执行lls,lrm,lcd,lpwd,ls,rm,cd,pwd,get,put,pid,quit
  使用:
  1.编译./a.sh
  2.运行./mycl 服务器ip 端口号
  3.输入quit
 **************************************************************************************************************/
#include "ser.h"

int main(int argc,char **argv)
{
	if(argc != 3){	//判断输入参数个数是否正确
		printf("usage: command listen_port\n");
		goto Exit;
	}
	
	int c_fd;	//网络描述符
	struct sockaddr_in addr;	//网络信息结构体,协议,ip,端口号

	memset(&addr,0,sizeof(struct sockaddr_in));	//清空信息结构体
	addr.sin_family = AF_INET;	//IPv4因特网域
	addr.sin_port = htons(atoi(argv[2]));	//端口号
	inet_aton(argv[1],&addr.sin_addr);	//IP地址
	c_fd = socket(AF_INET,SOCK_STREAM,0);	//创建网络套接字
	if(c_fd < 0){
		perror("socket");
		goto Exit;
	}
	if(socket_init(c_fd) != 0){		//设置socket缓冲区,禁用Nagle算法
		perror("socket");
		goto Cfd;
	}

	if(connect(c_fd,(struct sockaddr *)&addr,sizeof(struct sockaddr_in)) == -1){	//连接网络
		perror("connect");
		goto Cfd;
	}
	printf("client......\n");	//客户端连接成功
	printf("client IP:%s\n",inet_ntoa(addr.sin_addr));	//打印客户端ip地址
	client_handle(c_fd);	//客户端用户输入命令,处理命令发送网络服务器
	close(c_fd);	//关闭网络描述符

	printf("client quit\n");	//客户端退出
	return 0;

Cfd:
	close(c_fd);
Exit:
	return -1;
}

七、项目演示

虚拟机:

  1. 查看vi a.sh 运行编译 ./a.sh
  2. cp mycl_ARM到共享文件
  3. 运行服务器./myser
  4. mycl_ARM放入树莓派
  5. ls -l贪吃蛇文件a.out get cur.c

树莓派:

  1. 内网访问,pwd cd进入temporary文件夹
  2. ls get a.out get cur.c 退出
  3. ls -l比较
  4. 外网访问,pwd rm *
  5. put a.out put cur.c
  6. pid 退出

虚拟机:

  1. quit退出服务器 ./b.sh
  2. 查看贪吃蛇文件 运行a.out 退出

Linux文件服务站

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dz小伟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值