隨着互聯網的發展,傳統的HTTP協議已經很難知足Web應用日益複雜的需求了。近年來,隨着HTML5的誕生,WebSocket協議被提出,它實現了瀏覽器與服務器的全雙工通訊,擴展了瀏覽器與服務端的通訊功能,使服務端也能主動向客戶端發送數據。javascript
咱們知道,傳統的HTTP協議是無狀態的,每次請求(request)都要由客戶端(如 瀏覽器)主動發起,服務端進行處理後返回response結果,而服務端很難主動向客戶端發送數據;這種客戶端是主動方,服務端是被動方的傳統Web模式 對於信息變化不頻繁的Web應用來講形成的麻煩較小,而對於涉及實時信息的Web應用卻帶來了很大的不便,如帶有即時通訊、實時數據、訂閱推送等功能的應 用。在WebSocket規範提出以前,開發人員若要實現這些實時性較強的功能,常常會使用折衷的解決方法:輪詢(polling)和Comet技術。其實後者本質上也是一種輪詢,只不過有所改進。html
輪詢是最原始的實現實時Web應用的解決方案。輪詢技術要求客戶端以設定的時間間隔週期性地向服務端發送請求,頻繁地查詢是否有新的數據改動。明顯地,這種方法會致使過多沒必要要的請求,浪費流量和服務器資源。java
Comet技術又能夠分爲長輪詢和流技術。長輪詢改進了上述的輪詢技術,減少了無用的請求。它會爲某些數據設定過時時間,當數據過時後纔會向服務端發送請求;這種機制適合數據的改動不是特別頻繁的狀況。流技術一般是指客戶端使用一個隱藏的窗口與服務端創建一個HTTP長鏈接,服務端會不斷更新鏈接狀態以保持HTTP長鏈接存活;這樣的話,服務端就能夠經過這條長鏈接主動將數據發送給客戶端;流技術在大併發環境下,可能會考驗到服務端的性能。jquery
這兩種技術都是基於請求-應答模式,都不算是真正意義上的實時技術;它們的每一次請求、應答,都浪費了必定流量在相同的頭部信息上,而且開發複雜度也較大。web
伴隨着HTML5推出的WebSocket,真正實現了Web的實時通訊,使B/S模式具有了C/S模式的實時通訊能力。WebSocket的工做流程是這 樣的:瀏覽器經過JavaScript向服務端發出創建WebSocket鏈接的請求,在WebSocket鏈接創建成功後,客戶端和服務端就能夠經過 TCP鏈接傳輸數據。由於WebSocket鏈接本質上是TCP鏈接,不須要每次傳輸都帶上重複的頭部數據,因此它的數據傳輸量比輪詢和Comet技術小 了不少。本文不詳細地介紹WebSocket規範,主要介紹下WebSocket在Java Web中的實現。spring
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以上的版本才能運行。apache
pom.xml:json
<?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:api
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>