【Linux网络编程】第十弹---打造初级网络计算器:从协议设计到服务实现

✨个人主页:熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】

目录

1、Protocol.hpp

1.1、Request类

1.1.1、基本结构

1.1.2、构造析构函数

1.1.3、序列化函数

1.1.4、反序列化函数

1.1.5、获取成员变量函数

1.1.6、设置成员变量值函数

1.2、Response类

1.2.1、基本结构

1.2.2、构造析构函数

1.2.3、序列化函数

1.2.4、反序列化函数

1.2.5、打印函数

1.3、Factory类

1.4、添加报头

1.5、解析报头

2、Service.hpp

2.1、构造析构函数

2.2、IOExcute()

2.3、Recv()

3、NetCal.hpp

3.1、构造析构函数

3.2、Calculator()

4、ClientMain.cc

5、完整代码

5.1、Protocol.hpp

5.2、Service.hpp

5.3、ClientMain.cc

5.4、Makefile


上一弹已经写好服务端代码,并讲解了序列化使用到的json库,这弹开始写协议,并完成网络计算器的代码

1、Protocol.hpp

该文件实现序列化与反序列使用到的类和相关函数(加报头解报头)!

1.1、Request类

该类是向服务器发送请求的类 ,需要三个成员变量,_x,_y,_oper(运算符号),统一的计算格式是 _x _oper _y,内部主要是序列化(发送)和反序列化(接受)函数

1.1.1、基本结构

该类有三个成员变量,_x,_y,_oper(运算符号),序列化,反序列化,构造,析构及其他获取成员变量与打印的函数!

class Request
{
public:
    Request()
    {}
    // 序列化 将结构化转成字符串
    bool Serialize(std::string *out);
    // 反序列化 将字符串转成结构化
    bool Deserialize(const std::string &in);
    void Print();
    ~Request();
    int X();
    int Y();
    char Oper();
private:
    int _x;
    int _y;
    char _oper; // + - * / %   // x oper y
};

1.1.2、构造析构函数

为了方便后面的使用,此处实现两个构造函数,一个无参,一个带参函数,析构函数无需处理!

Request()
{}
Request(int x,int y,char oper):_x(x),_y(y),_oper(oper)
{}

~Request()
{}

1.1.3、序列化函数

序列化将结构化转成字符串,并将字符串以输出型参数传出

// 序列化 将结构化转成字符串
bool Serialize(std::string *out)
{
    // 1.自己做: "x oper y" (麻烦)
    // 2.使用现成的库, xml,json(jsoncpp库), protobuf
    Json::Value root;
    root["x"] = _x;
    root["y"] = _y;
    root["oper"] = _oper;
    Json::FastWriter writer;
    std::string s = writer.write(root);
    *out = s;
    return true;
}

1.1.4、反序列化函数

反序列化将字符串转成结构化,参数传入字符串

// 反序列化 将字符串转成结构化
bool Deserialize(const std::string &in)
{
    Json::Value root;
    Json::Reader reader;
    bool res = reader.parse(in,root);

    _x = root["x"].asInt();
    _y = root["y"].asInt();
    _oper = root["oper"].asInt();

    return true;
}

打印函数

void Print()
{
    std::cout << _x << std::endl;
    std::cout << _y << std::endl;
    std::cout << _oper << std::endl;
}

1.1.5、获取成员变量函数

因为成员变量是私有的,外部访问使用成员函数!

int X()
{
    return _x;
}
int Y()
{
    return _y;
}
char Oper()
{
    return _oper;
}

1.1.6、设置成员变量值函数

cpp 复制代码
void SetValue(int x, int y, char oper)
{
    _x = x;
    _y = y;
    _oper = oper;
}

1.2、Response类

该类是向客户端发送结果的类 ,需要三个成员变量,_result(计算结果),_code(自定义错误码[**0: success 1: div error 2: 非法操作 ]),_desc(错误码描述),内部主要是序列化(发送)和反序列化(接受)函数**!

1.2.1、基本结构

