目录
一.什么是协议
二.协议与报文
三.自定义协议
1.封装套接字
2.构建请求与响应
3.序列化和反序列化
4.报头添加和去除
5.报文读取
四.服务器端程序
五.客户端程序
一.什么是协议
协议在生活中泛指:双方或多方为了完成某项任务或达成某种目的而制定的共同遵守的规定、标准或约定。
在计算机网络中:就是一种约定,约定了通信的双方,怎么发数据,怎么读数据,双方使用早就已经约定好的方式来进行数据的通信,这种早已经约定好的方式,就是一种协议。
协议主要作用是定义了在两个或多个通信实体之间交换的报文的格式和顺序,以及报文发送或接受一条报文或其他事件所采取的动作。
面对不同的场景,通信的方式自然也是不同的,在双方的面对不同的场景做出的约定自然也是不同的。所以在计算机网络中,协议的种类非常多,且是以层状的结构展现。
二.协议与报文
报文是网络中交换与传输的数据单元,包含了将要发送的完整的数据信息。因此可以看出,协议主要规定了如何进行通信和数据传输的规则,而报文则是这些规则下实际传输的数据内容,协议也决定了报文的格式和顺序等特性。
简单来说,协议是一种类型,报文就是这种类型下的对象。
下图每一个传输的都是一个报文,报文格式不同,因为所处的协议不同。
报文整体格式:
报头:包含了该报文的元数据,例如源地址、目标地址、长度等信息。
有效载荷:这是实际的数据内容,可能是一个文件、一个数据库记录,或者其他任何类型的数据。
三.自定义协议
今天我们自己定义的协议的隶属于,传输层之上的应用层协议。我们将完成对协议的请求报文,响应报文的构建。以及如何,设计添加报头和去除报头,对报文的序列化和反序列化。
今天我们实现的服务器功能是计算器的功能。
报文格式:
1.封装套接字
关于套接字上一篇已经有详细的说明,这里不多介绍。
Sock.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
#define TCP SOCK_STREAM
#define UDP SOCK_DGRAM
const static int backlog = 32;enum
{SOCK_ERR = 10,BING_ERR,LISTEN_ERR,CONNECT_ERR
};class Udp
{
public:Udp(int SOCK){_listensock = socket(AF_INET, SOCK, 0);if (_listensock == -1){Logmessage(Fatal, "socket err ,error code %d,%s", errno, strerror(errno));exit(SOCK_ERR);}}Udp(uint16_t port, int SOCK): _port(port){_listensock = socket(AF_INET, SOCK, 0);if (_listensock == -1){Logmessage(Fatal, "socket err ,error code %d,%s", errno, strerror(errno));exit(10);}}void Bind(){struct sockaddr_in host;host.sin_family = AF_INET;host.sin_port = htons(_port);host.sin_addr.s_addr = INADDR_ANY; // #define INADDR_ANY 0x00000000socklen_t hostlen = sizeof(host);int n = bind(_listensock, (struct sockaddr *)&host, hostlen);if (n == -1){Logmessage(Fatal, "bind err ,error code %d,%s", errno, strerror(errno));exit(BING_ERR);}}int FD(){return _listensock;}~Udp(){close(_listensock);}protected:int _listensock;uint16_t _port;
};class Tcp : public Udp
{
public:Tcp(uint16_t port): Udp(port, TCP){}Tcp(): Udp(TCP){}void Listen(){int n = listen(_listensock, backlog);if (n == -1){Logmessage(Fatal, "listen err ,error code %d,%s", errno, strerror(errno));exit(LISTEN_ERR);}}int Accept(string *clientip, uint16_t *clientport){struct sockaddr_in client;socklen_t clientlen;int sock = accept(_listensock, (struct sockaddr *)&client, &clientlen);if (sock < 0){Logmessage(Warning, "bind err ,error code %d,%s", errno, strerror(errno));}else{*clientip = inet_ntoa(client.sin_addr);*clientport = ntohs(client.sin_port);}return sock;}void Connect(string ip, 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());socklen_t hostlen = sizeof(server);int n = connect(_listensock, (struct sockaddr *)&server, hostlen);if (n == -1){Logmessage(Fatal, "Connect err ,error code %d,%s", errno, strerror(errno));exit(CONNECT_ERR);}}~Tcp(){}
};
2.构建请求与响应
请求:有待计算的数据,和运算符,以及对应将请求序列化,反序列化的函数。
class Request
{
public:Request(){}Request(int x, int y, char op): _x(x), _y(y), _op(op){}// 序列化std::string serialize(){}// 反序列化"123+321"void deserialize(const std::string &str){}public:int _x;int _y;char _op;
};
响应:有请求的计算结果,和退出码(标识计算结果的正确性),以及对应的序列化,和反序列化函数。
class Responce
{
public:Responce(int result, int code): _result(result), _code(code){}Responce(){}// 序列化std::string serialize(){}// 反序列化void deserialize(const std::string &str){}public:int _result;int _code;
};
3.序列化和反序列化
序列化:将某种结构化的数据,变为方便传输的序列,可以是字符串,也可以是二进制字节流。
反序列化:将序列化的数据,变为结构化的数据。
序列化和反序列化这个工作我们可以自己做也可以使用第三放工具——json。
json:头文件<jsoncpp/json/json.h>
请求序列化和反序列化:
//序列化
std::string serialize(){
#ifdef MYSELFstring strresult = to_string(_result);string strcode = to_string(_code);return strresult + SEP + strcode;
#else// 使用json序列化Json::Value root;root["result"] = _result;root["code"] = _code;Json::StyledWriter writer;return writer.write(root);
#endif}// 反序列化void deserialize(const std::string &str){
#ifdef MYSELFstring strresult;string strcode;bool isleft = 1;for (auto e : str){if (e >= '0' && e <= '9' && isleft){strresult += e;}else if (e <= '0' || e >= '9'){isleft = 0;}else if (e >= '0' && e <= '9' && !isleft){strcode += e;}}_result = atoi(strresult.c_str());_code = atoi(strcode.c_str());
#else// 使用json反序列化Json::Value root;Json::Reader reader; // Reader: 用来进行反序列化的reader.parse(str, root);_result = root["result"].asInt();_code = root["code"].asInt();
#endif}
响应序列化和反序列化:
// 序列化std::string serialize(){
#ifdef MYSELFstring strresult = to_string(_result);string strcode = to_string(_code);return strresult + SEP + strcode;#else// 使用json序列化Json::Value root;root["result"] = _result;root["code"] = _code;Json::StyledWriter writer;return writer.write(root);#endif}// 反序列化void deserialize(const std::string &str){
#ifdef MYSELFstring strresult;string strcode;bool isleft = 1;for (auto e : str){if (e >= '0' && e <= '9' && isleft){strresult += e;}else if (e <= '0' || e >= '9'){isleft = 0;}else if (e >= '0' && e <= '9' && !isleft){strcode += e;}}_result = atoi(strresult.c_str());_code = atoi(strcode.c_str());
#else// 使用json反序列化Json::Value root;Json::Reader reader; // Reader: 用来进行反序列化的reader.parse(str, root);_result = root["result"].asInt();_code = root["code"].asInt();
#endif}
4.报头添加和去除
在对报头部分我们添加有效载荷的长度,和固定的分隔符。以便将报头和有效载荷分离。
#define SEP "\r\n"
#define SEPLEN strlen(SEP)// str:报文;len:有效载荷的长度
std::string Rehead(std::string str, int len)
{return str.substr(str.length() - 2 - len, len);
}// 报文=报头+有效载荷——————"有效载荷长度"\r\n"有效载荷"\r\n
std::string Addhead(std::string str)
{std::string len = to_string(str.length());//"7\r\n123+123\r\n"return len + SEP + str + SEP;
}
5.报文读取
判断是否读取到一个完整的报文,只有读取到一个完整的报文,才将报文输出。
int ReadFormat(int fd, std::string &inputstr, std::string *target)
{// 从流中读取—————— "7"+\n\r+"123+321"+\n\rchar buff[1024];int n = recv(fd, buff, sizeof(buff), 0);if (n < 0){return n;}string readstr = buff;// cout << readstr << endl;// 解析判断读取的字符串是否完整// 尝试读取报头int pos = readstr.find(SEP, 0);if (pos == std::string::npos) // 没有找到分割"\n\r"return 0;// 找到报头分隔符,提取报头—————得到有效载荷的长度string headstr = readstr.substr(0, pos);int len = atoi(headstr.c_str());// 计算出整个报文应该有的长度——————报头+分割符+有效载荷int formatlen = headstr.length() + len + 2 * SEPLEN;if (readstr.length() < formatlen) // 读取的报文长度小于报文应该有的长度,没有读取完整return 0;// 读取到一个完整的报文了*target = readstr.substr(0, formatlen);inputstr.erase(0, formatlen);// cout << *target << endl;return len;
}
四.服务器端程序
服务器程序采用多线程处理请求的方式执行。
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include "Sock.hpp"
#include "Protocol.hpp"using fun_t = std::function<Responce(const Request &)>;//处理函数
class server;
//线程函数参数
struct Args
{Args(server *ser, string ip, uint16_t port, int fd): _ip(ip), _port(port), _pserver(ser), _fd(fd){}int _fd;uint16_t _port;string _ip;server *_pserver;
};class server
{
public:server(fun_t func, int port): _func(func){tcp = new Tcp(port);tcp->Bind();tcp->Listen();cout << "服务器创建成功" << endl;}void start(){while (1){string clientip;uint16_t clientport;cout << "start accept" << endl;int sock = tcp->Accept(&clientip, &clientport);cout << "get a new connect" << endl;// 多线程处理请求pthread_t t;Args *args = new Args(this, clientip, clientport, sock);pthread_create(&t, nullptr, ThreadRun, args);}}~server(){delete tcp;}private:static void *ThreadRun(void *args){pthread_detach(pthread_self());Args *ts = static_cast<Args *>(args);ts->_pserver->serverIO(ts->_fd);delete ts;return nullptr;}void serverIO(int fd){}private:Tcp *tcp;fun_t _func;
};
服务器处理读取请求处理请求返回响应:
void serverIO(int fd){// 由于使用tcp面向数据流传输数据,所以我们并不能知道我们读取的是不是一个完整的报文。// 1.读取一个完整的请求报文string inputstr;string message;while (1){int len = ReadFormat(fd, inputstr, &message);if (len == 0) // 读取的不是完整报文,继续读取continue;if (len < 0) // 读取出错{break;}cout << "得到一个完整的报文:" << message << endl;// 2.去除报头——将报头和有效载荷分离message = Rehead(message, len);cout << "去除报头:" << message << endl;// 3.有效载荷反序列化Request request;request.deserialize(message);cout << "有效载荷反序列化后" << request._x << " " << request._op << " " << request._y << endl;// 4.处理业务逻辑Responce responce = _func(request);cout << "响应序列化前" << responce._result << ":" << responce._code << endl;// 5.有效载荷序列化message = responce.serialize();cout << "响应序列化后:" << message << endl;// 6.有效载荷添加报头message = Addhead(message);cout << "响应添加报头后:" << message << endl;// 7.发送响应send(fd, message.c_str(), message.length(), 0);}close(fd);}
服务器主调用逻辑:
// 请求处理函数,返回响应
Responce calculate(Request request)
{int result;int exitcode;switch (request._op){case '+':result = request._x + request._y;exitcode = 0;break;case '-':result = request._x - request._y;exitcode = 0;break;case '*':result = request._x * request._y;exitcode = 0;break;case '/':if (request._y == 0)exitcode = 1;else{result = request._x / request._y;exitcode = 0;}break;case '%':if (request._y == 0)exitcode = 2;else{result = request._x % request._y;exitcode = 0;}break;default:break;}return Responce(result, exitcode);
}int main()
{server Server(calculate, 8081);Server.start();return 0;
}
五.客户端程序
#include <iostream>
#include "Protocol.hpp"
#include "Sock.hpp"static void usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);Tcp tcp;tcp.Connect(serverip, serverport);while (1){// 构建一个请求int x;int y;char op;cout << "Input operand 1: ";cin >> x;cout << "Input operand 2: ";cin >> y;cout << "Input operand op: ";cin >> op;// 1.构建请求Request request(x, y, op);// 2.有效载荷序列化string message = request.serialize();// 3.添加报头message = Addhead(message);// 4.发送给服务器send(tcp.FD(), message.c_str(), message.length(), 0);int formatlen = 0;string input;string target;// 1.构建响应Responce respon;while (1){// 2.读取响应int formatlen = ReadFormat(tcp.FD(), input, &target);if (formatlen == 0)continue;if (formatlen < 0)break;if (formatlen > 0){// 读取到一个完整的报文cout << "读取到一个完整的报文:" << target << endl;// 3.去报头string format = Rehead(target, formatlen);cout << "报文去报头后:" << format << endl;// 4.有效载荷反序列化respon.deserialize(format);cout << "反序列化:" << respon._result << ":" << respon._result << endl;break;}}cout << "Result :" << respon._result << ",Exit code:" << respon._code << endl;}return 0;
}
效果展示:
自己序列化:
使用json序列化:
本次自定义协议主要体现在:
- 对报文格式的整体设计。
- 构建合理的请求和响应。
- 如何将报头和有效载荷分离。
- 如何读取完整的报文。
- 如何将序列化和反序列化。