文章目录
- 一、网络协议
- 二、序列化和反序列化
- 1. 结构化数据
- 2. 序列化和反序列化
- 三、网络版计算器
- 1. 协议定制
- 2. 客户端处理收到的数据
- 3. 整体代码
一、网络协议
网络协议
是通信计算机双方必须共同遵从的一组约定,为了使数据在网络上能够从源地址到目的地址,网络通信的参与方必须遵循相同的规则,因此我们将这套规则称为协议,协议最终需要通过计算机语言的方式表示出来。
二、序列化和反序列化
1. 结构化数据
如果需要传输的数据是字符串,那么我们可以直接将这个字符串发送到网络当中。但是如果我们传输的是一些结构化的数据
,此时就不能直接将这些数据发送到网络当中。
如果服务器将这些结构化的数据单独一个一个发送到网络当中,那么服务器从网络当中获取这些数据时也只能一个一个获取,并且结构化的数据往往具有特定的格式和规范,例如数据库表格或者特定的数据模型。如果直接将这些数据发送到网络,服务端可能需要处理大量复杂的格式转换和数据清洗工作,而且还有可能会影响数据的正确性。所以客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据。
常见的打包方式:
-
将结构化的数据组合成一个字符串:
客户端可以按照某种方式将这些结构化的数据组合成一个字符串,然后将该字符串发送到网络当中,当服务器接收到这个字符串时,以相同的方式将这个字符串进行解析。就可以从这个字符串中提取出这些结构化的数据。 -
定制结构体 + 序列化与反序列化
客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后在对其进行反序列化,此时服务端就能得到客户端发送过来的结构体了。
2. 序列化和反序列化
序列化:
将对象的状态信息转换为可以存储或传输的形式的过程。反序列化:
将字节序列恢复为的过程。
OSI七层模型中表示层的作用就是,实现设备固有数据格式和网络标准数据格式的转换。其中设备固有的数据格式指的是数据在应用层上的格式,而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式。
💕 序列化和反序列化的目的:
序列化的目的是方便网络数据的发送和接收,无论何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络传输时看到的都是二进制序列。
序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制数据的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。
三、网络版计算器
1. 协议定制
💕 请求数据和响应数据
这里我们实现一个请求结构体和一个响应结构体。
请求数据
结构体中包含两个操作数和一个操作符,操作数表示需要进行计算的两个数,操作符表示是要进行+-*/
中的哪一个操作。响应数据
结构体需要包含一个计算结果和一个状态码,表示本次计算的结果和计算状态。
状态码所表示的含义:
0
表示计算成功1
表示出现除0错误2
表示模0错误3
表示非法计算
因为协议定制好以后要被客户端和服务端同时遵守,所以需要将它写到一个头文件中,同时被客户端和服务端包含该头文件。
// 请求
class Request
{
public:Request(){}Request(int x, int y, char op):_x(x), _y(y), _op(op){}
public: int _x;int _y;char _op;
};// 响应
class Response
{
public:Response(){}Response(int result, int code):_result(result), _code(code){}
public:int _result;int _code;
};
💕 自定义序列化和反序列化
请求序列化
请求反序列化
响应序列化
响应反序列化
💕 添加报头和去除报头
为了保证服务器在接收时能够接收到一个完整的报文,我们需要给已经序列化完成的字符串添加报头。此报头来标识服务器读取到的数据是否是完整的,因此我们在序列化后的字符串前面添加要发送的字符串的长度,以便于服务器读取到完整的报文。
添加报头
去除报头
去除报头
2. 客户端处理收到的数据
当客户端收到已经反序列化完成之后的数据后,需要将其分隔到数组里面,然后在转换为结构化的数据类型进行计算。
服务器收到去除字符串报头后的结果,需要将其分隔并反序列化
客户端收到去除字符串报头后的结果,需要将其分隔并反序列化
3. 整体代码
💕 Sock.hpp
#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Err.hpp"using namespace std;
static const int gbacklog = 32;
static const int defaultfd = -1;class Sock
{
public:Sock():_sock(defaultfd){}Sock(int sock):_sock(sock){}// 创建套接字void Socket(){_sock = socket(AF_INET, SOCK_STREAM, 0);if(_sock < 0){logMessage(Fatal, "socket error, code: %d, errstring: %s", errno, strerror(errno));exit(SOCKET_ERR);}}// 绑定端口号 和 ip地址void Bind(const uint16_t& port){struct sockaddr_in local;local.sin_addr.s_addr = INADDR_ANY; // 绑定任意IP地址local.sin_port = htons(port); // 绑定端口号local.sin_family = AF_INET;if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));exit(BIND_ERR);}}// 监听void Listen(){if(listen(_sock, gbacklog) < 0){logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));exit(LISTEN_ERR);}}// 获取连接int Accept(string* clientip, uint16_t* clientport){struct sockaddr_in temp;socklen_t len = sizeof(temp);int sock = accept(_sock, (struct sockaddr*)&temp, &len);if(sock < 0){logMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));}else{*clientip = inet_ntoa(temp.sin_addr);*clientport = ntohs(temp.sin_port);}return sock;}// 客户端和服务器建立连接int Connect(const string& serverip, const uint16_t& serverport){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());return connect(_sock, (struct sockaddr*)&server, sizeof(server));}int Fd(){return _sock;}void Close(){if(_sock != defaultfd)close(_sock);}~Sock(){}private:int _sock;
};
💕 Protocol.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <jsoncpp/json/json.h>
#include "Util.hpp"
// 给网络版本计算器定制协议// #define MYSELF 1// 给网络版本计算器定制协议
namespace protocol_ns
{#define SEP " "#define SEP_LEN strlen(SEP) // 注意不能写成 sizeof#define HEADER_SEP "\r\n"#define HEADER_SEP_LEN strlen("\r\n")// 添加报头后 —— "长度"\r\n""_x _op _y"\r\n string AddHeader(const string& str){std::cout << "AddHeader 之前:\n"<< str << std::endl;string s = to_string(str.size());s += HEADER_SEP;s += str;s += HEADER_SEP;std::cout << "AddHeader 之后:\n"<< s << std::endl;return s;}// 去除报头 "长度"\r\n""_x _op _y"\r\n —— _x _op _ystring RemoveHeader(const std::string &str, int len){std::cout << "RemoveHeader 之前:\n"<< str << std::endl;string res = str.substr(str.size() - HEADER_SEP_LEN - len, len);return res;std::cout << "RemoveHeader 之后:\n"<< res << std::endl;}int ReadPackage(int sock, string& inbuffer, string*package){std::cout << "ReadPackage inbuffer 之前:\n"<< inbuffer << std::endl;// 边读取char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer - 1), 0);if(s <= 0)return -1;buffer[s] = 0;inbuffer += buffer;std::cout << "ReadPackage inbuffer 之中:\n"<< inbuffer << std::endl;// 边读取边分析, "7"\r\n""10 + 20"\r\nauto pos = inbuffer.find(HEADER_SEP); // 查找 \r\nif(pos == string::npos)return 0;string lenStr = inbuffer.substr(0, pos); // 获取头部字符串int len = Util::toInt(lenStr);int targetPackageLen = lenStr.size() + len + 2 * HEADER_SEP_LEN; // 获取到的完整字符串的长度if(inbuffer.size() < targetPackageLen)return 0;*package = inbuffer.substr(0, targetPackageLen);inbuffer.erase(0, targetPackageLen); std::cout << "ReadPackage inbuffer 之后:\n"<< inbuffer << std::endl;return len; // 返回有效载荷的长度}// Request && Response都要提供序列化和反序列化功能——自己手写// 请求class Request{public:Request(){}Request(int x, int y, char op):_x(x), _y(y), _op(op){}// 协议的序列化 struct -> stringbool Serialize(string* outStr){*outStr = "";
#ifdef MYSELFstring x_string = to_string(_x);string y_string = to_string(_y);// 手动序列化*outStr = x_string + SEP + _op + SEP + y_string;std::cout << "Request Serialize:\n"<< *outStr << std::endl;
#elseJson::Value root; // Value 是一种万能对象, 可以接受任意的kv类型root["x"] = _x;root["y"] = _y;root["op"] = _op;// Json::FastWriter writer; // Writer 是用来序列化的, struct -> stringJson::StyledWriter writer;*outStr = writer.write(root);
#endifreturn true;}// 协议的反序列化 string -> structbool Deserialize(const string& inStr){
#ifdef MYSELF// 将inStr分隔到数组里面 -> [0]=>10 [1]=>+ [2]=>20vector<string> result;Util::StringSplit(inStr, SEP, &result);if(result.size() != 3)return false;if(result[1].size() != 1)return false;_x = Util::toInt(result[0]);_y = Util::toInt(result[2]);_op = result[1][0];
#elseJson::Value root;Json::Reader reader; // Reader是用来进行反序列化的reader.parse(inStr, root);_x = root["x"].asInt();_y = root["y"].asInt();_op = root["op"].asInt();
#endifreturn true;}~Request(){}public: int _x; // 操作数 _xint _y; // 操作数 _ychar _op;// 操作符 _op};// 响应class Response{public:Response(){}Response(int result, int code):_result(result), _code(code){}bool Serialize(string* outStr){*outStr = "";
#ifdef MYSELFstring res_string = to_string(_result);string code_string = to_string(_code);// 手动序列化*outStr = res_string + SEP + code_string;std::cout << "Response Serialize:\n"<< *outStr << std::endl;
#elseJson::Value root;root["result"] = _result;root["code"] = _code;// Json::FastWriter writer;Json::StyledWriter writer;*outStr = writer.write(root);
#endifreturn true;}bool Deserialize(const string& inStr){
#ifdef MYSELF// 将inStr分隔到数组里面 -> [0]=>10 [1]=>+ [2]=>20vector<string> result;Util::StringSplit(inStr, SEP, &result);if(result.size() != 2)return false;_result = Util::toInt(result[0]);_code = Util::toInt(result[1]);
#elseJson::Value root;Json::Reader reader;reader.parse(inStr, root);_result = root["result"].asInt();_code = root["code"].asInt();
#endifPrint();return true;}void Print(){std::cout << "_result: " << _result << std::endl;std::cout << "_code: " << _code << std::endl;}public:int _result;int _code;};
}
💕 CalculatorClient.cc
#include "Sock.hpp"
#include "Protocol.hpp"#include <iostream>
#include <string>using namespace std;
using namespace protocol_ns;static void usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}enum
{LEFT,OPER,RIGHT
};Request ParseLine(const std::string &line)
{std::string left, right;char op;int status = LEFT;int i = 0;while(i < line.size()){switch (status){case LEFT:if (isdigit(line[i]))left.push_back(line[i++]);elsestatus = OPER;break;case OPER:op = line[i++];status = RIGHT;break;case RIGHT:if (isdigit(line[i]))right.push_back(line[i++]);break;}}Request req;req._x = std::stoi(left);req._y = std::stoi(right);req._op = op;return req;
}int main(int argc, char* argv[])
{if(argc != 3){usage(argv[0]);exit(USAGE_ERR);}string serverip = argv[1];uint16_t serverport = atoi(argv[2]);Sock sock;sock.Socket();int n = sock.Connect(serverip, serverport);if(n != 0) return 1;string buffer;while(true){cout << "Enter# ";string line;getline(cin, line);Request req = ParseLine(line);cout << "test:" << req._x << req._op << req._y << endl;// 序列化string sendString;req.Serialize(&sendString);// 添加报头sendString = AddHeader(sendString);// sendsend(sock.Fd(), sendString.c_str(), sendString.size(), 0);// 获取响应string package;int n = 0;START:n = ReadPackage(sock.Fd(), buffer, &package);if(n == 0)goto START;else if(n < 0)break;else{}// 去掉报头package = RemoveHeader(package, n);// 反序列化Response resp;resp.Deserialize(package);cout << "result: " << resp._result << "[code: " << resp._code << "]" << endl;}sock.Close();return 0;
}
💕 TcpServer.hpp
#pragma once
#include <iostream>
#include <functional>
#include <cstring>
#include <pthread.h>
#include "Sock.hpp"
#include "Protocol.hpp"using namespace std;namespace tcpserver_ns
{using namespace protocol_ns;using func_t = function<Response(const Request&)>;class TcpServer;class ThreadData{public:ThreadData(int sock, string ip, uint16_t port, TcpServer* tsvrp):_sock(sock), _ip(ip), _port(port), _tsvrp(tsvrp){}~ThreadData(){}public:int _sock;string _ip;uint16_t _port;TcpServer *_tsvrp;};class TcpServer{public:TcpServer(func_t func, uint16_t port):_func(func), _port(port){}// 初始化服务器void InitServer(){_listensock.Socket();_listensock.Bind(_port);_listensock.Listen();logMessage(Info, "init server done, listensock: %d, errstring: %s", errno, strerror(errno));}// 运行服务器void Start(){while(true){string clientip;uint16_t clientport;int sock = _listensock.Accept(&clientip, &clientport);if(sock < 0) continue;logMessage(Debug, "get a new client, client info : [%s:%d]", clientip.c_str(), clientport);pthread_t tid; // 创建多线程ThreadData *td = new ThreadData(sock, clientip, clientport, this);pthread_create(&tid, nullptr, ThreadRoutine, td);}}static void* ThreadRoutine(void* args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->_tsvrp->ServiceIO(td->_sock, td->_ip, td->_port);logMessage(Debug, "thread quit, client quit ...");delete td;return nullptr;}// 服务器对客户端的数据进行IO处理void ServiceIO(int sock, const std::string &ip, const uint16_t &port){string inbuffer;while(true){// 保证自己读到一个完整的字符串报文 "7"\r\n""10 + 20"\r\nstring package;int n = ReadPackage(sock, inbuffer, &package);if(n == -1)break;else if(n == 0)continue;else{// 已经得到了一个"7"\r\n""10 + 20"\r\n// 1. 提取有效载荷package = RemoveHeader(package, n);// 2. 已经读到了一个完整的stringRequest req;req.Deserialize(package);// 3. 直接提取用户的请求数据Response resp = _func(req); // 业务逻辑// 4. 给用户返回响应——序列化string send_string;resp.Serialize(&send_string);// 5. 添加报头send_string = AddHeader(send_string);// 6. 发送send(sock, send_string.c_str(), send_string.size(), 0);}}close(sock);}~TcpServer(){}private:uint16_t _port; // 端口号Sock _listensock; // 监听套接字func_t _func;};
}
💕 CalculatorServer.cc
#include "TcpServer.hpp"
#include <memory>
using namespace tcpserver_ns;Response calculate(const Request &req)
{// 走到这里,一定保证req是有具体数据的!Response resp(0, 0);switch (req._op){case '+':resp._result = req._x + req._y;break;case '-':resp._result = req._x - req._y;break;case '*':resp._result = req._x * req._y;break;case '/':if (req._y == 0)resp._code = 1;elseresp._result = req._x / req._y;break;case '%':if (req._y == 0)resp._code = 2;elseresp._result = req._x % req._y;break;default:resp._code = 3;break;}return resp;
}int main()
{uint16_t port = 8080;unique_ptr<TcpServer> tsvr(new TcpServer(calculate, port));tsvr->InitServer();tsvr->Start();return 0;
}
💕 Util.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>using namespace std;class Util
{
public:static bool StringSplit(const string&str, const string& sep, vector<string>* result){size_t start = 0;while(start < str.size()){auto pos = str.find(sep, start);if(pos == string::npos) break;result->push_back(str.substr(start, pos - start));start = pos + sep.size();}if(start < str.size())result->push_back(str.substr(start));return true;}static int toInt(const string& s){return atoi(s.c_str());}
};
💕 效果演示
整体源码