该类有三个成员变量,_result(计算结果),_code(自定义错误码****),_desc(错误码描述),序列化,反序列化,构造,析构和打印函数!

注意:为了方便类外访问该类成员变量,该成员变量是公有的!

class Response
{
public:
    Response();
    // 序列化 将结构化转成字符串
    bool Serialize(std::string *out);
    // 反序列化 将字符串转成结构化
    bool Deserialize(const std::string &in);
    ~Response();
public:
    int _result;
    int _code; // 0: success 1: div error 2: 非法操作
    std::string _desc; 
};

1.2.2、构造析构函数

构造函数直接手动初始化 (结果和错误码初始化为0,描述默认初始化为success),析构函数无需处理

Response():_result(0),_code(0),_desc("success")
{}

~Response()
{}

1.2.3、序列化函数

序列化将结构化转成字符串,并将字符串以输出型参数传出

// 序列化 将结构化转成字符串
bool Serialize(std::string *out)
{
    // 使用现成的库, xml,json(jsoncpp库), protobuf
    Json::Value root;
    root["result"] = _result;
    root["code"] = _code;
    root["desc"] = _desc;
    Json::FastWriter writer;
    std::string s = writer.write(root);
    *out = s;
    
    return true;
}

1.2.4、反序列化函数

反序列化将字符串转成结构化,参数传入字符串

// 反序列化 将字符串转成结构化
bool Deserialize(const std::string &in)
{
    Json::Value root;
    Json::Reader reader;
    bool res = reader.parse(in,root);

    _x = root["x"].asInt();
    _y = root["y"].asInt();
    _oper = root["oper"].asInt();

    return true;
}

1.2.5、打印函数

将成员变量以字符串形式打印出来即可!

void PrintResult()
{
    std::cout << "result: " << _result << ", code: " << _code 
    << ", desc: " << _desc << std::endl;
}

1.3、Factory类

因为Request类和Response类可能频繁创建,因此我们可以设计一个工厂类内部设计两个创建类的静态函数(没有this指针,外部直接调用函数即可)

cpp 复制代码
class Factory
{
public:
    static std::shared_ptr<Request> BuildRequestDefault()
    {
        return std::make_shared<Request>();
    }
    static std::shared_ptr<Response> BuildResponseDefault()
    {
        return std::make_shared<Response>();
    }
};

1.4、添加报头

在实际的网络通信中,传的不仅仅是序列化之后的字符串,还会有报头信息 ,此处我们也设计一下报头信息,格式如下:

1、"len"\r\n"{json}"\r\n --- 完整的报文

2、len 有效载荷的长度

3、\r\n(第一个): 区分len 和 json 串

4、\r\n(第二个): 暂时没有其他用,打印方便,debug

cpp 复制代码
static const std::string sep = "\r\n"; // 分隔符

// 添加报头
std::string Encode(const std::string &jsonstr)
{
    int len = jsonstr.size();
    std::string lenstr = std::to_string(len);
    return lenstr + sep + jsonstr + sep;
}

1.5、解析报头

将发送过来的有报头的信息解析成有效信息 ,即去掉前面的长度和分割符与有效信息后面的分隔符

注意:可能没有一个有效信息或者有多个有效信息!

cpp 复制代码
static const std::string sep = "\r\n"; // 分隔符

// 不能带const
// "le
// "len"\r\n"{j [)
// "len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"

std::string Decode(std::string &packagestream)
{
    // 分析
    auto pos = packagestream.find(sep); // 报文流
    if (pos == std::string::npos)
        return std::string(); // 没找到返回空
    std::string lenstr = packagestream.substr(0, pos);
    int len = std::stoi(lenstr); // json长度
    // 计算一个完整的报文应该是多长
    int total = lenstr.size() + len + 2 * sep.size();
    // 传进来的字符串长度小于报文总长,说明没有一个完整的有效信息,返回空
    if (packagestream.size() < total)
        return std::string();
        
    // 提取
    std::string jsonstr = packagestream.substr(pos + sep.size(), len);
    packagestream.erase(0, total); // 从0位置删除total长度

    return jsonstr;
}

