基于tcp协议实现远程命令操作功能服务器
TCP是面向连接的,也就是需要连接的,使用Tcp协议的客户端进行通信的时候必须要先和服务器建立连接,这就要求服务器随时都要处于等待连接的状态,这也就是所谓的监听,才能进行通信,而Udp一旦绑定就自动建立连接,可以直接通信;backlog是指服务器中全链接的个数,其实也就是排队个数;还有就是Tcp是面向字节流进行通信的,Udp是面向数据报通信,两者的区别是面向数据报是不可能会发生读不完这种情况的,但
1.tcp服务器实现的过程
1.1 创建套接字 socket

当一个进程使用了socket函数,就会创建一个套接字,并返回一个指向这个套接字的文件描述符,而这个文件描述符比较神奇,就是它不再是像其他文件描述符一样在内存中读取数据,而是在传输层的发送缓冲区中读取数据,向传输层的接收缓冲区中读取数据;
①domain :协议族 分别有三个
AF_INET:IPv4 协议(最常用,比如 192.168.1.1 这种地址)AF_INET6:IPv6 协议AF_UNIX:本地进程间通信(Unix 域套接字,不用网络)
而这些参数未来是会被写到网络层的版本号(Version)字段 里的;
② type :传输的方式
SOCK_STREAM:流式套接字,对应 TCP 协议(可靠、有序、面向连接,打电话一样) SOCK_DGRAM:数据报套接字,对应 UDP 协议(不可靠、无连接,发快递一样)
所谓的流式套接字其实就是面向字节流的传输的套接字,这也是我们今天要学习的,tcp协议使用的传输方式,当我们的参数选择的式SOCK_STREAM的时候,数据的传输天然就有了三次握手建立连接、确认应答(ACK)、 超时重传 、快重传 / 拥塞控制、 滑动窗口流量控制、 乱序重组、去重;这些特性。
③protocal :具体协议
这个参数用来指定通信的具体协议,大部分时候填 0 就行,让系统根据前两个参数自动匹配:
1.2 绑定地址和端口号 bind

首先bind的作用是给刚才创建的socket绑定特定的ip和端口号,底层做了什么呢?把你指定的端口号注册到内核的全局端口监听表(在内核中)里,标记为 “被这个 socket 占用”,其他进程不能再绑定同一个端口,绑定 IP 地址:告诉内核,这个 socket 只接收发给这个 IP 地址的数据包(比如服务器有多张网卡时,可以指定只监听某一张网卡的 IP)。关联 socket 与地址:把 addr 里的 IP 和端口信息,写入 socket 内核结构体中,让后续的 listen()、accept() 知道该监听哪个地址。
值得注意的是,一个进程可以绑定多个端口号,但是一个端口号只能被一个进程绑定;
参数:
① int sockfd
你用 socket() 创建出来的文件描述符,就是要被绑定的那个套接字。
② const struct sockaddr *addr
指向一个通用地址结构体的指针,里面存着你要绑定的IP 地址和端口号。实际写代码时,一般会用更具体的 struct sockaddr_in(IPv4)来填充,再强转成 sockaddr* 传给 bind。
③ socklen_t addrlen
地址结构体 addr 的大小,告诉内核你传进去的地址数据有多长。
为什么服务端必须 bind,客户端可以不用?
服务端:需要固定的 IP 和端口,客户端才能找到它,所以必须 bind 明确地址。
客户端:只需要主动连接服务端,内核会自动分配一个临时端口(ephemeral port),所以不用手动 bind。
1.3 标记套接字为监听状态 listen
干什么呢?为啥要把套接字改成listen状态呢?怎么改的呢?

首先listen之后都做了什么呢?
- 内核通过
fd找到对应的 socket 对象 - 把这个 socket 对象的状态改成
TCP_LISTEN - 为它创建半连接队列和全连接队列
- 把它注册到内核的端口监听表中
半连接队列:半连接队列,就是内核为处于 TCP 三次握手过程中、尚未完成连接的客户端请求开辟的等待队列,也叫 SYN 队列。
全连接队列:是内核为完成TCP三次握手过程的客户端开辟的存储队列,等待被accept()取走;

backlog参数: 直接限制的是已完成三次握手、等待 accept() 取走的连接队列(全连接队列) 的长度 ;
值得注意的是一旦服务器监听到一个客户端,并且这个客户端完成了三次握手,OS就会为这个客户端创建一个socket,当accept的时候就会返回这个客户端的文件描述符,为什么要给创建一个新的文件socket结构体呢?
首先我们前面创建的socket只是用来监听的,它不能用于通信,所以每连上一个客户端,内核就要单独创建独立发送缓冲区、独立接收缓冲区、独立 TCP 状态、序号、重传机制这些都打包就成一个新 socket 内核对象;这个socket专门用于和与它建立连接的客户端通信,而这个新的socket对象的创建本身是创建了一个新的进程;因为这样这个服务器才能让客户端访问服务器中的对应资源;但是对于每次建立一个连接就创建一个进程未免太吃资源了,所以可以使用线程,并且线程天生就可以共享进程的资源;
1.4 接收连接函数 accept( )

