-
引入
- 普通請求-響應方式:例如Servlet中HttpServletRequest和HttpServletResponse相互配合先接受請求、解析數據,再發出響應,處理完成后連接便斷開了,沒有數據的實時性可言。
- Ajax輪詢:客戶端定時發送多次Ajax請求,服務器不斷響應,時間頻率極小,雖然實時性有了卓越提高,但是大多數的請求是沒有意義的。
- WebSocket長連接:客戶端只需要向服務器發送一次Http請求,與服務器建立一個以sessioId標示的channel,便可以與服務器在自己的管道中實時通訊,連接是不斷開的。在此介紹一個基於WebSocket的框架GoEasy,非常的方便簡單,大家可以用來實現消息推送、實時聊天等功能。
-
Demo介紹
- 基於WebSocket的聊天室,可以發送接受消息並實時查看在線用戶。
-
Maven依賴
<?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.atguigu</groupId> <artifactId>spring-boot-websocket-02</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-boot-websocket-02</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.4</version> <classifier>jdk15</classifier> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.7.0</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.5</version> </dependency> <dependency> <groupId>net.sf.ezmorph</groupId> <artifactId>ezmorph</artifactId> <version>1.0.3</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
WebSocket配置文件
- 后台基於SpringBoot非常的方便,只要編寫配置類即可,注入的實體Bean為方法名。SpringBoot推薦Thymeleaf進行html渲染,但是老師說Thymeleaf相對於Jsp等其他渲染工具性能較差,咱也不知道為什么。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class MyWebSocketConfig extends WebMvcConfigurerAdapter { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login").setViewName("login"); } }
-
JavaScript
- 客戶端通過js發送socket請求建立連接,連接成功后建立管道。通過onopen、onclose、onmessage等回調函數接收服務器響應的反饋。
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>聊天頁面</title> </head> <body> <div class="container"> <div class="left"> <div class="top"></div> <div class="bottom"> <div class="content"> <input type="text" name="content" id="content" value="輸入文本內容"> </div> <input type="button" value="發送" id="send"> </div> </div> <div class="right"></div> </div> </body> </html>
<script th:inline="javascript"> window.onload=function(){ var username=[[${username}]]; if("WebSocket" in window){ var webSocket=new WebSocket("ws://10.7.84.48:8080/mywebsocket?username="+username); //type 0上線 1下線 2聊天信息 3拉取在線用戶 webSocket.onopen=function(e){ var data='{"type":"0","username":"'+username+'","content":""}'; webSocket.send(data); } window.onbeforeunload=function(){ var data='{"type":"1","username":"'+username+'","content":""}'; webSocket.send(data); webSocket.close(); } document.getElementById("send").onclick=function (ev) { var content=document.getElementById("content").value; var data='{"type":"2","username":"'+username+'","content":"'+content+'"}'; webSocket.send(data); document.getElementsByClassName("top")[0] .innerHTML+="<p><font color='red'>我: </font><font color='#8a2be2'>"+content+"</font></p>"; document.getElementById("content").value=""; var scrollDiv = document.getElementsByClassName('top')[0]; scrollDiv.scrollTop = scrollDiv.scrollHeight; } webSocket.onmessage=function (ev) { var data=ev.data; var obj=eval('('+data+')'); var type=obj.type; switch (type) { case 0: document.getElementsByClassName("right")[0] .innerHTML+="<p id="+obj.senSessionId+"><font color='blue'>"+obj.senName+"</font></p>"; break; case 1: var id=obj.senSessionId; var parent=document.getElementsByClassName("right")[0]; var child=document.getElementById(id); parent.removeChild(child); break; case 2: document.getElementsByClassName("top")[0] .innerHTML+="<p>"+obj.time.hours+":"+obj.time.minutes+":"+obj.time.seconds+" <font color='#4169e1'>"+obj.senName+": </font><font color='#8a2be2'>"+obj.content+"</font></p>"; var scrollDiv = document.getElementsByClassName('top')[0]; scrollDiv.scrollTop = scrollDiv.scrollHeight; break; case 3: var map=obj.map; for(var key in map){ document.getElementsByClassName("right")[0] .innerHTML+="<p id="+key+"><font color='blue'>"+map[key]+"</font></p>"; } break; default: } } } } </script>
<style> .container{ width: 700px; height: 500px; border: 1px solid black; margin: 0px auto; } .left{ width: 75%; height: 100%; float: left; } .right{ width: 20%; height: 100%; float: left; border-left: 1px solid black; } .top{ width: 100%; height: 75%; overflow-y: scroll; } .bottom{ width: 100%; height: 25%; border-top: 1px solid black; } .content{ width: 100%; height: 65%; border-bottom: 1px solid black; } #content{ width: 99%; height: 92%; } #send{ float: right; width: 70px; height: 42px; } </style>
-
WebSocket服務端代碼
import com.atguigu.springbootwebsocket02.bean.Msg;
import net.sf.json.JSONObject;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
@Component @ServerEndpoint("/mywebsocket") public class WebSocketListener { private static int onlineCount=0; private static synchronized int getOnlineCount(){ return onlineCount; } private static synchronized int addOnlineCount(){ return ++onlineCount; } private static synchronized int subOnlineCount(){ return --onlineCount; } //用於區分每個WebSocket session.getId() private Session session; private String username; private static CopyOnWriteArraySet<WebSocketListener>webSocketListeners =new CopyOnWriteArraySet<>(); @OnOpen public void onOpen(Session session){ this.session=session; webSocketListeners.add(this); WebSocketListener.addOnlineCount(); try { this.username= URLDecoder.decode(session.getQueryString().split("=")[1],"utf8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } //獲取在線用戶信息 try { Map<String,String>map=new HashMap<>(); for(WebSocketListener webSocketListener :webSocketListeners){ if(!webSocketListener.session.getId().equals(this.session.getId())){ map.put(webSocketListener.session.getId(),webSocketListener.username); } } JSONObject jm=JSONObject.fromObject(map); JSONObject jsonObject=new JSONObject(); jsonObject.put("type",3); jsonObject.put("map",jm); this.session.getBasicRemote().sendText(jsonObject.toString()); }catch (Exception e){ e.printStackTrace(); } } @OnClose public void onClose(Session session){ webSocketListeners.remove(this); WebSocketListener.subOnlineCount(); } private void broadcast(String data){ for(WebSocketListener webSocketListener :webSocketListeners){ try { if(!webSocketListener.session.getId().equals(this.session.getId())){ webSocketListener.session.getBasicRemote().sendText(data); } }catch (Exception e){ e.printStackTrace(); } } } @OnMessage public void onMessage(String data,Session session){ JSONObject jsonObject=JSONObject.fromObject(data); Msg msg=new Msg(); msg.setSenName(jsonObject.getString("username")); msg.setSenSessionId(session.getId()); msg.setType(Integer.parseInt(jsonObject.getString("type"))); msg.setTime(new Date()); msg.setContent(jsonObject.getString("content")); JSONObject broadcast=JSONObject.fromObject(msg); broadcast(broadcast.toString()); } }
-
其他代碼
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登錄頁面</title> </head> <body> <form action="/enter" method="get"> <table border="1px"> <tr> <td>User:</td> <td><input type="text" name="username" value="輸入用戶名"/></td> </tr> <tr> <td colspan="2"><input type="submit" value="登錄"/></td> </tr> </table> </form> </body> </html>
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @Controller public class MyWebSocketHandler { @RequestMapping(value = "/enter",method = RequestMethod.GET) public String enterChat( Model model, @RequestParam(name="username",required = false)String username){ model.addAttribute("username",username); return "index"; } }
import java.util.Date; public class Msg { private String senSessionId; private String senName; private String recSessionId=""; private String recName=""; private Date time=new Date(); private String content; private Integer type; public Integer getType() { return type; } public void setType(Integer type) { this.type = type; } public String getSenSessionId() { return senSessionId; } public void setSenSessionId(String senSessionId) { this.senSessionId = senSessionId; } public String getSenName() { return senName; } public void setSenName(String senName) { this.senName = senName; } public String getRecSessionId() { return recSessionId; } public void setRecSessionId(String recSessionId) { this.recSessionId = recSessionId; } public String getRecName() { return recName; } public void setRecName(String recName) { this.recName = recName; } public Date getTime() { return time; } public void setTime(Date time) { this.time = time; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public String toString() { return "Msg{" + "senSessionId='" + senSessionId + '\'' + ", senName='" + senName + '\'' + ", recSessionId='" + recSessionId + '\'' + ", recName='" + recName + '\'' + ", time=" + time + ", content='" + content + '\'' + ", type=" + type + '}'; } }