之前基于golang rod
实现了类似的功能,刚好这次有新的需求,就想用rust实现一个类似的功能。
代码在 https://github.com/shenjinti/browserlify
主要的功能:
- 提供
headless chrome
的API服务,不需要本机启动一个headless chrome
, 支持puppeteer
直接connect
一个远程的headdless chrome instance
.
const browser = await puppeteer.connect({
browserWSEndpoint: `ws://localhost:9000`,
});
const page = await browser.newPage();
await page.goto('https://browserlify.com');
await page.screenshot({ path: 'browserlify.png' });
- 内置生成
pdf/screenshot/innerText/innerHTML
的快捷功能,可以直接生成pdf和截图
curl "http://localhost:9000/screenshot?url=http://browserlify.com" > browserlify.png
3.提供远程浏览器的功能,可以web上开启不同的浏览器,可以测试或者开启web3的各种浏览器节点
过程中遇到一些问题,感谢Rust编程语言群的解惑, 意识到async-std
的更新不活跃,后来选择了tokio
心得:
- Drop功能和C++的析构功能很像,虽然没有
defer
方便,还是简单可靠 log
功能比较强大- vscode对tokio的支持还不是那么友好,断点调试的时候,遇到await会出现进入poll的代码区域,导致调试比较困难
- axum的文档还是比较简单,但是基本的功能还是比较完善的,比如
extract
的功能,可以直接从request中提取参数,还有response
的功能,可以直接返回json tokio::select!
这个功能比go的select
功能强大,可以并行监控多个task,将复杂的并发逻辑用串行的思路解决futures::StreamExt
的代码不是特别好理解,还是习惯for循环的方式
Axum vs Gin
gin
作为golang的最流行的web框架,有丰富的middleware扩展能力,并基于http
的Request
与Response
机制,可以方便的处理Form
与Response
Axum
也有类似的middleware能力,但是没有gin
那么丰富,但是Axum
的extract
功能,可以直接从Request
中提取参数,而不需要自己去解析Form
,这个功能还是比较方便的。
两者的route
功能比较接近,从gin
迁移比较方便.
Axum
的错误处理并不是特别友好,如果handler是Result类型,并不能方便的将Err变成Status + Json的方式,这点gin
做的比较好,可以直接返回c.JSON(500, gin.H{"error": err.Error()})
如何实现PDF和Screenshot
browserlify
设计的第一个功能就是为了渲染pdf和网页图片,通过创建一个browser
实例,然后创建一个page
实例,然后通过page.goto
加载网页,然后通过page.pdf
和page.screenshot
生成pdf和图片。
这个过程中,需要实现打开一个页面,并且等待页面加载完成,chromiumoxide
的API中无法指定一个操作的超时时间,所以通过tokio::select
的方式实现超时等待的功能:
let page = browser
.new_page(params.url.as_str())
.await
.map_err(|e| e.to_string())?;
if let Some(wait_load) = params.wait_load {
select! {
_ = time::sleep(time::Duration::from_secs(wait_load)) => {}
_ = page.wait_for_navigation() => {}
};
} else {
page.wait_for_navigation().await.ok();
}
如何实现puppeteer连接一个浏览器实例
browserlify
的第二个功能是将一个headless 的chrome实例暴露给外部,这样可以通过puppeteer
连接到这个实例,然后通过puppeteer
的API来操作这个实例,这样可以实现一个browser as a service
的功能。
所以实现了一个websocket的桥接功能:
let (upstream, _) = tokio_tungstenite::connect_async(&session.ws_url)
.await
.map_err(|e| e.to_string())?;
let (mut server_ws_tx, mut server_ws_rx) = upstream.split();
// 1. 连接后端的chrome实例
//
let r = ws.on_upgrade(|client_stream| async move {
let (mut client_ws_tx, mut client_ws_rx) = client_stream.split();
// 2. 获取客户端(比如puppeteer)的websocket
let server_to_client = async {
while let Some(Ok(msg)) = server_ws_rx.next().await {
if let Some(msg) = from_ts_message(msg) {
if let Err(e) = client_ws_tx.send(msg).await {
log::error!("client_ws_tx.send id: {} error: {}", id, e);
break;
}
}
}
};
let client_to_server = async {
while let Some(Ok(msg)) = client_ws_rx.next().await {
if let Err(e) = server_ws_tx.send(to_ts_message(msg)).await {
log::error!("server_ws_tx.send id: {} error: {}", id, e);
break;
}
}
};
// 3. 将客户端的websocket和后端的websocket进行桥接
select! {
_ = server_to_client => {}
_ = client_to_server => {}
_ = async {
while let Some(_) = handler.next().await {}
} => { }
_ = shutdown_rx => {
log::info!("shutdown_rx shutdown id: {}", id);
}
}
browser.kill().await;
});
在golang实现的版本,我们用了一个比较hack的方案实现桥接,看起来还是比rust精简许多,缺点就是无法知晓每个message的内容
req := c.Request.Clone(context.TODO())
req.RequestURI = url
b, _ := httputil.DumpRequest(req, false)
cdpConn.Write(b)
errChan := make(chan error, 2)
copyConn := func(dst, src net.Conn) {
_, err := io.Copy(dst, src)
errChan <- err
}
go copyConn(conn, cdpConn) // response
go copyConn(cdpConn, conn) // request
发布相关
我们通过Dockerfile.cn
实现了Dockerfile的多阶段构建,并且支持了rsproxy.cn
的加速,大家有兴趣可以看一下Dockerfile
默认的镜像选择了Debian:bookwarm
, 程序运行比较稳定
总结
通过这次迁移,我们得到的感受就是rust的异常处理和异步非常适合严谨的后台服务开发,但是配套的库没有golang成熟和方便,比如chromiumoxide想动态修改设备的Viewport就没有实现。
另外rust的编译速度还是比较慢,相比go多了一倍以上的时间,大型项目的CI资源消耗更多
Ext Link: https://github.com/shenjinti/browserlify
评论区
写评论还没有评论