【Linux系统】进程间通信:实现命名管道通信




在这里插入图片描述





认识命名管道通信

命名管道通信的结构图示:


在这里插入图片描述



图中的 ServerClient 是不同的进程, Server 负责发送数据, Client 则是接收数据,进程之间通过命名管道进行数据通信



准备工作:


创建以下文件

Server.hpp  #服务器类的头文件,包含服务器类的声明和各个函数的声明
Server.cc	#服务器类的实现文件,包含Server.hpp中声明的方法的具体实现代码。
Client.hpp	#客户端类的头文件,包含客户端类的声明和各个函数的声明
Client.cc	#客户端类的实现文件,包含Client.hpp中声明的方法的具体实现代码。
Makefile	#构建项目的Makefile文件,定义了编译规则、依赖关系、编译选项等信息,用于自动化编译过程。


Makefile


只能生成一个: 注意,我们需要形成两个可执行文件,而如果只是下面这样写,make 指令从上到下扫描只会形成一个可执行文件就停下了!

SERVER=server
CLIENT=client
CC=g++
SERVER_SRC=Server.cc
Client_SRC=Client.cc

$(SERVER):$(SERVER_SRC)
	$(CC) -o $@ $^ -std=c++11

$(CLIENT):$(Client_SRC)
	$(CC) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f $(SERVER) $(CLIENT)


若想形成两个可执行文件,则需要加上下面这段:

.PHONY:all
all:$(SERVER) $(CLIENT)

原理:若想形成目标 all, 则需要获取到 $(SERVER) $(CLIENT),则需要执行后面两组操作形成两个可执行文件


SERVER=server
CLIENT=client
CC=g++
SERVER_SRC=Server.cc
Client_SRC=Client.cc

.PHONY:all
all:$(SERVER) $(CLIENT)
    
$(SERVER):$(SERVER_SRC)
	$(CC) -o $@ $^ -std=c++11

$(CLIENT):$(Client_SRC)
	$(CC) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f $(SERVER) $(CLIENT)


命名管道通信理论


我们两个进程共享同一份文件资源:

这份公共资源:一般要让指定的一个进程先行创建,其他进程再获取该资源并共享使用

  • 创建&&使用
  • 获取&&使用

本次我们会让 Server 进程创建命名管道,Client 进程只需获取并使用该文件



实现命名管道通信


创建命名管道


代码创建管道:本质也是新建文件,使用函数 mkfifo 创建命名管道 FIFO 文件,其中的mode 为创建文件的权限

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


因为共享一个文件,因此我们可以将该共享文件资源的相关信息封装成一个头文件,其实就是共享区域



创建 Comm.hpp


加入该共享文件资源的相关信息:

  • 文件名路径
  • 创建该命名管道需要的 mode

意思是:

Server 进程创建命名管道,需要指定文件路径和文件名,如 ./myfifo ,表示在当前目录下创建一个命名管道名为 myfifo

Client 进程只需获取并使用该文件,也需要指定文件路径和文件名,如 ./myfifo

那么这两个进程都需要使用同一个文件名用于创建或打开该文件,我们可以将该 “同类” 资源放在 Comm.hpp 中,表示 Server 进程和 Client 进程共享这一份即可



创建 Server


(1)由 Server 创建共享命名管道文件

就直接写在构造函数里面:创建命名管道文件

因为命名管道通信的基础是需要一个命名管道,因此, Server 类创建的同时,就应该创建一个命名管道文件,因此可以放于该类的构造函数中

在这里插入图片描述



运行结果当然是成功创建文件

在这里插入图片描述


**优化一下:**既然需要给自定义 mode,则跟着也要将缺省权限:权限掩码清零:umask(0)



(2)由 Server 释放共享命名管道文件

既然 Server 创建该命名管道文件,则也需要由该进程释放该文件

删除该命名文件的程序写法:使用函数 unlink

在这里插入图片描述


