【网络】协议与网络版计算器

协议与网络版计算器

文章目录

  • 1.协议的概念
    • 1.1序列化与反序列化
  • 2.网络版计算器
    • 2.1封装套接字
    • 2.2协议定制
      • 2.2.1Jsoncpp
      • 2.2.2报文处理
    • 2.3会话层:TcpServer
    • 2.4应用层:Calculate
    • 2.5表示层:Service
    • 2.6应用层、表示层和会话层->应用层

1.协议的概念

为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。

通俗的讲,协议本质就是双方约定好的结构化的数据。

假设我们此时要实现一个网络版计算器,那么很明显我们需要“操作数1”、“操作符”、“操作数2”等多个信息,为了减小服务器的压力,我们一定不能将这些信息分批发送,一定是打包好成为一个数据发送给对端,那么此时就有了以下方案:

定制结构体+序列化和反序列化

  • 定制结构体来表示需要交互的信息。
  • 发送数据时将这个结构体按照一个规则转换成字符串,接收数据时再按照相同的规则把接收到的字符串转化为结构体。
  • 这个过程叫做**“序列化”“反序列化”**。

1.1序列化与反序列化

在之前实现的聊天室服务器中,用户发送数据实际上发送的不仅仅有“message”,还可能包括用户名、发送时间等信息,很明显这就是一个结构化的数据,那序列化就是将该结构化的数据转化为字符串方便网络发送,反序列化就是把信息一变多,方便上层读取处理。

QQ_1721955806551

那么readwrite或者是recvsend函数是在网络中是如何工作起来的呢?

在系统中,我们是知道readwrite函数的工作过程的,比如write函数将用户缓冲区中的数据拷贝到文件描述符所指向的文件结构体的内核缓冲区,操作系统会在合适的时间将内核缓冲区的内容刷新到磁盘上。

那在网络中也是一样的,只不过内核缓冲区变成了传输层维护的发送缓冲区和接收缓冲区(实际上也是内核级缓冲区,这块是方便理解),那么什么时候发?怎么发?出错了怎么办?这些问题就是TCP协议需要考虑解决的问题,所以TCP协议即(Transmission Control Protocol)传输控制协议,这个控制就体现在这了。

所以readwrite或者是recvsend函数本质上是拷贝函数,他们完成的工作无非就是将一块区域的数据拷贝到另一块区域,即发送数据的本质就是将自己的发送缓冲区中的数据拷贝到接收方的接收缓冲区,所以也可以说通信的本质就是拷贝,也证明双发的主机通信本质是双方操作系统在进行通信。

TCP协议支持全双工的原因也可以找到了:TCP协议拥有两块缓冲区:发送缓冲区接收缓冲区,这两块缓冲区互不干扰,仅需一个文件描述符fd就可以实现,因为写是向发送缓冲区写,读是在接收缓冲区读。

而且这些过程像极了系统中学习的管道、文件部分,比如read函数为什么会阻塞,因为接收缓冲区中无数据,所以系统对于网络学习是十分重要的。

QQ_1721955953305

但既然TCP是面向字节流的,那我们该如何解决数据传输过程中数据缺失的问题呢?毕竟在传输层眼里数据都是字节流,无法识别出字段含义的。

为什么不能将结构体直接发送呢?还需要转化成字符串发送?

  • 技术上:跨平台问题,不同操作系统结构体内存对齐方式不同,甚至可能有大小端的问题。
  • 业务上:结构体成员可能会随着业务变化而变化,如果在通信过程不转化为字符串,那么在后期维护上会面临诸多问题,每次修改都可能会牵一发而动全身。

2.网络版计算器

2.1封装套接字

首先我们先将套接字进行封装,封装的主要目的是为了简化操作,让我们仅需调用几个函数就可以完成一些对于套接字的初始化工作。

我们可以设计一个父类Socket,内部包含有若干接口,然后再根据具体套接字对父类Socket进行实现,比如TcpSocket继承父类Socket,实现父类Socket的接口。

