spring websocket 和socketjs實現單聊群聊,廣播的消息推送詳解
spring websocket 和socketjs實現單聊群聊,廣播的消息推送詳解
WebSocket簡單介紹
隨著互聯網的發展,傳統的HTTP協議已經很難滿足Web應用日益復雜的需求了。近年來,隨著HTML5的誕生,WebSocket協議被提出,它實現了瀏覽器與服務器的全雙工通信,擴展了瀏覽器與服務端的通信功能,使服務端也能主動向客戶端發送數據。
我們知道,傳統的HTTP協議是無狀態的,每次請求(request)都要由客戶端(如 瀏覽器)主動發起,服務端進行處理後返回response結果,而服務端很難主動向客戶端發送數據;這種客戶端是主動方,服務端是被動方的傳統Web模式 對於信息變化不頻繁的Web應用來說造成的麻煩較小,而對於涉及實時信息的Web應用卻帶來了很大的不便,如帶有即時通信、實時數據、訂閱推送等功能的應 用。在WebSocket規範提出之前,開發人員若要實現這些實時性較強的功能,經常會使用折衷的解決方法:輪詢(polling)
輪詢是最原始的實現實時Web應用的解決方案。輪詢技術要求客戶端以設定的時間間隔周期性地向服務端發送請求,頻繁地查詢是否有新的數據改動。明顯地,這種方法會導致過多不必要的請求,浪費流量和服務器資源。
Comet技術又可以分為長輪詢和流技術。長輪詢改進了上述的輪詢技術,減小了無用的請求。它會為某些數據設定過期時間,當數據過期後才會向服務端發送請求;這種機制適合數據的改動不是特別頻繁的情況。流技術通常是指客戶端使用一個隱藏的窗口與服務端建立一個HTTP長連接,服務端會不斷更新連接狀態以保持HTTP長連接存活;這樣的話,服務端就可以通過這條長連接主動將數據發送給客戶端;流技術在大並發環境下,可能會考驗到服務端的性能。
這兩種技術都是基於請求-應答模式,都不算是真正意義上的實時技術;它們的每一次請求、應答,都浪費了一定流量在相同的頭部信息上,並且開發復雜度也較大。
伴隨著HTML5推出的WebSocket,真正實現了Web的實時通信,使B/S模式具備了C/S模式的實時通信能力。WebSocket的工作流程是這 樣的:瀏覽器通過JavaScript向服務端發出建立WebSocket連接的請求,在WebSocket連接建立成功後,客戶端和服務端就可以通過 TCP連接傳輸數據。因為WebSocket連接本質上是TCP連接,不需要每次傳輸都帶上重復的頭部數據,所以它的數據傳輸量比輪詢和Comet技術小 了很多。本文不詳細地介紹WebSocket規範,主要介紹下WebSocket在Java Web中的實現。
JavaEE 7中出了JSR-356:Java API for WebSocket規範。不少Web容器,如Tomcat,Nginx,Jetty等都支持WebSocket。Tomcat從7.0.27開始支持 WebSocket,從7.0.47開始支持JSR-356,下面的Demo代碼也是需要部署在Tomcat7.0.47以上的版本才能運行。
項目結構圖:
相關代碼:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com</groupId>
<artifactId>websocket-singlechat</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>websocket-singlechat Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.7</version>
</dependency>
</dependencies>
<build>
<finalName>websocket-singlechat</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
ChatSocket:
package com.home.chat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import com.google.gson.Gson;
import com.home.vo.ContentVo;
import com.home.vo.Message;
/**
* 總通信管道
*
*/
@ServerEndpoint("/chatSocket")
public class ChatSocket {
//定義一個全局變量集合sockets,用戶存放每個登錄用戶的通信管道
private static Set<ChatSocket> sockets=new HashSet<ChatSocket>();
//定義一個全局變量Session,用於存放登錄用戶的用戶名
private Session session;
//定義一個全局變量map,key為用戶名,該用戶對應的session為value
private static Map<String, Session> map=new HashMap<String, Session>();
//定義一個數組,用於存放所有的登錄用戶,顯示在聊天頁面的用戶列表欄中
private static List<String>names=new ArrayList<String>();
private String username;
private Gson gson=new Gson();
/*
* 監聽用戶登錄
*/
@OnOpen
public void open(Session session){
System.out.println("建立了一個socket通道" + session.getId());
this.session = session;
//將當前連接上的用戶session信息全部存到scokets中
sockets.add(this);
//拿到URL路徑後面所有的參數信息
String queryString = session.getQueryString();
System.out.println();
//截取=後面的參數信息(用戶名),將參數信息賦值給全局的用戶名
this.username = queryString.substring(queryString.indexOf("=")+1);
//每登錄一個用戶,就將該用戶名存入到names數組中,用於刷新好友列表
names.add(this.username);
//將當前登錄用戶以及對應的session存入到map中
this.map.put(this.username, this.session);
System.out.println("用戶"+this.username+"進入聊天室");
Message message = new Message();
message.setAlert("用戶"+this.username+"進入聊天室");
//將當前所有登錄用戶存入到message中,用於廣播發送到聊天頁面
message.setNames(names);
//將聊天信息廣播給所有通信管道(sockets)
broadcast(sockets, gson.toJson(message) );
}
/*
* 退出登錄
*/
@OnClose
public void close(Session session){
//移除退出登錄用戶的通信管道
sockets.remove(this);
//將用戶名從names中剔除,用於刷新好友列表
names.remove(this.username);
Message message = new Message();
System.out.println("用戶"+this.username+"退出聊天室");
message.setAlert(this.username+"退出當前聊天室!!!");
//刷新好友列表
message.setNames(names);
broadcast(sockets, gson.toJson(message));
}
/*
* 接收客戶端發送過來的消息,然後判斷是廣播還是單聊
*/
@OnMessage
public void receive(Session session,String msg) throws IOException{
//將客戶端消息轉成json對象
ContentVo vo = gson.fromJson(msg, ContentVo.class);
//如果是群聊,就像消息廣播給所有人
if(vo.getType()==1){
Message message = new Message();
message.setDate(new Date().toLocaleString());
message.setFrom(this.username);
message.setSendMsg(vo.getMsg());
broadcast(sockets, gson.toJson(message));
}else{
Message message = new Message();
message.setDate(new Date().toLocaleString());
message.setFrom(this.username);
message.setAlert(vo.getMsg());
message.setSendMsg("<font color=red>正在私聊你:</font>"+vo.getMsg());
String to = vo.getTo();
//根據單聊對象的名稱拿到要單聊對象的Session
Session to_session = this.map.get(to);
//如果是單聊,就將消息發送給對方
to_session.getBasicRemote().sendText(gson.toJson(message));
}
}
/*
* 廣播消息
*/
public void broadcast(Set<ChatSocket>sockets ,String msg){
//遍歷當前所有的連接管道,將通知信息發送給每一個管道
for(ChatSocket socket : sockets){
try {
//通過session發送信息
socket.session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ServerConfig:
package com.home.config;
import java.util.Set;
import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig;
/**
* 項目啟動時會自動啟動,類似與ContextListener.
* 是webSocket的核心配置類。
*
*/
public class ServerConfig implements ServerApplicationConfig {
//掃描src下所有類@ServerEndPoint註解的類。
@Override
public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scan) {
System.out.println("掃描到"+scan.size()+"個服務端程序");
return scan;
}
//獲取所有以接口方式配置的webSocket類。
@Override
public Set<ServerEndpointConfig> getEndpointConfigs(
Set<Class<? extends Endpoint>> point) {
System.out.println("實現EndPoint接口的類數量:"+point.size());
return null;
}
}
LoginServlet:
package com.home.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class LoginServlet extends HttpServlet {
public void doGet(HttpServletRequest request,
HttpServletResponse response)throws IOException,ServletException {
}
public void doPost(HttpServletRequest request,
HttpServletResponse response)throws IOException,ServletException{
String username = request.getParameter("username");
System.out.println("doPost當前登錄用戶為"+username);
request.getSession().setAttribute("username",username);
//這裏只是簡單地模擬登錄,登陸之後直接跳轉到聊天頁面
response.sendRedirect("chat.jsp");
}
}
ContentVo:
package com.home.vo;
/**
* 客戶端發送給服務端消息實體
*
*/
public class ContentVo {
private String to;
private String msg;
private Integer type;
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
}
Message:
package com.home.vo;
import java.util.Date;
import java.util.List;
/**
* 服務端發送給客戶端消息實體
*
*/
public class Message {
private String alert; //
private List<String> names;
private String sendMsg;
private String from;
private String date;
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getSendMsg() {
return sendMsg;
}
public void setSendMsg(String sendMsg) {
this.sendMsg = sendMsg;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getAlert() {
return alert;
}
public void setAlert(String alert) {
this.alert = alert;
}
public List<String> getNames() {
return names;
}
public void setNames(List<String> names) {
this.names = names;
}
public Message() {
super();
}
}
web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>Archetype Created Web Application</display-name>
<servlet>
<description></description>
<display-name>LoginServlet</display-name>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.home.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/LoginServlet</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
chat.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>
<script type="text/javascript" src="./jquery-3.3.1/jquery-3.3.1.js"></script>
<script type="text/javascript">
var ws;
var userName='${sessionScope.username}';
//通過URL請求服務端(chat為項目名稱)
var url = "ws://localhost:8080/chatSocket?username="+userName;
//進入聊天頁面就是一個通信管道
window.onload = function() {
console.log(url);
if ('WebSocket' in window) {
ws = new WebSocket(url);
} else if ('MozWebSocket' in window) {
ws = new MozWebSocket(url);
} else {
alert('WebSocket is not supported by this browser.');
return;
}
ws.onopen=function(){
// showMsg("webSocket通道建立成功!!!");
console.log("webSocket通道建立成功!!!");
};
//監聽服務器發送過來的所有信息
ws.onmessage = function(event) {
eval("var result=" + event.data);
//如果後臺發過來的alert不為空就顯示出來
if (result.alert != undefined) {
$("#content").append(result.alert + "<br/>");
}
//如果用戶列表不為空就顯示
if (result.names != undefined) {
//刷新用戶列表之前清空一下列表,免得會重復,因為後臺只是單純的添加
$("#userList").html("");
$(result.names).each(
function() {
$("#userList").append(
"<input type=checkbox value='"+this+"'/>"
+ this + "<br/>");
});
}
//將用戶名字和當前時間以及發送的信息顯示在頁面上
if (result.from != undefined) {
$("#content").append(
result.from + " " + result.date + " 說:<br/>"
+ result.sendMsg + "<br/>");
}
};
};
//將消息發送給後臺服務器
function send() {
//拿到需要單聊的用戶名
//alert("當前登錄用戶為"+userName);
var ss = $("#userList :checked");
console.log("ss==>"+ss);
console.log(" ss.length()=="+ss.length);
//alert("群聊還是私聊"+ss.size());
var to = $('#userList :checked').val();
if (to == userName) {
alert("你不能給自己發送消息啊");
return;
}
//根據勾選的人數確定是群聊還是單聊
var value = $("#msg").val();
//alert("消息內容為"+value);
var object = null;
if (ss.length == 0) {
object = {
msg : value,
type : 1, //1 廣播 2單聊
};
} else {
object = {
to : to,
msg : value,
type : 2, //1 廣播 2單聊
};
}
//將object轉成json字符串發送給服務端
var json = JSON.stringify(object);
//alert("str="+json);
ws.send(json);
//消息發送後將消息欄清空
$("#msg").val("");
}
</script>
</head>
<body>
<h3>歡迎 ${sessionScope.username }使用本聊天系統!!</h3>
<div id="content"
style="border: 1px solid black; width: 400px; height: 300px; float: left; color: #7f3f00;"></div>
<div id="userList"
style="border: 1px solid black; width: 120px; height: 300px; float: left; color: #00ff00;"></div>
<div style="clear: both;" style="color:#00ff00">
<input id="msg" />
<button onclick="send();">發送消息</button>
</div>
</body>
</html>
login.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>
<script type="text/javascript" src="jquery-1.4.4.min.js"></script>
</head>
<body>
<form name="ff" action="LoginServlet" method="post" >
用戶名:<input name="username" /><br/>
<input type="submit" value="登錄"/>
</form>
</body>
</html>
項目演示:
spring websocket 和socketjs實現單聊群聊,廣播的消息推送詳解