WebSocket入門篇——基于Spring

WebSocket入門篇


  • 背景介紹

    最近突然發(fā)現(xiàn)有一種http協(xié)議不能很好的滿足的一種需求。具體來說就是钞诡,頁面的某個組件需要動態(tài)的根據(jù)數(shù)據(jù)庫中的內(nèi)容的更新而即時的刷新出來。而傳統(tǒng)的做法無論是輪詢還是長連接對性能來說都不是很友好擂涛。比如說如果我前端用輪詢/長連接丢氢,而后臺數(shù)據(jù)庫這邊又得去輪詢數(shù)據(jù)中的更新記錄,實在是忍不了啊裕寨。

有沒有一種方式既可以在數(shù)據(jù)庫更新數(shù)據(jù)的時候去告訴后端來取數(shù)據(jù)了(或者直接把更新的內(nèi)容推送到后端)浩蓉,又可以讓后端把處理ok的數(shù)據(jù)再直接推送到后端呢?

機制的你是不是直接聯(lián)想到了一種叫socket的技術(shù)呢宾袜。沒錯現(xiàn)在的socket也能直接再H5上使用了捻艳,只不過加了個前綴叫websocket,是H5的新增的一種支持協(xié)議庆猫,個人覺得和socket一樣都是全雙工通信的方式认轨,就是你不理我,我也可以來理你月培。


websocket簡單實現(xiàn)

  • 首先搭建一個SpringMVC的項目嘁字,各種配置好,可以簡單在網(wǎng)頁上實現(xiàn)hello word就足矣

首先是后端的配置

為更加可控需要自定義一個websocket攔截器和websocket的握手器杉畜,用于處理連接的過濾和具體數(shù)據(jù)交互細節(jié)

攔截器

創(chuàng)建攔截器繼承HttpSessionHandshakeInterceptor纪蜒,具體如下:


import java.util.Map;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import javax.servlet.http.HttpSession;

import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

/**
 * WebSocket攔截器
 *
 *
 */
public class SpringWebSocketHandlerInterceptor extends HttpSessionHandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
            Map<String, Object> attributes) throws Exception {
        // TODO Auto-generated method stub
        System.out.println("Before Handshake");
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
            HttpSession session = servletRequest.getServletRequest().getSession(false);
            if (session != null) {
                //使用userName區(qū)分WebSocketHandler,以便定向發(fā)送消息
                String userName = (String) session.getAttribute("SESSION_USERNAME");
                if (userName==null) {
                    userName="default-system";
                }
                attributes.put("WEBSOCKET_USERNAME",userName);
            }
        }
        return super.beforeHandshake(request, response, wsHandler, attributes);
        
    }
    
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
            Exception ex) {
        // TODO Auto-generated method stub
        super.afterHandshake(request, response, wsHandler, ex);
    }
}

握手器此叠,確定具體的握手細節(jié)纯续,代碼:


import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.ArrayList;

import org.apache.log4j.Logger;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

public class SpringWebSocketHandler extends TextWebSocketHandler {
    private static final ArrayList<WebSocketSession> users;//這個會出現(xiàn)性能問題,最好用Map來存儲灭袁,key用userid
    private static Logger logger = Logger.getLogger(SpringWebSocketHandler.class);
    static {
        users = new ArrayList<WebSocketSession>();
    }
    
    public SpringWebSocketHandler() {
        // TODO Auto-generated constructor stub
    }

