2026/4/6 7:28:45
网站建设
项目流程
常州网站定制,做渠道该从哪些网站入手,建设网站找网络公司,python可以做网站模板吗首先我们要知道#xff0c;在之前的Socket编程学习中#xff0c;我们通过 socket API 实现了简单的字符串发送和接收#xff0c;即EchoServer。但在实际的开发场景中#xff0c;我们需要传输的是“结构化的数据”。socket API 本质上是面向字节流的#xff0c;它并不理解什…首先我们要知道在之前的Socket编程学习中我们通过socketAPI 实现了简单的字符串发送和接收即EchoServer。但在实际的开发场景中我们需要传输的是“结构化的数据”。socketAPI 本质上是面向字节流的它并不理解什么是“结构体”或“类” 。因此我们需要在应用层解决如何把业务数据在“结构体”和“网络字节流”之间进行转换的问题这就是自定义协议与序列化的由来。一、什么是“协议”所谓的“协议”本质上就是通信双方约定好的结构化数据。比如我们要实现一个网络计算器客户端需要把12发给服务端。方案一直接发送字符串12。这就需要约定好格式数字间用运算符隔开没有空格等 。方案二定义一个结构体来表示交互信息 。无论采用哪种方案只要保证一端发送的数据另一端能够按照约定的规则正确解析这就是应用层协议。二、序列化与反序列化为了在网络上传输结构化的数据我们需要进行转化序列化 (Serialization)发送数据时将内存中的结构体/对象按照既定规则转换成“字节流”或“字符串”的过程 。反序列化 (Deserialization)接收数据时将收到的“字节流”或“字符串”按照相同规则还原回结构体/对象的过程 。序列化和反序列化的目的在网络传输时序列化目的是为了方便网络数据的发送和接收无论是何种类型的数据经过序列化后都变成了二进制序列此时底层在进行网络数据传输时看到的统一都是二进制序列。序列化后的二进制序列只有在网络传输时能够被底层识别上层应用是无法识别序列化后的二进制序列的因此需要将从网络中获取到的数据进行反序列化将二进制序列的数据转换成应用层能够识别的数据格式。我们可以认为网络通信和业务处理处于不同的层级在进行网络通信时底层看到的都是二进制序列的数据而在进行业务处理时看得到则是可被上层识别的数据。如果数据需要在业务处理和网络通信之间进行转换则需要对数据进行对应的序列化或反序列化操作。这个过程使得上层业务逻辑不需要关心底层的网络字节流细节只需处理结构体即可 。三、重新理解 TCP 通信与 IO 系统调用在编写协议处理代码前必须深入理解read、write、recv、send这些系统调用的本质。本质是拷贝当我们调用write(sockfd, buffer, ...)时并不是直接把数据发到了网络上而是将数据从用户层缓冲区拷贝到了内核层的 TCP 发送缓冲区。同理read也是从内核的接收缓冲区拷贝数据到用户层 。全双工的物理基础TCP 支持全双工通信同时收发是因为在操作系统内核中一个 TCP 连接既有发送缓冲区又有接收缓冲区。TCP 协议传输控制协议负责决定什么时候发数据、发多少、出错重传等细节 。四、解决“粘包”问题定制协议报文TCP 是面向字节流的它没有“报文”的概念。如果不做处理接收端可能会一次读到半个请求或者一次读到两个半请求即粘包问题。我们需要在应用层明确报文的边界。常见的自定义协议格式如下协议头 有效载荷有效载荷长度\r\n(分隔符) 有效载荷内容\r\n例如要发送 JSON 字符串{x:1, y:2}封装后的报文可能是16\r\n{x:1, y:2}\r\n编码与解码实现在代码实现中我们需要处理字节流缓冲区Encode (打包) 计算消息长度拼接字符串len \r\n message \r\n。Decode (解包) 这是一个循环处理的过程因为缓冲区里可能包含多条消息或不完整的消息查找第一个分隔符\r\n的位置提取出长度len。计算一条完整报文需要的总长度total len.size() message_len 2 * Sep.size()。判断缓冲区剩余数据是否足够total。如果不够说明报文不完整返回等待新数据 。如果足够根据长度截取出一个完整的message并从缓冲区中移除已处理的字节 。五、使用 Jsoncpp 库在实际开发中我们很少手写二进制序列化而是使用成熟的序列化方案如JSON。安装sudo apt-get install libjsoncpp-dev # Ubuntu sudo yum install jsoncpp-devel # CentOS核心操作Json::Value这是最核心的类它可以表示 JSON 中的对象、数组、字符串、数字等。用法类似于std::map。使用示例Json::Value root; root[datax] 10; root[oper] ; // 支持自动类型转换序列化Json::StyledWriter/toStyledString(): 生成带缩进、格式好看的字符串适合调试 。Json::FastWriter: 生成紧凑的字符串去掉了空格换行体积小适合网络传输 。使用示例Json::FastWriter writer; std::string s writer.write(root); // 输出: {datax:10,oper:43}反序列化使用Json::Reader将字符串解析回Json::Value对象 。使用示例Json::Reader reader; Json::Value root; if (reader.parse(json_string, root)) { int x root[datax].asInt(); // 提取数据 char op root[oper].asInt(); }六、实战用例结合上述讲的几点我们来构建一个网络版本的计算器加强我们的理解。服务端代码首先我们需要对服务器进行初始化调用socket函数创建套接字。调用bind函数为服务端绑定一个端口号。调用listen函数将套接字设置为监听状态。初始化完服务器后就可以启动服务器了服务器启动后要做的就是不断调用accept函数从监听套接字当中获取新连接每当获取到一个新连接后就创建一个新线程让这个新线程为该客户端提供计算服务。TcpServer.hpp:#pragma once #include signal.h #include functional #include InetAddr.hpp #include Socket.hpp using callback_t std::functionstd::string(std::string ); class Tcpserver { public: Tcpserver(uint16_t port, callback_t cb) : _port(port), _listensocket(std::make_uniqueTcpSocket()), _cb(cb) { _listensocket-BuildListenSocketMethod(_port); } void HandlerRequest(std::shared_ptrSocket sockfd, InetAddr addr) { std::string inbuffer; while (true) { ssize_t n sockfd-Recv(inbuffer, 100); if (n 0) { LOG(LogLevel::INFO) addr.ToString() # inbuffer; // 处理收到的数据 std::string ret_str _cb(inbuffer); // 检查 序列化 解包 if (ret_str.empty()) // 空串返回 continue; sockfd-Send(ret_str); } else if (n 0) { LOG(LogLevel::INFO) client addr.ToString() quit, close sockfd: sockfd-GetSockFd(); break; } else { LOG(LogLevel::WARNING) read client addr.ToString() error, sockfd: sockfd-GetSockFd(); break; } } sockfd-CloseSocket(); } void Run() { signal(SIGCHLD, SIG_IGN); while (true) { // 方法 1 InetAddr clientaddr; auto sockfd _listensocket-Accept(clientaddr); if (sockfd nullptr) { // Accept 失败如临时资源不足或信号打断避免忙循环 sleep(1); continue; } LOG(LogLevel::INFO) 获取新链接成功, sockfd is : sockfd-GetSockFd() client addr: clientaddr.ToString(); // 方法 2 // std::string *peerip; // uint16_t *peerport; // _listensocket-AcceptConnection(peerip, peerport); if (fork() 0) { // 子进程 _listensocket-CloseSocket(); HandlerRequest(sockfd, clientaddr); exit(0); } sockfd-CloseSocket(); } } ~Tcpserver() { } private: uint16_t _port; std::unique_ptrSocket _listensocket; callback_t _cb; };上述的Socket.hppInetAddr.hpp是对套接字、IP、端口号的封装具体封装如下Socket.hpp:#pragma once #include iostream #include string #include cstring #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include unistd.h #include memory #include Logger.hpp #include Comm.hpp #include InetAddr.hpp static const int defaultsockfd -1; #define Convert(addrptr) ((struct sockaddr *)addrptr) static const int gbacklog 5; // 封装⼀个基类Socket接⼝类 // 设计模式模版⽅法类 class Socket { public: virtual ~Socket() {} virtual void CreateSocketOrDie() 0; virtual void BindSocketOrDie(uint16_t port) 0; virtual void ListenSocketOrDie(int backlog) 0; virtual std::unique_ptrSocket AcceptConnection(std::string *peerip, uint16_t *peerport) 0; virtual std::shared_ptrSocket Accept(InetAddr *addr) 0; virtual bool ConnectServer(std::string serverip, uint16_t serverport) 0; virtual int GetSockFd() 0; virtual void SetSockFd(int sockfd) 0; virtual void CloseSocket() 0; virtual bool Recv(std::string *buffer, int size) 0; virtual void Send(const std::string send_str) 0; // TODO public: void BuildListenSocketMethod(uint16_t port, int backlog gbacklog) { CreateSocketOrDie(); BindSocketOrDie(port); ListenSocketOrDie(backlog); } bool BuildConnectSocketMethod(std::string serverip, uint16_t serverport) { CreateSocketOrDie(); return ConnectServer(serverip, serverport); } void BuildNormalSocketMethod(int sockfd) { SetSockFd(sockfd); } }; class TcpSocket : public Socket { public: TcpSocket(int sockfd defaultsockfd) : _sockfd(sockfd) { } ~TcpSocket() { } void CreateSocketOrDie() override { _sockfd socket(AF_INET, SOCK_STREAM, 0); if (_sockfd 0) { LOG(LogLevel::FATAL) create tcp socket error; exit(SOCK_CREATE_ERROR); } LOG(LogLevel::INFO) create tcp socket success; } void BindSocketOrDie(uint16_t port) override { struct sockaddr_in local; memset(local, 0, sizeof(local)); local.sin_family AF_INET; local.sin_addr.s_addr INADDR_ANY; local.sin_port htons(port); // InetAddr lc(port); // int n bind(_sockfd, lc.Addr(), lc.Length()); int n bind(_sockfd, Convert(local), sizeof(local)); if (n 0) { LOG(LogLevel::FATAL) bind socker error; exit(SOCK_BIND_ERROR); } LOG(LogLevel::INFO) bind socker success; } void ListenSocketOrDie(int backlog) override { int n listen(_sockfd, backlog); if (n 0) { LOG(LogLevel::FATAL) listen socket error; exit(SOCK_LISTEN_ERROR); } LOG(LogLevel::INFO) listen socket success; } std::shared_ptrSocket Accept(InetAddr *addr) { struct sockaddr_in peer; socklen_t len sizeof(peer); int newsockfd accept(_sockfd, Convert(peer), len); if (newsockfd 0) { LOG(LogLevel::WARNING) accept client error strerror(errno); return nullptr; } addr-Init(peer); std::shared_ptrSocket s std::make_sharedTcpSocket(newsockfd); return s; } std::unique_ptrSocket AcceptConnection(std::string *peerip, uint16_t *peerport) override { struct sockaddr_in peer; socklen_t len sizeof(peer); int newsockfd accept(_sockfd, Convert(peer), len); if (newsockfd 0) return nullptr; *peerport ntohs(peer.sin_port); // *peerip inet_ntoa(peer.sin_addr); char buffer[64]; inet_ntop(AF_INET, (peer.sin_addr.s_addr), buffer, sizeof(buffer)); *peerip buffer; std::unique_ptrSocket s std::make_uniqueTcpSocket(newsockfd); return s; } bool ConnectServer(std::string serverip, uint16_t serverport) override { struct sockaddr_in server; memset(server, 0, sizeof(server)); server.sin_family AF_INET; // server.sin_addr.s_addr inet_addr(serverip.c_str()); inet_pton(AF_INET, serverip.c_str(), (server.sin_addr.s_addr)); server.sin_port htons(serverport); int n connect(_sockfd, Convert(server), sizeof(server)); if (n 0) return true; else return false; } int GetSockFd() override { return _sockfd; } void SetSockFd(int sockfd) override { _sockfd sockfd; } void CloseSocket() override { if (_sockfd defaultsockfd) close(_sockfd); } // 读 序列化及反序列 bool Recv(std::string *buffer, int size) override { char inbuffer[size]; ssize_t n recv(_sockfd, inbuffer, size - 1, 0); if (n 0) { inbuffer[n] 0; *buffer inbuffer; // 故意拼接的 增加 return true; } else if (n 0) return false; else return false; } void Send(const std::string send_str) override { send(_sockfd, send_str.c_str(), send_str.size(), 0); } private: int _sockfd; };InetAddr.hpp:#pragma once #include iostream #include stdlib.h #include string.h #include sys/types.h #include sys/socket.h #include arpa/inet.h #include netinet/in.h #include strings.h #include functional #include string #include Logger.hpp class InetAddr { private: void Net2Host() { _port ntohs(_addr.sin_port); // _ip inet_ntoa(_addr.sin_addr); char ipbuffer[64]; // 不需要调用函数内部的区域,防止覆盖 inet_ntop(AF_INET, (_addr.sin_addr.s_addr), ipbuffer, sizeof(ipbuffer)); _ip ipbuffer; } void Host2Net() { bzero(_addr, sizeof(_addr)); _addr.sin_family AF_INET; _addr.sin_port htons(_port); // _addr.sin_addr.s_addr inet_addr(_ip.c_str()); inet_pton(AF_INET, _ip.c_str(), (_addr.sin_addr.s_addr)); } public: InetAddr() {} InetAddr(const struct sockaddr_in client) : _addr(client) { Net2Host(); } InetAddr(uint16_t port, const std::string ip 0.0.0.0) : _port(port), _ip(ip) { Host2Net(); } void Init(const struct sockaddr_in client) { _addr client; Net2Host(); } uint16_t Port() { return _port; } std::string Ip() { return _ip; } struct sockaddr *Addr() { return (sockaddr *)_addr; } socklen_t Length() { socklen_t len sizeof(_addr); return len; } std::string ToString() { return _ip - std::to_string(_port); } bool operator(const InetAddr addr) { return _ip addr._ip _port addr._port; } ~InetAddr() { } private: struct sockaddr_in _addr; // 网络风格地址 std::string _ip; // 主机风格地址 uint16_t _port; };而 Logger.hpp 是一个封装好的日志类用于输出日志便于调试。最后的服务端启动文件Main.cc:#include Calculator.hpp// 业务 // 应用层 #include Parser.hpp // 报文解析序列反序列化打包解包 // 表示层 #include TcpServer.hpp // 网络通信开断连接 // 会话层 #include Daemon.hpp #include memory void Usage(std::string proc) { std::cerr Usage : proc serverport std::endl; } // ./tcp_client serverport int main(int argc, char *argv[]) { if (argc ! 2) { Usage(argv[0]); exit(0); } uint16_t serverport std::stoi(argv[1]); Daemon(); // EnableConsoleLogStrategy(); EnableFileLogStrategy(); // 1. 计算机对象 std::unique_ptrCalculator cal std::make_uniqueCalculator(); // 2. 协议解析模块 std::unique_ptrParser parser std::make_uniqueParser([cal](Request rq) - Response { return cal-Exec(rq); }); // 3. 网络通信模块 std::unique_ptrTcpserver tcpsock std::make_uniqueTcpserver(serverport, [parser](std::string inbuffer) - std::string { return parser-Parse(inbuffer); }); tcpsock-Run(); while (true) { } return 0; }Daemon.hpp:#pragma once #include iostream #include sys/types.h #include unistd.h #include signal.h #include fcntl.h void Daemon() { // 1. 忽略信号 signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); if (fork() 0) exit(0); setsid(); int fd open(dev/null, O_RDWR); if (fd 0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); } }Calculator.hpp:#pragma once #include Protocol.hpp #include iostream #include string class Calculator { public: Calculator() { } Response Exec(Request rq) { Response rp; switch (rq.Oper()) { case : rp.SetResult(rq.X() rq.Y()); break; case -: rp.SetResult(rq.X() - rq.Y()); break; case *: rp.SetResult(rq.X() * rq.Y()); break; case /: { if (rq.Y() 0) { rp.SetCode(1); // 1 - / } else { rp.SetResult(rq.X() / rq.Y()); } } break; case %: { if (rq.Y() 0) { rp.SetCode(2); // 2 - % } else { rp.SetResult(rq.X() % rq.Y()); } } break; default: rp.SetCode(3); // false break; } return rp; } ~Calculator() { } };协议定制要实现一个网络版的计算器就必须保证通信双方能够遵守某种协议约定因此我们需要设计一套简单的约定。数据可以分为请求数据和响应数据因此我们分别需要对请求数据和响应数据进行约定。采用C当中的类来实现请求类中需要包括两个操作数以及对应需要进行的操作。响应类中需要包括一个计算结果除此之外响应类中还需要包括一个状态字段表示本次计算的状态因为客户端发来的计算请求可能是无意义的。规定状态字段对应的含义状态字段为0表示计算成功。状态字段为1表示出现除0错误。状态字段为2表示出现模0错误。状态字段为3表示非法计算。只有当响应结构体当中的状态字段为0时计算结果才是有意义的否则计算结果无意义。Protocol.hpp:#pragma once #include iostream #include string #include jsoncpp/json/json.h class Request { public: Request() { } // 序列 反序列化对象 bool Serializ(std::string *out) { Json::Value root; root[x] _x; root[y] _y; root[oper] _oper; Json::StyledWriter writer; *out writer.write(root); if (out-empty()) return false; return true; } bool Deserialize(std::string in) { Json::Reader reader; Json::Value droot; bool ret reader.parse(in, droot); if (!ret) return false; _x droot[x].asInt(); _y droot[y].asInt(); _oper droot[oper].asInt(); return true; } int X() { return _x; } int Y() { return _y; } char Oper() { return _oper; } ~Request() { } public: // 约定 int _x; int _y; char _oper; }; class Response { public: Response() : _result(0), _code(0) { } bool Serializ(std::string *out) { Json::Value root; root[result] _result; root[code] _code; Json::StyledWriter writer; *out writer.write(root); if (out-empty()) return false; return true; } bool Deserialize(std::string in) { Json::Reader reader; Json::Value droot; bool ret reader.parse(in, droot); if (!ret) return false; _result droot[result].asInt(); _code droot[code].asInt(); return true; } void SetResult(int r) { _result r; } void SetCode(int c) { _code c; } void Print() { std::cout _result [ _code ] std::endl; } ~Response() { } private: int _result; int _code; }; bool DigitSafeCheck(const std::string str) { for (int i 0; i str.size(); i) { if (!(str[i] 0 str[i] 9)) { return false; } } return true; } static const std::string sep \r\n; class Protocol { public: static std::string Package(const std::string jsonstr) { // jsonstd - len\r\njsonstr\r\n if (jsonstr.empty()) { return std::string(); } std::string jsonlen std::to_string(jsonstr.size()); return jsonlen sep jsonstr sep; } static int UnPackage(std::string origin_str, std::string *package) // 输入输出 { if (package nullptr) return -2; auto pos origin_str.find(sep); if (pos std::string::npos) { return 0; } std::string len_str origin_str.substr(0, pos); if (!DigitSafeCheck(len_str)) { return -1; } int digit_len std::stoi(len_str); int target_len len_str.size() digit_len 2 * sep.size(); if (origin_str.size() target_len) { return 0; } *package origin_str.substr(pos sep.size(), digit_len); origin_str.erase(0, target_len); // 移除 return package-size(); } };在上述源码中不仅含有请求类、响应类也有对应的协议类用于解包封包。为了更好的解耦我们将报文解析的过程单独封装成一个Parser类。Parser.hpp:#pragma once #include Protocol.hpp #include Calculator.hpp #include Parser.hpp #include Logger.hpp #include iostream #include string #include functional using handler_t std::functionResponse(Request ); // 只负责报文解析 class Parser { public: Parser(handler_t handler) : _handler(handler) { } std::string Parse(std::string inbuffer) { std::string package_ptr; while (true) // 循环处理多个请求 { // 1. 解包 std::string jsonstr; int n Protocol::UnPackage(inbuffer, jsonstr); if (n 0) { // return std::string(); break; } else if (n 0) { exit(1); } // 解包成功 LOG(LogLevel::DEBUG) jsonstr; // 2. 反序列化 Request rq; rq.Deserialize(jsonstr); // 3. 业务处理 Response rp _handler(rq); // 4. 序列化 std::string send_str; rp.Serializ(send_str); // 5. 打包 package_ptr Protocol::Package(send_str); } // 6. 返回 return package_ptr; } ~Parser() { } private: handler_t _handler; };注意协议定制好后必须要被客户端和服务端同时看到这样它们才能遵守这个约定那么客户端和服务端都应该包含这个头文件。客户端代码客户端首先也需要进行初始化调用socket函数创建套接字。客户端初始化完毕后需要调用connect函数连接服务端当连接服务端成功后客户端就可以向服务端发起计算请求了。用户输入两个数和一个操作符构建一个计算请求然后将该请求发送给服务端。当服务端处理完该计算请求后会对客户端进行响应因此客户端发送完请求后还需要读取服务端发来的响应数据。客户端在向服务端发送或接收数据时可以使用write或read函数进行发送或接收也可以使用send或recv函数对应进行发送或接收。send函数函数原型ssize_t send(int sockfd, const void *buf, size_t len, int flags);参数说明sockfd特定的文件描述符表示将数据写入该文件描述符对应的套接字。buf需要发送的数据。len需要发送数据的字节个数。flags发送的方式一般设置为0表示阻塞式发送。返回值说明写入成功返回实际写入的字节数写入失败返回-1同时错误码会被设置。recv函数函数原型ssize_t recv(int sockfd, void *buf, size_t len, int flags);参数说明sockfd特定的文件描述符表示从该文件描述符中读取数据。buf数据的存储位置表示将读取到的数据存储到该位置。len数据的个数表示从该文件描述符中读取数据的字节数。flags读取的方式一般设置为0表示阻塞式读取。返回值说明如果返回值大于0则表示本次实际读取到的字节个数。如果返回值等于0则表示对端已经把连接关闭了。如果返回值小于0则表示读取时遇到了错误。Client.cc:#include iostream #include Socket.hpp #include Protocol.hpp #include Parser.hpp void Usage(std::string proc) { std::cerr Usage : proc serverip serverport std::endl; } // ./tcp_client serverip serverport int main(int argc, char *argv[]) { if (argc ! 3) { Usage(argv[0]); exit(0); } std::string serverip argv[1]; uint16_t serverport std::stoi(argv[2]); std::unique_ptrSocket client std::make_uniqueTcpSocket(); if (client-BuildConnectSocketMethod(serverip, serverport)) { std::string inbuffer; while (true) { // 1. 构建请求 Request rq; std::cout Please Enter X: ; std::cin rq._x; std::cout Please Enter Y: ; std::cin rq._y; std::cout Please Enter Oper: ; std::cin rq._oper; // 2. 序列化 std::string jsonstr; rq.Serializ(jsonstr); // 3. 打包 std::string sendstr Protocol::Package(jsonstr); // 4. 发送 client-Send(sendstr); // 5. 接收 client-Recv(inbuffer, 100); std::string package; int n Protocol::UnPackage(inbuffer, package); if (n 0) { Response rp; bool r rp.Deserialize(package); if (r) { rp.Print(); } } } } return 0; }上述 demo 就是一个轻量级 TCP 服务示例使用自定义包协议传输 JSON 请求。父进程监听端口并对每个连接 fork 子进程处理子进程循环读取、解帧、调用业务Calculator并将计算结果序列化返回。