作用:从全连接队列中取出完成三次握手的客户端连接,也就是套接字;
参数:
① int sockfd:
告诉内核 “我要从这个监听套接字的全连接队列里,取出一个已完成三次握手的客户端连接”
② struct sockadd *_Nullable restrict addr:
含义:一个指向 struct sockaddr 结构体的指针,用来接收内核返回的客户端地址信息(IP 地址 + 端口号)。
常见用法:实际写代码时,一般会用更具体的 struct sockaddr_in(IPv4)或 sockaddr_in6(IPv6)来接收数据,再强制转换成 struct sockaddr* 传给 accept。
特殊说明:参数上的 _Nullable 表示这个指针可以传 NULL。如果你不关心客户端的地址信息,直接传 NULL 就行。
③ socklen_t * _Nullable restrict addlen:
一个指向 socklen_t 类型变量的指针,用来告诉内核 addr 缓冲区的大小,同时内核会把实际写入的地址信息长度写回这个变量里。
用法细节:
输入时:你要把变量初始化为 sizeof(struct sockaddr_in),告诉内核缓冲区的大小。
输出时:内核会把实际客户端地址的长度更新到这个变量里(一般和初始值相同,但对变长协议族可能不同)。 特殊说明:同样可以传 NULL,前提是 addr 也传了 NULL。
1.5 客户端发起连接connect( )