    /**
     * 連接成功時候猬错,會觸發(fā)頁面上onopen方法
     */
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // TODO Auto-generated method stub
        System.out.println("connect to the websocket success......當(dāng)前數(shù)量:"+users.size());
        users.add(session);
        //這塊會實現(xiàn)自己業(yè)務(wù),比如茸歧,當(dāng)用戶登錄后倦炒,會把離線消息推送給用戶
        //TextMessage returnMessage = new TextMessage("你將收到的離線");
        //session.sendMessage(returnMessage);
    }
    
    /**
     * 關(guān)閉連接時觸發(fā)
     */
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        logger.debug("websocket connection closed......");
        String username= (String) session.getAttributes().get("WEBSOCKET_USERNAME");
        System.out.println("用戶"+username+"已退出!");
        users.remove(session);
        System.out.println("剩余在線用戶"+users.size());
    }

    /**
     * js調(diào)用websocket.send時候举娩,會調(diào)用該方法
     */
    @Override    
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        System.err.println(message.toString());
        String username = (String) session.getAttributes().get("WEBSOCKET_USERNAME");
        // 獲取提交過來的消息詳情
        logger.info("收到用戶 " + username + "的消息:" + message.toString());
        //super.handleTextMessage(session, message);
        
        session.sendMessage(new TextMessage("reply msg:" + message.getPayload()));
        sendMessageToUsers(message);
    }

    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        if(session.isOpen()){session.close();}
        logger.debug("websocket connection closed......");
        users.remove(session);
    }

    public boolean supportsPartialMessages() {
        return false;
    }
    
    
    /**
     * 給某個用戶發(fā)送消息
     *
     * @param userName
     * @param message
     */
    public void sendMessageToUser(String userName, TextMessage message) {
        for (WebSocketSession user : users) {
            if (user.getAttributes().get("WEBSOCKET_USERNAME").equals(userName)) {
                try {
                    if (user.isOpen()) {
                        user.sendMessage(message);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            }
        }
    }
    
    /**
     * 給所有在線用戶發(fā)送消息
     *
     * @param message
     */
    public void sendMessageToUsers(TextMessage message) {
        for (WebSocketSession user : users) {
            try {
                if (user.isOpen()) {
                    user.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

最后設(shè)置好連接wbsocket端點析校,代碼:


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Configuration
@EnableWebMvc
@EnableWebSocket
public class SpringWebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
         registry.addHandler(webSocketHandler(),"/websocket/socketServer.do").addInterceptors(new SpringWebSocketHandlerInterceptor());
            registry.addHandler(webSocketHandler(), "/sockjs/socketServer.do").addInterceptors(new SpringWebSocketHandlerInterceptor()).withSockJS();
        
    }
    @Bean
    public TextWebSocketHandler webSocketHandler(){
        return new SpringWebSocketHandler();
    }

}

  • 由于使用了很多spring的注解比如@Configuration构罗,@EnableWebMvc,@EnableWebSocket智玻,因此需要確定springmvc的配置中加入了:

  • <mvc:annotation-driven/>
  • <context:component-scan base-package="你得配置路徑"/>

controller 層連接到websocket的代碼:

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.socket.TextMessage;
@Controller
@RequestMapping("/")
public class SignlController {
    @Bean//這個注解會從Spring容器拿出Bean
    public SpringWebSocketHandler infoHandler() {
        return new SpringWebSocketHandler();
    }
    @RequestMapping("start")
    public String start(HttpServletRequest request,HttpServletResponse response) {
        
        return "index";
    }
    @RequestMapping("/websocket/login")
    public ModelAndView login(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String username = request.getParameter("username");
        System.out.println(username+"登錄");
        HttpSession session = request.getSession(false);
        session.setAttribute("SESSION_USERNAME", username);
        //response.sendRedirect("/quicksand/jsp/websocket.jsp");
        return new ModelAndView("shareMsg");
    }
    
    @RequestMapping("send")
    public String send(HttpServletRequest request) {
        String username = request.getParameter("username");
        System.out.println(username);
        infoHandler().sendMessageToUser(username, new TextMessage("你好遂唧,測試!5跎荨8桥怼!"));
        return "shareMsg";
    }
}

最后貼出依賴jar包页滚,pom.xml:

<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${spring.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>${spring.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-websocket</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>  
      <groupId>org.springframework</groupId>  
      <artifactId>spring-messaging</artifactId>  
      <version>${spring.version}</version>
      <scope>provided</scope>  
    </dependency>
    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.0</version>
    <scope>compile</scope>
</dependency>
    <dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
    <groupId>org.java-websocket</groupId>
    <artifactId>Java-WebSocket</artifactId>
    <version>1.3.5</version>
</dependency>
<dependency>
  <groupId>log4j</groupId>
     <artifactId>log4j</artifactId>
    <version>1.2.15</version>
    <scope>runtime</scope>
 </dependency>
   <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.1.0</version>
        </dependency>
  </dependencies>
  <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring.version>4.2.5.RELEASE</spring.version>
  </properties>

最后是前端jsp的hello websocket的測試代碼:

登陸的jsp召边,用于你想websocket連接內(nèi)的某個用戶發(fā)送消息

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<h2>Hello World!</h2>
<body>
    <!-- ship是我的項目名-->
    <form action="websocket/login.do">
        登錄名:<input type="text" name="username"/>
        <input type="submit" value="登錄"/>
    </form>
</body>
</body>
</html>

發(fā)送消息和接收消息的jsp代碼:

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
<script type="text/javascript" src="http://cdn.bootcss.com/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.js"></script>
<script type="text/javascript">
    var websocket = null;
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://localhost:8080/Socket-demo/websocket/socketServer.do");
    } 
    else if ('MozWebSocket' in window) {
        websocket = new MozWebSocket("ws://localhost:8080/Socket-demo/websocket/socketServer.do");
    } 
    else {
        websocket = new SockJS("http://localhost:8080/Socket-demo/sockjs/socketServer.do");
    }
    websocket.onopen = onOpen;
    websocket.onmessage = onMessage;
    websocket.onerror = onError;
    websocket.onclose = onClose;
              
    function onOpen(openEvt) {
        //alert(openEvt.Data);
    }
    
    function onMessage(evt) {
        alert(evt.data);
    }
    function onError() {}
    function onClose() {}
    
    function doSend() {
        if (websocket.readyState == websocket.OPEN) {          
            var msg = document.getElementById("inputMsg").value;  
            websocket.send(msg);//調(diào)用后臺handleTextMessage方法
            alert("發(fā)送成功!");  
        } else {  
            alert("連接失敗!");  
        }  
    }

   window.close=function()
       {
             websocket.onclose();
       }
    
</script>
請輸入:<textarea rows="5" cols="10" id="inputMsg" name="inputMsg"></textarea>
<button onclick="doSend();">發(fā)送</button>
<form action="send.c">
      指定發(fā)送:<input type="text" name="username"/>
        <input type="submit" value="確定"/>
    </form>
</body>
</html>

最后把完整的項目代碼上傳到了我的GitHup


最后再貼一個java的websocket客戶端的代碼,用于數(shù)據(jù)庫主動推送消息到后臺/或者前臺


import java.net.URI;
import java.net.URISyntaxException;

import org.apache.log4j.Logger;
import org.java_websocket.WebSocket;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;

public class WebsocketClient {
    
    private static Logger logger = Logger.getLogger(WebsocketClient.class);
    public static WebSocketClient client;
    public static void main(String[] args) {
        try {
            client = new WebSocketClient(new URI("ws://localhost:8080/Socket-demo/websocket/socketServer.do"),new Draft_6455()) {
                @Override
                public void onOpen(ServerHandshake serverHandshake) {
                     logger.info("握手成功");
                }
 
                @Override
                public void onMessage(String msg) {
                     logger.info("收到消息=========="+msg);
                     if(msg.equals("over")){
                         client.close();
                     }
                     
                }
 
                @Override
                public void onClose(int i, String s, boolean b) {
                     logger.info("鏈接已關(guān)閉");
                }
 
                @Override
                public void onError(Exception e){
                    e.printStackTrace();
                    logger.info("發(fā)生錯誤已關(guān)閉");
                }
            };
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
 
        client.connect();
        logger.info(client.getDraft());
       while(!client.getReadyState().equals(WebSocket.READYSTATE.OPEN)){
           logger.info("正在連接...");
        }
       //連接成功,發(fā)送信息
    client.send("哈嘍,連接一下啊");
       
    }
    
}


結(jié)語:通過websocket技術(shù)你可以實現(xiàn)后臺主動推送消息到前端裹驰,以提高數(shù)據(jù)的實時性隧熙,同時你也可以通過websocket的java客戶端,在數(shù)據(jù)庫中通過觸發(fā)器去調(diào)它幻林,實現(xiàn)數(shù)據(jù)庫推送消息到后臺/前臺贞盯。但是這種方式缺點在于:對于不同的用戶推送不同的消息,需要手動去寫邏輯和指定規(guī)則在后臺的握手器中實現(xiàn)沪饺,較為麻煩躏敢。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市整葡,隨后出現(xiàn)的幾起案子件余,更是在濱河造成了極大的恐慌,老刑警劉巖遭居,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件啼器,死亡現(xiàn)場離奇詭異,居然都是意外死亡魏滚,警方通過查閱死者的電腦和手機镀首,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鼠次,“玉大人更哄,你說我怎么就攤上這事⌒瓤埽” “怎么了成翩?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長赦役。 經(jīng)常有香客問我麻敌,道長,這世上最難降的妖魔是什么掂摔? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任术羔,我火速辦了婚禮赢赊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘级历。我一直安慰自己释移,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布寥殖。 她就那樣靜靜地躺著玩讳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嚼贡。 梳的紋絲不亂的頭發(fā)上熏纯,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音粤策,去河邊找鬼樟澜。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播菌瘫,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼途事,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了吭服?” 一聲冷哼從身側(cè)響起嚷堡,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎艇棕,沒想到半個月后蝌戒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡沼琉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年北苟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片打瘪。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡友鼻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出闺骚,到底是詐尸還是另有隱情彩扔,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布僻爽,位于F島的核電站虫碉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏胸梆。R本人自食惡果不足惜敦捧,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一须板、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧兢卵,春花似錦习瑰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至王滤,卻和暖如春贺嫂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背雁乡。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工第喳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人踱稍。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓曲饱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親珠月。 傳聞我的和親對象是個殘疾皇子扩淀,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容