Linux中的System V通信标准--共享内存、消息队列以及信号量

        关于 System V 标准,一共有三种通信方式,分别为:共享内存、信号量和消息队列三种通信方式。本篇将较为详细的讲解三种通信方式的实现原理,以及介绍在 Linux 系统下调用这三种的通信方式的接口,其中以共享内存为例,较为详细的讲解和用代码实现这种通信方式。

        最后我们得出这三种通信方式存在很大的共同点,以及总结了操作系统对这三种通信方式的管理。目录如下:

目录

共享内存

1. 实现原理

2. 代码实现进程的共享内存通信

共享内存的创建

共享内存的释放

共享内存挂接

共享内存间通信代码

3. 共享内存的优缺点

4. 获取共享内存的属性

消息队列

1. 消息队列的原理

2. 消息队列的接口

3. 消息队列的指令操作

信号量

1. 信号量的原理

2. 信号量的操作

3. 信号量的指令

操作系统对三种 System V 的管理

共享内存

1. 实现原理

        共享内存为进程间通信方案,则一定会遵守进程间通信的的原则:让不同进程看见同一份资源。对于共享内存的实现原理为:操作系统会在物理内存中专门给需要通信的进程开辟一段物理内存,然后分别映射到不同进程的虚拟地址中,不同的进程就可以看到同一份的内存资源。原理图如下:

        如上的步骤1:操作系统在物理内存中开辟出同于共享内存通信的物理内存;

        步骤2:操作系统将物理共享内存通过页表映射到对应的虚拟地址中去。

共享内存实现的几个关键点:

        1. 关于实现共享内存的所有操作,都是由OS(操作系统)完成的;

        2. OS给需要共享内存通信的进程提供步骤1、2的系统调用,让他们通过系统调用来通信。

        3. 共享内存在系统中可以同时存在多份,不同对进程之间可以同时进行通信。

        4. 操作系统需要对共享内存进行管理,所以会有对应的共享内存数据结构,以及匹配的算法。        

        5. 共享内存 = 内存空间 + 共享内存的属性。

2. 代码实现进程的共享内存通信

共享内存的创建

        关于使用系统调用实现共享内存的一个重要的系统调用,shmget,也就是用来获取在物理内存中的共享内存,使用方法如下:

int shmget(key_t key, size_t size, int shmflg);
系统调用中的参数:key:用于唯一的表示物理内存中存在的共享内存;size:需要开辟的物理内存的大小,通常建议为4096的倍数shmflg:获取共享内存的方式,常用的为IPC_EXCL 和 IPC_CREAT,关于这两个的搭配有:IPC_CREAT:若不存在共享内存则创建,存在则获取对应的共享内存且返回总能获取一个IPC_EXCL:不能单独的使用IPC_CREAT | IPC_EXCL:对应的共享内存不存在则创建,存在则出错返回只能获取新的

        关于在 shmget 中的 key 参数,不同的进程想要进程通信,则填入的 key 值则需要相同,对于这个 key 值,我们一般不建议直接由我们自己来填,而是建议使用系统提供的 ftok 函数给我们随机生成一个,如下:

key_t ftok(const char *pathname, int proj_id);

        只需要提供同样的 pathname 和同样的 proj_id,我们就可以生成同样的 key 值,也就可以让不同的进程之间实现通信。

共享内存的释放

        还有一个需要注意的点:我们使用进程创建出的共享内存,并不会随着进程的结束而释放,进程结束仍然会保留在内存中,因为这是由底层的操作系统创建出的共享内存,所以我们需要在每一次进程结束的时候,将对应的共享内存关闭,防止内存泄漏。(共享内存的生命周期随内核,文件的生命周期随进程)

        我们可以使用 ipcs -m 查看当前系统中存在哪些共享内存,然后使用 ipcrm -m shmid 删除对应的共享内存,如下:

        共享内存的 key 与 shmid 的比较:

        key:属于用户形成,内核使用的一个字段,内核用于区分共享内存的唯一性,用户不能使用 key 来进行共享内存的管理。

        shmid:内核给用户返回的一个标识符,用来进行用户级对共享内存进行管理的 id 值。

        使用系统调用删除共享内存为 shmctl,如下:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
该系统调用用于对共享内存的控制,可以为增删查改
但本篇只需要将共享内存释放,所以只需要填入的参数为:
共享内存的shmid
选择模式,删除为 IPC_RMID
buf 为 nullptr
共享内存挂接

        现在我们既然已经创建出对应的共享内存,只需要将共享内存挂接到对应的共享内存,然后就可以让对应进程之间开始通信了,如下:

void *shmat(int shmid, const void *shmaddr, int shmflg);
用于挂接共享内存
shmaddr表示将挂接共享内存的位置,通常可以设置为nullptr
shmflg表示共享内存的访问权限
shmat的返回值:挂接成功为共享内存的起始地址,连接失败为nullptrint shmdt(const void *shmaddr);
用于将挂接上的共享内存给去掉
去挂接成功返回0,失败返回-1
共享内存间通信代码

Shm.hpp

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>// 路径,随便使用一个路径都可以
const std::string SHPathName = "/home/JZhong/code/linux-code/test_7_20";
const int SHProj_Id = 0x8585;#define SHCreater 1
#define SHUser    0 
#define SHsize 4096class Shm {std::string ToHex(int n) {char buff[128];int cx = snprintf(buff, sizeof(buff) - 1, "0x%x", n);buff[cx] = '\0';return buff;}int GetShm(int shmflg) {int sh = shmget(_key, SHsize, shmflg);if (sh == -1) {std::cout << "Create shm fail!" << "the reason is " << strerror(errno) << std::endl;}return sh;}std::string WhoamI() {if (_who == SHCreater)return "Creater";else if (_who == SHUser)return "User";elsereturn "None";}void* AttachShm() {void* shmaddr = shmat(_shmid, nullptr, 0);if (shmaddr == nullptr) {perror("shm attach: ");return nullptr;}std::cout << WhoamI() << " attach the shm" << std::endl;return shmaddr;}void DettachShm(void* shmaddr) {if (shmaddr == nullptr)return;shmdt(shmaddr);}
public:Shm(const std::string& pathname, int proj_id, int who): _pathname(pathname), _proj_id(proj_id) , _who(who), _shmaddr(nullptr){_key = ftok(pathname.c_str(), proj_id);// 创建共享内存if (who == SHCreater) {// 加入 0666 权限才可以将共享内存挂接上_shmid = this->GetShm(IPC_CREAT | IPC_EXCL | 0666);std::cout << "Creater create the shm, the key: " << ToHex(_key) << " shmid:" << _shmid << std::endl;} else {_shmid = this->GetShm(IPC_CREAT | 0666);std::cout << "User get the shm, the key: " << ToHex(_key) << " shmid:" << _shmid << std::endl;}_shmaddr = this->AttachShm();}void Zero() {if (_shmaddr) memset(_shmaddr, 0, SHsize);}void* GetAddress() {return _shmaddr;}~Shm() {if (_shmaddr != nullptr)DettachShm(_shmaddr);if (_who == SHCreater) {shmctl(_shmid, IPC_RMID, nullptr);}std::cout << "shm remove done..." << std::endl;}
private:std::string _pathname;int _proj_id;int _who;int _shmid;key_t _key;void* _shmaddr;
};

server.cc

#include "Shm.hpp"int main() {Shm shm(SHPathName, SHProj_Id, SHCreater);char* addr = (char*)shm.GetAddress();while (true) {std::cout << addr << std::endl;sleep(1);}return 0;
}

client.cc

#include "Shm.hpp"int main() {Shm shm(SHPathName, SHProj_Id, SHUser);shm.Zero();char* addr = (char*)shm.GetAddress();char ch = 'A';while (ch <= 'Z') {addr[ch - 'A'] = ch;ch++;sleep(2);}return 0;
}

        以上的代码让服务器端创建出共享内存,然后让客户端写、客户端读,运行如下:

3. 共享内存的优缺点

       优点:通过上面关于读出共享内存的代码我们可以发现,我们没有调用任何的系统调用,直接就将数据写入和读出,这是因为虚拟地址和共享内存已经建立连续,所以共享内存通信方式,是所有进程间通信方式中最快的一种

        缺点:如上代码的运行结果显示,当我们客户端写数据每秒写一个数据,服务器端每两秒读一个数据的时候,我们会发现:服务器端每次读的时候会将读过的数据在读一遍。这也说明共享内存不提供对共享内存的数据任何保护机制,这样的机制同时还会导致数据不一致问题,也就是客户端还没写完,服务器端就将数据读出,会导致数据分析存在问题。

        假若我们要解决这种数据不一致问题,我们可以在通信双方间在增加一个管道,增加管道可以每次将数据阻塞起来,只要写端每写完或者没写,读端就不可以读出来,如下:

增加的 NamedPipe.hpp

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>#define NPCreater 0
#define NPUser    1
#define NPMode 0666
#define NPRead  O_RDONLY
#define NPWrite O_WRONLY
#define DEFAULT_FDX -1
#define NPReadSize 1024const std::string ComPath = "./myfifo";class NamedPipe {
private:std::string GetName() {if (_who == NPCreater)return "Creater";else if (_who == NPUser)return "User";elsereturn "None";}// 以不同的方式打开对应的文件int OpenNamedPipe(mode_t mode) {int fd = open(_path.c_str(), mode);if (fd == -1) {std::cout << "open file fail" << " the reason is " << strerror(errno) << std::endl;}return fd;}
public:NamedPipe(const std::string& path, size_t who): _path(path), _who(who), _fd(DEFAULT_FDX){// 让服务器端读,让客户端写,服务器创建出对应的管道文件if (who == NPCreater) {int n = mkfifo(_path.c_str(), NPMode);if (n < 0) {std::cout << "create named pipe fail!" << " the reason is " << strerror(errno) << std::endl;}_fd = OpenNamedPipe(NPRead);std::cout << GetName() << " create the named pipe" << std::endl;} else {_fd = OpenNamedPipe(NPWrite);   }}int SomeoneUseToRead(std::string* out) {char inbuff[NPReadSize];int n = read(_fd, inbuff, sizeof(inbuff) - 1);if (n == -1) {std::cout << "read failed" << " the reason is " << strerror(errno) << std::endl;} inbuff[n] = '\0';*out = inbuff;return n;}void SomeoneUseForWrite(const std::string& info) {int n = write(_fd, info.c_str(), info.size());if (n == -1) {std::cout << "write failed" << " the reason is " << strerror(errno) << std::endl;}}~NamedPipe() {if (_who == NPCreater) {// 让创建者删除对应的管道文件int n = unlink(_path.c_str());if (n < 0) std::cout << "remove the named pipe fail!" << " the reason is " << strerror(errno) << std::endl;    }std::cout << GetName() << " unlink the named pipe file " << std::endl;if (_fd != DEFAULT_FDX) {close(_fd);}}
private:std::string _path;size_t _who;int _fd;
};

client.cc

#include "Shm.hpp"
#include "NamedPipe.hpp"int main() {Shm shm(SHPathName, SHProj_Id, SHUser);shm.Zero();char* addr = (char*)shm.GetAddress();NamedPipe fifo(ComPath, NPUser);char ch = 'A';while (ch <= 'Z') {addr[ch - 'A'] = ch;std::cout << "add " << ch << " into shm " << "wakeup reader" << std::endl;ch++;std::string temp("wakeup");fifo.SomeoneUseForWrite(temp);sleep(2);}return 0;
}

server.cc

#include "Shm.hpp"
#include "NamedPipe.hpp"int main() {Shm shm(SHPathName, SHProj_Id, SHCreater);char* addr = (char*)shm.GetAddress();NamedPipe fifo(ComPath, NPCreater);while (true) {// 每次先阻塞读std::string temp;fifo.SomeoneUseToRead(&temp);std::cout << addr << std::endl;sleep(1);}return 0;
}

4. 获取共享内存的属性

         当我们想要获取共享内存的属性的时候,我们可以使用 shmctl 系统调用接口,然后创建出一个 struct shmid_ds 变量获取到 共享内存的属性,如下:

struct shmid_ds ds;
shmctl(_shmid, IPC_STAT, &ds);
获取共享内存的属性,使用 IPC_STAT 获取,获取出的属性保存在 ds 中struct shmid_ds {struct ipc_perm shm_perm;    /* Ownership and permissions */size_t          shm_segsz;   /* Size of segment (bytes) */time_t          shm_atime;   /* Last attach time */time_t          shm_dtime;   /* Last detach time */time_t          shm_ctime;   /* Last change time */pid_t           shm_cpid;    /* PID of creator */pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */shmatt_t        shm_nattch;  /* No. of current attaches */...
};struct ipc_perm {key_t          __key;    /* Key supplied to shmget(2) */uid_t          uid;      /* Effective UID of owner */gid_t          gid;      /* Effective GID of owner */uid_t          cuid;     /* Effective UID of creator */gid_t          cgid;     /* Effective GID of creator */unsigned short mode;     /* Permissions + SHM_DEST and SHM_LOCKED flags */unsigned short __seq;    /* Sequence number */
};

消息队列

1. 消息队列的原理

        操作系统会现在内存空间中创建出一个消息队列,然后要通信的进程会使用消息队列的系统调用接口往消息队列中放入结点(放入数据块),接着进程通过消息队列中的标识逐一拿出对应的数据块(所以,消息队列是一个进程向另一个进程发送有类型数据块的一种方式),如下:

2. 消息队列的接口

        获取消息队列的接口为 msgget,如下:

int msgget(key_t key, int msgflg);

        对于参数 key,我们可以使用 fotk 获取,对于 msgflg,我们也可以使用 IPC_CREAT 和 IPC_EXCL 带入。返回值为 msqid。

        当我们想要将消息队列删除时,可以使用接口,msgctl,如下:

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

        使用方法和 shmctl 一样。

获取消息队列的属性数据结构:

struct msqid_ds {struct ipc_perm msg_perm;     /* Ownership and permissions */time_t          msg_stime;    /* Time of last msgsnd(2) */time_t          msg_rtime;    /* Time of last msgrcv(2) */time_t          msg_ctime;    /* Time of last change */unsigned long   __msg_cbytes; /* Current number of bytes in queue (nonstandard) */msgqnum_t       msg_qnum;     /* Current number of messages in queue */msglen_t        msg_qbytes;   /* Maximum number of bytes allowed in queue */pid_t           msg_lspid;    /* PID of last msgsnd(2) */pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};struct ipc_perm {key_t          __key;       /* Key supplied to msgget(2) */uid_t          uid;         /* Effective UID of owner */gid_t          gid;         /* Effective GID of owner */uid_t          cuid;        /* Effective UID of creator */gid_t          cgid;        /* Effective GID of creator */unsigned short mode;        /* Permissions */unsigned short __seq;       /* Sequence number */
};

        向消息队列发送数据块和接收数据块

        我们可以使用接口 msgsnd 和 msgrcv 接口,如下:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

3. 消息队列的指令操作

        消息队列和共享内存一样,生命周期也是随内核的。当我们要查看当前内核存在哪些消息队列,我们可以使用 ipcs -q,当我们删除对应的消息队列,使用 ipcrm -q miqid,如果我们想要查看消息队列、共享内存和信号量在内存中是否存在,使用 ipcs 指令,如下:

信号量

        在介绍信号量前先介绍一些概念和基础认识:

        1. 共享资源:能被多个执行流(进程)看到的一份资源。

        2. 临界资源:被保护起来的资源,一般是通过互斥的方式保护共享资源。

        3. 互斥和同步:对于互斥来说,如何时刻只能有一个进程访问共享资源(多个进程形成互斥)。同步为,多个进程可以同时访问一个共享资源,比如共享内存,可以写一点读一点.

        4. 临界区:访问共享资源的代码。对来代码来说,分为访问共享资源的代码(一般为使用系统调用接口访问共享资源的代码)和不访问共享资源的代码。

        5. 对共享资源的保护,本质是对访问共享资源的代码(临界区)进程保护。

        信号量也可以被称为信号灯,是用来保护共享资源(临界资源)的。

1. 信号量的原理

        在我们使用共享资源的时候,很多情况下都是将共享资源当成一个整体使用,但是当我们需要将共享资源分块,不整体使用的时候,拆分成很多小块,多个执行流(进程)访问不同的共享资源小块,保证共享资源的一定并发度。

        所以信号量就是用来将共享资源拆分小块进行管理的一种方式

        信号量本质是一个对临界资源预先申请的计数器。进程在访问共享资源之前需要向共享资源提出预定申请,也就是申请信号量,当信号量充足的时候才可以申请成功,放完共享资源之后,释放信号量。

        当我们将共享资源整体使用的时候,就是能访问的共享资源只有一个,所以这种特殊的情况为二元信号量(二元信号量的访问机制为互斥),当由多个共享资源可以访问的时候就为多元信号量

        关于信号量的设计,信号量本质是一个计数器,那我们能不使用一个全局的变量充当信号量呢?答案是不能,因为信号量是共享资源小块的计数器,这个信号量需要让不同的进程看见的,一个全局变量只能在一个进程中看见。所以对于信号量来说,这是一个在操作系统内核中的一个”计数器“

        但是我们可以发现:信号量和共享内存、消息队列进行比较我们可以发现,信号量作为一个计数器,为什么仍然被纳入到了进程间通信呢?

        这是因为信号量仍然可以让不同的进程看到同一份资源,同时信号量也属于一种公共资源,因为其他进程都可以看见信号量。

        进程申请信号量的的原理如下:

2. 信号量的操作

         首先是关于获取信号量的系统调用 semget,如下:

int semget(key_t key, int nsems, int semflg);

        信号量的获取操作和共享内存以及消息队列基本一致,不过操作系统允许我们一次申请多个信号量,也就是 nsems。申请几个 nsems 就为几个。

        信号量的删除操作,semctl,如下:

int semctl(int semid, int semnum, int cmd, ...);

        和共享内存、消息队列仍然差不多,不过由于系统中存在多个信号量,所以可以定位是哪个信号量,只需要填入参数 semnum 即可。假若要删除全部信号量,只需要将 semnum 设置为 0 即可,关于后面的可变参数,可以填入 struct semid_ds 也就是获取更加详细的信号量的属性。

        信号量的属性数据结构,如下:

struct semid_ds {struct ipc_perm sem_perm;  /* Ownership and permissions */time_t          sem_otime; /* Last semop time */time_t          sem_ctime; /* Last change time */unsigned long   sem_nsems; /* No. of semaphores in set */
};struct ipc_perm {key_t          __key;       /* Key supplied to semget(2) */uid_t          uid;         /* Effective UID of owner */gid_t          gid;         /* Effective GID of owner */uid_t          cuid;        /* Effective UID of creator */gid_t          cgid;        /* Effective GID of creator */unsigned short mode;        /* Permissions */unsigned short __seq;       /* Sequence number */
};

        对指定信号量进行操作,如下:

int semop(int semid, struct sembuf *sops, unsigned nsops);

3. 信号量的指令

        获取信号量的指令为:ipcs -s,删除信号量的指令为:ipcrm -s semid(和共享内存消息队列相统一)。如下:

操作系统对三种 System V 的管理

        通过以上的认识,我们会发现操作系统对于共享内存、消息队列以及信号量三种进程间通信方式的管理方式有着很大的一致性,比如:

        1. 获取和删除都是 XXXget 和 XXXctl

        2. 他们的属性数据结构都为 XXXid_ds,且数据结构中的第一个元素为 ipc_perm(且三个的 ipc_perm 都相同)。

        所以对于这三种通信方式存在很大的共性。

        在操作系统中,操作系统会维护一个 struct ipc_id_arr 的柔性数组并且用其中的 struct kern_ipc_perm* p[0] 来实现管理我们的 IPC 进程间通信,如下:

        通过如上的方式就将不同的通信方式进行了统一管理,所以我们平时所获取的 shmid msqid semid,其实就是柔性数组的下标。所以当我们要访问不同的内容的时候,我们只需要使用 (struct sem_arry*) p[semid],(struct shmid_kernel*) p[shmid],(struct msg_queue*) p[msqid] 来进行访问不同的通信方式,这样的一种实现,其实就是一种多态

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

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

相关文章

.netcore TSC打印机打印

此文章给出两种打印案例&#xff0c; 第一种是单列打印&#xff0c;第二种是双列打印 需要注意打印机名称的设置&#xff0c;程序中使用的打印机名称为999&#xff0c;电脑中安装打印机时名称也要为999。 以下是我在使用过程中总结的一些问题&#xff1a; 一 TSC打印机使用使…

【区块链+绿色低碳】巴中市生态价值核算创新应用 | FISCO BCOS应用案例

生态产品总值&#xff08;GEP&#xff09;&#xff0c;指一定区域生态系统为人类福祉和经济社会可持续发展提供的产品与服务价值总和&#xff0c;包 括供给产品价值、调节服务价值和文化服务价值。当前&#xff0c;推动生态产品价值有效转化存在“难度量、难抵押、难交易、 难变…

【机器学习算法基础】(基础机器学习课程)-08-决策树和随机森林-笔记

一、决策树之信息论基础 决策树是一种用来做决策的工具&#xff0c;就像我们生活中的选择树。例如&#xff0c;你在选择今天穿什么衣服时&#xff0c;会根据天气情况、出行活动等进行判断。决策树的构建过程涉及一些信息论的概念&#xff0c;用来衡量和选择最好的“分叉点”来进…

Adobe Dimension(DN)安装包软件下载

目录 一、软件简介 二、软件下载 三、注意事项 四、软件功能 五、常用快捷键 快捷键&#xff1a; 一、软件简介 Adobe Dimension&#xff08;简称DN&#xff09;是Adobe公司推出的一款三维设计和渲染软件。与一般的3D绘图软件相比&#xff0c;DN在操作界面和功能上有所不…

Spark实时(一):StructuredStreaming 介绍

文章目录 StructuredStreaming 介绍 一、SparkStreaming实时数据处理痛点 1、复杂的编程模式 2、SparkStreaming处理实时数据只支持Processing Time 3、微批处理&#xff0c;延迟高 4、精准消费一次问题 二、StructuredStreaming概述 三、​​​​​​​​​​​​​​…

python使用 tkinter 生成随机颜色

先看效果: 只要不停点击底部的按钮&#xff0c;每次都会生成新的颜色。炫酷啊。 import random import tkinter import tkinter.messagebox from tkinter import Button# todo """ 1. 设置一个按钮&#xff0c;来让用户选择是否显示颜色值 2. 把按钮换成 Label…

《白话机器学习的数学》第2章——学习回归

2.1设置问题 1.机器学习所做的事情正是从数据中进行学习&#xff0c;然后给出预测值。 2.2定义模型 1.一次函数的表达式&#xff1a; 其中θ叫做参数。 在统计学领域&#xff0c;人们常常使用 θ 来表示未知数和推测值。采用 θ加数字下标的形式&#xff0c;是为了防止当未知数…

网络访问(Socket/WebSocket/HTTP)

概述 HarmonyOS为用户提供了网络连接功能&#xff0c;具体由网络管理模块负责。通过该模块&#xff0c;用户可以进行Socket网络通滚、WebSocket连接、HTTP数据请求等网络通信服务。 Socket网络通信&#xff1a;通过Socket(嵌套字)进行数据通信&#xff0c;支持的协议包括UDP核…

【教程】vscode添加powershell7终端

win10自带的 powershell 是1.0版本的&#xff0c;太老了&#xff0c;更换为powershell7后&#xff0c;在 vscode 的集成终端中没有显示本篇教程记录在vscode添加powershell7终端的过程 打开vscode终端配置 然后来到这个页面进行设置 查看 powershell7 的安装位置&#xff…

JDK HttpClient - Java 11 可用的 JDK 内置的 HTTP 客户端

在 Java 应用的开发中&#xff0c;发送 HTTP 请求是一个常见的需求。应用在开发时&#xff0c;通常会使用流行的开源第三方库作为 HTTP 客户端&#xff0c;如 Apache HttpClient 或 OkHttp 等。这里介绍的是 JDK 自带的 HttpClient 实现&#xff0c;Java 11 可用。说到这里&…

物联网主机 E6000:智慧应急领域的创新力量

在当今瞬息万变的世界中&#xff0c;突发事件和紧急情况时有发生。如何迅速、准确地应对这些挑战&#xff0c;保障人民生命财产安全&#xff0c;成为了社会发展的重要课题。而物联网主机 E6000 的出现&#xff0c;为智慧应急领域带来了全新的解决方案。 一、强大的性能与功能 物…

Java学习 - Spring Boot整合 Thymeleaf 实例

什么是 Thymeleaf Thymeleaf 是新一代的 Java 模板引擎&#xff0c;类似于 Velocity、FreeMarker 等传统引擎&#xff0c;其语言和 HTML 很接近&#xff0c;而且扩展性更高&#xff1b; Thymeleaf 的主要目的是将优雅的模板引入开发工作流程中&#xff0c;并将 HTML 在浏览器中…

ROS话题发布与订阅

续上一篇文章。 我们现在在VScode里面加入订阅方的实现。 demo02_sub.cpp #include "ros/ros.h" #include "std_msgs/String.h"/*订阅方实现&#xff1a;1、包含头文件ROS中文本类型 ---> std_msgs/String.h2、初始化ROS节点3、创建节点句柄4、创建订…

中国篆刻艺术孙溟㠭作品《活着》

活着只是一吸一呼之间&#xff0c;在意觉醒之间&#xff0c;在血液流动之间&#xff0c;抛却灵与肉生死&#xff0c;一切都是惘然。妻吴晓蕾题款&#xff0c;甲辰夏月溟㠭于寒舍小窗下刊石。

力扣高频SQL 50题(基础版)第七题

文章目录 力扣高频SQL 50题&#xff08;基础版&#xff09;第七题1068. 产品销售分析 I题目说明思路分析实现过程准备数据&#xff1a;实现方式&#xff1a;结果截图:总结&#xff1a; 力扣高频SQL 50题&#xff08;基础版&#xff09;第七题 1068. 产品销售分析 I 题目说明 …

PCIe 以太网芯片 RTL8125B 的 spec 和 Linux driver 分析备忘

1,下载 RTL8125B driver 下载页&#xff1a; https://www.realtek.com/Download/List?cate_id584 2,RTL8125B datasheet下载 下载页&#xff1a; https://file.elecfans.com/web2/M00/44/D8/poYBAGKHVriAHnfWADAT6T6hjVk715.pdf3, 编译driver 解压&#xff1a; $ tar xj…

【案例】使用React+redux实现一个Todomvc

About 大家好&#xff0c;我是且陶陶&#xff0c;今天跟大家分享一个redux的todoList案例&#xff0c;通过这个案例能够快速掌握redux的基本知识点&#x1f339; ❤️…❤️…❤️…❤️…❤️…❤️…❤️…❤️…❤️…❤️…❤️…❤️…❤️…❤️…❤️…❤️…❤️…❤️…

Helm部署k8s应用

文章目录 一、概述1、什么是Helm2、特点3、工作流程4、核心概念 二、安装Helm1、二进制版本安装1.1、下载需要的版本1.2、解压1.3、将helm移动到指定路径1.4、验证 三、Helm安装资源顺序四、--set 的格式和限制1、最简单的name/value对2、多个name/value对3、更复杂的表达式4、…

Java语言程序设计基础篇_编程练习题*15.3 (移动小球)

*15.3 (移动小球) 编写一个程序&#xff0c;在面板上移动小球。应该定义一个面板类来显示小球&#xff0c;并提供向左、 向右 、向上和向下移动小球的方法&#xff0c;如图15-24c所示。请进行边界检査以防止球完全移到视线之外 代码展示&#xff1a;编程练习题15_3MoveBall.ja…

鸿蒙OpenHarmony Native API【drawing_pen.h】 头文件

drawing_pen.h Overview Related Modules: [Drawing] Description: 文件中定义了与画笔相关的功能函数 Since: 8 Version: 1.0 Summary Enumerations Enumeration NameDescription[OH_Drawing_PenLineCapStyle] { [LINE_FLAT_CAP], [LINE_SQUARE_CAP], [LINE_ROUND_…