簡單的說,websocket是真正實現了全雙工通信的伺服器向客戶端推的網際網路技術。
全雙工與單工、半雙工的區別?
全雙工:簡單地說,就是可以同時進行信號的雙向傳輸(A->B且B->A),是瞬時同步的。推送和拉取的區別?
推:由伺服器主動發消息給客戶端,就像廣播。優勢在於,信息的主動性和及時性。實現消息通信的幾種方式?
接下來我們主要講第三種,使用websocket協議,來實現服務端定時向客戶端推送消息。
後臺:springmvc、websocket、quartz實現步驟一、環境搭建(1)導入相關約束:在pom文件中加入需要的約束,spring相關的約束,請各位自己導入,這裡我就不貼出來了。
<!-- 定時器的包 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<!--
spring-support.jar 這個jar 文件包含支持UI模版(Velocity,FreeMarker,JasperReports),郵件服務,腳本服務(JRuby),緩存Cache(EHCache),任務計劃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:如果使用原始的配置方式,需要導入spring-websocket、spring-messaging的包,我們這裡就通過註解實現
-->
(2)配置xml文件web.xml中就配置前端控制器,大家自行配置。然後,加載springmvc的配置文件。
springmvc.xml文件中
<!-- 自動將控制器加載到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>
<!-- 自動註冊 DefaultAnnotationHandlerMapping 與 AnnotationMethodHandlerAdapter 兩個 bean, 解決了 @Controller 註解的使用前提配置 -->
<mvc:annotation-driven/>
<!-- 使用fastjson 解析json 因為本人的項目中用到了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>到此,環境就基本搭建完成了。
二、完成後臺的功能這裡我就直接貼出代碼了,上面有相關的注釋。
首先,完成websocket的實現類。
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: 清風一陣吹我心
* @ProjectName: socket
* @Package: com.socket.web.socket
* @ClassName: WebSocketServer
* @Description:
* @Version: 1.0
**/
//ServerEndpoint它的功能主要是將目前的類定義成一個websocket伺服器端。註解的值將被用於監聽用戶連接的終端訪問URL地址。
@ServerEndpoint(value = "/socket/{ip}")
@Component
public class WebSocketServer {
//使用slf4j打日誌
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketServer.class);
//用來記錄當前在線連接數
private static int onLineCount = 0;
//用來存放每個客戶端對應的WebSocketServer對象
private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<String, WebSocketServer>();
//某個客戶端的連接會話,需要通過它來給客戶端發送數據
private Session session;
//客戶端的ip地址
private String ip;
/**
* 連接建立成功,調用的方法,與前臺頁面的onOpen相對應
* @param ip ip地址
* @param session 會話
*/
@OnOpen
public void onOpen(@PathParam("ip")String ip,Session session){
//根據業務,自定義邏輯實現
this.session = session;
this.ip = ip;
webSocketMap.put(ip,this); //將當前對象放入map中
addOnLineCount(); //在線人數加一
LOGGER.info("有新的連接加入,ip:{}!當前在線人數:{}",ip,getOnLineCount());
}
/**
* 連接關閉調用的方法,與前臺頁面的onClose相對應
* @param ip
*/
@OnClose
public void onClose(@PathParam("ip")String ip){
webSocketMap.remove(ip); //根據ip(key)移除WebSocketServer對象
subOnLineCount();
LOGGER.info("WebSocket關閉,ip:{},當前在線人數:{}",ip,getOnLineCount());
}
/**
* 當伺服器接收到客戶端發送的消息時所調用的方法,與前臺頁面的onMessage相對應
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message,Session session){
//根據業務,自定義邏輯實現
LOGGER.info("收到客戶端的消息:{}",message);
}
/**
* 發生錯誤時調用,與前臺頁面的onError相對應
* @param session
* @param error
*/
@OnError
public void onError(Session session,Throwable error){
LOGGER.error("WebSocket發生錯誤");
error.printStackTrace();
}
/**
* 給當前用戶發送消息
* @param message
*/
public void sendMessage(String message){
try{
//getBasicRemote()是同步發送消息,這裡我就用這個了,推薦大家使用getAsyncRemote()異步
this.session.getBasicRemote().sendText(message);
}catch (IOException e){
e.printStackTrace();
LOGGER.info("發送數據錯誤:,ip:{},message:{}",ip,message);
}
}
/**
* 給所有用戶發消息
* @param message
*/
public static void sendMessageAll(final String message){
//使用entrySet而不是用keySet的原因是,entrySet體現了map的映射關係,遍歷獲取數據更快。
Set<Map.Entry<String, WebSocketServer>> entries = webSocketMap.entrySet();
for (Map.Entry<String, WebSocketServer> entry : entries) {
final WebSocketServer webSocketServer = entry.getValue();
//這裡使用線程來控制消息的發送,這樣效率更高。
new Thread(new Runnable() {
public void run() {
webSocketServer.sendMessage(message);
}
}).start();
}
}
/**
* 獲取當前的連接數
* @return
*/
public static synchronized int getOnLineCount(){
return WebSocketServer.onLineCount;
}
/**
* 有新的用戶連接時,連接數自加1
*/
public static synchronized void addOnLineCount(){
WebSocketServer.onLineCount++;
}
/**
* 斷開連接時,連接數自減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;
}
}然後寫我們的定時器(quartz),這裡我就不詳解定時器了。大家可以自行去了解。
這裡我使用的是xml註解的方式,創建一個job類,此類不需要繼承任何類和實現任何接口。
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: 清風一陣吹我心
* @ProjectName: socket
* @Package: com.socket.web.quartz
* @ClassName: TestJob
* @Description:
* @Version: 1.0
**/
public class TestJob {
public void task(){
//獲取WebSocketServer對象的映射。
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("每隔兩秒,向客戶端推送一次數據");
}catch (IOException e){
e.printStackTrace();
}
}
}else {
System.out.println("WebSocket未連接");
}
}
}定時器的實現類就完成了,我們還需要在springmvc.xml中進行配置
springmvc.xml配置:
<!-- 要執行的任務類 -->
<bean id="testJob" class="com.socket.web.quartz.TestJob"></bean>
<!-- 將需要執行的定時任務注入job中 -->
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="testJob"/>
<!-- 任務類中需要執行的方法 -->
<property name="targetMethod" value="task"></property>
<!-- 上一次未執行完成的,要等待有再執行。 -->
<property name="concurrent" value="false" />
</bean>
<!-- 基本的定時器,會綁定具體的任務。 -->
<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層的代碼,就一個登錄的功能。
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: 清風一陣吹我心
* @ProjectName: socket
* @Package: com.socket.web
* @ClassName: ChatController
* @Description:
* @CreateDate: 2018/11/9 11:04
* @Version: 1.0
**/
@RequestMapping("socket")
@Controller
public class ChatController {
/**
* 跳轉到登錄頁面
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String goLogin(){
return "login";
}
/**
* 跳轉到聊天頁面
* @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";
}
}以上就是登錄的代碼了,基本上就是偽代碼,只要輸入用戶名就可以了,後面的邏輯,大家可以根據自己的業務來實現。
最後就是前臺頁面的設計了,登錄,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">
//判斷當前瀏覽器是否支持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已經連接");
}
//接收推送的消息
webSocket.onmessage = function (event) {
console.info(event);
alert(event.data);
}
//錯誤時
webSocket.onerror = function (event) {
console.info("發生錯誤");
alert("websocket發生錯誤" + event);
}
//關閉連接
webSocket.onclose = function () {
console.info("關閉連接");
}
//監聽窗口關閉
window.onbeforeunload = function (event) {
webSocket.close();
}
</script>
</head>
<body>
</body>
</html>基本上,數據推送的功能就完成了,下面附上效果圖。
啟動tomcat。後臺定時器兩秒刷新一次,判斷是否有websocket連接。
登錄頁面:
數據推送頁面:
伺服器定時向客戶端推送數據的功能就完成了,有不明白的可以給博主留言,如果有什麼錯誤,也希望各位朋友指出,謝謝大家。
本文源碼:
https://github.com/Qingfengchuiwoxin/websocket