【计算机网络】Socket网络编程

news2025/2/22 15:48:49
在这里插入图片描述

💻文章目录

  • 📄前言
  • Socket编程基础
    • 概念
    • 工作原理
  • Socket API介绍
    • socket函数
    • 绑定、监听函数
    • accept、connect
    • 接受/发送函数
  • Socket API的应用
    • Socket类与其派生类的设计
    • 服务器与客户端的设计
    • 使用
  • 📓总结


📄前言

现今我们的日常生活当中,网络已经成为了必不可少的存在,大到覆盖全世界的互联网,小到身边的各种电器,可以说网络无处不在。我们作为一名程序员,如果对网络不甚了解,那么注定会度过一个相对失败的一生,需要利用网络进行通信的应用正变得越来越多,企业对程序员网络知识的需求也越发变得重要,因此,学习网络一定会对你有所帮助。

Socket编程基础

概念

Socket 的中文名可以译为套接字、插座,就像它的直译插座一样,Socket 是两台机器网络通讯的端点,只要使用Socket就能连接两台机器,从而实现数据的传输、交换。

在介绍Socket是如何工作前,我们需要先了解一下网络的基本术语。

  • 基础术语

  • IP地址:IP地址是设备在网络上的标识符,要进行网络通信就必须拥有一个IP地址

  • 端口:端口的设计是为了让网络数据正确发送到应用程序,计算机通过IP+端口号来确保数据收发正确。

  • 协议:协议是定义数据如何在网络传输的规则,Socket编程中会接触到的协议有UDP、TCP协议。

工作原理

正如上方所说Socket是网络通信的端点,Socket的工作原理是基于C—S模型,即必定会有客户端与服务端的存在。既然要通信,那么就一定会有协议的存在。socket 有面向字节流协议的SOCK_STREAM面向数据报的 SOCK_DGRAM直接将数据发往IP层的原始套接字 SOCK_RAW。

其实 SOCK_STREAM 与 SOCK_DGRAM 就已经可以完成99%的网络通讯设计,毕竟现在网络上主流的协议也就是UDP和TCP协议。虽然协议本身区别很大,但在应用层的使用上,大体还是差不多的。

  • 服务器端的工作流程

    1. 创建套接字。

    2. 绑定地址。

    3. 接受数据。

    4. 发送数据。

  • 客户端的工作流程

    1. 创建套接字
    2. 提前确定远端的地址、端口
    3. 发送数据
    4. 接受数据。

Socket API介绍

socket函数

socket函数是系统用于创建套接字描述符的接口,该函数会返回一个文件描述符,之后网络的通信便围绕着这个文件描述符进行。

#include <sys/socket.h>

//函数原型	
int socket(int domain, int type, int protocol);
// 返回值为文件描述符
int fd = socket(AF_INET, SOCK_STREAM, 0);
  • 参数选项
    • domain: 用于指定通信域,常用的选项为AF_INET(指定使用IPV4通信),AF_INET6(指定IPV6通信),AF_UNIX(指定本地进程间通信)。
    • type: 用于指定socket的类型。常用的选项为SOCK_STREAM(提供可靠的流传输服务,也就是TCP),SOCK_DGRAM(提供不可靠的数据报服务,也就是UDP)。
    • protocl: 用于指定是否使用特殊协议,一般设为0。

绑定、监听函数

bind 函数用于让程序绑定一个固定的端口号,使套接字只从该端口号接受/发送数据,一般用于服务器显示绑定地址,客户端通过系统自动分配。listen 函数用于监听端口号,等待客户端的连接。

#include <sys/socket.h>

int listen(int sockfd, int backlog);	//成功返回0


int bind(int sockfd, const struct sockaddr *addr,	//成功返回0
                socklen_t addrlen);

/* socketaddr是C语言历史缘由而留下来的结构体,因为当初C语言还不支持
void* 类型,所以设计出了sockaddr类型,以应对不同的选项。 */

