应用层自定义协议与序列化

1. 再深入协议

协议:双方约定好的结构化数据

数据在网络上传输时数据的形式:

1.字符串

2.二进制数据、结构体

3.序列化和反序列化

序列化------>把信息由多变成一个完整的序列,方便网络发送------>jsoncpp、protobuf

反序列化------>把信息一变多(结构化数据,方便描述、组织),方便上层处理

如果要实现一个简单的网络业务

  1. 网络功能 ------ TCP来做
  2. 协议定好 + 序列化和反序列化方案来实现应用层

2. 序列化反序列化

序列化 指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件

中。
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。

方案1:自己做

复制代码
// 这是一个简单计算x oper y的计算器的序列化字段,head-length表示整个序列化字符串长度
head-length"x""oper""y"\n

方案2:xml 或 json 或 protobuf
JSON(JavaScript Object Notation)是一种 轻量级、人类可读的文本格式,用于结构化数据的交换和存储,用键值对的形式表现数据

javascript 复制代码
{
  "id": 101,
  "name": "张三",
  "age": 28,
  "isStudent": false,
  "email": "zhangsan@test.com",
  "hobbies": ["跑步", "看书", "编程"]
}

3. TCP全双工+面向字节流 解析

3.1 TCP全双工+面向字节流

TCP的双缓冲区保证全双工

发送缓冲区(发队列):应用层调用 send() 发送的数据,不会直接发往网络,而是先写入这个队列,由 TCP 协议栈负责后续的分组、发送、重传。

接收缓冲区(收队列):TCP 协议栈从网络收到的数据,先存入这个队列,按顺序缓存(保证有序性),等待应用层调用 recv() 读取。

TCP面向字节流

TCP不进行报文完整性处理,需要应用层保证自己报文完整性。TCP是传输控制协议,数据什么时候发,发多少,出错了怎么办,由 TCP 控制

4 Jsoncpp

Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字

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

  1. 简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
  2. 高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
  3. 全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数
    字、布尔值和 null。
  4. 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。

安装:

bash 复制代码
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

编译命令:
g++ client.cpp server.cpp -o client -ljsoncppg++ server.cpp -o server -ljsoncpp -pthread

4.1序列化

cpp 复制代码
// 使用 Json::Value 的 toStyledString 方法:
// 优点:将 Json::Value 对象直接转换为格式化的 JSON 字符串
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
	Json::Value root;
	root["name"] = "joe";
	root["sex"] = "男";
	std::string s = root.toStyledString();
	std::cout << s << std::endl;
	return 0;
}

$ ./test.exe
{
	"name" : "joe",
	"sex" : "男"
}

// 使用 Json::StreamWriter:
// 优点:提供了更多的定制选项,如缩进、换行符等
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
	Json::Value root;
	root["name"] = "joe";
	root["sex"] = "男";
	Json::StreamWriterBuilder wbuilder; // StreamWriter 的工厂
	std::unique_ptr<Json::StreamWriter>
	writer(wbuilder.newStreamWriter());
	std::stringstream ss;
	writer->write(root, &ss);
	std::cout << ss.str() << std::endl;
	return 0;
}
$ ./test.exe
{
	"name" : "joe",
	"sex" : "男"
}

4.2 反序列化

cpp 复制代码
// 使用 Json::Reader:
// 优点:提供详细的错误信息和位置,方便调试。
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main() {
	// JSON 字符串
	std::string json_string = "{\"name\":\"张三\",\"age\":30, \"city\":\"北京\"}";
	// 解析 JSON 字符串
	Json::Reader reader;
	Json::Value root;
	// 从字符串中读取 JSON 数据
	bool parsingSuccessful = reader.parse(json_string,root);
	if (!parsingSuccessful) {
		// 解析失败,输出错误信息
		std::cout << "Failed to parse JSON: " <<std::endl;
		reader.getFormattedErrorMessages() << std::endl;
		return 1;
	}
	// 访问 JSON 数据
	std::string name = root["name"].asString();
	int age = root["age"].asInt();
	std::string city = root["city"].asString();
	// 输出结果
	std::cout << "Name: " << name << std::endl;
	std::cout << "Age: " << age << std::endl;
	std::cout << "City: " << city << std::endl;
	return 0;
}

