I- 封装套接字(TcpServer.hpp)
Socket 作为ip+端口号的集合 , 可以表示唯一一台电脑上的唯一网络进程 , 是网络通信的基础 . 但是 , socket的原生接口有c语言实现 , 因此面向过程式的使用很不优雅 , 因此先进行c++式面向对象的封装 .
I.1- 骨架(socket的基本构建):
#Linux/网络/socket封装/模板设计模式
- 套接字socket分为tcp和udp两大类 , 两者在操作上大部分相同 , 因此使用模板设计模式
- 这里只使用Tcp套接字 , 但这样的设计也方便之后的代码复用.
下面代码中:
- 基类Socket , 派生类TcpSocket
- 基类中将
套接字创建-绑定-监听设置为纯虚函数 ,强制派生类重写 ; 并在基类中定义构建方法BuildServerTcp实现优雅的Tcp服务端初始化
c++
class Socket
{
public:
//创建套接字
virtual void SocketOrDie() = 0;
//绑定
virtual void BindOrDie(uint16_t port) = 0;
//监听
virtual void ListenOrDie(int backlog) = 0;
public:
//构建ServerTcp
void BuildServerTcp(uint16_t port,int backlog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
};
class TcpSocket : public Socket
{
public:
void SocketOrDie() override
{
_fd = socket(AF_INET , SOCK_STREAM , 0);
if(_fd < 0)
{
LOG(LogLevel::FATAL) << "套接字创建失败\n";
exit(ExitNum::SocketErr);
}
LOG(LogLevel::INFO) << "套接字创建成功,fd=" << std::to_string(_fd) << "\n";
}
void BindOrDie(uint16_t port) override
{
AddrIn addr(port);
int n =bind(_fd,addr.GetAddr(),addr.GetSize());
if(n < 0)
{
LOG(LogLevel::FATAL) << "绑定失败\n";
exit(ExitNum::BindErr);
}
LOG(LogLevel::INFO) << "服务端绑定成功!!!\n";
}
void ListenOrDie(int backlog) override
{
int n = listen(_fd,backlog);
if(n < 0)
{
LOG(LogLevel::FATAL) << "监听失败\n";
exit(ExitNum::ListenErr);
}
LOG(LogLevel::INFO) << "服务器监听成功!!!\n";
}
private:
int _fd;
};
I.2- Accept函数:
- Tcp通信面向连接 , 而
accept函数的调用正是告诉未来的客户端 : "我接受你们的连接" .- 而客户端的连接信息同样需要记录 , 所以调用
accept又会创建一个独立于socket的套接字 .
下面的代码中:
- 理解这段代码的设计需要站在调用方的角度 , 即套接字的使用者---TcpServer .
- 进行网络通信时需要 accept函数返回的套接字 , 而TcpSocket类里已经有了一个_sock来记录
socket函数返回的套接字,此时有两种做法:- 在TcpSocket类的成员变量里定义两个套接字 :
listen_socket和service_socket - 不新增套接字成员变量 , 而是让
Accept函数返回一个TcpSocket对象的指针(涉及返回值拷贝,所以用shared_ptr).
- 在TcpSocket类的成员变量里定义两个套接字 :
- 进行网络通信时同样需要 客户端的网络地址信息 , 因此使用传递一个
AddrIn类的输出型参数来将客户端信息带出来.
I.2.1- 踩过的坑:
这是调用方(TcpServer的Start函数中 )期望的用法 :
c++
AddrIn addr;
std::shared_ptr<Socket> sock = _listen_socket->Accept(addr);
//然后就可以通过sock对象来进行send和receive了 (其中sock对象的_fd是新的 , 由Accept而来,而非之前的socket)
我的坑 :
可以看到 , 我直接在第三行把 当前对象的成员变量_fd给覆盖了!!! 于是我虽然返回了正确的Socket对象 ,但是把原来那个给破坏了.
c++
std::shared_ptr<Socket> Accept(AddrIn& addr) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
_fd = accept(_fd,CONV(peer),&len); //大坑!!!!!!!!!!
if(fd < 0)
{
LOG(LogLevel::FATAL) << "接受失败\n";
return nullptr;
}
LOG(LogLevel::INFO) << "accept成功!!!\n";
addr.SetAddr(peer);
return std::make_shared<TcpSocket>();
}
纠正:
c++
//添加构造函数
TcpSocket(int fd) :_fd(fd)
{}
//正确的Accept
std::shared_ptr<Socket> Accept(AddrIn& addr) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(_fd,CONV(peer),&len); //使用局部变量接受新fd,而非成员变量_fd
if(new_sock < 0)
{
LOG(LogLevel::FATAL) << "接受失败\n";
return nullptr;
}
LOG(LogLevel::INFO) << "accept成功!!! , 新fd = " << std::to_string(_fd);
addr.SetAddr(peer);
return std::make_shared<TcpSocket>(new_sock); //使用新的局部fd来构造新对象的指针
}
I.3- Receive函数:
Tcp通信面向字节流 , 服务器本身不关心接收到的具体是什么内容.
上层期望的调用方式:
对数据流的读取情况做判断
c++
std::string buffer;
int n = sock->Receive(&buffer);
//在根据返回值n来决定处理逻辑
底层实现:
只管数据流式的接受 , 不做任何处理和判断 ; 使用一个输出型参数将读取到的内容带出去
c++
//接收数据
virtual int Receive(std::string* st) override
{
char buffer[1024];
int n = recv(_fd , buffer,sizeof(buffer)-1 , 0); //sizeof(buffer)-1很关键 ,不然可能和buffer[len] = 0;冲突
if(n > 0)
{
buffer[n] = '\0';
*(st) += buffer;
}
return n;
}
II- 服务端代码(一部分)
c++
const int DefaultBackLog = 8;
class TcpServer
{
public:
TcpServer(uint16_t port, int backlog = DefaultBackLog) : _port(port), _listen_socket(std::make_unique<TcpSocket>()), _isRunning(false)
{
_listen_socket->BuildServerTcp(_port, backlog);
}
void Start()
{
_isRunning = true;
while (_isRunning)
{
AddrIn addr;
std::shared_ptr<Socket> sock = _listen_socket->Accept(addr);
// Accept失败则重新尝试
if (sock == nullptr)
continue;
// Accept成功后尝试接收数据
std::string buffer;
int n = sock->Receive(&buffer);
if (n < 0)
{
LOG(LogLevel::ERROR) << "数据接受失败\n";
break;
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "客户端退出...:" << addr.GetInfo() << "\n";
break;
}
else
{
//开始创建子进程处理任务
int pid = fork();
if(pid == 0)
{
if(fork() > 0)
{
exit(0);
}
//此时孙子进程为孤儿进程
// TODO : 对于数据的处理逻辑
//.................................................
//.................................
sock->Close();
exit(ExitNum::OK);
}
else
{
int n = waitpid(pid,nullptr,0);
(void)n ;
}
}
}
_isRunning = false;
}
private:
uint16_t _port;
std::unique_ptr<Socket> _listen_socket;
bool _isRunning;
};
III- 协议定制(网络计算器) :
到此为止 , 服务器的主要逻辑已经完成 .但是Tcp作为面向字节流的协议 , 想要保证服务端和客户端能够正确的处理字节流 , 就需要我们定制协议了.
- 目标 : 实现一个网络计算器 , 能够接受类似 "4+8" 这样的客户端请求 , 并返回结果 "12".
III.1- 序列化和反序列化:
III.1.1- 序列化方案 : JSON
III.1.1.1- 头文件
jsoncpp/json/json.hpp
注: 这是第三方库 , 在用g++编译时要带上选项-ljsoncpp
III.1.1.2- 基础类
核心元素
Json::Value: Json的格式化数据 , 方便被Json相关类的函数处理.
序列化相关
Json::StyledWriter: 可用于生成可读性高的Json串(换行) , 便于调试Json::FastWriter: 可用于生成更精简的Json串(不换行) , 方便网络传输Json::StreamWriter: 可用于更加个性化的Json串 . 常常用StreamWriterBuilder类对象的newStreamWriter并结合streamstring类的对象来初始化.
反序列化相关
Json::Reader: 可以使用类成员函数Parse来将
III.1.2- 封装Request和Response类
III.1.2.1- Request类
Request类主要代表客户端 , 用于序列化客户端请求后发送 , 将其反序列化给服务端来接受 .
III.1.2.2- Response类
Response类主要面向服务端 , 用于将服务端的计算结果序列化 , 并在客户端反序列化.
III.2- 协议定制(Protocol.hpp):
III.2.1- 遇到的问题:
III.2.1.1- GetRequest函数的参数问题 :
- Tcp服务端面向字节流 , 需要面对数据的序列和反序列化处理 .
- 为了解耦合 , 服务端只负责建立连接和派发任务 . 处理数据和收发数据的动作交给协议 .
- 服务端通过回调函数调用协议中的相关函数来进行处理.
因此, Protocol类的 GetRequest作为提供给服务端的回调方法 :
- 首先要接受套接字信息(保证最基础的网络收发功能)->
std::shared_ptr<Socket> sock - 其次 , 为了知道客户端信息 , 也接受网络地址信息 ->
const AddrIn &client
我一开始这样传递的 ,完全是无稽之谈_:void GetRequest(std::string & buffer_queue, const AddrIn & addr)
最终的函数声明: void GetRequest(std::shared_ptr<Socket> sock, const AddrIn &client)
III.2.1.2- GetResponse函数的参数问题:
- 同样是因为Tcp服务端面向字节流的特性 , 客户端接受到的回复也不一定是刚刚好的 , 有可能多余一个有效载荷.
因此 , Protocol类的 GetResponse作为提供给客户端的函数 :
- 首先 , 要接受套接字信息(保证最基础的网络收发功能) ->
std::shared_ptr<Socket> sock - 其次 , 需要一个局部的缓冲区无条件保存服务端发来的原始消息 ->
std::string resp_buffer - 最后 , 需要就原式消息
resp_buffer进行处理后得到反序列化过的有效载荷 , 并存在对象里方便打印和读取 ->Response& response
最终的函数声明 : bool GetResponse(std::shared_ptr<Socket> sock, std::string& resp_buffer,Response& response)
其中 : 返回值代表是否获得有效数据
III.3- 协议的回调方法(NetCal.hpp)
这个最简单 , 可以使用仿函数实现 .
一个细节 : 除了计算结果之外 , 还要设置Response对象的标志位 . 以便让客户端判断结果的合法性.
IV- 完整代码:
其中:(Log.hpp(日志模块) / Common.hpp(公共的声明定义) / mutex.hpp(给日志用的互斥锁)) 略
IV.1- Protocol.hpp
c++
#pragma once
#include "Common.hpp"
#include "Socket.hpp"
#include <jsoncpp/json/json.h>
#include <sstream>
#include <cstring>
#include <functional>
const char *Sep = "\n\r";
// TODO 为了照顾包装器的定义,声明一下(似乎没用???)
// class Request;
// class Reponse;
// using func_t = std::function<Response(const Request& , const AddrIn&)>;
class Request
{
public:
Request() = default;
Request(int x, int y, char oper)
: _x(x), _y(y), _oper(oper)
{
}
std::string Serialize()
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::StreamWriterBuilder builder;
std::unique_ptr<Json::StreamWriter> pwriter(builder.newStreamWriter());
auto &writer = *pwriter;
std::stringstream ss;
int OK = writer.write(root, &ss);
return ss.str();
}
bool DeSerialize(const std::string &json)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(json, root);
if (!ok)
{
std::cout << "客户端解析JSON串失败" << std::endl;
return false;
}
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
return true;
}
int GetX() const { return _x; }
int GetY() const { return _y; }
int Oper() const { return _oper; }
void ShowMyself()
{
std::cout << GetX() << " " << GetY() << " " << Oper() << std::endl;
}
private:
int _x;
int _y;
int _oper;
};
class Response
{
public:
Response()
{
}
Response(int result, int flag) : _result(result), _flag(flag)
{
}
std::string Serialize()
{
Json::Value root;
root["result"] = _result;
root["flag"] = _flag;
Json::StreamWriterBuilder builder;
std::unique_ptr<Json::StreamWriter> pwriter(builder.newStreamWriter());
auto &writer = *pwriter;
std::stringstream ss;
int OK = writer.write(root, &ss);
return ss.str();
}
bool DeSerialize(const std::string &json)
{
Json::Value root;
Json::Reader reader;
bool ok = reader.parse(json, root);
if (!ok)
{
std::cout << "客户端解析JSON串失败" << std::endl;
return false;
}
_result = root["result"].asInt();
_flag = root["flag"].asInt();
return true;
}
void SetResult(int result)
{
_result = result;
}
void SetFlag(int flag)
{
_flag = flag;
}
void ShowRresponse()
{
std::cout << _result << "[" << _flag << "]" << std::endl;
}
private:
int _result;
int _flag;
};
using func_t = std::function<Response(const Request &, const AddrIn &)>;
class Protocal
{
public:
Protocal()
{
}
Protocal(func_t func) : _func(func)
{
}
void EnCode(std::string &st)
{
size_t len = st.size();
st = std::to_string(len) + Sep + st + Sep;
}
// 此时收掉了一串 , 可能满足一个报文,也可能多于一个报文
bool DeCode(std::string *buffer, std::string &buffer_queue)
{
// 1,如果没有特定字符,直接凉拌
size_t pos = buffer_queue.find(Sep);
if (pos == std::string::npos)
return false;
// 2,提取长度
std::string lenStr = buffer_queue.substr(0, pos);
int len = std::stoi(lenStr);
size_t expectedLen = strlen(Sep) * 2 + lenStr.size() + len;
// 3,看看是否满足至少一个完整报文
if (buffer_queue.size() < expectedLen)
return false;
// 4,从头部截取有效载荷
*buffer = buffer_queue.substr(pos + strlen(Sep), len);
buffer_queue.erase(0, expectedLen);
return true;
}
// void GetRequest(std::string & buffer_queue, const AddrIn & addr) // NOTE:这次的参数一开始又传错了
void GetRequest(std::shared_ptr<Socket> sock, const AddrIn &client)
{
LOG(LogLevel::DEBUG) << "代码执行到:GetRequest\n";
// 1 , 接收数据流
std::string buffer_queue;
while (true)
{
int n = sock->Receive(&buffer_queue);
LOG(LogLevel::DEBUG) << "sock->Receive结束";
if (n < 0)
{
LOG(LogLevel::WARNING) << "client:" << client.GetInfo() << ", recv error";
break;
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "client:" << client.GetInfo() << "Quit!";
break;
}
else
{
// 2,解码数据
std::string buffer;
// bool ok = DeCode(&buffer, buffer_queue);
// if (ok == false)
// return;
while (DeCode(&buffer, buffer_queue))
{
// 3,反序列化
Request request;
bool ok = request.DeSerialize(buffer);
if (ok == false)
return;
//LOG(LogLevel::DEBUG) << "服务端调用GetRequest进行反序列化后的结果:" ;
request.ShowMyself();
// 4,处理
Response response(_func(request, client));
//LOG(LogLevel::DEBUG) << "服务端调用回调函数后的计算结果:";
response.ShowRresponse();
// 5,序列化
std::string json_str = response.Serialize();
// 6,包装
EnCode(json_str);
// 7,发送
//LOG(LogLevel::DEBUG) << "服务端结算出结果:" << json_str << " 并发送给客户端\n";
sock->Send(json_str.c_str());
}
}
}
}
//std::shared_ptr<Response> GetResponse(std::shared_ptr<Socket> sock, const AddrIn &client) // NOTE:参数不太对哦
bool GetResponse(std::shared_ptr<Socket> sock, std::string& resp_buffer,Response& response)
{
//std::string buffer_queue;
while (true)
{
//int n = sock->Receive(&buffer_queue); //BUG:接受buffer时弄错啦
int n = sock->Receive(&resp_buffer);
// LOG(LogLevel::DEBUG) << "Receive 返回: " << std::to_string(n)
// << ", buffer_queue 当前长度: " << std::to_string(buffer_queue.size())
// << ", 内容预览: " << buffer_queue.substr(0, 50);
if(n <= 0) return false;
// if (n > 0)
// {
// // 1,解码
// std::string buffer;
// bool ok = DeCode(&buffer,buffer_queue);
// if(ok == false)
// continue;
// // 2,反序列化
// Response response;
// ok = response.DeSerialize(buffer);
// if(ok == false)
// continue;
// //return response;
// return std::make_shared<Response>(response);
std::string json_buffer;
//while(DeCode(&json_buffer,resp_buffer)) // BUG:不能用循环,
if(DeCode(&json_buffer,resp_buffer))
{
return response.DeSerialize(json_buffer);
}
// }
// else if (n == 0)
// {
// LOG(LogLevel::WARNING) << "服务端停止\n";
// return false;
// }
// else
// {
// LOG(LogLevel::ERROR) << "客户端接受错误\n";
// return false;
// }
}
}
std::string BuildRequestString(int x , int y , char oper)
{
Request request(x,y,oper);
std::string st = request.Serialize();
EnCode(st);
return st;
}
private:
func_t _func;
};
IV.2- Socket.hpp
c++
#pragma once
#include<iostream>
#include<sys/socket.h>
#include"Common.hpp"
class Socket
{
public:
//创建套接字
virtual void SocketOrDie() = 0;
//绑定
virtual void BindOrDie(uint16_t port) = 0;
//监听
virtual void ListenOrDie(int backlog) = 0;
//server尝试连接client
virtual std::shared_ptr<Socket> Accept(AddrIn& addr) = 0;
//接收数据
virtual int Receive(std::string* st) = 0;
//发送数据
virtual void Send(const std::string& st) = 0;
//关闭文件描述符
virtual void Close( )= 0;
//客户端尝试建立连接
virtual bool Connect(std::string ip , uint16_t port) = 0;
public:
//构建ServerTcp
void BuildServerTcp(uint16_t port,int backlog)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(backlog);
}
//构建ClientTcp
void BuildClientTcp()
{
SocketOrDie();
}
};
class TcpSocket : public Socket
{
public:
TcpSocket() = default;
TcpSocket(int fd) :_fd(fd)
{}
void SocketOrDie() override
{
_fd = socket(AF_INET , SOCK_STREAM , 0);
if(_fd < 0)
{
LOG(LogLevel::FATAL) << "套接字创建失败\n";
exit(ExitNum::SocketErr);
}
LOG(LogLevel::INFO) << "套接字创建成功,fd=" << std::to_string(_fd) << "\n";
}
void BindOrDie(uint16_t port) override
{
AddrIn addr(port);
int n =bind(_fd,addr.GetAddr(),addr.GetSize());
if(n < 0)
{
LOG(LogLevel::FATAL) << "绑定失败\n";
exit(ExitNum::BindErr);
}
LOG(LogLevel::INFO) << "服务端绑定成功!!!\n";
}
void ListenOrDie(int backlog) override
{
int n = listen(_fd,backlog);
if(n < 0)
{
LOG(LogLevel::FATAL) << "监听失败\n";
exit(ExitNum::ListenErr);
}
LOG(LogLevel::INFO) << "服务器监听成功!!!\n";
}
std::shared_ptr<Socket> Accept(AddrIn& addr) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(_fd,CONV(peer),&len);
if(new_sock < 0)
{
LOG(LogLevel::FATAL) << "接受失败\n";
return nullptr;
}
LOG(LogLevel::INFO) << "accept成功!!!" << addr.GetInfo() << "\n";
addr.SetAddr(peer);
return std::make_shared<TcpSocket>(new_sock);
}
//接收数据
virtual int Receive(std::string* st) override
{
char buffer[1024];
int n = recv(_fd , buffer,sizeof(buffer)-1 , 0);
if(n > 0)
{
buffer[n] = '\0';
*(st) += buffer;
}
return n;
}
//发送数据
virtual void Send(const std::string& st) override
{
int n = send(_fd,st.c_str(),st.size(),0);
if(n < 0)
LOG(LogLevel::WARNING) << "发送失败\n";
}
void Close( ) override
{
close(_fd);
}
bool Connect(std::string ip , uint16_t port) override
{
AddrIn addr(ip,port);
int n = connect(_fd,addr.GetAddr(),addr.GetSize());
if(n < 0)
{
LOG(LogLevel::WARNING) << "客户端连接失败\n";
return false;
}
LOG(LogLevel::INFO) << "客户端连接成功!!! \n";
return true;
}
private:
int _fd;
};
IV.3- TcpServer.hpp
c++
#include "Common.hpp"
#include "Socket.hpp"
#include <sys/wait.h>
#include <functional>
const int DefaultBackLog = 8;
using iofunc_t = std::function<void(std::shared_ptr<Socket>, const AddrIn &)>;
class TcpServer
{
public:
TcpServer(uint16_t port, iofunc_t func, int backlog = DefaultBackLog)
: _port(port),
_func(func),
_listen_socket(std::make_unique<TcpSocket>()),
_isRunning(false)
{
_listen_socket->BuildServerTcp(_port, backlog);
}
void Start()
{
_isRunning = true;
while (_isRunning)
{
AddrIn addr;
std::shared_ptr<Socket> sock = _listen_socket->Accept(addr);
// Accept失败则重新尝试
if (sock == nullptr)
continue;
// Accept成功后尝试接收数据 //BUG : 超级无敌大坑,不应该在服务器里接受数据,而是全盘交给协议
// std::string buffer;
// int n = sock->Receive(&buffer);
// if (n < 0)
// {
// LOG(LogLevel::ERROR) << "数据接受失败\n";
// break;
// }
// else if (n == 0)
// {
// LOG(LogLevel::INFO) << "客户端退出...:" << addr.GetInfo() << "\n";
// break;
// }
// else
// {
// 开始创建子进程处理任务
int pid = fork();
if (pid == 0)
{
if (fork() > 0)
{
exit(ExitNum::OK);
}
// 此时孙子进程为孤儿进程,执行回调方法
//LOG(LogLevel::DEBUG) << "客户端开始执行回调方法\n";
_func(sock,addr);
//结束后关闭文件描述符并让孙子进程退出
sock->Close();
exit(ExitNum::OK);
}
// else
// {
// int n = waitpid(pid, nullptr, 0);
// (void)n;
// }
// }
}
_isRunning = false;
}
private:
uint16_t _port;
iofunc_t _func;
std::unique_ptr<Socket> _listen_socket;
bool _isRunning;
};
IV.4- NetCal.hpp
c++
#pragma once
#include "Protocal.hpp"
class Cal
{
public:
Response operator()(const Request &request, const AddrIn &addr)
{
Response response(0, 0);
switch (request.Oper())
{
case '+':
response.SetResult(request.GetX() + request.GetY());
break;
case '-':
response.SetResult(request.GetX() - request.GetY());
break;
case '*':
response.SetResult(request.GetX() * request.GetY());
break;
case '/':
if (request.GetY() == 0)
{
response.SetResult(-1); // HACK : 服务器中除零错误的处理(并非直接让程序崩溃)
response.SetFlag(1); // 除零错误
}
else
{
response.SetResult(request.GetX() / request.GetY());
}
break;
case '%':
if (request.GetY() == 0)
{
response.SetResult(-1); // HACK : 服务器中除零错误的处理(并非直接让程序崩溃)
response.SetFlag(2); // 模零错误
}
else
{
response.SetResult(request.GetX() % request.GetY());
}
break;
default:
response.SetResult(-1);
response.SetFlag(3); //符号传递错误
break;
}
return response;
}
};
IV.5- Server.cpp
c++
#include"NetCal.hpp"
#include"TcpServer.hpp"
#include "Protocal.hpp"
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "服务端参数传递错误";
return 1;
}
uint16_t port = std::stoi(argv[1]);
Cal cal;
std::unique_ptr<Protocal> protocal = std::make_unique<Protocal>([&cal](const Request &request, const AddrIn &addr)->Response{
return cal(request,addr);
});
std::unique_ptr<TcpServer> ptr = std::make_unique<TcpServer>(port,[&protocal](std::shared_ptr<Socket> sock, const AddrIn &addr){
protocal->GetRequest(sock,addr);
});
ptr->Start();
return 0;
}
IV.6- Client.cpp
c++
#include"Common.hpp"
#include"Protocal.hpp"
void GetDataFromStdin(int *x, int *y, char *oper)
{
std::cout << "Please Enter x: ";
std::cin >> *x;
std::cout << "Please Enter y: ";
std::cin >> *y;
std::cout << "Please Enter oper: ";
std::cin >> *oper;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "客户端参数传递错误";
return 1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
std::shared_ptr<Socket> sock = std::make_shared<TcpSocket>();
sock->BuildClientTcp();
if(sock->Connect(ip,port) == false)
{
LOG(LogLevel::ERROR) << "Connect error";
exit(ExitNum::ConnectErr);
}
//连接成功
std::unique_ptr<Protocal> protocal = std::make_unique<Protocal>();
std::string resp_buffer; //NOTE:这个放在while循环外有风险 , 可能导致数据残留 , 影响下一次读取
while(true)
{
// 1. 从标准输入当中获取数据
int x, y;
char oper;
GetDataFromStdin(&x, &y, &oper);
// 2. 构建一个请求-> 可以直接发送的字符串
std::string st = protocal->BuildRequestString(x,y,oper);
//std::cout << "客户端构建好的JSON串: " << st << std::endl;
// 3. 发送请求
sock->Send(st);
// 4. 获取应答
Response response;
bool ok = protocal->GetResponse(sock,resp_buffer,response);
if(ok == false)
break;
// 5. 显示结果
response.ShowRresponse();
}
sock->Close();
return 0;
}
V- 补充 : 守护进程:
V.1- 进程 / 进程组
对于进程来讲 : 父子进程间是父子关系 ; 由同一个父进程创建的子进程间是兄弟关系 ;
而进程们除了父子关系外 , 还存在组 的关系 , 即进程组
V.1.1- 测试:
通过 管道
|来依次执行多条命令 ; 通过&来让命令后台执行
bash
$ sleep 3000 | sleep 5000 | sleep 4000 & #执行三个后台运行的sleep进程
查看sleep进程相关的信息 , 用
ps
V.1.2- 现象:
- (看第二行)
PID是进程id ,PGID是进程组id - 对于
PID, 由于三个sleep进程是一次创建的 , 所以PID也是一次递增. - 关键的来了: 对于
PGID, 三个sleep进程是相同的 , 并且数值等于第一个sleep进程 . 因此第一个sleep进程就叫做组长进程 , 他和剩下的两个同为一组.
bash
ps -ajx | head -1 && ps -ajx | grep sleep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
17020 17104 17104 17020 pts/4 17109 S 1000 0:00 sleep 3000
17020 17105 17104 17020 pts/4 17109 S 1000 0:00 sleep 5000
17020 17106 17104 17020 pts/4 17109 S 1000 0:00 sleep 4000
V.2- 会话
- 为了直击本质 , 我们借助Linux理解具体的操作系统时 , 会用到终端软件 , 打开之后是命令行的界面.
- 有时候为了监控程序的各种状态 , 我们发现还可以在同一台机器上打开多个终端窗口.
这样的一个个窗口 , 在系统层面叫做一个个的会话(session) !!!
V.2.1- SID:
- 还是一样的输出 , 但是看向第四列 :
SID,代表会话ID . - 可以看到 , 因为是同一个终端下执行的指令 , 所以这三个sleep进程的会话id相同 .
bash
ps -ajx | head -1 && ps -ajx | grep sleep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
17020 17104 17104 17020 pts/4 17109 S 1000 0:00 sleep 3000
17020 17105 17104 17020 pts/4 17109 S 1000 0:00 sleep 5000
17020 17106 17104 17020 pts/4 17109 S 1000 0:00 sleep 4000
V.2.2- bash的产生:
上面第四行里各个进程的数值并非凭空产生 , 而是继承自比普通进程更加权威的进程->bash 进程 . 也就是在系统启动时会立刻创建的命令行解释器 , 并且 , 没新建一个终端就会有一个bash进程 .
V.2.3- 验证bash的归属:
使用ps指令查看bash进程的信息
- 可以看到最后一行的bash进程的pid整好等于上面那三个sleep进程的 SID , 这就说明了由同一个bash创建的进程使用同一个SID.
bash
ps -ajx | head -1 && ps -ajx | grep bash
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1353 1505 1505 1353 tty1 1505 S+ 1000 0:00 -bash
1739 15599 15599 15599 pts/2 15599 Ss+ 1000 0:00 /bin/bash --init-file /home/ha2042894194/.vscode-server/bin/994fd12f8d3a5aa16f17d42c041e5809167e845a/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
16033 16034 16034 16034 pts/3 17153 Ss 1000 0:00 -bash
17019 17020 17020 17020 pts/4 17020 Ss+ 1000 0:00 -bash
V.2.4- 创建会话( setsid )
只要不是组长进程 , 就可以调用
setsid函数来新建会话
c++
#include <unistd.h>
/*
*功能:创建会话
*返回值:创建成功返回SID, 失败返回-1
*/
pid_t setsid(void);
setsid的作用:
- 调用进程成为 新会话 的首进程 , 自然 也是组长进程
- 该进程 没有控制终端 , 同时也会和先前的终端脱离关系
因为
setsid被组长进程调用时会出错 , 所以 常见的做法是: 利用创建出的子进程来调用此函数.
V.3- 控制终端 :
- unix系统中 , 用户通过终端 (如xshell) 登录系统后得到一个 shell进程 ;
- 本质上来说 : 人和计算机的交互就是和进程的交互 , 所以可以认为 shell进程就是终端的化身
终端控制是 保存在进程PCB里的信息 , 因此由 shell进程创建的其他进程也会继承同样的终端消息
默认情况下没有重定向 : 终端内所有进程的 标准输入/输出/错误流 都指向控制终端 , 因此进程默认从标准输入里读取内容 / 向终端输出内容 .
- 一个会话只有一个终端
- 会话里的第一个进程叫做 控制进程 , 如 bash进程 .
- 会话中的进程分为 一个 前台进程组 和 多个 后台进程组
- 当终端接受 中断键 (Ctrl+c)或 退出键 (Ctrl + \) , 会向 前台进程组里的所有进程发送信号.
- 当终端检测到 调制解调器 (网络) 断开 , 会将挂断信号发给 控制进程 .
!为啥前台进程只有一个?
- 因为键盘只有一个 , 必须得指定一个进程来接受键盘输入
- 显示器也只有一个 , 但他本质上是一个文件 , 允许多个进程写入 (不加管控还是会脏数据哈)
V.4- 作业控制 :
V.4.1- 概念 :
- 简单来说 , 作业就是 进程/进程组 , 只不过更能体现出OS里为用户干活的象征意义
- 因此 , 适用于 进程/进程组 的概念 , 在作业这里同样适用
V.4.2- 作业号 :
- 下面的命令行中: 我用结合管道 ' | ' 一次性创建了三个sleep 进程(统称为一个作业) ,并用 ' & ' 将其全部设置为后台作业
- 如下面的注释所示 : 成功设置后台作业后 , 会显式
作业号 + 进程组号信息
bash
14_Tcp_Cal$ sleep 1000 | sleep 2000 | sleep 3000 &
[1] 173253
14_Tcp_Cal$ sleep 1000 | sleep 2000 | sleep 3000 &
[2] 173256
14_Tcp_Cal$ sleep 1000 | sleep 2000 | sleep 3000 &
[3] 173259
V.4.3- 作业状态(相关指令) :
- 使用
Ctrl + z会让前台作业停止 ,并显示相关作业信息 :
bash
14_Tcp_Cal$ ./server 8080
^Z #终端接收到了 Ctrl+z 产生的暂停信号
[4]+ Stopped ./server 8080 #被暂停信号的信息
- 使用
fg指令让指令重新运行 :
bash
14_Tcp_Cal$ fg %
./server 8080 #作业被fg唤醒了
| 参数( fg指令的 ) | 含义 |
|---|---|
| %n | n为正整数 , 表示作业号 |
| %string | 以字符串开头的命令所对应的作业 |
| %?string | 包含字符串的命令所对应的作业 |
| %+或%% | 最近提交的一个作业 |
| %- | 倒数第二个提交的作业 |
- 使用
jobs指令查看本用户当前后台执行或挂起的作业 (参数 -l 显式详细 ; 参数 -p 则只显示作业pid)
shell
14_Tcp_Cal$ jobs #默认使用
[2] Running sleep 1000 | sleep 2000 | sleep 3000 &
[3]- Running sleep 1000 | sleep 2000 | sleep 3000 &
[4]+ Stopped ./server 8080
14_Tcp_Cal$ jobs -l #带参数 -l 使用
[2] 173254 Running sleep 1000
173255 Running | sleep 2000
173256 Running | sleep 3000 &
[3]- 173257 Running sleep 1000
173258 Running | sleep 2000
173259 Running | sleep 3000 &
[4]+ 173324 Stopped ./server 8080
14_Tcp_Cal$ jobs -p # 带参数 -p 使用
173254
173257
173324
!作业的状态信息
- 从上面可以看到 , 有的作业号后面还会携带 - 或 +.
- + 表示此作业是最近进入后台的
- - 表示此作业是次最近进入后台的.
V.5- 程序的守护进程化 :
作为服务器 , 不能仅仅是依赖特定的会话 , 所以需要守护进程化 -> 孤儿进程的一种