//以下是socketaddr家族
struct sockaddr {	//基础类型
   sa_family_t sa_family;	
   char        sa_data[14];
}

struct sockaddr_in {	
	__uint8_t       sin_len;	//无特殊要求不会指定值
	sa_family_t     sin_family;	//设置协议家族(如AF_INET、AF_UNIX)
	in_port_t       sin_port;	//设置端口
	struct  in_addr sin_addr;	//设置IP地址
	char            sin_zero[8];	
};

//socket_in6  用于IPV6设置。		
  • bind 参数选项
    • sockfd: socket 文件描述符。
    • addr: 绑定socket_addr。
    • addrlen: 指定socket_addr的长度。
  • listen 参数选项
    • sockfd: 指定需要监听的套接字。
    • backlog: 用于指定套接字中处于排队TCP连接数(还未得到处理),用于防止 SYN 泛洪攻击。

accept、connect

accept 和 connect 这两个函数,它们一般用于TCP协议,因为UDP是无连接的所以用不上(connect除外)。

accept 函数用于接受一个TCP连接,并返回它的套接字描述符,之后的读写则往该套接字描述符进行。注意,使用前需要先建立好监听状态。

connect 函数用于连接一个远端的服务器,成功则返回0.

#include <sys/types.h>
#include <sys/socket.h>
 
int
accept(int socket, struct sockaddr *address, socklen_t *address_len);


int	//connect函数用于连接服务器
connect(int socket, const struct sockaddr *address, socklen_t address_len);
//UDP连接也可以使用connect函数,一般用于为UDP的套接字绑定一个固定的远端地址,从此该套接字就只能接受该地址的数据(过滤)。
  • accept 的参数选项

    • socket: 指定需要接受数据的套接字接口
    • address: 该结构用于接收连接方的协议地址。如果不想要远端的信息,可以设null。
    • address_len: 用于指定address的长度。
  • connect 的参数选项

    • socket: socket 文件描述符
    • address: 指向存放目标服务器地址的信息。
    • addlen: 指定addr结构体的长度。

接受/发送函数

unix like 系统中,UDP与TCP协议数据的收发所使用的函数有些许的差别,主要就是是否需要指定远端的地址、端口的差别,TCP方面因为已经通过 accpt 创建了一个包含远端信息的套接字,而UDP是无连接的,所以需要传入一个包含远端信息的sockaddr 结构体。

TCP协议所使用的接发函数:

#include <sys/socket.h>

// recv send 参数都是一致的。
ssize_t recv(int sockfd, void buf, size_t len,
                int flags);		

ssize_t send(int sockfd, const void buf, size_t len, int flags);
  • 参数选项:
    • sockfd: 指定远端的套接字接口
    • buf: 需要接受/发送的数据
    • len: 数据的长度
    • flags: 可提供额外的控制选项,如指定阻塞等待(MSG_DONTWAIT)

UDP协议所使用的接收/发送函数:

ssize_t recvfrom(int sockfd, void buf, size_t len,
                int flags,	
                struct sockaddr * src_addr,	//可设为空,但如果要发送数据则要存储该结构体
                socklen_t * addrlen);	

ssize_t sendto(int sockfd, const void buf, size_t len, int flags,
              const struct sockaddr *dest_addr, socklen_t addrlen);

// 额外所需要用的
uint16_t
htons(uint16_t hostshort);	//将主机序列转为网络序列,网络数据使用大端传送

unsigned long 
inet_addr(const char *cp);	//用于将字符串ip地址转为网络字节序的二进制形式
  • 参数选项:
    • sockfd: 指定需要接收/发送数据的套接字。
    • buf: 数据所存放的内存
    • flags: 发送的选项,与TCP一样
    • addr: 如果需要发送数据,则需要在recvfrom指定中存储的sockadd结构体
    • addrlen: sockaddr 的长度,如 sockaddr_in 。

Socket API的应用

