Linux网络编程应用层协议的初认识
Linux网络编程——应用层协议的初认识
一、前言
在前面的文章中我们已经对UDP和TCP通信协议有了一定的了解,并且实现了不同版本的UDP和TCP网络通信代码。
我们在之前实现通信网络中用到的各种接口: socket() 、 bind() 、 listen() 等这些实际上都是 Berkeley套接字接口 ( Berkeley sockets API),它们是标准库函数,提供了对底层网络通信协议的抽象,通过这些函数,应用程序能够方便的进行网络通信,而无需直接处理复杂的网络协议细节。
然而网络发展至今天,已经出现了许许多多成熟的应用层协议, HTTP、HTTPS、DNS、FTP 等,尽管我们在之前的文章中默认使用了字符串进行通信,并没有制定应用层协议,但是在特定的应用场景下开发新的应用时, 开发者可能需要定义自己的应用层协议来满足自己的具体需求,因为这涉及到的不仅仅是简单的语言原生类型数据,更多的是自定义且结构化的数据 。
二、应用层协议概念
在之前使用的各种套接字接口发送和接收时,其实都是以字节流的形式传输的,事实上,在网络通信中数据都是以字节流的形式 (在网络通信过程中,数据被分解成一系列的字节(8位二进制数)进行发送和接收) 进行传输的,无论是使用哪种编程语言,最终都需要转换为字节流来进行发送和接收。
但是如果是要传输结构化的数据时呢(如结构体或者类)?由于这种数据通常是由多个不同类型的成员组成,而套接字接口只能以字节流的形式发送和接收数据,因此我们还需要 将结构化数据转化为字节流 ,这个过程称为 序列化 。 接收方在接收到字节流后,再将其还原为结构化数据 ,这个过程称为 反序列化 。
序列化和反序列化理解
对于C++的原生数据类型,在内存中的读取就是直接以二进制的形式读写的,并且不会受到平台规格的影响。
但是结构化的数据不同,虽然结构化的数据同样是以二进制的形式进行读写的,但是结构化的数据通常包括各种类型的数据,不能以一种固定的格式读写,而且结构化数据的读写会收到平台规格的影响。如
- 字节序(Endianess) :不同的机器可能使用不同的字节序(大端或小端),这会影响多字节数值(如整数、浮点数)的解释。
- 对齐和填充 :编译器可能会在结构体内插入填充字节以满足特定的对齐要求,这意味着同样的结构体在不同平台上的内存布局可能不同。
- 指针值 :结构体中如果包含指针,则指针的实际值通常只在本地有意义,在另一台机器上没有意义。
- 复杂的嵌套结构 :对于包含其他复杂类型(如其他结构体、数组等)的结构体,手动处理所有这些细节会变得非常复杂。
那么该怎么处理上述的问题呢? 即序列化和反序列化 。
比如, 将 msg1 = {“July.cc”, “xxxxxxx”, “Hello world”} 转换成 “July.cc\1xxxxxxx\1Hello world” 字符串, 即 将数据以' \1’ 分割, 然后从平台1 发送到 平台2. 平台2 收到 “July.cc\1xxxxxxx\1Hello world” 字符串之后, 再以 ’\1’ 将其还原成原本的数据.
即, 平台1和平台2约定好 发送的数据分三个区域, 以 ‘\1’ 分割.
在上面的过程中
- 步骤1,将结构化的数据按照协议转换为可以直接传输的字符串或者二进制码流等形式的操作,被称为 序列化 。
- 步骤2,将接收到的经过序列化的数据,按照协议还原成原本的结构化的数据的操作被称为 反序列化 。
- 传输数据的结构都是约定好的,比如分几个区域,用什么分割,每个区域代表什么含义等。
接下来又有一个问题 接收方是如何知道接收到的字符串的长度的呢 ?
如果无法知道字符串的长度,该如何将字符串还原成原本的结构化数据呢?这是不行的,因为接收方不是一条一条地接收发送过来的数据的,很可能是发送了很多条,然后一次性接收。所以说一次接受了很长的数据,这数据中有很多的结构化的数据,如果不知道每条数据的长度,又怎么转化成原本的样子呢?
所以就 需要发送方在序列化之后的字符串数据前面声明一下此次传输的字符串长度 。如在序列化的字符串数据前面用四个字节大小的空间存储字符串的长度,然后在接收方接收到数据之后,先读取一下前四个字节的数据就知道本次的字符串长度了。
像这样的,在序列化之后的实际有效內容之前添加的内容相关属性字段的行为,叫 encode编码 ,但是encode操作并不只是添加一些属性字段,还有其他的比如加密行为等。
反过来,将encode过的数据还原为实际的有效内容的动作,叫做 decode解码 。
在此例中,添加了有效字符串的长度字段就可以称为 报头 ,有效字符串就可以被称为 有效载荷 。
三、网络整型计算器
简单了解了上面的应用层协议相关内容之后,我们开始尝试定制一个协议,来实现一个网络整型计算器服务。
我们在之前的TCP服务器与客户端的网络通信的基础上实现。接下来考虑如何实现这个网络计算器。
1、一些头文件和定义一些宏
// 一些头文件以及宏 util.hpp
#pragma once
#include <iostream>
#include <string>
#include <map>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "logMessage.hpp"
#define SOCKET_ERR 1//套接字错误
#define BIND_ERR 2//绑定错误
#define LISTEN_ERR 3//监听错误
#define USE_ERR 4//输入错误
#define CONNECT_ERR 5//连接错误
#define FORK_ERR 6//子进程创建错误
#define WAIT_ERR 7//
#define BUFFER_SIZE 1024//缓冲区大小
2、守护进程
我们知道守护进程实际上是一个运行在后台的,提供某种服务或者执行某种任务的特殊进程,通常脱离于终端且与用户没有交互,常用于执行周期性任务或者持续性任务。
而我们现在所实现的是一个网络通信服务, 需要长时间监听网络端口等待客户端的连接,所以我们还需要一个守护进程如下
// 守护进程接口 daemonize.hpp
//这段代码定义了一个 daemonize 函数,
//用于将当前进程转变为守护进程(Daemon)。
//守护进程是一种在后台运行的特殊进程,通常用于执行系统服务。
#include <iostream>
#include <cstdio>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void daemonize() {
int fd = 0;
// 1. 忽略SIGPIPE
//当进程尝试写入一个已经关闭或不可写的管道或套接字时,操作系统会发送 SIGPIPE 信号,
//默认情况下会导致进程终止。通过忽略这个信号,可以让程序在遇到这种情况时继续运行而不是崩溃。
signal(SIGPIPE, SIG_IGN);
// 2. 改变工作路径
//chdir(const char *__path);
// 3. 不要成为进程组组长
//确保子进程不是进程组组长,以便后续调用 setsid() 成功
//fork() 创建一个新的子进程。父进程会得到子进程的 PID 并立即退出(exit(0))
//这样就保证了只有子进程继续运行,并且它不会是进程组组长。
if (fork() > 0) {
exit(0);
}
// 4. 创建独立会话
//创建一个新的会话,使该进程成为新会话的会话首进程、新进程组的组长,并且脱离控制终端。
//setsid() 调用后,进程不再与任何终端相关联,成为一个独立的会话和进程组的领导者。
setsid();
// 重定向文件描述符0 1 2
//将标准输入(0)、标准输出(1)和标准错误(2)重定向到 /dev/null,以防止它们占用不必要的资源。
//最后检查 fd 是否大于 STDERR_FILENO(即 2),如果是,则关闭多余的文件描述符 fd,因为它已经被复制到标准输入、输出和错误中了。
if ((fd = open("/dev/null", O_RDWR)) != -1) { // 执行成功fd大概率为3
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
// dup2三个标准流之后, fd就没有用了
//原本打开 /dev/null 得到的文件描述符 fd 已经没有用了,因为它已经被复制到了标准输入、输出和错误的文件描述符上。(上面的重定向)
if (fd > STDERR_FILENO) {
close(fd);
}
}
}
3、日志接口
由于守护进程的存在,该进程脱离了终端会话,与标准输入、标准输出、标准错误就没有关系了。所以我们日志文件输出不到终端上,所以我们只能设计一个日志类 class log,用于将以后的标准输出、标准错误都重定向到日志文件中。
// 日志接口 logMessage.hpp
#pragma once
#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
#define LOGFILEPATH "serverLog.log"//日志文件路径
const char* log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
class log {
public:
log()
: _logFd(-1) {}//初始化为-1,表示尚未打开任何文件
void enable() {
umask(0);//创建屏蔽字,确保后续创建的文件权限不受当前进程的umask影响
_logFd = open(LOGFILEPATH, O_WRONLY | O_CREAT | O_APPEND, 0666);//创建日志文件
assert(_logFd != -1);
//将_logFd重定向到标准输出STDOUT_FILENO(宏,用于表示标准输出的文件描述符,1)
//意味着以后所有对标准输出的操作,实际上都会写入_logFd所指向的文件中
dup2(_logFd, STDOUT_FILENO);//将标准输出和标准错误都重定向到日志文献描述符中
dup2(_logFd, STDERR_FILENO);
}
~log() {
if (_logFd != -1) {
// 该函数确保将与 _logFd相关联的所有修改过的数据
//(包括元数据)从操作系统缓存同步到存储设备上。
//即确保了所有缓存的日志数据都被写到了日志文件中。避免丢失。
fsync(_logFd);
close(_logFd);
}
}
private:
int _logFd;//日志文件描述符
};
void logMessage(int level, const char* format, ...) {
assert(level >= DEBUG);
assert(level <= FATAL);
char* name = getenv("USER");
char logInfo[1024];//
va_list ap;
va_start(ap, format);
vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);//安全格式化字符串,防止缓冲区溢出
va_end(ap); // ap = NULL
FILE* out = (level == FATAL) ? stderr : stdout;
time_t tm = time(nullptr);
struct tm* localTm = localtime(&tm);
char* localTmStr = asctime(localTm);
char* nC = strstr(localTmStr, "\n");
if (nC) {
*nC = '\0';
}
fprintf(out, "%s | %s | %s | %s\n",
log_level[level],
localTmStr,
name == nullptr ? "unknow" : name,
logInfo);
//在进行I/O操作时,操作系统和C库通常会使用缓冲机制来提高性能。
//这意味着当你执行像 fprintf() 或 fwrite() 这样的写操作时,数据首先被写入到一个内存中的缓冲区而不是直接写入到目标文件或设备。
//只有当缓冲区满了、显式调用了刷新操作(如 fflush())、程序正常结束或者进行了某些特定的系统调用时,
//缓冲区的内容才会被实际写入到磁盘或其他输出设备。
//将c语言中的缓冲区內容完全写到文件中
// 首先刷新c库的缓冲区到系统缓冲区
fflush(out);
fsync(fileno(out));
//fileno() 返回的是内核分配给这些文件的唯一描述符。
//使用 fileno(out) 的目的是为了获取当前输出流 (out) 的文件描述符
// 将系统缓冲区的内容 刷入文件
}
4、封装锁
封装锁便于更好地使用,在锁对象使用结束后会自动销毁。
//锁的封装
#pragma once
#include<iostream>
#include<pthread.h>
class Mutex{
public:
Mutex()
{
pthread_mutex_init(&_lock,nullptr);
}
void lock(){
pthread_mutex_lock(&_lock);
}
void unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard{
public:
LockGuard(Mutex* mutex)
:_mutex(mutex){
_mutex->lock();
}
~LockGuard()
{
_mutex->unlock();
}
private:
Mutex* _mutex;
};
5、线程池
线程池的实现我们采用单例模式。理由如下:
- 资源共享 :线程池的主要目的是管理和重用一组固定的工作线程来执行多个任务,从而减少线程创建和销毁的开销。如果允许存在多个线程池实例,每个实例都会维护自己的工作线程集合,这将导致系统资源(如内存、CPU等)的浪费,并可能降低整体性能。
- 任务统一调度 :通过使用单一的线程池实例,所有提交的任务都可以被集中管理和调度。这样可以更有效地控制任务执行的顺序、优先级以及并发度,确保系统的稳定性和响应性。
- 简化管理和监控 :拥有一个线程池实例意味着更容易对其进行管理和监控。例如,可以方便地调整线程数量、查看当前正在处理的任务数、队列中的待处理任务数等。如果存在多个线程池,则需要对每个线程池分别进行这样的管理,增加了复杂性。
- 避免竞争和冲突 :如果有多个线程池实例同时运行,可能会出现对相同资源的竞争或冲突情况。例如,两个线程池尝试同时访问同一个外部资源时,如果没有适当的协调机制,可能会导致数据不一致或其他并发问题。而单一实例的设计可以更好地控制这些交互。
- 全局可访问性 :在某些应用场景下,希望整个应用程序的所有部分都能方便地访问到线程池服务。采用单例模式可以让任何地方无需传递线程池对象即可调用其方法,提高了代码的简洁性和可维护性。
- 一致性保障 :通过限制只能有一个线程池实例,可以保证在整个应用生命周期内使用的都是同一组配置参数(比如最大线程数、任务队列大小等),从而保持行为的一致性。
//线程池
#pragma once
#include<cstddef>
#include<iostream>
#include<ostream>
#include<queue>
#include<cassert>
#include<pthread.h>
#include<unistd.h>
#include"lock.hpp"
using std::queue;
using std::cout;
using std::endl;
#define THREADNUM 5//线程数
template<class T>
class threadPool{
public:
//双重检查锁定(Double-Check Locking)模拟实现线程池的单例模式,确保只有一个threadPool实例被创建。
static threadPool<T>* getInstance()//获取单例实例的静态方法
{
static Mutex mutex;//静态局部变量,确保线程安全初始化,缘于C++中静态局部变量的特性,保证只有其在首次访问时才会被初始化
//即如果多个线程同时到达静态局部变量的初始化语句,只会有一个线程执行初始化,其他线程会等待初始化完成。
//意味着锁对象不会在程序启动时就被创建,而是直到第一个线程尝试获取单例实例时才会创建,减少了资源消耗
//通过使用静态局部变量,无需手动管理锁对象的生命周期或担心它的初始化顺序问题。这使得代码更加简洁且易于维护。
//如果没有使用静态局部变量,而是在每次检查 _instance 是否为 nullptr 之前都创建一个新的 Mutex 对象,
//那么每次都会产生一个新的锁对象,这不仅浪费资源,还可能导致不必要的性能开销。
//使用静态局部变量可以确保在整个程序运行期间只有一个 Mutex 实例用于保护单例的初始化过程。
if(_instance==nullptr)//第一次检查,如果实例存在则直接返回
{
LockGuard lockG(&mutex);//上锁,确保只有一个线程可以进入
if(_instance==nullptr)//在被锁保护的情况下进行第二次检查
{
_instance=new threadPool<T>();//创建单例实例
}
}
return _instance;
}
static void* threadRountine(void* args)//回调函数,每个线程都要执行的函数,只能设为静态的
{ //先让自己进入分离状态,然后进入一个无限循环,在其中等待任务队列中有任务可以处理
pthread_detach(pthread_self());//分离后自动回收
threadPool<T>* tP=static_cast<threadPool<T>*>(args);
while(true)
{
tP->lockQueue();
while(!tP->haveTask())//判断任务队列中是否留有可执行任务
{
tP->waitForTask();//如果有没有就条件等待
}
T task=tP->popTask();
tP->unlockQueue();
task.run();//执行任务
}
}
void start()//启动线程池,创建指定数量的工作线程,并将它们设置为工作状态
{
try{//利用try-catch块捕捉抛出的异常
if(_isStart)
throw"Error: thread pool already exists";
}
catch(const char* e)
{
cout<<e<<endl;
return;
}
for(int i=0;i<_threadNum;i++)//创建线程
{
pthread_t temp;
pthread_create(&temp,nullptr,threadRountine,this);
}
_isStart=true;
}
void pushTask(const T& in)//将新任务添加到任务队列中,并通知一个等待的任务处理器。
{
lockQueue();//加锁处理保证对于共享资源的访问时安全的。
_taskQueue.push(in);//添加任务
choiceThreadForHandler();//唤醒线程去处理任务
unlockQueue();//解锁
}
int getThreadNum()
{
return _threadNum;
}
~threadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
threadPool(const threadPool<T>&)=delete;//禁止拷贝构造和赋值操作
threadPool<T>& operator=(const threadPool<T>&)=delete;
private:
threadPool(size_t threadNum=THREADNUM)//构造函数私有,防止外部实例化。
:_threadNum(threadNum) //初始化线程池的基本属性,线程数量、是否已启动、互斥锁和条件变量
,_isStart(false){
assert(_threadNum>0);
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_cond,nullptr);
}
void lockQueue()
{
pthread_mutex_lock(&_mutex);
}
void unlockQueue()
{
pthread_mutex_unlock(&_mutex);
}
bool haveTask()//检查任务队列中是否有待处理任务
{
return !_taskQueue.empty();
}
void waitForTask()//条件变量等待
{
pthread_cond_wait(&_cond,&_mutex);
}
T popTask()//移除处理完成的任务
{
T task=_taskQueue.front();
_taskQueue.pop();
return task;
}
void choiceThreadForHandler()//当有新的任务被添加到任务队列时,通过该函数去通知一个闲的线程去处理新任务。
{
pthread_cond_signal(&_cond);//唤醒线程
}
private:
size_t _threadNum;//线程数
bool _isStart;//线程池是否已启动
queue<T> _taskQueue;//任务队列,属于共享资源
pthread_mutex_t _mutex;//锁
pthread_cond_t _cond;//条件变量
static threadPool<T>* _instance;//单例模式中的唯一一个静态变量,用来保存类的唯一实例的指针。
};
template<class T>
threadPool<T>* threadPool<T>::_instance=nullptr;//该指针必须在类外面初始化为nullptr;
6、任务封装
在该头文件中对客户端发送的任务模块进行封装,任务中包含了发送端的各种信息IP、端口号还有socket套接字文件描述符。
为什么任务执行不直接在 Task 类中实现?
- 解耦任务和具体逻辑 :任务的具体逻辑可能与任务的管理逻辑无关。通过将任务逻辑委托给回调函数,可以将任务的管理(如日志记录、线程调度)与任务的执行逻辑分离,提高代码的模块化和可维护性。
- 这种设计符合 单一职责原则 :Task 类只负责封装任务和执行任务的框架,而具体逻辑由回调函数负责。
- 灵活性和扩展性 :通过回调函数,您可以在运行时动态指定任务的具体逻辑,而不需要修改 Task 类本身。例如,同一个 Task 对象可以执行不同的逻辑,只需传入不同的回调函数。
- 通用性 :这种设计使得 Task 类更加通用,可以适用于各种不同的任务场景,而不局限于特定的逻辑。
//任务封装 task.hpp
#pragma once
#include<iostream>
#include<string>
#include<functional>
#include<pthread.h>
#include<unistd.h>
#include"logMessage.hpp"
using std::string;
class Task{
public:
//使用了C++中的类型声明别名,为复杂的类型提供了一个简易的名字
//定义了一个名为callback_t的类型别名,代表的是一个特定类型的std::function对象
//std::function 是一个通用的多态函数封装器,可以指向任何可调用的目标。
//表示callback_t 可以用来表示任何返回类型为 void 并接受三个参数的可调用对象。
using callback_t=std::function<void(int,string,uint16_t)>;
Task()//默认构造函数
:_sock(-1)
,_port(-1){}
Task(int sock,string ip,uint16_t port,callback_t func)//带参构造函数,第四个参数就是任务处理的回调函数
:_sock(sock)
,_ip(ip)
,_port(port)
,_func(func){}
void operator()(){//重载operator(),使得Task对象可以直接作为函数调用
logMessage(DEBUG,"Thread[%p] has done %s:%d request ---start",pthread_self(),_ip.c_str(),_port);
_func(_sock,_ip,_port);//在执行回调函数前后分别记录日志信息,显示当前线程正在处理的任务及状态
logMessage(DEBUG,"Thread[%p] has done %s:%d request ---end",pthread_self(),_ip.c_str(),_port);
}
void run(){
(*this)();//*this代表获取 调用run()的Task对象所以(*this)()代表调用当前对象的operator()方法。
}
private:
//都是与任务相关的信息(套接字、发送任务端的IP和端口号)
int _sock;
string _ip;
uint16_t _port;
callback_t _func;//保存一个回调函数,该函数将在任务执行时被调用。
};
7、数据传输协议 procotol.hpp
可以定义一个类,该类成员变量包含: int _x 、 int _y 、 char _op ,分别表示两个整数和一个运算符。接着将实例化的对象发给服务端,服务端完成之后在响应给客户端。然而,我们在应用层通信还需要制定协议将结构化的数据序列化才能传输。
所以我们就需要两个类:
- 一个用于请求计算,成员变量: int _x 、 int _y 、 char _op 分别表示 两个计算数和一个运算符。
- 一个用于响应请求,成员变量: int _exitCode 、 int _result 分别表示退出码(主要用于记录是否出现除零错误或者模零错误)和计算结果。
且由于是应用层传输,所以两个类中还需要各自实现序列化和反序列化的接口。
我们应该了解在整个的传输过程中,服务端和客户端都干了那些事情,顺序是什么?我们先考虑服务端。
先指定协议,对于传输的数据,
单个完整的结构化数据转换成传输格式为 strLen\r\n_x _op _y\r\n
,
strLen :使用该字符串表示有效载荷的实际长度。
_x _op _y
:表示实际的有效载荷,单个的完整数据,这里称为 strPackage
。
先考虑服务端做的事情,
- 接收请求:将接收到的数据先要 decode解码 ,接着进行 反序列化
- 拿到原本数据之后进行 计算 ,将计算结果和退出码进行 序列化 ,再进行 code编码 后发送至客户端。
客户端:
- 首先要 产生请求 ,接着将 请求序列化 ,在进行 code编码 ,然后发送给服务端。
- 后面等服务端处理完之后还要接收服务端的响应,将响应信息接收到之后,进行 decode解码, 再 反序列化 ,拿到最终的数据。
从上面的流程我们知道需要实现的操作有 编码、解码、生产请求、序列化、反序列化。
code编码和decode解码
由于编码和解码的操作就只是针对有效载荷头部的內容,与有效载荷的实际內容关系不大(不需要对有效载荷内容进行操作),所以只需要各使用一个函数就可以
std::string encode(const std::string& inS,uint32_t len)//encode编码
{
std::string encodeIn=std::to_string(len);
encodeIn += CRLF;
encodeIn += inS;
encodeIn += CRLF;
return encodeIn;
}
std::string decode(std::string& inS,uint32_t* len)//decode解码
{
assert(len);
*len=0;
size_t pos=inS.find(CRLF);
if(pos==std::string::npos)
{
return "";
}
std::string inLen=inS.substr(0,pos);
int intlen=atoi(inLen.c_str());//atoi需要以C风格的字符串作为参数
//确认有效载荷完整,通过判断有效载荷的长度和报头给的是否一致
int surplus=inS.size()-2*CRLF_LEN-pos;
if(surplus<intlen)
{
return "";
}
//获取有效载荷
std::string package=inS.substr(pos+CRLF_LEN,intlen);
*len=intlen;
//将完整的strPackage从原始输入字符串inBuffer中删除
int removeLen=inLen.size()+2*CRLF_LEN+intlen;
inS.erase(0,removeLen);
return package;
}
序列化和反序列化
由于发送请求和请求响应的有效载荷的內容不一样,所以针对不同的内容,其序列化和反序列化的方式也不一样,所以针对接收请求和请求响应分别封装一个类,在各自的类中实现对不同内容的序列化和反序列化。
class request//接受请求
{
public:
request(){}
~request(){}
//序列化,将结构转化成字符串样的数据
//序列化的结构:_x _op _y,即空格分割
void serialize(std::string* out)
{
std::string xStr=std::to_string(get_x());
std::string yStr=std::to_string(get_y());
*out += xStr;
*out += SPACE;
*out += get_op();
*out +=SPACE;
*out += yStr;
}
bool deserialize(const std::string& in)
{
//根据约定好的格式先找空格
size_t spaceOnePos=in.find(SPACE);
if(spaceOnePos==std::string::npos)
return false;
size_t spaceTwoPos=in.rfind(SPACE);
if(spaceTwoPos==std::string::npos)
return false;
std::string dataOne=in.substr(0,spaceOnePos);
std::string dataTwo=in.substr(spaceTwoPos+SPACE_LEN,std::string::npos);
std::string oper=in.substr(spaceOnePos+SPACE_LEN,spaceTwoPos);
if(oper.size()!=1)
return false;
_x=atoi(dataOne.c_str());
_y=atoi(dataTwo.c_str());
_op=oper[0];//string到char
return true;
}
int get_x() const
{
return _x;
}
int get_y() const{
return _y;
}
char get_op() const{
return _op;
}
void set_X(int x)
{
_x=x;
}
void set_y(int y)
{
_y=y;
}
void set_op(char op)
{
_op=op;
}
void debug(){
std::cout<<_x<<""<<_op<<"<<_y"<<std::endl;
}
private:
int _x;
int _y;
char _op;
};
//请求响应
class response
{
public:
response()
:_exitCode(0)
,_result(0){}
~response(){}
void serialize(std::string* out)//序列化
{
std::string exitCode=std::to_string(_exitCode);
std::string result=std::to_string(_result);
*out = exitCode;
*out = SPACE;
*out = result;
}
bool deserialize(const std::string& in)
{
size_t posSpace=in.find(SPACE);
if(posSpace==std::string::npos)
{
return false;
}
std::string exitCodeStr=in.substr(0,posSpace);
std::string resultStr=in.substr(posSpace+SPACE_LEN,std::string::npos);
_exitCode=atoi(exitCodeStr.c_str());
_result=atoi(resultStr.c_str());
return true;
}
void set_exitCode(int exitCode){
_exitCode=exitCode;
}
void set_result(int result)
{
_result=result;
}
int get_exitCode()
{
return _exitCode;
}
int get_result()
{
return _result;
}
void debug()
{
std::cout<<_exitCode<<""<<_result<<std::endl;
}
public:
int _exitCode;
int _result;
};
产生请求
编码、解码、序列化和反序列化都完成了,还有一个生产请求的代码没有实现,对用户的输入,我们要识别输入的內容,判断输入的正确与否,还要讲內容中的运算符和数字择出来,接着将他们作为参数填充进一个接收请求(request)的实例化对象中,这样就是一个请求的产生。
bool makeRequest(const std::string& message,request* req)//定制请求
{
//首先消除指令消息中的空格
std::string tmpMsg;
std::string opStr=OPS;
for(auto e:message)
if((e<='9'&&e>='0')||(std::string::npos!=opStr.find(e)))
{
tmpMsg +=e;
}
else if(e!=' '){
return false;
}
std::cout<<tmpMsg<<std::endl;
int opPos=0;
int first_pos=tmpMsg.find_first_of(opStr);
int last_pos=tmpMsg.find_last_of(opStr);
if((tmpMsg[last_pos]!='-'&&tmpMsg[last_pos]!='+')&&!isdigit(tmpMsg[last_pos-1]))
{
return false;
}
if(tmpMsg[first_pos]=='-'||tmpMsg[first_pos]=='+')
{
if(first_pos==0)
{
opPos=tmpMsg.find_first_of(opStr,first_pos+1);
}
else{
opPos=first_pos;
}
}
else{
if(first_pos==0)
{
return false;
}
opPos=first_pos;
}
std::string left=tmpMsg.substr(0,opPos);
std::string right=tmpMsg.substr(0,opPos+1);
req->set_X(atoi(left.c_str()));
req->set_y(atoi(right.c_str()));
req->set_op(tmpMsg[opPos]);
req->debug();
return true;
}
procotol.hpp 完整代码
//定制协议,编码、解码、序列化、反序列化、制作请求
#pragma once
#include<iostream>
#include<string>
#include<cassert>
#include<cstring>
#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define OPS "+-*/%"
#define BUFFER_SIZE 1024
std::string encode(const std::string& inS,uint32_t len)//encode编码
{
std::string encodeIn=std::to_string(len);
encodeIn += CRLF;
encodeIn += inS;
encodeIn += CRLF;
return encodeIn;
}
std::string decode(std::string& inS,uint32_t* len)//decode解码
{
assert(len);
*len=0;
size_t pos=inS.find(CRLF);
if(pos==std::string::npos)
{
return "";
}
std::string inLen=inS.substr(0,pos);
int intlen=atoi(inLen.c_str());//atoi需要以C风格的字符串作为参数
//确认有效载荷完整,通过判断有效载荷的长度和报头给的是否一致
int surplus=inS.size()-2*CRLF_LEN-pos;
if(surplus<intlen)
{
return "";
}
//获取有效载荷
std::string package=inS.substr(pos+CRLF_LEN,intlen);
*len=intlen;
//将完整的strPackage从原始输入字符串inBuffer中删除
int removeLen=inLen.size()+2*CRLF_LEN+intlen;
inS.erase(0,removeLen);
return package;
}
//定制请求的协议
class request
{
public:
request(){}
~request(){}
//序列化,将结构转化成字符串样的数据
//序列化的结构:_x _op _y,即空格分割
void serialize(std::string* out)
{
std::string xStr=std::to_string(get_x());
std::string yStr=std::to_string(get_y());
*out += xStr;
*out += SPACE;
*out += get_op();
*out +=SPACE;
*out += yStr;
}
bool deserialize(const std::string& in)
{
//根据约定好的格式先找空格
size_t spaceOnePos=in.find(SPACE);
if(spaceOnePos==std::string::npos)
return false;
size_t spaceTwoPos=in.rfind(SPACE);
if(spaceTwoPos==std::string::npos)
return false;
std::string dataOne=in.substr(0,spaceOnePos);
std::string dataTwo=in.substr(spaceTwoPos+SPACE_LEN,std::string::npos);
std::string oper=in.substr(spaceOnePos+SPACE_LEN,spaceTwoPos);
if(oper.size()!=1)
return false;
_x=atoi(dataOne.c_str());
_y=atoi(dataTwo.c_str());
_op=oper[0];//string到char
return true;
}
int get_x() const
{
return _x;
}
int get_y() const{
return _y;
}
char get_op() const{
return _op;
}
void set_X(int x)
{
_x=x;
}
void set_y(int y)
{
_y=y;
}
void set_op(char op)
{
_op=op;
}
void debug(){
std::cout<<_x<<""<<_op<<"<<_y"<<std::endl;
}
private:
int _x;
int _y;
char _op;
};
//定制响应的协议
class response
{
public:
response()
:_exitCode(0)
,_result(0){}
~response(){}
void serialize(std::string* out)//序列化
{
std::string exitCode=std::to_string(_exitCode);
std::string result=std::to_string(_result);
*out = exitCode;
*out = SPACE;
*out = result;
}
bool deserialize(const std::string& in)
{
size_t posSpace=in.find(SPACE);
if(posSpace==std::string::npos)
{
return false;
}
std::string exitCodeStr=in.substr(0,posSpace);
std::string resultStr=in.substr(posSpace+SPACE_LEN,std::string::npos);
_exitCode=atoi(exitCodeStr.c_str());
_result=atoi(resultStr.c_str());
return true;
}
void set_exitCode(int exitCode){
_exitCode=exitCode;
}
void set_result(int result)
{
_result=result;
}
int get_exitCode()
{
return _exitCode;
}
int get_result()
{
return _result;
}
void debug()
{
std::cout<<_exitCode<<""<<_result<<std::endl;
}
public:
int _exitCode;
int _result;
};
bool makeRequest(const std::string& message,request* req)//定制请求
{
//首先消除指令消息中的空格
std::string tmpMsg;
std::string opStr=OPS;
for(auto e:message)
if((e<='9'&&e>='0')||(std::string::npos!=opStr.find(e)))
{
tmpMsg +=e;
}
else if(e!=' '){
return false;
}
std::cout<<tmpMsg<<std::endl;
int opPos=0;
int first_pos=tmpMsg.find_first_of(opStr);
int last_pos=tmpMsg.find_last_of(opStr);
if((tmpMsg[last_pos]!='-'&&tmpMsg[last_pos]!='+')&&!isdigit(tmpMsg[last_pos-1]))
{
return false;
}
if(tmpMsg[first_pos]=='-'||tmpMsg[first_pos]=='+')
{
if(first_pos==0)
{
opPos=tmpMsg.find_first_of(opStr,first_pos+1);
}
else{
opPos=first_pos;
}
}
else{
if(first_pos==0)
{
return false;
}
opPos=first_pos;
}
std::string left=tmpMsg.substr(0,opPos);
std::string right=tmpMsg.substr(0,opPos+1);
req->set_X(atoi(left.c_str()));
req->set_y(atoi(right.c_str()));
req->set_op(tmpMsg[opPos]);
req->debug();
return true;
}
8、服务端 tcpServer.cc
到这一步我们基本的接口已经实现完成了,接下来就是服务器和客户端的连接工作了,在这之前,我们先把网络计算器的逻辑简单实现一下
//这里是确保不会出现除零和模零的情况
//初始化列表中包含了5个元素,每个元素都是一个键值对,用于初始化opFunctions这个std::map。
std::map<char,std::function<int(int,int)>>opFunctions
{
{'+',[](int elemOne, int elemTwo){return elemOne+elemTwo;}},
{'-',[](int elemOne, int elemTwo){return elemOne-elemTwo;}},
{'*',[](int elemOne, int elemTwo){return elemOne*elemTwo;}},
{'/',[](int elemOne, int elemTwo){return elemOne/elemTwo;}},
{'%',[](int elemOne, int elemTwo){return elemOne%elemTwo;}}
};
static response calculator(const request& req)
{
response resp;
int x=req.get_x();
int y=req.get_y();
int op=req.get_op();
if(opFunctions.find(req.get_op())==opFunctions.end())
{
resp.set_exitCode(-3);
}
else
{
if(y==0&&op=='/'){
resp.set_exitCode(-1);
}
else if(y==0&&op=='%'){
resp.set_exitCode(-2);
}
else{
resp.set_result(opFunctions[op](x,y));
}
}
return resp;
}
接着我们实现任务处理的具体逻辑,也就是封装的Task类中的线程所执行的回调函数,,将它作为参数传给Task对象
该函数 netCal 主要用于处理网络通信中的客户端请求,包括读取数据、解析请求、计算结果以及发送响应。
void netCal(int sock,const std::string& clientIp,uint16_t clientPort)
{
assert(sock>=0);
assert(!clientIp.empty());
assert(clientPort>=1024);
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 socket,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));
}
// 走到这里 读取成功
// 但是, 读取到的内容是什么呢?
// 本次读取, 有没有可能读取到的只是发送过来的一部分呢? 如果发送了一条或者多条完整strPackage, 却没有读取完整呢?
// 这种情况是有可能发生的, 所以不能直接进行 decode 以及 反序列化, 需要先检查
buffer[s]='\0';
inBuffer+=buffer;
uint32_t strPackageLen=0;
std::string package=decode(inBuffer,&strPackageLen);
if(strPackageLen==0)
continue;
if(req.deserialize(package))
{
response resp=calculator(req);
std::string respPackage;
resp.serialize(&respPackage);
respPackage=encode(respPackage,respPackage.size());
write(sock,respPackage.c_str(),respPackage.size());
}
}
}
后面就是基本的东西了,我们之前已经实现过这里就不过多赘述了
class tcpServer
{
public:
tcpServer(uint16_t port,const std::string& ip="")
:_port(port)
,_ip(ip)
,_LiSockFd(-1){}
void Init()
{
_LiSockFd=socket(AF_INET,SOCK_STREAM,0);
if(_LiSockFd<0)
{
logMessage(FATAL,"socket() failed::%s : %d",strerror(errno),_LiSockFd);
exit(SOCKET_ERR);
}
logMessage(DEBUG,"socket() success::%d",_LiSockFd);
struct sockaddr_in local;
std::memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(_port);
_ip.empty()?(local.sin_addr.s_addr=htonl(INADDR_ANY)):(inet_aton(_ip.c_str(),&local.sin_addr));
if(bind(_LiSockFd,(const struct sockaddr*)&local,sizeof(local))==-1)
{
logMessage(FATAL,"socket() failed::%s : %d",strerror(errno),_LiSockFd);
exit(BIND_ERR);
}
logMessage(DEBUG,"bind() success::%d",_LiSockFd);
if(listen(_LiSockFd,5)==-1)
{
logMessage(FATAL,"listen() failed::%s : %d",strerror(errno),_LiSockFd);
exit(LISTEN_ERR);
}
logMessage(DEBUG,"listen() success::%d",_LiSockFd);
_tp->threadPool<Task>::getInstance();
}
void start()
{
_tp->start();
logMessage(DEBUG,"threadPool start,thread num:%d",_tp->getThreadNum());
while(true)
{
struct sockaddr_in peer;
socklen_t peerlen=sizeof(peer);
int serviceSockFd=accept(_LiSockFd,(struct sockaddr*)&peer,&peerlen);
if(serviceSockFd==-1)
{
logMessage(WARINING,"accept() failed::%s : %d",strerror(errno),_LiSockFd);
continue;
}
std::string peerIp=inet_ntoa(peer.sin_addr);
uint16_t peerPort=ntohs(peer.sin_port);
logMessage(DEBUG,"accept() success:: [%s: %d] | %d",peerIp.c_str(),peerPort,serviceSockFd);
Task t(serviceSockFd,peerIp,peerPort,netCal);
_tp->pushTask(t);
}
}
private:
uint16_t _port;
std::string _ip;
int _LiSockFd;
threadPool<Task>* _tp;
};
void Usage(string proc)
{
std::cerr<<"Usage::\n\t"<<proc<<"port ip"<<std::endl;
std::cerr<<"example::\n\t"<<proc<<"8080 127.0.0.1"<<std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=3&&argc!=2)
{
Usage(argv[0]);
exit(USE_ERR);
}
uint16_t port=atoi(argv[1]);
string ip;
if(argc==3)
{
ip=argv[2];
}
daemonize();
log log;
log.enable();
tcpServer Usvr(port,ip);
Usvr.Init();
Usvr.start();
return 0;
}
tcpServer完整代码
//服务端代码
#include"util.hpp"
#include"threadPool.hpp"
#include"task.hpp"
#include"daemonize.hpp"
#include "protocol.hpp"
//这里是确保不会出现除零和模零的情况
//初始化列表中包含了5个元素,每个元素都是一个键值对,用于初始化opFunctions这个std::map。
std::map<char,std::function<int(int,int)>>opFunctions
{
{'+',[](int elemOne, int elemTwo){return elemOne+elemTwo;}},
{'-',[](int elemOne, int elemTwo){return elemOne-elemTwo;}},
{'*',[](int elemOne, int elemTwo){return elemOne*elemTwo;}},
{'/',[](int elemOne, int elemTwo){return elemOne/elemTwo;}},
{'%',[](int elemOne, int elemTwo){return elemOne%elemTwo;}}
};
static response calculator(const request& req)
{
response resp;
int x=req.get_x();
int y=req.get_y();
int op=req.get_op();
if(opFunctions.find(req.get_op())==opFunctions.end())
{
resp.set_exitCode(-3);
}
else
{
if(y==0&&op=='/'){
resp.set_exitCode(-1);
}
else if(y==0&&op=='%'){
resp.set_exitCode(-2);
}
else{
resp.set_result(opFunctions[op](x,y));
}
}
return resp;
}
//任务处理的具体逻辑,将它作为参数传给Task对象
//该函数 netCal 主要用于处理网络通信中的客户端请求,包括读取数据、解析请求、计算结果以及发送响应。
void netCal(int sock,const std::string& clientIp,uint16_t clientPort)
{
assert(sock>=0);
assert(!clientIp.empty());
assert(clientPort>=1024);
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 socket,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));
}
// 走到这里 读取成功
// 但是, 读取到的内容是什么呢?
// 本次读取, 有没有可能读取到的只是发送过来的一部分呢? 如果发送了一条或者多条完整strPackage, 却没有读取完整呢?
// 这种情况是有可能发生的, 所以不能直接进行 decode 以及 反序列化, 需要先检查
buffer[s]='\0';
inBuffer+=buffer;
uint32_t strPackageLen=0;
std::string package=decode(inBuffer,&strPackageLen);
if(strPackageLen==0)
continue;
if(req.deserialize(package))
{
response resp=calculator(req);
std::string respPackage;
resp.serialize(&respPackage);
respPackage=encode(respPackage,respPackage.size());
write(sock,respPackage.c_str(),respPackage.size());
}
}
}
class tcpServer
{
public:
tcpServer(uint16_t port,const std::string& ip="")
:_port(port)
,_ip(ip)
,_LiSockFd(-1){}
void Init()
{
_LiSockFd=socket(AF_INET,SOCK_STREAM,0);
if(_LiSockFd<0)
{
logMessage(FATAL,"socket() failed::%s : %d",strerror(errno),_LiSockFd);
exit(SOCKET_ERR);
}
logMessage(DEBUG,"socket() success::%d",_LiSockFd);
struct sockaddr_in local;
std::memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(_port);
_ip.empty()?(local.sin_addr.s_addr=htonl(INADDR_ANY)):(inet_aton(_ip.c_str(),&local.sin_addr));
if(bind(_LiSockFd,(const struct sockaddr*)&local,sizeof(local))==-1)
{
logMessage(FATAL,"socket() failed::%s : %d",strerror(errno),_LiSockFd);
exit(BIND_ERR);
}
logMessage(DEBUG,"bind() success::%d",_LiSockFd);
if(listen(_LiSockFd,5)==-1)
{
logMessage(FATAL,"listen() failed::%s : %d",strerror(errno),_LiSockFd);
exit(LISTEN_ERR);
}
logMessage(DEBUG,"listen() success::%d",_LiSockFd);
_tp->threadPool<Task>::getInstance();
}
void start()
{
_tp->start();
logMessage(DEBUG,"threadPool start,thread num:%d",_tp->getThreadNum());
while(true)
{
struct sockaddr_in peer;
socklen_t peerlen=sizeof(peer);
int serviceSockFd=accept(_LiSockFd,(struct sockaddr*)&peer,&peerlen);
if(serviceSockFd==-1)
{
logMessage(WARINING,"accept() failed::%s : %d",strerror(errno),_LiSockFd);
continue;
}
std::string peerIp=inet_ntoa(peer.sin_addr);
uint16_t peerPort=ntohs(peer.sin_port);
logMessage(DEBUG,"accept() success:: [%s: %d] | %d",peerIp.c_str(),peerPort,serviceSockFd);
Task t(serviceSockFd,peerIp,peerPort,netCal);
_tp->pushTask(t);
}
}
private:
uint16_t _port;
std::string _ip;
int _LiSockFd;
threadPool<Task>* _tp;
};
void Usage(string proc)
{
std::cerr<<"Usage::\n\t"<<proc<<"port ip"<<std::endl;
std::cerr<<"example::\n\t"<<proc<<"8080 127.0.0.1"<<std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=3&&argc!=2)
{
Usage(argv[0]);
exit(USE_ERR);
}
uint16_t port=atoi(argv[1]);
string ip;
if(argc==3)
{
ip=argv[2];
}
daemonize();
log log;
log.enable();
tcpServer Usvr(port,ip);
Usvr.Init();
Usvr.start();
return 0;
}
9、客户端 tcpClient.cc
//客户端
#include"util.hpp"
#include"protocol.hpp"
using std::cerr;
using std::endl;
using std::string;
using std::cin;
using std::getline;
using std::cout;
volatile bool quit=false;
void Usage( string proc)
{
cerr<<"Usage::\n\t"<<proc<<"serverIp serverPort"<<endl;
cerr<<"example::\n\t"<<proc<<"127.0.0.1 8080"<<endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(USE_ERR);
}
string serverIp=argv[1];
uint16_t serverPort=atoi(argv[2]);
int sockFd=socket(AF_INET,SOCK_STREAM,0);
if(sockFd<0)
{
logMessage(FATAL,"socket() failed::%s %d",strerror(errno),sockFd);
exit(SOCKET_ERR);
}
logMessage(DEBUG,"socket() success::%d",sockFd);
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_port=serverPort;
server.sin_family=AF_INET;
inet_aton(serverIp.c_str(),&server.sin_addr);
if(connect(sockFd,(const sockaddr*)&server,sizeof(server))==-1)
{
logMessage(FATAL,"connect() failed::%s: %d",strerror(errno),sockFd);
exit(CONNECT_ERR);
}
logMessage(DEBUG,"connect() success.");
string message;
while(!quit)
{
message.clear();
cout<<"Please enter";
getline(cin,message);
if(strcasecmp(message.c_str(),"quit")==0)
{
quit=true;
continue;
}
request req;
if(!makeRequest(message,&req))
{
continue;
}
string package;
req.serialize(&package);
package=encode(package,package.size());
ssize_t sw=write(sockFd,package.c_str(),package.size());
if(sw>0)
{
char buff[BUFFER_SIZE];
ssize_t sr=read(sockFd,buff,sizeof(buff)-1);
if(sr>0)
{
message[sr]='\0';
}
std::string echoPackage=buff;
response resp;
uint32_t packageLen=0;
echoPackage=decode(echoPackage,&packageLen);
if(packageLen)
{
resp.deserialize(echoPackage);
printf("[exitcode :%d] %d\n",resp.get_exitCode(),resp.get_result());
}
}
else if(sw<0)
{
logMessage(FATAL,"Client write() failed: %d : %s",sockFd,strerror(errno));
break;
}
}
close(sockFd);
return 0;
}
10.makefile
.PHONY:all
all:tcpServerd tcpClient
tcpServerd: tcpServer.cc
g++ -std=c++11 -o $@ $^ -lpthread
tcpClient: tcpClient.cc
g++ -std=c++11 -o $@ $^
.PHONY:clean
clean:
rm -rf tcpServerd tcpClient
以上就是所有的代码了。
四、jsoncpp库序列化与反序列化
在我们上面实现的协议中, request 和 respoonse 这两个类的序列化和反序列化都是我们自己实现的。
实际上,有许多第三方库也提供了一些比较好用的序列化方法。
下面使用 jsoncpp库
来实现序列化和反序列化的具体操作。
先安装
对 protocol.hpp 做一些修改
// 定制请求的协议
class request {
public:
request() {}
~request() {}
// 序列化 -- 结构化的数据 -> 字符串
// 我们序列化的结构是 : "_x _op _y", 即 空格分割
void serialize(std::string* out) {
#ifdef MY_SELF
std::string xStr = std::to_string(get_x());
std::string yStr = std::to_string(get_y());
*out += xStr;
*out += SPACE;
*out += get_op();
*out += SPACE;
*out += yStr;
#else
Json::Value root;
root["x"] = _x; // Json::Value 是key:value类型的结构, 这里相当于 在root中添加 key: "x" 对应 value: _x的值
root["y"] = _y; // 同上
root["op"] = _op; // 同上
Json::FastWriter fw;
*out = fw.write(root);
std::cout << "debug json after: " << *out << std::endl;
#endif
}
// 反序列化 -- 字符串 -> 结构化的数据
bool deserialize(const std::string& in) {
#ifdef MY_SELF
// in 的格式 1 + 1
// 先查找两个空格的位置
size_t posSpaceOne = in.find(SPACE);
if (posSpaceOne == std::string::npos)
return false;
size_t posSpaceTwo = in.rfind(SPACE);
if (posSpaceTwo == std::string::npos)
return false;
// 再获取三段字符串
std::string dataOne = in.substr(0, posSpaceOne);
std::string dataTwo = in.substr(posSpaceTwo + SPACE_LEN, std::string::npos);
std::string oper = in.substr(posSpaceOne + SPACE_LEN, posSpaceTwo - (posSpaceOne + 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); // 将使用Json序列化过的字符串, 再转换存储到 Json::Value root 中
_x = root["x"].asInt();
_y = root["y"].asInt();
_op = root["op"].asInt();
return true;
#endif
}
int get_x() const {
return _x;
}
int get_y() const {
return _y;
}
char get_op() const {
return _op;
}
void set_x(int x) {
_x = x;
}
void set_y(int y) {
_y = y;
}
void set_op(char op) {
_op = op;
}
void debug() {
std::cout << _x << " " << _op << " " << _y << std::endl;
}
private:
int _x;
int _y;
char _op;
};
// 定制响应的协议
class response {
public:
response()
: _exitCode(0)
, _result(0) {}
~response() {}
void serialize(std::string* out) {
#ifdef MY_SELF
std::string exitCode = std::to_string(_exitCode);
std::string result = std::to_string(_result);
*out = exitCode;
*out += SPACE;
*out += result;
#else
Json::Value root;
root["exitCode"] = _exitCode;
root["result"] = _result;
Json::FastWriter fw;
*out = fw.write(root);
#endif
}
// 反序列化
bool deserialize(const std::string& in) {
#ifdef MY_SELF
size_t posSpace = in.find(SPACE);
if (posSpace == std::string::npos) {
return false;
}
std::string exitCodeStr = in.substr(0, posSpace);
std::string resultStr =
in.substr(posSpace + SPACE_LEN, std::string::npos);
_exitCode = atoi(exitCodeStr.c_str());
_result = atoi(resultStr.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 set_exitCode(int exitCode) {
_exitCode = exitCode;
}
void set_result(int result) {
_result = result;
}
int get_exitCode() const {
return _exitCode;
}
int get_result() const {
return _result;
}
void debug() {
std::cout << _exitCode << " " << _result << std::endl;
}
private:
int _exitCode;
int _result;
};
我们通过条件编译, 给请求和响应类的序列化与反序列化接口, 实现了两种方式.
- 纯手写的方式, 针对数据做一系列的字符串操作
- 使用jsoncpp库, 提供的序列化与反序列化接口, 快速实现
实现之后, 我们使用
g++ -std=c++11 -o tcpServerd tcpServer.cc -ljsoncpp -lpthread
g++ -std=c++11 -o tcpClient tcpClient.cc -ljsoncpp
感谢指正!