摘要

本文记录在Linux使用Docker安装GUI工具,并使用Firefox浏览器访问页面。

正文

一、通过noVNC访问GUI

本文记录在Linux使用Docker安装GUI工具,并使用Firefox浏览器访问页面。

获取docker镜像ubuntu-desktop-lxde-vnc,该镜像内置如下工具

  • Web桌面服务(noVNC):默认端口80
  • VNC服务:默认端口5900
  • 火狐浏览器

有如上工具,基本上使用GUI的条件都满足了。

1.) 获取镜像

sh
1
docker pull dorowu/ubuntu-desktop-lxde-vnc:focal-arm64

2.) 启动镜像,只开启noVNC服务,并设置密码123456

sh
1
2
3
4
5
6
7
8
docker run -d --rm \
-p 80:80 \
-e TZ=Asia/Shanghai \
-e VNC_PASSWORD=123456 \
-v /etc/localtime:/etc/localtime:ro \
-v /dev/shm:/dev/shm \
--name gui \
dorowu/ubuntu-desktop-lxde-vnc:focal-arm64

-v /dev/shm:/dev/shm:挂载共享内存设备,提高图形性能(Chromium/Firebase 会用到)。

3.) 浏览器访问http://ip:80

image-20250531193449681.png

image-20250531193459113.png

二、HTTP Upgrade

2.1 原理

WebSocket 不是独立于 HTTP 的协议,而是通过 HTTP/1.1 的 Upgrade 机制建立连接。建立之后,通信就完全脱离 HTTP,变成 WebSocket 专用的帧格式。

而像上面的 noVNC 就是通过一个端口,实现了 HTTP 服务以及升级成 WebSocket 进行实时通信。

查看 WebSocket 升级的过程。

1.) 客户端通过 HTTP/1.1 向服务端发起一个升级请求。

sh
1
2
3
4
5
6
7
GET /ws HTTP/1.1
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: cVw5C1V0TEmN6ZTY76U1OQ==
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Host: 192.168.1.106:8088

说明

  • Connection:Upgrade
    • 表示这个连接要进行升级
  • Upgrade:websocket
    • 表示这个连接要升级成 WebSocket
  • Sec-WebSocket-Version:13
    • 当前使用的WebSocket版本
  • Sec-WebSocket-Key:随机生成的Base64编码。

2.) 服务端同意升级。

sh
1
2
3
4
5
HTTP/1.1 101 Switching Protocols
upgrade: websocket
connection: upgrade
sec-websocket-accept: rpyOTEZGtzbDpjvK9/TiQPfjp3I=
sec-websocket-extensions: permessage-deflate

说明

  • 101 Switching Protocols
    • 协议升级成功
  • Sec-WebSocket-Accept:rpyOTEZGtzbDpjvK9/TiQPfjp3I=
    • 服务端根据客户端的key生成的校验值。生成方式是将Sec-WebSocket-Key的值+协议版本对应的GUID拼接后的字符串进行SHA-1编码。客户端拿到后,做同样的操作,然后校验。

具体的细节可自行查阅文档。RFC 6455 - The WebSocket Protocol

image-20250622131155159.jpg

下面记录使用 Vertx 实现 HTTP/WebSocket 服务的示例,源码参考meethigher/vertx-examples: learn to use vertx

java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Example17 {


    private static final Logger log = LoggerFactory.getLogger(Example17.class);

    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();

        HttpServer server = vertx.createHttpServer();

        // 处理 HTTP 请求
        server.requestHandler(req -> {
            if ("/hello".equals(req.path())) {
                req.response()
                        .putHeader("content-type", "text/plain")
                        .end("HTTP " + System.currentTimeMillis());
            } else {
                req.response()
                        .setStatusCode(404)
                        .end("Not Found");
            }
        });

        // 处理 WebSocket 请求
        server.webSocketHandler(ws -> {
            if ("/ws".equals(ws.path())) {
                ws.handler(buffer -> {
                    // 收到消息后,原封不动回写
                    ws.writeTextMessage("WebSocket " + System.currentTimeMillis());
                });
            } else {
                ws.reject();
            }
        });

        // 监听端口
        server.listen(8080, res -> {
            if (res.succeeded()) {
                log.info("http server started on port {}", res.result().actualPort());
            } else {
                log.error("http server start failed", res.cause());
            }
        });
    }
}

Postman 可以支持发起 WebSocket 请求,右键 File-New-WebSocket 即可。

image-20250622135123844.png

2.2 http2对websocket升级的调整

HTTP2 明确禁止通过 Upgrade 请求头来进行协议升级。参考协议升级机制 - HTTP | MDN

image-20250622132035432.png

但是也不能说 HTTP2 完全不支持 WebSocket 升级,只是换了升级方式,大部分浏览器支持的不太好而已。RFC 8441 - Bootstrapping WebSockets with HTTP/2

2.3 为何在大模型LLM使用上sse多于websocket

最近在使用 mcp 时,发现内部通信机制使用了异步 sse,形成双向通信。

  1. 客户端与服务端建立 sse 长连接,服务端返回 sessionId 接口
  2. 客户端通过的 http post 向 sessionId 接口发起请求
  3. 服务端通过 sse 长连接返回响应

于是就思考为何不直接使用 websocket?原因也很简单。

像这种实时通信的,最好使用长连接,直接基于 tcp 自定义协议也行。但是便于开发的角度而言,可选的方案有 sse 和 websocket。

websocket 在 http2 上的升级,目前支持的并不好。而 http2 又解决了 http1.1 的队头阻塞问题,目前使用比较广泛。所以我认为这是弃用 websocket 的一个主要原因。