介绍完了基本的函数信息,也该到实践的环节了,但如果只是简单写下函数的使用方法,也并没有什么实际意义,那么不如构建一个Socket编程的模版,这样以来使用 Socket 编程就不必再敲重复的代码,而且也能提高对设计模式的理解。

代码的大致样子:

派生
派生
RemoteData
-sockaddr_in _addr
-std::string _data
-int _socket
«abstract»
Socket
-int _socket
-int _port
-std::string _ip
-sockaddr_in _addr
+virtual ~Socket()
+virtual bool BindSocket()
+virtual bool Accept(RemoteData* data)
+virtual bool ConnectSocket()
+virtual bool CreateSocket()
+virtual bool RecvData(RemoteData*)
+virtual bool SendData(RemoteData*)
+void BuildServer()
+void BuildClient()
UdpSocket
+bool CreateSocket()
+bool RecvData(RemoteData* remoteData)
+bool SendData(RemoteData* data)
TcpSocket
+bool CreateSocket()
+bool BindSocket()
+bool RecvData(RemoteData* remoteData)
+bool SendData(RemoteData* data)

Socket类与其派生类的设计

Socket 基类是TcpSocket、UdpSocket的抽象基类,用于提高代码的复用性。

#include <iostream>
#include <utility>
#include <arpa/inet.h>

#define MAX_BUFFER_SIZE 1024

struct RemoteData   //用于获取远端数据
{
public:
    RemoteData() = default;
    explicit RemoteData(sockaddr_in& client, int fd = -1)
        :_addr(client), _socket(fd)
    {
     	_data.resize(MAX_BUFFER_SIZE);       
    }
    ~RemoteData() = default;

    sockaddr_in _addr;
    std::string _data;	// 改用 char buffer[];
    int _socket = -1;
};

// 使用模版方法进行封装
class Socket
{
public:
    Socket(std::string ip, int port)
            :_ip(std::move(ip)), _port(port)
    {}

    void BuildServer()  //用于服务器的构造
    {
        bool socket = CreateSocket();
        bool bindSocket = BindSocket();
        if(!(socket && bindSocket))
        {
            std::cerr << "socket build failed\n";
            return;
        }
        std::cout << "socket build success\n";
    }

    void BuildClient()  // 用于客户端的构造
    {
        bool socket = CreateSocket();
        bool connectSocket = ConnectSocket();
        if(!(socket && connectSocket))
        {
            std::cerr << "socket build failed\n";
            return;
        }

        std::cout << "socket build success\n";
    }

protected:
    virtual bool BindSocket()   // 用于绑定套接字
    {
        if(::bind(_socket, (struct sockaddr*)&_addr, sizeof(_addr)) < 0)
            return false;

        return true;
    }

    virtual bool Accept(RemoteData* data)   // 用于接受套接字
    {
        socklen_t len = sizeof (sockaddr_in);
        sockaddr_in* addr =  &data->_addr;
        data->_socket = accept(_socket, (sockaddr*)addr, &len);
        if(data->_socket < 0)
        {
            std::cerr << "accept failed " << strerror(errno) << std::endl;
            return false;
        }
        return true;
    }

    virtual bool ConnectSocket()    // 用于连接套接字
    {
        if(::connect(_socket, (struct sockaddr*)&_addr, sizeof(_addr)) < 0)
            return false;

        return true;
    }

virtual ~Socket() = default;            // 基类继承需要把析构函数设为虚函数。
    virtual bool CreateSocket() = 0;        // 创建套接字
    virtual bool RecvData(RemoteData*) = 0; // 接收数据
    virtual bool SendData(RemoteData*) = 0; // 发送数据
protected:
    int _socket=-1;
    int _port{};
    std::string _ip{};
    sockaddr_in _addr{};
};

TcpSocket 和 UdpSocket

TcpSocket 与 UdpSocket 就如其名,对应了TCP与UDP的socket编程设计。

