大家好,我是寶哥!
websocket 簡(jiǎn)單的說,websocket是真正實(shí)現(xiàn)了全雙工通信的服務(wù)器向客戶端推的互聯(lián)網(wǎng)技術(shù)。
全雙工與單工、半雙工的區(qū)別?
全雙工:簡(jiǎn)單地說,就是可以同時(shí)進(jìn)行信號(hào)的雙向傳輸(A->B且B->A),是瞬時(shí)同步的。 單工、半雙工:一個(gè)時(shí)間段內(nèi)只有一個(gè)動(dòng)作發(fā)生。 推送和拉取的區(qū)別?
推:由服務(wù)器主動(dòng)發(fā)消息給客戶端,就像廣播。優(yōu)勢(shì)在于,信息的主動(dòng)性和及時(shí)性。 拉:由客戶端主動(dòng)請(qǐng)求所需要的數(shù)據(jù)。 實(shí)現(xiàn)消息通信的幾種方式?
傳統(tǒng)的http協(xié)議實(shí)現(xiàn)方式:。 websocket協(xié)議實(shí)現(xiàn)方式。 接下來我們主要講第三種,使用websocket協(xié)議,來實(shí)現(xiàn)服務(wù)端定時(shí)向客戶端推送消息。
開發(fā)環(huán)境:jdk1.8、tomcat7 后臺(tái):springmvc、websocket、quartz 實(shí)現(xiàn)步驟 一、環(huán)境搭建 (1)導(dǎo)入相關(guān)約束: 在pom文件中加入需要的約束,spring相關(guān)的約束,請(qǐng)各位自己導(dǎo)入,這里我就不貼出來了。
<!-- 定時(shí)器的包 --> <dependency > <groupId > org.quartz-scheduler</groupId > <artifactId > quartz</artifactId > <version > 2.3.0</version > </dependency > <!-- spring-support.jar 這個(gè)jar 文件包含支持UI模版(Velocity,F(xiàn)reeMarker,JasperReports),郵件服務(wù),腳本服務(wù)(JRuby),緩存Cache(EHCache),任務(wù)計(jì)劃Scheduling(uartz)方面的類。 外部依賴spring-context, (spring-jdbc, Velocity, FreeMarker, JasperReports, BSH, Groovy, JRuby, Quartz, EHCache) --> <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context-support</artifactId > <version > 5.1.1.RELEASE</version > </dependency > <!-- websocket的包 --> <dependency > <groupId > javax.websocket</groupId > <artifactId > javax.websocket-api</artifactId > <version > 1.1</version > <scope > provided</scope > </dependency > <!-- ps:如果使用原始的配置方式,需要導(dǎo)入spring-websocket、spring-messaging的包,我們這里就通過注解實(shí)現(xiàn) -->
(2)配置xml文件 web.xml中就配置前端控制器,大家自行配置。然后,加載springmvc的配置文件。
springmvc.xml文件中
<!-- 自動(dòng)將控制器加載到bean --> <context:component-scan base-package ="com.socket.web" /> <!-- 配置視圖解析器 --> <bean class ="org.springframework.web.servlet.view.InternalResourceViewResolver" > <property name ="prefix" value ="/WEB-INF/views/" /> <property name ="suffix" value =".jsp" /> <property name ="contentType" value ="text/html; charset=utf-8" /> </bean > <!-- 自動(dòng)注冊(cè) DefaultAnnotationHandlerMapping 與 AnnotationMethodHandlerAdapter 兩個(gè) bean, 解決了 @Controller 注解的使用前提配置 --> <mvc:annotation-driven /> <!-- 使用fastjson 解析json 因?yàn)楸救说捻?xiàng)目中用到了fastjson,所以這段配置大家可以忽略。 --> <mvc:annotation-driven > <mvc:message-converters register-defaults ="true" > <bean class ="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" > <property name ="supportedMediaTypes" > <list > <value > text/html;charset=UTF-8</value > <value > application/json</value > </list > </property > <property name ="features" > <list > <value > WriteMapNullValue</value > <value > QuoteFieldNames</value > </list > </property > </bean > </mvc:message-converters > </mvc:annotation-driven >
到此,環(huán)境就基本搭建完成了。
二、完成后臺(tái)的功能 這里我就直接貼出代碼了,上面有相關(guān)的注釋。
首先,完成websocket的實(shí)現(xiàn)類。
package com.socket.web.socket;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import javax.websocket.*;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import java.io.IOException;import java.util.Map;import java.util.Set;import java.util.concurrent.ConcurrentHashMap;/** * @Author : 清風(fēng)一陣吹我心 * @ProjectName : socket * @Package : com.socket.web.socket * @ClassName : WebSocketServer * @Description : * @Version : 1.0 **/ //ServerEndpoint它的功能主要是將目前的類定義成一個(gè)websocket服務(wù)器端。注解的值將被用于監(jiān)聽用戶連接的終端訪問URL地址。 @ServerEndpoint (value = "/socket/{ip}" )@Component public class WebSocketServer { //使用slf4j打日志 private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketServer.class ) ; //用來記錄當(dāng)前在線連接數(shù) private static int onLineCount = 0 ; //用來存放每個(gè)客戶端對(duì)應(yīng)的WebSocketServer對(duì)象 private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<String, WebSocketServer>(); //某個(gè)客戶端的連接會(huì)話,需要通過它來給客戶端發(fā)送數(shù)據(jù) private Session session; //客戶端的ip地址 private String ip; /** * 連接建立成功,調(diào)用的方法,與前臺(tái)頁面的onOpen相對(duì)應(yīng) * @param ip ip地址 * @param session 會(huì)話 */ @OnOpen public void onOpen (@PathParam("ip" ) String ip,Session session) { //根據(jù)業(yè)務(wù),自定義邏輯實(shí)現(xiàn) this .session = session; this .ip = ip; webSocketMap.put(ip,this ); //將當(dāng)前對(duì)象放入map中 addOnLineCount(); //在線人數(shù)加一 LOGGER.info("有新的連接加入,ip:{}!當(dāng)前在線人數(shù):{}" ,ip,getOnLineCount()); } /** * 連接關(guān)閉調(diào)用的方法,與前臺(tái)頁面的onClose相對(duì)應(yīng) * @param ip */ @OnClose public void onClose (@PathParam("ip" ) String ip) { webSocketMap.remove(ip); //根據(jù)ip(key)移除WebSocketServer對(duì)象 subOnLineCount(); LOGGER.info("WebSocket關(guān)閉,ip:{},當(dāng)前在線人數(shù):{}" ,ip,getOnLineCount()); } /** * 當(dāng)服務(wù)器接收到客戶端發(fā)送的消息時(shí)所調(diào)用的方法,與前臺(tái)頁面的onMessage相對(duì)應(yīng) * @param message * @param session */ @OnMessage public void onMessage (String message,Session session) { //根據(jù)業(yè)務(wù),自定義邏輯實(shí)現(xiàn) LOGGER.info("收到客戶端的消息:{}" ,message); } /** * 發(fā)生錯(cuò)誤時(shí)調(diào)用,與前臺(tái)頁面的onError相對(duì)應(yīng) * @param session * @param error */ @OnError public void onError (Session session,Throwable error) { LOGGER.error("WebSocket發(fā)生錯(cuò)誤" ); error.printStackTrace(); } /** * 給當(dāng)前用戶發(fā)送消息 * @param message */ public void sendMessage (String message) { try { //getBasicRemote()是同步發(fā)送消息,這里我就用這個(gè)了,推薦大家使用getAsyncRemote()異步 this .session.getBasicRemote().sendText(message); }catch (IOException e){ e.printStackTrace(); LOGGER.info("發(fā)送數(shù)據(jù)錯(cuò)誤:,ip:{},message:{}" ,ip,message); } } /** * 給所有用戶發(fā)消息 * @param message */ public static void sendMessageAll (final String message) { //使用entrySet而不是用keySet的原因是,entrySet體現(xiàn)了map的映射關(guān)系,遍歷獲取數(shù)據(jù)更快。 Set<Map.Entry<String, WebSocketServer>> entries = webSocketMap.entrySet(); for (Map.Entry<String, WebSocketServer> entry : entries) { final WebSocketServer webSocketServer = entry.getValue(); //這里使用線程來控制消息的發(fā)送,這樣效率更高。 new Thread(new Runnable() { public void run () { webSocketServer.sendMessage(message); } }).start(); } } /** * 獲取當(dāng)前的連接數(shù) * @return */ public static synchronized int getOnLineCount () { return WebSocketServer.onLineCount; } /** * 有新的用戶連接時(shí),連接數(shù)自加1 */ public static synchronized void addOnLineCount () { WebSocketServer.onLineCount++; } /** * 斷開連接時(shí),連接數(shù)自減1 */ public static synchronized void subOnLineCount () { WebSocketServer.onLineCount--; } public Session getSession () { return session; } public void setSession (Session session) { this .session = session; } public static ConcurrentHashMap<String, WebSocketServer> getWebSocketMap () { return webSocketMap; } public static void setWebSocketMap (ConcurrentHashMap<String, WebSocketServer> webSocketMap) { WebSocketServer.webSocketMap = webSocketMap; } }
然后寫我們的定時(shí)器(quartz),這里我就不詳解定時(shí)器了。大家可以自行去了解。
這里我使用的是xml注解的方式,創(chuàng)建一個(gè)job類,此類不需要繼承任何類和實(shí)現(xiàn)任何接口。
package com.socket.web.quartz;import com.socket.web.socket.WebSocketServer;import java.io.IOException;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/** * @Author : 清風(fēng)一陣吹我心 * @ProjectName : socket * @Package : com.socket.web.quartz * @ClassName : TestJob * @Description : * @Version : 1.0 **/ public class TestJob { public void task () { //獲取WebSocketServer對(duì)象的映射。 ConcurrentHashMap<String, WebSocketServer> map = WebSocketServer.getWebSocketMap(); if (map.size() != 0 ){ for (Map.Entry<String, WebSocketServer> entry : map.entrySet()) { WebSocketServer webSocketServer = entry.getValue(); try { //向客戶端推送消息 webSocketServer.getSession().getBasicRemote().sendText("每隔兩秒,向客戶端推送一次數(shù)據(jù)" ); }catch (IOException e){ e.printStackTrace(); } } }else { System.out.println("WebSocket未連接" ); } } }
定時(shí)器的實(shí)現(xiàn)類就完成了,我們還需要在springmvc.xml中進(jìn)行配置
springmvc.xml配置:
<!-- 要執(zhí)行的任務(wù)類 --> <bean id ="testJob" class ="com.socket.web.quartz.TestJob" > </bean > <!-- 將需要執(zhí)行的定時(shí)任務(wù)注入job中 --> <bean id ="jobDetail" class ="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean" > <property name ="targetObject" ref ="testJob" /> <!-- 任務(wù)類中需要執(zhí)行的方法 --> <property name ="targetMethod" value ="task" > </property > <!-- 上一次未執(zhí)行完成的,要等待有再執(zhí)行。 --> <property name ="concurrent" value ="false" /> </bean > <!-- 基本的定時(shí)器,會(huì)綁定具體的任務(wù)。 --> <bean id ="trigger" class ="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean" > <property name ="jobDetail" ref ="jobDetail" /> <property name ="startDelay" value ="3000" /> <property name ="repeatInterval" value ="2000" /> </bean > <bean id ="scheduler" class ="org.springframework.scheduling.quartz.SchedulerFactoryBean" > <property name ="triggers" > <list > <ref bean ="trigger" /> </list > </property > </bean >
接下來是controller層的代碼,就一個(gè)登錄的功能。
package com.socket.web.controller;import com.socket.domain.User;import com.sun.org.apache.bcel.internal.generic.RETURN;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpSession;import java.util.UUID;/** * @Author : 清風(fēng)一陣吹我心 * @ProjectName : socket * @Package : com.socket.web * @ClassName : ChatController * @Description : * @CreateDate : 2018/11/9 11:04 * @Version : 1.0 **/ @RequestMapping ("socket" )@Controller public class ChatController { /** * 跳轉(zhuǎn)到登錄頁面 * @return */ @RequestMapping (value = "/login" ,method = RequestMethod.GET) public String goLogin () { return "login" ; } /** * 跳轉(zhuǎn)到聊天頁面 * @param request * @return */ @RequestMapping (value = "/home" ,method = RequestMethod.GET) public String goMain (HttpServletRequest request) { HttpSession session = request.getSession(); if (null == session.getAttribute("USER_SESSION" )){ return "login" ; } return "home" ; } @RequestMapping (value = "/login" ,method = RequestMethod.POST) public String login (User user, HttpServletRequest request) { HttpSession session = request.getSession(); //將用戶放入session session.setAttribute("USER_SESSION" ,user); return "redirect:home" ; } }
以上就是登錄的代碼了,基本上就是偽代碼,只要輸入用戶名就可以了,后面的邏輯,大家可以根據(jù)自己的業(yè)務(wù)來實(shí)現(xiàn)。
最后就是前臺(tái)頁面的設(shè)計(jì)了,登錄,login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <c:set var ="path" value="${pageContext.request.contextPath}" /> <html> <head> <title>登錄</title> </head> <body> <form action="${path}/socket/login" method="post" > 登錄名:<input type="text" name="username" /> <input type="submit" value="登錄" /> </form> </body> </html>
消息接收頁面,home.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>聊天</title> <script type="text/javascript" > //判斷當(dāng)前瀏覽器是否支持WebSocket var webSocket = null ; if ('WebSocket' in window) { webSocket = new WebSocket("ws://localhost:9001/socket/127.0.0.1" ); } else if ('MozWebSocket' in window) { webSocket = new MozWebSocket("ws://localhost:9001/socket/127.0.0.1" ); } else { alert('Not support webSocket' ); } //打開socket,握手 webSocket.onopen = function (event) { alert("websocket已經(jīng)連接" ); } //接收推送的消息 webSocket.onmessage = function (event) { console.info(event); alert(event.data); } //錯(cuò)誤時(shí) webSocket.onerror = function (event) { console.info("發(fā)生錯(cuò)誤" ); alert("websocket發(fā)生錯(cuò)誤" + event); } //關(guān)閉連接 webSocket.onclose = function () { console.info("關(guān)閉連接" ); } //監(jiān)聽窗口關(guān)閉 window.onbeforeunload = function (event) { webSocket.close(); } </script> </head> <body> </body> </html>
基本上,數(shù)據(jù)推送的功能就完成了,下面附上效果圖。
啟動(dòng)tomcat。后臺(tái)定時(shí)器兩秒刷新一次,判斷是否有websocket連接。
登錄頁面:
數(shù)據(jù)推送頁面:
服務(wù)器定時(shí)向客戶端推送數(shù)據(jù)的功能就完成了,有不明白的可以給博主留言,如果有什么錯(cuò)誤,也希望各位朋友指出,謝謝大家。
本文源碼:
https://github.com/Qingfengchuiwoxin/websocket