// 模板方法模式
namespace socket_ns
{class Socket;const static int gbacklog = 8;using socket_sptr = std::shared_ptr<Socket>;enum{SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR,USAGE_ERROR};class Socket{public:virtual void CreateSocketOrDie() = 0;virtual void BindSocketOrDie(InetAddr &addr) = 0;virtual void ListenSocketOrDie() = 0;virtual socket_sptr Accepter(InetAddr *addr) = 0;virtual bool Connector(InetAddr &addr) = 0;virtual int SockFd() = 0;virtual int Recv(std::string *out) = 0;virtual int Send(const std::string &in) = 0;// virtual void Other()=0;public:void BuildListenSocket(InetAddr &addr){CreateSocketOrDie();BindSocketOrDie(addr);ListenSocketOrDie();}bool BuildClientSocket(InetAddr &addr){CreateSocketOrDie();return Connector(addr);}// void BuildUdpSocket()// {//   CreateSocketOrDie();//   BindSocketOrDie();// }};class TcpSocket : public Socket{public:TcpSocket(int fd = -1) : _sockfd(fd){}void CreateSocketOrDie() override{// 1. 创建流式套接字_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(FATAL, "socket error");exit(SOCKET_ERROR);}LOG(DEBUG, "socket create success,sockfd is : %d", _sockfd);}void BindSocketOrDie(InetAddr &addr) override{// 2. 绑定struct sockaddr_in local;            // struct sockaddr_in 系统提供的数据类型。local是变量,用户栈上开辟空间。bzero(&local, sizeof(local));        // 将从&local开始的sizeof(local)大小的内存区域置零local.sin_family = AF_INET;          // 设置网络通信方式local.sin_port = htons(addr.Port()); // port要经过网络传输给对面,所有需要从主机序列转换为网络序列local.sin_addr.s_addr = inet_addr(addr.Ip().c_str());int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "bind error");exit(BIND_ERROR);}LOG(DEBUG, "bind success,sockfd is : %d", _sockfd);}void ListenSocketOrDie() override{// 3. tcp是面向连接的,所以通信之前,必须先建立连接,服务器是被链接的//  tcpserver启动,未来首先要一直等待客户端的连接,listenint n = listen(_sockfd, gbacklog);if (n < 0){LOG(FATAL, "listen error");exit(LISTEN_ERROR);}LOG(DEBUG, "listen success,sockfd is : %d", _sockfd);}socket_sptr Accepter(InetAddr *addr) override{struct sockaddr_in peer;socklen_t len = sizeof(peer);// accept会阻塞等待,直到有客户端连接int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);if (sockfd < 0){LOG(WARNING, "accept error");return nullptr;};*addr = peer;socket_sptr sock = std::make_shared<TcpSocket>(sockfd);return sock;}bool Connector(InetAddr &addr) override{// 构建目标主机的socket信息struct sockaddr_in server;memset(&server, 0, sizeof(server)); // bzeroserver.sin_family = AF_INET;server.sin_port = htons(addr.Port());server.sin_addr.s_addr = inet_addr(addr.Ip().c_str());int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){std::cerr << "connect error" << std::endl;return false;}return true;}int Recv(std::string *out){char inbuffer[1024];ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if (n > 0){inbuffer[n] = 0;*out += inbuffer; // 为什么是+=? 后面会提到}return n;}int Send(const std::string &in){int n = ::send(_sockfd, in.c_str(), in.size(), 0);return n;}int SockFd() override{return _sockfd;}private:int _sockfd;};
}

2.2协议定制

为了保证通信双方能够识别发送接收的数据,这里需要进行协议定制,即设计发送数据结构体、接收数据结构体、包括序列化和反序列化的方案。

请求结构体中需要包括两个操作数,以及对应需要进行的操作。响应结构体中需要包括一个计算结果,除此之外,响应结构体中还需要包括一个状态字段,表示本次计算的状态,因为客户端发来的计算请求可能是无意义的,比如除0。

规定状态字段对应的含义:

  • 状态字段为0,表示计算成功。
  • 状态字段为1,表示出现除0错误。
  • 状态字段为2,表示出现模0错误。
  • 状态字段为3,表示非法计算。

上述我们提到过数据的发送取决于TCP协议,即我们只管将数据通过write函数将数据拷贝到发送缓冲区,剩下的发送工作由传输控制协议TCP完成,那么如何判断从接收缓冲区中读取到的数据是否准确完整呢?

我们知道一个完整的报文应该包含报头和有效载荷。

即:

// "有效载荷的长度"\r\n"有效载荷"\r\n
// "len"\r\n"_x _op _y"\r\n  -> len: 有效载荷的长度,约定\r\n是分隔符,不参与统计

也就是说当我们识别到\r\n的时候,我们一定可以得到有效载荷的长度,得到了有效载荷的长度我们就可以根据该长度判断是否得到了完整的报文。

当然这里有效载荷即序列化反序列化的方案我们采用开源的JSON方案。


2.2.1Jsoncpp

Jsoncpp是一个用于处理 JSON 数据的 C++ 库。 它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。 Jsoncpp是开源的, 广泛用于各种需要处理 JSON 数据的 C++ 项目中。

这里我们简要说明如何使用Jsoncpp完成序列化和反序列化的工作。

假设我们设计了一个数据结构为:

struct stu
{std::string name;int age;double weight;public:void debug(){std::cout << name << std::endl;std::cout << age << std::endl;std::cout << weight << std::endl;}
};

(1)序列化

序列化是将数据结构转化为字符串,在序列化之前我们必须先创建一个Json::Value对象,并将数据结构中的各个成员变量的值赋给该对象,即将C++数据结构转化为Json数据

就像这样:

struct stu zs = {"张三", 18, 70};
Json::Value root;
root["name"] = zs.name;
root["age"] = zs.age;
root["weight"] = zs.weight;

之后我们需要再创建一个Json::StyledWriter对象或者Json::FastWriter对象,这两个对象简要的说就是决定了转化出来的字符串格式。

比如对于Json::StyledWriter对象来说,转化出来的字符串是这样的:

{"age" : 18,"name" : "张三","weight" : 70
}

对于Json::FastWriter对象来说,转化出来的字符串是这样的:

{"age":18,"name":"张三","weight":70}

Json::FastWriter优点就是比StyledWriter更快,因为它不添加额外的空格和换行符。

言归正传,得到Json::FastWriter对象后,我们可以调用该对象的write方法,该方法参数为Json::Value对象,返回转化后的字符串。

完整示例:

#include <iostream>
#include <string>
#include <fstream>
#include <jsoncpp/json/json.h>
int main()
{// 结构化数据struct stu zs = {"张三", 18, 70};// 转换成为字符串Json::Value root;root["name"] = zs.name;root["age"] = zs.age;root["weight"] = zs.weight;// root["self"] = root;Json::FastWriter writer;// Json::StyledWriter writer;std::string str = writer.write(root);std::ofstream out("out.txt");if (!out.is_open()){std::cout << str;return 1;}out << str;out.close();return 0;
}

(2)反序列化

反序列化是将字符串转化为数据结构,同样的我们需要创建两个对象:Json::Value对象用于接收字符串中的信息,Json::Reader

parse方法用于将字符串中的数据赋给Json::Value对象,第一个参数为json字符串,第二个参数为需要赋给的Json::Value对象,返回值为成功或者失败,就像这样:

std::string json_string = buffer;
Json::Value root;
Json::Reader reader;
bool res = reader.parse(json_string, root);

需要注意的是Json::Value对象还需要转化为具体的数据结构,在转化时我们需要指明Json::Value对象成员的属性,就像这样:

struct stu zs;
zs.name = root["name"].asString();
zs.age = root["age"].asInt();
zs.weight = root["weight"].asDouble();

完整示例:

int main()
{std::ifstream in("out.txt");if (!in.is_open())return 1;char buffer[1024];in.read(buffer, sizeof(buffer));in.close();std::string json_string = buffer;Json::Value root;Json::Reader reader;bool res = reader.parse(json_string, root);(void)res;struct stu zs;zs.name = root["name"].asString();zs.age = root["age"].asInt();zs.weight = root["weight"].asDouble();zs.debug();return 0;
}

所以根据Jsoncpp,我们可以实现对请求响应的序列化和反序列化。

2.2.2报文处理

然后更为重要的是我们如何保证读取到的内容是完整的报文,上面提到过:添加报头,根据报头的长度信息判断,也就是说完整的报文应该是如下结构:

len\r\n{"age":18,"name":"张三","weight":70}\r\n

所以该协议定制我们还要实现两个方法:

  1. Encode添加报头
  2. Decode判断完整报文并将该报文从缓冲区中清除

协议完整代码如下:

#pragma once
// protocol协议
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>namespace protocol_ns
{const std::string SEP = "\r\n";// 添加报头和分隔符,将json串变为完整的报文格式"len"\r\n"{             }"std::string Encode(const std::string &json_str){int json_str_len = json_str.size();std::string proto_str = std::to_string(json_str_len);proto_str += SEP;proto_str += json_str;proto_str += SEP;return proto_str;}// "len"\r\n"{// "len"\r\n"{             }"// "len"\r\n"{             }"\r\n;// "len"\r\n"{             }"\r\n"len";// "len"\r\n"{             }"\r\n"len"\r\n"{             }";// "len"\r\n"{             }"\r\n"len"\r\n"{             }"\r\n// 判断完整报文并将该报文从缓冲区中清除std::string Decode(std::string &inbuffer){auto pos = inbuffer.find(SEP);if (pos == std::string::npos)return std::string();std::string len_str = inbuffer.substr(0, pos);if (len_str.empty())return std::string();int packlen = std::stoi(len_str);// 计算报文总长int total = packlen + len_str.size() + 2 * SEP.size();if (inbuffer.size() < total)return std::string();std::string package = inbuffer.substr(pos + SEP.size(), packlen);inbuffer.erase(0, total); // 从缓冲区中删掉该报文return package;}//请求class Request{public:Request(){}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}bool Serialize(std::string *out) // 序列化{Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;*out = writer.write(root);return true;}bool Deserialize(const std::string &in) // 反序列化{Json::Value root;Json::Reader reader;bool res = reader.parse(in, root);if (!res)return false;_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}public:int _x;int _y;char _oper; // "+-*/%" _x _oper _y}; // --- "字符串"//响应class Response{public:Response(){}Response(int result, int code) : _result(result), _code(code){}bool Serialize(std::string *out){Json::Value root;root["result"] = _result;root["code"] = _code;Json::FastWriter writer;*out = writer.write(root);return true;}bool Deserialize(const std::string &in){Json::Value root;Json::Reader reader;bool res = reader.parse(in, root);if (!res)return false;_result = root["result"].asInt();_code = root["code"].asInt();return true;}public:int _result; // 结果int _code;   // 0:success 1: 除0 2: 非法操作 3. 4. 5}; // --- "字符串"//构建请求与响应,客户端用class Factory{public:Factory(){srand(time(nullptr) ^ getpid());opers = "+-*/%^&|";}std::shared_ptr<Request> BuildRequest(){int x = rand() % 10 + 1;usleep(x * 10);int y = rand() % 5; // [01,2,3,4]usleep(y * x * 5);char oper = opers[rand() % opers.size()];std::shared_ptr<Request> req = std::make_shared<Request>(x, y, oper);return req;}std::shared_ptr<Response> BuildResponse(){return std::make_shared<Response>();}~Factory(){}private:std::string opers;};
}

2.3会话层:TcpServer

我们利用封装好的Socket中的BuildListenSocket()方法,实现创建初始化服务器,即TcpServer的构造函数,初始化完成后我们需要让服务器不断循环处理业务逻辑,同样的利用Socket中封装的Accepter()方法用来获取客户端的链接,得到本次提供服务的套接字,将该套接字作为线程参数传递给多线程处理,任务利用包装器封装为io_service_t由外部传递。

由于任务是由外部传递的,所以我们就完成了TcpServer类与具体业务的解耦,将来只需要改变传递进去的任务就可以改变TcpServer的业务逻辑。

换句话说TcpServer此时就是OSI七层模型中**“会话层”**的实现,它仅负责通信管理、负责建立和断开通信连接(数据流动的逻辑通路)。

TcpServer代码:

using namespace socket_ns;
class TcpServer; // 声明using io_service_t = std::function<void(socket_sptr sockfd, InetAddr client)>;class ThreadData
{public:ThreadData(socket_sptr fd, InetAddr addr, TcpServer *s) : sockfd(fd), clientaddr(addr), self(s){}public:socket_sptr sockfd;InetAddr clientaddr;TcpServer *self;
};class TcpServer
{public:TcpServer(int port, io_service_t service): _localaddr("0", port), _listensock(std::make_unique<TcpSocket>()), _service(service), _isrunning(false){_listensock->BuildListenSocket(_localaddr);}static void *HandlerSock(void *args) // IO和业务进行解耦合{pthread_detach(pthread_self()); // 线程分离ThreadData *td = static_cast<ThreadData *>(args);// 需要调用Service函数,但是Service函数是类内函数,静态成员函数没有this指针无法调用,如何解决?// 将this指针设为ThreadData的类内成员,再通过这个this调用Servicetd->self->_service(td->sockfd, td->clientaddr);::close(td->sockfd->SockFd()); // 文件描述符泄露delete td;return nullptr;}void Loop(){_isrunning = true;// 4. 不能直接接收数据,先获取连接while (_isrunning){InetAddr peeraddr;socket_sptr normalsock = _listensock->Accepter(&peeraddr);if (normalsock == nullptr)continue;// version 2 :采用多线程pthread_t t;ThreadData *td = new ThreadData(normalsock, peeraddr, this);pthread_create(&t, nullptr, HandlerSock, td);}_isrunning = false;}~TcpServer(){}private:InetAddr _localaddr;std::unique_ptr<Socket> _listensock;bool _isrunning;io_service_t _service;
};

2.4应用层:Calculate

具体业务是什么呢?网络版计算器。

非常简单,只需要完成相应的计算并返回结果和状态码即可。

using namespace protocol_ns;class Calculate
{public:Calculate(){}Response Excute(const Request &req){Response resp(0, 0);switch (req._oper){case '+':resp._result = req._x + req._y;break;case '-':resp._result = req._x - req._y;break;case '*':resp._result = req._x * req._y;break;case '/':{if (req._y == 0){resp._code = 1;}else{resp._result = req._x / req._y;}}break;case '%':{if (req._y == 0){resp._code = 2;}else{resp._result = req._x % req._y;}}break;default:resp._code = 3;break;}return resp;}~Calculate(){}private:
};

2.5表示层:Service

以服务器收到请求并响应这一过程举例,完整的流程应为:

  1. 读取数据
  2. 分析数据,获取完整报文
  3. 反序列化
  4. 业务处理
  5. 对响应进行序列化
  6. 对响应添加报头
  7. 发送数据

这其实就是OSI七层模型中**“表示层”**的实现,表示层负责设备固有数据格式和网络标准数据格式的转换。表示层就是协议。

序列化、反序列化、添加报头、分析数据这些都是数据格式的转换工作。

具体用代码体现就是这样:

using namespace protocol_ns;void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " local_port\n"<< std::endl;
}using callback_t = std::function<Response(const Request &req)>;class Service
{public:Service(callback_t cb) : _cb(cb){}void ServiceHelper(socket_sptr sockptr, InetAddr client){int sockfd = sockptr->SockFd();LOG(DEBUG, "get a new link ,info %s:%d,fd:%d", client.Ip(), client.Port(), sockfd);std::string clientaddr = "[" + client.Ip() + ":" + std::to_string(client.Port()) + "] ";std::string inbuffer;while (true){sleep(5); // 测试用,人为的让服务端先不处理,积压一部分请求,测试服务器是否能够解决TCP粘包问题Request req;// 1.读取数据int n = sockptr->Recv(&inbuffer); // 如何保证读到的是一个完整的请求?if (n < 0){LOG(DEBUG, "client %s quit", clientaddr.c_str());break;}// 2.分析数据,获取完整报文std::string package;while (true){sleep(1);std::cout << "服务器未处理的请求: " << inbuffer << std::endl;package = Decode(inbuffer);if (package.empty())break; // 证明此时没有一个完整的报文继续读取,这也是为什么Socket类中Recv接口中*out+=inbuffer;是+=的原因// 代码执行到这一定有完整json字符串std::cout << "----------------------begin----------------------" << std::endl;std::cout << "请求json字符串:\n"<< package << std::endl;// 3.反序列化req.Deserialize(package);// 4.业务处理Response resp = _cb(req);// 5.对响应序列化std::string send_str;resp.Serialize(&send_str);std::cout << "响应序列化:" << std::endl;std::cout << send_str << std::endl;// 6.添加长度报头send_str = Encode(send_str);std::cout << "响应完整报文:" << std::endl;std::cout << send_str << std::endl;sockptr->Send(send_str); // 本次不对发送做处理 EPOLL}}}private:callback_t _cb;
};

客户端完整流程与服务端类似,只不过反过来,流程如下:

  1. 构建请求
  2. 对请求进行序列化
  3. 添加报头
  4. 发送数据
  5. 读取服务器响应
  6. 分析数据,获取完整报文
  7. 反序列化

具体代码实现:

void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);InetAddr serveraddr(serverip, serverport);Factory factory;std::unique_ptr<Socket> cli = std::make_unique<TcpSocket>();bool res = cli->BuildClientSocket(serveraddr);std::string inbuffer;while (res){sleep(1);std::string str;// 一次构建五个请求,测试服务器对积压请求处理for (int i = 0; i < 5; i++){// 1.构建一个请求auto req = factory.BuildRequest();// 2. 对请求进行序列化std::string send_str;req->Serialize(&send_str);std::cout << "请求序列化: \n"<< send_str << std::endl;// 3. 添加长度报头send_str = Encode(send_str);std::cout << "请求完整报文: \n"<< send_str << std::endl;str += send_str;}// 4. "len"\r\n"{}"\r\ncli->Send(str);// 5. 读取服务器响应int n = cli->Recv(&inbuffer);if (n <= 0)break;std::string package = Decode(inbuffer);if (package.empty())continue;// 6. 我能保证package一定是一个完整的响应!auto resp = factory.BuildResponse();// 6.1 反序列化resp->Deserialize(package);// 7. 拿到了结构化的响应std::cout << "计算结果: " << resp->_result << "[" << resp->_code << "]" << std::endl;}return 0;
}

2.6应用层、表示层和会话层->应用层

接下来我们需要将代码捏合在一起,我们通过bind将旧的可调用对象捆绑新的参数成为新的可调用对象层层传递,OSI七层模型的应用层、表示层和会话层全部统称为应用层的原因就是所有的这三层需要实现的全部都由用户自己实现自己定义,就好比用户需要决定报文以什么标准传递,是“xml”还是“json”,网络传输协议是Tcp协议还是Udp协议,这些都需要用户自己决定,所以这三层我们合并为一层应用层。

// ./tcpserver port
// 云服务器的port默认都是禁止访问的。云服务器放开端口8080 ~ 8085
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);// exit(USAGE_ERROR);return 1;}uint16_t port = std::stoi(argv[1]);Calculate cal;                                                                  // 应用层Service calservice(std::bind(&Calculate::Excute, &cal, std::placeholders::_1)); // 表示层io_service_t service = std::bind(&Service::ServiceHelper, &calservice, std::placeholders::_1, std::placeholders::_2);std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, service); // 会话层tsvr->Loop();return 0;
}

