通过WebSocket实现网页版XShell

该代码示例展示了如何通过WebSocket与服务器建立连接会话,并建立SSH连接,实现网页上的XShell功能。它利用@OnOpen、@OnMessage等注解处理连接打开、消息接收等事件,并通过JsonDecoder进行JSON解码。同时,服务端的initConnection方法初始化SSH连接,receiveHandle方法处理客户端发送的数据,进行命令执行等操作。
部署运行你感兴趣的模型镜像

功能描述:通过webSocket与服务器建立连接会话,并建立SSH连接,实现网页版XShell配置

文件结构:

1、创建常量类ConstantPool

public class ConstantPool {

    /**
     * 发送指令:连接
     */
    public static final String WEBSSH_OPERATE_CONNECT = "connect";
    /**
     * 发送指令:命令
     */
    public static final String WEBSSH_OPERATE_COMMAND = "command";
}

2、创建 WebSshData实体类用来保存链接信息:

public class WebSshData extends BaseEntity {

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 主机名称
     */
    @TableField(value = "name")
    private String name;

    /**
     * 协议
     */
    @TableField(value = "protocol")
    private String protocol;

    /**
     * ip地址
     */
    @TableField(value = "host")
    private String host;

    /**
     * 端口号默认为22
     */
    @TableField("port")
    private Integer port = 22;

    /**
     * 用户名
     */
    @TableField("username")
    private String username;

    /**
     * 密码
     */
    @TableField("password")
    private String password;

    /**
     * 逻辑删除 0 未删除 1 已删除
     */
    @TableField("is_del")
    private Integer isDel;

    /**
     * 操作
     */
    @TableField(exist = false)
    private String operate;

    /**
     * 命令
     */
    @TableField(exist = false)
    private String command = "";
}

创建WebSocket消息接收实体类 SocketReceiveInfo:

@Data
public class SocketReceiveInfo implements Serializable {

    /**
     * 用户token
     */
    private String identifier;

    /**
     * 主机IP
     */
    private String host;

    /**
     * 端口号
     */
    private Integer port;

    /**
     * 用户名
     */
    private String userName;

    /**
     * 密码
     */
    private String password;

    /**
     * 唯一标识
     */
    private String uuid;

    /**
     * 消息
     */
    private String message;

    /**
     * 操作
     */
    private String operation;
}

创建ssh链接 信息实体类SshConnectInfo:

public class SshConnectInfo {
    private Session session;
    private JSch jSch;
    private Channel channel;


    public Session getSession() {
        return session;
    }

    public void setSession(Session session) {
        this.session = session;
    }

    public JSch getjSch() {
        return jSch;
    }

    public void setjSch(JSch jSch) {
        this.jSch = jSch;
    }

    public Channel getChannel() {
        return channel;
    }

    public void setChannel(Channel channel) {
        this.channel = channel;
    }
}

3、创建webSocket连接 及ssh链接

@Slf4j
@Component
@ServerEndpoint(value = "/{identifier}/{uuid}",decoders = JsonDecoder.class)
public class ApiSsh {

    private static final String IDENTIFIER = "identifier";

    private static TokenService tokenService;

    @Resource
    public void setTokenService(TokenService tokenService) {
        ApiSsh.tokenService = tokenService;
    }

    private static WebSshService webSshService;

    @Resource
    public void setWebSshService(WebSshService webSshService) {
        ApiSsh.webSshService = webSshService;
    }

    private MemWebSocketManager webSocketManager = new MemWebSocketManager();

    @OnOpen
    public void onOpen(Session session, @PathParam(IDENTIFIER) String identifier, @PathParam("uuid") String uuid) {
        try {
            if (StringUtils.isEmpty(identifier)) {
                throw new WebSocketException("client identifier is empty !!!");
            }
            LoginUser loginUser = tokenService.getLoginUser(identifier);
            if (loginUser == null) {
                WebSocketUtil.sendMessage(session, "用户token解析失败!!!");
                session.close();
                return;
            }
            log.info("前端 接入========>" + uuid);
            WebSocket webSocket = new WebSocket();
            webSocket.setIdentifier(uuid);
            webSocket.setLastHeart(System.currentTimeMillis());
            webSocket.setSession(session);
            webSocketManager.put(uuid, webSocket);
            webSshService.initConnection(session, uuid);
        } catch (Exception e) {
            log.error("websocket client 接入异常:{}", e.getMessage());
        }
    }

