网络版本计算器(再谈“协议“)
终于考完学校的课程了,大概有1个多月没有再深入学习编程了,博主堕落了堕落哩。这次博主真的要发力嘞,大家一起学习,即使在今年大背景不好的情况下也能逆流而上,拿到好offer。
目录
再谈"协议"
json工具
json工具安装
json序列化演示
json反序列化演示
网络版本计算器
CalClient.cc
CalServer.cc
Protocol.hpp
Sock.hpp
Makefile
总结
再谈"协议"
协议是一种 "约定",socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的。如果我们要传输一些"结构化的数据" 怎么办呢?
网络中传输数据时,是以字符串的形式来发送接收,如果我们客户端发送的数据是一个结构化的数据,那就需要先将数据"序列化"成字符串,服务端接收到数据时,根据协议把序列化的数据转换成结构化的数据。
是不是感觉上面一段话还是很不理解?没关系,我们来设计一个场景来帮助大家理解,最后我们再来谈一遍这个"协议"。我们谈之前,先来学习一个工具json。
json工具
json工具安装
[cyq@VM-0-7-centos test_json]$ sudo yum install jsoncpp-devel
json序列化演示
#include<iostream>
#include<jsoncpp/json/json.h>
using namespace std;
typedef struct request
{
int x;
int y;
char op;
}request_t;
int main()
{
//序列化的过程
request_t req = {10, 20, '+'};
Json::Value root; //可以承装任何对象,json是一个kv式的序列化方案
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op;
//FastWriter
//Json::FastWriter writer;
//StyledWriter
Json::StyledWriter writer;
string json_string = writer.write(root); //kv转换成字符串
cout << json_string << endl;
return 0;
}
Json::Value root; //可以承装任何对象,json是一个kv式的序列化方案
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op; //这几步骤就相当于把req中结构化的数据放入类似于map/哈希表的root容器里面。
下面是两个不同的字符串序列化风格,我们分别来打印一下就知道风格的区别了。
//FastWriter
Json::FastWriter writer;
//StyledWriter
Json::StyledWriter writer;
总结:通过上面的两种输出结果就知道了FastWriter和StyledWriter的区别了。实际使用中,我们在写代码的时候会优先使用StyledWriter,因为它方便调试,等到项目上线后,再改为FastWriter。
string json_string = writer.write(root); //kv转换成字符串,这里write的返回值就是序列化好后的字符串。
json反序列化演示
int main()
{
//反序列化的过程
string json_string = R"({"datax":10,"datay":20,"operator":43})";
Json::Value root;
Json::Reader reader;
request_t req;
reader.parse(json_string, root);
req.x = root["datax"].asInt();
req.y = root["datay"].asInt();
req.op = (char)root["operator"].asInt();
cout << req.x << " " << req.y << " " << req.op << endl;
return 0;
}
int main()
{
//反序列化的过程
string json_string = R"({"datax":10,"datay":20,"operator":43})";//json序列化后的字符串
Json::Value root; //我们反序列化也借助了这个root容器。
Json::Reader reader;
request_t req;
reader.parse(json_string, root); //把序列化的数据读取到容器里面。
req.x = root["datax"].asInt(); //这时候我们再把容器里面的数据读入到结构体里面。
req.y = root["datay"].asInt();
req.op = (char)root["operator"].asInt();
cout << req.x << " " << req.y << " " << req.op << endl;
return 0;
}
我们来输出一下拿到的数据:
是不是感觉Josn使用还是比较简单的?我们画个图来进一步理解:
Makefile
test:test.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp
.PHONY:clean
clean:
rm -f test
注意:我们在编译的时候,使用了jsoncpp第三方库,所以我们需要加上-ljsoncpp。
网络版本计算器
例如, 我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。
约定方案一:
客户端发送一个形如"1+1"的字符串;
这个字符串中有两个操作数, 都是整形;
两个数字之间会有一个字符是运算符, 运算符只能是 + ;
数字和运算符之间没有空格;
...约定方案二:
定义结构体来表示我们需要交互的信息;
发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
这个过程叫做 "序列化" 和 "反序列化"
CalClient.cc
#include "Protocol.hpp"
#include"Sock.hpp"
#include<unistd.h>
void Usage(string proc)
{
cout << "Usage: " << proc << " server_ip server_port" << endl;
}
// ./CalClient server_ip server_port
int main(int args, char* argv[])
{
if(args != 3)
{
Usage(argv[0]);
exit(1);
}
int sock = Sock::Socket();
Sock::Connect(sock, argv[1], atoi(argv[2]));
//业务逻辑
request_t req;
memset(&req, 0, sizeof(req));
cout << "Plase Enter datax: ";
cin >> req.x;
cout << "Plase Enter datay: ";
cin >> req.y;
cout << "Plase Enter operator: ";
cin >> req.op;
string json_string = SerializeRequest(req);
ssize_t s = write(sock, json_string.c_str(), json_string.size());
//得到响应
char buffer[1024];
s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
responce_t resp;
string str = buffer; //读到的是序列化的字符串
DeserializerResponce(str, resp);//反序列化
cout << "code[0:success]: " << resp.code << endl;
cout << "request: " << resp.result << endl;
}
return 0;
}
CalServer.cc
#include <pthread.h>
#include "Protocol.hpp"
#include "Sock.hpp"
#include <unistd.h>
void Usage(string proc)
{
cout << "Usage: " << proc << " port" << endl;
}
void *HandlerRequest(void *args)
{
int sock = *(int *)args;
delete (int *)args;
pthread_detach(pthread_self());
//读取请求
char buffer[1024];
request_t req;
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
cout << "get a new request ..." << endl;
string str = buffer; //有一个隐式构造函数
DeserializerRequest(str, req); //反序列化
responce_t 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;
}
else
{
resp.result = req.x / req.y;
}
break;
case '%':
if (req.y == 0)
{
resp.code = -2;
}
else
{
resp.result = req.x % req.y;
}
break;
default:
resp.code = -3; //代表请求方法异常
break;
}
cout << "request: " << req.x << req.op << req.y << endl;
string send_string = SerializeResponce(resp);
write(sock, send_string.c_str(), send_string.size());
cout << "服务结束: " << send_string << endl;
}
close(sock);//记住要关闭链接
return nullptr;
}
// ./CalServer port
int main(int args, char *argv[])
{
if (args != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock, port);
Sock::Listen(listen_sock);
while (1)
{
int sock = Sock::Accept(listen_sock);
if (sock >= 0)
{
//任务处理
cout << "get a new client..." << endl;
int *pram = new int(sock);
pthread_t tid;
pthread_create(&tid, nullptr, HandlerRequest, (void *)pram);
}
}
return 0;
}
Protocol.hpp
#pragma once
#include<iostream>
#include<string>
#include<jsoncpp/json/json.h>
using namespace std;
//请求格式
typedef struct request
{
int x;
int y;
char op;
}request_t;
//响应格式
typedef struct responce
{
int code;
int result;
}responce_t;
//request_t -> string
string SerializeRequest(const request_t& req)
{
//序列化的过程
Json::Value root;
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op;
Json::FastWriter writer;
string json_string = writer.write(root);
return json_string;
}
void DeserializerRequest(const string& json_string, request_t& out)
{
//反序列化
Json::Reader reader;
Json::Value root;
reader.parse(json_string, root);
out.x = root["datax"].asInt();
out.y = root["datay"].asInt();
out.op = (char)root["operator"].asInt();
}
string SerializeResponce(const responce_t& resp)
{
Json::Value root;
root["code"] = resp.code;
root["result"] = resp.result;
Json::FastWriter writer;
string json_string = writer.write(root);
return json_string;
}
void DeserializerResponce(const string& json_string, responce_t& out)
{
Json::Value root;
Json::Reader reader;
reader.parse(json_string, root);
out.code = root["code"].asInt();
out.result = root["result"].asInt();
}
Sock.hpp
#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<stdlib.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
cerr << "socket err" << endl;
exit(2);
}
return sock;
}
static void Bind(int sock, 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; //服务端 ip地址
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cerr << "bind error" << endl;
exit(3);
}
}
static void Listen(int sock)
{
if(listen(sock, 5) < 0)
{
cerr << ";isten error" << endl;
exit(4);
}
}
static int Accept(int sock)
{
struct sockaddr_in peer; //输出型参数
socklen_t len = sizeof(peer);
int fd = accept(sock, (struct sockaddr*)&peer, &len);
if(fd >= 0)
{
return fd;
}
return -1;
}
static void Connect(int sock, 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()); //字符串转整型
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
{
cout << "Connect Success" << endl;
}
else
{
cout << "Connect failed" << endl;
exit(5);
}
}
};
Makefile
.PHONY:all
all:CalClient CalServer
CalClient:CalClient.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp
CalServer:CalServer.cc
g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
.PHONY:clean
clean:
rm -f CalClient CalServer
演示结果:
总结
思考:为什么我们不直接传输结构化的数据?
我们不能站在上帝视角来看待这个问题,对于主机B,它不知道主机A要发送什么样的结构化的数据,那么它就无法定义响应相应的结构体来存储传过来的数据。这就好像成为了先有鸡还是先有蛋的问题了。
如果传输字符串,我们就不必担心这个问题,对于主机B中的接收缓冲区来说,是以字节为单位来读取数据,至于读取字符串出现的问题,还是根据协议来解决,后面的博客博主再来讲解。
看到这里,给博主点个赞吧~