1. 程式人生 > >輕鬆搞定WebSocket

輕鬆搞定WebSocket

實現後臺向前端推送資訊

pom.xml引入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

WebSocketConfig

啟用WebSocket的支援也是很簡單,幾句程式碼搞定

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
​
/**
 * 開啟WebSocket支援
 * @author zhengkai
 */
@Configuration  
public class WebSocketConfig {  
    
    @Bean  
    public ServerEndpointExporter serverEndpointExporter() {  
        return new ServerEndpointExporter();  
    }  
  
}

WebSocketServer

因為WebSocket是類似客戶端服務端的形式(採用ws協議),那麼這裡的WebSocketServer其實就相當於一個ws協議的Controller

直接@ServerEndpoint("/websocket")@Component啟用即可,然後在裡面實現@OnOpen,@onClose,@onMessage等方法

/**
 * @Author: ynz
 * @Date: 2018/12/22/022 10:35
 */
@ServerEndpoint("/websocket/{sid}")
@Component
@Slf4j
public class WebSocketServer {
​
    //靜態變數,用來記錄當前線上連線數。應該把它設計成執行緒安全的。
    private static int onlineCount = 0;
    //concurrent包的執行緒安全Set,用來存放每個客戶端對應的MyWebSocket物件。
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet =
            new CopyOnWriteArraySet<WebSocketServer>();
​
    //與某個客戶端的連線會話,需要通過它來給客戶端傳送資料
    private Session session;
​
    //接收sid
    private String sid="";
    /**
     * 連線建立成功呼叫的方法*/
    @OnOpen
    public void onOpen(Session session,@PathParam("sid") String sid) {
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //線上數加1
        log.info("有新視窗開始監聽:"+sid+",當前線上人數為" + getOnlineCount());
        this.sid=sid;
        try {
            sendMessage("連線成功");
        } catch (IOException e) {
            log.error("websocket IO異常");
        }
    }
​
    /**
     * 連線關閉呼叫的方法
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);  //從set中刪除
        subOnlineCount();           //線上數減1
        log.info("有一連線關閉!當前線上人數為" + getOnlineCount());
    }
​
    /**
     * 收到客戶端訊息後呼叫的方法
     *
     * @param message 客戶端傳送過來的訊息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到來自視窗"+sid+"的資訊:"+message);
        //群發訊息
        for (WebSocketServer item : webSocketSet) {
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
​
    /**
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("發生錯誤");
        error.printStackTrace();
    }
    /**
     * 實現伺服器主動推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }
​
​
    /**
     * 群發自定義訊息
     * */
    public static void sendInfo(String message,@PathParam("sid") String sid)
            throws IOException {
        log.info("推送訊息到視窗"+sid+",推送內容:"+message);
        for (WebSocketServer item : webSocketSet) {
            try {
                //這裡可以設定只推送給這個sid的,為null則全部推送
                if(sid==null) {
                    item.sendMessage(message);
                }else if(item.sid.equals(sid)){
                    item.sendMessage(message);
                }
            } catch (IOException e) {
                continue;
            }
        }
    }
​
    public static synchronized int getOnlineCount() {
        return onlineCount;
    }
​
    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }
​
    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }
}

訊息推送

至於推送新資訊,可以再自己的Controller寫個方法呼叫WebSocketServer.sendInfo();即可