class UdpSocket : public Socket
{	// UdpSocket如果使用connect函数,可以使用send、recv来代替sendto、recvfrom
public:
    UdpSocket(std::string ip, int port)
            : Socket(std::move(ip), port)	//初始化基类
    {}

    ~UdpSocket() override = default;

protected:
    bool CreateSocket() override
    {
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);	//将主机字节序列转为网络字节序列
        _addr.sin_addr.s_addr = inet_addr(_ip.c_str());

        _socket = socket(AF_INET, SOCK_DGRAM, 0);
        if(_socket < 0) return false;	//错误处理

        return true;
    }

    bool RecvData(RemoteData* remoteData) override
    {
        char* buffer = remoteData->_data.data();
        socklen_t len = sizeof(sockaddr_in);
        sockaddr_in* client = &remoteData->_addr;
        ssize_t n = recvfrom(_socket, buffer, MAX_BUFFER_SIZE-1, 0, (struct sockaddr*)client, &len);
        if(n == 0)
        {	
            std::cout << "client close\n";
            return false;
        }
        else if(n > 0)
        {
            buffer[n] = '\0';
            std::cout << "recv data : " << buffer << std::endl;
            return true;
        }
        else
        {
            std::cerr << "recvfrom error\n";
            return false;
        }
    }

    bool ConnectSocket() override
    {
        return true;
    }

    bool SendData(RemoteData* data) override
    {
        char* buffer = data->_data.data();
        sockaddr_in* client = &data->_addr;
        ssize_t n = sendto(_socket, buffer, strlen(buffer), 0, (struct sockaddr*)client, sizeof(*client));
        if(n < 0)
        {
            std::cerr << "sendto error: " << strerror(errno) << std::endl;
            return false;
        }
        return true;
    }
};

class TcpSocket : public Socket
{
public:
    TcpSocket(std::string ip, int port)
            : Socket(std::move(ip), port)
    {}

    ~TcpSocket() override = default;

    bool CreateSocket() override
    {	
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = inet_addr(_ip.c_str());

        _socket = socket(AF_INET, SOCK_STREAM, 0); //使用SOCK_STREAM
        if(_socket < 0) return false;
        return true;
    }

    bool BindSocket() override
    {
        if(::bind(_socket, (struct sockaddr*)&_addr, sizeof(_addr)) )
        {
            std::cerr << "bind failed\n";
            return false;
        }
        
        listen(_socket, 5); //TCP服务器需要监听端口

        return true;
    }

    bool RecvData(RemoteData* remoteData) override
    {
        int socket = remoteData->_socket;
        char* buffer = remoteData->_data.data();
        ssize_t n = recv(socket, buffer, MAX_BUFFER_SIZE-1, 0);
        if(n == 0)
        {
            std::cout << "client close\n";
            return false;
        }
        else if(n > 0)
        {
            buffer[n] = '\0';
            std::cout << "recv data : " << buffer << std::endl;
            return true;
        }
        else
        {
            std::cerr << "recv error\n";
            return false;
        }
    }

    bool SendData(RemoteData* data) override
    {
        int socket = data->_socket;
        char* buffer = data->_data.data();
        ssize_t n = send(socket, buffer, strlen(buffer), 0);
        if(n < 0)
        {
            std::cerr << "send error\n";
            return false;
        }
        return true;
    }
};

服务器与客户端的设计

服务器设计

#include <memory>
#include <print>
#include "Socket.hpp"
//#include "ThreadPool.hpp"	//不懂线程池的可以去看看我写的线程池博客

class UdpServer : protected UdpSocket
{
public:
    UdpServer(int port, std::function<void(RemoteData*)> handler)
            : UdpSocket("0.0.0.0", port), _handle(std::move(handler))
    {
        BuildServer();	//构建Socket
    }

    void Run(RemoteData* data)
    {
        _handle(data);	//业务处理函数
        SendData(data);
    }

