什么是协议
在计算机网络中,协议是指在网络中进行通信和数据交换时,双方遵循的规则和约定集合。它定义了数据的传输格式、顺序、错误处理、认证和安全性等方面的规范。
协议的设计和实现是计算机网络能够正常运行的基础。它确保了不同设备和系统之间能够相互理解和协作。协议定义了数据的结构和编码方式,规定了数据传输的方式和顺序,以及双方之间进行通信的交互规则。协议的标准化和普遍采用,使得计算机网络得以互联互通,并支持各应用和服务的实现。
为了满足不同的应用场景和需求,已经存在许多成熟的应用层协议。这些协议定义了在特定应用中进行通信和数据交换的规则和格式。(http、https、DNS、ftp、smtp…)。
结构化的数据传输
协议是一种 “约定”,socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些 “结构化的数据” 怎么办呢?
例如,我们需要实现一个网络版本的简易计算器。此时,客户端给服务端发送的数据中,包含一个左操作数、操作符、右操作数。然后由服务端接收处理之后再将结果发送给客户端。
此时,就遇到了一个问题,客户端发送给服务端的不是一个简单的字符串,而是一组结构化的数据。如果客户端将这些结构化的数据单独的通过网络发送给服务端,那么服务端很难将收到的数据进行排列形成正确的数据。因此,客户端最好将这些数据一次性进行发出,此时服务端获取到的就是一个完整的数据请求,客户端常见的方案有以下两种。
约定方案一:将结构化的数据组合成为一个字符串
- 客户端发送一个形如 “5+7” 的字符串;
- 这个字符串中有两个操作数,都是整形;
- 两个数字之间会有一个字符是运算符;
- 数字和运算符之间没有空格;
- …
这种将结构化的数据组合成一个字符串的方式可以是一个简单的方法,适用于简单的数据结构和通信需求。通过约定字符串的格式和规则,可以将结构化数据转换为字符串进行传输。
约定方案二:将结构数据序列化和反序列化
- 定义结构体来表示我们需要交互的信息;
- 发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体;
- 这个过程叫做 “序列化” 和 “反序列化”。
通过序列化和反序列化可以更灵活地处理结构化数据,并支持复杂的数据类型和结构。通过定义一个结构体或对象来表示需要交互的信息,可以使用序列化算法将结构体转换为字符串形式进行传输。接收端则可以使用相同的序列化算法进行反序列化,将字符串转化为原始的数据结构。
序列化和反序列化
序列化和反序列化是将数据在不同表示形式之间进行转化的过程。
- 序列化是指将数据从内存中的对象或数据结构转化为可存储或传输的格式,例如字符串、字节流或二进制数据。
- 反序列化是将序列化之后的数据重新转化为内存中的对象或数据结构。
网络版本计算器
接下来我们使用自己制定的协议来写一个网络版本的计算器。
首先,将后续代码需要的日志文件引入,日志 log.hpp
代码如下:
#pragma once
#include <stdio.h>
#include <cstdarg>
#include <ctime>
#include <cassert>
#include <stdlib.h>
#include <cstring>
#include <cerrno>
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMessage(int level,const char *format, ...)
{
assert(level >= DEBUG);
assert(level <= FATAL);
char *name = getenv("USER");
char logInfo[1024];
va_list ap; // ap -> char*
va_start(ap, format);
vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);
FILE *out = (level == FATAL) ? stderr : stdout;
fprintf(out, "%s | %u | %s | %s\n",
log_level[level],
(unsigned int)time(nullptr),
name == nullptr ? "unknow" : name,
logInfo);
va_end(ap); // ap = NULL
}
协议定制
要实现一个网络版本的计算器,就必须确保通信双方能够遵守某种协议约定。这里我们制定一套简单的协议约定。数据分为请求数据和响应数据。因此我们可以定义请求类和响应类来实现协议约定。
// 请求类
class Request {
public:
int x_;
int y_;
char op_;
};
// 响应类
class Response {
public:
int exitCode_; // 状态字段
int result_; // 计算结果
};
定义了一个名为 Request
的请求类和一个名为 Response
的响应类。这些类具有公共的成员变量来存储请求和响应的数据。
请求类中包括两个操作数和一个操作符,响应类中包含一个计算结果以及该次计算的状态字段。状态字段用于表示该次的计算是否符合计算要求。
约定状态字段对应的含义如下:
0
- 表示计算成功;-1
- 表示除0错误;-2
- 表示模0错误;-3
- 表示非法操作符;
注意,计算结果只有在状态码为0的时候才有意义,否则计算的结果是没有意义的。
序列化和反序列化
下面代码实现了一个简单的请求类和响应类,并提供了序列化和反序列化的方法来将结构化的数据转换为字符串,并将字符串转换会结构化的数据。根据定义的宏 MY_SELF
的是否定义,可以使用自定义的序列化方法或者使用 JSON 库进行序列化。
#pragma once
#include <iostream>
#include <string>
#include <cassert>
#include <jsoncpp/json/json.h>
#include "util.hpp"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) // 坑:sizeof(CRLF)
#define OPS "+-*/%"
#define BUFFER_SIZE 1024
// #define MY_SELF 1
// encode,整个序列化之后的字符串进行添加长度
std::string encode(const std::string &in, uint32_t len)
{
std::string encodein = std::to_string(len);
encodein += CRLF;
encodein += in;
encodein += CRLF;
return encodein;
}
// decode,这个序列化之后的字符串进行提取长度
// 1.必须具有完整的长度 2.必须具有和len相符合的有效载荷
// 3.具备上述两个长度才返回有效载荷和len,否则,就是一个检测函数
std::string decode(std::string &in, uint32_t *len)
{
assert(len);
// 1.确认是否是一个包含len的有效字符串
*len = 0;
std::size_t pos = in.find(CRLF);
if (pos == std::string::npos)
return "";
// 2.提取长度
std::string Len = in.substr(0, pos);
int intLen = atoi(Len.c_str());
// 3.确认有效载荷也是符合要求的
int surplus = in.size() - 2 * CRLF_LEN - pos;
if (surplus < intLen)
return "";
// 4.确认有完整的报文结构
std::string package = in.substr(pos + CRLF_LEN, intLen);
*len = intLen;
// 5.将当前的报文完整的从in中移除掉
int removeLen = Len.size() + package.size() + 2 * CRLF_LEN;
in.erase(0, removeLen);
// 6.正常返回
return package;
}
class Request
{
public:
Request() {}
~Request() {}
// 序列化 - 结构化的数据 -> 字符串
void serialize(std::string *out)
{
#ifdef MY_SELF
std::string xStr = std::to_string(x_);
std::string yStr = std::to_string(y_);
*out = xStr;
*out += SPACE;
*out += op_;
*out += SPACE;
*out += yStr;
#else
// 1.Value对象,json基于KV,json是有两套操作方法的
Json::Value root;
root["x"] = x_;
root["y"] = y_;
root["op"] = op_;
Json::FastWriter fw;
*out = fw.write(root);
#endif
}
// 反序列化 - 字符串 -> 结构化的数据
bool deserialize(std::string &in)
{
#ifdef MY_SELF
// 100 + 200
std::size_t spaceOne = in.find(SPACE);
if (std::string::npos == spaceOne)
return false;
std::size_t spaceTwo = in.rfind(SPACE);
if (std::string::npos == spaceTwo)
return false;
std::string dataOne = in.substr(0, spaceOne);
std::string dataTwo = in.substr(spaceTwo + SPACE_LEN);
std::string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));
if (oper.size() != 1)
return false;
x_ = atoi(dataOne.c_str());
y_ = atoi(dataTwo.c_str());
op_ = oper[0];
return true;
#else
Json::Value root;
Json::Reader rd;
rd.parse(in, root);
x_ = root["x"].asInt();
y_ = root["y"].asInt();
op_ = root["op"].asInt();
return true;
#endif
}
void debug()
{
std::cout << "debug---------------------" << std::endl;
std::cout << x_ << " " << op_ << " " << y_ << std::endl;
std::cout << "debug---------------------" << std::endl;
}
public:
int x_;
int y_;
char op_;
};
class Response
{
public:
Response() {}
~Response() {}
// 序列化
void serialize(std::string *out)
{
#ifdef MY_SELF
// "exitCode_ result_"
std::string ec = std::to_string(exitCode_);
std::string res = std::to_string(result_);
*out = ec;
*out += SPACE;
*out += res;
#else
Json::Value root;
root["exitcode"] = exitCode_;
root["result"] = result_;
Json::FastWriter fw;
*out = fw.write(root);
#endif
}
// 反序列化 - 不仅仅是在网路中应用,本地也是可以直接使用的
bool deserialize(std::string &in)
{
#ifdef MY_SELF
// "0 300"
std::size_t pos = in.find(SPACE);
if (std::string::npos == pos)
return false;
std::string codeStr = in.substr(0, pos);
std::string restStr = in.substr(pos + SPACE_LEN);
exitCode_ = atoi(codeStr.c_str());
result_ = atoi(restStr.c_str());
return true;
#else
Json::Value root;
Json::Reader rd;
rd.parse(in, root);
exitCode_ = root["exitcode"].asInt();
result_ = root["result"].asInt();
return true;
#endif
}
void debug()
{
std::cout << "debug---------------------" << std::endl;
std::cout << "exitCode = " << exitCode_ << " result = " << result_ << std::endl;
std::cout << "debug---------------------" << std::endl;
}
public:
// 退出状态,0表示运算结果合法,非0表示表示运行结果是非法的,!0是几就表示是因为什么原因错了。
int exitCode_;
int result_;
};
bool makeRequest(std::string &str, Request *req)
{
// 1+1
char strcmp[BUFFER_SIZE];
snprintf(strcmp, sizeof strcmp, "%s", str.c_str());
char *left = strtok(strcmp, OPS);
if (!left)
return false;
char *right = strtok(nullptr, OPS);
if (!right)
return false;
char mid = str[strlen(left)];
req->x_ = atoi(left);
req->y_ = atoi(right);
req->op_ = mid;
return true;
}
debug
方法用于调试,打印对象的成员变量值。makeRequese
函数用于解析字符串并生成请求对象。- 此外,代码还定义了一些变量和辅助函数来处理字符串的拼接和分割。
- 若要使用第三方库
jsoncpp
来处理JSON
数据的序列化和反序列化。需要确保项目中包含了这个库,并在编译时链接到正确的库文件。
服务端代码
TCP 服务器我们使用多线程版本的,当服务器初始化完成并启动之后。当有一个客户端来请求服务器时,服务器就为其创建一个新的线程用于服务该客户端。这里,我们为客户端提供的是简单的计算器功能,服务端完成客户端给出的计算并给客户端返回结果。
class ServerTcp; // 声明一下
static Response calculator(const Request &req)
{
Response resp;
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.exitCode_ = -1; // -1,除0
else
resp.result_ = req.x_ / req.y_;
}
break;
case '%':
{
if (req.y_ == 0)
resp.exitCode_ = -2; // -2,模0
else
resp.result_ = req.x_ % req.y_;
}
break;
default:
resp.exitCode_ = -3; // -3,非法操作符
break;
}
return resp;
}
void netCalc(int sock, const std::string &clientIp, uint16_t clientPort)
{
assert(sock >= 0);
assert(!clientIp.empty());
assert(clientPort >= 1024);
// 9\r\n100 + 200\r\n 9\r\n100 + 200\r\n
std::string inbuffer;
while (true)
{
// 定义一个请求对象
Request req;
char buffer[128];
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s == 0)
{
logMessage(NOTICE, "client[%s:%d] close sock,service done.", clientIp.c_str(), clientPort);
break;
}
else if (s < 0)
{
logMessage(WARINING, "read client[%s:%d] error,errorCode: %d,errorMessage: %s", clientIp.c_str(), clientPort, errno, strerror(errno));
break;
}
// read success
buffer[s] = 0;
inbuffer += buffer;
// 1.检查inbuffer是否已经具有了一个strPackage
uint32_t packgeLen = 0;
std::string package = decode(inbuffer, &packgeLen);
if (packgeLen == 0)
continue; // 无法提取一个完整的报文,继续提取
// 2.已经获取了一个完整的package
if (req.deserialize(package))
{
// 3.处理逻辑,输入的是一个req,得到一个resp
Response resp = calculator(req); // resp是一个结构化的数据
// 4.对resp进行序列化
std::string respPackage;
resp.serialize(&respPackage);
// 5.对报文进行encode
respPackage = encode(respPackage, respPackage.size());
// 6.简单进行发送
write(sock, respPackage.c_str(), respPackage.size());
}
}
}
class ThreadData
{
public:
uint16_t clientPort_;
std::string clientIp_;
int sock_;
ServerTcp *this_;
ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts)
: clientPort_(port), clientIp_(ip), sock_(sock), this_(ts) {}
};
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1) {}
~ServerTcp() {}
public:
void init()
{
// 1. 创建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
logMessage(FATAL, "socket:%s", strerror(errno));
exit(SOCKET_ERR);
}
logMessage(DEBUG, "socket:%s,%d", strerror(errno), listenSock_);
// 2. bind
// 2.1 填充服务器信息
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,写入sock_对应的内核区域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
logMessage(FATAL, "bind:%s", strerror(errno));
exit(BIND_ERR);
}
logMessage(DEBUG, "bind:%s,%d", strerror(errno), listenSock_);
// 3. 监听socket,为何要监听呢?tcp是面向连接的!
if (listen(listenSock_, 5) < 0)
{
logMessage(FATAL, "bind:%s", strerror(errno));
exit(LISTEN_ERR);
}
logMessage(DEBUG, "listen:%s,%d", strerror(errno), listenSock_);
}
void loop()
{
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4. 获取连接,accept的返回值是一个新的socket fd
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
// 获取连接失败,继续获取
logMessage(WARINING, "accept:%s[%d]", strerror(errno), serviceSock);
continue;
}
// 4.1 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
logMessage(DEBUG, "accept:%s | %s[%d], socket fd:%d", strerror(errno), peerIp.c_str(), peerPort, serviceSock);
// 5. 提供服务,echho -> 小写 -> 大写
// v2版本 - 多线程
// 多线程不需要关闭文件描述符,因为多线程会共享文件描述符表!
ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void *)td);
}
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
netCalc(td->sock_, td->clientIp_, td->clientPort_);
delete td;
}
private:
int listenSock_;
uint16_t port_;
std::string ip_;
};
代码解释:
- 定义了一个
calculator
函数,用于根据请求对象进行计算并返回响应对象。根据请求的操作符,执行相应的计算,并将结果储存在响应对象中。 netCalc
函数用于吃力与客户端的网路通信。在循环中,读取客户端发送的数据,并解析出完整的请求报文。然后,调用calculator
函数进行计算,并将计算结果序列化为响应报文发送给客户端。- 其余的 TCP 服务器相关的代码在 TCP网络程序 中有详细的解释。
客户端代码
接下来实现一个简单的客户端程序,可以向服务端发送请求并接收响应。其主要作用是与服务器进行通信,实现请求和响应的交互。
客户端代码如下:
volatile bool quit = false;
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " prot ip" << std::endl;
std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8080\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t serverPort = atoi(argv[2]);
std::string serverIp = argv[1];
// 1. 创建socket SOCK_STREAM
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// 2. connect,向服务器发起连接请求
// 2.1 先填充需要连接的远端主机的基本信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
inet_aton(serverIp.c_str(), &server.sin_addr);
// 2.2 发送请求,connect 会自动帮我们进行bind
if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "connect: " << strerror(errno) << std::endl;
exit(CONN_ERR);
}
std::string message;
while (!quit)
{
message.clear();
std::cout << "Place Enter# ";
std::getline(std::cin, message);
if (strcasecmp(message.c_str(), "quit") == 0)
{
quit = true;
continue;
}
// message=trimStr(message);
Request req;
if (!makeRequest(message, &req))
continue;
std::string package;
req.serialize(&package);
std::cout << "debug->serialize-> " << package << std::endl;
package = encode(package, package.size());
std::cout << "debug->encode-> \n" << package << std::endl;
ssize_t s = write(sock, package.c_str(), package.size());
if (s > 0)
{
char buffer[1024];
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
buffer[s] = 0;
std::string echoPackage = buffer;
Response resp;
uint32_t len = 0;
std::string tmp = decode(echoPackage, &len);
if (len > 0)
{
echoPackage = tmp;
resp.deserialize(echoPackage);
printf("[exitcode: %d] %d\n", resp.exitCode_, resp.result_);
}
}
else if (s <= 0)
{
break;
}
}
return 0;
}
运行测试:
编写 makefile
构建程序,如下所示,需要定义 MY_SELF
时将 Method=-DMY_SELF
写上,不需要定义时就不需要写。
.PHONY:all
all:clientTcp serverTcpd
Method=-DMY_SELF
clientTcp:clientTcp.cc
g++ -o $@ $^ $(Method) -std=c++11 -ljsoncpp
serverTcpd:serverTcp.cc
g++ -o $@ $^ $(Method) -std=c++11 -lpthread -ljsoncpp
.PHONY:clean
clean:
rm -f serverTcpd clientTcp
使用我们写的序列化与反序列化代码进行测试。