$ ./test.exe
Name: 张三
Age: 30
City: 北京

4.3 API

API 代码示例 作用
Json::Value Json::Value root; root["x"] = x; 构建JSON对象,存储键值对数据
Json::Reader::parse reader.parse(json_str, root) 反序列化核心:将JSON字符串解析为 Json::Value 对象,返回 bool 表示解析成功与否
Json::StreamWriterBuilder Json::StreamWriterBuilder writer; writeString(writer, root) 序列化核心:将 Json::Value 对象转换为JSON字符串
asInt()/asChar()/asString() root["x"].asInt() Json::Value 中提取对应类型的数据,确保类型一致

5. 以序列化数据做交换的网络程序

以下以 TCP 网络计算器 为实战案例,通过可运行的完整代码,详细拆解"序列化/反序列化 + 协议封装"如何保障网络传输数据完整性。

5.1 公共头文件(Protocol.hpp)定义数据结构、协议及其序列反序列化

cpp 复制代码
#pragma once
#include <string>
#include <json/json.h>

// 协议分隔符(用于分割长度字段和JSON数据)
const std::string SEP = "\r\n";
// 最大缓冲区大小
const int BUF_SIZE = 4096;

// 计算请求结构体(客户端→服务器)
struct Request {
    int x;          // 操作数1
    int y;          // 操作数2
    char oper;      // 运算符(+、-、*、/)

    // 序列化:Request对象 → JSON字符串
    std::string Serialize() const {
        Json::Value root;
        root["x"] = x;
        root["y"] = y;
        root["oper"] = oper;

        Json::StreamWriterBuilder writer;
        return Json::writeString(writer, root);
    }

    // 反序列化:JSON字符串 → Request对象
    bool Deserialize(const std::string& json_str) {
        Json::Reader reader;
        Json::Value root;

        // 解析JSON失败(格式错误)→ 数据不完整/非法
        if (!reader.parse(json_str, root)) {
            return false;
        }

        // 校验必填字段(避免字段缺失导致数据异常)
        if (!root.isMember("x") || !root.isMember("y") || !root.isMember("oper")) {
            return false;
        }

        x = root["x"].asInt();
        y = root["y"].asInt();
        oper = root["oper"].asChar();
        return true;
    }
};

// 计算响应结构体(服务器→客户端)
struct Response {
    int code;       // 错误码(0=成功,1=失败)
    int result;     // 计算结果(code=0时有效)
    std::string msg;// 错误信息(code=1时有效)

    // 序列化:Response对象 → JSON字符串
    std::string Serialize() const {
        Json::Value root;
        root["code"] = code;
        root["result"] = result;
        root["msg"] = msg;

        Json::StreamWriterBuilder writer;
        return Json::writeString(writer, root);
    }

    // 反序列化:JSON字符串 → Response对象
    bool Deserialize(const std::string& json_str) {
        Json::Reader reader;
        Json::Value root;

        if (!reader.parse(json_str, root)) {
            return false;
        }

        if (!root.isMember("code") || !root.isMember("result") || !root.isMember("msg")) {
            return false;
        }

        code = root["code"].asInt();
        result = root["result"].asInt();
        msg = root["msg"].asString();
        return true;
    }
};

// 协议编码:JSON字符串 → 带长度标识的数据包(解决TCP粘包/半包)
std::string Encode(const std::string& json_str) {
    // 协议格式:长度\r\nJSON字符串\r\n
    return std::to_string(json_str.size()) + SEP + json_str + SEP;
}