2、Service.hpp

Service.hpp中的 IOService类 是用于通信的类,而且内部需要执行传入的回调函数,因此该类需要加一个执行方法的成员!

执行方法的声明:

参数是请求类的指针,返回值是应答类的指针!

using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;

2.1、构造析构函数

构造函数需要传入函数对象,用于初始化成员变量,析构函数无需处理!

cpp 复制代码
IOService(process_t process) :_process(process)
{}

~IOService()
{}

2.2、IOExcute()

IOExcute()函数进行客户端与服务端的通信,并处理发送过来的信息(调用执行方法),有以下7个主要步骤!

1、接收消息

2、报文解析(保证获取至少获得一条有效信息,没有则继续接受消息)

3、反序列化(将字符串转成结构化)

4、业务处理(调用构造函数传入的回调函数)

5、序列化应答

6、添加len长度(报头)

7、发送回去

cpp 复制代码
void IOExcute(SockSPtr sock, InetAddr &addr)
{
    std::string packagestreamqueue; // 写在while循环外,存储信息
    while (true)
    {
        // 1.负责读取
        ssize_t n = sock->Recv(&packagestreamqueue);
        if(n <= 0)
        {
            LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());
            break;
        }

        std::cout << "--------------------------------------------" << std::endl;
        std::cout << "packagestreamqueue: \n" << packagestreamqueue << std::endl;

        // 我们能保证读到的是完整的报文? 不能!
        // 2.报文解析,提取报头和有效载荷
        std::string package = Decode(packagestreamqueue);
        if(package.empty()) continue;
        // 我们能保证读到的是一个完整的报文!!!
        auto req = Factory::BuildRequestDefault();
        std::cout << "package: \n" << package << std::endl;

        // 3.反序列化
        req->Deserialize(package); // 反序列化 将字符串转成结构化

        // 4.业务处理
        auto resp = _process(req); // 业务处理(通过请求,得到应答)

        // 5.序列化应答
        std::string respjson;
        resp->Serialize(&respjson); // 序列化
        std::cout << "respjson: \n" << respjson << std::endl;

        // 6.添加len长度
        respjson = Encode(respjson);
        std::cout << "respjson add header done: \n" << respjson << std::endl;

        // 7.发送回去
        sock->Send(respjson);
    }

此处有一个问题,如果第一次接收消息没有读到完整的报文就会继续接受消息,但是以我们前面写的接收消息函数会清空内容,因此我们需要做稍微的修改

2.3、Recv()

Recv()函数是Socket.hpp文件中TcpServer类的成员函数,接收消息成功之后需要该为拼接旧的内容

cpp 复制代码
// 接收消息
ssize_t Recv(std::string *out) override
{
    char inbuffer[4096];
    ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
    if (n > 0)
    {
        inbuffer[n] = 0;
        // *out = inbuffer;
        *out += inbuffer; // 调整(可能一次读取不成功 | 读取多次)
    }
    return n;
}

3、NetCal.hpp

NetCal.hpp文件中的NetCal类包含回调函数的具体实现

3.1、构造析构函数

该类没有成员变量,构造析构函数无需处理!

cpp 复制代码
NetCal()
{}

~NetCal()
{}

3.2、Calculator()

Calculator() 函数用于网络计算器的计算逻辑!

cpp 复制代码
std::shared_ptr<Response> Calculator(std::shared_ptr<Request> req)
{
    auto resp = Factory::BuildResponseDefault();
    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;
                resp->_desc = "divc zero";
            }
            else
            {
                resp->_result = req->X() / req->Y();
            }
        }
        break;
    case '%':
        {
            if(req->Y() == 0)
            {
                resp->_code = 2;
                resp->_desc = "mod zero";
            }
            else
            {
                resp->_result = req->X() % req->Y();
            }
        }
        break;
    default:
        {
            resp->_code = 3;
            resp->_desc = "illegal operation";
        }
        break;
    }
    return resp;
}

