由于socket api的接口,在读写数据的时候是以字符串的方式发送接收的,如果需要传输结构化的数据,就需要制定一个协议
结构化数据在发送到网络中之前需要完成序列化
接收方收到的是序列字节流,需要完成反序列化才能使用(如ChatInfo._name)
当我们进行网络通信的的时候,一端发送时构造的数据, 在另一端能够正确的进行解析(完整的读到一条报文), 就是可行的的. 这种约定, 就是 应用层协议
如何保证读到的消息是一个完整的请求
TCP是面向字节流的,无法直接读取,需要明确报文和报文的边界,常见的方法有.定长:固定报文长度、特殊符号:在报文前面加上一个字段、自描述。
我们调用的所有的发送函数(read),不是把数据发送到网络中,发送函数的本质是拷贝函数(将数据从应用层缓冲区拷贝到发送缓冲区)
有了前面的知识,下面实现一个服务器版的计算器. 我们需要客户端把要计算的两个数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端
在完成服务器与客户端正常通信的基础上完成一次请求与响应的流程
客户端:
“123+123"
的格式服务端:
Protocol.hpp包含了协议定制函数、请求响应序列化与反序列化函数、完整报文获取函数
#pragma once
#include
#include
#include
#include
using namespace std;
#define SEP " " // 分隔符
#define SEP_LEN strlen(SEP) // 分隔符长度,不能用sizeof
#define LINE_SEP "\r\n"
#define LINE_SEP_LINE strlen(LINE_SEP)
enum
{
OK = 0,
DIV_ZERO,
MOD_ZERO,
OP_ERROR
};
// 协议定制:给报文段加一个特殊字段:有效载荷的长度
// 报头 有效载荷
//
//"exitcode result"---->"content_len"\r\n"exitcode result"\r\n----
std::string enlength(const std::string &text)
{
// text就是"x op y"
string send_string = std::to_string(text.size()); // content_len
send_string += LINE_SEP;
send_string += text;
send_string += LINE_SEP;
return send_string;
}
// 去掉报头,提取有效载荷
//"content_len"\r\n"exitcode result"\r\n---->exitcode result
bool delength(const std::string &package, std::string *text)
{
auto pos = package.find(LINE_SEP);
if (pos == string::npos)
return false;
// 提取报头字符串
string text_len_string = package.substr(0, pos);
// 将报头信息转化成字符串
int text_len = std::stoi(text_len_string);
// 提取有效载荷
*text = package.substr(pos + LINE_SEP_LINE, text_len);
return true;
}
// 请求
class Request
{
public:
Request()
: x_(0), y_(0), op_(0)
{
}
Request(int x, int y, char op)
: x_(x), y_(y), op_(op)
{
}
// 序列化
bool serialize(std::string *out)
{
#ifdef MYSELF
// 将结构化数据转化成-->"x op y"
out->clear();
std::string x_string = std::to_string(x_);
std::string y_string = std::to_string(y_);
*out = x_string;
*out += SEP;
*out += op_;
*out += SEP;
*out += y_string;
#else
Json::Value root;//定义一个万能对象
root["first"]=x_;
root["second"]=y_;
root["oper"]=op_;
Json::FastWriter writer;
*out=writer.write(root);
#endif
return true;
}
// 反序列化
bool deserialize(const std::string &in)
{
#ifdef MYSELF
//"x op yyy";
auto left = in.find(SEP);
auto right = in.rfind(SEP);
if (left == std::string::npos || right == std::string::npos)
return false;
if (left == right)
return false;
// 截取子串
if (right - (left + SEP_LEN) != 1)
return false;
std::string x_string = in.substr(0, left); // 定位到x
std::string y_string = in.substr(right + SEP_LEN); // 定位到yyyy
if (x_string.empty())
return false;
if (y_string.empty())
return false;
x_ = std::stoi(x_string);
y_ = std::stoi(y_string);
op_ = in[left + SEP_LEN]; // 截取op
#else
Json::Value root;
Json::Reader reader;
reader.parse(in,root);//将解析出来的值放进root里面
x_=root["first"].asInt();//将val转化成整数
y_=root["second"].asInt();
op_=root["oper"].asInt();
#endif
return true;
}
public:
int x_;
int y_;
char op_;
};
// 响应
class Response
{
public:
Response()
: exitcode_(0), result_(0)
{
}
Response(int exitcode, int result)
: exitcode_(exitcode), result_(result)
{
}
// 序列化
bool serialize(std::string *out)
{
#ifdef MYSELF
// 清空字符串
out->clear();
// 将退出码和结果转换成字符串
string ec_string = std::to_string(exitcode_);
string res_string = std::to_string(result_);
// 合并字符
*out = ec_string;
*out += SEP;
*out += res_string;
#else
Json::Value root;
root["exitcode"]=exitcode_;
root["result"]=result_;
Json::FastWriter writer;
*out=writer.write(root);
#endif
return true;
}
// 反序列化
bool deserialize(const std::string &in)
{
#ifdef MYSELF
//"exitcode result"
auto mid = in.find(SEP);
if (mid == std::string::npos)
return false;
// 截取字符串
string ec_string = in.substr(0, mid);
string res_string = in.substr(mid + SEP_LEN);
if (ec_string.empty() || res_string.empty())
return false;
// 写入退出码和结果
exitcode_ = std::stoi(ec_string);
result_ = std::stoi(res_string);
#else
Json::Reader reader;
Json::Value root;
reader.parse(in,root);
exitcode_=root["exitcode"].asInt();
result_=root["result"].asInt();
#endif
return true;
}
public:
int exitcode_; // 0成功,!0错误
int result_; // 计算结果
};
// 读取一个完整的请求放入text里面
//"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n
bool recvRequest(int sock, std::string &inbuffer, string *text)
{
char buffer[1024];
while (true)
{
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
inbuffer += buffer;
// 边读边处理
auto pos = inbuffer.find(LINE_SEP);
// 如果没有读到\r\n,接着去读
if (pos == string::npos)
continue;
// 走到这已经读到了content_len,知道了有效载荷长度
string text_len_string = inbuffer.substr(0, pos); // 报头
int text_len = std::stoi(text_len_string); // 正文长度
int total_len = text_len_string.size() + 2 * LINE_SEP_LINE + text_len;
std::cout << "处理前#inbuffer: \n"
<< inbuffer << endl;
if (inbuffer.size() < total_len)
{
std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;
continue; // 没有读到一个完整的报文
}
// 至少有一个报文
*text = inbuffer.substr(0, total_len);
inbuffer.erase(0, total_len);
std::cout << "处理后#inbuffer: \n"
<< inbuffer << endl;
break;
}
else
return false;
}
return true;
}
服务端响应流程
void handlerEnter(int sock, func_t func)
{
string inuffer;//将所有信息写入到inbuffer
while(true)
{
// 1.读取:"content_len"\r\n"x op y"\r\n
// 1.1 保证读到的消息是【一个完整】的请求
std::string req_text; // 输出型参数,整个报文
if (!recvRequest(sock, inuffer,&req_text))
return;
std::cout<<"带报头的请求:\n"<<req_text<<endl;
// 1.2 去报头,只要正文
std::string req_str; // 正文部分
if (!delength(req_text, &req_str))
return;
std::cout<<"去掉报头后的正文:\n"<<req_str<<endl;
// 2.反序列化
// 2.1 得到一个结构化对象,对象中的成员已经被填充
Request req;
if (!req.deserialize(req_str))
return;
// 3.处理数据---------业务逻辑
// 3.1 得到一个结构化的响应,resp成员已被填充
Response resp;
func(req, resp); // 回调
// 4.对响应Response,序列化
// 4.1 得到一个字符串
std::string resp_str;
resp.serialize(&resp_str); // 输出型参数,将序列化结果写入resp_str
std::cout<<"计算完成,序列化响应: "<<resp_str<<endl;
// 5.然后发送响应
// 5.1添加协议报头,构建成一个完整的报文
std::string send_string = enlength(resp_str);
std::cout<<"构建带报头的响应正文: \n"<<send_string<<endl;
// 发送
send(sock, send_string.c_str(), send_string.size(), 0); // 有问题
std::cout<<"发送响应报文成功: \n"<<endl;
}
}
客户端请求流程
void run()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(clientport_);
server.sin_addr.s_addr = inet_addr(clientip_.c_str());
// 发起链接
if (connect(sockfd_, (struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "connect create error" << endl;
}
else
{
string msg;
string inbuffer;
while (true)
{
cout << "mycal>>> ";
std::getline(std::cin, msg);
Request req = ParseLine(msg); // 从键盘提取字符串
string content;
// 序列化结构数据
req.serialize(&content);
// 添加报头
string send_string = enlength(content);
// 发送数据
send(sockfd_, send_string.c_str(), send_string.size(), 0);
// 接收响应报文
string package, text;
if (!recvRequest(sockfd_,inbuffer,&package))
continue;
//去掉报头,获取正文放在text里面
if(!delength(package,&text)) continue;
//将收到的响应正文反序列化
Response resp;
resp.deserialize(text);
std::cout << "exitCode: " << resp.exitcode_ << std::endl;
std::cout << "result: " << resp.result_ << std::endl;
}
}
}
以上只是提供了几个核心的代码块,完整版代码可以去我的Gitee,代码注释详细,希望对你有所帮助