// 协议解码:从缓存中提取完整数据包(保证数据完整性)
// 协议格式:长度\r\nJSON字符串\r\n
// 参数:package-接收缓存,content-输出完整JSON字符串
bool Decode(std::string& package, std::string* content) {
    // 1. 查找第一个分隔符(分割长度字段和JSON)
    size_t sep_pos = package.find(SEP);
    if (sep_pos == std::string::npos) {
        return false; // 未找到分隔符→数据不完整
    }

    // 2. 提取长度字段并转换为整数
    std::string len_str = package.substr(0, sep_pos);
    int json_len = atoi(len_str.c_str());
    if (json_len <= 0) {
        return false; // 长度非法→数据无效
    }

    // 3. 校验缓存是否包含完整数据包(长度+2个SEP的长度)
    size_t full_len = sep_pos + SEP.size() + json_len + SEP.size();
    if (package.size() < full_len) {
        return false; // 缓存长度不足→半包,等待后续数据
    }

    // 4. 提取完整JSON字符串
    *content = package.substr(sep_pos + SEP.size(), json_len);

    // 5. 移除缓存中已处理的完整数据包(避免粘包)
    package.erase(0, full_len);
    return true;
}

5.2 服务端:

TcpServer.hpp
cpp 复制代码
#ifndef __TCP_SERVER_HPP__
#define __TCP_SERVER_HPP__
#include <iostream>
#include <string>
#include <string.h>
#include <pthread.h>

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "MyPthread/Log.hpp"
#include "MyPthread/ThreadPool.hpp"
#include "Protocol.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
using namespace ThreadModule;

// 监听队列最大连接数
#define BACKLOG 8
const static int gsockfd = -1; // 无效socket描述符

// 任务类型:无参无返回值函数对象
using task_t = std::function<void()>;
// 业务处理函数:接收请求包,返回响应包
using handler_t = std::function<std::string(std::string &package)>;
class TcpServer;

// 线程参数结构体:传递给线程入口的参数
struct ThreadData
{
    TcpServer *_this;    // 指向TcpServer实例
    int _sockfd;         // 客户端连接fd
    InetAddr _client;    // 客户端地址
    ThreadData(TcpServer *t, int sockfd, InetAddr client)
        : _this(t), _sockfd(sockfd), _client(client) {}
};

// TCP服务器核心类
class TcpServer
{
private:
    // 处理单个客户端请求:接收数据→业务处理→返回响应
    void HandlerRequest(int sockfd, InetAddr client)
    {
        LOG(LogLevel::INFO) << "HandlerRequest, sockfd is : " << sockfd;

        char buffer[1024];
        std::string package, response;
        while (true)
        {
            // 接收客户端数据
            int n = ::recv(sockfd, buffer, sizeof(buffer) - 1, 0);
            if (n > 0) // 成功接收数据
            {
                buffer[n] = 0;
                package += buffer;
                response = _handler(package); // 调用业务处理回调
            }
            else if (n == 0) // 客户端断开连接
            {
                LOG(LogLevel::INFO) << "client quit: " << sockfd;
                break;
            }
            else break; // 接收失败

            // 发送响应
            ::send(sockfd, response.c_str(), response.size(), 0);
        }
        ::close(sockfd); // 关闭客户端连接
        LOG(LogLevel::INFO) << "close sockfd: " << sockfd;
    }

    // 线程入口函数(静态)
    static void *ThreadEntry(void *args)
    {
        ThreadData *threadData = (ThreadData *)args;
        threadData->_this->HandlerRequest(threadData->_sockfd, threadData->_client);
        return nullptr;
    }

public:
    // 构造函数:初始化服务器端口
    TcpServer(uint16_t server_port = gserver_port)
        : _listen_sockfd(gsockfd), _server(server_port), _isrunning(false) {}
    ~TcpServer() {}

    // 初始化服务器:创建socket→绑定地址→监听端口
    void InitServer()
    {
        // 1. 创建TCP监听socket
        _listen_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0) { LOG(LogLevel::FATAL) << "create socket failed"; Die(SOCKET_ERR); }

        // 2. 绑定服务器地址
        int rn = ::bind(_listen_sockfd, CONV(_server.NetAddr()), _server.NetAddrLen());
        if (rn < 0) { LOG(LogLevel::FATAL) << "bind failed"; Die(1); }

