首先我们要知道,在之前的Socket编程学习中,我们通过socketAPI 实现了简单的字符串发送和接收,即EchoServer。但在实际的开发场景中,我们需要传输的是“结构化的数据”。
socketAPI 本质上是面向字节流的,它并不理解什么是“结构体”或“类” 。因此,我们需要在应用层解决如何把业务数据在“结构体”和“网络字节流”之间进行转换的问题,这就是自定义协议与序列化的由来。
一、什么是“协议”?
所谓的“协议”,本质上就是通信双方约定好的结构化数据。
比如我们要实现一个网络计算器,客户端需要把1+2发给服务端。
- 方案一:直接发送字符串
"1+2"。这就需要约定好格式:数字间用运算符隔开,没有空格等 。 - 方案二:定义一个结构体来表示交互信息 。
无论采用哪种方案,只要保证一端发送的数据,另一端能够按照约定的规则正确解析,这就是应用层协议。
二、序列化与反序列化
为了在网络上传输结构化的数据,我们需要进行转化:
序列化 (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::function<std::string(std::string &)>; class Tcpserver { public: Tcpserver(uint16_t port, callback_t cb) : _port(port), _listensocket(std::make_unique<TcpSocket>()), _cb(cb) { _listensocket->BuildListenSocketMethod(_port); } void HandlerRequest(std::shared_ptr<Socket> 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_ptr<Socket> _listensocket; callback_t _cb; };上述的Socket.hpp,InetAddr.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_ptr<Socket> AcceptConnection(std::string *peerip, uint16_t *peerport) = 0; virtual std::shared_ptr<Socket> 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_ptr<Socket> 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_ptr<Socket> s = std::make_shared<TcpSocket>(newsockfd); return s; } std::unique_ptr<Socket> 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_ptr<Socket> s = std::make_unique<TcpSocket>(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_ptr<Calculator> cal = std::make_unique<Calculator>(); // 2. 协议解析模块 std::unique_ptr<Parser> parser = std::make_unique<Parser>([&cal](Request &rq) -> Response { return cal->Exec(rq); }); // 3. 网络通信模块 std::unique_ptr<Tcpserver> tcpsock = std::make_unique<Tcpserver>(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::function<Response(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_ptr<Socket> client = std::make_unique<TcpSocket>(); 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)并将计算结果序列化返回。