    void start()
    {
        std::string msg;
        while (true)
        {
            sockaddr_in client{};
            std::shared_ptr<RemoteData> data = std::make_shared<RemoteData>(RemoteData(client));
            if(!RecvData(data.get()))
                continue;
//            ThreadPool::GetInstance()->enqueue([this, data]{ Run(data.get());});
            Run(data.get());
        }
    }

private:
    std::function<void(RemoteData*)> _handle;	//业务处理函数
};

class TcpServer : TcpSocket	// 注意:这个TCP协议需要进行粘包处理。
{
public:
    TcpServer(int port, std::function<void(RemoteData*)> handler)
        : TcpSocket("0.0.0.0", port), _handle(std::move(handler))
    {
        BuildServer();
    }

    void ThreadRun(RemoteData* data)
    {
        while (true)
        {
            if(!RecvData(data)) break;
            _handle(data);
            if(!SendData(data)) break;
        }
        close(data->_socket);
    }

    void start()
    {
        std::string msg;
        while (true)
        {
            sockaddr_in client{};
            std::shared_ptr<RemoteData> data = std::make_shared<RemoteData>(RemoteData(client));
            if(!Accept(data.get()))
                break;
//            ThreadPool::GetInstance()->enqueue([this, data]{ ThreadRun(data.get());});	//最好使用多线程进行业务处理,否则将只能处理一条连接
            ThreadRun(data.get());
        }
    }
private:
    std::function<void(RemoteData*)> _handle;
};

客户端设计

class UdpClient : public UdpSocket
{
public:
    UdpClient(std::string ip, int port, std::function<void(RemoteData*)> func)
        : UdpSocket(std::move(ip), port), _func(std::move(func))
    {
        BuildClient();	//
    }

    void start()
    {
        while (true)
        {
            std::shared_ptr<RemoteData> data = std::make_shared<RemoteData>(RemoteData(_addr));
            _func(data.get());
            SendData(data.get());
            RecvData(data.get());
        }
    }

private:
    std::function<void(RemoteData*)> _func;
};

class TcpClient : public TcpSocket
{
public:
    TcpClient(std::string ip, int port, std::function<void(RemoteData*)> func)
        : TcpSocket(std::move(ip), port), _func(std::move(func))
        {
            BuildClient();
        }

    void start()
    {
        while (true)
        {
            std::shared_ptr<RemoteData> data = std::make_shared<RemoteData>(RemoteData(_addr, _socket));
            _func(data.get());
            SendData(data.get());
            RecvData(data.get());
        }
    }

private:
    std::function<void(RemoteData*)> _func;
};

使用

//client.cpp
#include "Client.hpp"

void handler(RemoteData* data)
{
    std::cout << "client: ";
    std::cin >> data->_data;
}

int main()
{					// 使用本地环回进行通信
    UdpClient client("127.0.0.1", 8888, handler);
    client.start();
    return 0;
}

//server.cpp
#include "Server.hpp"

void handler(RemoteData* data)
{}

int main() {
    ThreadPool* pool = ThreadPool::GetInstance(5);
    UdpServer server(8888, std::function<void(RemoteData*)>(handler));
    server.start();
    return 0;
}

📓总结

学习Socket编程只是迈入网络编程的第一步,计算机网络中还有TCP、UDP协议、IP协议等各种难关来等着我们来一一攻破。虽然你可能觉得学习Socket编程对学习TCP/IP协议这些没什么帮助,学校的老师也从来不会从代码开始攻坚计算机网络,但计算机网络就应该自顶至下,从应用层的应用开始学起。

📜博客主页:主页
📫我的专栏:C++
📱我的github:github

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【爬虫基础1.1课】——requests模块上

目录索引 requests模块的作用&#xff1a;实例引入&#xff1a; 特殊情况&#xff1a;锦囊1&#xff1a;锦囊2: 这一个栏目&#xff0c;我会给出我从零开始学习爬虫的全过程。感兴趣的小伙伴可以关注一波&#xff0c;用于复习和新学都是不错的选择。 那么废话不多说&#xff0c…