    @OnMessage
    public void onMessage(Session session, SocketReceiveInfo socketReceiveInfo) throws Exception {
        String uuid = socketReceiveInfo.getUuid();
        String message = socketReceiveInfo.getMessage();
        if (log.isInfoEnabled()) {
            log.info("前端发来消息");
        }
        System.out.println(uuid + "--" + message);
        WebSocket webSocket = webSocketManager.get(uuid);
        // 心跳监测
        if ("ping".equalsIgnoreCase(message)) {
            WebSocketUtil.sendMessage(session, "pong");
            if (webSocket != null) {
                webSocket.setLastHeart(System.currentTimeMillis());
            }
            return;
        }

        // 业务逻辑
        WebSshData webSshData = new WebSshData();
        webSshData.setHost(socketReceiveInfo.getHost());
        webSshData.setPort(socketReceiveInfo.getPort());
        webSshData.setUsername(socketReceiveInfo.getUserName());
        webSshData.setPassword(RsaUtils.decryptByPrivateKey(socketReceiveInfo.getPassword()));
        if (ConstantPool.WEBSSH_OPERATE_COMMAND.equals(socketReceiveInfo.getOperation())){
            message = message + "\r";
        }
        webSshData.setOperate(socketReceiveInfo.getOperation());
        webSshData.setCommand(message);
        webSshService.receiveHandle(webSshData, session, uuid);
    }

    @OnClose
    public void onClose(Session session, @PathParam("uuid") String uuid) {
        try {
            log.info("前端连接关闭");
            session.close();
        } catch (IOException e) {
            if (log.isInfoEnabled()) {
                log.info("关闭客户端,连接异常" + e.getMessage());
            } else {
                log.info("关闭客户端,连接异常:{}", e.getMessage());
            }
        }
        disconnect(uuid);
    }

    @OnError
    public void onError(Throwable t, @PathParam("uuid") String uuid) {
        log.error("客户端发生异常:{}", t.getMessage());
        disconnect(uuid);
    }

    private void disconnect(String identifier) {
        webSocketManager.remove(identifier);
    }

}

service层:

/**
     * 初始化连接
     *
     * @param session 会话
     * @param userId  uuid
     */
    @Override
    public void initConnection(javax.websocket.Session session, String userId) {
        JSch jSch = new JSch();
        SshConnectInfo sshConnectInfo = new SshConnectInfo();
        sshConnectInfo.setjSch(jSch);
        sshConnectInfo.setSession(session);
        //将这个ssh连接信息放入map中
        sshMap.put(userId, sshConnectInfo);
        String s = "success";
        byte[] bytes = s.getBytes();
        WebSocketUtil.sendBytes(session,Arrays.copyOfRange(bytes, 0, bytes.length));
    }

    /**
     * 处理客户端发送的数据
     *
     * @param webSSHData XShell实体类
     * @param session    会话
     * @param userId     uuid
     */
    @Override
    public void receiveHandle(WebSshData webSSHData, javax.websocket.Session session, String userId) {
        if (ConstantPool.WEBSSH_OPERATE_CONNECT.equals(webSSHData.getOperate())) {
            //找到刚才存储的ssh连接对象
            SshConnectInfo sshConnectInfo = (SshConnectInfo) sshMap.get(userId);
            //启动线程异步处理
            WebSshData finalWebSSHData = webSSHData;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        connectToSSH(sshConnectInfo, finalWebSSHData, session);
                    } catch (JSchException | IOException e) {
                        logger.error("webssh连接异常");
                        logger.error("异常信息:{}", e.getMessage());
                        close(userId);
                    }
                }
            });
        } else if (ConstantPool.WEBSSH_OPERATE_COMMAND.equals(webSSHData.getOperate())) {
            String command = webSSHData.getCommand();
            SshConnectInfo sshConnectInfo = (SshConnectInfo) sshMap.get(userId);
            if (sshConnectInfo != null) {
                try {
                    transToSSH(sshConnectInfo.getChannel(), command);
                } catch (IOException e) {
                    logger.error("webssh连接异常");
                    logger.error("异常信息:{}", e.getMessage());
                    close(userId);
                }
            }
        } else {
            logger.error("不支持的操作");
            close(userId);
        }
    }

    /**
     * 关闭链接
     */
    @Override
    public void close(String userId) {
        SshConnectInfo sshConnectInfo = (SshConnectInfo) sshMap.get(userId);
        if (sshConnectInfo != null) {
            //断开连接
            if (sshConnectInfo.getChannel() != null) {
                sshConnectInfo.getChannel().disconnect();
            }
            //map中移除
            sshMap.remove(userId);
        }
    }

    /**
     * 使用jsch连接终端
     *
     * @param sshConnectInfo   sshConnectInfo
     * @param webSSHData       webSSHData
     * @param webSocketSession webSocketSession
     * @throws JSchException JSchException
     * @throws IOException   IOException
     */
    private void connectToSSH(SshConnectInfo sshConnectInfo, WebSshData webSSHData, javax.websocket.Session webSocketSession) throws JSchException, IOException {
        Session session = null;
        Properties config = new Properties();
        config.put("StrictHostKeyChecking", "no");
        //获取jsch的会话
        session = sshConnectInfo.getjSch().getSession(webSSHData.getUsername(), webSSHData.getHost(), webSSHData.getPort());
        session.setConfig(config);
        //设置密码
        session.setPassword(webSSHData.getPassword());
        //连接  超时时间30s
        session.connect(30000);

        //开启shell通道
        Channel channel = session.openChannel("shell");

        //通道连接 超时时间3s
        channel.connect(3000);

        //设置channel
        sshConnectInfo.setChannel(channel);

        //转发消息
        transToSSH(channel, "\r");

        //读取终端返回的信息流
        InputStream inputStream = channel.getInputStream();
        try {
            //循环读取
            byte[] buffer = new byte[1024];
            int i = 0;
            //如果没有数据来,线程会一直阻塞在这个地方等待数据。
            while ((i = inputStream.read(buffer)) != -1) {
                WebSocketUtil.sendBytes(webSocketSession, Arrays.copyOfRange(buffer, 0, i));
            }

        } finally {
            //断开连接后关闭会话
            session.disconnect();
            channel.disconnect();
            if (inputStream != null) {
                inputStream.close();
            }
        }

    }

    /**
     * 将消息转发到终端
     *
     * @param channel channel
     * @param command command
     * @throws IOException IOException
     */
    private void transToSSH(Channel channel, String command) throws IOException {
        if (channel != null) {
            OutputStream outputStream = channel.getOutputStream();
            outputStream.write(command.getBytes());
            outputStream.flush();
        }
    }

总结:

一、@onOpen注解只能接收通过@PathParam传递的参数,其余形式的传参会报参数不识别的错

误,@onMessage注解如果传递对象类型参数,需要前端通过json格式转换并解码操作才能识别,

并在@ServerEndpoint注解中添加解码器类。

解码器:

public class JsonDecoder implements Decoder.Text<SocketReceiveInfo> {

    /**
     * Initialization does nothing
     *
     * @param config The end-point configuration
     */
    @Override
    public void init(EndpointConfig config) {
    }

    /**
     * Judgment of whether decoding is possible
     *
     * @param text text
     * @return boolean
     */
    @Override
    public boolean willDecode(String text) {
        return (text != null);
    }

    /**
     * Decoding process(JSON → object)
     *
     * @param text text
     * @return SocketReceiveInfo
     * @throws DecodeException
     */
    @Override
    public SocketReceiveInfo decode(String text) throws DecodeException {
        ObjectMapper mapper = new ObjectMapper();
        SocketReceiveInfo obj = null;
        try {
            obj = mapper.readValue(text, SocketReceiveInfo.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return obj;
    }

    /**
     * Do nothing to destroy
     */
    @Override
    public void destroy() {
    }
}

二、第一次请求onOpen会话请求建立服务器会话连接,服务器必须返回信息使得浏览器确认建立

成功,否则webSocket响应码会为1000,浏览器会自动关闭会话。

您可能感兴趣的与本文相关的镜像

Seed-Coder-8B-Base

Seed-Coder-8B-Base

文本生成
Seed-Coder

Seed-Coder是一个功能强大、透明、参数高效的 8B 级开源代码模型系列,包括基础变体、指导变体和推理变体,由字节团队开源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值