含苞待放的荷花
夜景灯展
转眼间23年已经过了一半,简单回顾一下这半年的一些事情。
22年底12月份阳了,到了23年大家再也不关注新冠了
自从去年年底放开新冠防控之后,新冠在去年十二月份席卷全国,深圳万人空巷,就如同到了春节大家都回老家了一样,街头几乎见不到车辆与行人,阳了的人在家里躺着,没阳的人害怕阳了,不敢出门,但最终还是逃不过阳了。工作日公司办公室,整层楼到岗人数不足十人。
阳了很难受,发烧两天才停止发烧,之后感觉自己气管就像被刮掉一层皮一样,气管里面咳出来的全是浓痰,咳嗽异物感严重。恢复之后,还连续干咳了一个多月,才逐渐消停。发烧的这两天,还去社康“求助”过,平日人很少的社康,塞满了人,社康门口全是病人排队付钱做抗原检测,因为全国全网都买不到抗原检测试剂盒,公司发的五份抗原检测试剂盒也很快被家人用完了,我发烧第一天在社康测抗原没测出来,医生开了点治疗感冒的中成药,问到退烧药,医生说早就已经没有了;第二天再去社康测抗原,强阳性,我就直接回家了,反正医生那边连退烧药都开不出来,去看医生也没用,好在家里还有一盒退烧药。那几天也有邻居到处借退烧药,分了2颗给别人。
去年十二月份初次阳了之后,很长时间没有阳了,直到5月。大家都在传第二波新冠来了,然后5月底,我老婆二阳了,6月初,我也二阳了。第二轮感染新冠,只发烧了一晚上,吃了一颗退烧药,没多久就退烧了,第二天测出来强阳性,然后就是有点咽喉发炎,扁桃体的老毛病又犯了。乘着周末跑了一趟医院,医生开了点感冒药,然后咽喉含片,吃了两天,完全恢复了。总体来看,第二轮阳了病情比较轻,恢复也很快,遗留症状几乎没有。
困扰我多日的“牙齿”崩坏问题通过洗牙解决了
从去年开始,我的牙齿经常会因为咬到硬骨头就会崩下来一块,崩下来的东西外层黄色,内层黑色,还有臭味。我一度以为那是我牙齿的一部分骨质。再加上牙龈会出血,我以为我牙齿要坏掉了。于是挂了牙齿专科医院看了一下牙齿。医生看了之后,告诉我,我牙齿上全是结石,让我预约洗牙。最终,进洗牙室人生第一次洗了牙,那感觉真是“酸爽”,洗完之后,满嘴的血,牙龈出血严重。但是我照镜子之后才知道,原来我的牙齿可以这么白亮。遵医嘱,当天注意饮食,吃清淡的白粥,不受冷热刺激。到了第二天下午,出血的牙龈基本上已经完全恢复,留下的只有一口的白牙。为此我还嘚瑟了几天。洗掉了二三十年积累下来的牙结石之后,困扰我多日的牙龈出血问题终于解决了。
拖了四年多的驾校学习,终于拿到驾驶证了
另一件值得一提的事情就是,我在6月份终于拿到了驾驶证。困扰我多年的科一练题在二月份一个月的时间里终于解决了,其实找到科一练题的方法之后,科一并没有那么难。如果没有找到方法,去刷题库1000题或者精简题库500题,终究是反复折腾记了忘、忘了记。科二练习了5天,然后去考试,一把过。科三练习了3天,去考试,两把都是挂在终点。科三第二次考试,第一把也是挂在终点,因为终点掉灯之后忘记补灯了,第二把设备故障导致中断,重新补考第二把,终于完美通过了。科三考完回家第二天就阳了。最终,等了一个星期,完全恢复之后,练了差不多一个星期的科四题目,在周六去考了科四,终于拿到了驾驶证,成为了一个合法的驾驶员。
时间过得真快,很多事情已经是物是人非了。估计下半年还会有更多值得回顾的事情。
我们经常可以在网页上看到QQ快捷登录,只需要点击一下QQ图像,不需要账号,不需要密码,不需要扫码,就可以直接登录了。
下面简单介绍一下这其中的原理,我们可以在浏览器请求中找到这么一个请求,如下图
可以发现它请求的是127.0.0.1:4301
,很明显这个服务是一个本地服务,它就运行在你电脑上。其实它就是你QQ客户端上运行的一个服务,可以看到,请求了这个服务的接口之后,服务响应中设置了相关的cookies,后续,QQ系的网站就可以根据这个cookies进行鉴权了。
所以说,要实现类似于QQ的这种一键登录能力,你需要有个本地客户端,客户端要提供一个http接口,你的网站会请求这个本地客户端,已鉴权的本地客户端收到这个网页上的请求之后,就会将当前登录账号的鉴权cookies植入到浏览器了,这样就成功的实现了一键快捷登录鉴权。这也就是所谓的桌面客户端为浏览器植入cookies的技术。
最近用到了libc这个包,调用其中的statfs
函数,用于查询指定路径挂载的磁盘占用。可以看到
pub fn statfs(path: *const ::c_char, buf: *mut statfs) -> ::c_int;
statfs
第二个参数是一个C语言结构体statfs
,其定义如下
pub struct statfs {
pub f_type: ::__fsword_t,
pub f_bsize: ::__fsword_t,
pub f_blocks: ::fsblkcnt_t,
pub f_bfree: ::fsblkcnt_t,
pub f_bavail: ::fsblkcnt_t,
pub f_files: ::fsfilcnt_t,
pub f_ffree: ::fsfilcnt_t,
pub f_fsid: ::fsid_t,
pub f_namelen: ::__fsword_t,
pub f_frsize: ::__fsword_t,
f_spare: [::__fsword_t; 5],
}
如果用常规的rust初始化方法,必须填充结构体的各字段
let x = statfs{
...,
f_spare: ...,
}
可以看到其中f_spare
字段是一个数组,属于复杂类型,用rust的初始化方式,需要一个个字段的填充,而且需要填充为初始值0,将会非常复杂。其实有一种简单方法,使用std::mem::zeroed
函数即可。举例如下
fn statfs(&mut self, mount_path: String) -> String {
let x = CString::new(mount_path.as_bytes()).expect("covert cstring failed");
let mut info = String::from("");
let mut statfs_buf = unsafe { std::mem::zeroed() };
let ret = unsafe { libc::statfs(x.as_ptr(), &mut statfs_buf) };
if ret == 0 {
info = format!(
"{} {} {} {}",
statfs_buf.f_bsize, statfs_buf.f_blocks, statfs_buf.f_bfree, statfs_buf.f_bavail
);
}
return info;
}
可以看到std::mem::zeroed()
方法,直接能够初始化一段0值的内存空间,这段空间具体大小,直接由下一行libc::statfs(...)
调用的第二个参数类型决定,由这一行直接能推导出该申请多大的0空间。
最近再次在rust中尝试用tokio::spawn实现类似go语言中goroutine的用法,但是报错了。
#[tokio::main]
async fn main() {
let cfg = cfg::get_config();
let client_id = cfg::get_client_id();
let ws_addr = cfg.agent.as_ref().unwrap().remote_ws_addr.as_ref().unwrap();
let mut wsc = ws::WebSocketClient::new(ws_addr.clone(), client_id);
tokio::spawn(wsc.start());
let monitor_addr = cfg.agent.as_ref().unwrap().remote_ws_addr.as_ref().unwrap();
let mut monitor_client = monitor::Monitor::new(monitor_addr.to_string());
monitor_client.start().await;
}
报错位置在这一行 tokio::spawn(wsc.start());
,报错内容如下
error: future cannot be sent between threads safely
--> src/main.rs:19:18
|
19 | tokio::spawn(wsc.start());
| ^^^^^^^^^^^ future returned by `start` is not `Send`
|
= help: the trait `Sync` is not implemented for `std::sync::mpsc::Receiver<Message>`
note: future is not `Send` as this value is used across an await
--> src/ws.rs:117:47
|
115 | if let Ok(msg) = rx.recv() {
| -- has type `&std::sync::mpsc::Receiver<Message>` which is not `Send`
116 | println!("Socket Send : {:?}", msg);
117 | let rst = writer.send(msg).await;
| ^^^^^^ await occurs here, with `rx` maybe used later
...
126 | }
| - `rx` is later dropped here
help: consider moving this into a `let` binding to create a shorter lived borrow
可以看到原因是,我在WebSocketClient
中用到了MutexGuard
,而MutexGuard
不支持被tokio::spawn
调用。解释参考这篇文档。实际上我是在另一个结构体Pty
中使用了Arc<Mutex<Receiver<Message>>>
类型,然后WebSocketClient
中又实用了Pty
结构体,所以在调用tokio::spawn
时报错。
如何解决?别用标准库的Mutex了,用tokio的。而且,我同样发现了Sender
,Receiver
都在报类似的错误the trait std::marker::Send is not implemented for ...
。
use std::sync::mpsc::{channel, Receiver, Sender}
// 改为
use tokio::sync::mpsc::channel;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::Mutex;
channel
,Receiver
,Sender
全部改为tokio版本的,就可以兼容tokio了。
在rust中序列化反序列化JSON我们常用的是serde
这个包,但是json中有一种情况就是存在不确定类型的字段,或者说这个字段可能是多种类型;在golang中,我们用interface{}
类型来表达,在java中,我们用Object
表达,那么在rust应该如何表达呢。直接说结果,用enum来组合类型
比如我定义了一种JSON数据结构,如下
// 第一种
{
"msg_type": "resize",
"content": {
"width": 12,
"height": 3
}
}
// 第二种
{
"msg_type": "ping",
"content": {
"ts": 123
}
}
很明显,两个数据中,content字段表现的类型不一样,在msg_type
为resize
时content
字段是一种结构体,msg_type
为ping
时content
字段是另一种结构体。我们定义枚举和结构体如下
#[derive(Clone, Debug, Deserialize)]
struct ControlMessage {
msg_type: String,
#[serde(default)]
content: ControlContent,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(untagged)]
enum ControlContent {
Empty,
Resize(ResizeControl),
Time(TimeControl),
}
impl Default for ControlContent {
fn default() -> Self {
Self::Empty
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
struct ResizeControl {
width: u32,
height: u32,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
struct TimeControl {
ts: u32,
}
可以看到我们定义了两种类型ResizeControl
和TimeControl
,需要注意以下几点:
Deserialize
注解Empty
,Resize
,Time
这3个都属于枚举的字段名,圆括号()里定义类型,没有定义的表示空untagged
注解Default::default
方法,否则无法被Deserialize
注解ResizeControl
,TimeControl
,以及枚举ControlContent
都需要加上PartialEq
注解,否则无法在枚举圆括号()中引用调用反序列化
#[test]
pub fn test_field_deserialize() -> serde_json::Result<()> {
let a = r#"{"msg_type": "resize", "content": {"width": 12, "height": 3}}"#;
let v: ControlMessage = serde_json::from_str(a)?;
println!("v: {:?}", v);
let b = r#"{"msg_type": "resize", "content": {"ts": 123}}"#;
let v: ControlMessage = serde_json::from_str(b)?;
println!("v: {:?}", v);
Ok(())
}
结果调用成功
running 1 test
v: ControlMessage { msg_type: "resize", content: Resize(ResizeControl { width: 12, height: 3 }) }
v: ControlMessage { msg_type: "resize", content: Time(TimeControl { ts: 123 }) }
test control::test_field_deserialize ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
在rust中,我们在一开始学习rust时,就被告诉,在函数结尾可以不用写return,只需要结尾的那个语句别写分号(;),那么结尾的这个语句计算的值将会作为这个函数的返回值返回。
#[test]
fn test_return_2() {
println!("none semi: {}", none_semicolon());
}
fn none_semicolon() -> String {
String::from("hello world")
}
正常输出
running 1 test
none semi: hello world
test test_return_2 ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
这时,我形成一个印象,“结尾语句不加分号,可以当return用”。还有如下的例子
fn test_return_1() -> bool {
let x = Option::Some(true);
let y = match x {
Some(val) => {
println!("{:?}", val);
val
}
None => false,
};
println!("will finish function, y: {:?}", y);
false
}
我可以看到y
变量是bool
类型,这时候我以为val这句没有加分号,所以它将值返回给语句块,从而赋值给y,但是,这时候,我改为下面这样的
#[test]
fn test_return_2() {
println!("none semi: {}", test_return_1());
}
fn test_return_1() -> bool {
let x = Option::Some(true);
let y = match x {
Some(val) => {
println!("{:?}", val);
return val;
}
None => false,
};
println!("will finish function, y: {:?}", y);
false
}
我发现,return 处就结束了函数并返回。后续的println并没有执行。
running 1 test
true
none semi: true
test test_return_2 ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
总结,位于语句块或者函数体结尾无分号结尾的语句,并不等同于return
,它处于语句块结尾时,代表将它的值赋值给语句块所代表的变量,处于函数体结尾时,代表将它的值赋值给函数体所代表的变量(函数返回变量);而return
这个函数终止返回的指令,从来都跟无分号结尾的语句无关,而是函数体的反花括号 (}) 的出现所带来的行为。
最近发现一个问题,rust的线程安全机制导致无法实现socket读写分离到两个不同的线程。
先说一下程序的背景,程序是将本地终端pty(cli)拉起,并且将pty的输入输出通过channel对接,并将cli输出的数据经过channel写入到服务端socket,将从服务端socket收取到的数据经另一个channel写入到cli的输入。从而实现远程连接pty。
按照rust的写法,读线程中,在读socket之前需要先锁socket,然后读取,再释放锁;同样,在写线程中,也需要先锁socket,然后写入,再释放锁。这样一来代码应该如下:
连接与初始化代码如下
let (ws_stream, response) =
connect(Url::parse("wss://ws.postman-echo.com/raw").unwrap()).expect("msg");
println!("Connected to the server");
println!("Response HTTP code: {}", response.status());
println!("Response contains the following headers:");
for (ref header, _value) in response.headers() {
println!("* {}", header);
}
let socket = Arc::new(Mutex::new(ws_stream));
// init cli
self.pty_start();
let mut me = self.clone();
let skt = socket.clone();
thread::spawn(move || {
me.watch_socket_read_in(skt);
});
println!("---> watch_socket_read_in");
self.watch_socket_write_out(socket.clone());
println!("---> watch_socket_write_out");
读取方法在一个新起的线程中watch_socket_read_in
,如下
fn watch_socket_read_in(&mut self, socket: Arc<Mutex<WebSocket<MaybeTlsStream<TcpStream>>>>) {
loop {
let mut skt = socket.lock().unwrap();
let msg = skt.read_message().unwrap();
println!("Socket Received: {:?}", msg);
drop(skt);
self.tx_in
.send(msg.clone())
.expect("send msg into in channel failed");
println!("send pipe in succ: {:?}", msg);
}
}
可以看到,不停的从socket读取数据,读取前锁,读取后drop锁。
写入方法在初始化代码所在的主线程中watch_socket_write_out
,如下
fn watch_socket_write_out(&mut self, socket: Arc<Mutex<WebSocket<MaybeTlsStream<TcpStream>>>>) {
let rx = self.rx_out.lock().expect("lock rx out failed");
for msg in rx.iter() {
println!("msg from cli -> {:?}", msg);
let mut skt = socket.lock().unwrap();
println!("Socket Send : {:?}", msg);
skt.write_message(msg).unwrap();
drop(skt);
}
println!("out of socket write out block....")
}
可是,运行的结果却出乎我的意料,运行结果现象是这样的,先是只能够从socket读取到服务端的PING数据,而cli发出的数据经过channel读取出来之后,锁socket,准备发送,但是发现锁socket卡主死锁了,导致无法经socket发送,然后就卡了很久;但是过了一段时间,写socket获取的锁成功了,发了一大堆的数据,然后又轮到读socket卡主,稍后随机的时间后,读socket锁成功,又只能读到PING,如此反复。这种状态的读写,完全不能用,根本实现不了cli与服务端的实时通讯。
分析了一下,应该是socket网络读写是网络通讯,因此读写的锁定socket时长是不确定的且相对耗时算是比较长的,所以导致无法预料是读获取到锁还是写获取到锁,而且这种锁强行将读写串行化了,完全不符合并发读写的要求了。
几经查找,于是采用tokio-tungstenite
这个crate替换了tungstenite
,因为它可以将WebSocketStream
通过split
方法分隔为reader
和writer
,这样一来,读与写就分离开了,在不同的线程中无需对socket
加锁。
let (ws_stream, response) =
connect_async(Url::parse("wss://ws.postman-echo.com/raw").unwrap())
.await
.expect("msg");
let (ws_writer, ws_reader) = ws_stream.split();
这样一来,读socket
用ws_reader
,写socket
用ws_writer
。
// socket read
let me = self.clone();
tokio::spawn(async move {
let mut incoming = ws_reader.map(Result::unwrap);
while let Some(msg) = incoming.next().await {
if msg.is_text() {
println!("Socket Received: {:?}", msg);
me.tx_in.send(msg).expect("send msg into in channel failed");
}
}
});
// socket write
self.watch_socket_write_out(ws_writer).await;
async fn watch_socket_write_out(
&mut self,
mut writer: SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
) {
let rx = self.rx_out.lock().expect("lock rx out failed");
for msg in rx.iter() {
println!("Socket Send : {:?}", msg.to_text().unwrap());
writer.send(msg).await.unwrap();
}
println!("out of socket write out block....")
}
可以看到,新线程中读socket
,主线程中写socket
;ws_reader
经map
方法后,可以在死循环中阻塞调用next()
不断的读取socket
中的信息。写socket
则从channel
中读取到数据,按常规的方法send
即可。
接入tokio-tungstenite
解决了这个问题,不过它是基于tokio
的,tokio
是一个协程库,有自己的运行时,用了tokio
的程序起协程后,程序会自动启动若干个线程,类比goroutine,它也是有初始的资源消耗的,比如这个程序只需要4个线程,但是使用了tokio的程序,会有10个线程(如上图),内存占用会明显增多。
在rust中有个常用个方法clone
,按字面意思就是克隆。这个函数的作用是对对象进行深度拷贝,生成的新对象与原对象相互独立。
很多常用的类型或者容器类型都支持clone
,例如rust中的HashMap
也支持clone
,我们用一段代码实验一下。
#[test]
fn test_hash_map_clone() {
let xx: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
let mut mp = xx.lock().unwrap();
mp.insert("hi".to_string(), "hello".to_string());
println!("origin: {:?}", mp);
let mut cp = mp.clone();
cp.insert("k".to_string(), "v".to_string());
println!("origin: {:?}", mp);
println!("cp : {:?}", cp);
}
输出
running 1 test
origin: {"hi": "hello"}
origin: {"hi": "hello"}
cp : {"hi": "hello", "k": "v"}
test test_hash_map_clone ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
上面的测试代码运行结果表示,修改克隆后的对象cp
,源对象mp不会发生变化。
那么我们自己定义的类型如何才能支持clone
呢?使用#[derive(Clone)]
这个指令修饰自定义类型,就会自动支持clone
,但是要注意,如果自定义类型结构体里,如果有字段类型不支持clone
,将无法通过#[derive(Clone)]
指令快速支持clone
。
自定义类型clone
测试如下
#[derive(Debug, Clone)]
struct User {
name: String,
age: i32,
}
#[test]
fn test_struct_clone() {
let mut u1 = User {
name: "rex".to_string(),
age: 1,
};
println!("origin: {:?}", u1);
let mut ucp = u1.clone();
ucp.name = "agnes".to_string();
ucp.age = 2;
println!("origin: {:?}", u1);
println!("cp : {:?}", ucp);
u1.age = 3;
println!("origin: {:?}", u1);
println!("cp : {:?}", ucp);
}
运行结果
running 1 test
origin: User { name: "rex", age: 1 }
origin: User { name: "rex", age: 1 }
cp : User { name: "agnes", age: 2 }
origin: User { name: "rex", age: 3 }
cp : User { name: "agnes", age: 2 }
test test_struct_clone ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
同样可以看到,修改clone
后的对象,源对象不变,修改源对象,clone
后的对象也不变。
最近将一个golang程序由原本的单线程改为双线程处理日志解析,在生产环境节点上运行发现出现了一个空指针异常,异常信息如下:
[E] 2023/02/27 11:53:25 panic.go:838: parse error:runtime error: invalid memory address or nil pointer dereference,line:Feb 27 11:53:25 xxxxx.site nginx: [xxxxxxxxxxxxxxxxxxx] [27/Feb/2023:11:53:25 +0800] [https] [120.232.31.196:443] [xxxxx.map.xx.com] [39.144.41.41:37647] [200] [30.171.153.132:10000] [200] [3339597] [POST /tr?mllc HTTP/1.1] [621] [152] [xxxxxxx.map.xx.com] [Dalvik/2.1.0 (Linux; U; Android 10; HMA-AL00 Build/HUAWEIHMA-AL00)] [-] [0.008] [0.008] [0.004] [0.008] [46795] [311532943564] [1][1:46:0:0:0:0:0] [ECDHE-RSA-AES128-GCM-SHA256] [TLSv1.2] [r][-] [-] [n] [2358837] [-1] [1677470005.398|5|51|126|-1|-1|126|126|130|-1|-1|130|134|134|134|134#200|200|8|152|120.232.31.196|30.171.153.132:10000|0|0] [POST] [/tr] [mllc] [HTTP/1.1] [39.144.41.41] [-] [-] [-] [-] [-] [-] [-] [-] [-] [-] [169.254.213.29:50937] [0]
修改程序,打印出异常栈
发现异常发生在代码133行,代码如下
很明显这里不太可能出现空指针,除非运行到这一行的时候sl
对象被置为nil
,但是我很确定这里不存在其他线程共享sl
的情况,也就是不可能被其他线程置为nil
,何况,我这里没有任何操作将sl置为nil
,百思不得其解。最后发现key1
,key2
这两个变量是全局变量,全局变量在多线程环境下会存在数据竞争问题。原本定义如下
var (
uriSep *regexp.Regexp = nil
key1 = ""
key2 = ""
)
可以看到key1
,key2
被定义为全局变量,忘记修改了。修改之后,神奇的发现,空指针异常已经不再存在了。
我在发现这个问题之前,在本地开发服务器上尝试重现,但是一直未能重现出来,估计是我本地qps不够高,所以难以复现,生产环境qps是3w到4w左右,这个空指针异常呈现无规律的隔几秒钟出现一次。
golang中,双协程(绑定在双M和双cpu上)中同时读写一个共享变量导致空指针异常,这种情况我还是第一次遇见,以前遇到这种双协程读写一个共享变量的情况都是数据错乱,并没有空指针。据说这种线程安全、数据竞争导致的空指针异常在C++中也是常见的情况。所以,我在想,这里会不会是因为我将2个协程绑核了,所以在双线程绑双核的情况下更容易复现呢。