你与自己的关系,会奠定下你与其他所有关系的基石。 —罗伯特·霍尔登
参数成为新的可调用对象层层传递,OSI七层模型的应用层、表示层和会话层全部统称为应用层的原因就是所有的这三层需要实现的全部都由用户自己实现自己定义,就好比用户需要决定报文以什么标准传递,是“xml”还是“json”,网络传输协议是Tcp协议还是Udp协议,这些都需要用户自己决定,所以这三层我们合并为一层应用层。

// ./tcpserver port
// 云服务器的port默认都是禁止访问的。云服务器放开端口8080 ~ 8085
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);// exit(USAGE_ERROR);return 1;}uint16_t port = std::stoi(argv[1]);Calculate cal;                                                                  // 应用层Service calservice(std::bind(&Calculate::Excute, &cal, std::placeholders::_1)); // 表示层io_service_t service = std::bind(&Service::ServiceHelper, &calservice, std::placeholders::_1, std::placeholders::_2);std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, service); // 会话层tsvr->Loop();return 0;
}

你与自己的关系,会奠定下你与其他所有关系的基石。 —罗伯特·霍尔登

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

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

相关文章

PHP企业培训考试系统小程序源码

&#x1f680;企业培训考试系统&#xff0c;赋能员工成长新引擎&#x1f4da; &#x1f331; 开篇&#xff1a;解锁企业培训新篇章 在快速变化的商业环境中&#xff0c;员工的能力提升是企业持续发展的关键。&#x1f680; 传统的培训方式已难以满足现代企业的需求&#xff0…