sqli-labs 第十七关

目录 找注入点&#xff1a; 源码分析&#xff1a; 测试&#xff1a; 奇怪现象&#xff1a; &#xff08;1&#xff09;&#xff1a;当我们输入的密码为字符进行注入时。 &#xff08;2&#xff09;&#xff1a;当我们输入的密码为整数时。 产生原因&#xff1a; 解决方法…

孙宇晨对话大公网:香港Web3政策友好环境示范意义重大

日前,全球知名华文媒体大公网发布《湾区web3大有可为》重磅系列报道。报道通过对中国香港与大湾区其他城市Web3政策、行业创新和生态建设等方面的梳理,以及对行业领袖和重要行业机构的走访,全面展现了在大湾区一体化发展的背景下,Web3等数字经济模式在该地区的长远发展潜力。 …

基于Idea搭建Android开发环境

文章目录 下载SDK ManagerAndroid SDK Platform-toolsAndroid SDK Build-toolsAndroid SDKAndroid SDK Extras IDEA设置创建TestApp导入Android Studio创建的项目 下载SDK Manager SDK Manager是Google提供的&#xff0c;专门用于下载/管理&#xff0c;安卓开发中需要用到的工…

【MYSQL】一颗B+树可以保存多少条数据

引言 事万物都有自己的单元体系&#xff0c;若干个小单体组成一个个大的个体。就像拼乐高一样&#xff0c;可以自由组合。所以说&#xff0c;如果能熟悉最小单元&#xff0c;就意味着我们抓住了事物的本事&#xff0c;再复杂的问题也会迎刃而解。 存储单元 存储器范围比较大…

鸿蒙ArkUI开发:常用布局【交叉轴】

交叉轴 垂直于主轴方向的轴线。Row容器交叉轴为纵向&#xff0c;Column容器交叉轴为横向。通过alignItems属性设置子元素在交叉轴&#xff08;排列方向的垂直方向&#xff09;上的对齐方式alignSelf属性用于控制单个子元素在容器交叉轴上的对齐方式&#xff0c;其优先级高于al…

鸿蒙内核源码分析(内核态锁篇) | 如何实现快锁Futex(下)

本篇为快锁下篇&#xff0c;说清楚快锁在内核态的实现&#xff0c;解答以下问题&#xff0c;它们在上篇的末尾被提出来。 鸿蒙内核进程池默认上限是64个&#xff0c;除去两个内核进程外&#xff0c;剩下的都归属用户进程&#xff0c;理论上用户进程可以创建很多快锁&#xff0…

Wikimedia To Opensearch

概览 Wikimedia ⇒ Kafka ⇒ OpensearchJava Library&#xff1a;OKhttp3和OkHttp EventSource&#xff1b;生产者&#xff1a;Wikimedia&#xff1a;WikimediaChangeHandler和WikimediaChangeProducer&#xff1b;消费者&#xff1a;Opensearch&#xff1a;OpenSearchConsume…

【知识碎片】2024_05_13

本文记录了两道代码题【自除数】和【除自身以外数组的乘积】&#xff08;利用了前缀积和后缀积&#xff0c;值得再看&#xff09;&#xff0c;第二部分记录了关于指针数组和逗号表达式的两道选择题。 每日代码 自除数 . - 力扣&#xff08;LeetCode&#xff09; /*** Note: T…

k8s StatefulSet

Statefulset 一个 Statefulset 创建的每个pod都有一个从零开始的顺序索引&#xff0c;这个会体现在 pod 的名称和主机名上&#xff0c;同样还会体现在 pod 对应的固定存储上。这些 pod 的名称是可预知的&#xff0c;它是由 Statefulset 的名称加该实例的顺序索引值组成的。不同…

JUC下的ThreadLocalRandom详解

ThreadLocalRandom 是Java并发包&#xff08;java.util.concurrent&#xff09;中提供的一个随机数生成器类&#xff0c;它是从Java 7开始引入的。相较于传统的Math.random()或Random类&#xff0c;ThreadLocalRandom更适用于多线程环境&#xff0c;因为它为每个线程维护了一个…