        // 3. 启动监听
        rn = ::listen(_listen_sockfd, BACKLOG);
        if (rn < 0) { LOG(LogLevel::FATAL) << "listen error"; Die(LISTEN_ERR); }
        LOG(LogLevel::INFO) << "listen success, sockfd : " << _listen_sockfd;
    }

    // 注册业务处理回调(解耦网络层与业务层)
    void RegisterSerive(handler_t handler) { _handler = handler; }

    // 启动服务器:循环接收连接,交给线程池处理
    void Start()
    {
        _isrunning = true;
        struct sockaddr_in peer; // 客户端地址结构体
        while (true)
        {
            // 接收客户端连接(阻塞)
            int sockfd = ::accept(_listen_sockfd, CONV(&peer), &sizeof(peer));
            if (sockfd < 0) { LOG(LogLevel::WARNING) << "accept error"; continue; }

            // 线程池异步处理请求ThreadPoolMoudule::ThreadPool<task_t>::getInstance()
                ->Enqueue(std::bind(&TcpServer::HandlerRequest, this, sockfd, InetAddr(peer)));
            LOG(LogLevel::DEBUG) << "线程池处理请求...";
        }
        _isrunning = false;
    }

private:
    int _listen_sockfd;    // 监听socket描述符
    InetAddr _server;      // 服务器地址
    bool _isrunning;       // 服务器运行状态
    handler_t _handler;    // 业务处理回调函数
};

#endif
TcpServerMain.cc
cpp 复制代码
#include "TcpServer.hpp"    // 传输层:TCP服务器核心(监听、连接管理、数据收发)
#include "Protocol.hpp"     // 表示层核心:定义Request/Response数据结构、序列化/反序列化接口、协议编解码(Encode/Decode)
#include "Daemon.hpp"       // 工具类:将进程转为守护进程(脱离终端,后台运行)
#include "MyPthread/Log.hpp"// 日志模块:调试/运维用,跟踪数据流转过程(关键:可验证序列化/反序列化结果是否正确)
#include <memory>
using namespace LogModule;
// 定义计算逻辑的函数类型(回调思想:解耦表示层和应用层)
using cal_fun = std::function<Response(const Request &req)>;

// 应用层:业务逻辑实现(计算器核心)
class Calculator
{
public:
    // 重载()运算符,使Calculator对象可作为cal_fun类型的回调函数
    Response operator()(const Request &req)
    {
        Response resp; // 初始化响应对象(结构化数据,待后续序列化)
        // 从Request对象中获取结构化字段(反序列化已保证字段存在、类型正确)
        int x = req.GetX(), y = req.GetY();
        char oper = req.GetOper();

        // 业务逻辑:根据运算符计算结果,设置响应状态
        switch (oper)
        {
        case '+':
            resp.SetRes(x + y); // 计算成功,设置结果(code默认0:成功)
            break;
        case '-':
            resp.SetRes(x - y);
            break;
        case '*':
            resp.SetRes(x * y);
            break;
        case '/':
            if (y == 0)
            {
                resp.SetCode(1); // 错误码1:除数为0(业务错误,需告知客户端)
            }
            else
            {
                resp.SetRes(x / y);
            }
            break;
        case '%':
        {
            if (y == 0)
            {
                resp.SetCode(2); // 错误码2:取模除数为0
            }
            else
            {
                resp.SetRes(x % y);
            }
        }
        break;
        default:
            resp.SetCode(3); // 错误码3:不支持的运算符
            break;
        }
        return resp; // 返回结构化响应对象(交给表示层序列化)
    }
};

// package一定会有完整的报文吗??不一定吧
// 不完整->继续读
// 完整-> 提取 -> 反序列化 -> Request -> 计算模块,进行处理
class Parse
{
public:
    // 注入计算逻辑(回调函数)
    Parse(cal_fun c)
        : _cal(c)
    {
    }

