一:重谈协议
1 理解网络协议,可以把它想象成网络世界里的"交通规则"和"通用语言"。它是一套预先定义好的规则、标准和约定,使得不同设备、不同系统之间能够顺利地进行通信和数据交换。
我们从TCP协议上面理解一下,首先TCP服务是全双工的,怎么理解是全双工的呢,就是如果使用TCP服务,在客户端或者是服务端,都有两个缓冲区---一个是发送缓冲区,一个是接收缓冲区,我们在TCP接收发送文件不是用的是read,wirte吗,当要接收数据时,就从接收缓冲区里读数据交给用户空间,也就是从内核到用户,从而发送数据时,write函数就把用户空间的数据拷贝到发送缓冲区里,也就是从用户层到内核层。刷新什么的由TCP自己决定,全全交给了OS。所以TCP又叫做传输控制协议,里面有各种报头来确认数据传输的正确性。
这个确认数据的完整性或者正确性什么的,UDP和TCP就有了各自的特点,UDP是面向报文的,相当于快递,他就会要求传递报文必须是完整的,TCP是相当于自来水接水,所以呢他就有可能传过来的数据是一段一段的,我们在报头里就会有一些报文的长度啦,还有什么分隔符啦,分开报头和有效载荷了什么的。而这些要求就是我们今天要说的序列化和反序列化。
二:序列化和反序列化
我们所说的协议就是一种约定,客户端和服务端用的是同一套协议,举个例子,假设客户端发送请求,这个报文里面呢设置报文长度,有效载荷传过来的数据,性别啦,年龄了等等。而服务端接收到的数据就是已经序列化好的,就是把这些数据按照一定顺序排列好了给你发送过来了,接着就要把它反序列化,就是把这些存的东西一个一个的对应的从字符串里拿出来。说简单点了就是把字符串里的内容存放在对应的结构体里。协议是一种 "约定". socket api 的接口。
实现序列化和反序列化大体上有两种方法:
第一种就是自定义的方式,我自己规定传过来的数据 结构是什么样,从而让你读取到一个序列化好的,你可以通过反序列化拿到对应的数据,然后再返回一个序列化的结果,服务端接收到数据,也能通过反序列化拿到对应的结果
第二种方式就是用现成的 JSON 序列化 (Serialize),将内存中的数据结构(如对象、数组、字符串、数字、布尔值等)转换成符合 JSON 格式的字符串的过程。JSON 反序列(Deserialize),将一个符合 JSON 格式的字符串 解析并转换回内存中的数据结构(如对象、数组等)的过程。
三:使用序列化和反序列化实现一个简单的网络计算器
客户端和服务端没什么太大的变化。我们今天主要写的就是一个序列化和反序列化
我们直接用一个JSON的,比较方便,但是你前提的安装一下,
Ubuntu的:sudo yum install -y jsoncpp-devl
Centos的:sudo apt install -y jsoncpp-devl
1.我们需要构建两个类,一个请求,一个应答
请求在发送之前,客户端要进行序列化
请求在发送之后,服务端要进行反序列化
应答在发送之前,服务端要进行序列化
应答在发送之后,客户端要进行反序列化
我们实现的计算器比较简单 请求就是 一个数字 x 一个数字 再加一个符号,例子 30 + 20
cpp
int _x;
int _y;
char _oper;
然后把他进行序列化
cpp
bool Serialize(std::string& out_string)
{
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["oper"] = _oper;
Json::StreamWriterBuilder wb;
std::unique_ptr<Json::StreamWriter> w(wb.newStreamWriter());
std::stringstream ss;
w->write(root, &ss);
out_string = ss.str();
return true;
}
这个out_string ,是作为输出型参数,把序列化的结果存进out_string
反序列化
cpp
bool deserialize(std::string& in_string )
{
Json::Value root;
Json::Reader reader;
bool parsingSuccessful = reader.parse(in_string, root);
if (!parsingSuccessful)
{
return false;
}
_x = root["x"].asInt();
_y = root["y"].asInt();
_oper = root["oper"].asInt();
return true;
}
in_string 就是作为一个输入型参数,把对应的结果存给对方就可以
应答的话就是一个结果,还有一个结果描述符,0设为正常,1设置为 / 或% 0了等等,这些都可以自己约定。
cpp
private:
int _result;
int _code;
序列化和反序列化道理一致,我们就不再分开展示了。
完整的代码Protocol.hpp
cpp
class Response
{
public:
Response() : _result(0), _code(0)
{
}
Response(int result, int code) : _result(result), _code(code)
{
}
~Response()
{
}
bool Serialize(std::string& out_string)
{
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::StreamWriterBuilder wg;
std::unique_ptr<Json::StreamWriter> w(wg.newStreamWriter());
std::stringstream ss;
w->write(root,&ss); //注意这里一个取地址,一个不取地址
out_string = ss.str();
return true;
}
bool deserialize(std::string& in_string)
{
Json::Value root;
Json::Reader reader;
bool parsesucess = reader.parse(in_string,root);
if(!parsesucess)
{
std::cout<<"parseucess false"<<std::endl;
return false;
}
_result = root["result"].asInt();
_code = root["code"].asInt();
return true;
}
int Result() const { return _result; }
int Code() const { return _code; }
void SetResult(int res) { _result = res;}
void SetCode(int c) {_code = c;}
private:
int _result;
int _code;
};
2.客户端
cpp
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <cstdlib>
#include <string>
#include "Log.hpp"
#include "Common.hpp"
#include "Protocol.hpp"
using namespace LogModule;
int main(int argc , char* argv[])
{
if(argc != 3 )
{
std::cout<<"Clinet need two arguments"<<std::endl;
return 1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
LOG(LogLevel::ERROR) << "create scokfd false";
}
struct sockaddr_in peer;
memset(&peer,0,sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = ::htons(port);
peer.sin_addr.s_addr = ::inet_addr(ip.c_str());
//客户端不用bind 绑定 ,tcp是面向连接的,connect 会自动绑定
int n = ::connect(sockfd,CONV(&peer),sizeof(peer));
if(n<0)
{
LOG(LogLevel::ERROR)<<"connect false";
return 1;
}
std::string message;
while(true)
{
int x, y;
char oper;
std::cout << "input x: ";
std::cin >> x;
std::cout << "input y: ";
std::cin >> y;
std::cout << "input oper: ";
std::cin >> oper;
Request req(x,y,oper);
//序列化
std::string message;
req.Serialize(message);
Encode(message);
char inbuffer[1024];
// n = ::write(sockfd, message.c_str(), message.size());
n = ::send(sockfd,message.c_str(),message.size(),0);
if(n > 0)
{
//int m = ::read(sockfd, inbuffer, sizeof(inbuffer));
int m =::recv(sockfd,inbuffer,sizeof(inbuffer),0);
if(m > 0)
{
inbuffer[m] = 0;
std::string tmp = inbuffer;
std::string content;
Decode(tmp,&content);
Response resp;
resp.deserialize(content);
std::cout<<resp.Result()<<resp.Code()<<std::endl;
}
else
break;
}
else
break;
}
::close(sockfd);
return 0;
}
3.服务端
TCPServer.hpp
cpp
#pragma once
#include <iostream>
#include "Common.hpp"
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>
#include <functional>
#include "Log.hpp"
#include "Internet.hpp"
#include "ThreadPool.hpp"
#define BackWait 8
using namespace LogModule;
using namespace ThreadPoolModule;
using task_t = std::function<void()>;
using handler_t = std::function<std::string(std::string& tmp)>;
uint16_t defaultport = 8080;
std::string defaultip = "127.0.0.1";
class TcpServer
{
struct Thread
{
int sockfd;
TcpServer* self;
};
public:
TcpServer(handler_t hander, uint16_t port = defaultport, std::string ip = defaultip)
: _port(port), _ip(ip), _isrunning(false), _listensockfd(-1)
,_hander(hander)
{
}
~TcpServer()
{
}
void InitServer()
{
_listensockfd = ::socket(AF_INET,SOCK_STREAM,0);
if(_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "create sockfd false";
Die(1);
}
LOG(LogLevel::INFO) << "socket create success, sockfd is : " << _listensockfd;
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = ::htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
// //面向连接的,还要等待随时被连接
// //所以要设置为监听模式
// 2. bind
int n = ::bind(_listensockfd,CONV(&local),sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
Die(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success, sockfd is : " << _listensockfd;
// 3. cs,tcp是面向连接的,就要求tcp随时随地等待被连接
// tcp 需要将socket设置成为监听状态
int m = ::listen(_listensockfd,8);
if(m < 0 )
{
LOG(LogLevel::FATAL) <<"监听失败";
Die(LISTEN_ERR);
}
LOG(LogLevel::FATAL) <<"监听成功";
}
void HandlerRequest(int sockfd)
{
char buffer[1024];
std::string package;
while(true)
{
//接受消息
ssize_t n = ::read(sockfd,buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n] = 0;
LOG(LogLevel::DEBUG)<<"接受到消息了#:"<<buffer;
package += buffer;
std::string cmd_result = _hander(package);
::send(sockfd, cmd_result.c_str(), cmd_result.size(), 0); // 写入也是不完善
}
else if(n == 0)
{
LOG(LogLevel::INFO) << "client quit: " << sockfd;
break;
}
else
break;
}
}
static void* ThreadHandler(void* args)
{
//用线程也要等待回收(join) 必须等待回收的话就会阻塞,所以让线程自己结束释放资源
pthread_detach(pthread_self());
Thread* tmp = (Thread*)args;
tmp->self->HandlerRequest(tmp->sockfd);
return nullptr;
}
void Start()
{
_isrunning = true;
while(true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = ::accept(_listensockfd,CONV(&peer),&len);
//建立连接之后,这个对应的文件描述符才负责传信(接待)
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);
continue;
}
LOG(LogLevel::INFO) << "accept success, sockfd is : " << sockfd;
InetAddr inetaddr(peer);
LOG(LogLevel::INFO) << "client info: " << inetaddr.Addr();
//version 0
//HandlerRequest(sockfd);
// version -1 多线程版本
// pid_t pid = fork();
//::signal(SIGCHLD,SIG_IGN);
// if(pid == 0)
// {
// //子进程再创建孙子进程,子进程直接退掉,由系统进程1 来回收管理孙子进程
// //子进程和父进程 各有一张文件描述符表 文件都是通过引用计数进行管理的
// //就像管道一样
// ::close(_listensockfd);
// if(fork() > 0)
// {
// exit(0);
// }
// HandlerRequest(sockfd);
// exit(0);
// }
// 给出建议父进程不要关闭文件描述符,这个设计叫权责分明
// 现在语法执行没错,如果修改内容容易有错
// ::close(sockfd);
// pid_t waitid = ::waitpid(pid,nullptr,0);
// if(waitid<0)
// {
// LOG(LogLevel::ERROR)<<"回收父进程失败";
// }
//version 2 用多线程
// pthread_t pid;
// Thread* data = new Thread;
// data->self = this;
// data->sockfd = sockfd;
// pthread_create(&pid,nullptr,ThreadHandler,data);
//version 3 线程池
task_t f = std::bind(&TcpServer::HandlerRequest,this,sockfd);
ThreadPool<task_t>::getInstance()->Equeue([&sockfd,this](){
this->HandlerRequest(sockfd);
});
}
}
private:
int _listensockfd; //这个文件描述符 只负责监听(也就是送客人,不负责招待)
uint16_t _port;
std::string _ip;
bool _isrunning;
handler_t _hander;
};
cpp
#include"TcpServer.hpp"
#include<memory>
#include"Log.hpp"
#include"Calculator.hpp"
#include"Protocol.hpp"
#include<functional>
#include"Daemon.hpp"
using namespace LogModule;
using Cal_t = std::function<Response(const Request& req)>;
// using cal_fun = std::function<Response(const Request &req)>;
// // package一定会有完整的报文吗??不一定把
// // 不完整->继续读
// // 完整-> 提取 -> 反序列化 -> Request -> 计算模块,进行处理
class Parse
{
public:
Parse(Cal_t cal)
:_cal(cal)
{
}
std::string Entry(std::string& package)
{
std::string message;
std::string resptr;
while(Decode(package,&message))
{
LOG(LogLevel::DEBUG) << "Content: \n" << message;
if(message.empty())
break;
//反序列化
Request req;
if(!req.deserialize(message))
break;
//计算
std::string tmp;
Response resp = _cal(req);
resp.Serialize(tmp);
//添加长度字段
Encode(tmp);
//拼接应答,这样的话有多少个需求都会处理,最后统一返回
resptr+=tmp;
}
return resptr;
}
private:
Cal_t _cal;
};
int main()
{
//ENABLE_FILE_LOG();
//Daemon(false,false);
Cal cal;
Parse parse([&cal](const Request& req){
return cal.entry(req);
});
std::shared_ptr<TcpServer> tserver = std::make_shared<TcpServer>([&parse](std::string &package){
return parse.Entry(package);
});
tserver->InitServer();
tserver->Start();
return 0;
}
4.计算端
cpp
#include<iostream>
#include<string>
#include<memory>
#include"Protocol.hpp"
class Cal
{
public:
Cal()
{
}
Response entry(const Request& req)
{
Response resp;
switch (req.Oper())
{
case '+':
resp.SetResult(req.X() + req.Y());
break;
case '-':
resp.SetResult(req.X() - req.Y());
break;
case '*':
resp.SetResult(req.X() * req.Y());
break;
case '/':
{
if (req.Y() == 0)
{
resp.SetCode(1); // 1 就是除0
}
else
{
resp.SetResult(req.X() / req.Y());
}
}
break;
case '%':
{
if (req.Y() == 0)
{
resp.SetCode(2); // 2 就是mod 0
}
else
{
resp.SetResult(req.X() % req.Y());
}
}
break;
default:
resp.SetCode(3);
break;
}
return resp;
}
private:
};
5.Log.hpp
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <fstream>
#include <sstream>
#include <memory>
#include <filesystem> //C++17
#include <unistd.h>
#include <time.h>
#include "Mutex.hpp"
// + 日志的可变部分(<< "hello world"
namespace LogModule
{
using namespace Lock;
std::string CurrentTime()
{
time_t time_stamp=::time(nullptr);
struct tm curr;
localtime_r(&time_stamp, &curr);
char buffer[1024];
snprintf(buffer,sizeof(buffer),"%4d-%02d-%02d %02d:%02d:%02d",
curr.tm_year + 1900,
curr.tm_mon + 1,
curr.tm_mday,
curr.tm_hour,
curr.tm_min,
curr.tm_sec);
return buffer;
}
enum class LogLevel
{
DEBUG=1,
INFO,
WARNING,
ERROR,
FATAL
};
std::string Level2String(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "None";
}
}
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string & message) = 0 ;
};
class ControlStrategy : public LogStrategy
{
public:
ControlStrategy()
{
}
~ControlStrategy()
{
}
void SyncLog(const std::string& message)
{
LockGuard lockguard(_mut);
std::cout<<message<<std::endl;
}
private:
Mutex _mut;
};
/*std::filesystem::exists(_logpath)*/
const std::string defaultlogpath = "./log/";
const std::string defaultlogname = "log.txt";
class FileStrategy : public LogStrategy
{
public:
FileStrategy(const std::string& logpath=defaultlogpath,const std::string& logname = defaultlogname)
:_logpath(logpath)
,_logname(logname)
{
LockGuard lockguard(_mut);
if(std::filesystem::exists(_logpath))
{
return;
}
try
{
std::filesystem::create_directories(_logpath);
}
catch(std::filesystem::filesystem_error& t)
{
std::cerr<<t.what()<<std::endl;
}
}
~FileStrategy()
{
}
void SyncLog(const std::string& message)
{
LockGuard lockgurad(_mut);
std::string log = _logpath+_logname;
std::ofstream out(log,std::ios::app);
if(!out.is_open())
{
std::cout<<"打开失败"<<std::endl;
return;
}
out<<message<<"\n";
out.close();
}
private:
Mutex _mut;
std::string _logpath;
std::string _logname;
};
class Logger
{
public:
Logger()
{
_strategy=std::make_shared<ControlStrategy>();
}
void EnableControl()
{
_strategy=std::make_shared<ControlStrategy>();
}
void EnableFile()
{
_strategy=std::make_shared<FileStrategy>();
}
~Logger()
{
}
//一条完整的信息: [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16]
class LogMessage
{
public:
LogMessage(LogLevel level,const std::string &filename, int line,Logger& logger)
:_time(CurrentTime())
,_level(level)
,_pid(::getpid())
,_name(filename)
,_line(line)
,_logger(logger)
{
std::stringstream ssbuffer;
ssbuffer << "[" << _time << "] "
<< "[" <<Level2String(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _name << "] "
<< "[" << _line << "] - ";
_loginfo = ssbuffer.str();
//std::cout<<_loginfo<<std::endl;
}
template<class T>
LogMessage &operator<<(const T& info)
{
std::stringstream ss;
ss<<info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if(_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
std::string _time;
LogLevel _level;;
pid_t _pid;
std::string _name;
int _line;
std::string _loginfo;
Logger &_logger;
};
LogMessage operator()(LogLevel level, const std::string &filename, int line)
{
return LogMessage(level, filename, line,*this);
}
private:
std::shared_ptr<LogStrategy> _strategy;
};
Logger logger;
#define LOG(level) logger(level,__FILE__,__LINE__)
#define ENABLE_CONSOLE_LOG() logger.EnableControl()
#define ENABLE_FILE_LOG() logger.EnableFile()
}
四 重谈七层协议
五层协议: 物理层 数据链路层 网络层 应用层 传输层
七层协议:物理层 数据链路层 网络层 应用层 传输层 会话层 表示层
五层模型是将 OSI 七层模型中的会话层、表示层和应用层这三层合并成了一个单一的应用层。
为什么这样合并?
实际应用:在实际的 TCP/IP 协议栈中,会话管理(如建立、管理和终止会话)和数据表示(如数据加密、压缩、格式转换)的功能通常由应用程序本身或应用层协(如 HTTP、TLS/SSL)来实现,而不是由一个独立的、通用的协议层来处理。也就是说应用层实现大部分,所以为了不冲突,就由用户层自我决定。