【计算机网络】基于TCP进行socket编程——实现客户端到服务端远程命令行操作
🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:计算机网络
🌹往期回顾🌹: 【计算机网络】基于UDP进行socket编程——实现服务端与客户端业务
🔖流水不争,争的是滔滔不息
一、TCPsocket编程
TCP Socket 编程是一种基于传输控制协议(TCP)的网络通信方式,用于在客户端和服务器之间建立可靠的双向连接。TCP 提供面向连接、可靠的数据传输,适用于需要确保数据完整性和顺序的应用场景,如网页浏览、文件传输等。
1.1基本接口
1.1.1通用接口
创建套接字
int socket(int domain, int type, int protocol);
参数,IPV4:AF_INET,TCP:SOCK_STREAM,标志位一般为0。失败返回-1。
关闭套接字
int close(int fd);
发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
返回值是返回发送的字节数,第一个参数套接字,第二个参数buffer,第三个参数是buffer大小,第四个参数是标志位。
接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
返回值是返回接收到的字节数,第一个参数套接字,第二个参数buffer,第三个参数是buffer大小,第四个参数是标志位。
TCP中发送和接收,用write和read也行。
1.1.2服务器主要流程
绑定端口号和ip
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
绑定本地ip和端口
监听
int listen(int sockfd, int backlog);
监听端口,backlog表示等待连接的队列的最大长度。
accept,返回新的socket描述符用于通信
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
接收客户端的请求,返回一个新的socket描述符,原来的socket描述符用于监听,新的用来和客户端通信。
1.1.3客户端主要流程
connect
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
主动连接服务器端的socket。
1.2 接口理解
服务端
- bind是绑定本地主机的ip和端口号。
- listen是开始监听,把绑定好的socket变成监听状态,等待客户端来连接。打个比喻也就是说listen相当于“开门营业”让内核知道这个socket是服务端的,准备接电话了。backlog表示等待连接的队列的最大长度,“最多有多少个客户拍队”。
- accept是接收连接,从监听 socket 中拿出一个连接请求,生成一个新的 socket,用于和客户端通信。你可以理解成服务员开门迎客,把客户带到一个新的包间聊天。返回的新 socket 是专用于与这个客户端通信的,原来的 socket 继续监听下一个连接。
客户端
connect是客户端主动给服务端发送连接请求,去找服务端,可以理解为客户端给客户端打电话。
二、TCP实现客户端到服务器远程网络命令行操作
Tcpserver.hpp
#include "Common.hpp"using namespace std;
using namespace LogModule;using func_t =function<string(const string&,InetAddr&)>;const static int defaultsockfd=-1;
const static int backlog=8;class Tcpserver : public NoCopy
{
public:Tcpserver(uint16_t port,func_t func):_port(port),_listensockfd(defaultsockfd),_sockfd(defaultsockfd),_isrunning(false),_func(func){}void Init(){_listensockfd=socket(AF_INET,SOCK_STREAM,0); //创建套接字if(_listensockfd<0){LOG(LogLevel::FATAL)<<"socket error";exit(SOCKET_ERR);}LOG(LogLevel::INFO)<<"socket success";struct sockaddr_in local;memset(&local,0,sizeof(local));local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY;int n=bind(_listensockfd,(struct sockaddr*)&local,sizeof(local)); //绑定if(n<0){LOG(LogLevel::FATAL)<<"bind error";exit(BIND_ERR);}LOG(LogLevel::INFO)<<"bind sucess";n=listen(_listensockfd,backlog); //监听if(n<0){LOG(LogLevel::FATAL)<<"listen error";exit(LISTEN_ERR);}LOG(LogLevel::INFO)<<"listen sucess";}void Server(int _sockfd,InetAddr& peer){while(true){char buffer[1024];ssize_t n=read(_sockfd,buffer,sizeof(buffer)-1);if(n>0){buffer[n]=0;LOG(LogLevel::DEBUG) << peer.StringAddr() << " #" << buffer;string echo_string=_func(buffer,peer);write(_sockfd,echo_string.c_str(),echo_string.size());}else if(n==0){LOG(LogLevel::DEBUG)<<"退出";close(_sockfd);break;}else{LOG(LogLevel::FATAL)<<"异常";close(_sockfd);break;}}}class ThreadDate{public:ThreadDate(int fd,InetAddr& ad,Tcpserver* th):sockfd(fd),addr(ad),tsvr(th){}~ThreadDate(){}int sockfd;InetAddr addr;Tcpserver* tsvr;};static void* Routine(void* args) //线程处理任务{pthread_detach(pthread_self());ThreadDate* td=static_cast<ThreadDate*>(args);td->tsvr->Server(td->sockfd,td->addr);delete td;return nullptr;}void Run(){_isrunning=true;while(_isrunning){struct sockaddr_in peer; //客户端(网络)传来socklen_t len=sizeof(peer);_sockfd=accept(_listensockfd,(struct sockaddr*)&peer,&len); //接收连接if(_sockfd<0){LOG(LogLevel::WARNING)<<"accept error";continue;}LOG(LogLevel::INFO)<<"accept sucess";InetAddr addr(peer); //转为主机序列pthread_t tid; //每当一个客户端连接,就创建线程去服务客户端想要干的事ThreadDate* td=new ThreadDate(_sockfd,addr,this);pthread_create(&tid,nullptr,Routine,td); //真正创建线程的,把td就是那些信息传进去到Routine}_isrunning=false;}~Tcpserver(){}
private:bool _isrunning;int _listensockfd;int _sockfd;uint16_t _port;func_t _func;
};
服务器初始化
void Init(){_listensockfd=socket(AF_INET,SOCK_STREAM,0); //创建套接字if(_listensockfd<0){LOG(LogLevel::FATAL)<<"socket error";exit(SOCKET_ERR);}LOG(LogLevel::INFO)<<"socket success";struct sockaddr_in local;memset(&local,0,sizeof(local));local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY;int n=bind(_listensockfd,(struct sockaddr*)&local,sizeof(local)); //绑定if(n<0){LOG(LogLevel::FATAL)<<"bind error";exit(BIND_ERR);}LOG(LogLevel::INFO)<<"bind sucess";n=listen(_listensockfd,backlog); //监听if(n<0){LOG(LogLevel::FATAL)<<"listen error";exit(LISTEN_ERR);}LOG(LogLevel::INFO)<<"listen sucess";}
创建套接字,绑定本地ip和端口号,把socket搞成监听状态。
有客户端connect给服务端发送连接请求,开始执行
void Run(){_isrunning=true;while(_isrunning){struct sockaddr_in peer; //客户端(网络)传来socklen_t len=sizeof(peer);_sockfd=accept(_listensockfd,(struct sockaddr*)&peer,&len); //接收连接if(_sockfd<0){LOG(LogLevel::WARNING)<<"accept error";continue;}LOG(LogLevel::INFO)<<"accept sucess";InetAddr addr(peer); //转为主机序列pthread_t tid; //每当一个客户端连接,就创建线程去服务客户端想要干的事ThreadDate* td=new ThreadDate(_sockfd,addr,this);pthread_create(&tid,nullptr,Routine,td); //真正创建线程的,把td就是那些信息传进去到Routine}_isrunning=false;}
accept,服务端接收客户端的连接,创建新的套接字。这里采用来一个客户端信息创建一个线程的方式。ThreadDate* td=new ThreadDate(_sockfd,addr,this);
把连接信息打包成 ThreadDate 对象(this是当前类的对象)。为啥封装成结构体?pthread_create 的第四个参数只能传 void*,所以你封装成 ThreadDate*,传给线程函数后可以解包用。下面是ThreadDate的构造的代码
class ThreadDate{public:ThreadDate(int fd,InetAddr& ad,Tcpserver* th):sockfd(fd),addr(ad),tsvr(th){}~ThreadDate(){}int sockfd;InetAddr addr;Tcpserver* tsvr;};static void* Routine(void* args) //线程处理任务{pthread_detach(pthread_self());ThreadDate* td=static_cast<ThreadDate*>(args);td->tsvr->Server(td->sockfd,td->addr);delete td;return nullptr;}
Routine 是线程处理函数;传进去的参数是 ThreadDate* 类型的 td。在Routine中线程分离,解包然后通过td调用server进入回调函数。
void Server(int _sockfd,InetAddr& peer){while(true){char buffer[1024];ssize_t n=read(_sockfd,buffer,sizeof(buffer)-1);if(n>0){buffer[n]=0;LOG(LogLevel::DEBUG) << peer.StringAddr() << " #" << buffer;string echo_string=_func(buffer,peer);write(_sockfd,echo_string.c_str(),echo_string.size());}else if(n==0){LOG(LogLevel::DEBUG)<<"退出";close(_sockfd);break;}else{LOG(LogLevel::FATAL)<<"异常";close(_sockfd);break;}}}
从缓冲区中读数据,然后回调函数对客户端传来的信息进行解析,然后在返回去。
Tcpclient.cc
#include "Common.hpp"using namespace std;
using namespace LogModule;int main(int argc, char *argv[])
{string server_ip = argv[1];uint16_t server_port = stoi(argv[2]);int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){LOG(LogLevel::FATAL) << "socket error";exit(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket success";struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());int n=connect(sockfd,(struct sockaddr*)&server,sizeof(server));if(n<0){LOG(LogLevel::FATAL)<<"connect error";exit(CONNECT_ERR);}LOG(LogLevel::INFO)<<"connect sucess";while(true){string line;cout<<"请输入"<<endl;getline(cin,line);write(sockfd,line.c_str(),line.size());char buffer[1024];int n=read(sockfd,buffer,sizeof(buffer)-1);if(n>0){buffer[n]=0;cout<<buffer<<endl;}}close(sockfd);return 0;
}
创建套接字,connect去连接服务器,连接上服务器,给服务器发信息,读服务器的应答。
Command.hpp
#pragma once#include <iostream>
#include <string>
#include <cstdio>
#include <set>
#include "Command.hpp"
#include "InetAddr.hpp"
#include "Log.hpp"using namespace LogModule;class Command
{
public:std::string Execute(const std::string &cmd, InetAddr &addr){std::string who = addr.StringAddr();// 2. 执行命令FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){return std::string("你要执行的命令不存在: ") + cmd;}std::string res;char line[1024];while(fgets(line, sizeof(line), fp)){res += line;}pclose(fp);std::string result = who + "execute done, result is: \n" + res;LOG(LogLevel::DEBUG) << result;return result;}~Command(){}
private:
};
执行命令的方法
Tcpserver.cc
#include "Tcpserver.hpp"
using namespace std;
using namespace LogModule;int main(int argc,char* argv[])
{uint16_t port=stoi(argv[1]);Enable_Console_Log_Strategy(); // 启用控制台输出Command cmd;unique_ptr<Tcpserver> u=make_unique<Tcpserver>(port,[&cmd](const string& buffer,InetAddr& peer){return cmd.Execute(buffer,peer);});u->Init();u->Run();return 0;
}
lambda表达式,对服务端的回调函数进行处理,让客户端发来去的信息去执行命令行操作。
客户端到服务端远程命令行操作:源码