linux篇【13】:网络应用层—网络版计算器,序列化

news/2024/12/28 17:43:28/

目录

一.应用层

1.再谈 "协议"

2.序列化,反序列化

(1)序列化,反序列化的实例:

(2)自描述长度的协议

3.网络版计算器

细节(1):报头方案

(2)netCal函数步骤

(3)client.hpp中如果是quit就是直接continue检测退出。

serverTcp.cc 中的 netCal函数

 易错点:

Protocol.hpp(半成品)

二.json 序列化和反序列化

1.安装json库

2.request的jason序列化:

 Json::FastWriter与Json::StyledWriter两种显示风格

2.request的jason反序列化:

3.makefile中定义变量-D

 4.代码

Protocol.hpp

clientTcp.cc

serverTcp.cc


一.应用层

1.再谈 "协议"

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

2.序列化,反序列化

直接发送同样的结构体对象,是不可取的,虽然在某些情况下,它确实行,但是我们需要进行序列化和反序列化

定义:定义结构体来表示我们需要交互的信息; 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体; 这个过程叫做 "序列化" "反序列化"

序列化,反序列化的操作是在用户发送和网络发送中间加了一层软件层,

(1)序列化,反序列化的实例:

把客户端的结构体通过软件层变成一个字符串叫做序列化,发送给网络服务器前把字符串再转化回结构体叫做反序列化。约定这个字符串有一共三个区域,用特殊字符"\3"分割,三部分都是char类型。

(2)自描述长度的协议

这一个字符串每个部分的字符串整体的长度对方怎么知道?
——我们定制协议的时候,序列化之后,需要将长度(长度类型我们设置为4字节),将长度放入序列化之后的字符串的开始之前——自描述长度的协议! !
encode:涉及加密
decode:解密

3.网络版计算器

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算,
后再把结果返回给客户端.
约定方案一:
客户端发送一个形如"1+1"的字符串;
这个字符串中有两个操作数, 都是整形;
两个数字之间会有一个字符是运算符, 运算符只能是 + ;
数字和运算符之间没有空格;
...
约定方案二:
定义结构体来表示我们需要交互的信息;
发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转
化回结构体;
这个过程叫做 "序列化" "反序列化"

细节(1):报头方案

encode,整个序列化之后的字符串进行添加长度(\r\n是我们规定的特殊字符)
定长报头方案:strlen XXXXXXXXXX 前面长度是四字节,后面是字符串。这种方案也可以,但是都是二进制,打印不方便,可读性不好
"strlen\r\n"XXXXXXXXX\r\n --采用这种方案 

解释:那直接""XXXXXXXXX\r\n"读一行不行吗?——答:我们不知道字符串内是否包含\r\n,如果内部包含就会影响读取(比如XXXXXXXXX是123\r\n,总的是123\r\n\r\n,那我们会读到第一个\r\n结束,但实际上第一个\r\n是属于字符串内容的,就出错了),但是"strlen\r\n" 中的长度描述字符串一定不包含 \r\n ,读了字符串长度后,再从后面读这么长的字符串即可

(2)netCal函数步骤

string的rfind是从右往左找

netCal函数步骤:先创建好收集处理请求的对象Request req; 。从套接字中读数据。①检查是否已经具有了一个完整报文strPackage,不具有就continue,具有才继续执行。②读到的报文是字符串(序列化的),然后利用decode——对整个序列化之后的字符串进行提取长度。③deserialize(const std::string &in)反序列化 -- 字符串 -> 结构化的数据。④(3.)处理请求逻辑,Response resp = calculator(req);输入的是一个Request req,得到一个Response resp。⑤(4.)对resp进行序列化。⑥(5.)对报文进行encode。⑦简单进行发送

(3)client.hpp中如果是quit就是直接continue检测退出。

因为有了协议,如果是quit再往下继续处理数据会出错。

serverTcp.cc 中的 netCal函数