c++
#pragma once
#include"Common.hpp"
#include<unistd.h> //setsid
#include<signal.h>
#include<fcntl.h> //open
const char* root = "/";
const char* dev_null = "/dev/null";
void Deamon(bool ischdir , bool isclose)
{
//1,忽略可能引起程序退出的📶
signal(SIGPIPE,SIG_IGN);
signal(SIGCHLD,SIG_IGN);
//2,避免自己成为组长
if(fork() > 0)
exit(0);
//3,让子进程作为新的会话
setsid();
//4,按需求切换目录
if(ischdir)
chdir(root);
//5
if(isclose)
{
close(0);
close(1);
close(2);
}
else
{
int fd = open(dev_null,O_RDWR);
if(fd > 0)
{
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
close(fd);
}
}
}
VI- 补充 : 软件的部署与发布(简易版)
守护进程化后的基于tcp的网络计算器就算是一个小项目了 , 可以试着把项目整理成一个软件 : 能够通过shell脚本文件实现自动安装和删除
VI.1- 必要文件 :
- 服务端的源文件 :
server.cpp - 客户端的源文件 :
client.cpp makefile脚本文件 ( 关键 ) ---- 用于构建项目所需的目录/文件结构 , 并形成项目压缩包- 两个
bash脚本文件 --- 用于实现自动化 安装和卸载
VI.2- makefile :
makefile
//编译指令略...
//执行 make output即可自动 创建项目目录->拷贝可执行文件->拷贝bash脚本文件
.PHONY:output
output:
@mkdir output
@mkdir -p output/bin
@mkdir -p output/log
@cp server output/bin
@cp client output/bin
@cp install.sh output/
@cp uninstall.sh output/
@tar czf output.tgz output
VI.3- install.sh
bash
#!/usr/bin/bash
cp -f ./bin/server /usr/bin
cp -f ./bin/client /usr/bin
VI.4- uninstall.sh
bash
#!/usr/bin/bash
rm -rf /usr/bin/server
rm -f /usr/bin/client