    // 处理传输层传来的字节流缓存,返回待发送的字节流(响应)
    // package:传输层接收的原始字节流缓存(可能包含粘包/半包数据)
    std::string Entry(std::string &package)
    {
        std::string content;    
        std::string resp_str;
        LOG(LogLevel::DEBUG) << "package: \n" << package; 

        // 1. 判断报文完整性
        // 循环处理粘包
        // 循环处理多条完整报文  ------------ 收到数据不断往package中写数据,此处不断取数据(生产者消费者模型!!)
        while (Decode(package, &content))
        {
            if (content.empty())
            {
                std::cerr << "incomplate content" << std::endl;
                break;
            }

            // 2. 反序列化 → JSON字符串 → 结构化Request对象。content是一个被序列化的Request对象
            Request req;
            req.Deserialize(content);

            // 3. 调用应用层业务逻辑 → 结构化Request → 结构化Response
            // Parse不关心计算逻辑,仅负责调用注入的回调函数
            Response resp = _cal(req);
            // 4. 序列化 → 结构化Response对象 → JSON字符串
            std::string res_content;
            resp.Serialize(res_content); 

            // 5. 协议编码,添加长度报头字段 → JSON字符串 → 带长度报头的字节流
            Encode(res_content); // 核心API:编码(Protocol.hpp中实现,修改res_content为带报头的格式)
            LOG(LogLevel::INFO) << "res_content: \n" << res_content;
            // 6. 拼接应答
            resp_str += res_content;
        }

        LOG(LogLevel::DEBUG) << "respstr: \n" << resp_str; 
        return resp_str; // 返回编码后的响应,传输层可直接send
    }

private:
    cal_fun _cal; // 存储计算逻辑回调函数(解耦表示层和应用层)
};

int main(int argc, char *argv[])
{
    ENABLE_FILE_LOG(); // 文件日志(生产环境用)

    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
        Die(USAGE_ERR); // 退出函数(Daemon.hpp中定义)
    }
    uint16_t port = std::stoi(argv[1]); // 转换端口号(网络字节序由TcpServer内部处理)

    // 0:创建守护进程 → 脱离终端,后台运行
    Daemon(false, false);
    // 1: 计算器模块(应用层)
    Calculator cal;
    // 2: 解析模块(表示层)
    std::shared_ptr<Parse> parse = std::make_shared<Parse>(cal);
    // 3: 服务器模块(传输层)
    std::shared_ptr<TcpServer> server = std::make_shared<TcpServer>(std::stoi(argv[1]));

    // 4:TCP服务器初始化 → 绑定端口、监听
    server->InitServer();
    // 5:注册服务回调 → 绑定传输层和表示层
    server->RegisterSerive(
        [&parse](std::string &package)
        {
            return parse->Entry(package);
        });
    // 6:启动服务器 → 开始监听连接、处理客户端请求
    server->Start();

    return 0;
}

5.3 客户端:

TcpClient.hpp

cpp 复制代码
#ifndef __TCP_CLIENT_HPP__
#define __TCP_CLIENT_HPP__
#include "MyPthread/Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "Protocol.hpp"
using namespace LogModule;

const static int gsockfd = -1;
class TcpClient
{
private:
public:
    TcpClient(std::string server_ip = gserver_ip, uint16_t server_port = gserver_port)
        : _sockfd(gsockfd), _server(server_ip, server_port)
    {
    }
    ~TcpClient()
    {
    }
    // 1. 创建socket
    void InitClient()
    {
        // 1. 创建socket
        _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "socket error";
            Die(SOCKET_ERR);
        }
        // 2. 连接
        // client 不需要显式地进行bind, tcp是面向连接的, connect 底层会自动进行bind
        int n = connect(_sockfd, _server.NetAddr(), _server.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "connect failed";
            Die(CONNECT_ERR);
        }
    }
    void Start()
    {

        while (true)
        {
            // std::cout << "Please Enter# ";
            std::string message;

            int x, y;
            char oper;
            std::cout << "input x: ";
            std::cin >> x;
            std::cout << "input y: ";
            std::cin >> y;
            std::cout << "input oper: ";
            std::cin >> oper;

            Request req(x, y, oper);

            // 1.序列化
            req.Serialize(message);

            // 2.封装报文添加头部
            Encode(message);

            // 3.发送报文
            int n = ::send(_sockfd, message.c_str(), message.size(), 0);
            (void)n;

            // LOG(LogLevel::DEBUG) << "send succeed:" << message;

            char inbuffer[1024];
            n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);

            if (n > 0)
            {
                inbuffer[n] = 0;


                // 4.解析报文
                std::string out_string;
                std::string package = inbuffer;

                Decode(package, &out_string);

                // 5.反序列化
                Response res;
                res.Deserialize(out_string);

                // 6.打印结果       
                res.Print();

            }
        }
    }

