当前位置: 首页 > news >正文

【BMC】jsnbd介绍

jsnbd介绍

本文主要介绍一个名为jsnbd的开源项目,位于GitHub - openbmc/jsnbd,它实现了一个前端(包含HTML和JS文件)页面,作为存储服务器,可以指定存储内容;还包含一个后端的代理,这里所谓的代理实际上的作用是将nbd-client和前端连接起来,作为客户端,可以通过网络下载前面指定的存储内容。该开源项目在OpenBMC(OpenBMC · GitHub)中有使用。

在介绍jsnbd之前先介绍整体原理,这需要从NBD开始。

NBD全称Network Block Device,可以让用户(下图左侧部分)将一个远程主机(下图右侧部分)的磁盘空间当作一个块设备来使用,就像使用本地的硬盘一样。其大致框架如下图所示:

在这里插入图片描述

两台服务器之间通过网络通信,一台安装有nbd-client作为客户端,另一台安装有nbd-server,这样作为客户端就可以从服务端获取服务端硬盘的数据,而且由于NBD的封装,从客户端用户来看,就像是操作普通硬盘设备一样。为此,无论是客户端还是服务端,它们的操作系统都还需要包含NBD模块,用于底层的操作。其结构如下:

在这里插入图片描述

这一部分由操作系统提供,并不会在本文中详细介绍。

而对于jsnbd来说,服务端稍有不同,它不再使用NBD Server应用,而是替换成一个Web程序,所以第一张图变为如下的形式:

在这里插入图片描述

但是实际使用时并没有这么简单,因为Web端程序无法直接通过网络跟nbd-client交互,Web需要首先跟Web服务器交互,而Web服务器又需要通过代理转到nbd-client上,所以这里还有两层,实际的框图应该是这样的:

在这里插入图片描述

而本文介绍的重点就是上图的橙色部分,这三个部分分别是:

  1. 前端:这里由一个JS文件和一个HTML文件完成,前者提供一个WebSocket的实现,后者提供一个简单的操作界面,用于选择文件作为存储“设备”供客户端访问。
  2. Web服务器(lighttpd):前端(作为服务端)需要与后端(作为客户端)的Web服务器沟通,这里使用lighttpd作为Web服务器,除了作为Web服务器,它还需要实现WebSocket的转发,这可以通过lighttpd自带的mod来实现。
  3. nbd-proxy:作为jsnbd的后端部分,其代码也已经由jsnbd项目提供,作为nbd-client和lighttpd之间的代理工具,lighttpd的转发会被它处理,而它的实现简单来说就是接收数据和操作nbd-client。

当然从图中还可以看到额外的一些内容:

  • Linux:由于jsnbd只实现了Linux版本,所以需要在Linux下测试(本文使用Windows操作系统的VMware虚拟机安装Ubuntu来作为测试用Linux)。

  • NBD模块:它是Linux内核模块,通过CONFIG_BLK_DEV_NBD=y包含,需要注意WSL(Windows自带的虚拟机)中没有NBD模块,所以无法使用WSL来测试NBD,会报错:

    jw@HOME:~/code/nbdjs$ sudo modprobe nbd
    modprobe: FATAL: Module nbd not found in directory /lib/modules/5.15.90.1-microsoft-standard-WSL2
    
  • nbd-client:Linux下的应用,完成与底层NBD的交互。

它们可能会在本文提到,但并不会重点介绍。

本文使用的代码已经上传https://gitee.com/jiangwei0512/nbdjs.git,经过测试可以完全基本的使用。

lighttpd

这里首先介绍lighttpd,因为它是前后端交互的基础。

lighttpd是一款轻量级的开源Web服务器,跟Apache、Nginx功能差不多,对应的官网http://www.lighttpd.net/。

lighttpd目前只支持Linux,所以这里在虚拟机Linux上编译和使用lighttpd,对应的Linux版本:

jw@ubuntu:~/code/www/html$ uname -a
Linux ubuntu 5.15.0-82-generic #91~20.04.1-Ubuntu SMP Fri Aug 18 16:24:39 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

编译和使用

目前下载到的最新版本是lighttpd-1.4.71.tar.gz。

  1. 首先解压缩源代码:
jw@ubuntu:~/code$ tar -xzvf lighttpd-1.4.71.tar.gz
  1. 安装依赖:
jw@ubuntu:~/code/lighttpd-1.4.71$ sudo apt install zlib1g-dev libpcre2-dev
  1. 进入解压缩得到的目录,然后进行configure:
jw@ubuntu:~/code/lighttpd-1.4.71$ ./configure --prefix=/usr/local/lighttpd
  1. 编译:
jw@ubuntu:~/code/lighttpd-1.4.71$ make
  1. 安装:
jw@ubuntu:~/code/lighttpd-1.4.71$ sudo make install

安装的位置是:

jw@ubuntu:~/code/lighttpd-1.4.71$ ls -al /usr/local/lighttpd/
total 20
drwxr-xr-x  5 root root 4096 Sep  3 07:03 .
drwxr-xr-x 11 root root 4096 Sep  3 07:03 ..
drwxr-xr-x  2 root root 4096 Sep  3 07:03 lib
drwxr-xr-x  2 root root 4096 Sep  3 07:03 sbin
drwxr-xr-x  3 root root 4096 Sep  3 07:03 share
  1. 进入到lighttpd程序所在的目录,后续以root进行操作:
root@ubuntu:/usr/local/lighttpd/sbin# ll
total 1992
drwxr-xr-x 2 root root    4096 Sep  3 07:03 ./
drwxr-xr-x 5 root root    4096 Sep  3 07:03 ../
-rwxr-xr-x 1 root root 2004088 Sep  3 07:03 lighttpd*
-rwxr-xr-x 1 root root   23048 Sep  3 07:03 lighttpd-angel*
  1. 为了使用lighttpd,需要有配置文件,下面是一个最简单的例子(test.conf):
server.document-root = "/home/jw/code/www/html"
server.port = 80
mimetype.assign = (".html" => "text/html",".txt" => "text/plain",".jpg" => "image/jpeg",".png" => "image/png"
)
index-file.names = ( "index.html" )

这些配置的意义如下:

  • server.document-root:指定了Web服务器目录,我们需要在这里放浏览器可以访问的文件,后续使用的jsnbd前端代码都会放到这里。
  • server.port:指定端口,默认非安全的Web服务器端口就是80。
  • mimetype.assign:指定支持的文件。
  • index-file.names:指定入口文件,就是浏览器输入IP之后首先看到的页面。
  1. server.document-root指定的目录中存放html文件,下面是一个例子(index.html ):
<html><body>Hello Wolrd!</body>
</html>

当通过浏览器登录服务器时,首先访问到的就是这个文件。

启动lighttpd的应用程序的命令如下:

root@ubuntu:/usr/local/lighttpd/sbin# ./lighttpd -D -f test.conf 
2023-09-03 07:17:49: (server.c.1909) server started (lighttpd/1.4.71)

启动之后该服务器会持续运行,此时就可以通过浏览器访问,输入的IP就是Linux系统的IP,端口可以不写,默认就是80。

测试结果如下图所示:

在这里插入图片描述

到这里一个简单的lighttpd服务器就已经开启了。

当然这只是一个开始,此时浏览器只能访问lighttpd中的简单html文件,要想访问后续的nbd-proxy,还需要修改配置文件,这个将在本文后续说明。这里简单介绍lighttpd如何将内容转发到Linux下的程序,这依赖于lighttpd的ws_tunnel插件。

lighttpd和WebSocket

  1. 为了使lighttpd支持WebSocket,首先需要修改它的配置,以下是修改之后的test.conf :
server.modules += ("mod_wstunnel"
)server.document-root = "/home/jw/code/www/html"
server.port = 80
mimetype.assign = (".html" => "text/html",".txt" => "text/plain",".jpg" => "image/jpeg",".png" => "image/png"
)
static-file.exclude-extensions = ( ".fcgi", ".php", ".rb", "~", ".inc" )
index-file.names = ( "index.html" )$HTTP["url"] =~ "^/websocket.test" {wstunnel.server = ("" => (("host" => "127.0.0.1","port" => "888")))wstunnel.frame-type = "text"
}

这里的改动有以下的几个:

  • 通过server.modules引入lighttpd插件,在lighttpd中,通过插件的方式可以引入很多新的特性,比如这里的WebSocket(对应插件mod_wstunnel),还有CGI,代理,等等。

  • 配置wstunnel,所有的参数可以在Docs ConfigurationOptions - Lighttpd - lighty labs找到,这里的配置主要针对特定格式的WebSocket,其配置有两个:一个是转发的地址和端口,指向了localhost(127.0.0.1)和888端口,注意它们需要跟转发过去的程序有相同的配置,否则该程序接收不到转发的内容;另一个是WebSocket的数据格式,这里指定的是文本格式。

配置修改之后重新打开lighttpd:

root@ubuntu:/usr/local/lighttpd/sbin# ./lighttpd -D -f test.conf 
2023-09-09 22:20:15: (server.c.1909) server started (lighttpd/1.4.71)

此时可以查看到网络状态:

root@ubuntu:/usr/local/lighttpd/sbin# netstat -ntlv
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN     
tcp6       0      0 ::1:631                 :::*                    LISTEN 

这里显示的第一行就是lighttpd服务器,它监听80端口,IP没有限制。

  1. 然后是编写Linux端的转发处理程序,下面是一个示例:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>// Should be same with the one in lihttpd.conf and index.html.
#define DEFAULT_PORT 888
// Should be same with the one in lihttpd.conf.
#define DEFAULT_IP "127.0.0.1"int main(int argc, char **argv)
{int server_socket = -1;int client_socket = -1;struct sockaddr_in server_addr;struct sockaddr_in client_addr;char received_buffer[1024]; // Buffer for received.int received_len = -1;int sended_len = -1;int res = -1;socklen_t addr_len = sizeof(struct sockaddr);int index;// Create a socket.server_socket = socket(AF_INET, SOCK_STREAM, 0);if (server_socket < 0){printf("Create socket failed: %s\n", strerror(errno));return -1;}// Bind the created socket on special IP and port.server_addr.sin_family = AF_INET;server_addr.sin_port = htons(DEFAULT_PORT);server_addr.sin_addr.s_addr = inet_addr(DEFAULT_IP);if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){printf("Bind server failed: %s\n", strerror(errno));return -2;}printf("Socket[%d] has bond on port[%d] for IP address[%s]!\n",server_socket, DEFAULT_PORT, DEFAULT_IP);// Listen on the created socket.listen(server_socket, 10);while (1){printf("Waiting and accept new client connect...\n");client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &addr_len);if (client_socket < 0){printf("Accept client socket failed: %s\n", strerror(errno));return -3;}printf("Accept new client[%d] socket[%s:%d]\n", client_socket,inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));while (1){memset(received_buffer, 0, sizeof(received_buffer));received_len = read(client_socket, received_buffer, sizeof(received_buffer));if (received_len < 0){printf("Read data from client [%d] failed: %s\n", client_socket, strerror(errno));close(client_socket);break;}else if (0 == received_len){printf("Client [%d] disconnected!\n", client_socket);close(client_socket);break;}else{printf("Read %d bytes from client[%d] and the data is : %s\n",received_len, client_socket, received_buffer);// Send back the received buffer to client.sended_len = write(client_socket, received_buffer, received_len);if (sended_len < 0){printf("Write data back to client[%d] failed: %s \n", client_socket,strerror(errno));close(client_socket);break;}}}sleep(1);}if (client_socket){close(client_socket);}close(server_socket);return 1;
}

这里使用了socket编程,注意socket和前面提到的WebSocket虽然都用来网络通信,但是它们不是同一个东西,关于它们的具体差别涉及到socket和WebSocket的基础,这里不展开。

这个程序的实现很简单,就是将服务器获取到的数据直接返回给发送端。编译然后使用该程序:

root@ubuntu:/home/jw/code/www/html# gcc websocket_server.c 
root@ubuntu:/home/jw/code/www/html# ./a.out 
Socket[3] has bond on port[888] for IP address[127.0.0.1]!
Waiting and accept new client connect...

再次查看网络状态:

root@ubuntu:/usr/local/lighttpd/sbin# netstat -ntlv
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:888           0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN     
tcp6       0      0 ::1:631                 :::*                    LISTEN 

可以看到又多监听了一个端口888,IP是localhost(127.0.0.1),由于ligtttpd的配置,前端连接过来的特定WebSocket(满足^/websocket.test的格式)就会被本程序处理。

  1. 发送端代码的编写。前面的两步都在Linux系统中(本例使用了虚拟机中的Ubuntu系统),而这里的操作可以在任意的系统中使用,只要存在浏览器,且跟Linux系统可以通过网络通信即可。不过这里编写的Web程序最后还是会在Linux系统中,且在lighttpd指定的目录下,其示例代码(index.html):
<h1>Websocket Test</h1>
<pre id="messages" style="height: 400px; overflow: scroll"></pre>
<input type="text" id="messageBox" placeholder="Type your message here"style="display: block; width: 100%; margin-bottom: 10px; padding: 10px;" />
<button id="send" title="Send Message!" style="width: 100%; height: 30px;">Send Message</button><script>(function () {const sendBtn = document.querySelector('#send');const messages = document.querySelector('#messages');const messageBox = document.querySelector('#messageBox');let ws;function showMessage(message) {messages.textContent += `\nReceived: ${message}`;messages.scrollTop = messages.scrollHeight;messageBox.value = '';}function init() {if (ws) {ws.onerror = ws.onopen = ws.onclose = null;ws.close();}ws = new WebSocket("ws://" + location.host + "/websocket.test");ws.onopen = () => {console.log('Connection opened!');}ws.onmessage = ({ data }) => showMessage(data);ws.onclose = function () {console.log('Connectino closed!');ws = null;}}sendBtn.onclick = function () {if (!ws) {showMessage("No WebSocket connection :(");return;}ws.send(messageBox.value);console.log("Sended: " + messageBox.value);}init();})();
</script>

注意这里的:

ws = new WebSocket("ws://" + location.host + "/websocket.test");

location.host对应的是Linux的IP,整个URL满足lighttpd中ws_tunnel的转发要求,所以会被第二步中的程序接收到。

通过浏览器访问location.host对应的地址(也就是Linux系统的IP地址),执行结果如下:

在这里插入图片描述

图中的虚拟机安装有Ubuntu20.04,开启两个进程,上面的是lighttpd作为Web服务器,下面是socket编写的服务器程序;虚拟机外面是浏览器,输入Ubuntu20.04系统的IP即可访问lighttpd,并显示指定目录下的index.html文件,在该界面下输入的内容会被lighttpd转发给服务器程序,而后者打印传递过来的内容然后返回,最后在浏览器显示出来。

nbd-proxy

代码

nbd-proxy的源代码可以通过https://github.com/openbmc/jsnbd.git下载,注意这个开源代码的名称为jsnbd,它包含了前端和后端的代码,但是本节主要介绍的是后端nbd-proxy这个部分。还有一个gitee版:https://gitee.com/jiangwei0512/nbdjs.git,本文后续内容是按照这个版本为基础的。

代码如下:

jw@HOME:~/code/nbdjs$ ll
total 92
drwxr-xr-x 5 jw jw  4096 Sep  1 22:17 ./
drwxr-xr-x 4 jw jw  4096 Sep  1 22:09 ../
-rw-r--r-- 1 jw jw  3693 Sep  1 22:17 .clang-format
drwxr-xr-x 8 jw jw  4096 Sep  1 22:17 .git/
-rw-r--r-- 1 jw jw   368 Sep  1 22:17 .gitignore
-rw-r--r-- 1 jw jw 11358 Sep  1 22:10 LICENCE
-rw-r--r-- 1 jw jw   211 Sep  1 22:10 Makefile.am
-rw-r--r-- 1 jw jw  1562 Sep  1 22:10 OWNERS
-rw-r--r-- 1 jw jw  2307 Sep  1 22:10 README
-rwxr-xr-x 1 jw jw    73 Sep  1 22:10 bootstrap.sh*
-rw-r--r-- 1 jw jw   362 Sep  1 22:10 config.sample.json
-rw-r--r-- 1 jw jw   446 Sep  1 22:10 configure.ac
drwxr-xr-x 2 jw jw  4096 Sep  1 22:10 m4/
-rw-r--r-- 1 jw jw  1067 Sep  1 22:10 meson.build
-rw-r--r-- 1 jw jw   103 Sep  1 22:10 meson_options.txt
-rw-r--r-- 1 jw jw 20297 Sep  1 22:10 nbd-proxy.c
drwxr-xr-x 3 jw jw  4096 Sep  1 22:17 web/

这里其实包含两个部分:

  • 一部分是前端Web程序,位于web目录下,它们是NBD服务器的一部分,可以在任意浏览器中使用。
  • 剩下的属于另外一部分(主要就是nbd-proxy.c),属于NBD客户端的一部分,最终会生成一个nbd-proxy工具,目前只有Linux版本。

编译

nbd-proxy在Linux环境下编译,对应的环境:

jw@ubuntu:~$ uname -a
Linux ubuntu 5.15.0-82-generic #91~20.04.1-Ubuntu SMP Fri Aug 18 16:24:39 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
  1. 编译之前先安装依赖:
jw@ubuntu:~$ sudo apt install libjson-c-dev libudev-dev autoconf
  1. 然后是编译:
jw@ubuntu:~/code/nbdjs$ autoscan
configure.ac: warning: missing AC_CHECK_FUNCS([dup2]) wanted by: nbd-proxy.c:156
configure.ac: warning: missing AC_CHECK_FUNCS([memset]) wanted by: nbd-proxy.c:263
configure.ac: warning: missing AC_CHECK_FUNCS([socket]) wanted by: nbd-proxy.c:89
configure.ac: warning: missing AC_CHECK_FUNCS([strchr]) wanted by: nbd-proxy.c:412
configure.ac: warning: missing AC_CHECK_FUNCS([strdup]) wanted by: nbd-proxy.c:653
configure.ac: warning: missing AC_CHECK_HEADERS([fcntl.h]) wanted by: nbd-proxy.c:25
configure.ac: warning: missing AC_CHECK_HEADERS([limits.h]) wanted by: nbd-proxy.c:29
configure.ac: warning: missing AC_CHECK_HEADERS([stdint.h]) wanted by: nbd-proxy.c:32
configure.ac: warning: missing AC_CHECK_HEADERS([stdlib.h]) wanted by: nbd-proxy.c:34
configure.ac: warning: missing AC_CHECK_HEADERS([string.h]) wanted by: nbd-proxy.c:35
configure.ac: warning: missing AC_CHECK_HEADERS([sys/socket.h]) wanted by: nbd-proxy.c:37
configure.ac: warning: missing AC_CHECK_HEADERS([unistd.h]) wanted by: nbd-proxy.c:42
configure.ac: warning: missing AC_CHECK_HEADER_STDBOOL wanted by: nbd-proxy.c:47
configure.ac: warning: missing AC_CHECK_MEMBERS([struct stat.st_rdev]) wanted by: nbd-proxy.c:812
configure.ac: warning: missing AC_FUNC_FORK wanted by: nbd-proxy.c:137
configure.ac: warning: missing AC_FUNC_MALLOC wanted by: nbd-proxy.c:869
configure.ac: warning: missing AC_TYPE_PID_T wanted by: nbd-proxy.c:58
configure.ac: warning: missing AC_TYPE_SIZE_T wanted by: nbd-proxy.c:63
configure.ac: warning: missing AC_TYPE_SSIZE_T wanted by: nbd-proxy.c:196
configure.ac: warning: missing AC_TYPE_UINT8_T wanted by: nbd-proxy.c:62
jw@ubuntu:~/code/nbdjs$ aclocal
jw@ubuntu:~/code/nbdjs$ autoreconf --install
configure.ac:9: installing './ar-lib'
configure.ac:8: installing './compile'
configure.ac:5: installing './install-sh'
configure.ac:5: installing './missing'
Makefile.am: installing './depcomp'
jw@ubuntu:~/code/nbdjs$ ./configure 
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /usr/bin/mkdir -p
checking for gawk... no
checking for mawk... mawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking whether make supports nested variables... (cached) yes
checking for gcc... gcc
checking whether the C compiler works... yes
checking for C compiler default output file name... a.out
checking for suffix of executables... 
checking whether we are cross compiling... no
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ISO C89... none needed
checking whether gcc understands -c and -o together... yes
checking whether make supports the include directive... yes (GNU style)
checking dependency style of gcc... gcc3
checking for ar... ar
checking the archiver (ar) interface... ar
checking whether make sets $(MAKE)... (cached) yes
checking whether C compiler accepts -Wall... yes
checking whether C compiler accepts -Werror... yes
checking for splice... yes
checking for pkg-config... /usr/bin/pkg-config
checking pkg-config is at least version 0.9.0... yes
checking for JSON... yes
checking for UDEV... yes
checking that generated files are newer than configure... done
configure: creating ./config.status
config.status: creating Makefile
config.status: creating config.h
config.status: executing depfiles commands
jw@ubuntu:~/code/nbdjs$ make
make  all-am
make[1]: Entering directory '/home/jw/code/nbdjs'CC       nbd_proxy-nbd-proxy.oCCLD     nbd-proxy
make[1]: Leaving directory '/home/jw/code/nbdjs'

最终生成的nbd-proxy就是我们需要的工具:

jw@ubuntu:~/code/nbdjs$ ll nbd-proxy
-rwxrwxr-x 1 jw jw 64184 Sep  1 09:45 nbd-proxy*

使用

  1. 在使用之前还需要安装另外一个工具nbd-client:
root@ubuntu:/home/jw/code/nbdjs# sudo apt install nbd-client

不过光安装应用还不够,还需要使用到Linux的NBD模块,所以还需要执行如下的命令:

root@ubuntu:/home/jw/code/nbdjs# sudo modprobe nbd

为了确定NBD模块是否加载,可以使用如下的命令查看:

jw@ubuntu:~/code/nbdjs$ ls /dev/nbd*
/dev/nbd0  /dev/nbd10  /dev/nbd12  /dev/nbd14  /dev/nbd2  /dev/nbd4  /dev/nbd6  /dev/nbd8
/dev/nbd1  /dev/nbd11  /dev/nbd13  /dev/nbd15  /dev/nbd3  /dev/nbd5  /dev/nbd7  /dev/nbd9

如果有上述的一系列/dev/nbd*设备,表示NBD模块已经成功加载。

  1. 之后创建一个配置文件config.json,内容如下:
jw@ubuntu:~/code/nbdjs$ cat config.json
{"timeout": 30,"configurations": {"0": {"nbd-device": "/dev/nbd0","metadata": {"description": "Virtual media device"}}}
}

将该文件放到/usr/local/etc/nbd-proxy目录下:

jw@ubuntu:~/code/nbdjs$ sudo mkdir /usr/local/etc/nbd-proxy
jw@ubuntu:~/code/nbdjs$ sudo cp config.json /usr/local/etc/nbd-proxy/
  1. 之后还需要修改代码:
// static const char* sockpath_tmpl = RUNSTATEDIR "/nbd.%d.sock";
static const char* sockpath_tmpl = "/tmp/nbd.%d.sock";

因为RUNSTATEDIR对应的/usr/local/var/run无法直接访问。

  1. 修改之后就可以执行nbd-proxy工具:
root@ubuntu:/home/jw/code/nbdjs# ./nbd-proxy 

此时不会有什么输出内容,也不能直接交互。这需要使用到Web前端内容,而为了启动前端,这里先使用websocketd工具,它可以完成整个jsnbd的测试。

  1. 下载工具:
root@ubuntu:/home/jw/code/nbdjs# apt install websocketd
  1. 启动该工具:
root@ubuntu:/home/jw/code/nbdjs# websocketd --port=8000 --staticdir=web --binary ./nbd-proxy
Sun, 12 Nov 2023 15:03:10 +0800 | INFO   | server     |  | Serving using application   : ./nbd-proxy 
Sun, 12 Nov 2023 15:03:10 +0800 | INFO   | server     |  | Serving static content from : web
Sun, 12 Nov 2023 15:03:10 +0800 | INFO   | server     |  | Starting WebSocket server   : ws://ubuntu:8000/
Sun, 12 Nov 2023 15:03:10 +0800 | INFO   | server     |  | Serving CGI or static files : http://ubuntu:8000/

参数说明:

  • port:指定端口,浏览器打开IP时需要增加该端口。
  • staticdir:指定前端页面入口,浏览器默认会去获取该目录下的index.html,该文件也已经在开源代码中。
  • binary:指定二进制模式,数据会被转到nbd-proxy程序中。
  1. 打开浏览器,界面如下,点击“Browse”可以选择文件,“Serve Image”后开始服务:

在这里插入图片描述

  1. 通过/dev/nbd0就可以查看到远端的文件:
root@ubuntu:/home/jw# fdisk -l /dev/nbd0
Disk /dev/nbd0: 1 MiB, 1048576 bytes, 2048 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
  1. 而点击浏览器的“Stop”之后,就无法再看到设备了:
root@ubuntu:/home/jw# fdisk -l /dev/nbd0
fdisk: cannot open /dev/nbd0: Inappropriate ioctl for device

到这里jsnbd的前后端就联系起来了。

不过这里使用的是websocketd工具,它将数据直接传递给了nbd-proxy,对于lighttpd并不能直接使用这一套东西。为此需要修改相关的代码,为此需要先了解nbd-proxy的实现。

代码说明

nbd-proxy的实现主要是以下的几个部分。

  1. 首先初始化,这需要读取外部的文件,这个文件在之前已经介绍过,就是config.json。读取文件和初始化的流程大致如下:
获取参数
由参数确定配置项
初始化上下文
初始化数据缓存
初始化配置
指定配置项

关于配置项的初始化依赖于两个函数config_init()config_select(),后者通过入参来指定配置项。

配置项的一个示例如下:

{"timeout": 30,"configurations": {"0": {"nbd-device": "/dev/nbd0","metadata": {"description": "Virtual media device"}},"1": {"nbd-device": "/dev/nbd1","metadata": {"description": "Dump Offload"}}}
}

整个初始化和选择配置结果是:

  • 由参数决定具体使用哪个配置。
  • 如果没有参数指定,则使用配置中的默认项。
  • 如果连配置项也没有,则第一项就是默认项。

后续简化了这个配置文件,直接留一项,这样就会只使用这项配置:

{"timeout": 30,"configurations": {"0": {"nbd-device": "/dev/nbd0","metadata": {"description": "Virtual media device"}}}
}
  1. 创建用于nbd-proxy和nbd-client的socket,对应的函数是open_nbd_socket(),其主体的代码:
static int open_nbd_socket(struct ctx* ctx)
{rc = asprintf(&path, sockpath_tmpl, getpid());if (rc < 0)return -1;// 创建socket,参数说明如下:// SOCK_STREAM://  Provides sequenced, reliable, two-way, connection-based//  byte streams. An out-of-band data transmission mechanism//  may be supported.// SOCK_CLOEXEC://  Set the close-on-exec (FD_CLOEXEC) flag on the new file//  descriptor. See the description of the O_CLOEXEC flag in//  open(2) for reasons why this may be useful.sd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);// S_IRUSR: 00400 user has read permission// S_IWUSR: 00200 user has write permissionrc = fchmod(sd, S_IRUSR | S_IWUSR);addr.sun_family = AF_UNIX;  // 表示是PF_UNIX类型的socketstrncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);// addr表示的是本地地址rc = bind(sd, (struct sockaddr*)&addr, sizeof(addr));rc = listen(sd, 1);ctx->sock = sd;ctx->sock_path = path;
}

socket有两种类型,这里使用的是AF_UNIX类型的,重点就是这里的ctx->sock_path,它会被nbd-client使用到,这样socket就与nbd-client联系起来了,该操作在之后的代码中会进一步说明。

  1. 开启nbd-client,对应的函数是start_nbd_client()
static int start_nbd_client(struct ctx* ctx)
{pid = fork();// 子进程开始执行nbd-client命令if (pid == 0){// 执行的命令大致是:// nbd-client -u /usr/local/var/run/nbd.xxx.sock -n -L -t 30 /dev/nbd0// 关于参数的说明:// -u:指定UNIX域的socket,这个socket就是nbd-client拿数据的地方// -n: 表示nofor// -L:表示nonetlink// -t:指定超时时间// 最后是NDB设备execlp("nbd-client", "nbd-client", "-u", ctx->sock_path, "-n", "-L","-t", timeout_str, ctx->config->nbd_device, NULL);err(EXIT_FAILURE, "can't start ndb client");}ctx->nbd_client_pid = pid;
}

这里操作的命令就是:

nbd-client -u /usr/local/var/run/nbd.xxx.sock -n -L -t 30 /dev/nbd0

-u之后的就是前面提到的AF_UNIX类型的sockekt,最后的是nbd设备,也是我们最终访问的设备。

  1. 最重要的数据处理位于run_proxy()函数,其实现基础就是数据的搬运(通过copy_fd()函数):
static int run_proxy(struct ctx* ctx)
{/* main proxy: forward data between stdio & socket */pollfds[0].fd = ctx->sock_client;pollfds[0].events = POLLIN;pollfds[1].fd = STDIN_FILENO;pollfds[1].events = POLLIN;// 数据处理的主题,还有其它信号和设备操作的处理for (;;){errno = 0;rc = poll(pollfds, n_fd, -1);if (rc < 0){if (errno == EINTR)continue;warn("poll failed");break;}if (pollfds[0].revents){rc = copy_fd(ctx, ctx->sock_client, STDOUT_FILENO);if (rc <= 0)break;}if (pollfds[1].revents){rc = copy_fd(ctx, STDIN_FILENO, ctx->sock_client);if (rc <= 0)break;}}
}

这里的数据搬运涉及到的两端,一端是前面的socket对应的文件描述符,另一端是标准输入输出,由于websocketd接管了nbd-proxy程序的标准输入输出,所以实际上就是websocketd与socket的通信。

最终数据的传输如下图所示:

/dev/nbd0
nbd-client
socket
websocketd

当然nbd-client中还有一些其它的处理代码,它们跟信号、udev等有关,不过这些部分不影响代码修改,所以暂时不关注。

代码修改

为了使nbd-proxy能够与lighttpd连接,只需要根据lighttpd和WebSocket的示例代码进行修改即可,修改的重点在于下图:

/dev/nbd0
nbd-client
socket
新增socket
lighttpd

下面是具体的操作:

  1. 首先是新增socket,用于在lighttpd和原有的socket之间的数据通信:
static int open_web_socket(struct ctx* ctx)
{sd = socket(AF_INET, SOCK_STREAM, 0);rc = fchmod(sd, S_IRUSR | S_IWUSR);server_addr.sin_family = AF_INET;server_addr.sin_port = htons(DEFAULT_PORT);server_addr.sin_addr.s_addr = inet_addr(DEFAULT_IP);rc = bind(sd, (struct sockaddr *)&server_addr, sizeof(server_addr));rc = listen(sd, 1);ctx->web_sock = sd;
}

与nbd-proxy原始代码中的socket类型不同,这个是AF_INET类型的,因为它需要与lighttpd通信,这里的DEFAULT_PORTDEFAULT_IP需要跟lighttpd中的配置一致:

$HTTP["url"] =~ "^/websocket.test" {wstunnel.server = ("" => (("host" => "127.0.0.1","port" => "888")))wstunnel.frame-type = "binary"
}

该配置在前面也出现过,不同的是frame-type不再是text而是binary。

  1. 然后是建立lighttpd和socket的连接:
static int wait_for_web_socket(struct ctx* ctx)
{pollfds[0].fd = ctx->web_sock;pollfds[0].events = POLLIN;for (;;){rc = poll(pollfds, 1, -1);if (pollfds[0].revents){rc = accept4(ctx->web_sock, NULL, NULL, SOCK_CLOEXEC);ctx->web_sock_fd = rc;break;}}return 0;
}

连接成功之后得到文件描述符ctx->web_sock_fd,后续数据通信需要依赖于它。

  1. 修改原始代码中的数据传输代码:
static int run_proxy(struct ctx* ctx)
{if (pollfds[0].revents){rc = copy_fd(ctx, ctx->sock_client, ctx->web_sock_fd);if (rc <= 0)break;}if (pollfds[1].revents){rc = copy_fd(ctx, ctx->web_sock_fd, ctx->sock_client);if (rc <= 0)break;}}
}

该代码在前面已经出现过,不过copy_fd()的参数有做修改,不再是标准输入输出,而是前面代码中得到的文件描述符。

  1. 前端的代码也需要稍微修改,主要是WebSocket需要修改:
function start_server()
{server = new NBDServer("ws://" + location.host + "/websocket.test", file);
}

这里增加了websocket.test标识符,这样lighttpd才能够转发。

再次使用

之后就可以使用lighttpd服务器来使用nbd-proxy了,操作如下:

  1. 打开lighttpd(注意test.conf已经跟lighttpd和WebSocket中的不同):
root@ubuntu:/usr/local/lighttpd/sbin# ./lighttpd -D -f test.conf 
  1. 重新编译nbd-proxy并开启服务:
root@ubuntu:/home/jw/code/nbdjs# ./nbd-proxy 
  1. 打开浏览器上传文件:

在这里插入图片描述

指定文件,并点击“Server Image”后就开启了服务器,之后就可以正常查看/dev/nbd0了,表示已经连接上。

注意目前的代码修改非常的原始,还存在不少的问题,只能作为示例使用。

前端

前端代码没有编译之类的部分,只是包含一个html和一个js文件,把它们放在服务器配置指定的位置能够让服务器找到即可,这样打开浏览器输入正确的网址之后就会直接访问到html文件。html的实现没有多少可以介绍的,这里详细说明js文件。

代码

js代码中包含了一个状态机来处理数据,其初始化和状态机处理流程如下:

在这里插入图片描述

代码说明已经包含在注释中:

/* handshake flags */
const NBD_FLAG_FIXED_NEWSTYLE = 0x1;
const NBD_FLAG_NO_ZEROES = 0x2;/* transmission flags */
const NBD_FLAG_HAS_FLAGS = 0x1;
const NBD_FLAG_READ_ONLY = 0x2;/* option negotiation */
const NBD_OPT_EXPORT_NAME = 0x1;
const NBD_REP_FLAG_ERROR = 0x1 << 31;
const NBD_REP_ERR_UNSUP = NBD_REP_FLAG_ERROR | 1;/* command definitions */
const NBD_CMD_READ = 0;
const NBD_CMD_WRITE = 1;
const NBD_CMD_DISC = 2;
const NBD_CMD_FLUSH = 3;
const NBD_CMD_TRIM = 4;/* errno */
const EPERM = 1;
const EIO = 5;
const ENOMEM = 12;
const EINVAL = 22;
const ENOSPC = 28;
const EOVERFLOW = 75;
const ESHUTDOWN = 108;/* internal object state */
const NBD_STATE_UNKNOWN = 1;
const NBD_STATE_OPEN = 2;
const NBD_STATE_WAIT_CFLAGS = 3;
const NBD_STATE_WAIT_OPTION = 4;
const NBD_STATE_TRANSMISSION = 5;// 定义对象构造器,后续会创建该构造类型的对象,为了web\index.html
function NBDServer(endpoint, file) {// index.html中得到的文件this.file = file;this.endpoint = endpoint;this.ws = null;// NBD的状态,不同的状态下nbd有不同的操作,该操作在recv_handlers中定义// 默认初始化的是无效状态,该状态下什么也不会做,因为recv_handlers中没有它的操作函数this.state = NBD_STATE_UNKNOWN;// 存放获取到的数据this.msgbuf = null;// start函数,用于创建WebSocketthis.start = function () {// 当NBD开始执行时的状态,该状态本身也没有多大的意义this.state = NBD_STATE_OPEN;// WebSocket的构造函数:WebSocket(url[, protocols]),这里没有使用protocols,所以就只使用了urlthis.ws = new WebSocket(this.endpoint);this._log("WebSocket created");// 传输的数据格式按ArrayBuffer对象来,表示对固定长度的连续内存的引用// The ArrayBuffer object is used to represent a generic raw binary data buffer// 构造函数:ArrayBuffer()// 具体参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBufferthis.ws.binaryType = 'arraybuffer';// WebSocket通过onmessage事件来接收服务器返回的数据// 注意这里使用了bind函数// bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用// 简单说就是onmessage回调函数对应到_on_ws_messagethis.ws.onmessage = this._on_ws_message.bind(this);// WebSocket连接建立时触发this.ws.onopen = this._on_ws_open.bind(this);}// stop函数主要就是关闭WebSocketthis.stop = function () {this.ws.close();this.state = NBD_STATE_UNKNOWN;}this._log = function (msg) {// 在NBDServer创建成功之前先用console.log,后续会在index.html的代码中设置this.onlog// 这样就会在页面直接显示出来结果if (this.onlog) {this.onlog(msg);} else {console.log(msg);}}/* websocket event handlers */// ws.onopen的实现,会在建立WebSocket连接时执行// 本函数跟后端nbd-server进行协商,不过似乎也不是协商,因为就是前端发过去数据,并没有其它的交互// 具体的实现主要在_negotiate函数this._on_ws_open = function (ev) {this.client = {flags: 0,};this._negotiate();}// 入参是MessageEvent,它包含属性:// data: 返回DOMString, Blob或者ArrayBuffer,包含来自发送者的数据。对于本例疾就是ArrayBufferthis._on_ws_message = function (ev) {// 获取到ArrayBuffervar data = ev.data;if (this.msgbuf == null) {// 最开始msgbuf中是没有数据的,所以直接就可以拿来用this.msgbuf = data;} else {// 之后有数据了,就要将原本的数据放到最开始,然后往后面添加数据// Uint8Array数组类型表示一个8位无符号整型数组,创建时内容被初始化为0// 创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素// 这里创建一个足够大的内存空间,包括原有的数据和新增的数据,所以长度是两者相加var tmp = new Uint8Array(this.msgbuf.byteLength + data.byteLength);// 从源数据,比如this.msgbuf和data,拷贝数据到新的内存空间tmp.set(new Uint8Array(this.msgbuf), 0);tmp.set(new Uint8Array(data), this.msgbuf.byteLength);// 新的空间被赋值该this.msgbuf,因为后续处理数据的对象就是它this.msgbuf = tmp.buffer;}for (; ;) {// 根据不同的状态来获取处理函数// 当NBD开始工作的时候,初始的状态是_negotiate函数中设置的NBD_STATE_WAIT_CFLAGS// 所以最开始执行的操作是_handle_cflags// 当一次_handle_cflags函数处理之后,值又被设置成了NBD_STATE_WAIT_OPTION// 之后就是执行_handle_option// 当一次_handle_option函数处理之后,值可能变成NBD_STATE_TRANSMISSION,那么就会执行_handle_cmd// 或者不变,那么还是执行_handle_option// 而这个变或者不变的条件是本次的数据msgbuf// 如果是_handle_cmd,则还要根据msgbuf中的请求类型执行不同的操作:// NBD_CMD_READ:对应_handle_cmd_read操作,它是当前代码唯一支持的操作,就是后台读数据,这也是我们需要的// NBD_CMD_DISC:表示nbd-server想要端口,所以最终就是端口WebSocket// NBD_CMD_WRITE:并没事实际支持// NBD_CMD_TRIM:不支持var handler = this.recv_handlers[this.state];if (!handler) {this._log("no handler for state " + this.state);this.stop();break;}var consumed = handler(this.msgbuf);if (consumed < 0) {this._log("handler[state=" + this.state +"] returned error " + consumed);this.stop();break;}if (consumed == 0)break;if (consumed > 0) {if (consumed == this.msgbuf.byteLength) {this.msgbuf = null;break;}this.msgbuf = this.msgbuf.slice(consumed);}}}this._negotiate = function () {// 相当与一段内存var buf = new ArrayBuffer(18);// 但是这段内存不能直接使用,必须要通过"View Object"来操作// DataView就是一种"View Object",指的是自定义的解析器var data = new DataView(buf, 0, 18);// 后面就是创建数据并通过WebSocket发送,数据总共18个字节// 首先是魔术字/* NBD magic: NBDMAGIC */data.setUint32(0, 0x4e42444d);data.setUint32(4, 0x41474943);/* newstyle negotiation: IHAVEOPT */data.setUint32(8, 0x49484156);data.setUint32(12, 0x454F5054);/* flags: fixed newstyle negotiation, no padding */// 这里的值需要从nbd-server/nbd-client代码中去找,为了cliserv.h// #define NBD_FLAG_FIXED_NEWSTYLE (1 << 0)	/**< new-style export that actually supports extending */// #define NBD_FLAG_NO_ZEROES	(1 << 1)	/**< we won't send the 128 bits of zeroes if the client sends NBD_FLAG_C_NO_ZEROES */// 实际上这些数据最终都会送到后台的nbd-server/nbd-client中,而本源码中的nbd-proxy只是一个中转站data.setUint16(16, NBD_FLAG_FIXED_NEWSTYLE | NBD_FLAG_NO_ZEROES);// 当与后台的nbd-server建立连接之后的状态// 该状态下涉及处理函数_handle_cflagsthis.state = NBD_STATE_WAIT_CFLAGS;this.ws.send(buf);}/* handlers */this._handle_cflags = function (buf) {if (buf.byteLength < 4)return 0;var data = new DataView(buf, 0, 4);this.client.flags = data.getUint32(0);this._log("client flags received: 0x" +this.client.flags.toString(16));this.state = NBD_STATE_WAIT_OPTION;return 4;}this._handle_option = function (buf) {if (buf.byteLength < 16)return 0;var data = new DataView(buf, 0, 16);if (data.getUint32(0) != 0x49484156 ||data.getUint32(4) != 0x454F5054) {this._log("invalid option magic");return -1;}var opt = data.getUint32(8);var len = data.getUint32(12);this._log("client option received: 0x" + opt.toString(16));if (buf.byteLength < 16 + len)return 0;switch (opt) {case NBD_OPT_EXPORT_NAME:this._log("negotiation complete, starting transmission mode");var n = 10;if (!(this.client.flags & NBD_FLAG_NO_ZEROES))n += 124;var resp = new ArrayBuffer(n);var view = new DataView(resp, 0, 10);/* export size. */var size = this.file.size;view.setUint32(0, Math.floor(size / (2 ** 32)));view.setUint32(4, size & 0xffffffff);/* transmission flags: read-only */view.setUint16(8, NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY);this.ws.send(resp);this.state = NBD_STATE_TRANSMISSION;break;default:/* reject other options */var resp = new ArrayBuffer(20);var view = new DataView(resp, 0, 20);view.setUint32(0, 0x0003e889);view.setUint32(4, 0x045565a9);view.setUint32(8, opt);view.setUint32(12, NBD_REP_ERR_UNSUP);view.setUint32(16, 0);this.ws.send(resp);}return 16 + len;}this._create_cmd_response = function (req, rc, data = null) {var len = 16;if (data)len += data.byteLength;var resp = new ArrayBuffer(len);var view = new DataView(resp, 0, 16);view.setUint32(0, 0x67446698);view.setUint32(4, rc);view.setUint32(8, req.handle_msB);view.setUint32(12, req.handle_lsB);if (data)new Uint8Array(resp, 16).set(new Uint8Array(data));return resp;}this._handle_cmd = function (buf) {if (buf.byteLength < 28)return 0;var view = new DataView(buf, 0, 28);if (view.getUint32(0) != 0x25609513) {this._log("invalid request magic");return -1;}var req = {flags: view.getUint16(4),type: view.getUint16(6),handle_msB: view.getUint32(8),handle_lsB: view.getUint32(12),offset_msB: view.getUint32(16),offset_lsB: view.getUint32(20),length: view.getUint32(24),};/* we don't support writes, so nothing needs the data at present *//* req.data = buf.slice(28); */var err = 0;var consumed = 28;/* the command handlers return 0 on success, and send their* own response. Otherwise, a non-zero error code will be* used as a simple error response*/switch (req.type) {case NBD_CMD_READ:err = this._handle_cmd_read(req);break;case NBD_CMD_DISC:err = this._handle_cmd_disconnect(req);break;case NBD_CMD_WRITE:/* we also need length bytes of data to consume a write* request */if (buf.byteLength < 28 + req.length)return 0;consumed += req.length;err = EPERM;break;case NBD_CMD_TRIM:err = EPERM;break;default:this._log("invalid command 0x" + req.type.toString(16));err = EINVAL;}if (err) {var resp = this._create_cmd_response(req, err);this.ws.send(resp);}return consumed;}this._handle_cmd_read = function (req) {var offset;offset = (req.offset_msB * 2 ** 32) + req.offset_lsB;if (offset > Number.MAX_SAFE_INTEGER)return ENOSPC;if (offset + req.length > Number.MAX_SAFE_INTEGER)return ENOSPC;if (offset + req.length > file.size)return ENOSPC;this._log("read: 0x" + req.length.toString(16) +" bytes, offset 0x" + offset.toString(16));var blob = this.file.slice(offset, offset + req.length);var reader = new FileReader();// 发送获取到的文件内容reader.onload = (function (ev) {var reader = ev.target;if (reader.readyState != FileReader.DONE)return;var resp = this._create_cmd_response(req, 0, reader.result);this.ws.send(resp);}).bind(this);reader.onerror = (function (ev) {var reader = ev.target;this._log("error reading file: " + reader.error);var resp = this._create_cmd_response(req, EIO);this.ws.send(resp);}).bind(this);// 前面的blob加上这个函数,用来读取文件的内容reader.readAsArrayBuffer(blob);return 0;}this._handle_cmd_disconnect = function (req) {this._log("disconnect received");this.stop();return 0;}// NBD处于不同状态下时,对应的处理函数// 不同的状态由state控制this.recv_handlers = Object.freeze({[NBD_STATE_WAIT_CFLAGS]: this._handle_cflags.bind(this),[NBD_STATE_WAIT_OPTION]: this._handle_option.bind(this),[NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this),});
}

相关文章:

【BMC】jsnbd介绍

jsnbd介绍 本文主要介绍一个名为jsnbd的开源项目&#xff0c;位于GitHub - openbmc/jsnbd&#xff0c;它实现了一个前端&#xff08;包含HTML和JS文件&#xff09;页面&#xff0c;作为存储服务器&#xff0c;可以指定存储内容&#xff1b;还包含一个后端的代理&#xff0c;这…...

个推「数据驱动运营增长」上海专场:携程智行火车票分享OTA行业的智能用户运营实践

近日&#xff0c;以“数据增能&#xff0c;高效提升用户运营价值”为主题的个推「数据驱动运营增长」城市巡回沙龙上海专场圆满举行。携程智行火车票用户运营负责人王银笛分享OTA行业的智能用户运营实践。 ▲ 王银笛 携程智行火车票用户运营负责人 负责智行业务线用户运营。从0…...

Linux--gcc/g++

一、gcc/g是什么 gcc的全称是GNU Compiler Collection&#xff0c;它是一个能够编译多种语言的编译器。最开始gcc是作为C语言的编译器&#xff08;GNU C Compiler&#xff09;&#xff0c;现在除了c语言&#xff0c;还支持C、java、Pascal等语言。gcc支持多种硬件平台 二、gc…...

MySQL5.7源码编译安装

查看是否安装过mysql软件包 rpm -qa|grep mysql rpm -qa|grep mariadb rpm -e --nodeps mysql的软件包名建立mysql账号 useradd -s /sbin/nologin -M mysql安装依赖包 yum install -y gcc yum install -y gcc-c yum install -y ncurses yum install -y bison yum install -y…...

uniapp使用v-for页面不刷新解决办法

项目场景&#xff1a; 做一个项目&#xff0c;v-for循环数据库数据&#xff0c;使用uni-load-more&#xff0c;结果发现... DOM中的列表却没有更新 解决方案&#xff1a; 根据网上教程&#xff0c;加了一个触底函数onReachBottom&#xff0c;结果发现无论如何也更新不了DOM中…...

发布一款将APM日志转换为Excel的开源工具

这几年有不少朋友向我咨询如何将APM日志转换为Excel&#xff0c;我之前的答复是先将日志转换为MATLAB的格式&#xff0c;然后用MATLAB导出为Excel。但是实际上不是每个人都会用MATLAB&#xff0c;并且处理数据也不是非要用MATLAB&#xff0c;更不是说用MATLAB了就显得专业、显得…...

本地化小程序运营 同城小程序开发

时空的限制让本地化的线上平台成为一种追求&#xff0c;58及某团正式深挖人们城镇化、本地化的信息和商业需求而崛起的平台&#xff0c;将二者结合成本地化小程序&#xff0c;显然有着巨大的市场机会。本地化小程序运营可以结合本地化生活需求的一些信息&#xff0c;以及激发商…...

关于electron打包卡在winCodeSign下载问题

简单粗暴&#xff0c;直接上解决方案&#xff1a; 在你的项目根目录下创建一个.npmrc的文件&#xff0c;且在里面加上以下文本&#xff0c;不用在意这个镜像源是不是最新的&#xff0c;它会自己重定向到nodemirror这个域名里下载 ELECTRON_MIRRORhttps://npm.taobao.org/mirror…...

01_ddim_inversion_CN

DDIM反转 设置 # !pip install -q transformers diffusers accelerateimport torch import requests import torch.nn as nn import torch.nn.functional as F from PIL import Image from io import BytesIO from tqdm.auto import tqdm from matplotlib import pyplot as p…...

ElasticSearch的文档、字段、映射和高级查询

1. 文档&#xff08;Document&#xff09; 在ES中一个文档是一个可被索引的基础信息单元&#xff0c;也就是一条数据 比如&#xff1a;你可以拥有某一个客户的文档&#xff0c;某一个产品的一个文档&#xff0c;当然&#xff0c;也可以拥有某个订单的一个文档。文档以JSON&…...

vim相关命令讲解!

本文旨在讲解vim 以及其相关的操作&#xff01; 希望读完本文&#xff0c;读者会有一定的收获&#xff01;好的&#xff0c;干货马上就来&#xff01; 初识vim 在讲解vim之前&#xff0c;我们首先要了解vim是什么&#xff0c;有什么作用&#xff1f;只有了解了vim才能更好的理…...

22.构造一个关于员工信息的结构体数组,存储十个员工的信息

结构体问题。构造一个关于员工信息的结构体数组&#xff0c;存储十个员工的信息&#xff0c;包括员工工号&#xff0c;员工工资&#xff0c;员工所得税&#xff0c;员工实发工资。要求工号和工资由键盘输入&#xff0c;并计算出员工所得税&#xff08;所得税工资*0.2&#xff0…...

北京刘家窑中医院举行‘心梗救治日’宣传活动,郭自强主任呼吁提高群众防治意识

...

calico

calico:默认是ip-ip模式&#xff0c; ipip 开销小 vxlan模式&#xff1a;后期版本才支持 不会创建虚拟交换机 Calico 是一种用于构建和管理容器网络的开源软件定义网络&#xff08;SDN&#xff09;解决方案。它专门设计用于在容器和虚拟机之间提供高性能、高可扩展性和灵活的…...

web前端开发第3次Dreamweave课堂练习/html练习代码《网页设计语言基础练习案例》

目标图片&#xff1a; 文字素材&#xff1a; 网页设计语言基础练习案例 ——几个从语义上和文字相关的标签 * h标签&#xff08;h1~h6&#xff09;&#xff1a;用来定义网页的标题&#xff0c;成对出现。 * p标签&#xff1a;用来设置网页的段落&#xff0c;成对出现。 * b…...

APP备案获取安卓app证书公钥获取方法和签名MD5值

前言 在开发和发布安卓应用程序时&#xff0c;了解应用程序证书的公钥和签名MD5值是很重要的。这些信息对于应用程序的安全性和合规性至关重要。现在又因为今年开始APP必须接入备案才能在国内各大应用市场上架&#xff0c;所以获取这两个值成了所有开发者的必经之路。本文将介…...

cefsharp 93.1.140 如何在js中暴露c#类

从cefsharp79版本开始&#xff0c;旧的RegisterJsObject方法被删除了。 也就是说想使用79以后的版本&#xff0c;就必须更新js暴露c#对象的方法了。由于79之前的注册方法是不需要在js中进行注册的&#xff0c;在93版本上如何在不改动前端页面的基础上实现内核升级咧&#xff0c…...

同一台Linux同时安装MYSQL5.7和MYSQL8(第一篇)

在一台Linxu上面同时安装mysql5.7和mysql8.0的步骤&#xff0c;记录一下&#xff0c;方便后续回顾&#xff0c;后续文章之后会接着介绍搭建两台虚拟机一主一从的架构。 其中配置的文件名称、目录、端口号、IP地址要根据自己电脑的实际情况进行更改。 安装完成后效果 [rootzong…...

【CSS】解决上层盒子遮挡下层图片点击事件的三种方法

1. Pointer Events 属性 CSS 的 pointer-events 属性是一个强大的工具&#xff0c;可以控制元素是否接收用户的交互事件。通过将上层盒子的 pointer-events 设置为 none&#xff0c;我们可以确保它不会阻止下层图片的点击事件。 .upper-box {z-index: 999; /* 设置更高的 z-i…...

力扣每日一题 ---- 2906. 构造乘积矩阵

这题很简单(一下就能想到是前缀和的提米)&#xff0c;但是在处理12345上面需要仔细一点&#xff0c;本来我最开始想到的时候全部累乘在除掉当前数&#xff0c;但是这样就没有把12345考虑进去&#xff0c;如果他本身是12345的话&#xff0c;那么除他以外的乘积并不一定是0&#…...

网络编程(Modbus进阶)

思维导图 Modbus RTU&#xff08;先学一点理论&#xff09; 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议&#xff0c;由 Modicon 公司&#xff08;现施耐德电气&#xff09;于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

idea大量爆红问题解决

问题描述 在学习和工作中&#xff0c;idea是程序员不可缺少的一个工具&#xff0c;但是突然在有些时候就会出现大量爆红的问题&#xff0c;发现无法跳转&#xff0c;无论是关机重启或者是替换root都无法解决 就是如上所展示的问题&#xff0c;但是程序依然可以启动。 问题解决…...

PHP和Node.js哪个更爽?

先说结论&#xff0c;rust完胜。 php&#xff1a;laravel&#xff0c;swoole&#xff0c;webman&#xff0c;最开始在苏宁的时候写了几年php&#xff0c;当时觉得php真的是世界上最好的语言&#xff0c;因为当初活在舒适圈里&#xff0c;不愿意跳出来&#xff0c;就好比当初活在…...

STM32标准库-DMA直接存储器存取

文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA&#xff08;Direct Memory Access&#xff09;直接存储器存取 DMA可以提供外设…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享

文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的&#xff0c;根据Excel列的需求预估的工时直接打骨折&#xff0c;不要问我为什么&#xff0c;主要…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

C++八股 —— 单例模式

文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全&#xff08;Thread Safety&#xff09; 线程安全是指在多线程环境下&#xff0c;某个函数、类或代码片段能够被多个线程同时调用时&#xff0c;仍能保证数据的一致性和逻辑的正确性&#xf…...

React---day11

14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store&#xff1a; 我们在使用异步的时候理应是要使用中间件的&#xff0c;但是configureStore 已经自动集成了 redux-thunk&#xff0c;注意action里面要返回函数 import { configureS…...

AGain DB和倍数增益的关系

我在设置一款索尼CMOS芯片时&#xff0c;Again增益0db变化为6DB&#xff0c;画面的变化只有2倍DN的增益&#xff0c;比如10变为20。 这与dB和线性增益的关系以及传感器处理流程有关。以下是具体原因分析&#xff1a; 1. dB与线性增益的换算关系 6dB对应的理论线性增益应为&…...

PostgreSQL——环境搭建

一、Linux # 安装 PostgreSQL 15 仓库 sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm# 安装之前先确认是否已经存在PostgreSQL rpm -qa | grep postgres# 如果存在&#xff0…...