认识协议
协议(Protocol) 是一种通信规则或标准,用于定义通信双方或多方之间如何交互和传输数据。在计算机网络和通信系统中,协议规定了通信实体之间信息交换的格式、顺序、定时以及有关同步等事宜的约定。简易来说协议就是通信双方所约定好的结构化字段。
序列化与反序列化的认识
但在网络传输中由于不同主机和不同系统对于数据存储的大小端差异,所以在传输结构化字段的时候,并不能保证每个结构化的成员数据都能够准确的对应上。
所以一般会采用将结构化的字段内容进行序列化成一个字符串,然后再通过网络发送出去,接收端再将数据反序列化接收,也就是将各个序列化的结构字段数据提取出来。
自定义协议实现网络版本计算器
在自定义协议时必须让客户端与服务器都能够看到同一份协议字段,这样才能在接收数据的时候按照规定的格式进行准确无误的接收。
对于序列化的字段格式采用空格作为分隔符。而反序列化的时候就可以通过空格分隔符将数据提取出来。
自定义协议(序列化与反序列化和报头)
对于我们实现的协议不仅仅有序列化结构体字段,其实还有包头的封装,因为tcp是有连接的,数据是流式传输的,所以传输过程并不是一发一收的形式,所以报头数据就可以分离每一次的收发,因为报头中有一个字段是存放序列化字段的长度。
#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
using namespace std;// 模拟定协议const string sepa = " ";
const string line_break = "\n";// 添加报头数据和解报头
void Encode(string &mes)
{int len = mes.size();string ret = to_string(len) + line_break + mes + line_break;mes = ret;
}bool Decode(string &package, string &ret)
{//"len\n123 wuwu\n"// 先判断收到的数据是否完整int len = package.size();int pos = package.find(line_break); // 指向第一个换行符if (pos == string::npos)return false;int pos1 = package.find(line_break, pos + line_break.size()); // 指向第二个换行符if (pos1 == string::npos)return false;// 解包后的数据ret = package.substr(pos + line_break.size(), pos1 - pos - 1);// 去掉被读走的数据package = package.substr(pos1 + line_break.size());return true;
}class Request
{friend class Cal;public:Request() {}Request(int x, int y, char op): _x(x), _y(y), _oper(op){}void info(){cout << _x << ' ' << _oper << ' ' << _y << " = ?" << endl;}void Serialize(string &out) // 序列化{out = to_string(_x) + sepa + _oper + sepa + to_string(_y);}//"x + y"void Deserialize(const string s) // 反序列化{int begin = 0;int end = s.find(sepa, begin);_x = stoi(s.substr(begin, end - begin));begin = end + sepa.size(); // 加的1其实就是' '的长度end = s.find(sepa, begin);_oper = s.substr(begin, end - begin)[0];begin = end + sepa.size();_y = stoi(s.substr(begin));}private:int _x;int _y;char _oper;
};class Response
{
public:Response(){}Response(int re, string ret_info): _result(re), _ret_info(ret_info){}void Info(){cout << "result = " << _result << " (" << _ret_info << ')' << endl;}void Serialize(string &out) // 序列化{out = to_string(_result) + sepa + _ret_info;}//"_result _ret_info"void Deserialize(const string s) // 反序列化{int begin = 0;int end = s.find(sepa, begin);_result = stoi(s.substr(begin, end - begin));begin = end + sepa.size(); // 加的1其实就是分隔符的长度_ret_info = s.substr(begin);}private:int _result; // 保存结果string _ret_info; // 结果信息
};
封装套接字
封装套接字就是实现代码分离,使得可读性更高,还有就是省的以后再写。
#pragma once
#include <iostream>
#include <cstdint>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <thread>
#include <functional>
#include <memory>
using namespace std;#define default_backlog 5// 设计模式:模版方法类
class my_socket // 抽象类
{
public:virtual void Creat_socket() = 0; // 纯虚函数,必须重写virtual void Bind(int port) = 0;virtual void Listen(int backlog) = 0;virtual my_socket *Accept(string &ip, uint16_t &port) = 0;virtual void Connect(string ip, uint16_t port) = 0;virtual int Get_sockfd() = 0;virtual void Close() = 0;virtual void Recv(string &ret, int len) = 0;public:void tcpserver_socket(uint16_t port, int backlog = default_backlog){Creat_socket();Bind(port);Listen(backlog);// 因为服务会返回的执行accept获取连接,所以选择分离}void tcpclient_socket(string ip, uint16_t port){Creat_socket();Connect(ip, port);}
};class tcp_socket : public my_socket // 继承并重写虚函数
{
public:tcp_socket(){}tcp_socket(int sockfd): _sockfd(sockfd){}virtual void Creat_socket(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){cerr << "创建套接字失败" << endl;exit(-1);}}virtual void Bind(int port){struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;int n = bind(_sockfd, (sockaddr *)&local, sizeof(local));if (n < 0){cerr << "绑定套接字失败" << endl;exit(-1);}}virtual void Listen(int backlog){int n = listen(_sockfd, backlog);if (n == -1){cerr << "监听套接字失败" << endl;exit(-1);}}virtual my_socket *Accept(string &ip, uint16_t &port){while (1){struct sockaddr_in client;socklen_t len = sizeof(client);int newsockfd = accept(_sockfd, (sockaddr *)&client, &len); // 监听套接字不关闭,可以用来接收多个客户端的连接if (newsockfd < 0){cerr << "获取连接失败" << endl;}port = ntohs(client.sin_port);char buffer[64];inet_ntop(AF_INET, &client.sin_addr, buffer, sizeof(buffer)); // 1.网络转本机 2.4字节ip转字符串ipip = buffer;if (newsockfd < 0){cerr << "接收套接字失败" << endl;}elsecout << "接收套接字成功" << endl;return new tcp_socket(newsockfd);}}virtual void Connect(string ip, uint16_t port){struct sockaddr_in server;server.sin_family = AF_INET; // socket inet(ip) 协议家族,绑定网络通信的信息server.sin_port = htons(port); // 将主机端口号转成网络// server.sin_addr.s_addr = inet_addr(ip.c_str()); // 转成网络序列的四字节ipinet_pton(AF_INET, ip.c_str(), &server.sin_addr); // 转成网络序列的四字节ipint n = connect(_sockfd, (sockaddr *)&server, sizeof(server)); // 自动bindif (n != 0){cerr << "连接失败" << endl;exit(-1);}elsecout << "连接成功" << endl;}virtual int Get_sockfd(){return _sockfd;}virtual void Close(){if (_sockfd > 0)close(_sockfd);}virtual void Recv(string &ret, int len){char stream_buffer[len];int n = recv(_sockfd, stream_buffer, len - 1, 0);if (n > 0){stream_buffer[n] = 0;ret += stream_buffer; // ret在读取之前可能还有内容残留}else{exit(0);}}private:int _sockfd;
};
计算器代码
计算器类实现的功能就是服务于服务端的,将客户端发送的请求进行计算,并且同时将计算出的结果与返回信息都存到协议中的response类中,所以服务端就可以直接进行序列化,从而将数据发送给客户端。
#pragma once
#include "protocol.h"class Cal
{
public:Cal(Request req): _x(req._x), _y(req._y), _oper(req._oper){}Response cal(){switch (_oper){case '+':_result = _x + _y;break;case '-':_result = _x - _y;break;case '*':_result = _x * _y;break;case '/':{if (_y == 0)_retinfo = "除数为0,结果无意义";else_result = _x / _y;}break;case '%':{if (_y == 0)_retinfo = "模0,结果未定义";else_result = _x % _y;}break;default:_retinfo = "结果无效,未录入该运算符";break;}return {_result, _retinfo};}string Answer(){return "result = " + to_string(_result) + " ret_info = " + _retinfo;}private:int _x;int _y;char _oper;int _result;string _retinfo = "结果无误";
};
服务端代码
服务端就是负责接收客户端的信息并进行处理,然后将处理结果发送回去,为了满足多客户端的请求,服务端会采用创建线程的方式来进行与客户端对接,而服务端的主线程就负责实现accept,接受客户端发送的连接请求。
#pragma once
#include "socket.h"
#include "calculate.h"using func_t = function<void(my_socket *)>; // 执行任务的方法class tcp_server
{
public:tcp_server(uint16_t port, func_t f): _port(port), _sv(new tcp_socket()), _func(f){_sv->tcpserver_socket(port);}void thread_run(my_socket *socket) // 线程执行区域{_func(socket);socket->Close(); // 运行完毕就直接关闭accept返回的套接字描述符}void loop(){while (1){string client_ip;uint16_t client_port;my_socket *socket = _sv->Accept(client_ip, client_port); // 接收套接字cout << "获取新连接,ip= " << client_ip << " port= " << client_port << endl;// sleep(3);// _sv->Close();//监听套接字就是用来接收多个客户端的连接// 创建线程执行任务thread t(std::bind(&tcp_server::thread_run, this, placeholders::_1), socket);t.detach(); // 线程分离// t.join();}}~tcp_server(){delete _sv;}private:my_socket *_sv;uint16_t _port;func_t _func;
};
#include "tcp_server.h"
#include "protocol.h"void deal(my_socket *socket) // 存的套接字描述符就是accept返回值
{string buffer;while (1){// 1.数据读取socket->Recv(buffer, 100); // 将每一次序列化的数据都读进buffer里string msg;string total_info;// 2.解包装(将所有独读到的数据都解包,最后完成后一起再发送出去)while (Decode(buffer, msg)) // 此时buffer会存在残留数据{// 3.反序列化buffer,Request rq;rq.Deserialize(msg);// 4.数据读取完毕可以进行处理Cal c(rq);Response rsp = c.cal(); // 计算结果存到rsp里// 5.将处理结果返回给客户端(需要进行序列化和加包)string s;rsp.Serialize(s);Encode(s);total_info += s;}send(socket->Get_sockfd(), total_info.c_str(), total_info.size(), 0); // 任务发送给服务器}
}
int main(int argc, char *argv[])
{if (argc != 2){cout << "格式错误\n正确格式:" << argv[0] << " port"<< endl;}uint16_t port = atoi(argv[1]);// tcp_server tsv(port);unique_ptr<tcp_server> tsv(new tcp_server(port, deal));tsv->loop(); // accept客户端套接字
}
客户端代码
客户端也同样采用创建线程的方式来进行发送数据与接收数据,这其中一个线程专门发送数据,一个线程专门接收数据,这其中的好处就是不会受到干扰,如果都通过一个线程来完成的话就会导致数据必须是一发一收的方式,并不满足数据流式传输。
#include "socket.h"
#include "protocol.h"string ops = "+-*/%&|^";void thread_run(my_socket *clt)
{while (1){// 1.读取服务端处理后的信息string buffer;string msg;clt->Recv(buffer, 100); // 将每一次序列化的数据都读进buffer里// 2.解包装Decode(buffer, msg);// 3.反序列化msg,Response rsp;rsp.Deserialize(msg);rsp.Info();sleep(3);}
}
int main(int argc, char *argv[])
{srand((unsigned int)time(nullptr));if (argc != 3){cout << "格式错误\n正确格式:" << argv[0] << " ip"<< " port" << endl;}string ip = argv[1];uint16_t port = atoi(argv[2]);my_socket *clt = new tcp_socket();clt->tcpclient_socket(ip, port); // 连接服务端套接字//创建线程专门负责接收信息thread reciver(thread_run,clt);reciver.detach();while (1){int x = rand() % 100;int y = rand() % 100;char oper = ops[rand() % ops.size()];Request rq(x, y, oper);rq.info(); // 向客户端打印任务// 1.进行序列化并打包 发送数据string s;rq.Serialize(s);Encode(s);send(clt->Get_sockfd(), s.c_str(), s.size(), 0); // 任务发送给服务器sleep(1);}delete clt;
}
认识JSON
JSON是一种成熟的序列化反序列化方案。需要使用的话要安装JSON库
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
using namespace std;
// using namespace Json;int main()
{// Json::Value 万能类型Json::Value root;root["a"] = 10;root["b"] = 20;root["哈哈哈"] = "嘎嘎嘎";Json::Value tmp;tmp["who"] = "cr";tmp["age"] = 20;root["id"] = tmp;// Json::FastWriter writer;//行式风格序列化Json::StyledWriter writer; // 样式风格序列化string s = writer.write(root); // 将root结构化字段进行序列化操作cout << s << endl;cout << "-------------------------------------" << endl;// 反序列化Json::Value rets;Json::Reader reader;bool ret = reader.parse(s, root); // 调用反序列化方法,将序列化的数据s反序列到root里if (ret) // 解析root{int a = root["a"].asInt();int b = root["b"].asInt();string st = root["哈哈哈"].asString();tmp = root["id"];cout << a << ' ' << b << ' ' << st << ' ' << tmp << endl;}
}
目录
认识协议
序列化与反序列化的认识
自定义协议实现网络版本计算器
自定义协议(序列化与反序列化和报头)
封装套接字
计算器代码
服务端代码
客户端代码
认识JSON