-
引入
- 普通请求-响应方式:例如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 + '}'; } }