4、ClientMain.cc

该文件用户创建TcpServer类对象,并调用执行函数运行客户端

通信操作主要包括以下七步:

1、序列化

2、添加长度报头字段

3、发送数据

4、读取应答,response

5、报文解析,提取报头和有效载荷

6、反序列化

7、打印结果

cpp 复制代码
#include <iostream>
#include <ctime>
#include "Socket.hpp"
#include "Protocol.hpp"

using namespace socket_ns;

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    SockSPtr sock = std::make_shared<TcpSocket>();
    if (!sock->BuildClientSocket(serverip, serverport))
    {
        std::cerr << "connect error" << std::endl;
        exit(1);
    }

    srand(time(nullptr) ^ getpid());
    const std::string opers = "+-*/%&^!";

    std::string packagestreamqueue;
    while (true)
    {
        // 构建数据
        int x = rand() % 10;
        usleep(x * 1000);
        int y = rand() % 10;
        usleep(x * y * 100);
        char oper = opers[y % opers.size()];

        // 构建请求
        auto req = Factory::BuildRequestDefault();
        req->SetValue(x, y, oper);

        // 1.序列化
        std::string reqstr;
        req->Serialize(&reqstr);

        // 2.添加长度报头字段
        reqstr = Encode(reqstr);

        std::cout << "####################################" << std::endl;

        std::cout << "requset string: \n" << reqstr << std::endl;

        // 3.发送数据
        sock->Send(reqstr);

        while (true)
        {
            // 4.读取应答,response
            ssize_t n = sock->Recv(&packagestreamqueue);
            if (n <= 0)
            {
                break;
            }
            // 我们能保证读到的是完整的报文? 不能!
            // 5.报文解析,提取报头和有效载荷
            std::string package = Decode(packagestreamqueue);
            if (package.empty())
                continue;

            std::cout << "package: \n" << package << std::endl;

            // 6.反序列化
            auto resp = Factory::BuildResponseDefault();
            resp->Deserialize(package);

            // 7.打印结果
            resp->PrintResult();

            break;
        }

        sleep(1);
    }

    sock->Close();

    return 0;
}

5、完整代码

5.1、Protocol.hpp

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

static const std::string sep = "\r\n"; // 分隔符

// 设计一下协议的报头和报文的完整格式
// "len"\r\n"{json}"\r\n --- 完整的报文
// len 有效载荷的长度
// \r\n(第一个): 区分len 和 json 串
// \r\n(第二个): 暂时没有其他用,打印方便,debug

// 添加报头
std::string Encode(const std::string &jsonstr)
{
    int len = jsonstr.size();
    std::string lenstr = std::to_string(len);
    return lenstr + sep + jsonstr + sep;
}

// 不能带const
// "le
// "len"\r\n"{j [)
// "len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"
// "len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"len"\r\n"{json}"\r\n"

std::string Decode(std::string &packagestream)
{
    // 分析
    auto pos = packagestream.find(sep); // 报文流
    if (pos == std::string::npos)
        return std::string(); // 没找到返回空
    std::string lenstr = packagestream.substr(0, pos);
    int len = std::stoi(lenstr); // json长度
    // 计算一个完整的报文应该是多长
    int total = lenstr.size() + len + 2 * sep.size();
    // 传进来的字符串长度小于报文总长,说明没有一个完整的有效信息,返回空
    if (packagestream.size() < total)
        return std::string();

    // 提取
    std::string jsonstr = packagestream.substr(pos + sep.size(), len);
    packagestream.erase(0, total); // 从0位置删除total长度

    return jsonstr;
}