// 1. 全部手写
// 2. 部分采用别人的方案--序列化和反序列化的问题
void netCal(int sock, const std::string &clientIp, uint16_t clientPort)
{assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);    //因为一般1-1023端口属于系统保留端口,这些端口已经分配给一些应用了,所以我们只能使用1024及以上的端口// 9\r\n100 + 200\r\n    9\r\n112 / 200\r\nstd::string inbuffer;while (true){Request req;char buff[128];ssize_t s = read(sock, buff, sizeof(buff) - 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 successbuff[s] = 0;inbuffer += buff;// 1. 检查inbuffer是不是已经具有了一个strPackageuint32_t packageLen = 0;std::string package = decode(inbuffer, &packageLen); //TODOif (packageLen == 0) continue; // 无法提取一个完整的报文,继续努力读取吧// 2. 已经获得一个完整的packageif (req.deserialize(package)){req.debug();// 3. 处理逻辑, 输入的是一个req,得到一个respResponse resp = calculator(req); //resp是一个结构化的数据// 4. 对resp进行序列化std::string respPackage;resp.serialize(&respPackage);// 5. 对报文进行encode -- //TODOrespPackage = encode(respPackage, respPackage.size());// 6. 简单进行发送 -- 后续处理write(sock, respPackage.c_str(), respPackage.size());}}
}

 易错点:

(1)buff[s] = 0; inbuffer += buff; 遗漏

(2)decode中 in.find(CRLF) 查找失败错误遗漏

(3) decode中5. 将当前报文完整的从in中全部移除掉 遗漏

(4)这些全漏了!!!!

Protocol.hpp(半成品)

#pragma once#include <iostream>
#include <string>
#include <cassert>
// 我们要在这里进行我们自己的协议定制!
// 网络版本的计算器#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) // 坑:sizeof(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)// decode,整个序列化之后的字符串进行提取长度
// 1. 必须具有完整的长度
// 2. 必须具有和len相符合的有效载荷
// 我们才返回有效载荷和len
// 否则,我们就是一个检测函数!
// 9\r\n100 + 200\r\n    9\r\n112 / 200\r\n
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 ""; // 1234\r\nYYYYY for(int i = 3; i < 9 ;i++) [)// 2. 提取长度std::string inLen = in.substr(0, pos);int intLen = atoi(inLen.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 = inLen.size() + package.size() + 2 * CRLF_LEN;in.erase(0, removeLen);// 6. 正常返回return package;
}// encode, 整个序列化之后的字符串进行添加长度
std::string encode(const std::string &in, uint32_t len)
{// "exitCode_ result_"// "len\r\n""exitCode_ result_\r\n"std::string encodein = std::to_string(len);encodein += CRLF;encodein += in;encodein += CRLF;return encodein;
}// 定制的请求 x_ op y_
class Request
{
public:Request(){}~Request(){}// 序列化 -- 结构化的数据 -> 字符串void serialize(std::string *out){}// 反序列化 -- 字符串 -> 结构化的数据bool deserialize(const std::string &in){// 100 + 200std::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;}void debug(){std::cout << "#################################" << std::endl;std::cout << "x_: " << x_ << std::endl;std::cout << "op_: " << op_ << std::endl;std::cout << "y_: " << y_ << std::endl;std::cout << "#################################" << std::endl;}public:// 需要计算的数据int x_;int y_;// 需要进行的计算种类char op_; // + - * / %
};// 定制的响应
class Response
{
public:Response() : exitCode_(0), result_(0){}~Response(){}// 序列化void serialize(std::string *out){// "exitCode_ result_"std::string ec = std::to_string(exitCode_);std::string res = std::to_string(result_);*out = ec;*out += SPACE;*out += res;}// 反序列化void deserialize(std::string &in){}
public:// 退出状态,0标识运算结果合法,非0标识运行结果是非法的,!0是几就表示是什么原因错了!int exitCode_;// 运算结果int result_;
};

二.json 序列化和反序列化

1.安装json库

sudo yum install -y jsoncpp-devel

查看已安装的jsoncpp:

2.request的jason序列化:

// 序列化 -- 结构化的数据 -> 字符串// 认为结构化字段中的内容已经被填充void serialize(std::string *out){
#ifdef MY_SELFstd::string xstr = std::to_string(x_);std::string ystr = std::to_string(y_);// std::string opstr = std::to_string(op_); // op_ -> char -> int -> 43 ->*out = xstr;*out += SPACE;*out += op_;*out += SPACE;*out += ystr;
#else//json// 1. Value对象,万能对象// 2. json是基于KV// 3. json有两套操作方法// 4. 序列化的时候,会将所有的数据内容,转换成为字符串Json::Value root;root["x"] = x_;root["y"] = y_;root["op"] = op_;Json::FastWriter fw;// Json::StyledWriter fw;*out = fw.write(root);
#endif}

 Json::FastWriter与Json::StyledWriter两种显示风格

通常 Json::FastWriter 传输数据量较少,使用Json::StyledWriter较多

 

2.request的jason反序列化:

 // 反序列化 -- 字符串 -> 结构化的数据bool deserialize(std::string &in){
