一个简单的协议定制

目录

补充概念:三次握手,四次挥手

再谈协议 

网络版计算器

准备工作

makefile

log.hpp

calServer.hpp

calServer.cc

calClient.hpp

calClient.cc

服务端

新建文件与接口

Protocol.hpp

1.0服务端的一个流程

1.1创建一个回调方法

1.2保证你读到的消息是【一个】完整的请求

1.3类Request序列化和反序列化

注意1

calServer.hpp

 1.4接口handlerEnter修改1

calServer.cc

1.5接口cal业务逻辑操作

1.6接口handlerEnter修改2

protocol.hpp

1.7类Response中序列化和反序列化工作

注意2

1.8接口enLength增加有效载荷

calServer.hpp 

1.9发送已经序列化的数据

新接口send

1.10一个概念:协议定制 和 序列化和反序列化是工作在不同阶段的

protocol.hpp

1.11recvRequest读取请求

1.12deLength删除有效载荷并得到正文部分

1.13inbuffer放到循环外部去

测试1阶段 -- 编译测试

客户端

calClient.hpp 

2.0客户端的发送与接收

2.1简单的打印说明

测试2阶段 -- 功能基本完善测试

2.3简易版本的状态机--分割输入字符函数ParseLine

测试阶段3 -- 功能完善

2.4人为制造一个错误

测试阶段4 -- 当传输的数据不完整的时候(额外测试)

序列化和反序列化可以用现成的

Jasoncpp的安装

判断jsoncpp是否安装成功

直接安装的是动态库 

makefile修改1

json

条件编译修改protocol.hpp

注意事项:这两只负责添加删除报头,和序列化反序列化没关系

json的库包含

json是一个kye-val的格式

使用json修改protocol

使用json测试

条件编译,不使用自己写的

条件编译,使用自己写的

makefile修改2

序列化和反序列化自定义协议完成

源码

makefile

calClient.cc

calClient.hpp

calServer.cc

calServer.hpp

log.hpp

protocol.hpp

补充

简单认识http协议


补充概念:三次握手,四次挥手

下图是基于TCP协议的客户端/服务器程序的一般流程:
 

 服务器初始化:

        调用socket, 创建文件描述符;
        调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
        调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;

        调用accecpt, 并阻塞, 等待客户端连接过来;

建立连接的过程: 

        调用socket, 创建文件描述符;
        调用connect, 向服务器发起连接请求;
        connect会发出SYN段并阻塞等待服务器应答; (第一次)
        服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
        客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

        这个建立连接的过程, 通常称为 三次握手;


数据传输的过程

        建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;


        服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;


        这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;


        服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;


        客户端收到后从read()返回, 发送下一条请求,如此循环下去;

断开连接的过程: 

        如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);


        此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);


        read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)


        客户端收到FIN, 再返回一个ACK给服务器; (第四次)

        这个断开连接的过程, 通常称为 四次挥手


在学习socket API时要注意应用程序和TCP协议层是如何交互的:

        应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段


        应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段

再谈协议 

        协议是一种 "约定". socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?

        结构化的数据:

接下来手写一个简单协议

网络版计算器

准备工作

因为代码相关性的问题,这里的相关代码就直接复用上篇文章的了

简单的TCP网络程序·线程池(后端服务器)_tcp线程池_清风玉骨的博客-CSDN博客

参考下图稍作修改

makefile

cc=g++
.PHONY:all
all:calserver calclientcalclient:calClient.cc$(cc) -o $@ $^ -std=c++11calserver:calServer.cc$(cc) -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -f calclient calserver

log.hpp

