本次我们实现一个服务器版本的简单的计算器,通过自己在应用层定制协议来将数据传输出去。
协议代码
此处我们为了解耦,采用了两个类,分别表示客户端的请求和服务端的响应。
Request
class Request
{
public:
Request()
{
}
Request(int x, int y, char op)
: _x(x), _y(y), _op(op)
{
}
// 序列化
bool serialize(string &out)
{
out += to_string(_x);
out += SEP;
out += _op;
out += SEP;
out += to_string(_y);
return true;
}
// 反序列化 x + y
bool deserialize(const string &in)
{
string x_str, y_str;
int left = in.find(SEP);
if (left == string::npos)
return false;
int right = in.rfind(SEP);
if (right == string::npos)
return false;
x_str = in.substr(0, left);
y_str = in.substr(right + 1);
_x = stoi(x_str);
_y = stoi(y_str);
_op = in[left + SEP_LEN];
return true;
}
int _x;
int _y;
char _op;
};
Response
class Response
{
public:
Response()
: exitcode(0), result(0)
{
}
// 序列化
bool serialize(string &out)
{
out.clear();
out += to_string(exitcode);
out += SEP;
out += to_string(result);
}
// 反序列化
bool deserialize(const string &in)
{
// exitcode result
string exit_str, result_str;
int pos = in.find(SEP);
if (pos == string::npos)
return false;
exit_str = in.substr(0, pos);
result_str = in.substr(pos + SEP_LEN);
if (exit_str.empty() || result_str.empty())
return false;
exitcode = stoi(exit_str);
result = stoi(result_str);
}
int exitcode;
int result;
};
而对于两个类来说,它们最重要的就是序列化和反序列化的过程。
对于请求类,它需要将所输入的数据序列化转化成为字符串,转化的字符串就是一整个报文,便于后续添加报头;而它的反序列化就需要将报文重新转化为数据,便于后续的计算任务进行。
对于响应类,它需要将所得到的结果序列化转化为字符串;反序列化则是将报文转化为数据。
当数据序列化后就需要添加报头,而反序列化的任务则需要在去除报头后进行。
添加和去除报头
#define SEP " "
#define SEP_LEN strlen(SEP)
#define SEP_LINE "\\r\\n"
#define SEP_LINE_LEN strlen(SEP_LINE)
// 报头格式 text_len/r/ntext/r/n
// 添加报头
bool Enlength(const string &in, string &out)
{
out.clear();
out += to_string(in.size());
out += SEP_LINE;
out += in;
out += SEP_LINE;
return true;
}
// 去掉报头
// 去掉后 只剩 text
bool Delength(const string &in, string &out)
{
int pos = in.find(SEP_LINE);
if (pos == string::npos)
return false;
int text_len = stoi(in.substr(0, pos));
out = in.substr(pos + SEP_LINE_LEN, text_len);
return true;
}
此处我们的报头的格式就是 "text_len\r\ntext\r\n" 的格式。
这Enlength函数可以将报文转化为数据包,Delength可以将数据包转化为报文。
接收函数
// 接收的是一个数据 是 text_len/r/ntext/r/n的形式
bool Recv(int socket, string &inbuffer, string &pacakge)
{
char buffer[1024];
while (true)
{
int n = recv(socket, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
// 开始接收
buffer[n] = 0;
inbuffer += buffer;
int pos = inbuffer.find(SEP_LINE);
if (pos == string::npos)
continue;
int text_len = stoi(inbuffer.substr(0, pos));
string len_string = inbuffer.substr(0, pos);
int total_len = text_len + len_string.size() + SEP_LINE_LEN * 2;
if (inbuffer.size() < total_len)
continue; // 说明没有一个完整的报文
// 此时就说明至少有一个完整的报文
pacakge = inbuffer.substr(0, total_len + 1);
inbuffer.erase(total_len);
break;
}
else
{
return false;
}
}
return true;
}
此外,由于TCP是字节流的协议,我们需要自定义函数来保证收到了至少一个完整的报文。
计算函数
void CalHandler(const Request &rq, Response &rp)
{
switch (rq._op)
{
case '+':
rp.result = rq._x + rq._y;
break;
case '-':
rp.result = rq._x - rq._y;
break;
case '*':
rp.result = rq._x * rq._y;
break;
case '/':
if (rq._y == 0)
{
rp.exitcode = 1;
}
else
{
rp.result = rq._x / rq._y;
}
break;
case '%':
if (rq._y == 0)
{
rp.exitcode = 1;
}
else
{
rp.result = rq._x % rq._y;
}
break;
}
}
服务器
首先直接看看服务器的代码。
calsever.hpp
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include "protocol.hpp"
using namespace std;
#define gbacklog 5
class Sever;
class Sever
{
public:
Sever(const uint16_t &port)
: _port(port), _listensocket(-1)
{
}
void InitSever()
{
_listensocket = socket(AF_INET, SOCK_STREAM, 0); // TCP是面向字节流的协议
// bind该socket
struct sockaddr_in peer;
peer.sin_family = AF_INET;
peer.sin_port = htons(_port);
peer.sin_addr.s_addr = INADDR_ANY;
if (bind(_listensocket, (sockaddr *)&peer, sizeof(peer)) < 0)
{
cout << "bind err!" << endl;
exit(-1);
}
cout << "bind success!" << endl;
// 监听该socket
if (listen(_listensocket, gbacklog) < 0)
{
cout << "listen err!" << endl;
exit(-1);
}
cout << "listen success!" << endl;
}
void start()
{
while (true)
{
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len = sizeof(peer);
int socket = accept(_listensocket, (sockaddr *)&peer, &len);
if (socket < 0)
{
cout << "socket err" << endl;
exit(-1);
}
cout << "accept success socket : " << socket << endl;
// version 2.1 多进程版
pid_t id = fork();
if (id == 0) // 子进程内部
{
close(_listensocket); // 子进程不用监听,父进程监听即可
if (fork() > 0)
exit(-1); // 直接让子进程创建孙子进程,然后将子进程退出,让孙子进程被领养
handlerEnter(socket);
close(socket);
} // 但是父进程不用等待,否则会造成串行
}
}
void handlerEnter(int socket)
{
string inbuffer;
while (true)
{
// 读取数据
string recv_text,recv_pacakge;
while(!Recv(socket,recv_text,recv_pacakge))
{
}
//去报头
if(!Delength(recv_pacakge,recv_text))
return;
Request rq;
// 反序列化
if(!rq.deserialize(recv_text))
{
return;
}
// 计算公式
Response rp;
CalHandler(rq,rp);
// 将结果序列化
string send_text,send_pacakge;
rp.serialize(send_text);
//添加报头
Enlength(send_text,send_pacakge);
// 发送结果
send(socket,send_pacakge.c_str(),send_pacakge.size(),0);
}
}
private:
uint16_t _port;
int _listensocket;
};
calsever.cc
#include "calSever.hpp"
#include <memory>
using namespace std;
int main(int args, char *argv[])
{
if (args != 2) // 在运行时必须带有端口号
{
cout << " ./Sever port" << endl;
exit(-1);
}
uint16_t port = atoi(argv[1]);
unique_ptr<Sever> TCPSever(new Sever(port));
TCPSever->InitSever();
TCPSever->start();
return 0;
}
对于服务器的代码,并没有太大的改动,主要是服务器所需要进行的任务的代码需要了解。
首先是接收客户端发过来的数据,由于此处使用的是TCP协议进行传输,所以可能接受的数据包不是一个完整的数据包,因此需要利用循环和自定义的函数进行检测。
而该函数的逻辑大致为 接收到数据包——去掉报头——对报文进行反序列化——对获取的数据进行计算——将结果序列化——对报文添加报头——发送数据包。
整体逻辑十分简单,但前提是函数需要正确使用。
客户端
calclient.hpp
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include"protocol.hpp"
using namespace std;
class Client
{
public:
Client(const uint16_t &port, const string &ip)
: _severport(port), _severip(ip), _socket(-1)
{
}
void InitClient()
{
_socket = socket(AF_INET, SOCK_STREAM, 0);
if (_socket < 0)
{
cout << "socket failed" << endl;
}
}
void run()
{
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_severport);
peer.sin_addr.s_addr = inet_addr(_severip.c_str());
if (connect(_socket, (sockaddr *)&peer, sizeof(peer)) == -1)
{
cout << "connect failed" << endl;
exit(-1);
}
else
{
// 输入所需要计算的公式
string inbuffer;
while (true)
{
int x, y;
char op;
cout << "请输入第一个数据 :";
cin >> x;
cout << "请输入第二个数据 :";
cin >> y;
cout << "请输入计算方式 :";
cin >> op;
string send_text,send_pacakge;
Request rq(x,y,op);
// 进行序列化以及添加报头
rq.serialize(send_text);
Enlength(send_text,send_pacakge);
// 发送公式
send(_socket,send_pacakge.c_str(),send_pacakge.size(),0);
// 接收结果
string recv_text,recv_pacakge;
while(!Recv(_socket,inbuffer,recv_pacakge))
{
//若是接收失败就重试
}
//此时已经接收到结果
// 对结果进行反序列化并输出
Response rp;
//去掉报头
Delength(recv_pacakge,recv_text);
rp.deserialize(recv_text);
if(rp.exitcode != 0)
cout<<"err ! exitcode : "<<rp.exitcode<<endl;
else
{
cout<<"The result : "<<rp.result<<endl;
}
}
}
}
private:
uint16_t _severport;
string _severip;
int _socket;
};
calclient.cc
#include"calClient.hpp"
#include<memory>
using namespace std;
void Remind()
{
cout<<"./Client Severport Severip"<<endl;
}
int main(int args,char* argv[])
{
if(args!=3)
{
Remind();
exit(-1);
}
uint16_t port = atoi(argv[1]);
string ip = argv[2];
unique_ptr<Client> TCPClient(new Client(port,ip));
TCPClient->InitClient();
TCPClient->run();
return 0;
}
对于客户端而言,它的工作和服务器大差不差。
具体逻辑是 获取数据——序列化——添加报头——传输数据包——接收数据包——去掉报头——反序列化——获取结果。
接下来就来直接看看结果。
能看到我们确实成功的运行并计算出结果。
但是我们可以看到,由我们自己定义协议十分麻烦,这里是协议很小,当我们的项目很大的时候,也许协议就不止这么点了,因此我们有更简单的方法来定制协议。
更简单的方案:
- json
- protobuf
- xml
json方案的序列化和反序列化
class Request
{
public:
Request()
{
}
Request(int x, int y, char op)
: _x(x), _y(y), _op(op)
{
}
// 序列化
bool serialize(string &out)
{
#ifdef MYSELF
out += to_string(_x);
out += SEP;
out += _op;
out += SEP;
out += to_string(_y);
#else
Json::Value root;
root["first"] = _x;
root["second"] = _y;
root["operator"] = _op;
Json::FastWriter w;
out = w.write(root);
#endif
return true;
}
// 反序列化 x + y
bool deserialize(const string &in)
{
#ifdef MYSELF
string x_str, y_str;
int left = in.find(SEP);
if (left == string::npos)
return false;
int right = in.rfind(SEP);
if (right == string::npos)
return false;
x_str = in.substr(0, left);
y_str = in.substr(right + 1);
_x = stoi(x_str);
_y = stoi(y_str);
_op = in[left + SEP_LEN];
#else
Json::Value root;
Json::Reader r;
r.parse(in,root);
_x = root["first"].asInt();
_y = root["second"].asInt();
_op = root["operator"].asInt();
#endif
return true;
}
int _x;
int _y;
char _op;
};
class Response
{
public:
Response()
: exitcode(0), result(0)
{
}
// 序列化
bool serialize(string &out)
{
#ifdef MYSELF
out.clear();
out+=to_string(exitcode);
out+=SEP;
out+=to_string(result);
#else
Json::Value root;
root["exitcode"] = exitcode;
root["result"] = result;
Json::FastWriter w;
out = w.write(root);
#endif
return true;
}
// 反序列化
bool deserialize(const string &in)
{
#ifdef MYSELF
//exitcode result
string exit_str,result_str;
int pos = in.find(SEP);
if(pos == string::npos)
return false;
exit_str = in.substr(0,pos);
result_str = in.substr(pos+SEP_LEN);
if(exit_str.empty()||result_str.empty())
return false;
exitcode = stoi(exit_str);
result = stoi(result_str);
#else
Json::Value root;
Json::Reader r;
r.parse(in,root);
exitcode = root["exitcode"].asInt();
result = root["result"].asInt();
#endif
return true;
}
int exitcode;
int result;
};
我们能够看到确实成功了。
不过使用json需要一点小小的准备工作。
- 下载对应的工具
可以用root账号或者sudo进行下载。
yun install -y jsoncpp-devel
-
包含头文件
- 编译添加新指令
只要是使用了json的文件,就需要添加 -ljsoncpp 指令才行。