@Controller
public class CheckCenterController {
​
    //推送資料介面
    @ResponseBody
    @RequestMapping("/socket/push/{cid}")
    public String pushToWeb(@PathVariable String cid,@RequestBody String message) {
        try {
            WebSocketServer.sendInfo(message,cid);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return message;
    }
}

頁面發起socket請求

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
        <script src="js/jquery.min.js"></script>
        <script>
            var socket = null;
            function connect(){
                if(typeof(WebSocket) == "undefined") {
                    console.log("您的瀏覽器不支援WebSocket");
                }else{
                    console.log("您的瀏覽器支援WebSocket");
                    //實現化WebSocket物件,指定要連線的伺服器地址與埠  建立連線
                    socket = new WebSocket($("#url").val());
                    //開啟事件
                    socket.onopen = function() {
                        console.log("Socket 已開啟");
                        $("#status").html("已連線...");
                        //socket.send("這是來自客戶端的訊息" + location.href + new Date());
                    };
                    //獲得訊息事件
                    socket.onmessage = function(msg) {
                        console.log(msg.data);
                        $("#displayMsg").html( $("#displayMsg").html()+"<br>"+msg.data );
                        //發現訊息進入    開始處理前端觸發邏輯
                    };
                    //關閉事件
                    socket.onclose = function() {
                        console.log("Socket已關閉");
                        $("#status").html("未連線...");
                        socket = null;
                    };
                    //發生了錯誤事件
                    socket.onerror = function() {
                        alert("Socket發生了錯誤");
                        //此時可以嘗試重新整理頁面
                    }
                }
            }
​
            function send() {
                if(socket == null){
                    alert("未連線");
                    return false;
                }
                socket.send($("#sendMsg").val());
            }
​
            function closeConnect(){
                $("#status").html("已斷開...");
                socket.close();
            }
​
​
        </script>
    </head>
    <body>
        連線地址:<input type="text" id="url" style="width:400px;" value="ws://127.0.0.1:8080/websocket/22"></input>
        <button type="button" id="connect" onclick="connect()">連線</button>
        <button type="button" id="closeConnect" onclick="closeConnect()">斷開</button>
        &nbsp;&nbsp;&nbsp;&nbsp;<div id="status" style="display:inline;">未連線...</div>
        <br><br>
        傳送訊息:<input type="text" id="sendMsg" style="width:400px;"></input>
        <button type="button"  onclick="send()">傳送</button><br><br>
        <div>接收到訊息:</div>
        <div id="displayMsg"></div>
    </body>
</html>

訪問:http://127.0.0.1:8080/socket.html,可以連線服務,傳送訊息。

或者:http://127.0.0.1:8080/socket/push/{cid}給相應的服務發訊息。

實現SSH WEB客戶端

新增依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
​
<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.54</version>
</dependency>

WebSocketMessageBrokerConfigurer

配置訊息代理,預設情況下使用內建的訊息代理。 類上的註解@EnableWebSocketMessageBroker:此註解表示使用STOMP協議來傳輸基於訊息代理的訊息,此時可以在@Controller類中使用@MessageMapping

在方法registerStompEndpoints()裡addEndpoint方法:新增STOMP協議的端點。這個HTTP URL是供WebSocket或SockJS客戶端訪問的地址;withSockJS:指定端點使用SockJS協議

在方法configureMessageBroker()裡設定簡單訊息代理,並配置訊息的傳送的地址符合配置的字首的訊息才傳送到這個broker

@Configuration
// 此註解表示使用STOMP協議來傳輸基於訊息代理的訊息,此時可以在@Controller類中使用@MessageMapping
@EnableWebSocketMessageBroker
public class SSHSocketConfig  implements WebSocketMessageBrokerConfigurer {
​
    /**
     * setAllowedOrigins方法用來設定來自那些域名的請求可訪問,預設為localhost
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket")
                .setAllowedOrigins("*");
         //SockJS客戶端訪問
        /*registry.addEndpoint("/my-websocket").withSockJS();*/
    }
​
    /**
     * 配置訊息代理
     * 啟動Broker,訊息的傳送的地址符合配置的字首來的訊息才傳送到這個broker
     */
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        /**
         * 配置訊息代理
         * 啟動簡單Broker,訊息的傳送的地址符合配置的字首來的訊息才傳送到這個broker
         */
        registry.enableSimpleBroker("/topic");
        //只接收這字首傳送過來的訊息
        registry.setApplicationDestinationPrefixes("/send");//應用請求字首
    }
}

@Controller類 ,訊息處理

@Controller
public class SSHController {
    @Resource
    private SimpMessagingTemplate messagingTemplate ;
    static Map<String,SSHData> map = new HashMap<>();
​
    /**
     * 接收訊息
     */
    @MessageMapping("/receive/{id}")
   // @SendTo("/topic/test")
    public String receiver(@DestinationVariable("id") String id, String msg)
            throws IOException {
        SSHData sshData = map.get(id);
        if(sshData != null){
            OutputStream outputStream = map.get(id).getOutputStream();
            outputStream.write((msg).getBytes());
            outputStream.flush();
        }else{
            messagingTemplate.convertAndSend("/topic/"+id,"遠端伺服器未連線。。。\n\r");
        }
        return msg;
    }
​
    /**
     * 建立SSH連線
     */
    @RequestMapping("/connect")
    @ResponseBody
    public String connect(String user,String host,Integer port,String password,String id)
            throws IOException {
        SSHData sshData = map.get(id);
        if(sshData != null){
            sshData.release();
        }
        ChannelShell channelShell = SshUtils.getShellChannel( user, host, port , password, id);
        if(channelShell == null){
            messagingTemplate.convertAndSend("/topic/"+id,
                    "遠端伺服器連線失敗,請檢查使用者或者密碼是正確\n\r");
            return "";
        }
        map.put(id,new SSHData(channelShell,messagingTemplate,id));
        return "";
    }
​
    /**
     * 斷開連線
     */
    @RequestMapping("/disconnect")
    @ResponseBody
    public String disConnect(String id) throws IOException {
        SSHData sshData = map.get(id);
        if(sshData != null){
            sshData.release();
            map.remove(id);
        }
        messagingTemplate.convertAndSend("/topic/"+id,"已斷開連線。。。\n\r");
        return "";
    }
}
​
  • @MessageMapping:指定要接收訊息的地址,類似@RequestMapping

  • @SendTo預設訊息將被髮送到與傳入訊息相同的目的地,但是目的地前面附加字首(預設情況下為“/topic”}

  • @DestinationVariable接收URL的引數,類似@PathVariable