删文件的本质:就是在指定目录下,将该文件名和 inode 的映射关系移除,根据 inode 将这个文件的硬链接数 -1,当硬链接数为零时自然就会被系统释放掉该文件所有资源

对于系统命令 rm

当你使用 rm 命令删除一个文件时,实际上是将该文件名从目录中移除,断开了文件名和 inode 之间的映射关系。

这个操作并不会立即删除文件的实际数据,而是减少了该文件的 inode 的硬链接数。


这就是为什么 unlink 也可以用于删除文件


将删除文件的操作放于 Server 的析构函数处:

~Server()
{
    int ret = ::unlink(gfileName.c_str());
    if(ret < 0) {
        std::cerr << "unlink failed!" << '\n';
    }
    std::cout << "unlink success" << '\n';
}



建立命名管道的通信,需要先创建一个命名管道,当通信结束后,需要删除该命名管道文件

这些关于命名管道通信的 ”环境配置“ 操作,可以额外封装成一个 Init

注:其实是否需要封装 Init 类 都可以,看自己选择,喜欢原来那样在 Server 类中创建和删除该文件也行



封装 Init


  • 封装到 Init :为了代码直观性,我们将创建和关闭管道的工作封装成一个 Init 类,而不是在 Server 类中
  • 封装 Init 类后,同时定义一个全局变量 Init init :该全局变量生命周期随程序,当程序执行时,文件自动创建,当程序结束时,文件自动销毁,这样更加容易理解
  • Server 还算作文件创建者:这个 Init 类是放到 Server.hpp 中的,意在还是让 Server 作为文件创建者

class Init
{
public:
    Init()
    {
        // 创建共享命名管道文件
        // int mkfifo(const char *pathname, mode_t mode);
        umask(0);
        int ret = ::mkfifo(gfileName.c_str(), gmode);
        if(ret < 0) {
            std::cerr << "mkfifo failed!" << '\n';
        }
        std::cout << "mkfifo is successfully created" << '\n';
        sleep(3);
    }
    ~Init()
    {
        int ret = ::unlink(gfileName.c_str());
        if(ret < 0) {
            std::cerr << "unlink failed!" << '\n';
        }
        std::cout << "unlink success" << '\n';
    }
};


Init init;  // 定义全局变量


Server 类的打开和关闭管道文件

Server 类 和 Client 类本质上就是要读写操作一个管道文件,因此就需要获取该管道文件的 fd,则将 fd 设置为类成员会方便很多

class Server
{
public:
    Server()
        : _fd(g_default_fd)
    {
    }
    ~Server()
    {
    }

    // 打开文件函数
    bool OpenPipe()
    {
        // int open(const char *pathname, int flags);
        _fd = ::open(gfileName.c_str(), O_RDONLY); // 只读方式打开:默认文件一定存在
        if (_fd < 0)
        {
            std::cerr << "open pipe filed !" << '\n';
            return false;
        }
        return true;
    }
    // 关闭文件函数
    void ClosePipe()
    {
        if (_fd > 0)
            ::close(_fd);
    }

private: 
    int _fd;
};


Server 类的接收数据函数

Server 需要从命名管道中读取由 Client 发送的数据

// 接收数据函数
// 编程规范
// std::string *: 输出型参数
// const std::string & : 输入型参数
// std::string & : 输入输出型参数
int RecvPipe(std::string *out)
{
    char buff[1024];
    // ssize_t read(int fd, void *buf, size_t count);
    ssize_t n = read(_fd, buff, sizeof(buff)-1); // -1目的:读字符串数据出来最后一位留作 '\0'
    if(n < 0) 
    {
        std::cerr << "read pipe filed !" << '\n';
        return -1;
    }
    else if(n > 0)
    {
        buff[n] = 0;
        *out = buff;
    }
    return n;
}


封装 Client

该类为客户端类,总体上和 Server 类差不多,唯一不同的是: Client 类是写数据的类,会有一个写数据的函数

#pragma once

#include "Comm.hpp"

