I- TCP Socket 网络接口
下面的函数全部声明在
<sys/socket.h>头文件中
I.1- socket
c++
int socket(int domain, int type, int protocol);
函数描述:
- 打开一个网络通信端口 , 如果成功 , 就像open一样返回一个文件描述符 ; 如果失败 ,返回-1
- 进程可以像读写普通文件一样使用
read和write来从网络中读取和写入 (不准确) - 对于 IPV4 , 参数一
domain传入宏AF_INET - 对于TCP协议 , 参数二
type指定为宏SOCK_STREAM, 表示面向字节流的传输协议 - 参数三
protocal直接填0 , 因为有了参数一和参数二 , 已经可以确定具体的协议类型.
I.2- bind
c++
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- 用于将参数
sockfd和 参数addr绑定在一起 , 即本地文件描述符 和网络地址信息 ; 这样可以让 sockfd 监听 addr所描述的ip地址和端口号. - 绑定成功时返回 0 , 失败时返回1.
- 对于服务端 : 监视的网络地址和端口号往往不变 , 需要正常绑定.
- 对于客户端 : 监视的网络地址和端口号随不同的服务端变化 , 因此不用显式绑定 , 而是让操作系统来默默绑定端口.
!bind的第二个参数
- bind的第二个参数是一个通用类型的指针 , 指向一个网络结构体 , 描述了网络协议相关的信息 .
- 这里的
struct sockaddr采用了 多态 的设计思想 , 作为基类 , 他可以接受多种协议的结构体 .- 对于此处的tcp协议来说 , 就是结构体
struct sockaddr_in, 在传入bind函数时需要强制类型转换 , 此结构体的初始化方式如下 :
c++
//对于服务端:
struct sockaddr_in local ;
local = {}; //使用c++11的列表初始化(c语言就用memset)
local.sin_family = AF_INET; //地址族
local.sin_port = htons(port); //将端口号从主机序列转为网络序列
local.sin_addr.s_addr = INADDR_ANY; //宏,表示本地的任意ip地址(因为本地可能 //有多张网卡和多个ip地址)
//对于客户端 :
struct sockaddr_in server;
server = {}; //同上
server.sin_family = AF_INET; //同上
server.sin_port = htons(port); //同上
inet_pton(AF_INET,ip.c_str(),&server.sin_addr.s_addr);//需要指定服务端的 //ip,并将其转化为网络序列
I.3- listen
c++
int listen(int sockfd, int backlog);
- 用于声明
sockfd处于监听状态 , 且最大允许backlog个客户端处于连接等待状态 , 如果接收到更多的请求就忽略 . (一般设置为5 , 过大的backlog对服务端性能要求高) - 监听成功返回0 , 失败则返回 -1;
I.4- accept(服务端)
c++
int accept(int sockfd, struct sockaddr *_Nullable restrict addr,
socklen_t *_Nullable restrict addrlen);
- 三次握手 完成后 , 服务端调用
accept函数接受客户端连接 ; - 如果暂时没有客户端请求 , 则阻塞等待
accept的第二和第三个参数是 输出型参数 , 如果成功接受客户端请求 , 会将服务端传入的addr和addrlen填充为客户端的网络信息 .accept的返回值比较特殊 , 返回一个新的文件描述符 . 借助它 , 服务端就可以使用read和write向客户端发送数据
I.5- connect(客户端)
c++
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
- 用于让客户端连接服务端 , 成功后返回0/失败则返回-1
- 参数二和参数三需要接受 服务端网络信息 , 而非客户端自己
netstat -tnlp
telnet+ "website" = 测试tcp连接是否建立
netstat -tnap | grep PORT
II- 多进程的TCP服务端和客户端
II.1- 一些设计细节
错误码(避免硬编码)
当然啦 , 真正写的时候还是有一个需求加一个需求 , 而不是真的像预知未来一样一次性写到位.
c++
enum ExitNum
{
SocketErr,
BindErr,
ListenErr,
ConnectErr,
FileErr,
ForkErr
};
II.1.1- 使用宏简化地址强转(图个方便)
网络地址的强转又臭又长 , 不妨用耿直的宏来拯救一下...
c++
#define CONV(addr) (struct sockaddr*)&addr
II.1.2- 特殊基类(禁用拷贝/赋值)
c++
//作为TcpServer的积累 , 优雅地禁用构造
class NoCopy
{
public:
NoCopy() = default;
NoCopy(const NoCopy& obj) = delete;
NoCopy& operator=(const NoCopy& obj) = delete;
};
可以这样使用 :
c++
class TcpServer : public NoCopy
{
}
然后在TcpServer类里就没必要在担心拷贝和赋值的问题了.
II.2- 服务端(单进程版本)
II.3- tcp的独特函数接口:
II.4- 代码(简单echo):
c++
#pragma once
#include "Common.hpp"
class TcpServer : public NoCopy
{
private:
void Service(int fd ,const AddrIn &addr)
{
char buffer[1024];
while (true)
{
// 1,接受
int num = read(fd, buffer, sizeof(buffer));
if (num > 0)
{
buffer[num] = 0;
std::cout << "服务端收到消息," << "来自:" << addr.GetInfo() << " 内容:" << buffer << std::endl;
}
else if (num == 0)
{
LOG(LogLevel::INFO) << "客户端全部退出\n";
close(fd);
return;
}
else
{
LOG(LogLevel::ERROR) << "server attempt to read , but failed\n";
close(fd);
return;
}
// 2,处理
// 3,回复
std::string message = "服务端收到了你的消息:" + std::string(buffer);
num = write(fd, message.c_str(), message.size());
}
}
public:
TcpServer(uint16_t port)
: _port(port), _listen_fd(-1),_isRunning(false)
{
}
void Init()
{
int check_ret = 666;
// 1创建套接字
_listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_fd < 0)
{
LOG(LogLevel::ERROR) << "socket creattion failed ";
exit(ExitNum::SocketErr);
}
LOG(LogLevel::INFO) << "套接字创建成功,fd = " << std::to_string(_listen_fd) << "\n";
// 2,绑定
AddrIn addr(_port);
check_ret = bind(_listen_fd, addr.GetAddr(), addr.GetSize());
if (check_ret < 0)
{
LOG(LogLevel::ERROR) << "bind failed";
exit(ExitNum::BindErr);
}
LOG(LogLevel::INFO) << "和套接字:" << std::to_string(_listen_fd) << "绑定成功!!!\n";
//2.5 (另外的补充)给socket套上复活甲
int opt = 1;
setsockopt(_listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3,监听
check_ret = listen(_listen_fd, 8); // 注意:listen的参数二,代表了最大允许监听的端口
if (check_ret < 0)
{
LOG(LogLevel::ERROR) << "listen failed";
exit(ExitNum::ListenErr);
}
LOG(LogLevel::INFO) << "监听成功!!!\n";
}
void Start()
{
_isRunning = true;
while (_isRunning)
{
// 1,建立连接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(_listen_fd, CONV(peer), &len); //注意 : accept函数返回的fd才是通信中使用的.
if (fd < 0)
{
LOG(LogLevel::WARNING) << "建立连接失败,开始重试\n";
continue;
}
LOG(LogLevel::INFO) << "建立连接成功!!!\n";
// 2,处理
AddrIn addr(peer);
Service(fd,addr);
}
_isRunning = false;
}
~TcpServer()
{
close(_listen_fd);
}
private:
std::string _ip;
uint16_t _port;
int _listen_fd;
bool _isRunning;
};
II.5- 软件telnet临时测试:
II.6- 客户端
II.7- 代码:
c++
#pragma once
#include "Common.hpp"
#include "Log.hpp"
using namespace LogModule;
class TcpClient
{
public:
TcpClient(const std::string &ip, uint16_t port)
: _ip(ip), _addr(ip, port) ,_port(port), _fd(socket(AF_INET, SOCK_STREAM, 0))
{
}
void Init()
{
int ret = connect(_fd, _addr.GetAddr(), _addr.GetSize());
if (ret < 0)
{
LOG(LogLevel::ERROR) << "connect failed!!!\n";
exit(ExitNum::ConnectErr);
}
}
void Run()
{
std::string input;
char recv_buffer[256];
while (std::getline(std::cin, input))
{
write(_fd, input.c_str(), input.size());
int num = read(_fd , recv_buffer , sizeof(recv_buffer)-1); //注意,记得少接受一个字节,留出 \0的位置
if(num > 0)
{
recv_buffer[num] = 0;
std::cout << recv_buffer << std::endl;
}
}
}
~TcpClient()
{
}
private:
std::string _ip;
AddrIn _addr;
uint16_t _port;
int _fd;
};
II.8- 测试
c++
#include"TcpClient.hpp"
int main(int argc , char* argv[])
{
//1,处理命令行参数
if(argc != 3)
{
LOG(LogLevel::ERROR) << "客户端参数传递错误\n";
return 1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
TcpClient self(ip,port);
self.Init();
self.Run();
return 0;
}
III- 服务端代码(非单线程,部分代码)
III.1- 方案一: 多进程
III.1.1- 注意点:
III.1.2- 代码:
c++
//TcpServer类
void Start()
{
_isRunning = true;
while (_isRunning)
{
// 1,建立连接
//略...
// 2,处理
AddrIn addr(peer);
//a,多进程
int pid = fork();
if(pid == 0)
{
int n = fork();
if(n == 0)
{
close(_listen_fd); //关闭子进程不必要的文件描述符
Service(fd,addr);
}
exit(0);
}
else if(pid > 0)
{
close(fd);
int n = waitpid(pid,nullptr,0);
(void)n;
}
else
{
LOG(LogLevel::ERROR) << "子进程创建失败\n";
continue;
}
}
_isRunning = false;
}
III.2- 方案二 : 多线程
III.2.1- 注意点:
III.2.2- 代码:
c++
//TcpServer类
void Start()
{
_isRunning = true;
while (_isRunning)
{
// 1,建立连接
//略...
// 2,处理
AddrIn addr(peer);
//a,多进程
pthread_t tid = -1;
ThreadData* td = new ThreadData(this,addr,fd); //注意 : 这是while循环,addr必须传值,而非地址.
pthread_create(&tid , nullptr , Handler , td);
// pthread_join(tid,nullptr); //注意,如果使用join,就会阻塞!!! 所以还是得让线程自己detach
}
_isRunning = false;
}
III.3- 方案三 : 线程池
III.3.1- 注意点:
III.3.2- 代码:
c++
//TcpServer类
void Start()
{
_isRunning = true;
while (_isRunning)
{
// 1,建立连接
//略...
// 2,处理
AddrIn addr(peer);
//a,多线程
ThreadPool<task_t>::GetInstance().EnQueue([this,fd,addr](){
this->Service(fd,addr);
}); //注意 : fd和addr必须值捕获,而非引用捕获
}
_isRunning = false;
}
IV- 业务逻辑添加:远程指令执行:
类Command的设计:
- 构造函数 : 暴力一点 , 直接硬编码白名单指令
Execute: 接受 一串字符 , 返回执行结果 .
命令行解析方式:
- 可以使用自定义shell的命令行解析代码(问题 : 缺乏权限限制)
- 严格限制可供远程调用的命令!!!
- 此处采用的解析方式 : 函数
popen, 把解析工作交给系统
popen的基础用法:
c++
#pragma once
#include<iostream>
#include<cstdio>
#include<set>
#include<string>
#include"Log.hpp"
using namespace LogModule;
#include"AddrIn.hpp"
class CmdExecutor
{
public:
CmdExecutor()
{
_WhiteList.insert("ls");
_WhiteList.insert("pwd");
_WhiteList.insert("ll");
_WhiteList.insert("tree");
_WhiteList.insert("sl");
_WhiteList.insert("whoami");
}
std::string Execute(const std::string& cmd , const AddrIn& addr)
{
std::string ret;
//1,命令合法性校验
auto it = _WhiteList.find(cmd);
if(it == _WhiteList.end())
{
std::cout << cmd << "群众里有坏人,居然想执行" << "[" << cmd <<"]" ;
return std::string("群众里有坏人,居然想执行") + "[" + cmd + "]";
}
LOG(LogLevel::INFO) << "用户:" << addr.GetInfo() << "尝试执行命令:" << cmd << "\n";
//2,命令解析(利用popen)
char buffer[1204];
FILE* pf = popen(cmd.c_str(),"r");
if(pf == nullptr) //注意 , 记得防御性编程
{
return "server busy";
}
//3,构造返回值
while(fgets(buffer,sizeof(buffer),pf))
{
//std::cout << buffer;
ret += buffer;
//ret += "\n"; //fgets自带换行 , 不用自己加
}
LOG(LogLevel::INFO) << "一条命令执行完毕\n";
//4,善后工作
pclose(pf);
return ret;
}
~CmdExecutor()
{}
private:
std::set<std::string> _WhiteList;
};