目录
一、相关概念
二、自定义协议
三、编写服务器
四、编写客户端
五、JSON
六、补充内容
一、相关概念
在《网络编程套接字》中,我们实现了服务器与客户端之间字符串的通信。但是更多的时候,需要传输的不仅仅是字符串,而是结构化的数据。
我们可以定义结构体来表示需要交互的信息,发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体。这个过程叫做 "序列化" 和 "反序列化"。
根据某种约定,使一端发送时构造的数据,在另一端能够正确的进行解析。这种约定就是应用层协议。
二、自定义协议
现在我们自己定义一个协议,用来实现服务器版本的计算器。
服务器与客户端在互相收发消息时,并不是直接把数据发送给对方,直接从对方处读取数据。而是经过了一系列层状结构。
在传输层,TCP协议有自己的发送缓冲区和接收缓冲区。用户层也有一个缓冲区,用户输入数据是输入到用户层的缓冲区的。
以写入为例,用户向用户层的缓冲区输入了一份数据,调用 write 函数,本质是把用户层缓冲区的数据拷贝到传输层TCP协议的发送缓冲区中。对方调用 read 函数,本质上是把TCP协议的接收缓冲区中的数据拷贝到用户层的缓冲区。
用户层调用write函数,把数据拷贝完成后就返回了。数据什么时候发送到对方,怎么发,都由TCP决定,因此TCP协议被称为传输控制协议。TCP发送数据的本质,是把自己发送缓冲区里的数据,经过网络拷贝到对方的接收缓冲区中,所以TCP通信的本质,也是拷贝。TCP协议是全双工的。
为了保证每一次读取,都能从接收缓冲区中恰好读取到一段完整的、独立的报文字符串,这需要使用一些方法,比如在字符串前面加上独立报文字符串的长度。相当于加上报头。
//Util.hpp 用于做数据间转换
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
using namespace std;
//工具类用来做一些转换
class Util
{
public:
//输入:const&
//输出:*
//输入输出:&
static bool StringSplit(const std::string& str, const std::string& sep, std::vector<std::string>* result)
{
// 10 + 20
size_t start = 0;
while(start < str.size())
{
auto pos = str.find(sep, start);
if(pos == string::npos)
break;
result->push_back(str.substr(start, pos - start)); //前闭后开区间,尾减首刚好是元素个数
//位置的重新加载
start = pos + sep.size();
}
if(start < str.size())
result->push_back(str.substr(start));
return true;
}
static int toInt(const string& s)
{
return atoi(s.c_str());
}
};
//Protocol.hpp 自定义协议
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <vector>
#include "Util.hpp"
//给网络版本计算机定制协议
namespace Protocol_ns
{
#define SEP " "
#define SEP_LEN strlen(SEP) //一定不能使用sizeof
#define HEADER_SEP "\r\n"
#define HEADER_SEP_LEN strlen("\r\n")
//"长度"\r\n"_x _op _y"\r\n
//请求/响应 = 报头\r\n有效载荷\r\n
string AddHeader(const string& str)
{
string s = to_string(str.size());
s += HEADER_SEP;
s += str;
s += HEADER_SEP;
return s;
}
//"7"\r\n"10 + 20"\r\n => "10 + 20"
string RemoveHeader(const string& str, int len)
{
auto pos = str.find(HEADER_SEP);
string package;
package = str.substr(pos + HEADER_SEP_LEN, len); //有效载荷
return package;
}
int ReadPackage(int sock, string& inbuffer, string* package)
{
//边读取,边分析
//边读取
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer - 1), 0);
if(s <= 0)
return -1;
buffer[s] = 0;
inbuffer += buffer;
//边分析, "7"\r\n"10 + 20"\r\n
auto pos = inbuffer.find(HEADER_SEP);
if(pos == string::npos)
return 0;
string lenStr = inbuffer.substr(0, pos); //获取头部字符串
int len = Util::toInt(lenStr);
int targetPackageLen = lenStr.size() + len + 2 * HEADER_SEP_LEN;
if(inbuffer.size() < targetPackageLen)
return 0;
*package = inbuffer.substr(0, targetPackageLen); //数据报
inbuffer.erase(0, targetPackageLen); //从inbuffer中直接移除了整个报文
return len;
}
//Request && Response 都要提供序列化和反序列化的功能
class Request
{
public:
Request()
{}
Request(int x, int y, char op)
:_x(x)
,_y(y)
,_op(op)
{}
//struct->string
bool Serialize(std::string* outStr)
{
*outStr = "";
std::string x_string = std::to_string(_x);
std::string y_string = std::to_string(_y);
//手动序列化
*outStr = x_string + SEP + _op + SEP + y_string;
return true;
}
//string->struct
bool Deserialize(const std::string& inStr)
{
//inStr: 10 + 20 => [0] = 10, [1] = +, [2] = 20
std::vector<std::string> result;
Util::StringSplit(inStr, SEP, &result);
if(result.size() != 3) //一定可以切割成三部分
return false;
if(result[1].size() != 1) //中间部分一定只有一个字符
return false;
_x = Util::toInt(result[0]);
_y = Util::toInt(result[2]);
_op = result[1][0];
}
~Request()
{}
public:
int _x;
int _y;
char _op;
};
class Response
{
public:
Response()
{}
Response(int result, int code)
:_result(result)
,_code(code)
{}
//struct->string
bool Serialize(std::string* outStr)
{
//_result _code
*outStr = "";
string res_string = to_string(_result);
string code_string = to_string(_code);
*outStr = res_string + SEP + code_string;
return true;
}
//string->struct
bool Deserialize(const std::string& inStr)
{
std::vector<std::string> result;
Util::StringSplit(inStr, SEP, &result);
if(result.size() != 2)
return false;
_result = Util::toInt(result[0]);
_code = Util::toInt(result[1]);
return true;
}
~Response()
{}
public:
int _result;
int _code;
};
}
三、编写服务器
下面使用我们自定义的协议实现网络计算器的服务器:
//sock.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include "error.hpp"
#include "log.hpp"
static const int gbacklog = 32;
static const int defaultfd = -1;
class Sock
{
public:
Sock()
:_sock(defaultfd)
{
}
void Socket()
{
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock < 0)
{
logMessage(FATAL, "socket error, code: %d, errstring: %s", errno, strerror(errno));
exit(SOCKET_ERR);
}
}
void Bind(const 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;
if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, code: %d, errstring: %s", errno, strerror(errno));
exit(BIND_ERR);
}
}
void Listen()
{
if(listen(_sock, gbacklog) < 0)
{
logMessage(FATAL, "listen error, code: %d, errstring: %s", errno, strerror(errno));
exit(LISTEN_ERR);
}
}
int Accept(string* clientip, uint16_t* clientport)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int sock = accept(_sock, (struct sockaddr*)&temp, &len);
if(sock < 0)
{
logMessage(WARNING, "accept error, code: %d, errstring: %s", errno, strerror(errno));
}
else
{
*clientip = inet_ntoa(temp.sin_addr);
*clientport = ntohs(temp.sin_port);
}
return sock;
}
int Connect(const string& serverip, const uint16_t& serverport)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
// int n = connect(sock, (struct sockaddr*)&server, sizeof(server));
// if(n < 0)
// {
// logMessage(FATAL, "connect error, code: %d, errstring: %s", errno, strerror(errno));
// exit(CONNECT_ERR);
// }
return connect(_sock, (struct sockaddr*)&server, sizeof(server));
}
int Fd()
{
return _sock;
}
~Sock()
{
if(_sock != defaultfd)
close(_sock);
}
private:
int _sock; //只定义一个_sock,解释权归调用者所有
//server调用就是listensock
//client调用就是客户端的sock
};
//TcpServer.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <functional>
#include "Sock.hpp"
#include "Protocol.hpp"
using namespace Protocol_ns;
using func_t = std::function<Response(const Request &)>;
class TcpServer;
class ThreadData
{
public:
ThreadData(int s, string &clientip, uint16_t clientport, TcpServer *p)
: _sock(s), _clientip(clientip), _clientport(clientport), _tsvrp(p)
{
}
~ThreadData()
{
}
public:
int _sock;
string _clientip;
uint16_t _clientport;
TcpServer *_tsvrp;
};
class TcpServer
{
public:
TcpServer(func_t func, uint16_t port)
: _func(func), _port(port)
{
}
void InitServer()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
logMessage(INFO, "init server done, listensock: %d", _listensock.Fd());
}
void Start()
{
for (;;)
{
std::string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if (sock < 0)
continue;
logMessage(DEBUG, "get a new client, client info : [%s : %d]", clientip.c_str(), clientport);
pthread_t tid;
ThreadData *td = new ThreadData(sock, clientip, clientport, this);
pthread_create(&tid, nullptr, ThreadRoutine, td);
}
}
static void *ThreadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
// logMessage(DEBUG, "thread runing ");
td->_tsvrp->ServiceIO(td->_sock, td->_clientip, td->_clientport);
delete td;
return nullptr;
}
void ServiceIO(int sock, const string &clientip, const uint16_t port)
{
string inbuffer; // 用来保存每一次读取到的数据,以防这次读取的数据没有被返回,下一次再读取时,这一段数据就被覆盖了(这个数据不一定是符合协议格式的)
while (1)
{
// 1.读取报文
string package;
int n = ReadPackage(sock, inbuffer, &package);
if (n == -1)
break;
else if (n == 0)
continue;
else
{
// 2.提取有效载荷
package = RemoveHeader(package, n);
// 3.对读取到的数据做反序列化
Request req;
req.Deserialize(package);
// 4.直接提取用户的请求数据,进行处理
Response resp = _func(req); // 业务逻辑
// 5.给用户返回响应,进行序列化,形成可发送字符串
string send_string;
resp.Serialize(&send_string);
// 6.添加报头
send_string = AddHeader(send_string);
//7.发送
send(sock, send_string.c_str(), send_string.size(), 0);
}
}
close(sock);
}
~TcpServer()
{
}
private:
uint16_t _port;
Sock _listensock;
func_t _func;
};
//CalcolatorServer.cc
#include "TcpServer.hpp"
#include <memory>
Response calculate(const Request& req)
{
Response 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;
}
return resp;
}
//./calserver port
int main(int args, char* argv[])
{
if(args != 2)
{
userage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<TcpServer> tsvr(new TcpServer(calculate, port));
tsvr->InitServer();
tsvr->Start();
return 0;
}
四、编写客户端
//CalculatorClient.cc
#include <iostream>
#include <string>
#include "Sock.hpp"
#include "Protocol.hpp"
using namespace std;
using namespace Protocol_ns;
static void userage(string proc)
{
cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< endl;
}
enum
{
LEFT,
OPER,
RIGHT
};
Request Parseline(const string &line)
{
string left, right;
char op;
int status = LEFT;
int i = 0;
while(i < line.size())
{
switch (status)
{
case LEFT:
if (isdigit(line[i]))
left.push_back(line[i++]);
else
status = OPER;
break;
case OPER:
op = line[i++];
status = RIGHT;
break;
case RIGHT:
if (isdigit(line[i]))
right.push_back(line[i++]);
break;
}
}
Request req;
req._x = stoi(left);
req._y = stoi(right);
req._op = op;
return req;
}
//./tcpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
userage(argv[0]);
exit(USAGE_ERR);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
Sock sock;
sock.Socket();
int n = sock.Connect(serverip, serverport);
if (n != 0)
return 1;
string buffer;
while (1)
{
cout << "Enter>> ";
string line;
getline(cin, line);
Request req = Parseline(line);
cout << "test: " << req._x << req._op << req._y << endl;
// 1.序列化
string sendString;
req.Serialize(&sendString);
// 2.添加报头
sendString = AddHeader(sendString);
// 3.发送
send(sock.Fd(), sendString.c_str(), sendString.size(), 0);
// 4.获取响应
string package;
int n = 0;
START:
n = ReadPackage(sock.Fd(), buffer, &package);
if (n == 0)
goto START;
else if (n < 0)
break;
else
{
// 5.去报头
package = RemoveHeader(package, n);
// 6.反序列化
Response resp;
resp.Deserialize(package);
cout << "result: " << resp._result << "[code: " << resp._code << "]" << endl;
}
}
sock.Close();
return 0;
}
编译运行:
五、JSON
实际上,对于序列化与反序列化的规则,是不需要程序员自己实现的。因为已经存在了很多更加好用的,别的大佬实现好的规则了。以下是关于JSON的使用。
在Linux中安装JSON:
sudo yum install -y jsoncpp-devel
JSON的头文件会被安装在系统的 /usr/include/ 路径下。动静态库被安装在 /lib64/ 路径下。
在使用JSON时,需要包含头文件:
#include <jsoncpp/json/json.h>
JSON中的部分成员:
- Value:一种万能对象,能接收任意的kv类型
- FastWriter:是用来进行序列化的。struct->string,直接把结构体变成一行字符串。
- StyledWriter:进行风格化的序列化,使字符串更加好看。
- Reader:用来进行反序列化。
class Request
{
public:
//...
bool Serialize(std::string* outStr)
{
*outStr = "";
Json::Value root; //Value:一种万能对象,接受任意的kv类型
root["x"] = _x; //自动把所有类型转换成字符串
root["y"] = _y;
root["op"] = _op;
Json::FastWriter writer;
*outStr = writer.write(root);
return true;
}
bool Deserialize(const std::string& inStr)
{
Json::Value root;
Json::Reader reader;
reader.parse(inStr, root);
_x = root["x"].asInt(); //把数据从字符串转回指定的类型
_y = root["y"].asInt();
_op = root["op"].asInt();
return true;
}
public:
int _x;
int _y;
char _op;
};
class Response
{
public:
//...
bool Serialize(std::string* outStr)
{
*outStr = "";
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
*outStr = writer.write(root);
return true;
}
bool Deserialize(const std::string& inStr)
{
Json::Value root;
Json::Reader reader;
reader.parse(inStr, root);
_result = root["result"].asInt();
_code = root["code"].asInt();
return true;
}
public:
int _result;
int _code;
};
程序正常运行。
六、补充内容
一个程序里不一定只有一个协议。只需要在定制的报头中包含协议号,使程序能够分辨不同的协议,就可以同时使用多种协议。
在我们上面定制的报头中,是以 "长度"\r\n"_x _op _y" \r\n 的格式制定的。由于有效载荷中不包含 \r\n ,所以就算不在前面加长度,也能通过搜索字符串的方式读取到有效载荷。但是如果有效载荷中包含分隔符,就会造成错误。所以加上长度是稳妥的做法。