#pragma once#include <iostream>
#include <string>
#include <stdarg.h>
#include <ctime>
#include <unistd.h>// 定义五种不同的信息
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3 // 一种不影响服务器的错误
#define FATAL   4 // 致命错误const char *to_levelstr(int level)
{switch (level)  // 这里直接return了{case DEBUG:return "DEBUG";case NORMAL:return "NORMAL";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return nullptr;}
}void logMessage(int level, const char *format, ...)
{
#define NUM 1024// 获取前缀信息[日志等级] [时间戳/时间] [pid]char logprefix[NUM];snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",to_levelstr(level), (long int)time(nullptr), getpid());// 获取内容char logcontent[NUM];va_list arg;va_start(arg, format); // 因为压栈是反过来的,所以直接使用左边那个参数就行了vsnprintf(logcontent, sizeof(logcontent), format, arg); // 第三个参数是格式, 第四个就是初始化好的可变参数std::cout << logprefix << logcontent << std::endl; // 我们只需要日志显示就好了,其余的不需要
}

calServer.hpp

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>#include "log.hpp"namespace server
{enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport = 8080;static const int gbacklog = 5; // 10、20、50都可以,但是不要太大比如5千,5万class TcpServer{public:TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port){}void initServer(){// 1. 创建socket文件套接字对象 -- 流式套接字_listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0if (_listensock < 0){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success: %d", _listensock);// 2.bind绑定自己的网路信息 -- 注意包含头文件struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);      // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");// 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑{logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}void start(){for (;;) // 一个死循环{// 4. server 获取新链接// sock 和client 进行通信的fdstruct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){logMessage(ERROR, "accept error, next"); // 这个不影响服务器的运行,用ERROR,就像张三不会因为没有把人招呼进来就不干了continue;}logMessage(NORMAL, "accept a new link success, get new sock: %d", sock);    // 因为支持可变参数了// 这里直接使用多进程版的代码进行修改// version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,// 即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份// 也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensockpid_t id = fork();if (id == 0) // 当id为 0 的时候就代表这里是子进程{// 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符// 即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好close(_listensock);//if(fork()>0) exit(0); 解决方法1:利用孤儿进程特性// serviceIO(sock); 暂时不需要close(sock);exit(0);}// 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1// 子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了// 所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了// close(sock);// father//  那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待//  且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待//  一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了// 不需要等待了 version 2 pid_t ret = waitpid(id, nullptr, 0);if(ret > 0){logMessage(NORMAL, "wait child success"); // ?}}}~TcpServer() {}private:int _listensock; // 修改二:改为listensock 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!uint16_t _port;};} // namespace server

calServer.cc

#include "calServer.hpp"
#include <memory>using namespace server;
using namespace std;static void Usage(string proc)
{cout << "Usage:\n\t" << proc << " local_port\n\n"; // 命令提示符
}// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{ if (argc != 2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);unique_ptr<TcpServer> tsvr(new TcpServer(port));tsvr->initServer();tsvr->start();return 0;
}

calClient.hpp

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>#define NUM 1024class TcpClient
{
public:TcpClient(const std::string &serverip, const uint16_t &port): _sock(1), _serverip(serverip), _serverport(port){}void initClient(){// 1. 创建socket_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){// 客户端也可以有日志,不过这里就不再实现了,直接打印错误std::cout << "socket create error" << std::endl;exit(2);}// 2. tcp的客户端要不要bind? 要的! 但是不需要显示bind,这里的client port要让OS自定!// 3. 要不要listen? -- 不需要!客户端不需要建立链接// 4. 要不要accept? -- 不要!// 5. 要什么? 要发起链接!}void start(){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());if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0){std::cerr << "socket connect error" << std::endl;}else{std::string msg;while (true){std::cout << "Enter# ";std::getline(std::cin, msg);write(_sock, msg.c_str(), msg.size());char buffer[NUM];int n = read(_sock, buffer, sizeof(buffer) - 1);if (n > 0){// 目前我们把读到的数据当成字符串, 截至目前buffer[n] = 0;std::cout << "Server回显# " << buffer << std::endl;}else{break;}}}}~TcpClient(){if(_sock >= 0) close(_sock);    //不写也行,因为文件描述符的生命周期随进程,所以进程退了,自然也就会自动回收了}private:int _sock;std::string _serverip;uint16_t _serverport;
};

calClient.cc

#include "calClient.hpp"
#include <memory>using namespace std;static void Usage(string proc)
{cout << "Usage:\n\t" << proc << " serverip serverport\n\n"; // 命令提示符
}// ./tcpclient serverip serverport  调用逻辑
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t serverport = atoi(argv[2]);unique_ptr<TcpClient> tcli(new TcpClient(serverip, serverport));tcli->initClient();tcli->start();return 0;
}