前端stomp、sockjs的配置

Stomp

websocket使用socket實現雙工非同步通訊能力。但是如果直接使用websocket協議開發程式比較繁瑣,我們可以使用它的子協議Stomp

SockJS

sockjs是websocket協議的實現,增加了對瀏覽器不支援websocket的時候的相容支援 SockJS的支援的傳輸的協議有3類: WebSocket, HTTP Streaming, and HTTP Long Polling。預設使用websocket,如果瀏覽器不支援websocket,則使用後兩種的方式。

SockJS使用”Get /info”從服務端獲取基本資訊。然後客戶端會決定使用哪種傳輸方式。如果瀏覽器使用websocket,則使用websocket。如果不能,則使用Http Streaming,如果還不行,則最後使用 HTTP Long Polling。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
    <script src="js/jquery.min.js"></script>
    <script src="js/xterm/sockjs.min.js"></script>
    <script src="js/xterm/stomp.min.js"></script>
    <link href="js/xterm/xterm.css" rel="stylesheet"></link>
    <script src="js/xterm/xterm.js"></script>
</head>
<body>
​
    <div id="terminal"></div>
    <div id="terminal2" style="height: 10%">
        <div id="desc"></div><br>
        IP:<input id="ip" type="text" value="47.106.106.**"></input>
        Port:<input id="port" type="text" value="22"></input>
        使用者名稱:<input id="username" type="text" value="root"></input>
        密碼:<input id="password" type="text" value="*"></input>
        <button onclick="connect()">登入</button>
        <button onclick="disconnect()">斷開</button>
    </div>
​
<script>
    var stompClient ;
    var term = new Terminal({
        cols: 150,
        rows: 35,
        screenKeys: true,
        useStyle: true,
        cursorBlink: true
    });
​
    term.open(document.getElementById('terminal'));
    term.on('data', function($data) {
        //term.write($data);
        stompClient.send("/send/receive/1",{}, $data );
    });
​
    document.onkeydown=function(){
        if (event.keyCode == 13){
            term.write("\n\r");
        }
    }
​
</script>
<script>
    $(document).ready(function() {
        openSocket();
    });
    function openSocket() {
        if(stompClient==null){
            var socketPath ='ws://127.0.0.1:8080/websocket';
            var socket = new WebSocket(socketPath);
            stompClient = Stomp.over(socket);
            var headers={
                "Access-Control-Allow-Origin":"*",
                "Access-Control-Allow-Credentials":"true",
                "token":"kltoen"
            };
            stompClient.connect(headers, function(frame) {
                $("#desc").html("WebSocket已連線");
                stompClient.subscribe('/topic/1', function(event) {
                    term.write(event.body);
                },headers);
            },function (error) {
                $("#desc").html("WebSocket連線失敗");
            });
​
            window.setInterval(function(){ //每隔5秒鐘傳送一次心跳,避免websocket連線超時而自動斷開
                stompClient.send(" ");
            },30000);
        }
    }
​
    function connect() {
        $.ajax({
            type: "GET",
            url: "http://127.0.0.1:8080/connect?user="+$("#username").val()+
                       "&host="+$("#ip").val()+ "&port="+$("#port").val()+
                       "&password="+$("#password").val()+"&id=1"
        });
    }
​
    function disconnect() {
        $.ajax({
            type: "GET",
            url: "http://127.0.0.1:8080/disconnect?&id=1"
        });
    }
</script>
</body>
</html>

測試