文章目录
一、功能描述
比上一版本:
增加信号量互斥与同步,get put传输大容量文件(分批传输),服务器可以正常退出(垃圾清理),可以远距离外网访问
下一版本优化:
CRC校验,账号密码登录
- 客户端(树莓派)
- 对服务器的操作
- 实现指令 ls,rm,cd,pwd
- 获取,上传文件(get,put)
- 获取主服务器和副服务器pid
- 对客户端本机的操作
- lls,lrm,lcd,lpwd
- 退出,断开服务器(quit)
- 对服务器的操作
- 服务器(虚拟机)
- 处理客户端的命令ls,rm,cd,pwd,get,put,pid
- 详细描述(特点难点)
- 支持多台客户端同时接入,每接入一个客户端,fork一个进程去管理
- 支持一次性上传下载大容量文件
- 主、副服务器协同处理数据
- 主服务器,获取客户端指令放入共享内存,等待副服务器处理,完成后数据传回客户端
- 副服务器,拿出共享内存指令,处理,完成后放回共享内存
- 主、副服务器之间,涉及到临界资源的竞争(使用信号量解决竞争和同步问题)
- 服务器的退出,使用信号传入,使其退出
二、遇到的问题和未解决的问题
-
信号量,客户端退出后,信号量值错乱了
- 解决:semop函数的sem_flag参数
- 0代表阻塞调用
- IPC_NOWAIT代表非阻塞调用
- SEM_UNDO,在进程结束时,相应的操作将被取消
进程没有释放共享资源就退出时,内核将代为释放。
- SEM_UNDO,使信号量值变错乱
sem_flag参数设置为0解决
- 解决:semop函数的sem_flag参数
-
资源竞争问题,共享内存的竞争
- 信号量的同步与互斥
- 使用3个信号量
- 一个互斥共享内存,同一时间只能一个进程访问,一次访问处理一次数据
- 另外2个,实现同步问题
- 放入共享内存,v1操作通知副服务器去处理
- 处理完成,v2操作通知主服务器
-
文件超出共享内存大小(传输大文件)
- 分批次传输
- 文件拆分开,分多次传输
-
外网传输数据错乱
- 错乱后清空socket接收缓存区
- 设置缓存区大小,和发送接收大小一致
- 禁用Nagle算法
- 重传机制
-
未解决bug
- 客户端cd改变服务端位置后,其他客户端的位置也随之改变
- 客户端ip不对
- put不能连续使用
三、代码流程图及进程任务
- 服务器
- 主服务器负责(接收命令,放入共享内存等待处理)
- 启动副服务器
- 客户端接入,fork进程去管理(quit命令退出结束进程)
- 接收网络数据,放入共享内存1,等待数据处理读共享内存2
- 副服务器负责(拿出共享内存命令,处理)
- 读取共享内存1,处理命令,数据放入共享内存2
- 主服务器负责(接收命令,放入共享内存等待处理)
- 客户端
- 等待键盘输入命令
- 判断本机还是远程操控命令
- 发送命令,等待数据传回,输出
- quit进程
- 退出服务器使用
- 客户端使用pid命令,查看主、副服务器进程pid
- 给quit进程分别传入,副服务器pid和主服务器pid
四、为什么使用副服务器
- 一是为了巩固练习前面所学知识,项目需要的知识
- 网络编程
- Linux进程
- Linux文件
- Linux进程通信
- 共享内存
- 信号量
- 信号
- 只有线程没有用到
- 二是代码的可维护
- 代码分离,模块化,便于维护
- 多进程的好处
- 提高性能
- 并行处理,减少等待时间,提高服务器响应速度和处理能力
- 稳定性
- 一个进程出问题不会影响其他进程
- 提高性能
五、内网穿透(实现远程访问)
5.1、Linux安装配置花生壳内网穿透
-
下载
wget “https://down.oray.com/hsk/linux/phddns_5.2.0_amd64.deb” -O phddns_5.2.0_amd64.deb
-
安装
dpkg -i phddns_5.2.0_amd64.deb
-
安装完会给,SN和密码admin,还有配置连接
http://b.oray.com
-
登录上去使用SN登录,然后用已注册花生壳账号扫码激活
-
然后映射:TCP,外网域名,内网ip和端口号
-
点击诊断获取外网ip
-
可以使用外网ip访问服务器了
5.2、外网传输遇到的问题(数据错乱)
5.2.1、概念
原文链接:https://blog.csdn.net/u011146511/article/details/64905331
-
缓存区类型
- 全缓存,当填满标准I/O缓存后才进行实际I/O操作
- 行缓存,当在输入和输出中遇到换行符时,执行真正的I/O操作
- 不带缓存,也就是不进行缓冲
-
socket缓存区
- 创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
- write()/send(),先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器,一旦将数据写入到缓冲区,函数就可以成功返回
- TCP协议独立于 write()/send() 函数,不是写入就会立即发送,取决于当时的网络情况、当前线程是否空闲等诸多因素
-
数据的**“粘包”**问题
- 客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性
- read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理
5.2.2、解决方法
-
减少发送数据大小
- 减小传输结构体data数据大小,避免丢包
-
设置缓存区大小与发送数据大小一致
-
可以通过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"); }
-
-
禁用Nagle算法
-
Nagle算法会将数据合并
-
socketopt函数禁用
int enable = 1; if(setsockopt(s_fd, IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(enable)) != 0){ perror("setsockopt TCP_NODELAY"); }
-
-
清空socket接收缓存区数据
- 知晓缓存区中数据的多少
- 直接读出来就可以
- 不知道缓存区中数据的大小
- close一次socket
- 使用recv来读取,需要等待到超时
- 我是使用的recv+select清理缓存的
- select判断是否可读
- 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 } } } } }
- 知晓缓存区中数据的多少
-
重传机制
-
数据结构体,加入校验位,校验位内容本次传输命令的int型
-
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;
}
七、项目演示
虚拟机:
- 查看vi a.sh 运行编译 ./a.sh
- cp mycl_ARM到共享文件
- 运行服务器./myser
- mycl_ARM放入树莓派
- ls -l贪吃蛇文件a.out get cur.c
树莓派:
- 内网访问,pwd cd进入temporary文件夹
- ls get a.out get cur.c 退出
- ls -l比较
- 外网访问,pwd rm *
- put a.out put cur.c
- pid 退出
虚拟机:
- quit退出服务器 ./b.sh
- 查看贪吃蛇文件 运行a.out 退出
Linux文件服务站