// 协议
class Request
{
public:
    Request()
    {
    }
    Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper)
    {
    }
    // 序列化 将结构化转成字符串
    bool Serialize(std::string *out)
    {
        // 1.自己做: "x oper y" (麻烦)
        // 2.使用现成的库, xml,json(jsoncpp库), protobuf
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;
        Json::FastWriter writer;
        std::string s = writer.write(root);
        *out = s;
        return true;
    }
    // 反序列化 将字符串转成结构化
    bool Deserialize(const std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool res = reader.parse(in, root);

        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _oper = root["oper"].asInt();

        return true;
    }
    void Print()
    {
        std::cout << _x << std::endl;
        std::cout << _y << std::endl;
        std::cout << _oper << std::endl;
    }
    ~Request()
    {
    }
    int X()
    {
        return _x;
    }
    int Y()
    {
        return _y;
    }
    char Oper()
    {
        return _oper;
    }
    void SetValue(int x, int y, char oper)
    {
        _x = x;
        _y = y;
        _oper = oper;
    }

private:
    int _x;
    int _y;
    char _oper; // + - * / %   // x oper y
};

// class request resp = {30,0}
class Response
{
public:
    Response() : _result(0), _code(0), _desc("success")
    {
    }
    // 序列化 将结构化转成字符串
    bool Serialize(std::string *out)
    {
        // 使用现成的库, xml,json(jsoncpp库), protobuf
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;
        root["desc"] = _desc;
        Json::FastWriter writer;
        std::string s = writer.write(root);
        *out = s;

        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();
        _desc = root["desc"].asString();

        return true;
    }
    void PrintResult()
    {
        std::cout << "result: " << _result << ", code: " << _code << ", desc: " << _desc << std::endl;
    }
    ~Response()
    {
    }

public:
    int _result;
    int _code; // 0: success 1: div error 2: 非法操作
    std::string _desc;
};

class Factory
{
public:
    static std::shared_ptr<Request> BuildRequestDefault()
    {
        return std::make_shared<Request>();
    }
    static std::shared_ptr<Response> BuildResponseDefault()
    {
        return std::make_shared<Response>();
    }
};

5.2、Service.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <functional>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Log.hpp"
#include "Protocol.hpp"

using namespace socket_ns;
using namespace log_ns;

using process_t = std::function<std::shared_ptr<Response>(std::shared_ptr<Request>)>;

class IOService
{
public:
    IOService(process_t process) :_process(process)
    {
    }
    void IOExcute(SockSPtr sock, InetAddr &addr)
    {
        std::string packagestreamqueue; // 写在while循环外,存储信息
        while (true)
        {
            // 1.负责读取
            ssize_t n = sock->Recv(&packagestreamqueue);
            if(n <= 0)
            {
                LOG(INFO, "client %s quit or recv error\n", addr.AddrStr().c_str());
                break;
            }

            std::cout << "--------------------------------------------" << std::endl;
            std::cout << "packagestreamqueue: \n" << packagestreamqueue << std::endl;

            // 我们能保证读到的是完整的报文? 不能!
            // 2.报文解析,提取报头和有效载荷
            std::string package = Decode(packagestreamqueue);
            if(package.empty()) continue;
            // 我们能保证读到的是一个完整的报文!!!
            auto req = Factory::BuildRequestDefault();
            std::cout << "package: \n" << package << std::endl;

            // 3.反序列化
            req->Deserialize(package); // 反序列化 将字符串转成结构化

            // 4.业务处理
            auto resp = _process(req); // 业务处理(通过请求,得到应答)

            // 5.序列化应答
            std::string respjson;
            resp->Serialize(&respjson); // 序列化
            std::cout << "respjson: \n" << respjson << std::endl;

            // 6.添加len长度
            respjson = Encode(respjson);
            std::cout << "respjson add header done: \n" << respjson << std::endl;

            // 7.发送回去
            sock->Send(respjson);
        }
    }
    // 测试
    // void IOExcute(SockSPtr sock, InetAddr &addr)
    // {
    //     while (true)
    //     {
    //         std::string message;
    //         ssize_t n = sock->Recv(&message);
    //         if(n > 0)
    //         {
    //             LOG(INFO, "get message from client [%s],message: %s\n", addr.AddrStr().c_str(), message.c_str());
    //             std::string hello = "hello";