public:
private:
    int _sockfd;
    // struct sockaddr_in _client;
    InetAddr _server;
};

#endif

client.cc

cpp 复制代码
#include <iostream>

#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <memory>

#include "Common.hpp"
#include "TcpClient.hpp"
#include "MyPthread/Log.hpp"
using namespace LogModule;

int main(int argc, char *argv[])
{
    ENABLE_CONSOLE_LOG();
    if(argc <=1)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip"<< " server_port" << std::endl;
        Die(USAGE_ERR);
    }



    std::shared_ptr<TcpClient> client =std::make_shared<TcpClient>
    (std::string(argv[1]),std::stoi(argv[2]));


    client->InitClient();
    client->Start();
    
    return 0;
}

5.4 分层思想

我们来看服务端的主要执行逻辑

cpp 复制代码
    // 1: 计算器模块(应用层)
    Calculator cal;
    // 2: 解析模块(表示层)
    std::shared_ptr<Parse> parse = std::make_shared<Parse>(cal);
    // 3: 服务器模块(传输层)
    std::shared_ptr<TcpServer> server = std::make_shared<TcpServer>(std::stoi(argv[1]));
    
    // 4:TCP服务器初始化 → 绑定端口、监听
    server->InitServer();
    // 5:注册服务回调 → 绑定传输层和表示层
    server->RegisterSerive(
        [&parse](std::string &package)
        {
            return parse->Entry(package);
        });
    // 6:启动服务器 → 开始监听连接、处理客户端请求
    server->Start();

步骤1、2、3的分层结构刚好能对应上OSI模型中的应用层表示层、传输层

所以OSI模型好在哪?
会话层 :线程管理连接,tcp_server IO那一层
表示层 :表示自定义协议、序列和反序列化
应用层:用户自定义应用

那为什么TCP/IP协议将这三层压缩成了一层应用层

  • TCP/IP只实现了下四层,应用层交给上层程序员做,因为会话、表示、应用无法整合进内核

6. 小结

网络传输中,"序列化/反序列化"是数据格式统一的基础,"协议封装/解码"是解决传输边界问题的关键,二者结合才能真正保障数据完整性。

相关推荐
a努力。6 小时前
网易Java面试被问:偏向锁在什么场景下反而降低性能?如何关闭?
java·开发语言·后端·面试·架构·c#
前端达人6 小时前
CSS终于不再是痛点:2026年这7个特性让你删掉一半JavaScript
开发语言·前端·javascript·css·ecmascript
wjs20246 小时前
SVG 多边形
开发语言
H_-H6 小时前
值返回与引用返回(c++)
开发语言·c++
csbysj20206 小时前
Java 日期时间处理详解
开发语言
我命由我123456 小时前
Python Flask 开发 - Flask 快速上手(Flask 最简单的案例、Flask 处理跨域、Flask 基础接口)
服务器·开发语言·后端·python·学习·flask·学习方法
大飞记Python6 小时前
从零配置Python测试环境:详解路径、依赖与虚拟环境最佳实践
开发语言·python·环境配置·安装目录
zhougl9967 小时前
区分__proto__和prototype
开发语言·javascript·原型模式
weixin_420947647 小时前
php composer update 指定包的分支非tag
开发语言·php·composer