汇昌联信电商:拼多多新手怎么做店铺的免费流量会慢慢起来?

在拼多多上开店&#xff0c;新手们往往面临着如何吸引免费流量的挑战。毕竟&#xff0c;流量是店铺生存和发展的血脉&#xff0c;没有流量&#xff0c;就没有销量&#xff0c;店铺也就失去了生命力。那么&#xff0c;作为拼多多新手&#xff0c;如何做才能让店铺的免费流量慢慢…

设计模式Java实现-迭代器模式

✨这里是第七人格的博客✨小七&#xff0c;欢迎您的到来~✨ &#x1f345;系列专栏&#xff1a;设计模式&#x1f345; ✈️本篇内容: 迭代器模式✈️ &#x1f371; 本篇收录完整代码地址&#xff1a;https://gitee.com/diqirenge/design-pattern &#x1f371; 楔子 很久…

人脸识别技术在访客管理中的应用

访客办理体系&#xff0c;能够使用于政府、戎行、企业、医院、写字楼等众多场所。在办理时&#xff0c;需求对来访人员身份进行精确认证&#xff0c;才能保证来访人员的进入对被访单位不被外来风险入侵。在核实身份时&#xff0c;比较好的方法就是选用人脸辨认技能&#xff0c;…

RAG应用中的路由模式

依据的用户查询意图在 RAG 应用程序使用“路由控制模式”可以帮助我们创建更强大的 RAG 应用程序。我们通常希望用户能够访问的数据可以来自各种来源,如报告、文档、图片、数据库和第三方系统。 对于基于业务的 RAG 应用程序,我们可能还希望用户能够与其它业务系统进行交互,…

基于SpringBoot+Vue的教师个人成果管理系统

初衷 在后台收到很多私信是咨询毕业设计怎么做的&#xff1f;有没有好的毕业设计参考? 能感觉到现在的毕业生和当时的我有着同样的问题&#xff0c;但是当时的我没有被骗&#xff0c; 因为现在很多人是被骗的&#xff0c;还没有出学校还是社会经验少&#xff0c;容易相信别人…

嗨动PDF编辑器怎么进行PDF编辑?看完了解

嗨动PDF编辑器怎么进行PDF编辑&#xff1f;PDF作为一种通用的文档格式&#xff0c;被广泛应用于商务交流、学术研究、电子出版等多个领域。但当面对需要修改或调整PDF文件内容时&#xff0c;就需要一款功能强大的PDF编辑器。嗨动PDF编辑器&#xff0c;它拥有对用户友好的界面设…

对中介者模式的理解

目录 一、场景1、题目 【[来源](https://kamacoder.com/problempage.php?pid1094)】1.1 题目描述1.2 输入描述1.3 输出描述1.4 输入示例1.5 输出示例 二、不采用中介者设计模式1 代码2 问题 三、中介者设计模式1 代码2 更好的例子 四、个人思考 一、场景 设计模式不是银弹&am…

Postman基础功能-变量设置与使用

如果你因失去太阳而流泪&#xff0c;那你也将失去群星了。大家好&#xff0c;在 API 测试的广袤世界中&#xff0c;Postman 犹如一座闪耀的灯塔&#xff0c;为我们指引着前行的方向。而其中的全局变量、集合变量和环境变量&#xff0c;更是如同隐藏的宝藏&#xff0c;蕴含着巨大…

从3D模型到渲染:完整的流程指南---模大狮模型网

在当今数字化时代&#xff0c;3D模型和渲染技术在各个领域中扮演着至关重要的角色&#xff0c;从影视制作到建筑设计&#xff0c;从游戏开发到工程模拟。了解如何将3D模型转化为逼真的渲染图像是数字创意领域从业者的关键技能之一。本文将为您介绍从3D模型到渲染的完整流程&…