    //             sock->Send(hello);
    //         }
    //         else if(n == 0)
    //         {
    //             LOG(INFO, "client %s quit\n", addr.AddrStr().c_str());
    //             break;
    //         }
    //         else 
    //         {
    //             LOG(ERROR, "read error\n", addr.AddrStr().c_str());
    //             break;
    //         }
    //     }
    // }
    ~IOService()
    {
    }
private:
    process_t _process;
};

5.3、ClientMain.cc

cpp 复制代码
#include <iostream>
#include <ctime>
#include "Socket.hpp"
#include "Protocol.hpp"

using namespace socket_ns;

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    SockSPtr sock = std::make_shared<TcpSocket>();
    if (!sock->BuildClientSocket(serverip, serverport))
    {
        std::cerr << "connect error" << std::endl;
        exit(1);
    }

    srand(time(nullptr) ^ getpid());
    const std::string opers = "+-*/%&^!";

    std::string packagestreamqueue;
    while (true)
    {
        // 构建数据
        int x = rand() % 10;
        usleep(x * 1000);
        int y = rand() % 10;
        usleep(x * y * 100);
        char oper = opers[y % opers.size()];

        // 构建请求
        auto req = Factory::BuildRequestDefault();
        req->SetValue(x, y, oper);

        // 1.序列化
        std::string reqstr;
        req->Serialize(&reqstr);

        // 2.添加长度报头字段
        reqstr = Encode(reqstr);

        std::cout << "####################################" << std::endl;

        std::cout << "requset string: \n" << reqstr << std::endl;

        // 3.发送数据
        sock->Send(reqstr);

        while (true)
        {
            // 4.读取应答,response
            ssize_t n = sock->Recv(&packagestreamqueue);
            if (n <= 0)
            {
                break;
            }
            // 我们能保证读到的是完整的报文? 不能!
            // 5.报文解析,提取报头和有效载荷
            std::string package = Decode(packagestreamqueue);
            if (package.empty())
                continue;

            std::cout << "package: \n" << package << std::endl;

            // 6.反序列化
            auto resp = Factory::BuildResponseDefault();
            resp->Deserialize(package);

            // 7.打印结果
            resp->PrintResult();

            break;
        }

        sleep(1);
    }

    sock->Close();

    return 0;
}

5.4、Makefile

cpp 复制代码
.PHONY:all
all:calserver calclient

calserver:ServerMain.cc 
	g++ -o $@ $^ -std=c++14 -ljsoncpp

calclient:ClientMain.cc 
	g++ -o $@ $^ -std=c++14 -ljsoncpp

.PHONY:clean 
clean:
	rm -rf calserver calclient
相关推荐
Biomamba生信基地1 分钟前
R语言基础| 回归分析
开发语言·回归·r语言
西柚与蓝莓3 分钟前
报错:{‘csrf_token‘: [‘The CSRF token is missing.‘]}
前端·flask
AGI学习社10 分钟前
2024中国排名前十AI大模型进展、应用案例与发展趋势
linux·服务器·人工智能·华为·llama
黑客-雨15 分钟前
从零开始:如何用Python训练一个AI模型(超详细教程)非常详细收藏我这一篇就够了!
开发语言·人工智能·python·大模型·ai产品经理·大模型学习·大模型入门
Pandaconda20 分钟前
【Golang 面试题】每日 3 题(三十九)
开发语言·经验分享·笔记·后端·面试·golang·go
半盏茶香21 分钟前
扬帆数据结构算法之雅舟航程,漫步C++幽谷——LeetCode刷题之移除链表元素、反转链表、找中间节点、合并有序链表、链表的回文结构
数据结构·c++·算法
加油,旭杏24 分钟前
【go语言】变量和常量
服务器·开发语言·golang
行路见知24 分钟前
3.3 Go 返回值详解
开发语言·golang
xcLeigh28 分钟前
WPF实战案例 | C# WPF实现大学选课系统
开发语言·c#·wpf
哎呦,帅小伙哦29 分钟前
Effective C++ 规则41:了解隐式接口和编译期多态
c++·effective c++