至此准备工作完成 

服务端

新建文件与接口

Protocol.hpp

协议的初步定制与初始化 

#pragma once#include <iostream>class Request
{
public:Request():x(0), y(0), op(0) // 初始化{}
public:// "x op y" 规定请求一定是这样的格式,并且x操作数一定在前,y操作数在后int x;int y;char op; // 操作符可能有除0之类的错误,所以需要处理
};class Response
{
public:Response():exitcode(0), result(0){}
public:int exitcode; // 0: 计算成功,!0表示计算失败,具体是多少,定好标准,类似与错误码int result; // 计算结果
};

1.0服务端的一个流程

1.1创建一个回调方法

1.2保证你读到的消息是【一个】完整的请求

1.3类Request序列化和反序列化

注意1

注意修正地方,这部分是在测试代码中发现的错误,上图中可能有未修改过来的,以此图为基准

calServer.hpp

 1.4接口handlerEnter修改1

注意这边还有个无参构造,不然会报错

calServer.cc

1.5接口cal业务逻辑操作

1.6接口handlerEnter修改2

protocol.hpp

1.7类Response中序列化和反序列化工作

注意2

注意修正地方,这部分是在测试代码中发现的错误,上图中可能有未修改过来的,以此图为基准

1.8接口enLength增加有效载荷

calServer.hpp 

1.9发送已经序列化的数据

新接口send

当然我们对于字符流的写入可以直接使用write 

1.10一个概念:协议定制 和 序列化和反序列化是工作在不同阶段的

这里的协议定制指的是定制x,y,op和exitcode,result,enLength函数这些东西属于定制协议,其他的像serialize,deserialize这些接口就属于序列化和反序列化

protocol.hpp

1.11recvRequest读取请求

注意:后面更名为recvPackage

1.12deLength删除有效载荷并得到正文部分

1.13inbuffer放到循环外部去

测试1阶段 -- 编译测试

至此客户端的代码基本完成,现在编译代码是可以通过的

注意这里的类名为了防止混淆就行了修改

有关此次的TcpServer全部修改为CalServer 

 有关此次的TcpClient全部修改为CalClient

客户端

calClient.hpp 

2.0客户端的发送与接收

2.1简单的打印说明

测试2阶段 -- 功能基本完善测试

2.3简易版本的状态机--分割输入字符函数ParseLine

测试阶段3 -- 功能完善

2.4人为制造一个错误

测试阶段4 -- 当传输的数据不完整的时候(额外测试)

序列化和反序列化可以用现成的

注意这里指的是序列化和反序列化,这个是一个复杂的体力活,并且现在有很多成熟的解决方案可以供我们使用,没必要花费精力去做序列化和反序列化,当然协议我们还是可以自己写的

Jasoncpp的安装

判断jsoncpp是否安装成功

直接安装的是动态库 

makefile修改1

cc=g++
.PHONY:all
all:calserver calclientcalclient:calClient.cc$(cc) -o $@ $^ -std=c++11 -ljsoncpp #-DMYSELFcalserver:calServer.cc$(cc) -o $@ $^ -std=c++11 -ljsoncpp #-DMYSELF.PHONY:clean
clean:rm -f calclient calserver

 

 如果程序中有第三方库的使用,就在编译的时候要加入这一句,以代码为基准

json

条件编译修改protocol.hpp

注意事项:这两只负责添加删除报头,和序列化反序列化没关系

json的库包含

json是一个kye-val的格式

使用json修改protocol

 因为代码的耦合度不高,可以直接修改

使用json测试

条件编译,不使用自己写的

条件编译,使用自己写的

makefile修改2

cc=g++
LD=#-DMYSELF
.PHONY:all
all:calserver calclientcalclient:calClient.cc$(cc) -o $@ $^ -std=c++11 -ljsoncpp ${LD}calserver:calServer.cc$(cc) -o $@ $^ -std=c++11 -ljsoncpp ${LD}.PHONY:clean
clean:rm -f calclient calserver