#ifdef MY_SELF// 100 + 200std::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//jsonJson::Value root;Json::Reader rd;rd.parse(in, root);    把in反序列化进root中,这里的root是输出型参数x_ = root["x"].asInt();    asInt:把root["x"]对应的KV的V值转为整数存入x_y_ = root["y"].asInt();op_ = root["op"].asInt();    没有aschar这一说,就是存入ASCII值,因为return true;                 op_是char类型,所以读取时会以char类型读取
#endif}

3.makefile中定义变量-D

-D:命令行定义宏。目的:这样就不用把宏定义在源代码中(不用动源代码了),某种宏的定义会决定条件编译对相应代码进行裁剪。 

在makefile中定义变量Method=-DMY_SELF,编译就会加上这个变量(类似于Protocol.hpp
中定义的 #define MY_SELF 1),代码中的 #ifdef MY_SELF 条件编译会起作用,此时就会执行我们自己的序列化代码;

 

 如果利用#进行注释:Method=#-DMY_SELF —> 此时Method是无内容的,此时代码中的 #ifdef MY_SELF 条件编译不起作用而会执行#else,此时就会执行jason的序列化代码;

 

 4.代码

完整版:

 lesson43/calServer · whb-helloworld/104期 - 码云 - 开源中国 (gitee.com)

Protocol.hpp

#pragma once#include <iostream>
#include <string>
#include <cassert>
#include <jsoncpp/json/json.h>
#include "util.hpp"// 我们要在这里进行我们自己的协议定制!
// 网络版本的计算器#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) // 坑:sizeof(CRLF)
#define SPACE " "
#define SPACE_LEN strlen(SPACE)#define OPS "+-*/%"// #define MY_SELF 1// decode,整个序列化之后的字符串进行提取长度
// 1. 必须具有完整的长度
// 2. 必须具有和len相符合的有效载荷
// 我们才返回有效载荷和len
// 否则,我们就是一个检测函数!
// 9\r\n100 + 200\r\n    9\r\n112 / 200\r\n
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 ""; // 1234\r\nYYYYY for(int i = 3; i < 9 ;i++) [)// 2. 提取长度std::string inLen = in.substr(0, pos);int intLen = atoi(inLen.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 = inLen.size() + package.size() + 2 * CRLF_LEN;in.erase(0, removeLen);// 6. 正常返回return package;
}// encode, 整个序列化之后的字符串进行添加长度
std::string encode(const std::string &in, uint32_t len)
{// "exitCode_ result_"// "len\r\n""exitCode_ result_\r\n"std::string encodein = std::to_string(len);encodein += CRLF;encodein += in;encodein += CRLF;return encodein;
}// 定制的请求 x_ op y_
class Request
{
public:Request(){}~Request(){}// 序列化 -- 结构化的数据 -> 字符串// 认为结构化字段中的内容已经被填充void serialize(std::string *out){
#ifdef MY_SELFstd::string xstr = std::to_string(x_);std::string ystr = std::to_string(y_);// std::string opstr = std::to_string(op_); // op_ -> char -> int -> 43 ->*out = xstr;*out += SPACE;*out += op_;*out += SPACE;*out += ystr;
#else//json// 1. Value对象,万能对象// 2. json是基于KV// 3. json有两套操作方法// 4. 序列化的时候,会将所有的数据内容,转换成为字符串Json::Value root;root["x"] = x_;root["y"] = y_;root["op"] = op_;Json::FastWriter fw;// Json::StyledWriter fw;*out = fw.write(root);
#endif}// 反序列化 -- 字符串 -> 结构化的数据bool deserialize(std::string &in){
#ifdef MY_SELF// 100 + 200std::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//jsonJson::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 << "#################################" << std::endl;std::cout << "x_: " << x_ << std::endl;std::cout << "op_: " << op_ << std::endl;std::cout << "y_: " << y_ << std::endl;std::cout << "#################################" << std::endl;}public:// 需要计算的数据int x_;int y_;// 需要进行的计算种类char op_; // + - * / %
};// 定制的响应
class Response
{
public:Response() : exitCode_(0), result_(0){}~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//jsonJson::Value root;root["exitcode"] = exitCode_;root["result"] = result_;Json::FastWriter fw;// Json::StyledWriter fw;*out = fw.write(root);
#endif}// 反序列化bool deserialize(std::string &in){
#ifdef MY_SELF// "0 100"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//jsonJson::Value root;Json::Reader rd;rd.parse(in, root);exitCode_ = root["exitcode"].asInt();result_ = root["result"].asInt();return true;
#endif}void debug(){std::cout << "#################################" << std::endl;std::cout << "exitCode_: " << exitCode_ << std::endl;std::cout << "result_: " << result_ << std::endl;std::cout << "#################################" << std::endl;}public:// 退出状态,0标识运算结果合法,非0标识运行结果是非法的,!0是几就表示是什么原因错了!int exitCode_;// 运算结果int result_;
};bool makeReuquest(const std::string &str, Request *req)
{// 123+1  1*1 1/1char strtmp[BUFFER_SIZE];snprintf(strtmp, sizeof strtmp, "%s", str.c_str());char *left = strtok(strtmp, 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;
}