这个我觉得应该就很好理解吧;connect之后就会触发服务器一直等待连接的监听套接字的三次握手;而udp也有connect函数,但是它不会进行三次握手,而是直接和目标服务器进行通信;
① int sockfd
客户端自己创建的套接字用来和服务器进行通信;
②const struct sockaddr * addr
指向一个 struct sockaddr 结构体的指针,里面存着你要连接的服务器 IP 地址和端口号。
2.TCP服务器的实现
server.hpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/wait.h>
#include <pthread.h>
#include <functional>
#include "log.hpp"
#include "conmmen.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
using namespace LogModule;
using namespace ThreadPoolModule;
const int gloalsockfd = -1;
static const uint16_t gport = 8080;
#define BACKLOG 8
using handler_t =std::function<std::string (std::string)>;
class Tcpserver
{
using task_t =std::function<void()>;
struct ThreadData
{
int sockfd;
Tcpserver * self;
};
public:
Tcpserver(handler_t handler, uint16_t port = gport)
: _handler(handler),_listensockfd(gloalsockfd), _port(port), _isrunning(false)
{
}
void InitServer()
{
// 1.创建套接字
_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(loglevel::FATAL) << "socket:" << strerror(errno);
Die(SOCKET_ERR);
}
LOG(loglevel::INFO) << "create socket success";
// 2.绑定套接字到网络
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(gport);
local.sin_addr.s_addr=INADDR_ANY;
int n = ::bind(_listensockfd, cast(&local),sizeof(local));
if (n < 0)
{
LOG(loglevel::FATAL) << "bind:" << strerror(errno);
Die(BIND_ERR);
}
LOG(loglevel::INFO) << "bind success";
// 3.监听
n = ::listen(_listensockfd, BACKLOG);
if (n < 0)
{
LOG(loglevel::FATAL) << "listen:" << strerror(errno);
Die(LISTEN_ERR);
}
LOG(loglevel::INFO) << "listen success";
signal(SIGCHLD, SIG_IGN); //忽略子进程退出信号,os自动回收不再不要wait了
}
void HandlerRequest(int sockfd)
{
LOG(loglevel::INFO)<<"HandlerRequest ,sockfd is:"<<sockfd;
char inbuffer[4096];
while(true)
{
//ssize_t n = ::read(sockfd,inbuffer,sizeof(inbuffer)-1);//读取不完善
ssize_t n = ::recv(sockfd,inbuffer,sizeof(inbuffer)-1,0);
if(n>0)
{
inbuffer[n]=0;
// std::string echo_str="server echo #";
// echo_str+=inbuffer;
std::string result_str = _handler(inbuffer); //回调方法出去调用完还是会回来的
// ::write(sockfd,echo_str.c_str(),echo_str.size()); 写入也是不完善的
::sendto(sockfd,result_str.c_str(),result_str.size(),0,nullptr,0 );
}
else if(n==0)
{
LOG(loglevel::INFO)<<"client close the connection"<< sockfd;
break;
}
else
{
break;
}
}
::close(sockfd);
}
static void * threadEntry(void * args)
{
pthread_detach(pthread_self());
ThreadData * data =(ThreadData*)args;
data->self->HandlerRequest(data->sockfd);
return nullptr;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
struct sockaddr_in peer;
socklen_t peerlen = sizeof(peer); //对端地址的长度必须初始化好,不然会有随机值,参数就会出错
int sockfd = ::accept(_listensockfd, cast(&peer), &peerlen);
if (sockfd < 0)
{
LOG(loglevel::WARNING) << "accept:" << strerror(errno);
continue;
}
LOG(loglevel::INFO) << "accecpt success,sockfd:" << sockfd;
//version 1 多进程模式
// pid_t id=fork(); //创建子进程
// if(id == 0)
// {
// //关闭不需要的文件描述符,防止子进程误触发
// ::close(_listensockfd);
// if(fork() >0) exit(0); //子进程退出,孙子进程执行,因为子进程退出了,所以孙子进程成为了孤儿进程,会被系统收养,所以孙子进程也要退出,不然会造成僵尸进程
// //子进程处理请求
// HandlerRequest(sockfd);
// exit(0); //子进程退出,必须退出否则会造成内存泄露,因为子进程会拷贝父进程得文件描述符表
// }
// ::close(sockfd); //sockfd已经交给子进程了,父进程关闭自己不关心的文件描述符,防止误触发
// //子进程已经退出,父进程不再阻塞
// int rid =::waitpid(id,nullptr,0);
// if(id < 0)
// {
// LOG(loglevel::FATAL)<<"create fork error";
// }
//version 2 多线程模式
//新线程和主线程共享一张文件描述符表
// pthread_t tid ;
// ThreadData * data= new ThreadData; //sockfd是在栈上开辟的空间声明周期很短,int(sockfd)的意思是用sockfd初始化int类型的地址sockfdp;
// data->sockfd=sockfd;
// data->self=this;
// pthread_create(&tid,nullptr,threadEntry,data);
//version 3 线程池模式
task_t f=std::bind(&Tcpserver::HandlerRequest,this,sockfd);
ThreadPool<task_t>::getInstance()->Equeue([this,sockfd](){
this->HandlerRequest(sockfd);
});
}
}
void Stop()
{
_isrunning = false;
}
~Tcpserver()
{
}
private:
int _listensockfd;
bool _isrunning;
uint16_t _port;
handler_t _handler;
};
clientMain.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc,char * argv[])
{
if(argc !=3)
{
std::cout<<"Usage:./client_tcp server_ip server_port"<<std::endl;
return 1;
}
std::string server_ip = argv[1];
int server_port = std::stoi(argv[2]);
int sockfd=::socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0)
{
std::cout<<"create sockfd error"<<std::endl;
return 2;
}
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family=AF_INET;
server_addr.sin_port=::htons(server_port);
server_addr.sin_addr.s_addr=::inet_addr(server_ip.c_str());
int n = ::connect(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr)); //connect之后会自动bind
if(n<0)
{
std::cout<<"connect error"<<std::endl;
return 3;
}
std::string message;
while(true)
{
char inbuffer[1024];
std::cout<<"input message:";
std::getline(std::cin,message);
n= ::write(sockfd,message.c_str(),message.size());
if(n>0)
{
//读的是服务器发回来的数据
int m = ::read(sockfd,inbuffer,sizeof(inbuffer));
if(m>0)
{
inbuffer[m]=0;
std::cout<<"receive message from server:"<<inbuffer<<std::endl;
}
else
break;
}
else
break;
}
::close(sockfd);
return 0;
}
serverMain.cc
#include "server.hpp"
#include "commendExcu.hpp"
#include <memory>
int main()
{
ENABLE_CONSOLE_LOG();
commendExcu commend; //实例化命令执行类
std::unique_ptr<Tcpserver> tsvr=std::make_unique<Tcpserver>([&commend](std::string cmdstr){
return commend.execute(cmdstr);
}); //用智能指针初始化服务器
tsvr -> InitServer(); //初始化服务器
tsvr -> Start(); //启动服务器
return 0;
}
commentExct.hpp ->实现远程执行命令功能
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
class commendExcu
{
public:
std::string execute(std::string cmdstr)
{
FILE * fp = ::popen(cmdstr.c_str(), "r");
if(fp == nullptr)
{
return std::string("Failed to execute command");
}
char buffer[1024];
std::string result;
while(true)
{
char * ret = ::fgets(buffer, sizeof(buffer), fp);
if(!ret) break;
result += ret;
}
pclose(fp);
return result.empty()?std::string("done"):result;
}
};
本篇博客可以搭配"每日遗忘"专栏中的二三篇文章来看;
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)