class Client
{
public:
    Client()
        : _fd(g_default_fd)
    {
    }
    ~Client()
    {
    }

    // 打开文件
    bool OpenPipe()
    {
        // int open(const char *pathname, int flags);
        _fd = ::open(gfileName.c_str(), O_WRONLY); // 只写方式打开:作为写端
        if (_fd < 0)
        {
            std::cerr << "open pipe filed !" << '\n';
            return false;
        }
        return true;
    }
    // 关闭文件
    void ClosePipe()
    {
        if (_fd > 0)
            ::close(_fd);
    }

    // 发送数据
    // std::string & : 输入输出型参数
    int SendPipe(std::string &in)
    {
        //ssize_t write(int fd, const void *buf, size_t count);
        ssize_t n = write(_fd, in.c_str(), in.size());
        if(n < 0)
        {
            std::cerr << "write pipe filed !" << '\n';
            return -1;
        }
        return n;
    }

private: 
    int _fd;
};


初步通信


client.cc client 端循环写入

server.cc server 端循环读出



实现通信

client 端没有写入信息,则 server 端阻塞等待数据

在这里插入图片描述



代码演示效果如下:

注意, server 端是读端,负责读取数据与创建管道文件,因此需要先运行 server

然后再运行 client 端,否则 client 端打不开管道文件



解释一下这些程序的使用:client 进程,我们人为手动键盘输入一些字符,回车确认后,在 server 进程就会收到你输入的消息


在这里插入图片描述




关于为什么不能使用 cin

使用 cin >> message; 会遇到一个问题:当输入包含空格的字符串时,cin
只会读取第一个单词,而剩余的部分会留在输入流中。这会导致后续的循环中 cin 继续读取剩余的部分,从而导致 "Please Enter#: " 被连续打印多次。



上面的代码仅仅是一个基本轮廓框架,还有比较多的bug和细节需要填补:


填补细节和修 Bug


1、当我们关闭 client 端时,你会发现 server 端死循环了!!

这是因为,管道文件写端关闭,读端读到 n=0 本应该退出,但是我们的死循环逻辑中没有这个,因此在 server 端的代码中可以加上


while (true)
{
    cout << "Client Say#: ";
    int n = server.RecvPipe(&message);
    if (n > 0)
    {
        cout << message << '\n';
    }
    else 
    {
        cout << "读取结束!" << '\n';
        break;
    }
}
cout << "client quit, me too!" << '\n';

在这里插入图片描述



2、优化细节:封装 server 端 和 client 端重复的 OpenPipe()ClosePipe()

你可以发现 server 端 和 client 端都有一个 OpenPipe()ClosePipe() ,其中的代码大部分是重复的,我们可以将重复的代码抽取出来封装成共享的函数,然后两端调用即可,这样二次封装既减少代码重复,又可以增加可读性


实际的操作: 将重复部分抽取出来封装成 OpenPipe()ClosePipe() ,在 server 类 和 client 类中设计新的 ”专属“ 函数来调用这两个重复的 OpenPipe()ClosePipe() ,如在 serverOpenPipeForRead() 函数中调用 OpenPipe(gForRead)



server

这里的 gForReadopen 函数的选项,定义成了全局数据,后面有具体讲解

// 打开文件
bool OpenPipeForRead()
{
    _fd = OpenPipe(gForRead);
    if(_fd < 0) return false;
    return true;
}

// 关闭文件
void ClosePipeRead()
{
    ClosePipe(_fd);
}


client

这里的 gForWriteopen 函数的选项,定义成了全局数据,后面有具体讲解

// 打开文件
bool OpenPipeForWrite()
{
    _fd = OpenPipe(gForWrite);
    if(_fd < 0) return false;
    return true;
}

// 关闭文件
void ClosePipeWrited()
{
    ClosePipe(_fd);
}



公共代码部分:Comm.hpp

通过定义两种 flags,满足读写端的需求

