赞
踩
我们都知道,在进行网络通信的过程中,通信的双方可以是不同的设备,不同的平台,不同的平台,比如说,手机用户和电脑用户进行通信,ios系统和安卓系统进行通信。
自己的数据,如何保证对方端能安全接收到呢,假设linux为服务端,Windows为客户端,如何确保数据能被正确接收呢?
就像我们中国人用中文进行交流一样,假设我们和某个外国人进行交流,母语的差异导致双方无法正常交流,信息也无法传达,于是我们只能打开手机上的 同声传译功能,可以将信息转换为对方能听懂的语言,最终实现交流。
同声传译 这个功能可以看做一种 协议(可以确保对端能理解自己传达的信息),协议 的出现解决了主机间的交流问题。
也就是说,我们通过协议,规定了网络通信的双方,必须按照某种规则来对传输的内容进行解析或者是打包。
对于网络来说,协议是双方通信的基石,如果没有协议,那么即使数据传输的再完美也无法使用,比如下面这个就是一个简单的 两正整数运算协议。
- 协议要求:发送的数据必须由两个操作数(正整数)和一个运算符组成,并且必须遵循
x op y
这样的运算顺序。
- int x;
- int y;
- char op; // 运算符
主机A在发送消息时需要将 操作数x、操作数y和运算符op 进行传递,只要主机A和主机B都遵循这个 协议,那么主机B在收到消息后一定清楚这是两个操作数和一个运算符
现在的问题是如何传递?
方案一:将两个操作数和一个运算符拼接在一起直接传递
方案二:将两个操作数和一个运算符打包成一个结构体传递
- 方案一:直接拼接 xopy
-
- 方案二:封装成结构体
- struct Mssage{
- int x;
- int y;
- char op;
- };
无论是方案一还是方案二都存在问题,前者是对端接收到消息后无法解析,后者则是存在平台兼容问题(不同平台的结构体内存规则可能不同,会导致读取数据出错)
要想确保双方都能正确理解 协议,还需要进行 序列化与反序列化 处理。
序列化是指 将一个或多个需要传递的数据,按照协议的格式,拼接为一条字节流数据,反序列化则是 将收到的数据按照格式解析。
可见,反序列化和序列化就是协议的一部分。
比如主机A想通过 两正整数运算协议 给主机B发送这样的消息:
- //1+1
- int x = 1;
- int y = 1;
- char op = '+';
可以根据格式(这里使用 (空格))进行 序列化,序列化后的数据长这样:
- // 经过序列化后得到
- string msg = "1 + 1";
在经过网络传输后,主机B收到了消息,并根据 (空格)进行 反序列化,成功获取了主机A发送的信息。
- string msg = "1 + 1";
-
- // 经过反序列化后得到
- int x = 1;
- int y = 1;
- char op = '+';
这里可以将需要传递的数据存储在结构体中,传递/接收 时将数据填充至类中,类中提供 序列化与反序列化 的相关接口即可。
- class Request
- {
- public:
- void Serialization(string* str)
- {}
-
- void Deserialization(const sting& str)
- {}
-
- public:
- int _x;
- int _y;
- char _op;
- };
以上就是一个简单的 序列化和反序列化 流程,简单来说就是 协议 定制后不能直接使用,需要配合 序列化与反序列化 这样的工具理解,接下来我们就基于 两正整数运算协议 编写一个简易版的网络计算器,重点在于 理解协议、序列化和反序列化。
我们接下来要编写的程序从实现功能来看是十分简单的:
客户端给出两个正整数和一个运算符,服务器计算出结果后返回
整体框架为:
客户端获取正整数与运算符 -> 将这些数据构建出 Request 对象 -> 序列化 -> 将结果(数据包)传递给服务器 ->
服务器进行反序列化 -> 获取数据 -> 根据数据进行运算 -> 将运算结果构建出 Response 对象(回响对象) -> 序列化 -> 将结果(数据包)传递给客户端 -> 客户端反序列后获取最终结果。
既然这是一个基于网络的简易版计算器,必然离不开网络相关接口,在编写 服务器 与 客户端 的逻辑之前,需要先将 socket
接口进行封装,方面后续的使用。
注:当前实现的程序是基于
TCP
协议的
简单回顾下,服务器需要 创建套接字、绑定IP地址和端口号、进入监听连接状态、等待客户端连接,至于客户端需要 创建套接字、由操作系统绑定IP地址和端口号、连接服务器,等客户端成功连上服务器后,双方就可以正常进行网络通信了。
为了让客户端和服务器都能使用同一个头文件,我们可以把客户端和服务器需要的所有操作都进行实现,各自调用即可。
Sock.hpp 套接字相关接口头文件
- #pragma once
-
- #include "Log.hpp"
- #include "Err.hpp"
-
- #include <iostream>
- #include <string>
- #include <cstring>
- #include <cstdlib>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <unistd.h>
-
- class Sock{
-
- const static int default_sock = -1;
- const static int default_backlog = 32;
- public:
- Sock()
- :_sock(default_sock)
- {}
-
- // 创建套接字
- void Socket(){
- _sock = socket(AF_INET, SOCK_STREAM, 0);
- if(_sock == -1){
- logMessage(Fatal, "Creater Socket Fail! [%d]->%s", errno, strerror(errno));
- exit(SOCKET_ERR);
- }
- logMessage(Debug, "Creater Socket Success");
- }
-
- // 绑定IP与端口号
- void Bind(const uint16_t& port){
-
- 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;
-
- if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) == -1){
- logMessage(Fatal, "Bind Socket Fail! [%d]->%s", errno, strerror(errno));
- exit(BIND_ERR);
- }
- logMessage(Debug, "Bind Socket Success");
- }
-
- // 进入监听状态
- void Listen(){
- if(listen(_sock, default_backlog) == -1) {
- logMessage(Fatal, "Listen Socket Fail! [%d]->%s", errno, strerror(errno));
- exit(LISTEN_ERR);
- }
- }
-
- // 尝试处理连接请求
- int Accept(std::string* ip, uint16_t* port){
- struct sockaddr_in client;
- socklen_t len = sizeof(client);
-
- int retSock = accept(_sock, (struct sockaddr*)&client, &len)
- ;
- if(retSock < 0)
- logMessage(Warning, "Accept Fail! [%d]->%s", errno, strerror(errno));
- else{
- *ip = inet_ntoa(client.sin_addr);
- *port = ntohs(client.sin_port);
- logMessage(Debug, "Accept [%d -> %s:%d] Success", retSock, ip->c_str(), *port);
- }
-
- return retSock;
- }
-
- // 尝试进行连接
- int Connect(const std::string& ip, const uint16_t& port)
- {
- struct sockaddr_in server;
- memset(&server, 0, sizeof(server));
-
- server.sin_family = AF_INET;
- server.sin_port = htons(port);
- server.sin_addr.s_addr = inet_addr(ip.c_str());
-
- return connect(_sock, (struct sockaddr*)&server, sizeof(server));
- }
-
- // 获取sock
- int GetSock(){
- return _sock;
- }
-
- // 关闭sock
- void Close(){
- if(_sock != default_sock){
- close(_sock);
- }
- logMessage(Debug, "Close Sock Success");
- }
-
- ~Sock()
- {}
- private:
- int _sock; // 既可以是监听套接字,也可以是连接成功后返回的套接字
- };
Err.hpp
错误码头文件
- #pragma once
-
- enum{
-
- USAGE_ERR = 1,
- SOCKET_ERR,
- BIND_ERR,
- LISTEN_ERR,
- CONNECT_ERR,
- FORK_ERR,
- SETSID_ERR,
- CHDIR_ERR,
- OPEN_ERR,
- READ_ERR,
-
- };
Log.hpp
日志输出头文件
- #pragma once
-
- #include <iostream>
- #include <string>
- #include <vector>
- #include <cstdio>
- #include <time.h>
- #include <sys/types.h>
- #include <unistd.h>
- #include <stdarg.h>
-
- using namespace std;
-
- enum{
- Debug = 0,
- Info,
- Warning,
- Error,
- Fatal
- };
-
- static const string file_name = "./tcp_log";
-
- string getLevel(int level){
-
- vector<string> vs = {"<Debug>", "<Info>", "<Warning>", "<Error>", "<Fatal>", "<Unknown>"};
-
- //避免非法情况
- if(level < 0 || level >= vs.size() - 1){
- return vs[vs.size() - 1];
- }
-
- return vs[level];
- }
-
- string getTime(){
-
- time_t t = time(nullptr); //获取时间戳
- struct tm *st = localtime(&t); //获取时间相关的结构体
-
- char buff[128];
- snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);
-
- return buff;
- }
-
- //处理信息
- void logMessage(int level, const char* format, ...)
- {
- //日志格式:<日志等级> [时间] [PID] {消息体}
- string logmsg = getLevel(level); //获取日志等级
- logmsg += " " + getTime(); //获取时间
- logmsg += " [" + to_string(getpid()) + "]"; //获取进程PID
-
- //截获主体消息
- char msgbuff[1024];
- va_list p;
- va_start(p, format); //将 p 定位至 format 的起始位置
- vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取
- va_end(p);
-
- logmsg += " {" + string(msgbuff) + "}"; //获取主体消息
-
- // 直接输出至屏幕上 方便进行测试
- cout << logmsg << endl;
-
- // //持久化。写入文件中
- // FILE* fp = fopen(file_name.c_str(), "a"); //以追加的方式写入
- // if(fp == nullptr) return; //不太可能出错
-
- // fprintf(fp, "%s\n", logmsg.c_str());
- // fflush(fp); //手动刷新一下
- // fclose(fp);
- // fp = nullptr;
- }
有了 Sock.hpp
头文件后,服务器/客户端就可以专注于逻辑编写了.
首先准备好 TcpServer.hpp
头文件,其中实现了服务器初始化、服务器启动、序列化与反序列化等功能。
server.hpp
服务器头文件
- #pragma once
-
- #include "Sock.hpp"
- #include <iostream>
- #include <string>
- #include <pthread.h>
-
- namespace My_Server{
-
- class server;
- // 线程所需要的信息类
- class ThreadDate{
- public:
-
- ThreadDate(int& sock, std::string& ip, uint16_t& port, TcpServer* ptsvr)
- :_sock(sock)
- ,_ip(ip)
- ,_port(port)
- ,_ptsvr(ptsvr)
- {}
-
- ~ThreadDate()
- {}
-
- int _sock;
- std::string _ip;
- uint16_t _port;
- TcpServer* _ptsvr; // 回指指针
- };
-
- class server{
- const static uint16_t default_port = 8888;
- private:
- // 线程的执行函数
- static void* threadRoutine(void* args){
-
- // 线程剥离
- pthread_detach(pthread_self());
-
- ThreadDate* td = static_cast<ThreadDate*>(args);
-
- td->_ptsvr->ServiceIO(td->_sock, td->_ip, td->_port);
- delete td;
- return nullptr;
- }
-
- // 进行IO服务的函数
- void ServiceIO(const int& sock, const std::string ip, const uint16_t& port){
- // TODO
- }
-
- public:
-
- server(const uint16_t port = default_port)
- :_port(port)
- {}
-
- // 初始化服务器
- void Init(){
- _listen_sock.Socket();
- _listen_sock.Bind(_port);
- _listen_sock.Listen();
- }
-
-
- // 启动服务器
- void Start(){
-
- while(true){
- std::string ip;
- uint16_t port;
-
- int sock = _listen_sock.Accept(&ip, &port);
- if(sock == -1){
- continue;
- }
- // 创建子线程,执行业务处理
- pthread_t tid;
- ThreadDate* td = new ThreadDate(sock, ip, port, this);
- pthread_create(&tid, nullptr, threadRoutine, td);
- }
- }
-
- ~server(){
- _listen_sock.Close();
- }
-
- private:
- Sock _listen_sock; // 监听套接字
- uint16_t _port; // 服务器端口号
- };
- }
server.cc
简易计算器服务器源文件
- #include <iostream>
- #include <memory>
- #include "server.hpp"
-
- using namespace std;
-
- int main(){
- unique_ptr<My_Server::server> tsvr(new My_Server::server());
-
- tsvr->Init();
- tsvr->Start();
-
- return 0;
- }
Makefile
自动编译脚本
- .PHONY:all
- all:server
- # //client
-
- server:server.cc
- g++ -o $@ $^ -std=c++11 -lpthread
-
- # client:client.cc
- # g++ -o $@ $^ -std=c++11
-
- .PHONY:clean
- clean:
- rm -rf server
- # client
编译并运行程序,同时查看网络使用情况:
netstat -nltp
此时就证明前面写的代码已经没有问题了,接下来是填充 ServiceIO()
函数
ServiceIO()
函数需要做这几件事
除了 序列化和反序列化 外,其他步骤之前都已经见过了,所以我们先来看看如何实现 序列化与反序列化。
ServiceIO()
函数 — 位于 server.hpp
头文件中的server
类中
- // 进行IO服务的函数
- void ServiceIO(const int& sock, const std::string ip, const uint16_t& port){
- // 1.读取数据
-
- // 2.反序列化
-
- // 3.业务处理
-
- // 4.序列化
-
- // 5.发送数据
- }
需要明白我们当前的 协议 为 两正整数运算,分隔符为 (空格),客户端传给服务器两个操作数和一个运算符,服务器在计算完成后将结果返回,为了方便数据的读写,可以创建两个类:Request
(客户端发送的一串待求的运算字符串)和 Response(服务器发送给客户端的结果)
,类中的成员需要遵循协议要求,并在其中支持 序列化与反序列化。
但这两个函数,明显重复率有点高,我们的分隔符同样为一个空格,需要进行提取的,也都是数字,因此我们可以写个工具类,从而方便序列化和反序列化。
Util.hpp
工具类
- #pragma once
- #include <string>
- #include <vector>
-
- class Util{
- public:
-
- //数字转化为字符串
- static std::string IntToStr(int val){
- // 特殊处理
- if(val == 0)
- return "0";
-
- std::string str;
- while(val){
- str += (val % 10) + '0';
- val /= 10;
- }
-
- int left = 0;
- int right = str.size() - 1;
- while(left < right){
- std::swap(str[left++], str[right--]);
- }
- return str;
- }
-
-
- //字符串转化为数字
- static int StrToInt(const std::string& str) {
- int ret = 0;
- for(auto e : str){
- ret = (ret * 10) + (e - '0');
- }
- return ret;
- }
-
- // 将给定的字符串用分隔符进行分割
- static void StringSplit(const std::string& str, const std::string& sep, std::vector<std::string>* result){
- size_t left = 0;
- size_t right = 0;
- while(right < str.size()){
- // 每次right都查找到下一个分隔符
- right = str.find(sep, left);
- if(right == std::string::npos){
- break;
- }
- //left到right之间即为要提取的数字
- result->push_back(str.substr(left, right - left));
- //left指向分割符下一个数字
- left = right + sep.size();
- }
-
- //会漏掉最后一个数字
- if(left < str.size()){
- result->push_back(str.substr(left));
- }
- }
- };
Protocol.hpp
协议处理相关头文件。
- #pragma once
- #include <string>
- #include"Util.hpp"
- #include<iostream>
- namespace My_protocol{
-
- // 协议的分隔符 这里我们自己设定为" "
- const char* SEP= " ";
- //分隔符长度
- const int SEP_LEN = strlen(SEP);
-
- //对运算进行序列和反序列化
- class Request{
- public:
- Request(int x = 0, int y = 0, char op = '+')
- : _x(x), _y(y), _op(op)
- {}
-
- // 序列化
- bool Serialization(std::string *outStr){
- *outStr = ""; // 清空
-
- std::string left = Util::IntToStr(_x);
- std::string right = Util::IntToStr(_y);
- *outStr = left + SEP + _op + SEP + right;
-
- return true;
- }
-
- // 反序列化
- bool Deserialization(const std::string &inStr){
-
- std::vector<std::string> result;
- Util::StringSplit(inStr, SEP, &result);
-
- // 协议规定:只允许存在两个操作数和一个运算符
- if(result.size() != 3){
- return false;
- }
-
- // 规定:运算符只能为一个字符
- if(result[1].size() != 1){
- return false;
- }
-
- _x = Util::StrToInt(result[0]);
- _y = Util::StrToInt(result[2]);
- _op = result[1][0];
-
- return true;
- }
-
- ~Request()
- {}
-
- public:
- int _x;
- int _y;
- char _op;
- };
-
- //业务处理函数
- class Response
- {
- public:
- Response(int result = 0, int code = 0)
- :_result(result), _code(code)
- {}
-
- // 序列化
- bool Serialization(std::string *outStr) {
-
- *outStr = ""; // 清空
- std::string left = Util::IntToStr(_result);
- std::string right = Util::IntToStr(_code);
- *outStr = left + SEP + right;
-
- return true;
- }
-
- // 提取结果和错误码
- bool Deserialization(const std::string &inStr){
-
- std::vector<std::string> result;
- Util::StringSplit(inStr, SEP, &result);
-
- if(result.size() != 2)
- return false;
-
- _result = Util::StrToInt(result[0]);
- _code = Util::StrToInt(result[1]);
-
- return true;
- }
-
- ~Response()
- {}
-
- public:
- int _result; // 结果
- int _code; // 错误码
- };
-
- }
server
中的业务处理函数由 CalcServer.cc
传递,规定业务处理函数的类型为 void(Request&, Response*)
Calculate()
函数 — 位于server.cc
- #include "server.hpp"
- #include "Protocol.hpp"
-
- #include <iostream>
- #include <memory>
- #include <functional>
- #include <unordered_map>
-
- using namespace std;
-
- void Calculate(My_protocol::Request& req, My_protocol::Response* resp){
-
- // 这里只是简单的计算而已
- int x = req._x;
- int y = req._y;
- char op = req._op;
- unordered_map<char, function<int()>> hash =
- {
- {'+', [&](){ return x + y; }},
- {'-', [&](){ return x - y; }},
- {'*', [&](){ return x * y; }},
- {'/', [&]()
- {
- if(y == 0)
- {
- resp->_code = 1;
- return 0;
- }
- return x / y;
- }
- },
- {'%', [&]()
- {
- if(y == 0)
- {
- resp->_code = 2;
- return 0;
- }
- return x % y;
- }
- }
- };
-
- if(hash.count(op) == 0)
- resp->_code = 3;
- else
- resp->_result = hash[op]();
- }
-
- int main(){
-
- unique_ptr<My_Server::server> tsvr(new My_Server::server(Calculate));
-
- tsvr->Init();
- tsvr->Start();
-
- return 0;
- }
既然 CalcServer
中传入了 Calculate()
函数对象,server
类中就得接收并使用,也就是业务处理.
server.hpp
头文件
- #pragma once
-
- #include "Sock.hpp"
- #include <iostream>
- #include <string>
- #include <pthread.h>
- #include "Protocol.hpp"
- #include<functional>
-
- namespace My_Server
- {
-
- class server;
- // 线程所需要的信息类
- class ThreadDate
- {
- public:
- ThreadDate(int &sock, std::string &ip, uint16_t &port, server *ptsvr)
- : _sock(sock), _ip(ip), _port(port), _ptsvr(ptsvr)
- {
- }
-
- ~ThreadDate()
- {
- }
-
- int _sock;
- std::string _ip;
- uint16_t _port;
- server *_ptsvr; // 回指指针
- };
-
- using func_t = std::function<void(My_protocol::Request&, My_protocol::Response*)>;
-
- class server
- {
- const static uint16_t default_port = 8088;
-
- private:
- // 线程的执行函数
- static void *threadRoutine(void *args)
- {
-
- // 线程剥离
- pthread_detach(pthread_self());
-
- ThreadDate *td = static_cast<ThreadDate *>(args);
-
- td->_ptsvr->ServiceIO(td->_sock, td->_ip, td->_port);
- delete td;
- return nullptr;
- }
-
- // 进行IO服务的函数
- void ServiceIO(const int &sock, const std::string ip, const uint16_t &port){
- while (true){
- // 1.读取数据
- std::string package; // 假设这是已经读取到的数据包,格式为 "1 + 1"
-
- // 2.反序列化
- My_protocol::Request req;
- if (req.Deserialization(package) == false){
- logMessage(Warning, "Deserialization fail!");
- continue;
- }
-
- // 3.业务处理
- // TODO
- My_protocol::Response resp; // 业务处理完成后得到的响应对象
- _func(req, &resp);
-
- // 4.序列化
- std::string sendMsg;
- resp.Serialization(&sendMsg);
- std::cout<<sendMsg<<std::endl;
- // 5.发送数据
- }
- }
-
- public:
- server(func_t fun,const uint16_t port = default_port)
- : _port(port)
- ,_func(fun)
- {}
-
- // 初始化服务器
- void Init(){
- _listen_sock.Socket();
- _listen_sock.Bind(_port);
- _listen_sock.Listen();
- }
-
- // 启动服务器
- void Start(){
-
- while (true){
- std::string ip;
- uint16_t port;
-
- int sock = _listen_sock.Accept(&ip, &port);
- if (sock == -1)
- {
- continue;
- }
- // 创建子线程,执行业务处理
- pthread_t tid;
- ThreadDate *td = new ThreadDate(sock, ip, port, this);
- pthread_create(&tid, nullptr, threadRoutine, td);
- }
- }
-
- ~server(){
- _listen_sock.Close();
- }
-
- private:
- Sock _listen_sock; // 监听套接字
- uint16_t _port; // 服务器端口号
- func_t _func; // 上层传入的业务处理函数
-
- };
- }
这就做好业务处理了,ServiceIO() 函数已经完成了 50% 的工作,接下来的重点是如何读取和发送数据?
TCP 协议是面向字节流的,这也就意味着数据在传输过程中可能会因为网络问题,分为多次传输,这也就意味着我们可能无法将其一次性读取完毕,需要制定一个策略,来确保数据全部递达.
如何确认自己已经读取完了所以数据?答案是提前知道目标数据的长度,边读取边判断
数据在发送时,是需要在前面添加 长度 这个信息的,通常将其称为 报头,而待读取的数据称为 有效载荷,报头 和 有效载荷 的关系类似于快递单与包裹的关系,前者是后者成功递达的保障
最简单的 报头 内容就是 有效载荷 的长度。
问题来了,如何区分 报头 与 有效载荷 呢?
- 当前可以确定的是,我们的报头中只包含了长度这个信息
- 可以通过添加特殊字符,如
\r\n
的方式进行区分- 后续无论有效载荷变成什么内容,都不影响我们通过报头进行读取
报头处理属于协议的一部分
所以在正式读写数据前,需要解决 报头 的问题(收到数据后移除报头,发送数据前添加报头)
ReadPackage()
读取函数 — 位于 Protocol.hpp
头文件
在
Protocol.hpp
中完成报头的添加和移除
- #define HEAD_SEP "\r\n"
- #define HEAD_SEP_LEN strlen(HEAD_SEP)
-
- // 添加报头
- void AddHeader(std::string& str){
- // 先计算出长度
- size_t len = str.size();
- std::string strLen = Util::IntToStr(len);
-
- // 再进行拼接
- str = strLen + HEAD_SEP + str;
- }
-
- // 移除报头
- void RemoveHeader(std::string& str, size_t len){
- // len 表示有效载荷的长度
- str = str.substr(str.size() - len);
- }
报头+有效载荷需要通过 read()
或者 recv()
函数从网络中读取,并且需要边读取边判断。
ReadPackage()
读取函数 — 位于Protocol.hpp
头文件
- #define BUFF_SIZE 1024
- // 读取数据
- int ReadPackage(int sock, std::string& inBuff, std::string* package){
-
- // 也可以使用 read 函数
- char buff[BUFF_SIZE];
- int n = recv(sock, buff, sizeof(buff) - 1, 0);
- if(n < 0)
- return -1; // 表示读取失败
- else if(n == 0)
- return 0; // 需要继续读取
-
- buff[n] = '\0';
- inBuff += buff;
-
- // 判断 inBuff 中是否存在完整的数据包(报头\r\n有效载荷)
- int pos = inBuff.find(HEAD_SEP);
- if(pos == std::string::npos)
- return -1;
-
- std::string strLen = inBuff.substr(0, pos); // 有效载荷的长度
- int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 这是 报头+分隔符+有效载荷 的总长度
- if(inBuff.size() < packLen)
- return -1;
-
- *package = inBuff.substr(0, packLen); // 获取 报头+分隔符+有效载荷 ,也就是数据包
- inBuff.erase(0, packLen); // 从缓冲区中取走字符串
- return Util::StrToInt(strLen);
- }
完整代码:
- #pragma once
- #include <string>
- #include"Util.hpp"
- #include<iostream>
-
-
-
- namespace My_protocol{
-
- #define HEAD_SEP "\r\n"
- #define HEAD_SEP_LEN strlen(HEAD_SEP)
-
- // 添加报头
- void AddHeader(std::string& str){
- // 先计算出长度
- size_t len = str.size();
- std::string strLen = Util::IntToStr(len);
-
- // 再进行拼接
- str = strLen + HEAD_SEP + str;
- }
-
- // 移除报头
- void RemoveHeader(std::string& str, size_t len){
- // len 表示有效载荷的长度
- str = str.substr(str.size() - len);
- }
-
- #define BUFF_SIZE 1024
- // 读取数据
- int ReadPackage(int sock, std::string& inBuff, std::string* package){
-
- // 也可以使用 read 函数
- char buff[BUFF_SIZE];
- int n = recv(sock, buff, sizeof(buff) - 1, 0);
- if(n < 0)
- return -1; // 表示读取失败
- else if(n == 0)
- return 0; // 需要继续读取
-
- buff[n] = '\0';
- inBuff += buff;
-
- // 判断 inBuff 中是否存在完整的数据包(报头\r\n有效载荷)
- int pos = inBuff.find(HEAD_SEP);
- if(pos == std::string::npos)
- return -1;
-
- std::string strLen = inBuff.substr(0, pos); // 有效载荷的长度
- int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 这是 报头+分隔符+有效载荷 的总长度
- if(inBuff.size() < packLen)
- return -1;
-
- *package = inBuff.substr(0, packLen); // 获取 报头+分隔符+有效载荷 ,也就是数据包
- inBuff.erase(0, packLen); // 从缓冲区中取走字符串
- return Util::StrToInt(strLen);
- }
-
-
-
- // 协议的分隔符 这里我们自己设定为" "
- const char* SEP= " ";
- //分隔符长度
- const int SEP_LEN = strlen(SEP);
-
- //对运算进行序列和反序列化
- class Request{
- public:
- Request(int x = 0, int y = 0, char op = '+')
- : _x(x), _y(y), _op(op)
- {}
-
- // 序列化
- bool Serialization(std::string *outStr){
- *outStr = ""; // 清空
-
- std::string left = Util::IntToStr(_x);
- std::string right = Util::IntToStr(_y);
- *outStr = left + SEP + _op + SEP + right;
-
- return true;
- }
-
- // 反序列化
- bool Deserialization(const std::string &inStr){
-
- std::vector<std::string> result;
- Util::StringSplit(inStr, SEP, &result);
-
- // 协议规定:只允许存在两个操作数和一个运算符
- if(result.size() != 3){
- return false;
- }
-
- // 规定:运算符只能为一个字符
- if(result[1].size() != 1){
- return false;
- }
-
- _x = Util::StrToInt(result[0]);
- _y = Util::StrToInt(result[2]);
- _op = result[1][0];
-
- return true;
- }
-
- ~Request()
- {}
-
- public:
- int _x;
- int _y;
- char _op;
- };
-
- //业务处理函数
- class Response
- {
- public:
- Response(int result = 0, int code = 0)
- :_result(result), _code(code)
- {}
-
- // 序列化
- bool Serialization(std::string *outStr) {
-
- *outStr = ""; // 清空
- std::string left = Util::IntToStr(_result);
- std::string right = Util::IntToStr(_code);
- *outStr = left + SEP + right;
-
- return true;
- }
-
- // 提取结果和错误码
- bool Deserialization(const std::string &inStr){
-
- std::vector<std::string> result;
- Util::StringSplit(inStr, SEP, &result);
-
- if(result.size() != 2)
- return false;
-
- _result = Util::StrToInt(result[0]);
- _code = Util::StrToInt(result[1]);
-
- return true;
- }
-
- ~Response()
- {}
-
- public:
- int _result; // 结果
- int _code; // 错误码
- };
-
- }
此时对于 ServiceIO()
函数来说,核心函数都已经准备好了,只差拼装了。
ServiceIO()
函数 — 位于server.hpp
头文件中的server
类中
- // 进行IO服务的函数
- void ServiceIO(const int &sock, const std::string ip, const uint16_t &port)
- {
- std::string inBuff;
- while (true)
- {
- // 1.读取数据
- std::string package; // 假设这是已经读取到的数据包,格式为 "5\r\n1 + 1"
- int len = My_protocol::ReadPackage(sock, inBuff, &package);
- if (len < 0)
- break;
- else if (len == 0)
- continue;
-
- // 2.移除报头
- My_protocol::RemoveHeader(package, len);
-
- // 3.反序列化
- My_protocol::Request req;
- if (req.Deserialization(package) == false)
- {
- logMessage(Warning, "Deserialization fail!");
- continue;
- }
-
- // 4.业务处理
- My_protocol::Response resp; // 业务处理完成后得到的响应对象
- _func(req, &resp);
-
- // 5.序列化
- std::string sendMsg;
- resp.Serialization(&sendMsg);
- cout << sendMsg << endl;
-
- // 6.添加报头
- My_protocol::AddHeader(sendMsg);
-
- // 7.发送数据
- send(sock, sendMsg.c_str(), sendMsg.size(), 0);
- }
- }
至此服务器编写完毕,接下来就是进行客户端的编写了.
Client.hpp
客户端头文件
- #pragma once
-
- #include "Sock.hpp"
- #include "Protocol.hpp"
- #include "Log.hpp"
- #include "Err.hpp"
-
- #include <iostream>
- #include <string>
- #include <unistd.h>
-
- namespace My_Client
- {
- class client
- {
- public:
- client(const std::string& ip, const uint16_t& port)
- :_server_ip(ip)
- ,_server_port(port)
- {}
-
- void Init()
- {
- _sock.Socket();
- }
-
- void Start()
- {
- int i = 5;
- while(i > 0)
- {
- if(_sock.Connect(_server_ip, _server_port) != -1)
- break;
-
- logMessage(Warning, "Connect Server Fail! %d", i--);
- sleep(1);
- }
-
- if(i == 0)
- {
- logMessage(Fatal, "Connect Server Fail!");
- exit(CONNECT_ERR);
- }
-
- // 执行读写函数
- ServiceIO();
- }
-
- void ServiceIO()
- {
- while(true)
- {
- std::string str;
- std::cout << "Please Enter:> ";
- std::getline(std::cin, str);
-
- // 1.判断是否需要退出
- if(str == "quit")
- break;
-
- // 2.分割输入的字符串
- My_protocol::Request req;
- [&]()
- {
- std::string ops = "+-*/%";
- int pos = 0;
- for(auto e : ops)
- {
- pos = str.find(e);
- if(pos != std::string::npos)
- break;
- }
-
- req._x = Util::StrToInt(str.substr(0, pos));
- req._y = Util::StrToInt(str.substr(pos + 1));
- req._op = str[pos];
- }();
-
- // 3.序列化
- std::string sendMsg;
- req.Serialization(&sendMsg);
-
- // 4.添加报头
- My_protocol::AddHeader(sendMsg);
-
- // 5.发送数据
- send(_sock.GetSock(), sendMsg.c_str(), sendMsg.size(), 0);
-
- // 6.获取数据
- std::string inBuff;
- std::string package;
- int len = 0;
- while(true)
- {
- len = My_protocol::ReadPackage(_sock.GetSock(), inBuff, &package);
- if(len < 0)
- exit(READ_ERR);
- else if(len > 0)
- break;
- }
-
- // 7.移除报头
- My_protocol::RemoveHeader(package, len);
-
- // 8.反序列化
- My_protocol::Response resp;
- if(resp.Deserialization(package) == false)
- {
- logMessage(Warning, "Deserialization fail!");
- continue;
- }
-
- // 9.获取结果
- std::cout << "The Result: " << resp._result << " " << resp._code << endl;
- }
- }
-
- ~client()
- {
- _sock.Close();
- }
-
- private:
- Sock _sock;
- std::string _server_ip;
- uint16_t _server_port;
- };
- }
client.cc
客户端源文件
- #include "client.hpp"
-
- #include <iostream>
- #include <memory>
-
- using namespace std;
-
- int main()
- {
- unique_ptr<My_Client::client> tclt(new My_Client::client("127.0.0.1", 8088));
-
- tclt->Init();
- tclt->Start();
-
- return 0;
- }
事实上,序列化与反序列化 这种工作轮不到我们来做,因为有更好更强的库,比如 Json
、XML
、Protobuf
等
比如我们就可以使用 Json
来修改程序
首先需要安装 json-cpp
库,如果是 CentOS7
操作系统的可以直接使用下面这条命令安装
yum install -y jsoncpp-devel
安装完成后,可以引入头文件 <jsoncpp/json/json.h>
然后就可以在 Protocol.hpp
头文件中进行修改了,如果想保留原来自己实现的 序列化与反序列化 代码,可以利用 条件编译 进行区分
Protocol.hpp
协议相关头文件
- #pragma once
- #include "Util.hpp"
-
- #include <jsoncpp/json/json.h>
- #include <string>
- #include <vector>
- #include <cstring>
- #include <sys/types.h>
- #include <sys/socket.h>
-
- namespace My_protocol
- {
- // 协议的分隔符
- #define SEP " "
- #define SEP_LEN strlen(SEP)
- #define HEAD_SEP "\r\n"
- #define HEAD_SEP_LEN strlen(HEAD_SEP)
- #define BUFF_SIZE 1024
- // #define USER 1
-
- // 添加报头
- void AddHeader(std::string& str)
- {
- // 先计算出长度
- size_t len = str.size();
- std::string strLen = Util::IntToStr(len);
-
- // 再进行拼接
- str = strLen + HEAD_SEP + str;
- }
-
- // 移除报头
- void RemoveHeader(std::string& str, size_t len)
- {
- // len 表示有效载荷的长度
- str = str.substr(str.size() - len);
- }
-
- // 读取数据
- int ReadPackage(int sock, std::string& inBuff, std::string* package)
- {
- // 也可以使用 read 函数
- char buff[BUFF_SIZE];
- int n = recv(sock, buff, sizeof(buff) - 1, 0);
- if(n < 0)
- return -1; // 表示什么都没有读到
- else if(n == 0)
- return 0; // 需要继续读取
-
- buff[n] = 0;
- inBuff += buff;
-
- // 判断 inBuff 中是否存在完整的数据包(报头\r\n有效载荷)
- int pos = inBuff.find(HEAD_SEP);
- if(pos == std::string::npos)
- return 0;
-
- std::string strLen = inBuff.substr(0, pos); // 有效载荷的长度
- int packLen = strLen.size() + HEAD_SEP_LEN + Util::StrToInt(strLen); // 这是 报头+分隔符+有效载荷 的总长度
- if(inBuff.size() < packLen)
- return 0;
-
- *package = inBuff.substr(0, packLen); // 获取 报头+分隔符+有效载荷 ,也就是数据包
- inBuff.erase(0, packLen); // 从缓冲区中取走字符串
- return Util::StrToInt(strLen);
- }
-
- class Request
- {
- public:
- Request(int x = 0, int y = 0, char op = '+')
- : _x(x), _y(y), _op(op)
- {}
-
- // 序列化
- bool Serialization(std::string *outStr)
- {
- *outStr = ""; // 清空
- #ifdef USER
- std::string left = Util::IntToStr(_x);
- std::string right = Util::IntToStr(_y);
- *outStr = left + SEP + _op + SEP + right;
- #else
- // 使用 Json
- Json::Value root;
- root["x"] = _x;
- root["op"] = _op;
- root["y"] = _y;
-
- Json::FastWriter writer;
- *outStr = writer.write(root);
- #endif
- std::cout << "序列化完成: " << *outStr << std::endl << std::endl;
- return true;
- }
-
- // 反序列化
- bool Deserialization(const std::string &inStr)
- {
- #ifdef USER
- std::vector<std::string> result;
- Util::StringSplit(inStr, SEP, &result);
-
- // 协议规定:只允许存在两个操作数和一个运算符
- if(result.size() != 3)
- return false;
-
- // 规定:运算符只能为一个字符
- if(result[1].size() != 1)
- return false;
-
- _x = Util::StrToInt(result[0]);
- _y = Util::StrToInt(result[2]);
- _op = result[1][0];
- #else
- // 使用Json
- Json::Value root;
- Json::Reader reader;
- reader.parse(inStr, root);
-
- _x = root["x"].asInt();
- _op = root["op"].asInt();
- _y = root["y"].asInt();
- #endif
- return true;
- }
-
- ~Request()
- {}
-
- public:
- int _x;
- int _y;
- char _op;
- };
-
- class Response
- {
- public:
- Response(int result = 0, int code = 0)
- :_result(result), _code(code)
- {}
-
- // 序列化
- bool Serialization(std::string *outStr)
- {
- *outStr = ""; // 清空
- #ifdef USER
- std::string left = Util::IntToStr(_result);
- std::string right = Util::IntToStr(_code);
- *outStr = left + SEP + right;
- #else
- // 使用 Json
- Json::Value root;
- root["_result"] = _result;
- root["_code"] = _code;
-
- Json::FastWriter writer;
- *outStr = writer.write(root);
- #endif
- std::cout << "序列化完成: " << *outStr << std::endl << std::endl;
- return true;
- }
-
- // 反序列化
- bool Deserialization(const std::string &inStr)
- {
- #ifdef USER
- std::vector<std::string> result;
- Util::StringSplit(inStr, SEP, &result);
-
- if(result.size() != 2)
- return false;
-
- _result = Util::StrToInt(result[0]);
- _code = Util::StrToInt(result[1]);
- #else
- // 使用Json
- Json::Value root;
- Json::Reader reader;
- reader.parse(inStr, root);
-
- _result = root["_result"].asInt();
- _code = root["_code"].asInt();
- #endif
- return true;
- }
-
- ~Response()
- {}
-
- public:
- int _result; // 结果
- int _code; // 错误码
- };
- }
注意: 因为现在使用了 Json
库,所以编译代码时需要指明其动态库
- .PHONY:all
- all:server client
-
- server:server.cc
- g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
-
- client:client.cc
- g++ -o $@ $^ -std=c++11 -ljsoncpp
-
- .PHONY:clean
- clean:
- rm -rf server client
使用了 Json
库之后,序列化 后的数据会更加直观,当然也更易于使用
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。