获取客户端真实IP

出于安全考虑&#xff0c;近期在处理一个记录用户真实IP的需求。本来以为很简单&#xff0c;后来发现没有本来以为的简单。这里主要备忘下&#xff0c;如果服务器处于端口回流&#xff08;hairpin NAT&#xff09;,keepalived&#xff0c;nginx之后&#xff0c;如何取得客户端的…

【5G NAS】全球唯一临时标识符GUTI介绍

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G技术研究。 博客内容主要围绕…

通过python搭建文件传输服务器;支持多台电脑之间互相传输文件(支持局域网或广域网)(应该也能用于虚拟机和宿主机之间)

因为公司网络防火墙限制,所以在公司的电脑之间传输文件还是非常不方便的;所以自己搭建了一个文件传输服务器,用于多台电脑间的文件传输; 先放上最终效果: 文章目录 一、运行环境要求二、环境搭建2.1 安装python2.2 搭建虚拟环境方法1:创建Anaconda虚拟环境方法2:创建pyt…

行业大模型——详细介绍

行业垂类模型 行业垂类模型是指针对特定行业或领域而设计的人工智能模型&#xff0c;它们通过大量行业数据的训练&#xff0c;具备较高的专业性和针对性&#xff0c;能够更好地解决行业内的特定问题。以下是一个详细的构建行业垂类模型的步骤&#xff1a; 行业垂类模型的需求分…

【STM32】USART串口和I2C通信

个人主页~ USART串口和I2C通信 USART串口一、串口1、简介2、电路要求3、参数及时序 二、USART外设1、USART结构2、波特率发生器 三、数据包1、HEX数据包HEX数据包接收 2、文本数据包文本数据包接收 I2C通信一、简介二、通信协议1、硬件电路2、I2C时序基本单元 三、I2C外设1、简…

ST-LINK烧录MCU

打开ST-LINK软件&#xff1a; 主板断电状态下接入烧录器&#xff0c;烧录器USB连接电脑&#xff1a; 主板上电&#xff0c;点击连接按钮&#xff1a; 点击加载文件&#xff1a; 点击写入按钮&#xff0c;烧录成功后拔掉烧录器&#xff0c;主板重新上电

k8s使用kustomize来部署应用

k8s使用kustomize来部署应用 本文主要是讲述kustomzie的基本用法。首先&#xff0c;我们说一下部署文件的目录结构。 ./ ├── base │ ├── deployment.yaml │ ├── kustomization.yaml │ └── service.yaml └── overlays└── dev├── kustomization.…

C基础练习(学生管理系统)

1.系统运行&#xff0c;打开如下界面。列出系统帮助菜单&#xff08;即命令菜单&#xff09;&#xff0c;提示输入命令 2.开始时还没有录入成绩&#xff0c;所以输入命令 L 也无法列出成绩。应提示“成绩表为空&#xff01;请先使用命令 T 录入学生成绩。” 同理&#xff0c;当…

设计模式- 数据源架构模式

数据映射器&#xff08;Data mapper&#xff09; 在保持对象和数据库彼此独立的情况下&#xff0c;在二者之间移动数据的一个映射器层 数据映射器是分离内存对象域数据库的一个软件层。其职责是在内存对象与数据库之间传递数据并保持它们彼此独立。 运行机制 分离领域和数据源…

HVV小科普:蓝方是什么?