const int gForRead = O_RDONLY;
const int gForWrite = O_WRONLY;

int OpenPipe(int flags)
{
    // int open(const char *pathname, int flags);
    int fd = ::open(gfileName.c_str(), flags);
    if (fd < 0)
    {
        std::cerr << "open pipe filed !" << '\n';
        return -1;
    }
    return fd;
}

// 关闭文件
void ClosePipe(int fd)
{
    if (fd > 0)
        ::close(fd);
}

管道通信,需要先存在读端,然后写端才能正常运行,若没有读端,则写端不会被打开

若写端打开了,读端关闭了或没打开(即不存在),则写端也会直接被释放!

若读端打开了,写端关闭了或没打开(即不存在),则读端会阻塞等待写端打开并写入数据

因此在管道通信中,需要确保读端优先被打开


3、一个超级细节的点:若读端打开文件时,写端还没打开,读端的 open 函数会阻塞住,等待写端打开文件

观察前面的代码,在管道通信中,我们需要先通过 Server 端读端,创建并打开文件,再通过 Client 端写端打开文件,才能进行读写通信。

对于管道通信,写端关闭,同时读端会读到 n=0 ,则读端关闭



此时就引出一个问题:

问题:在我们前面的代码中, Server 端读端打开文件,而 Client 端写端没有打开文件之前, Server 端的读端不会被系统关闭呢?

答:因为此时 Server 端的读端是阻塞住了!open 函数内部检测到该文件的引用计数为 1(即读端打开),此时不会将读端关闭,因为写端连文件都没打开,又何谈是读端没有读取到数据的问题,写端连文件都没打开,因此就不属于前面讲的管道那种场景(写端关闭&&读端会读到 n=0 则读端关闭)

此时,读端会在 open 函数出阻塞等待,等待第二个进程打开该管道文件,系统知道这个管道文件一定要两个文件打开才能进行通信,因此就需要等待



总结来说:

读端打开文件,写端未打开文件,引用计数 1,open 函数识别到此时文件状态是:只有一个进程操作该文件,则阻塞等待

读端打开文件,写端也打开文件,引用计数 2,open 函数识别到此时文件状态是:有两个进程操作该文件,当写端关闭时,引用计数从 2 变为 1,读端读到 n=0 就关闭管道与读端进程



验证一下:

我在 Server 端打开文件的 open 函数前后都加上一个打印标记

Server server;
cout << "Pos 1" << '\n';
server.OpenPipeForRead();
cout << "Pos 2" << '\n';

在这里插入图片描述


观察动图,你可以发现,当写端 Client 没有启动时, Server 端读端只会打印 Pos 1,而不会打印 Pos 2,说明 Server 端读端阻塞在 open 函数处,而不会继续往后执行

只有当 写端 Client 启动,打开文件后,才会继续打印 Pos 2



完整代码


Client.hpp

#pragma once

#include "Comm.hpp"

class Client
{
public:
    Client()
        : _fd(g_default_fd)
    {
    }
    ~Client()
    {
    }

    // 打开文件
    bool OpenPipeForWrite()
    {
        _fd = OpenPipe(gForWrite);
        if(_fd < 0) return false;
        return true;
    }

    // 关闭文件
    void ClosePipeWrited()
    {
        ClosePipe(_fd);
    }


    // 发送数据
    // std::string & : 输入输出型参数
    int SendPipe(std::string &in)
    {
        //ssize_t write(int fd, const void *buf, size_t count);
        ssize_t n = write(_fd, in.c_str(), in.size());
        if(n < 0)
        {
            std::cerr << "write pipe filed !" << '\n';
            return -1;
        }
        return n;
    }

private: 
    int _fd;
};


Client.cc

#include<iostream>
#include<cstdio>
#include "Client.hpp"
using namespace std;

int main()
{
    Client client;
    client.OpenPipeForWrite();

    string message;
    
    while(true)
    {
        cout << "Please Enter#: ";
        //cin >> message;
        //这里不建议使用cin, 当你要输入的命令有空格, cin 将字符串分成好几段,然后读取就有bug
        getline(std::cin, message);
        client.SendPipe(message);
    }

    client.ClosePipeWrited();
    return 0;
}


Server.hpp
#pragma once

#include "Comm.hpp"

class Init
{
public:
    Init()
    {
        // 创建共享命名管道文件
        // int mkfifo(const char *pathname, mode_t mode);
        umask(0);
        int ret = ::mkfifo(gfileName.c_str(), gmode);
        if (ret < 0)
        {
            std::cerr << "mkfifo failed!" << '\n';
        }
        std::cout << "mkfifo is successfully created" << '\n';
        sleep(3);
    }
    ~Init()
    {
        int ret = ::unlink(gfileName.c_str());
        if (ret < 0)
        {
            std::cerr << "unlink failed!" << '\n';
        }
        std::cout << "unlink success" << '\n';
    }
};

Init init; // 定义全局变量



class Server
{
public:
    Server()
        : _fd(g_default_fd)
    {
    }
    ~Server()
    {
    }

    // 打开文件
    bool OpenPipeForRead()
    {
        _fd = OpenPipe(gForRead);
        if(_fd < 0) return false;
        return true;
    }

    // 关闭文件
    void ClosePipeRead()
    {
        ClosePipe(_fd);
    }

    // 接收数据
    // 编程规范
    // std::string *: 输出型参数
    // const std::string & : 输入型参数
    // std::string & : 输入输出型参数
    int RecvPipe(std::string *out)
    {
        char buff[1024];
        // ssize_t read(int fd, void *buf, size_t count);
        ssize_t n = read(_fd, buff, sizeof(buff)-1); // -1目的:读字符串数据出来最后一位留作 '\0'
        if(n < 0) 
        {
            std::cerr << "read pipe filed !" << '\n';
            return -1;
        }
        else if(n > 0)
        {
            buff[n] = 0;
            *out = buff;
        }
        return n;
    }

private: 
    int _fd;
};



Server.cc
#include <iostream>
#include "Server.hpp"
using namespace std;

int main()
{
    Server server;
    server.OpenPipeForRead();
    
    string message;
    while (true)
    {
        cout << "Client Say#: ";
        int n = server.RecvPipe(&message);
        if (n > 0)
        {
            cout << message << '\n';
        }
        else 
        {
            cout << "读取结束!" << '\n';
            break;
        }
    }
    cout << "client quit, me too!" << '\n';
    server.ClosePipeRead();
    return 0;
}


Comm.hpp
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string gfileName = "./myfifo";
const mode_t gmode = 0600;
const int g_default_fd = -1; // 默认的管道 fd
const int gForRead = O_RDONLY;
const int gForWrite = O_WRONLY;

int OpenPipe(int flags)
{
    // int open(const char *pathname, int flags);
    int fd = ::open(gfileName.c_str(), flags);
    if (fd < 0)
    {
        std::cerr << "open pipe filed !" << '\n';
        return -1;
    }
    return fd;
}

// 关闭文件
void ClosePipe(int fd)
{
    if (fd > 0)
        ::close(fd);
}



Makefile
SERVER=server 
CLIENT=client
CC=g++
SERVER_SRC=Server.cc 
CLIENT_SRC=Client.cc 

.PHONY:all
all:$(SERVER) $(CLIENT)


$(SERVER):$(SERVER_SRC)
	$(CC) -o $@ $^ -std=c++11
$(CLIENT):$(CLIENT_SRC)
	$(CC) -o $@ $^ -std=c++11

.PHONY:clean 
clean:
	rm -rf $(SERVER) $(CLIENT)


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot Web开发(SpringMVC)

SpringBoot Web开发&#xff08;SpringMVC) MVC 核心组件和调用流程 Spring MVC与许多其他Web框架一样&#xff0c;是围绕前端控制器模式设计的&#xff0c;其中中央 Servlet DispatcherServlet 做整体请求处理调度&#xff01; . 除了DispatcherServletSpringMVC还会提供其他…

Linux《基础指令》

在之前的Linux《Linux简介与环境的搭建》当中我们已经初步了解了Linux的由来和如何搭建Linux环境&#xff0c;那么接下来在本篇当中我们就要来学习Linux的基础指令。在此我们的学习是包括两个部分&#xff0c;即指令和关于Linux的基础知识&#xff1b;因此本篇指令和基础知识的…

我的求职面经:(1)C++里指针和数组的区别

经典问题&#xff1a; char s1[]"hello"; char *s2"hello"; 1、s1的值是放在栈上的&#xff0c;值是可以修改的&#xff0c;而hello是一个字符串常量放在静态存储区是不能修改的。 2、内存大小不一样 #include<stdio.h>int main(){char s1[]&quo…

react中如何获取dom元素

实现代码 const inputRef useRef(null) inputRef.current.focus()

【LLM】Deepseek本地部署学习

文章目录 1. 访问ollama官网安装平台2. 选择配置3. 下载和运行 1. 访问ollama官网安装平台 https://ollama.com/ 2. 选择配置 参考以下配置要求 3. 下载和运行 ollama run deepseek-r1:7b

deepseek R1 14b显存占用

RTX2080ti 11G显卡&#xff0c;模型7b速度挺快&#xff0c;试试14B也不错。 7B显存使用5.6G&#xff0c;14B显存刚好够&#xff0c;出文字速度差不多。 打算自己写个移动宽带的IPTV播放器&#xff0c;不知道怎么下手&#xff0c;就先问他了。

【漫话机器学习系列】064.梯度下降小口诀(Gradient Descent rule of thume)

梯度下降小口诀 为了帮助记忆梯度下降的核心原理和关键注意事项&#xff0c;可以用以下简单口诀来总结&#xff1a; 1. 基本原理 损失递减&#xff0c;梯度为引&#xff1a;目标是让损失函数减少&#xff0c;依靠梯度指引方向。负梯度&#xff0c;反向最短&#xff1a;沿着负…

让万物「听说」:AI 对话式智能硬件方案和发展洞察

本文整理自声网 SDK 新业务探索组技术负责人&#xff0c;IoT 行业专家 吴方方 1 月 18 日在 RTE 开发者社区「Voice Agent 硬件分享会」上的分享。本次主要介绍了 AI 对话式智能硬件的发展历程&#xff0c;新一波 AI 浪潮所带来的创新机遇、技术挑战以及未来的展望。 在语音交…

SpringBoot 日志

目录 一. 日志概述 二. 日志的使用 1. 打印日志 (1) 获取日志对象 (2) 输出要打印的内容 2. 日志框架简介 (1) 门面模式简介 (2) SLF4J 框架简介 3. 日志的格式 4. 日志的级别 5. 日志配置 (1) 配置日志级别 (2) 日志持久化存储 ① 配置日志文件名 ② 配置日志的…

Python 梯度下降法(一):Gradient Descent

文章目录 Python 梯度下降法&#xff08;一&#xff09;&#xff1a;Gradient Descent一、原理1.1 多元函数1.2 梯度下降法 二、常见的梯度公式2.1 标量对向量的梯度2.2 向量对向量的梯度2.3 向量对标量的梯度2.4 标量对矩阵的梯度 三、常见梯度算法3.1 Batch Gradient Descent…

从AD的原理图自动提取引脚网络的小工具

这里跟大家分享一个我自己写的小软件&#xff0c;实现从AD的原理图里自动找出网络名称和引脚的对应。存成文本方便后续做表格或是使用简单行列编辑生成引脚约束文件&#xff08;如.XDC .UCF .TCL等&#xff09;。 我们在FPGA设计中需要引脚锁定文件&#xff0c;就是指示TOP层…

【2025年最新版】Java JDK安装、环境配置教程 (图文非常详细)

文章目录 【2025年最新版】Java JDK安装、环境配置教程 &#xff08;图文非常详细&#xff09;1. JDK介绍2. 下载 JDK3. 安装 JDK4. 配置环境变量5. 验证安装6. 创建并测试简单的 Java 程序6.1 创建 Java 程序&#xff1a;6.2 编译和运行程序&#xff1a;6.3 在显示或更改文件的…

WGCLOUD服务器资源监控软件使用笔记 - Token is error是什么错误

[wgcloud-agent]2025/01/30 10:41:30 WgcloudAgent.go:90: 主机监控信息上报server开始 [wgcloud-agent]2025/01/30 10:41:30 WgcloudAgent.go:99: 主机监控信息上报server返回信息: {"result":"Token is error"} 这个错误是因为agent配置的wgToken和serv…

MySQL(表空间)

​开始前先打开此图配合食用 MySQL表空间| ProcessOn免费在线作图,在线流程图,在线思维导图 InnoDB 空间文件中的页面管理 后面也会持续更新&#xff0c;学到新东西会在其中补充。 建议按顺序食用&#xff0c;欢迎批评或者交流&#xff01; 缺什么东西欢迎评论&#xff01;我都…

白嫖DeepSeek:一分钟完成本地部署AI

1. 必备软件 LM-Studio 大模型客户端DeepSeek-R1 模型文件 LM-Studio 是一个支持众多流行模型的AI客户端&#xff0c;DeepSeek是最新流行的堪比GPT-o1的开源AI大模型。 2. 下载软件和模型文件 2.1 下载LM-Studio 官方网址&#xff1a;https://lmstudio.ai 打开官网&#x…

知识管理平台在数字经济时代推动企业智慧决策与知识赋能的路径分析

内容概要 在数字经济时代&#xff0c;知识管理平台被视为企业智慧决策与知识赋能的关键工具。其核心作用在于通过高效地整合、存储和分发企业内部的知识资源&#xff0c;促进信息的透明化与便捷化&#xff0c;使得决策者能够在瞬息万变的市场环境中迅速获取所需信息。这不仅提…

关于MySQL InnoDB存储引擎的一些认识

文章目录 一、存储引擎1.MySQL中执行一条SQL语句的过程是怎样的&#xff1f;1.1 MySQL的存储引擎有哪些&#xff1f;1.2 MyIsam和InnoDB有什么区别&#xff1f; 2.MySQL表的结构是什么&#xff1f;2.1 行结构是什么样呢&#xff1f;2.1.1 NULL列表&#xff1f;2.1.2 char和varc…

【开源免费】基于SpringBoot+Vue.JS公交线路查询系统(JAVA毕业设计)

本文项目编号 T 164 &#xff0c;文末自助获取源码 \color{red}{T164&#xff0c;文末自助获取源码} T164&#xff0c;文末自助获取源码 目录 一、系统介绍二、数据库设计三、配套教程3.1 启动教程3.2 讲解视频3.3 二次开发教程 四、功能截图五、文案资料5.1 选题背景5.2 国内…

【Unity3D】实现横版2D游戏角色二段跳、蹬墙跳、扶墙下滑

目录 一、二段跳、蹬墙跳 二、扶墙下滑 一、二段跳、蹬墙跳 GitHub - prime31/CharacterController2D 下载工程后直接打开demo场景&#xff1a;DemoScene&#xff08;Unity 2019.4.0f1项目环境&#xff09; Player物体上的CharacterController2D&#xff0c;Mask添加Wall层…

讯飞智作 AI 配音技术浅析(二):深度学习与神经网络

讯飞智作 AI 配音技术依赖于深度学习与神经网络&#xff0c;特别是 Tacotron、WaveNet 和 Transformer-TTS 模型。这些模型通过复杂的神经网络架构和数学公式&#xff0c;实现了从文本到自然语音的高效转换。 一、Tacotron 模型 Tacotron 是一种端到端的语音合成模型&#xff…