当需要使用自己写的时候就把 # 去掉即可

序列化和反序列化自定义协议完成

源码

makefile

cc=g++
LD=#-DMYSELF
.PHONY:all
all:calserver calclientcalclient:calClient.cc$(cc) -o $@ $^ -std=c++11 -ljsoncpp ${LD}calserver:calServer.cc$(cc) -o $@ $^ -std=c++11 -ljsoncpp ${LD}.PHONY:clean
clean:rm -f calclient calserver

calClient.cc

#include "calClient.hpp"
#include <memory>using namespace std;static void Usage(string proc)
{cout << "Usage:\n\t" << proc << " serverip serverport\n\n"; // 命令提示符
}// ./tcpclient serverip serverport  调用逻辑
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t serverport = atoi(argv[2]);unique_ptr<CalClient> tcli(new CalClient(serverip, serverport));tcli->initClient();tcli->start();return 0;
}

calClient.hpp

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>#include "protocol.hpp"#define NUM 1024class CalClient
{
public:CalClient(const std::string &serverip, const uint16_t &port): _sock(1), _serverip(serverip), _serverport(port){}void initClient(){// 1. 创建socket_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){// 客户端也可以有日志,不过这里就不再实现了,直接打印错误std::cout << "socket create error" << std::endl;exit(2);}// 2. tcp的客户端要不要bind? 要的! 但是不需要显示bind,这里的client port要让OS自定!// 3. 要不要listen? -- 不需要!客户端不需要建立链接// 4. 要不要accept? -- 不要!// 5. 要什么? 要发起链接!}void start(){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());if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0){std::cerr << "socket connect error" << std::endl;}else{std::string line;std::string inbuffer;while (true){std::cout << "mycal>>> ";std::getline(std::cin, line);    // 输入类似于 1 + 1Request req = ParseLine(line);   // 对输入的数据进行分割// 先简单测试一个该功能// Request req(10,10,'+');       这里是进行程序从测试,防止大改,先让它运行起来,这里就模拟出一个请求样例std::string content;req.serialize(&content);        // 序列化std::string send_string = enLength(content);    // 添加长度send(_sock, send_string.c_str(), send_string.size(), 0); // bug? 暂时不管std::string package, text;// "content_len"\r\n"exitcode result"\r\n 我们会拿到一个序列化的数据if(!recvPackage(_sock, inbuffer, &package)) continue; // 没有成功就继续if(!deLength(package, &text)) continue; // 去掉多余的,成功就会得到"exitcode result",否则继续// "exitcode result"    到这里就text里面就是一个正文部分了Response resp;resp.deserialize(text);std::cout << "exitCode: " << resp.exitcode << std::endl;std::cout << "result: " << resp.result << std::endl;}}}Request ParseLine(const std::string &line){// 简易版本的状态机// "1+1" "123*456" "12/0"int status = 0; // 0:操作符之前,1:碰到了操作符,2: 操作符之后int i = 0;int cnt = line.size(); // 这里的line不包括\n,因为getline的性质,不会把斜杠录进来std::string left, right;char op;while (i < cnt){switch(status){case 0:{if(!isdigit(line[i])) // 判断是不是数字字符,不是说明是操作符{op = line[i];status = 1;}else left.push_back(line[i++]); // string支持一个字符一个字符的push_back}break;case 1:i++; // 操作符直接下一位,在状态0的时候就处理了status = 2;break;case 2:right.push_back(line[i++]);break;}}std::cout << std::stoi(left) << " " << std::stoi(right) << " " << op << std::endl;return Request(std::stoi(left), std::stoi(right), op);}~CalClient(){if(_sock >= 0) close(_sock);    //不写也行,因为文件描述符的生命周期随进程,所以进程退了,自然也就会自动回收了}private:int _sock;std::string _serverip;uint16_t _serverport;
};

calServer.cc

#include "calServer.hpp"
#include <memory>using namespace server;
using namespace std;static void Usage(string proc)
{cout << "Usage:\n\t" << proc << " local_port\n\n"; // 命令提示符
}
// req: 里面一定是我们的处理好的一个完整的请求对象
// resp: 根据req,进行业务处理,填充resp,不用管理任何读取和写入(IO), 序列化和反序列化等任何细节
bool cal(const Request &req, Response &resp)
{// req已经有结构化完成的数据啦,你可以直接使用resp.exitcode = 0;resp.result = 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 '/': // switch case 里面也可以放花括号{if (req.y == 0)resp.exitcode = DIV_ZERO;   // 除0错误elseresp.result = req.x / req.y;}break;case '%':{if (req.y == 0)resp.exitcode = MOD_ZERO;   // 模0错误elseresp.result = req.x % req.y;}break;default:resp.exitcode = OP_ERROR;break;}return true;
}// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);unique_ptr<CalServer> tsvr(new CalServer(port));tsvr->initServer();tsvr->start(cal); // 传入方法return 0;
}

