< 返回版块

shenjinti 发表于 2024-01-23 14:47

Tags:headless-chrome,axum,chrome

之前基于golang rod实现了类似的功能,刚好这次有新的需求,就想用rust实现一个类似的功能。

代码在 https://github.com/shenjinti/browserlify

主要的功能:

  1. 提供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' });
  1. 内置生成pdf/screenshot/innerText/innerHTML的快捷功能,可以直接生成pdf和截图
curl "http://localhost:9000/screenshot?url=http://browserlify.com" > browserlify.png

3.提供远程浏览器的功能,可以web上开启不同的浏览器,可以测试或者开启web3的各种浏览器节点

过程中遇到一些问题,感谢Rust编程语言群的解惑, 意识到async-std的更新不活跃,后来选择了tokio

心得:

  1. Drop功能和C++的析构功能很像,虽然没有defer方便,还是简单可靠
  2. log功能比较强大
  3. vscode对tokio的支持还不是那么友好,断点调试的时候,遇到await会出现进入poll的代码区域,导致调试比较困难
  4. axum的文档还是比较简单,但是基本的功能还是比较完善的,比如extract的功能,可以直接从request中提取参数,还有response的功能,可以直接返回json
  5. tokio::select! 这个功能比go的select功能强大,可以并行监控多个task,将复杂的并发逻辑用串行的思路解决
  6. futures::StreamExt 的代码不是特别好理解,还是习惯for循环的方式

Axum vs Gin

gin作为golang的最流行的web框架,有丰富的middleware扩展能力,并基于httpRequestResponse机制,可以方便的处理FormResponse

Axum也有类似的middleware能力,但是没有gin那么丰富,但是Axumextract功能,可以直接从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.pdfpage.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

评论区

写评论

还没有评论

1 共 0 条评论, 1 页