Rust入门(十六):手写web服务器和线程池
这一章将实现一个手写的 web server 和 多线程的服务器,用到之前学到的所有特性
简单的web server
作为一个 web 服务器,我们首先要能接收到请求,目前市面上的 web 服务大多数都是基于 HTTP 和 HTTPS 协议的,而他们有是基于 TCP 协议传输的 ,所以我们希望我们的服务器可以监听 TCP 连接,rust 的库函数中提供了 TcpListener
这样的函数来监听 TCP 连接,我们新建一个项目,然后在 src/main.rs
中编写监听功能。
TcpListener
的 incoming
方法返回一个迭代器,它提供了一系列的流,流(stream)代表一个客户端和服务端之间打开的连接
use std::net::TcpListener;fn main() {let listener = TcpListener::bind("127.0.0.1:7878").unwrap();for stream in listener.incoming() {let stream = stream.unwrap();println!("Connection established!");}
}
运行代码,之后访问 127.0.0.1:7878 就可以看到接收到了信息,如果你成功接收到了信息,那么下一步就是怎么样处理信息,我们使用一个缓冲区接收我们的数据,此时它是一个 &u8 类型的字节流数据,我们将它转化为字符串打印出来:
use std::{io::{prelude::*, BufReader},net::{TcpListener, TcpStream},
};fn main() {let listener = TcpListener::bind("127.0.0.1:7878").unwrap();for stream in listener.incoming() {let stream = stream.unwrap();handle_connection(stream);}
}fn handle_connection(mut stream: TcpStream) {let mut buffer = [0; 1024];stream.read(&mut buffer).unwrap();println!("Request: {}", String::from_utf8_lossy(&buffer[..]));
}
现在运行我们的程序,它将打印出一个完整的 TCP 请求,作为一个服务器,对于一个请求,我们需要做出我们的回复,提供给它需要的资源,这里我们就返回先尝试返回一个简单的响应:
因为 stream
的 write_all
方法获取一个 &[u8]
并直接将这些字节发送给连接。所以我们调用as_bytes
对数据进行转化
fn handle_connection(mut stream: TcpStream) {let mut buffer = [0; 1024];stream.read(&mut buffer).unwrap();let response = "HTTP/1.1 200 OK\r\n\r\n";stream.write_all(response.as_bytes()).unwrap();stream.flush().unwrap();
}
此时我们再次访问我们的地址,将不会得到报错信息,而是返回一个空白的页面,说明我们的页面请求已经得到了响应,那么接下来我们需要让我们的服务器能够返回真实的 web 页面:
我们编写一个 hello.html 页面,放在项目的根目录下:
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><title>Hello!</title></head><body><h1>Hello!</h1><p>Hi from Rust</p></body>
</html>
之后在返回处理方法中读取我们刚刚编写页面的内容,构造成 TCP 请求的返回格式进行返回:
use std::{fs,io::{prelude::*, BufReader},net::{TcpListener, TcpStream},
};
// --snip--fn handle_connection(mut stream: TcpStream) {let mut buffer = [0; 1024];stream.read(&mut buffer).unwrap();let status_line = "HTTP/1.1 200 OK";let contents = fs::read_to_string("hello.html").unwrap();let length = contents.len();let response =format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");stream.write_all(response.as_bytes()).unwrap();stream.flush().unwrap();
}
当然我们的服务器不可能只返回一个页面,对于不同路径的 TCP 请求,我们需要为他们返回不同的页面,所以我们做了一个模式匹配,如果是请求的主页我们返回一个页面,如果请求其他页面,我们返回一个 404 页面表示找不到
fn handle_connection(mut stream: TcpStream) {let mut buffer = [0; 1024];stream.read(&mut buffer).unwrap();let get = b"GET / HTTP/1.1\r\n";let (status_line, filename) = if buffer.starts_with(get) {("HTTP/1.1 200 OK", "index.html")} else {("HTTP/1.1 404 NOT FOUND", "404.html")};let contents = fs::read_to_string(filename).unwrap();let response = format!("{}\r\nContent-Length: {}\r\n\r\n{}",status_line,contents.len(),contents);stream.write_all(response.as_bytes()).unwrap();stream.flush().unwrap();
}
多线程的web server
之前搭建的服务器,一次只能处理一个请求,如果我们遇到多个请求的情况,只有当上一个请求处理完毕才能处理下一个请求,当一个请求时间很长的时候,会极大影响整体的性能,比如我们构造一个 sleep 路径,它在阻塞 5 秒钟后才返回,那么整个服务器的所有请求都会阻塞:
use std::fs;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
use std::thread;
use std::time::Duration;
fn handle_connection(mut stream: TcpStream) {let mut buffer = [0; 1024];stream.read(&mut buffer).unwrap();let get = b"GET / HTTP/1.1\r\n";let sleep = b"GET /sleep HTTP/1.1\r\n";let (status_line, filename) = if buffer.starts_with(get) {("HTTP/1.1 200 OK", "index.html")} else if buffer.starts_with(sleep) {thread::sleep(Duration::from_secs(5));("HTTP/1.1 200 OK", "index.html")} else {("HTTP/1.1 404 NOT FOUND", "404.html")};let contents = fs::read_to_string(filename).unwrap();let response = format!("{}\r\nContent-Length: {}\r\n\r\n{}",status_line,contents.len(),contents);stream.write_all(response.as_bytes()).unwrap();stream.flush().unwrap();
}
为了解决这个情况我们需要一个多线程的服务器,我们需要设计一个 线程池(thread pool)。当程序收到一个新任务,线程池中的一个线程会被分配任务,这个线程会离开并处理任务。其余的线程则可用于处理在第一个线程处理任务的同时处理其他接收到的任务。当第一个线程处理完任务时,它会返回空闲线程池中等待处理新任务。线程池允许我们并发处理连接,增加 server 的吞吐量。同时,为了保证我们的服务器 不被 Dos 攻击,我们需要限定我们线程的数量,所有我们的线程池应该在初始化的时候可以限定最大线程数,所以我们的线程池调用应该是这样的:
使用 ThreadPool::new
来创建一个新的线程池,它有一个可配置的线程数的参数,pool.execute
有着类似 thread::spawn
的接口,它获取一个线程池运行于每一个流的闭包。
fn main() {let listener = TcpListener::bind("127.0.0.1:7878").unwrap();let pool = ThreadPool::new(4);for stream in listener.incoming() {let stream = stream.unwrap();pool.execute(|| {handle_connection(stream);});}
}
有了目标之后,我们就可以编写我们的 ThreadPool
程序,我们创建 src/lib.rs
文件,在其中创建我们的 ThreadPool
,它包含一个 new 函数以及一个 执行函数 execute
pub struct ThreadPool;impl ThreadPool {pub fn new(size: usize) -> ThreadPool {ThreadPool}
}
pub fn execute<F>(&self, f: F)whereF: FnOnce() + Send + 'static,{}
这里传入泛型如此约束的原因是,我们的 execute
函数参照了标准库的 thread::spawn
函数,它的定义如下:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>whereF: FnOnce() -> T,F: Send + 'static,T: Send + 'static,
之后我们可以看到,spawn
返回一个 JoinHandle
类型的数据,所以我们也可以使用它来包裹我们的线程,我们在 ThreadPool
结构中定义 Vec 来存储我们的线程:
use std::thread;pub struct ThreadPool {threads: Vec<thread::JoinHandle<()>>,
}impl ThreadPool {// --snip--pub fn new(size: usize) -> ThreadPool {assert!(size > 0);let mut threads = Vec::with_capacity(size);for _ in 0..size {// create some threads and store them in the vector}ThreadPool { threads }}// --snip--
}
此时我们的项目已经可以运行了,我们还没有实现我们的线程的具体内容,我们将要实现的行为是创建线程并稍后发送代码,所以我们需要在 ThreadPool
和线程间引入一个新数据类型来管理这种新行为,这个数据结构称为 Worker,每一个 Worker
会储存一个单独的 JoinHandle<()>
实例。接着会在 Worker
上实现一个方法,它会获取需要允许代码的闭包并将其发送给已经运行的线程执行。我们还会赋予每一个 worker id
,这样就可以在日志和调试中区别线程池中的不同 worker。
use std::thread;pub struct ThreadPool {workers: Vec<Worker>,
}impl ThreadPool {// --snip--pub fn new(size: usize) -> ThreadPool {assert!(size > 0);let mut workers = Vec::with_capacity(size);for id in 0..size {workers.push(Worker::new(id));}ThreadPool { workers }}// --snip--
}
struct Worker {id: usize,thread: thread::JoinHandle<()>,
}
impl Worker {fn new(id: usize) -> Worker {let thread = thread::spawn(|| {});Worker { id, thread }}
}
我们已经可以创建线程,但是之后我们需要将我们通过 execute
方法传递进来的方法放入我们的线程中,我们使用 信道 —— 作为沟通工具,方案如下:
ThreadPool
会创建一个信道并充当发送者。- 每个
Worker
将会充当接收者。 - 新建一个
Job
结构体来存放用于向信道中发送的闭包。 execute
方法会在发送者发出期望执行的任务。- 在线程中,
Worker
会遍历接收者并执行任何接收到的任务。
use std::{sync::mpsc, thread};pub struct ThreadPool {workers: Vec<Worker>,sender: mpsc::Sender<Job>,
}struct Job;impl ThreadPool {// --snip--pub fn new(size: usize) -> ThreadPool {assert!(size > 0);let (sender, receiver) = mpsc::channel();let mut workers = Vec::with_capacity(size);for id in 0..size {workers.push(Worker::new(id, receiver));}ThreadPool { workers, sender }}// --snip--
}// --snip--
impl Worker {fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {let thread = thread::spawn(|| {receiver;});Worker { id, thread }}
}
但是上述的代码有一个问题,这段代码尝试将 receiver
传递给多个 Worker
实例。这是不行的,Rust 所提供的信道实现是多 生产者,单 消费者 的。为了在多个线程间共享所有权并允许线程修改其值,需要使用 Arc<Mutex<T>>
。Arc
使得多个 worker 拥有接收端,而 Mutex
则确保一次只有一个 worker 能从接收端得到任务:
use std::{sync::{mpsc, Arc, Mutex},thread,
};
// --snip--impl ThreadPool {// --snip--pub fn new(size: usize) -> ThreadPool {assert!(size > 0);let (sender, receiver) = mpsc::channel();let receiver = Arc::new(Mutex::new(receiver));let mut workers = Vec::with_capacity(size);for id in 0..size {workers.push(Worker::new(id, Arc::clone(&receiver)));}ThreadPool { workers, sender }}// --snip--
}// --snip--impl Worker {fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {// --snip--}
}
最后我们需要实现 ThreadPool
上的 execute
方法,它将接收到的方法传递到线程中,并且执行它。而在 worker 中,传递给 thread::spawn
的闭包仍然还只是 引用 了信道的接收端。我们需要闭包一直循环,向信道的接收端请求任务,并在得到任务时执行他们。
这里调用了 lock
来获取互斥器,如果互斥器处于一种叫做 被污染(poisoned)的状态时获取锁可能会失败,这可能发生于其他线程在持有锁时 panic 了且没有释放锁。如果锁定了互斥器,接着调用 recv
从信道中接收 Job
,调用 recv
会阻塞当前线程,所以如果还没有任务,其会等待直到有可用的任务。
type Job = Box<dyn FnOnce() + Send + 'static>;impl ThreadPool {// --snip--pub fn execute<F>(&self, f: F)whereF: FnOnce() + Send + 'static,{let job = Box::new(f);self.sender.send(job).unwrap();}
}// --snip--impl Worker {fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {let thread = thread::spawn(move || loop {let job = receiver.lock().unwrap().recv().unwrap();println!("Worker {id} got a job; executing.");job();});Worker { id, thread }}
}
停机与清理
之前编写代码已经可以完整运行了,但是面临两个问题,一个是我们不能停止我们的服务器,只能使用命令行的强制停止程序命令,并且我们的服务器并没有清理所有的内容,所以我们需要继续完善我们的服务器:
首先,我们为线程池实现 Drop
。当线程池被丢弃时,应该 join 所有线程以确保他们完成其操作。这里首先遍历线程池中的每个 workers
。对于每一个线程,会打印出说明信息表明此特定 worker 正在关闭,接着在 worker 线程上调用 join
。如果 join
调用失败,通过 unwrap
使得 panic 并进行不优雅的关闭。
impl Drop for ThreadPool {fn drop(&mut self) {for worker in &mut self.workers {println!("Shutting down worker {}", worker.id);worker.thread.join().unwrap();}}
}
因为目前只有每一个 worker
的可变借用,而 join
需要获取其参数的所有权。如果 Worker
存放的是 Option<thread::JoinHandle<()>
,就可以在 Option
上调用 take
方法将值从 Some
成员中移动出来而对 None
成员不做处理。换句话说,正在运行的 Worker
的 thread
将是 Some
成员值,而当需要清理 worker 时,将 Some
替换为 None
,这样 worker 就没有可以运行的线程了。所以我们需要为此需要更新 Worker
的定义为如下:
struct Worker {id: usize,thread: Option<thread::JoinHandle<()>>,
}
对此,当新建 Worker
时需要将 thread
值封装进 Some
。
impl Worker {fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {// --snip--Worker {id,thread: Some(thread),}}
}impl Drop for ThreadPool {fn drop(&mut self) {for worker in &mut self.workers {println!("Shutting down worker {}", worker.id);if let Some(thread) = worker.thread.take() {thread.join().unwrap();}}}
}
现在程序可以通过编译了,但是存在的问题是: Worker
中分配的线程所运行的闭包中的逻辑:调用 join
并不会关闭线程,因为他们一直 loop
来寻找任务。如果采用这个实现来尝试丢弃 ThreadPool
,则主线程会永远阻塞在等待第一个线程结束上。
所以我们需要修改 ThreadPool
的 drop
实现并修改 Worker
循环。我们需要在等待线程结束前显式丢弃 sender
pub struct ThreadPool {workers: Vec<Worker>,sender: Option<mpsc::Sender<Job>>,
}
// --snip--
impl ThreadPool {pub fn new(size: usize) -> ThreadPool {// --snip--ThreadPool {workers,sender: Some(sender),}}pub fn execute<F>(&self, f: F)whereF: FnOnce() + Send + 'static,{let job = Box::new(f);self.sender.as_ref().unwrap().send(job).unwrap();}
}impl Drop for ThreadPool {fn drop(&mut self) {drop(self.sender.take());for worker in &mut self.workers {println!("Shutting down worker {}", worker.id);if let Some(thread) = worker.thread.take() {thread.join().unwrap();}}}
}
丢弃 sender
会关闭信道,这表明不会有更多的消息被发送,这时 worker 中的无限循环中的所有 recv
调用都会返回错误。我们修改 Worker
循环在这种情况下优雅地退出,这意味着当 ThreadPool
的 drop
实现调用 join
时线程会结束。
impl Worker {fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {let thread = thread::spawn(move || loop {let message = receiver.lock().unwrap().recv();match message {Ok(job) => {println!("Worker {id} got a job; executing.");job();}Err(_) => {println!("Worker {id} disconnected; shutting down.");break;}}});Worker {id,thread: Some(thread),}}
}
修改 main
函数,可以让收到指定数量的请求后停机:
fn main() {let listener = TcpListener::bind("127.0.0.1:7878").unwrap();let pool = ThreadPool::new(4);for stream in listener.incoming().take(2) {let stream = stream.unwrap();pool.execute(|| {handle_connection(stream);});}println!("Shutting down.");
}
至此一个完整的小型 web 服务器搭建完毕,完整的代码如下:
文件名:src/main.rs
use hello::ThreadPool;//改成你hello的项目名字
use std::fs;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
use std::thread;
use std::time::Duration;fn main() {let listener = TcpListener::bind("127.0.0.1:7878").unwrap();let pool = ThreadPool::new(4);for stream in listener.incoming().take(2) {let stream = stream.unwrap();pool.execute(|| {handle_connection(stream);});}println!("Shutting down.");
}fn handle_connection(mut stream: TcpStream) {let mut buffer = [0; 1024];stream.read(&mut buffer).unwrap();let get = b"GET / HTTP/1.1\r\n";let sleep = b"GET /sleep HTTP/1.1\r\n";let (status_line, filename) = if buffer.starts_with(get) {("HTTP/1.1 200 OK", "hello.html")} else if buffer.starts_with(sleep) {thread::sleep(Duration::from_secs(5));("HTTP/1.1 200 OK", "hello.html")} else {("HTTP/1.1 404 NOT FOUND", "404.html")};let contents = fs::read_to_string(filename).unwrap();let response = format!("{}\r\nContent-Length: {}\r\n\r\n{}",status_line,contents.len(),contents);stream.write_all(response.as_bytes()).unwrap();stream.flush().unwrap();
}
文件名:src/lib.rs:
use std::{sync::{mpsc, Arc, Mutex},thread,
};pub struct ThreadPool {workers: Vec<Worker>,sender: Option<mpsc::Sender<Job>>,
}type Job = Box<dyn FnOnce() + Send + 'static>;impl ThreadPool {/// Create a new ThreadPool.////// The size is the number of threads in the pool.////// # Panics////// The `new` function will panic if the size is zero.pub fn new(size: usize) -> ThreadPool {assert!(size > 0);let (sender, receiver) = mpsc::channel();let receiver = Arc::new(Mutex::new(receiver));let mut workers = Vec::with_capacity(size);for id in 0..size {workers.push(Worker::new(id, Arc::clone(&receiver)));}ThreadPool {workers,sender: Some(sender),}}pub fn execute<F>(&self, f: F)whereF: FnOnce() + Send + 'static,{let job = Box::new(f);self.sender.as_ref().unwrap().send(job).unwrap();}
}impl Drop for ThreadPool {fn drop(&mut self) {drop(self.sender.take());for worker in &mut self.workers {println!("Shutting down worker {}", worker.id);if let Some(thread) = worker.thread.take() {thread.join().unwrap();}}}
}struct Worker {id: usize,thread: Option<thread::JoinHandle<()>>,
}impl Worker {fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {let thread = thread::spawn(move || loop {let message = receiver.lock().unwrap().recv();match message {Ok(job) => {println!("Worker {id} got a job; executing.");job();}Err(_) => {println!("Worker {id} disconnected; shutting down.");break;}}});Worker {id,thread: Some(thread),}}
}
这里还有很多可以做的事!如果你希望继续增强这个项目,如下是一些点子:
- 为
ThreadPool
和其公有方法增加更多文档 - 为库的功能增加测试
- 将
unwrap
调用改为更健壮的错误处理 - 使用
ThreadPool
进行其他不同于处理网络请求的任务 - 在 crates.io 上寻找一个线程池 crate 并使用它实现一个类似的 web server,将其 API 和鲁棒性与我们的实现做对比
相关文章:
Rust入门(十六):手写web服务器和线程池
这一章将实现一个手写的 web server 和 多线程的服务器,用到之前学到的所有特性 简单的web server 作为一个 web 服务器,我们首先要能接收到请求,目前市面上的 web 服务大多数都是基于 HTTP 和 HTTPS 协议的,而他们有是基于 TCP…...

数据结构——第二章 线性表(1)——顺序结构
线性表1. 线性表1.1 线性表的定义1.1.1 访问型操作1.1.2 加工型操作1.2 线性表的顺序存储结构1.2.1 定义顺序表数据类型方法11.2.2 定义顺序表数据类型方法21.3 顺序表的基本操作实现1.3.1 顺序表的初始化操作1.3.2 顺序表的插入操作1.3.3 顺序表的删除操作1.3.4 顺序表的更新操…...

YOLO 格式数据集制作
目录 1. YOLO简介 2.分割数据集准备 3.代码展示 整理不易,欢迎一键三连!!! 1. YOLO简介 YOLO(You Only Look Once)是一种流行的目标检测和图像分割模型,由华盛顿大学的 Joseph Redmon 和 Al…...
基于linux内核的驱动开发
1 字符设备驱动框架 1.1字符设备 定义:只能以一个字节一个字节的方式读写的设备,不能随机的读取设备中中的某一段数据,读取数据需要按照先后顺序。(字符设备是面向字节流的) 常见的字…...

找不到工作的测试员一大把,大厂却招不到优秀软件测试员?高薪难寻测试工程师。
测试工程师招了快一个月了,实在招不到合适的,已经在被解雇的边缘了。。。” 初级测试工程师非常多,但真正掌握测试思维、能力强的优秀测试太少了! 据我所知, 当下的测试人员不少状态都是这样的: 在工作中…...

buuctf Basic
buuctf Basic 1.Linux Labs 根据提示我们可以知道需要远程连接linux服务器,这里使用xshell进行如下配置 输入ssh的用户名root,密码123456 连接成功 构造命令 ls …/ 查看文件 查看flag cat …/flag.txt 为flag{8fee8783-1ed5-4b67-90eb-a1d603a0208…...

赛狐ERP|亚马逊产品缺货怎么办?该如何补救?
由于物流时效的延长,运输成本的增加,亚马逊的仓储限制等各种原因,断货问题很常成为亚马逊卖家的普遍困扰。那么亚马逊产品缺货应该怎么办!1、提高产品价格:除了卖自己的Listing此外,提高产品价格也是一种保…...
《Elasticsearch源码解读与优化实战》张超-读书笔记
写在前面 好久没更新博客了,应届狗没办法啊╮(╯▽╰)╭为了秋招搞了小半年,从去年5月到现在搞了两段实习(京东、游戏公司),最终年前拿到一家还不错的offer,现在已经入职实习了,不出意外的话以…...

编码踩坑——运行时报错java.lang.NoSuchMethodError / 同名类加载问题 / 双亲委派【建议收藏】
本篇介绍一个实际遇到的排查异常的case,涉及的知识点包括:类加载机制、jar包中的类加载顺序、JVM双亲委派模型、破坏双亲委派模型及自定义类加载器的代码示例;问题背景业务版本,旧功能升级,原先引用的一个二方包中的du…...

软件测试选Python还是Java?
目录 前言 1、先从一门语言开始 2、两个语言的区别 3、两个语言的测试栈技术 4、如何选择两种语言? 总结 前言 对于工作多年的从业者来说,同时掌握java和Python两门语言再好不过,可以大大增加找工作时的选择范围。但是对于转行的人或者…...

“2023数据安全智能化中国行”活动,开幕即高能
工信部等16部门近日发布的《关于促进数据安全产业发展的指导意见》提出,到2025年,数据安全产业基础能力和综合实力明显增强,数据安全产业规模超过1500亿元,年复合增长率超过30%。到2035年,数据安全产业进入繁荣成熟期。…...

机器人操作规划——Deep Visual Foresight for Planning Robot Motion(2017 ICRA)
1 简介 model-based RL方法,预测Action对图像的变化,以push任务进行研究。 采用完全自监督的学习方式,不需要相机标定、3D模型、深度图像和物理仿真。 2 数据集 采用几百个物体、10个7dof机械臂采集了包括5万个push attempts的数据集。 每…...
go 连接redis集群
最近用redis shake做redis数据迁移,由于redis提供的客户端没有用于查看集群的工具,且我部署的redis集群是基于k8s来构建的,没有使用ingress做转发,所以只能在k8s内部访问集群,于是我先用gogin框架编写了访问redis集群的…...
LeetCode 146. LRU 缓存
原题链接 难度:middle\color{orange}{middle}middle 题目描述 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCacheLRUCacheLRUCache 类: LRUCache(intcapacity)LRUCache(int capacity)LRUCache(intcapacity) 以 正整数 …...

【mac】在m2 mbp上通过Parallels Desktop安装ubuntu22.04
文章目录前言一、参考文章二、版本信息三、方法1:通过ubuntu官网提供的iso安装3.1 配置服务器3.2 安装图形界面四、方法2:通过Parallels Desktop提供的安装包五、 小工具5.1 调整应用栏图标大小5.2 ubuntu获取mac的剪切板5.3 调整terminal字体大小5.4 安装samba5.5 ubuntu连接m…...

C++类和对象,初见类
坚持看完,结尾有思维导图总结 这里写目录标题C语言和 C 的区别类的定义类的初认识类的内容访问限定符类的作用域类的实例化类中的 this 指针总结C语言和 C 的区别 C 的祖师爷除了在 C语言的基础上化简了一些复杂操作 更为重要的是,两个语言实现的过程是…...

Redis常用数据结构及应用场景
1.总体结构 Redis中的数据,总体上是键值对,不同数据类型指的是键值对中值的类型。 2.string类型 Redis中最基本的类型,它是key对应的一个单一值。二进制安全,不必担心由于编码等问题导致二进制数据变化。所以redis的string可以…...
C++虚继承内存布局
C菱形继承内存布局 编译器:Visual Studio 2019 关于如何查看内存布局 B class B { public:B(): _ib(10), _cb(B){cout << "B()" << endl;}B(int ib, char cb): _ib(ib), _cb(cb){cout << "B(int,char)" << endl;}vi…...

IO模型--从BIO、NIO、AIO到内核select、poll、epoll剖析
IO基本概述 IO的分类 IO以不同的维度划分,可以被分为多种类型;从工作层面划分成磁盘IO(本地IO)和网络IO; 也从工作模式上划分:BIO、NIO、AIO;从工作性质上分为阻塞式IO与非阻塞式IO;…...

Zebec完成BNB Chain以及Near链上协议部署,多链化进程加速
从去年开始,Zebec 就开始以多链的形式来拓展自身的流支付生态,一方面向更多的区块链系统拓展自身流支付协议,即从Solana上向EVM链上对协议与通证等进行迁移与拓展。目前基本完成了在BNB Chain以及Near上的合约部署,且能够在这些EV…...

Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...

从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)
设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile,新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...
GitHub 趋势日报 (2025年06月08日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...

Python 实现 Web 静态服务器(HTTP 协议)
目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1)下载安装包2)配置环境变量3)安装镜像4)node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1)使用 http-server2)详解 …...

VisualXML全新升级 | 新增数据库编辑功能
VisualXML是一个功能强大的网络总线设计工具,专注于简化汽车电子系统中复杂的网络数据设计操作。它支持多种主流总线网络格式的数据编辑(如DBC、LDF、ARXML、HEX等),并能够基于Excel表格的方式生成和转换多种数据库文件。由此&…...

Axure 下拉框联动
实现选省、选完省之后选对应省份下的市区...

Matlab实现任意伪彩色图像可视化显示
Matlab实现任意伪彩色图像可视化显示 1、灰度原始图像2、RGB彩色原始图像 在科研研究中,如何展示好看的实验结果图像非常重要!!! 1、灰度原始图像 灰度图像每个像素点只有一个数值,代表该点的亮度(或…...