其实项目里用WebSocket主要是为了前端能通过网页直接操作后端的服务器,比如执行命令、看实时输出,就像本地连SSH一样。那我分服务端和前端两部分说说吧。


​​服务端怎么实现的?​​

首先,服务端得支持WebSocket协议。Spring Boot有内置的WebSocket支持,我们用了@EnableWebSocketMessageBroker来配置。核心是配两个东西:​​端点​​和​​消息代理​​。端点就是前端连过来的入口,比如/ssh-websocket,前端通过这个地址建立连接。消息代理的话,我们用SimpleBroker来处理订阅,前端发送的消息会通过/app前缀的路径到服务端,服务端处理完再通过/topic推送给前端。

然后,​​鉴权​​是关键。因为不是谁都能连这个WebSocket,得确保是登录过的用户。我们在握手阶段(也就是前端刚连上的时候)做了校验:前端请求头里带着JWT Token,服务端从请求里掏出来,用自定义的JwtTokenProvider验证是否有效。如果无效,直接拒绝连接;有效的话,就把用户信息(比如用户名)存到attributes里,后面处理消息时能取到。

接下来是​​会话管理​​。每个连进来的WebSocket会话(WebSocketSession)得和具体的SSH会话绑定。我们用了JSCH库来连服务器,每个用户连服务器前,得先创建一个JSCH的Session(比如输入用户名、密码、服务器地址)。为了不让这些会话乱,我们用了一个ConcurrentHashMap来存——键是WebSocket的sessionId,值是对应的JSCH Session。这样前端发命令时,服务端能根据sessionId找到对应的SSH连接,执行命令并返回结果。

处理消息的时候,前端发来的消息是JSON格式的,比如{"type":"command", "content":"ls -l"}。服务端解析后,如果是执行命令,就用JSCH的ChannelExec执行,把结果读出来再封装成JSON(比如{"type":"output", "content":"xxx"}),通过WebSocket发回前端。如果是调整窗口大小(比如用户拖拽终端窗口),就调用JSCH的setPtySize方法改终端尺寸。

还有,前端断开的时候,服务端得清理资源。我们在afterConnectionClosed方法里,根据sessionId把对应的JSCH Session关掉,避免资源泄露。


​​前端怎么实现的?​​

前端用的是Vue3,主要依赖SockJSStompJS——因为不是所有浏览器都支持原生的WebSocket,SockJS能兼容,StompJS用来处理消息协议。然后,用Xterm.js来渲染终端界面,它模拟了真实的命令行效果,支持输入、输出、光标移动这些。

具体步骤大概是这样的:

  1. ​建立连接​​:页面加载时,前端用SockJS连服务端的/ssh-websocket端点,再用Stomp订阅消息。比如:

    const socket = new SockJS('/api/ssh-websocket'); // 实际路径可能带前缀
    this.stompClient = Stomp.over(socket);
    this.stompClient.connect({}, () => {
      // 连成功后订阅消息,比如服务端返回的输出会到这里
      this.stompClient.subscribe('/user/topic/ssh-output', message => {
        const res = JSON.parse(message.body);
        if (res.type === 'OUTPUT') {
          this.terminal.write(res.content); // 输出到终端
        }
      });
    });
  2. ​初始化终端​​:用Xterm.js创建一个终端实例,挂载到页面的某个div上,设置主题(比如暗黑模式)、字体大小这些。然后监听用户的键盘输入,用户按键盘时,调用terminal.onData(data)拿到输入的内容,通过WebSocket发给服务端。

  3. ​发送命令和调整窗口​​:用户输入命令后,前端把命令包装成JSON(比如{"type":"command", "content":"ls"}),通过stompClient.send('/app/ssh-command', {}, JSON.stringify(payload))发给服务端。如果是调整窗口大小(比如浏览器窗口变了),就获取终端的行数和列数,同样发消息给服务端,让后端调整JSCH会话的终端尺寸。

  4. ​处理断开和重连​​:如果WebSocket断了,前端会自动尝试重连(可能需要加个延迟,避免一直连不上)。比如在disconnect事件里,设置个定时器,过几秒再试一次。


​​遇到的坑和解决​​

其实刚开始踩了不少坑。比如,​​会话绑定​​的时候,如果用户开了多个标签页,每个标签页的sessionId不一样,得确保每个标签页的SSH会话独立,不然会串命令。后来用ConcurrentHashMapsessionId和JSCH会话的映射,每个标签页独立管理,解决了这个问题。

还有​​鉴权失败​​的情况,前端连WebSocket时没带Token或者Token过期,服务端直接拒绝,但前端没提示。后来在连接失败的回调里加了个提示,让用户重新登录。

另外,​​命令输出的实时性​​也有问题。刚开始服务端执行命令后,一次性把所有输出发给前端,如果命令输出很大(比如ls -l很多文件),前端渲染会卡。后来改成按行流式发送,服务端读一行发一行,前端逐行渲染,体验好多了。

总的来说,WebSocket的核心是服务端和前端配合好消息格式、会话管理和鉴权,确保命令能准确执行,结果能实时显示。虽然中间遇到了一些细节问题,但通过调试和优化都解决了。