calServer.hpp

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>#include "log.hpp"
#include "protocol.hpp"namespace server
{enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport = 8080;static const int gbacklog = 5; // 10、20、50都可以,但是不要太大比如5千,5万// const Request &req: 输入型// Request & resp: 输出型typedef std::function<bool(const Request &req, Response &resp)> func_t; // 回调函数// 我们把它放到类外处理,保证解耦void handlerEnter(int sock, func_t func){std::string inbuffer; // 放到循环的外部while (true){// 1. 读取: "content_len"\r\n"x op y"\r\n// 1.1 你怎么保证你读到的消息是【一个】完整的请求?std::string req_text; // 输出型参数,这个模型是多执行流多进程版的,所以我们等的起,我们可以一直等到【一个】完整的请求被读上了来// 1.2 我们保证,我们req_text里面一定是一个完整的请求: "content_len"\r\n"x op y"\r\nif (!recvPackage(sock, inbuffer, &req_text))return;          // 和协议有关,放到protocol里面去std::cout << "带报头的请求: \n" << req_text << std::endl;std::string req_str; // 创建一个变量用来装东西if (!deLength(req_text, &req_str))return;std::cout << "去掉报头的正文: \n" << req_str << std::endl;// 2. 对请求Request, 反序列化// 2.1 得到一个结构化的请求对象Request req;if (!req.deserialize(req_str))return; // 如果反序列化失败就直接返回// 3. 计算处理, req.x, req.op, req.y  --- 业务逻辑// 3.1 得到一个结构和的响应Response resp;func(req, resp); // req的处理结果,全部放入到了resp。回调是不是不回来了? 不是!// 4.对响应Response, 进行序列化// 4.1 得到了一个"字符串"std::string resp_str;resp.serialize(&resp_str); // 因为步骤3已经完成了业务逻辑处理,并且把结构化的数据填充完成,这里就可以直接使用了std::cout << "计算完成, 序列化响应: " << resp_str << std::endl;// 5. 然后我们再发送响应// 5.1 构建成为一个完整的报文std::string send_string = enLength(resp_str);std::cout << "构建完成完整的响应\n" << send_string << std::endl;send(sock, send_string.c_str(), send_string.size(), 0); // 其实这里的发送也是有问题的,不过后面再说}}class CalServer{public:CalServer(const uint16_t &port = gport) : _listensock(-1), _port(port){}void initServer(){// 1. 创建socket文件套接字对象 -- 流式套接字_listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0if (_listensock < 0){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success: %d", _listensock);// 2.bind绑定自己的网路信息 -- 注意包含头文件struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);      // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");// 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑{logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}void start(func_t func){for (;;) // 一个死循环{// 4. server 获取新链接// sock 和client 进行通信的fdstruct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){logMessage(ERROR, "accept error, next"); // 这个不影响服务器的运行,用ERROR,就像张三不会因为没有把人招呼进来就不干了continue;}logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // 因为支持可变参数了// 这里直接使用多进程版的代码进行修改// version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,// 即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份// 也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensockpid_t id = fork();if (id == 0) // 当id为 0 的时候就代表这里是子进程{// 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符// 即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好close(_listensock);// if(fork()>0) exit(0); 解决方法1:利用孤儿进程特性//  serviceIO(sock); 暂时不需要handlerEnter(sock, func); // 保证解耦, 把接口的实现,放到类外close(sock);exit(0);}// 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1// 子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了// 所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了// close(sock);// father//  那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待//  且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待//  一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了// 不需要等待了 version 2pid_t ret = waitpid(id, nullptr, 0);if (ret > 0){logMessage(NORMAL, "wait child success"); // ?}}}~CalServer() {}private:int _listensock; // 修改二:改为listensock 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!uint16_t _port;};} // namespace server

log.hpp

#pragma once#include <iostream>
#include <string>
#include <stdarg.h>
#include <ctime>
#include <unistd.h>// 定义五种不同的信息
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3 // 一种不影响服务器的错误
#define FATAL   4 // 致命错误const char *to_levelstr(int level)
{switch (level)  // 这里直接return了{case DEBUG:return "DEBUG";case NORMAL:return "NORMAL";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return nullptr;}
}void logMessage(int level, const char *format, ...)
{
#define NUM 1024// 获取前缀信息[日志等级] [时间戳/时间] [pid]char logprefix[NUM];snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",to_levelstr(level), (long int)time(nullptr), getpid());// 获取内容char logcontent[NUM];va_list arg;va_start(arg, format); // 因为压栈是反过来的,所以直接使用左边那个参数就行了vsnprintf(logcontent, sizeof(logcontent), format, arg); // 第三个参数是格式, 第四个就是初始化好的可变参数std::cout << logprefix << logcontent << std::endl; // 我们只需要日志显示就好了,其余的不需要
}

protocol.hpp

#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>// 这里是为了当我们后面需要改动分割符的时候这里可以直接修改
#define SEP " "
#define SEP_LEN strlen(SEP)           // 不要使用sizeof,会出错
#define LINE_SEP "\r\n"               // 这里是为了打印(调试)出来便于观察,我们使用自描述的方式去定制协议前面会添加一个报头表示后面正文部分有多长,// 当然我们也可以用/r/n来分割,但是使用场景就没有那么完整了,基于可扩展可维护的理论
#define LINE_SEP_LEN strlen(LINE_SEP) // 同理不要使用sizeofenum
{OK = 0,DIV_ZERO,MOD_ZERO,OP_ERROR
}; // 正常,除零错误,模0错误,操作符错误// 增加有效载荷 -- 这里也可以用四个字节来标定大小,不过比较麻烦,就直接使用字符串了
// "x op y" -> "content_len"\r\n"x op y"\r\n
std::string enLength(const std::string &text) // 这两这边就不用引用返回了,不过用不用应该都没有事
{// std::string text_len = std::to_string(text.size()); 与下面一行合并,注意这里的text.size()还是没有包含\r\n,这两是在这一步添加的std::string send_string = std::to_string(text.size());send_string += LINE_SEP;send_string += text;send_string += LINE_SEP;return send_string;
}// 删除有效载荷
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text) // 输出型参数
{auto pos = package.find(LINE_SEP);if (pos == std::string::npos)return false;std::string text_len_string = package.substr(0, pos);int text_len = std::stoi(text_len_string);*text = package.substr(pos + LINE_SEP_LEN, text_len);return true;
}// 没有人规定我们网络通信的时候,只能有一种协议!!
// 我们怎么让系统知道我们用的是哪一种协议呢?
// "content_len"\r\n"协议编号"\r\n"x op y"\r\n   -- 我们可以通过加一个协议编号来人系统区分class Request
{
public:Request() : x(0), y(0), op(0){}Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_) // 初始化{}// 1.自己写 -- 目前使用// 2.用现成的bool serialize(std::string *out) // 输出型参数{
#ifdef MYSELF*out = ""; // c式风格清空// 结构化 -> "x op y"; 注意这里的空格, 这里不需添加LINE_SEP,因为在添加报头的时候就已经做了std::string x_string = std::to_string(x);std::string y_string = std::to_string(y);// 修正地方111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111*out = x_string;*out += SEP;*out += op;*out += SEP;*out += y_string;
#elseJson::Value root; // 这是一个万能对象,可以接收任意数据类型root["first"] = x; // 给x起个名字,这里的x是int类型,它会自动转为字符串,并且所有的类型都是如此root["second"] = y;root["oper"] = op;Json::FastWriter writer;    // 序列化// Json::StyledStreamWriter writer; 和上面的序列化不同的是,这个会比较好看,上面那个内容会一行全部显示出来*out = writer.write(root);  // 返回值是string
#endifreturn true;}// "x op y\r\n" -- 这里的\r\n会在上面的删除增那边处理// "x op yyyy" -- 所以这里只需要处理这种情况就行了bool deserialize(const std::string &in){
#ifdef MYSELF// "x op y\r\n" -> 结构化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; // op操作符不是一个字符std::string x_string = in.substr(0, left);         //[ ) 左闭右开区间 [start, end)std::string y_string = in.substr(right + SEP_LEN); // 因为后面的\r\n在传进来的时候已经被处理掉,所以这里直接截取到末尾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]; // 直接取到操作符,因为前面判断过了,到这里一定是一个字符
#elseJson::Value root;Json::Reader reader;reader.parse(in, root);x = root["first"].asInt();y = root["second"].asInt();op = root["oper"].asInt(); // 这里会比较奇怪,因为op在root里面会呈现字符串的形式,asInt会是int类型,但是又会被转为char类型
#endifreturn true;}public:// "x op y" 规定请求一定是这样的格式,并且x操作数一定在前,y操作数在后int x;int y;char op; // 操作符可能有除0之类的错误,所以需要处理
};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 = "";std::string ec_string = std::to_string(exitcode);std::string res_string = std::to_string(result);// 修正地方222222222222222222222222222222222222222222222222222222222222222222222222*out = ec_string;*out += SEP;*out += res_string;
#elseJson::Value root;root["exitcode"] = exitcode;root["result"] = result;Json::FastWriter writer;*out = writer.write(root);
#endifreturn true;}bool deserialize(const std::string &in){
#ifdef MYSELF// "exitcode result" 同理在这里会拿到这个字符串,后面的/r/n在传进来之前的时候就被去掉了auto mid = in.find(SEP);if (mid == std::string::npos)return false;std::string ec_string = in.substr(0, mid);std::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);
#elseJson::Value root;Json::Reader reader;reader.parse(in, root);exitcode = root["exitcode"].asInt();result = root["result"].asInt();
#endifreturn true;}public:int exitcode; // 0: 计算成功,!0表示计算失败,具体是多少,定好标准,类似与错误码int result;   // 计算结果
};// "content_len"\r\n"x op y"\r\n
bool recvPackage(int sock, std::string &inbuffer, std::string *text) // 把inbuffer当作参数传进来,防止在多线程里面出错,输出型参数
{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); // 寻找第一个分隔符if (pos == std::string::npos)continue;                                          // 没有一个完整的请求,所以继续读取std::string text_len_string = inbuffer.substr(0, pos); // 正文长度的子串int text_len = std::stoi(text_len_string);             // 长度就有了// text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size(); 至少有一个完整的报文,面向字节流读取可能有多个也可能一个都没有int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;std::cout << "处理前#inbuffer: \n"<< inbuffer << std::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 << std::endl;break;}elsereturn false;}return true;
}/*
我们也可以类似与这样把报文一个个全部塞到一个vector里面去,不过这里就不考虑了,这里只是表示这种方法是可行的
bool recvRequestALL(int sock, std::vector<std::string> *out)
{std::string line;while(recvRequest(sock, &line)) out->push_back(line);
}
*/

补充

简单认识http协议

平时我们俗称的 "网址" 其实就是说的 URL 

其实 在url中很常见,可以在后面再添加一些东西,未来再介绍

 

未完持续更新中……

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/68874.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一站式自动化测试平台-Autotestplat

3.1 自动化平台开发方案 3.1.1 功能需求 3.1.3 开发时间计划 如果是刚入门、但有一点代码基础的测试人员&#xff0c;大概 3 个月能做出演示版(Demo)进行自动化测试&#xff0c;6 个月内胜任开展工作中项目的自动化测试。 如果是有自动化测试基础的测试人员&#xff0c;大概 …

NGINX负载均衡及LVS-DR负载均衡集群

目录 LVS-DR原理搭建过程nginx 负载均衡 LVS-DR原理 原理&#xff1a; 1. 当用户向负载均衡调度器&#xff08;Director Server&#xff09;发起请求&#xff0c;调度器将请求发往至内核空间 2. PREROUTING链首先会接收到用户请求&#xff0c;判断目标IP确定是本机IP&#xff…

grafana 的 ws websocket 连接不上的解决方式

使用了多层的代理方式&#xff0c;一层没有此问题 错误 WebSocket connection to ‘wss://ip地址/grafana01/api/live/ws’ failed: 日志报错 msg“Request Completed” methodGET path/api/live/ws status403 解决方式 # allowed_origins is a comma-separated list of o…

基于Java+SpringMVC+Mybaties+layui+Vue+elememt基于协同过滤的电影推荐系统的设计与实现

一.项目介绍 基于协调过滤的电影推荐系统的设计与实现 本系统分为普通用户以及管理员两类 普通用户&#xff1a; 登录系统查看最新推荐电影、收藏、评论、查看电影信息、 对电影进行评分、浏览电影资讯信息、查看个人信息以及浏览收藏…

二.net core 自动化发布到docker (Jenkins安装之后向导)

目录 ​​​​​​​​​​​​​​ 参考资料&#xff1a;https://www.jenkins.io/doc/book/installing/docker/#setup-wizard Post-installation setup wizard.(安装后安装向导) 基于上一篇文章安装&#xff0c;在安装并运行Jenkins&#xff08;不包括使用Jenkins Opera…

同步、异步、协程

目录 同步异步https 异步请求&#xff1a; 协程1.为什么会要协程?2.异步的运行流程是什么3.协程的原语操作4.协程的定义?5.调度器的定义?6.调度的策略?7. api封装, hook8.多核的模式?9.协程的性能?10.要有哪些案例?nty_servernty_ mysql_client.cnty_ mysql oper.cnty_ …

网络编程555

上传代码 #include <stdio.h>//客户端 #include <string.h> #include <stdlib.h> #include<sys/types.h> #include<sys/socket.h> #include<arpa/inet.h> #include<head.h> #define PORT 69 #define IP "192.168.124.57"…

【C++面向对象】--- 继承 的奥秘(下篇)

个人主页&#xff1a;平行线也会相交&#x1f4aa; 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【C之路】&#x1f48c; 本专栏旨在记录C的学习路线&#xff0c;望对大家有所帮助&#x1f647;‍ 希望我们一起努力、成长&…

SpringCloud中 Sentinel 限流的使用

引入依赖 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>手动编写限流规则&#xff0c;缺点是不够灵活&#xff0c;如果需要改变限流规则需要修改源码…

【Linux】【驱动】杂项设备驱动

【Linux】【驱动】杂项设备驱动 Linux三大设备驱动1. 我们这节课要讲的杂项设备驱动是属于我们这三大设备驱动里面的哪个呢?2.杂项设备除了比字符设备代码简单&#xff0c;还有别的区别吗?3.主设备号和次设备号是什么? 挂载驱动 杂项设备驱动是字符设备驱动的一种&#xff0…

时序预测 | Python实现LSTM长短期记忆网络时间序列预测(电力负荷预测)

时序预测 | Python实现LSTM长短期记忆网络时间序列预测(电力负荷预测) 目录 时序预测 | Python实现LSTM长短期记忆网络时间序列预测(电力负荷预测)效果一览基本描述模型结构程序设计参考资料效果一览

C语言暑假刷题冲刺篇——day1

目录 一、选择题 二、编程题 &#x1f388;个人主页&#xff1a;库库的里昂 &#x1f390;CSDN新晋作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;C语言每日一练 ✨其他专栏&#xff1a;代码小游戏C语言初阶&#x1f91d;希望作者的文章能对你…