clientTcp.cc

#include "util.hpp"
#include "Protocol.hpp"
#include <cstdio>
// 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!!
// 3. 需要listen吗?不需要的!
// 4. 需要accept吗?不需要的!volatile bool quit = false;static void Usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"<< std::endl;
}
// ./clientTcp serverIp serverPort
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(USAGE_ERR);}std::string serverIp = argv[1];uint16_t serverPort = atoi(argv[2]);// 1. 创建socket SOCK_STREAMint 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::cout << "info : connect success: " << sock << std::endl;std::string message;while (!quit){message.clear();std::cout << "请输入表达式>>> "; // 1 + 1 std::getline(std::cin, message); // 结尾不会有\nif (strcasecmp(message.c_str(), "quit") == 0){quit = true;continue;}// message = trimStr(message); // 1+1 1 +1 1+ 1 1+     1 1      +1 => 1+1 -- 不处理Request req;if(!makeReuquest(message, &req)) continue;// req.debug();std::string package;req.serialize(&package); // donestd::cout << "debug->serialize-> " << package << std::endl;package = encode(package, package.size()); // donestd::cout << "debug->encode-> \n" << package << std::endl;ssize_t s = write(sock, package.c_str(), package.size());if (s > 0){char buff[1024];size_t s = read(sock, buff, sizeof(buff)-1);if(s > 0) buff[s] = 0;std::string echoPackage = buff;Response resp;uint32_t len = 0;// std::cout << "debug->get response->\n" << echoPackage << std::endl;std::string tmp = decode(echoPackage, &len); // doneif(len > 0){echoPackage = tmp;// std::cout << "debug->decode-> " << echoPackage << std::endl;resp.deserialize(echoPackage);printf("[exitcode: %d] %d\n", resp.exitCode_, resp.result_);}}else if (s <= 0){break;}}close(sock);return 0;
}

serverTcp.cc

