协议->网络协议的简称
为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议
而协议最终都需要通过计算机语言的方式表示出来.只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流
通信双方在进行网络通信时:
为什么呢?
//结构化数据的实例
struct message
{
昵称:xxx;
头像:yyy.png;
消息:zzz?;
时间:kkk;
}
由于数据的长度是未知的,无法每次都准确的接收数据,因此不能直接传结构化的数据
比如说:我们想实现一个网络版的计算器
那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据
因此客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据
将结构化的数据组合成一个字符串,常见的方式有以下两种:
约定方案1:
1)此时客户端可以按某种方式将这些结构化的数据组合成一个字符串,然后将这个字符串发送到网络当中
2)此时服务端每次从网络当中获取到的就是这样一个字符串,然后服务端再以相同的方式对这个字符串进行解析
3)服务端就能够从这个字符串当中提取出这些结构化的数据
约定方案2:
1)客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中,客户端发送数据时先对数据进行序列化
2)服务端接收到数据后再对其进行反序列化,此时服务端就能得到客户端发送过来的结构体,进而从该结构体当中提取出对应的信息
序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程
反序列化是把字节序列恢复为对象的过程
我们可以认为:序列化是将结构化的数据->字符串 反序列化是将字符串->结构化的数据
序列化和反序列化的目的
总结:
OSI七层模型中表示层的作用
实现设备固有数据格式和网络标准数据格式的转换 其中设备固有的数据格式指的是数据在应用层上的格式,而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式
我们可以认为网络通信和业务处理处于不同的层级
也就是说如何把结构化的数据和字符串相互进行转化,这个造轮子的工作是相对麻烦的,可以使用别人提供的组件,比如jsoncpp
安装第三方库:
sudo yum install jsoncpp-devel # centos7安装jsoncpp
安装库的本质就是把别人的头文件和库下载下来:
去掉前缀+后缀 这个库的名字是:jsoncpp
使用jsoncpp需要包含库文件#include
下面仅仅是了解序列化和反序列的过程
注意:
1)json是一种kv(key-value)式的序列化方案
2)Json::Value
创建的对象可以承载Json的其它类型创建出的对象
3)直接编译会报错: 因为jsoncpp是第三方库,我们要指定链接第三方库jsoncpp
使用jsoncpp进行序列化
#include
#include
#include
typedef struct request
{
int x; //左操作数
int y; //右操作数
char op; // 操作符 "+-*/%"
} request_t;
//序列化:结构化的数据->字符串
int main()
{
request_t req = {10,20,'*'};
Json::Value root;//这个对象可以承装任何对象.
//1)把结构化的数据承装到对象中
//json是一种kv(key-value)式的序列化方案
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op;
//2)进行序列化,有两种方式(有两个write类):FastWriter, StyledWriter
//方式1:
Json::StyledWriter writer1;
std::string json_string1 = writer1.write(root);//write函数返回的是序列化的结果
//方式2:
Json::FastWriter writer2;
std::string json_string2 = writer2.write(root);//write函数返回的是序列化的结果
std::cout <<"方式1序列化的结果 "<<std::endl;
std::cout<< json_string1 << std::endl;
std::cout <<"方式2序列化的结果 "<<std::endl;
std::cout<< json_string2 << std::endl;
return 0;
}
42是什么意思呢? 42实际是操作符*
的ascii码值
使用jsoncpp进行反序列化
#include
#include
#include
typedef struct request
{
int x; //左操作数
int y; //右操作数
char op; // 操作符 "+-*/%"
} request_t;
int main()
{
//反序列化
//R代表原生字符串,把里面的内容{"datax":10,"datay":20,"operator":42}当成是最原始的内容
//使用前面加个R为的是不转义字符
std::string json_string = R"({"datax":10,"datay":20,"operator":42})";
Json::Reader reader;
Json::Value root;//万能对象
//parse:第一个参数:你要进行反序列化的字符串 第二个参数:把处理结果k-v结构放到这里
reader.parse(json_string, root);
request_t req;//结构化的数据
//key-value结构
req.x = root["datax"].asInt(); //asInt函数作用:把这个值当成整数来看
req.y = root["datay"].asInt();
req.op = (char)root["operator"].asInt();
std::cout << req.x << " " << req.op << " " << req.y << std::endl;
return 0;
}
之前进行UDP TCP通信的时候并没有使用序列化和反序列化,因为我们没有结构化的数据,结构化的数据本质就是协议在代码层面的表现
要实现一个网络版的计算器,就必须保证通信双方能够遵守某种协议约定
我们需要设计一套简单的约定:数据可以分为请求数据和响应数据,我们分别需要对请求数据和响应数据进行约定
其中:
规定状态字段对应的含义:
此时我们就完成了协议的设计,但需要注意,只有当响应结构体当中的状态字段为0时,计算结果才是有意义的,否则计算结果无意义
typedef struct request //请求格式
{
int x; //10
int y; //0
char op; // '/' "+-*/%"
} request_t; //10/0 ->除0错误
// 响应格式
typedef struct response
{
int result; // 计算结果. 能否区分是正常的计算结果.还是异常的退出结果 ->不能
int code; //计算状态:表示server运算完毕的计算状态: code为0表示success), code为-1表示div 0 ...
//所以拿到退出结果的时候,需要先检查code的值
}response_t;
这里我们需要写4个对应的函数:
对于客户端
1)当我们在客户端填好request_t
结构体的内容之后,可以进行序列化之后, 再把这个序列化的内容作为请求发送给服务端
2)读取返回的结果的时候,读取到的是服务端发送的序列化的响应,此时我们需要先进行反序列化响应,然后输出结果
对于服务端
1)当我们接收请求的时候,接收到的是客户端发送的序列化之后的请求,此时我们需要对该请求进行反序列化
2)服务端处理好该请求之后,把响应结果进行序列化,再发送给客户端
//Protocol.hpp
#pragma once
#include
#include
#include
using namespace std;
// 定制协议的过程.目前就是定制结构化数据的过程
// 请求格式
// 我们自己定义的协议.client && server 都必须遵守! 这就叫做自定义协议
typedef struct request //请求格式
{
int x; //10
int y; //0
char op; // '/' "+-*/%"
} request_t; //10/0 ->除0错误
// 响应格式
typedef struct response
{
int result; // 计算结果. 能否区分是正常的计算结果.还是异常的退出结果 ->不能
int code; // 表示server运算完毕的计算状态: code为0表示success), code为-1表示div 0 ...
//所以拿到退出结果的时候,需要先检查code的值
}response_t;
//序列化请求
//序列化的函数:结构化的数据->字符串
//request_t -> string
std::string SerializeRequest(const request_t &req)
{
// 序列化的过程
Json::Value root; //可以承装任何对象. json是一种kv式的序列化方案
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op;
Json::FastWriter writer;
std::string json_string = writer.write(root);
return json_string;
}
//反序列化请求
//反序列化的函数:字符串->结构化的数据
// string -> request_t
void DeserializeRequest(const std::string &json_string, request_t &out) //输出型参数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();
}
//序列化响应
std::string SerializeResponse(const response_t &resp)
{
Json::Value root;
root["code"] = resp.code;
root["result"] = resp.result;
Json::FastWriter writer;
std::string res = writer.write(root);
return res;
}
//反序列化响应
void DeserializeResponse(const std::string &json_string, response_t &out)
{
//反序列化
Json::Reader reader;
Json::Value root;
reader.parse(json_string, root);
out.code = root["code"].asInt();
out.result = root["result"].asInt();
}
为了方便后序使用,我们可以对接口函数进行封装:
我们的方法都写成静态的,所以这个方法属于类而不属于单个对象,可以直接通过指定类域来访问
//对套接字的接口进行封装的文件
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class Sock
{
public:
//静态成员函数1:创建套接字
static int Socket()
{
//使用协议家族:选择IPV4网络通信:AF_INET
//套接字类型:流式套接: SOCK_STREAM
//协议类型 :默认为0
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock<0) //创建套接字失败,没必要往后执行了
{
cerr<<"socket errno"<<errno<<endl;
exit(2);
}
return sock;//返回创建好的套接字
}
//静态成员函数2:绑定套接字
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);
}
}
//静态成员函数3:设置套接字为监听状态
static void Listen(int sock) //要监听的套接字是谁
{
if (listen(sock, 5) < 0)
{
cerr << "listen error !" << endl;
exit(4);
}
}
//静态成员函数4:服务端从套接字获取链接
static int Accept(int sock) //从哪个套接字获取连接
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//从sock套接字获取,对端的信息会被保存放在peer里面
int fd = accept(sock, (struct sockaddr *)&peer, &len);
if(fd >= 0) //获取连接成功
{
return fd;//返回新的文件描述符
}
return -1;
}
//静态成员函数5:客户端连接服务器
//第二个,第三个参数表示要连接的服务器的相关属性信息
static void Connect(int sock, std::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());//字符串IP->整数IP
//连接
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
{
cout << "Connect Success!" << endl;
}
else
{
cout << "Connect Failed!" << endl;
exit(5);
}
}
};
.PHONY:all
all:CalClient CalServer
CalClient:CalClient.cc
g++ -o $@ $^ -std=c++11
CalServer:CalServer.cc
g++ -o $@ $^ -std=c++11 -lpthread //因为现在Server里面使用了线程库的函数,所以要指明链接线程库
.PHONY:clean
clean:
rm -rf CalClient CalServer
1)之后我们是这样启动程序的:./CalServer 服务端的端口号port
所以我们要引入命令行参数,如果有人使用错误,就把使用说明打印出来, 这样我们就能通过命令行参数拿到服务器的端口号,注意获取到的是字符串,需要通过atoi函数转为整数
2)调用Sock::sock()
函数,创建套接字 , 调用Sock::Bind()
函数,为服务端绑定一个端口号 ,调用Sock::Listen()
函数,将套接字设置为监听状态
3)调用accept函数,从监听套接字当中获取新连接,每当获取到一个新连接后就创建一个新线程,让这个新线程为该客户端提供计算服务
线程的例程函数如何执行:
1)先执行线程分离
2)实现业务逻辑
#include "Protocol.hpp"
#include "Sock.hpp"
using namespace std;
void Usage(std::string proc)
{
cout << "Usage: " << proc << " port" << endl;
}
//线程的例程执行函数
void *HandlerRequest(void *args)
{
int sock = *(int *)args;
delete (int *)args;
pthread_detach(pthread_self());//线程分离
//实现业务逻辑
//1.读取请求
request_t req;//请求结构体
ssize_t s = read(sock,&req,sizeof(req));//读取请求
cout << "request: " << req.x << req.op << req.y << endl;//把请求打印出来
if(s == sizeof(req)) //读取到了完整的请求
{
//2.分析请求&&计算结果
response_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; //代表除0错误,此时result是多少已经没所谓了
else
resp.result = req.x / req.y;
break;
case '%':
if (req.y == 0)
resp.code = -2; //代表模0错误
else
resp.result = req.x % req.y;
break;
default:
resp.code = -3; //代表请求方法异常 不是+-*/%
break;
}
//4.构建响应.并进行返回
write(sock, &resp, sizeof(resp));//把响应写回
cout <<"本轮服务结束~~~~~~~"<<endl;
}
//5.关闭链接
close(sock);
}
//之后我们是这样启动服务端的: ./CalServer 服务端的port
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(1);
}
//1.创建套接字
int listen_sock = Sock::Socket();
//2.将套接字绑定服务器
uint16_t port = atoi(argv[1]);//服务端的端口号
Sock::Bind(listen_sock,port);
//3.设置套接字为监听状态
Sock::Listen(listen_sock);
for(;;)
{
int new_sock = Sock::Accept(listen_sock);//从套接字中获取链接
if(new_sock>=0)
{
//创建新线程对请求做处理
cout << "get a new client..." << endl;
int *pram = new int(new_sock);//当前accpet返回的套接字信息
pthread_t tid;
//在线程的例程函数中执行线程分离,后序就不需要我们等待这个线程了
pthread_create(&tid, nullptr, HandlerRequest, pram);
}
}
return 0;
}
注意:
服务端创建新线程时,需要将调用accept获取到套接字作为参数传递给该线程,为了避免该套接字被下一次获取到的套接字覆盖,最好在堆区开辟空间存储该文件描述符的值
1)之后我们是这样启动程序的:./CalServer 服务端的端口号ip 服务端的port
所以我们要引入命令行参数,如果有人使用错误,就把使用说明打印出来, 这样我们就能通过命令行参数拿到服务器的端口号和ip,注意获取到的是字符串
2)创建套接字,进行连接
3)实现业务逻辑: 构造请求结构体,输入数据, 发送数据, 读取返回的结果,输出
#include
#include"Sock.hpp"
#include"Protocol.hpp"
using namespace std;
void Usage(string proc)
{
cout << "Usage: " << proc << " server_ip server_port" << endl;
}
//之后我们是这样运行: ./CalClient server_ip server_port
int main(int argc,char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
int sock = Sock::Socket();//创建套接字
//进行连接, Connect函数内部会帮我们转化,如:字符串IP->整数IP 主机序->网络序
Sock::Connect(sock, argv[1], atoi(argv[2]));
//业务逻辑
request_t req;
memset(&req, 0, sizeof(req));
cout << "Please Enter Data One# ";//输入第一个数据
cin >> req.x;
cout << "Please Enter Data Two# ";//输入第二个数据
cin >> req.y;
cout << "Please Enter operator# ";//输入操作符
cin >> req.op;
//发送请求
ssize_t s = write(sock, &req, sizeof(req));
//读取返回结果
response_t resp;
s = read(sock, &resp, sizeof(resp));
if (s == sizeof(resp))//读取到了完整的结果
{
//code只有0才代表成功,其他值表示遇到错误
cout << "Only code==0 is success! Now code is: " << resp.code << endl;
cout << "result: " << resp.result << std::endl;//计算结果
}
else
{
cout <<"读取失败"<<endl;
}
return 0;
}
上述代码存在的问题:
由于此时要使用jsoncpp
这个库,所以编译的时候需要指明链接这个库
.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 -rf CalClient CalServer
要更改的地方:
1)读取客户端发送的内容时:先进行反序列化请求 DeserializeRequest(str, req); //将读取到的内容反序列化
2)响应的时候:先序列化再发送回客户端
string send_string = SerializeResponse(resp); //把处理的结构序列化,发送
write(sock, send_string.c_str(),send_string.size());
#include "Protocol.hpp"
#include "Sock.hpp"
using namespace std;
//版本2:
void Usage(std::string proc)
{
cout << "Usage: " << proc << " port" << endl;
}
//线程的例程执行函数
void *HandlerRequest(void *args)
{
int sock = *(int *)args;
delete (int *)args;
pthread_detach(pthread_self());//线程分离
//实现业务逻辑
//1.读取请求
request_t req;//请求结构体
char buffer[1024];
ssize_t s = read(sock, buffer, sizeof(buffer) - 1); //读取序列化之后的请求
if(s>0)
{
buffer[s] = 0;
cout << "get a new request(序列化): " << buffer << endl;
std::string str = buffer;
DeserializeRequest(str, req); //将请求进行反序列化
cout << "request(反序列化): " << req.x << req.op << req.y << endl;//把请求打印出来
//2.分析请求&&计算结果
response_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; //代表除0错误,此时result是多少已经没所谓了
else
resp.result = req.x / req.y;
break;
case '%':
if (req.y == 0)
resp.code = -2; //代表模0错误
else
resp.result = req.x % req.y;
break;
default:
resp.code = -3; //代表请求方法异常 不是+-/*%
break;
}
//4.构建响应.并进行返回
std::string send_string = SerializeResponse(resp); //把响应进行序列化,发送给客户端
write(sock, send_string.c_str(),send_string.size());
cout << "服务结束,收到的请求是: " << send_string << endl;
}
//5.关闭链接
close(sock);
}
//之后我们是这样启动服务端的: ./CalServer 服务端的port
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(1);
}
//1.创建套接字
int listen_sock = Sock::Socket();
//2.将套接字绑定服务器
uint16_t port = atoi(argv[1]);//服务端的端口号
Sock::Bind(listen_sock,port);
//3.设置套接字为监听状态
Sock::Listen(listen_sock);
for(;;)
{
int new_sock = Sock::Accept(listen_sock);//从套接字中获取链接
if(new_sock>=0)
{
//创建新线程对请求做处理
cout << "get a new client..." << endl;
int *pram = new int(new_sock);//当前accpet返回的套接字信息
pthread_t tid;
//在线程的例程函数中执行线程分离,后序就不需要我们等待这个线程了
pthread_create(&tid, nullptr, HandlerRequest, pram);
}
}
return 0;
}
要更改的地方:
1)填好请求结构体之后,先序列化请求再发送给服务端
std::string json_string = SerializeRequest(req);
ssize_t s = write(sock, json_string.c_str(), json_string.size());//发送序列化之后的请求
2)从服务端读取响应的内容之后,先对该内容进行反序列化,然后再打印结果 DeserializeResponse(str, resp);//反序列化
#include
#include"Sock.hpp"
#include"Protocol.hpp"
using namespace std;
//版本2:
void Usage(string proc)
{
cout << "Usage: " << proc << " server_ip server_port" << endl;
}
//之后我们是这样运行: ./CalClient server_ip server_port
int main(int argc,char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
int sock = Sock::Socket();//创建套接字
//进行连接, Connect函数内部会帮我们转化,如:字符串IP->整数IP 主机序->网络序
Sock::Connect(sock, argv[1], atoi(argv[2]));
//业务逻辑
request_t req;
memset(&req, 0, sizeof(req));
cout << "Please Enter Data One# ";//输入第一个数据
cin >> req.x;
cout << "Please Enter Data Two# ";//输入第二个数据
cin >> req.y;
cout << "Please 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)
{
response_t resp;
buffer[s] = 0;//末尾置\0
std::string str = buffer;
cout <<"收到的响应是(反序列化):"<<str<<endl;
DeserializeResponse(str, resp);//先对响应进行反序列化
cout <<"收到的响应是(序列化):"<<str<<endl;
cout << "code[0:success]: " << resp.code << endl;
cout << "result: " << resp.result << std::endl;
}
return 0;
}
我们可以使用write或read函数进行发送或接收,也可以使用send或recv函数对应进行发送或接收
send函数
#include
ssize_t send(int socket, const void *buffer, size_t length, int flags);
参数说明
返回值说明
写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置
使用例子:
request_t rq;
send(sock, &rq, sizeof(rq), 0);
recv函数
#include
ssize_t recv(int socket, void *buffer, size_t length, int flags);
参数说明:
返回值说明
使用例子
//服务端读取请求
request_t rq;
ssize_t size = recv(sock, &rq, sizeof(rq), 0);
if(size>0){
//读取成功
}
else if(size == 0){
cout << "service done" << endl; //对端链接关闭
}
else{
cerr << "read error" << endl;//读取时遇到错误
}