实现原理
首先我们要有个客户端和一个服务器,客户端向服务器传递命令。而服务器收到命令后创建一个管道,并fork一个子进程。随后子进程解析命令,再把标准输出换成管道文件,因为命令行命令是自动输出到显示器的,所以我们要把命令的结果重定向到管道文件。然后服务器主进程等待子进程返回的结果,并把结果返回给客户端。
客户端需要做的事情:
1. 读取用户输入的命令
2. 把输入的命令发送给服务器
3. 读取服务器返回的结果并回显显示器
服务器需要做的事情:
1. 读取客户端发来的命令
2. 创建一个管道
3. 创建一个子进程
4. 关闭管道的写端(管道是单向通信的)
5. 等待子进程的返回结果(返回结果会在管道中)
6. 把结果发送给客户端
服务器的子进程需要做的事情
1. 关闭管道读端(管道会继承自父进程)
2. 把字符串拆分,例如: ls -a -l 拆分成ls,a,l这样的单个字符串
3. 把标准输出替换成管道的写端(这种行为也叫重定向)
4. 把拆分的字符串组织起来进行进程替换
server端代码
我们明白了shell的实现原理之后,那么我们先来编写服务器。服务器负责接收客户端发来的命令把把命令递交给子进程,由子进程进行程序替换来返回结果。子进程的返回结果本来会返回到显示器上,但是我们修改了子进程的标准输出,那么就会重定向到管道中。
server.cc代码:
#include "server.hpp"
#include <memory>
#include <unistd.h>
#include <fcntl.h>
#include <vector>
#include <sys/wait.h>
#include <cstring>
//请求处理函数
void CommandMessage(int sockfd,std::string ip , uint16_t port, std::string message)
{
//1创建管道
int fds[2];
if(pipe(fds) != 0)
{
std::cerr << "input pipe failed in " << ip << "-" << port << std::endl;
return;
}
int pid = fork();
if(pid > 0)
{
//父进程关闭写
close(fds[1]);
char buff[1024 * 4] = {0};
waitpid(pid,nullptr,0);
int n = read(fds[0],buff,sizeof buff - 1);
std::cout << buff << std::endl;
//把返回的结果发给客户端
struct sockaddr_in client;
client.sin_addr.s_addr = inet_addr(ip.c_str());
client.sin_port = htons(port);
client.sin_family = AF_INET;
sendto(sockfd,buff,strlen(buff),0,(struct sockaddr*)&client,sizeof client);
}else if(pid == 0)
{
//子进程关闭读
close(fds[0]);
char buff[1024] = {0};
//解析命令行
int idx = 0 ;
std::vector<std::string> cmds;
//把命令行参数分解到cmds中
while(true)
{
int pos = message.find(" ",idx);
if(pos == std::string::npos)
{
// std::cout<< message << pos << std::endl;
cmds.push_back(message.substr(idx,pos - idx));
break;
}
if(idx != pos)
{
cmds.push_back(message.substr(idx,pos - idx));
}
idx = pos + 1;
}
const char* ev[128] = {0}; //存储所有的参数
//把cmds中所有的参数放进ev中
for(int i = 0; i < cmds.size() ;i++){ ev[i] = cmds[i].c_str(); }
dup2(fds[1],1);// 相当于close(1) -> close(fds[1]) -> open(fds[1])
execvp(ev[0],(char* const *)ev); //程序替换
exit(1);
}
}
int main(int argc , char* argv[])
{
if(argc != 2) //命令行参数不为2就退出
{
std::cout << "Usage : " << argv[0] << " bindport" << std::endl; //打印使用手册
exit(1);
}
uint16_t port = atoi(argv[1]); //命令行传的端口转换成16位整形
std::unique_ptr<UdpServer> s(new UdpServer(port,CommandMessage)); //创建UDP服务器,并传入一个回调函数处理请求
s->init(); //初始化服务器,创建 + 绑定
s->start(); //运行服务器
}
server.hpp代码:
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <functional>
typedef std::function<void(int,std::string,uint16_t,std::string)> func_t;
class UdpServer
{
private:
int _sock;
uint16_t _port;
func_t _callback;
public:
UdpServer(uint16_t port,func_t callback): _port(port) ,_callback(callback){ }
~UdpServer() { close(_sock); }
void init()
{
_sock = socket(AF_INET,SOCK_DGRAM,0); //创建套接字
if(_sock < 0)
{
//创建失败
std::cout << "create socket failed...." << std::endl;
abort();
}
//绑定
struct sockaddr_in ser;
ser.sin_port = htons(_port); //填入端口
ser.sin_family = AF_INET; // 填入域
ser.sin_addr.s_addr = INADDR_ANY; //填入IP地址
if(bind(_sock,(sockaddr*)&ser,sizeof ser) != 0) //绑定
{
//绑定失败
std::cout << "bind socket failed...." << std::endl;
abort();
}
}
void start()
{
struct sockaddr_in peer; //对端
socklen_t peer_len = sizeof peer;
char buff[1024] = {0};
while(1)
{
int n = recvfrom(_sock,buff,1023,0,(struct sockaddr*)&peer,&peer_len);
buff[n] = 0;
if(read == 0)
{
std::cout << "one client quit..." << std::endl;
continue;
}else if(read < 0)
{
std::cout << "read error..." << std::endl;
break;
}
//获取客户端的端口和IP
std::string clientip = inet_ntoa(peer.sin_addr);
uint16_t clientport = ntohs(peer.sin_port);
std::cout << buff << std::endl; //回显客户端信息
//调用回调函数处理数据
_callback(_sock,clientip,clientport,buff);
}
}
};
client端代码
client端必须是先给服务端发送数据的,不过首先要先输入命令,然后把命令发给服务器。之后只需要等待服务器传回的结果,再把结果打印到显示器即可。
client.cc代码:
#include "client.hpp"
#include <memory>
int main(int argc , char* argv[])
{
if(argc != 3) //必须 ./client 服务器ip 服务器端口 才能成功运行客户端
{
std::cout << "Usage : " << argv[0] << " serverip serverport" << std::endl;
exit(1);
}
uint16_t port = atoi(argv[2]); //提取服务器的端口
std::string ip = argv[1]; //提取服务器的ip
std::unique_ptr<UdpClient> cli(new UdpClient(port,ip)); //创建客户端
cli->init(); //客户端初始化
cli->start(); //客户端启动!
}
client.hpp代码:
#pragma once
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <arpa/inet.h>
#include <cstdio>
#include <cstring>
class UdpClient
{
public:
UdpClient(uint16_t port , const std::string& ip) : _port(port), _svr_ip(ip){}
~UdpClient(){ close(_sock); }
void init()
{
_sock = socket(AF_INET,SOCK_DGRAM,0);
if(_sock < 0)
{
std::cout << "create socket failed...." << std::endl;
abort();
}
svr.sin_port = htons(_port);
svr.sin_addr.s_addr = inet_addr(_svr_ip.c_str());
svr.sin_family = AF_INET;
}
void start()
{
int i = 1;
char sendbuff[1024] = {0};
while(1)
{
//输入命令行
std::cout << "[XXXX@abcdefg]$ ";
fgets(sendbuff,sizeof sendbuff -1 , stdin);
sendbuff[strlen(sendbuff) - 1] = 0;
std::string message = sendbuff;
//发送命令信息
sendto(_sock,message.c_str(),message.size(),0,(struct sockaddr*)&svr,sizeof svr);
//收服务器请求
char recvbuff[1024 * 4] = {0};
recvfrom(_sock,recvbuff,sizeof recvbuff - 1,0,nullptr,nullptr);
//打印回收到的消息
std::cout << recvbuff;
}
}
private:
int _sock;
uint16_t _port;
std::string _svr_ip;
struct sockaddr_in svr;
};
接下来我们可以看看运行结果:
我们先启动服务器,并且为服务器绑定端口号8080
然后我们启动客户端,输入服务器的ip和对应的端口号8080
然后在客户端中执行各种命令
无论是增加文件还是删除文件,都是可以进行操作的。所以这就实现了我们的一个远程mini版shell。
代码的git地址: