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

Rust入门(十六):手写web服务器和线程池

这一章将实现一个手写的 web server 和 多线程的服务器,用到之前学到的所有特性

简单的web server

作为一个 web 服务器,我们首先要能接收到请求,目前市面上的 web 服务大多数都是基于 HTTP 和 HTTPS 协议的,而他们有是基于 TCP 协议传输的 ,所以我们希望我们的服务器可以监听 TCP 连接,rust 的库函数中提供了 TcpListener 这样的函数来监听 TCP 连接,我们新建一个项目,然后在 src/main.rs 中编写监听功能。

TcpListenerincoming 方法返回一个迭代器,它提供了一系列的流,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 请求,作为一个服务器,对于一个请求,我们需要做出我们的回复,提供给它需要的资源,这里我们就返回先尝试返回一个简单的响应:

因为 streamwrite_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 方法传递进来的方法放入我们的线程中,我们使用 信道 —— 作为沟通工具,方案如下:

  1. ThreadPool 会创建一个信道并充当发送者。
  2. 每个 Worker 将会充当接收者。
  3. 新建一个 Job 结构体来存放用于向信道中发送的闭包。
  4. execute 方法会在发送者发出期望执行的任务。
  5. 在线程中,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 成员不做处理。换句话说,正在运行的 Workerthread 将是 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,则主线程会永远阻塞在等待第一个线程结束上。

所以我们需要修改 ThreadPooldrop 实现并修改 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 循环在这种情况下优雅地退出,这意味着当 ThreadPooldrop 实现调用 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 和 多线程的服务器&#xff0c;用到之前学到的所有特性 简单的web server 作为一个 web 服务器&#xff0c;我们首先要能接收到请求&#xff0c;目前市面上的 web 服务大多数都是基于 HTTP 和 HTTPS 协议的&#xff0c;而他们有是基于 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.代码展示 整理不易&#xff0c;欢迎一键三连&#xff01;&#xff01;&#xff01; 1. YOLO简介 YOLO&#xff08;You Only Look Once&#xff09;是一种流行的目标检测和图像分割模型&#xff0c;由华盛顿大学的 Joseph Redmon 和 Al…...

基于linux内核的驱动开发

1 字符设备驱动框架 1.1字符设备 定义&#xff1a;只能以一个字节一个字节的方式读写的设备&#xff0c;不能随机的读取设备中中的某一段数据&#xff0c;读取数据需要按照先后顺序。&#xff08;字符设备是面向字节流的&#xff09; 常见的字…...

找不到工作的测试员一大把,大厂却招不到优秀软件测试员?高薪难寻测试工程师。

测试工程师招了快一个月了&#xff0c;实在招不到合适的&#xff0c;已经在被解雇的边缘了。。。” 初级测试工程师非常多&#xff0c;但真正掌握测试思维、能力强的优秀测试太少了&#xff01; 据我所知&#xff0c; 当下的测试人员不少状态都是这样的&#xff1a; 在工作中…...

buuctf Basic

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

赛狐ERP|亚马逊产品缺货怎么办?该如何补救?

由于物流时效的延长&#xff0c;运输成本的增加&#xff0c;亚马逊的仓储限制等各种原因&#xff0c;断货问题很常成为亚马逊卖家的普遍困扰。那么亚马逊产品缺货应该怎么办&#xff01;1、提高产品价格&#xff1a;除了卖自己的Listing此外&#xff0c;提高产品价格也是一种保…...

《Elasticsearch源码解读与优化实战》张超-读书笔记

写在前面 好久没更新博客了&#xff0c;应届狗没办法啊╮(╯▽╰)╭为了秋招搞了小半年&#xff0c;从去年5月到现在搞了两段实习&#xff08;京东、游戏公司&#xff09;&#xff0c;最终年前拿到一家还不错的offer&#xff0c;现在已经入职实习了&#xff0c;不出意外的话以…...

编码踩坑——运行时报错java.lang.NoSuchMethodError / 同名类加载问题 / 双亲委派【建议收藏】

本篇介绍一个实际遇到的排查异常的case&#xff0c;涉及的知识点包括&#xff1a;类加载机制、jar包中的类加载顺序、JVM双亲委派模型、破坏双亲委派模型及自定义类加载器的代码示例&#xff1b;问题背景业务版本&#xff0c;旧功能升级&#xff0c;原先引用的一个二方包中的du…...

软件测试选Python还是Java?

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

“2023数据安全智能化中国行”活动,开幕即高能

工信部等16部门近日发布的《关于促进数据安全产业发展的指导意见》提出&#xff0c;到2025年&#xff0c;数据安全产业基础能力和综合实力明显增强&#xff0c;数据安全产业规模超过1500亿元&#xff0c;年复合增长率超过30%。到2035年&#xff0c;数据安全产业进入繁荣成熟期。…...

机器人操作规划——Deep Visual Foresight for Planning Robot Motion(2017 ICRA)

1 简介 model-based RL方法&#xff0c;预测Action对图像的变化&#xff0c;以push任务进行研究。 采用完全自监督的学习方式&#xff0c;不需要相机标定、3D模型、深度图像和物理仿真。 2 数据集 采用几百个物体、10个7dof机械臂采集了包括5万个push attempts的数据集。 每…...

go 连接redis集群

最近用redis shake做redis数据迁移&#xff0c;由于redis提供的客户端没有用于查看集群的工具&#xff0c;且我部署的redis集群是基于k8s来构建的&#xff0c;没有使用ingress做转发&#xff0c;所以只能在k8s内部访问集群&#xff0c;于是我先用gogin框架编写了访问redis集群的…...

LeetCode 146. LRU 缓存

原题链接 难度&#xff1a;middle\color{orange}{middle}middle 题目描述 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCacheLRUCacheLRUCache 类&#xff1a; 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++类和对象,初见类

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

Redis常用数据结构及应用场景

1.总体结构 Redis中的数据&#xff0c;总体上是键值对&#xff0c;不同数据类型指的是键值对中值的类型。 2.string类型 Redis中最基本的类型&#xff0c;它是key对应的一个单一值。二进制安全&#xff0c;不必担心由于编码等问题导致二进制数据变化。所以redis的string可以…...

C++虚继承内存布局

C菱形继承内存布局 编译器&#xff1a;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以不同的维度划分&#xff0c;可以被分为多种类型&#xff1b;从工作层面划分成磁盘IO&#xff08;本地IO&#xff09;和网络IO&#xff1b; 也从工作模式上划分&#xff1a;BIO、NIO、AIO&#xff1b;从工作性质上分为阻塞式IO与非阻塞式IO&#xff1b…...

Zebec完成BNB Chain以及Near链上协议部署,多链化进程加速

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

wpscan常见的使用方法

目录 简单介绍 暴力破解 信息收集 指定用户爆破 命令集合 简单介绍 Wordpress是一个以PHP和MySQL为平台的免费自由开源的博客软件和内容管理系统。 WPScan是Kali Linux默认自带的一款漏洞扫描工具&#xff0c;它采用Ruby编写&#xff0c;能够扫描WordPress网站中的多种安…...

Tree 底层源码实现(二叉树、递归、迭代)

树&#xff08;Tree&#xff09;是一种非线性数据结构&#xff0c;由一组节点和它们之间的边组成。在树中&#xff0c;每个节点都有零个或多个子节点&#xff0c;除了根节点外&#xff0c;每个节点都有且仅有一个父节点。树可以被用于许多应用程序&#xff0c;如文件系统、XML文…...

家政服务小程序实战教程13-接入客服

小程序在微信里使用&#xff0c;以其无需安装随用随走为特点。但是有个问题是&#xff0c;如果提供商品或者服务的&#xff0c;用户如果有问题往往希望平台的运营方给出专业的解答。为了满足这类需求&#xff0c;就需要我们提供客服接入的功能&#xff0c;用户可以点击客服图标…...

大白话高并发(三)

背景 高并发得第三篇&#xff0c;讲一讲压测吧&#xff0c;因为我的目的是模拟100万人同时来秒杀。 是不是真的要找100万个人 没必要 &#xff0c;你就算100万人掐着表在同一毫秒内把请求请求某一台机器&#xff0c;服务器也不可能在同一时间处理那么多请求&#xff0c;因为…...

vue全家桶(四)前端工程化

vue全家桶&#xff08;四&#xff09;前端工程化1.模块化的相关规范1.1模块化概述1.2模块化的分类A.浏览器端的模块化B.服务器端的模块化C.ES6模块化1.2.1 Node.js中通过bable体验ES6模块化1.2.2 ES6模块化的基本语法1.2.2.1 默认导出与默认导入1.2.2.2 按需导出与按需导入1.2.…...

超螺旋滑模控制(STA)

超螺旋滑模控制(Super Twisting Algorithm, STA) 超螺旋滑模控制又称超扭滑模控制&#xff0c;可以说是二阶系统中最好用的滑模控制方法。 系统模型 对于二阶系统可以建立具有标准柯西形式的微分方程组 {x˙1x2x˙2fg⋅u\begin{cases} \dot x_1 x_2 \\ \dot x_2 f g \cdo…...

NX二次开发编译时dll自动数字签名及拷贝

前言 在UG5.0开始&#xff0c;所有基于UG二次开发的DLL都要“签名”后才能被客户端上正版的NX调用。 一、基于C# 开发签名 1、添加资源文件 &#xff08;1&#xff09;项目类库上右键–>属性–>资源–>添加资源右边小三角–>添加现有文件–>切换到UG安装目录下…...

教你如何搭建人事OA-薪资管理系统,demo可分享

1、简介1.1、案例简介本文将介绍&#xff0c;如何搭建人事OA-薪资管理。1.2、应用场景根据设置薪资基础及考勤和绩效的数据计算得到各个员工工资详情。2、设置方法2.1、表单搭建1&#xff09;新建表单【工资表】&#xff0c;字段设置如下&#xff1b;名称类型名称类型人员资料分…...

ChIP-seq 分析:Mapped 数据可视化(4)

1. Mapped reads 现在我们有了 BAM 文件的索引&#xff0c;我们可以使用 idxstatsBam() 函数检索和绘制映射读取的数量。 mappedReads <- idxstatsBam("SR_Myc_Mel_rep1.bam")TotalMapped <- sum(mappedReads[, "mapped"])ggplot(mappedReads, aes(x…...

Jenkins 基于Kubernetes 弹性构建池

流程&#xff1a;创建Jenkins Agent&#xff1b;获取Jenkins Agent的参数&#xff1b;渲染yaml模板&#xff1b;调用K8s API在固定的NS中创建一个Pod&#xff1b;运行Jenkins pipeline到agent&#xff1b;创建Agentimport hudson.model.Node.Mode import hudson.slaves.* impor…...

网站后台管理系统开发/常德今日头条新闻

回到目录 微软的web api是在vs2012上的mvc4项目绑定发行的&#xff0c;它提出的web api是完全基于RESTful标准的&#xff0c;完全不同于之前的&#xff08;同是SOAP协议的&#xff09;wcf和webService&#xff0c;它是简单&#xff0c;代码可读性强的&#xff0c;上手快的&…...

做外贸在那些网站找客户/福州短视频seo

一种特殊的索引&#xff0c;它会把某个数据表的某个数据列出现过的所有单词生成一份清单。 alter table tablename add fulltext(column1,column2) 只能在MyISAM数据表中创建 少于3个字符的单词不会被包含在全文索引里&#xff0c;可以通过修改my.cnf修改选项 ft_min_word_le…...

wap网站下载/日照高端网站建设

很多同学想知道计算机二级理论题如何复习&#xff0c;下面是小编整理的相关内容&#xff0c;希望对大家有所帮助&#xff01;计算机二级理论题如何复习做理论题在电脑上实验。如果是程序题&#xff0c;把程序输进电脑进行运行&#xff0c;看得出什么结果。若是命令和函数&#…...

泉州做网站优化哪家好/淮北seo

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼debian系统目前支持Usb camera是没有问题&#xff0c;走UVC功能接口。那么mipi 接口camera和并口接口的camera&#xff0c;在Debian系统怎么设置呢&#xff0c;其实原理一样&#xff0c;也走uvc接口封装函数.下面深圳视壮给大家简单…...

做旅游网站课程设计报告/5g站长工具查询

笔者为了学习PHP&#xff0c;WIN7系统上装了XAMPP。 默认情况下&#xff0c;mysql的root密码为空。在命令行下&#xff0c;通过mysql -u root即可进入。笔者在mysql数据库下user表中更改了root密码&#xff0c;退出后&#xff0c;发现root用户使用新设置密码登录不了。root用户…...

linux网站建设论文/爱论坛

介绍&#xff1a; 这是一个用vuejs2.0和element搭建的后台管理界面。 相关技术&#xff1a; vuejs2.0&#xff1a;渐进式JavaScript框架&#xff0c;易用、灵活、高效&#xff0c;似乎任何规模的应用都适用。 element&#xff1a;基于vuejs2.0的ui组件库。 vue-router&#xff…...