#include "Protocol.hpp"
#include "util.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"
#include "daemonize.hpp"#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>class ServerTcp; // 申明一下ServerTcp// 大小写转化服务
// TCP && UDP: 支持全双工
void transService(int sock, const std::string &clientIp, uint16_t clientPort)
{assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);char inbuffer[BUFFER_SIZE];while (true){ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); // 我们认为我们读到的都是字符串if (s > 0){// read successinbuffer[s] = '\0';if (strcasecmp(inbuffer, "quit") == 0){logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);break;}logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);// 可以进行大小写转化了for (int i = 0; i < s; i++){if (isalpha(inbuffer[i]) && islower(inbuffer[i]))inbuffer[i] = toupper(inbuffer[i]);}logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);write(sock, inbuffer, strlen(inbuffer));}else if (s == 0){// pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭// s == 0: 代表对方关闭,client 退出logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);break;}else{logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));break;}}// 只要走到这里,一定是client退出了,服务到此结束close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!logMessage(DEBUG, "server close %d done", sock);
}void execCommand(int sock, const std::string &clientIp, uint16_t clientPort)
{assert(sock >= 0);assert(!clientIp.empty());assert(clientPort >= 1024);char command[BUFFER_SIZE];while (true){ssize_t s = read(sock, command, sizeof(command) - 1); // 我们认为我们读到的都是字符串if (s > 0){command[s] = '\0';logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command);// 考虑安全std::string safe = command;if ((std::string::npos != safe.find("rm")) || (std::string::npos != safe.find("unlink"))){break;}// 我们是以r方式打开的文件,没有写入// 所以我们无法通过dup的方式得到对应的结果FILE *fp = popen(command, "r");if (fp == nullptr){logMessage(WARINING, "exec %s failed, beacuse: %s", command, strerror(errno));break;}char line[1024];while (fgets(line, sizeof(line) - 1, fp) != nullptr){write(sock, line, strlen(line));}// dup2(fd, 1);// dup2(sock, fp->_fileno);// fflush(fp);pclose(fp);logMessage(DEBUG, "[%s:%d] exec [%s] ... done", clientIp.c_str(), clientPort, command);}else if (s == 0){// pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭// s == 0: 代表对方关闭,client 退出logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);break;}else{logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));break;}}// 只要走到这里,一定是client退出了,服务到此结束close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!logMessage(DEBUG, "server close %d done", sock);
}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 '/':{ // x_ / y_if (req.y_ == 0) resp.exitCode_ = -1; // -1. 除0else resp.result_ = req.x_ / req.y_;}break;case '%':{ // x_ / y_if (req.y_ == 0) resp.exitCode_ = -2; // -2. 模0else resp.result_ = req.x_ % req.y_;}break;default:resp.exitCode_ = -3; // -3: 非法操作符break;}return resp;
} // 1. 全部手写 -- done
// 2. 部分采用别人的方案--序列化和反序列化的问题 -- xml,json,protobuf
void netCal(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\n112 / 200\r\nstd::string inbuffer;while (true){Request req;char buff[128];ssize_t s = read(sock, buff, sizeof(buff) - 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 successbuff[s] = 0;inbuffer += buff;std::cout << "inbuffer: " << inbuffer << std::endl;// 1. 检查inbuffer是不是已经具有了一个strPackageuint32_t packageLen = 0;std::string package = decode(inbuffer, &packageLen); if (packageLen == 0) continue; // 无法提取一个完整的报文,继续努力读取吧std::cout << "package: " << package << std::endl;// 2. 已经获得一个完整的packageif (req.deserialize(package)){req.debug();// 3. 处理逻辑, 输入的是一个req,得到一个respResponse 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 clinetIp_;int sock_;ServerTcp *this_;public:ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts): clientPort_(port), clinetIp_(ip), sock_(sock), this_(ts){}
};class ServerTcp
{
public:ServerTcp(uint16_t port, const std::string &ip = ""): port_(port),ip_(ip),listenSock_(-1),tp_(nullptr){quit_ = false;}~ServerTcp(){if (listenSock_ >= 0)close(listenSock_);}public:void init(){// 1. 创建socketlistenSock_ = 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, "listen: %s", strerror(errno));exit(LISTEN_ERR);}logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);// 运行别人来连接你了// 4. 加载线程池tp_ = ThreadPool<Task>::getInstance();}// static void *threadRoutine(void *args)// {//     pthread_detach(pthread_self()); //设置线程分离//     ThreadData *td = static_cast<ThreadData *>(args);//     td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_);//     delete td;//     return nullptr;// }void loop(){// signal(SIGCHLD, SIG_IGN); // only Linuxtp_->start();logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum());while (!quit_){struct sockaddr_in peer;socklen_t len = sizeof(peer);// 4. 获取连接, accept 的返回值是一个新的socket fd ??// 4.1 listenSock_: 监听 && 获取新的链接-> sock// 4.2 serviceSock: 给用户提供新的socket服务int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);if (quit_)break;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 提供服务, echo -> 小写 -> 大写// 5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept// transService(serviceSock, peerIp, peerPort);// 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的// pid_t id = fork();// assert(id != -1);// if(id == 0)// {//     close(listenSock_); //建议//     //子进程//     transService(serviceSock, peerIp, peerPort);//     exit(0); // 进入僵尸// }// // 父进程// close(serviceSock); //这一步是一定要做的!// 5.1 v1.1 版本 -- 多进程版本  -- 也是可以的// 爷爷进程// pid_t id = fork();// if(id == 0)// {//     // 爸爸进程//     close(listenSock_);//建议//     // 又进行了一次fork,让 爸爸进程//     if(fork() > 0) exit(0);//     // 孙子进程 -- 就没有爸爸 -- 孤儿进程 -- 被系统领养 -- 回收问题就交给了系统来回收//     transService(serviceSock, peerIp, peerPort);//     exit(0);// }// // 父进程// close(serviceSock); //这一步是一定要做的!// // 爸爸进程直接终止,立马得到退出码,释放僵尸进程状态// pid_t ret = waitpid(id, nullptr, 0); //就用阻塞式// assert(ret > 0);// (void)ret;// 5.2 v2 版本 -- 多线程// 这里不需要进行关闭文件描述符吗??不需要啦// 多线程是会共享文件描述符表的!// ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);// pthread_t tid;// pthread_create(&tid, nullptr, threadRoutine, (void*)td);// 5.3 v3 版本 --- 线程池版本// 5.3.1 构建任务// 5.3 v3.1// Task t(serviceSock, peerIp, peerPort, std::bind(&ServerTcp::transService, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));// tp_->push(t);// 5.3 v3.2// Task t(serviceSock, peerIp, peerPort, transService);// tp_->push(t);// 5.3 v3.3// Task t(serviceSock, peerIp, peerPort, execCommand);// tp_->push(t);// 5.4 v3.3Task t(serviceSock, peerIp, peerPort, netCal);tp_->push(t);// waitpid(); 默认是阻塞等待!WNOHANG// 方案1// logMessage(DEBUG, "server 提供 service start ...");// sleep(1);}}bool quitServer(){quit_ = true;return true;}private:// sockint listenSock_;// portuint16_t port_;// ipstd::string ip_;// 引入线程池ThreadPool<Task> *tp_;// 安全退出bool quit_;
};static void Usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl;std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n"<< std::endl;
}ServerTcp *svrp = nullptr;void sigHandler(int signo)
{if (signo == 3 && svrp != nullptr)svrp->quitServer();logMessage(DEBUG, "server quit save!");
}// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3)ip = argv[2];// daemonize(); // 我们的进程就会成为守护进程signal(3, sigHandler);// Log log;// log.enable();ServerTcp svr(port, ip);svr.init();svrp = &svr;svr.loop();return 0;
}


http://www.ppmy.cn/news/11227.html

相关文章

memcached面试专题及答案【三】

memcached 能接受的 key 的最大长度是多少&#xff1f;key 的最大长度是 250 个字符。需要注意的是&#xff0c;250 是 memcached 服务器端内部的限制&#xff0c;如果您使用的客户端支持”key 的前缀”或类似特性&#xff0c;那么 key&#xff08;前缀原始 key&#xff09;的最…

Vue 2 即将成为过去

自从 2020 年 9 月 18 日 Vue 3 正式发布以来&#xff0c;已经有两年多时间了&#xff0c;终于在 2022 年 2 月 7 日 Vue 作者发布了一则消息&#xff1a;Vue 3 将成为新的默认版本。与此同时&#xff0c;Vue 相关官方周边的核心库 latest 发布标签将指向其 Vue 3 的兼容版本。…

Springboot使用策略模式实现数据插入不同类型数据库

需求&#xff1a;前端会传来一些图片数据&#xff0c;比如图片名称&#xff0c;图片长宽、大小等。后端需要根据实际情况存入mysql、oracle、clickhouse等不同的数据库。 上面的需求是一个非常好的使用策略模式实现的例子。 Mapper层 定义一个顶级接口&#xff0c;主要定义操…

MySQL的行锁总结

文章目录前言一、行锁的介绍二、行锁的使用三、使用行锁所带来的问题四、死锁和死锁检测前言 上篇文章已经学习了MySQL的全局锁和表锁&#xff0c;今天这篇文章我们对行锁进行以下学习 一、行锁的介绍 行锁就是针对数据表中行记录的锁&#xff0c;比如事务A更新了一行&#x…

在线阅读网站|基于Springboot+Vue开发实现小说阅读网站

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容&#xff1a;Java项目、毕业设计、简历模板、学习资料、面试题库、技术互助 收藏点赞不迷路 关注作者有好处 文末获取源…

Java基础学习笔记(十二)—— 数据结构

数据结构1 栈2 队列3 数组4 链表5 二叉树5.1 二叉树5.2 二叉查找树5.3 平衡二叉树5.4 红黑树6 哈希表数据结构是计算机存储、组织数据的方式。是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下&#xff0c;精心选择的数据结构可以带来更高的运行或者存储效率。…

html实现酷炫的公司年会抽奖(附源码)

文章目录1.设计来源1.1 主界面1.2 抽奖效果1.2 中奖效果2.效果和源码配置2.1 动态效果2.2 员工信息配置2.3 奖品信息配置2.4 抽奖音效配置2.5 源代码源码下载作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/128640998 ht…

二、TTY子系统框架

个人主页&#xff1a;董哥聊技术我是董哥&#xff0c;嵌入式领域新星创作者创作理念&#xff1a;专注分享高质量嵌入式文章&#xff0c;让大家读有所得&#xff01;文章目录1、TTY子系统框架分析2、TTY数据处理流程3、驱动的目录结构及核心文件4、TTY在Linux下的分布1、TTY子系…