正文共&#xff1a;12345 字 19 图&#xff0c;预估阅读时间&#xff1a;9 分钟 网络实战攻防演习&#xff0c;俗称“护网”、“HW”等&#xff0c;是指模拟真实网络环境中的攻击和防御行为&#xff0c;旨在提高网络安全防护能力和应急响应能力。这种演习通常由安全团队、军事组…

ASP.NET Core 基础 - 入门实例

一. 下载 1. 下载vs2022 Visual Studio 2022 IDE - 适用于软件开发人员的编程工具 (microsoft.com) 学生,个人开发者选择社区版就行,免费的. 安装程序一直下一步下一步就行,别忘了选择安装位置,如果都放在C盘的话,就太大了. 2. 选择工作负荷 准备工作完成 二. 创建新项目 三…

数据结构复杂度

文章目录 一. 数据结构前言1.1 数据结构1.2 算法 二. 算法效率2.1 时间复杂度2.1.1 T(N)函数式2.1.2 大O的渐进表示法 一. 数据结构前言 1.1 数据结构 什么是数据结构呢&#xff1f;打开一个人的主页&#xff0c;有很多视频&#xff0c;这是数据&#xff08;杂乱无章&#xf…

嵌入式学习day12(LinuxC高级)

由于C高级部分比较零碎&#xff0c;各部分之间没有联系&#xff0c;所以学起来比较累&#xff0c;多练习就好了 一丶Linux起源 寻科普|第二期:聊聊Linux的前世今生 UNIX和linux的区别&#xff1a; &#xff08;1&#xff09;linux是开发源代码的自由软件&#xff0e;而unix是…

Python学习(2):在单机机器学习,使用Dask实现鸢尾数据集 Iris 的分类任务

目录 一、源码来源 二、鸢尾花数据集的品种分类 1、数据处理步骤 &#xff08;1&#xff09;数据集加载 &#xff08;2&#xff09;准备特征和标签 &#xff08;3&#xff09;训练集和测试集划分 2、安装必需的软件包 3、运行程序 三、信用卡欺诈数据集检测信用卡交易…

【VScode】如何在anaconda虚拟环境中打开vscode项目

文章目录 【必备知识】打开anaconda虚拟环境切换到项目工作目录激活anaconda虚拟路径让vscode从当前目录打开 【必备知识】 anaconda环境变量配置及配置python虚拟环境 https://blog.csdn.net/xzzteach/article/details/140621596 打开anaconda虚拟环境 切换到项目工作目录 …

LabVIEW液压传动系统

开发了一种高效的液压传动系统&#xff0c;其特点在于采用LabVIEW软件与先进的硬件配合&#xff0c;实现能量的有效回收。此系统主要应用于工业机械中&#xff0c;如工程机械和船机械等&#xff0c;通过优化液压泵和马达的测试台设计&#xff0c;显著提高系统的能效和操作性能。…

SpringBoot 集成 Sharding-JDBC 实现读写分离、分库分表

文章目录 一、Sharding-JDBC的应用场景二、SpringBoot 集成 Sharding-JDBC2.1、前期准备2.2、导入pom.xml依赖包2.3、结构代码实现2.3.1、MybatisPlusConfig&#xff08;分页插件&#xff09;2.3.2、TOrder&#xff08;订单对象&#xff09;2.3.3、TOrderMapper&#xff08;订单…

一样都是虚拟化技术,堆叠和M-LAG差异在哪?

号主&#xff1a;老杨丨11年资深网络工程师&#xff0c;更多网工提升干货&#xff0c;请关注公众号&#xff1a;网络工程师俱乐部 早上好&#xff0c;我的网工朋友。 随着信息技术的快速发展&#xff0c;网络架构也在不断地演进以满足日益增长的需求。 其中&#xff0c;虚拟化技…

没有mac电脑ios上架截屏截图的最新方法

很多人使用uniapp或其他跨平台框架开发ios的app&#xff0c;上架的时候都会遇到一个问题&#xff0c;上架的时候需要各种尺寸的设备来做ios截屏&#xff0c;比如目前最新的要求是&#xff0c;需要对6.7寸、6.5寸和5.5寸的iphone进行截屏&#xff0c;假如支持ipad则还需要对ipad…