Websocket --(3)实现
今天介绍另外一种websocket实现方式,结合了spring MVC,并完善了第二节所提到做一个简单的登录认证用来识别用户的名称。界面继续沿用第二节的布局样式,同时增加上线和下线功能。
参考了 https://blog.csdn.net/mybook201314/article/details/70173674 这篇文章,但是同时做了部分改进。更多内容请看https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#websocket 这是spring 官方文档,相当详细,我做的过程中更多的是看官方介绍。
1.环境
Eclisp + jdk1.7 +tomcat 8,低版本的Tomcat7可能跑不起来,项目采用maven构建。
2.jar 包(只写websocket部分,spring 核心包就不写了)
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>4.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId> <version>4.0.2.RELEASE</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.1.23</version> </dependency>
这里添加了阿里巴巴的fastjson来处理json字符串。当然不用这个也可以。
web.xml 配置如下:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <absolute-ordering /> <display-name>websocket2</display-name> <welcome-file-list> <welcome-file>login.jsp</welcome-file> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> <welcome-file>default.html</welcome-file> <welcome-file>default.htm</welcome-file> <welcome-file>default.jsp</welcome-file> </welcome-file-list> <!-- Spring配置 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-content.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- Spring MVC配置 --> <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 可以自定义servlet.xml配置文件的位置和名称,默认为WEB-INF目录下,名称为[<servlet-name>]-servlet.xml,如spring-servlet.xml --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <async-supported>true</async-supported> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 中文过滤器 --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.css</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.gif</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.jpg</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.js</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>*.html</url-pattern> </servlet-mapping> </web-app>
web.xml 里面有三点需要注意:命名空间要3.0及以上,添加配置文件开头添加<absolute-ordering />,具体可参看官方文档 Deployment 这个章节。
构建完成的项目如下图所示
没有用的文件请自行忽略
3.实现
首先新建一个自己的websocket处理类,这个类需要实现WebSocketHandler
这个接口,当然根据官方文档 里面的这句话Creating a WebSocket server is as simple as implementing WebSocketHandler
or more likely extending either TextWebSocketHandler
or BinaryWebSocketHandler 也可以实现
TextWebSocketHandler或者BinaryWebSocketHandler。这个接口里面有几个方法分别对应连接前,连接成功后,和消息处理,关闭连接等操作。
package com.lzl.ws; import java.util.ArrayList; import java.util.Map; import org.apache.log4j.Logger; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketMessage; import org.springframework.web.socket.WebSocketSession; import com.alibaba.fastjson.JSON; //websocket处理类 public class MyWebSocketHandler implements WebSocketHandler{ private static final Logger log = Logger.getLogger(MyWebSocketHandler.class); // 保存所有的用户session private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>(); //连接关闭后 @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { // TODO Auto-generated method stub log.info("已经关闭连接。。。。"); users.remove(session); } //连接就绪后 @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { log.info("已经成功连接。。。sessionID是"+session.getId()); users.add(session); } //处理信息 @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { log.info("收到消息。。。sessionID:"+session.getId()); log.info("消息内容:"+message.getPayload().toString()); Map<String,Object > map = JSON.parseObject(message.getPayload().toString(),Map.class); TextMessage textMessage = new TextMessage(map.get("userID").toString()+":"+map.get("msgContent").toString(),true); log.info("转发:"+textMessage.getPayload().toString()); for(WebSocketSession user:users){ user.sendMessage(textMessage); } log.info("发送完成。。。。"); } //发生错误 @Override public void handleTransportError(WebSocketSession session, Throwable throwable) throws Exception { // TODO Auto-generated method stub log.info("发生错误。。。。"+throwable.getMessage()+"*****SessionID"+session.getId()); } @Override public boolean supportsPartialMessages() { // TODO Auto-generated method stub return false; } }
我这里是把消息发送给所有已经连接的客户了
接下来新建websocket注册类
package com.lzl.ws; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; @Configuration @EnableWebMvc @EnableWebSocket public class MyWebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer{ @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { // TODO Auto-generated method stub registry.addHandler(new MyWebSocketHandler(), "/websocket").addInterceptors(new HttpSessionHandshakeInterceptor()); } }
这个类一定 要在spring MVC 自动扫描的包下面。其中 HttpSessionHandshakeInterceptor 类称为握手操作类,可以自己新建一个类去继承他,这里面可以根据需要写一些建立连接前,和建立连接后的动作,我这里暂时没有用到,所有就直接调用父类了。
最后一步,在spring 配置文件中配置websocket
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <!-- 引入jdbc配置文件 --> <!-- <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:application.properties</value> </list> </property> </bean> --> <context:property-placeholder location="classpath:application.properties"/> <!-- 自动扫描注解的bean --> <context:component-scan base-package="com.lzl" /> <!-- 配置数据源 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.user}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置Mybatis的文件 ,mapperLocations配置**Mapper.xml文件位置,configLocation配置mybatis-config文件位置--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="configLocation" value="classpath:mybatis-config.xml" /> </bean> <!-- 自动扫描了所有的XxxxMapper.xml对应的mapper接口文件,这样就不用一个一个手动配置Mpper的映射了,只要Mapper接口类和Mapper映射文件对应起来就可以了。 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.lzl.dao" /> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property> </bean> <bean id = "Handler" class = "com.lzl.ws.MyWebSocketHandler"></bean> <websocket:handlers> <websocket:mapping path="/websocket" handler="Handler"/> <websocket:handshake-interceptors> <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/> </websocket:handshake-interceptors> </websocket:handlers> </beans>
这里需要注意的是在在头文件添加webcoket支持。对于不支持的websocket的浏览器可以参照官方文档的 SockJS Fallback 这个章节处理,或者参考https://blog.csdn.net/mybook201314/article/details/70173674
至此,websocket 相关内容完成,接着做登录操作。登录这里很简单,直接将用户名称和聊天页面的地址返回。
package com.lzl.controller; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; import com.lzl.dao.User; @Controller @RequestMapping("/login") public class LoginController { @RequestMapping(value = "/setUser.do",method=RequestMethod.POST) public ModelAndView login(User user ,HttpServletRequest request,HttpServletResponse response ){ HttpSession session = request.getSession(); session.setAttribute("user", user); ModelAndView mv = new ModelAndView(); mv.addObject("user", user); mv.setViewName("chat"); return mv; } @RequestMapping(value = "/getLogin.do",method=RequestMethod.GET) public ModelAndView getlogin(){ System.out.println("-----------------------"); ModelAndView mv = new ModelAndView(); mv.setViewName("login"); return mv; } }
前端界面有两个,分别是登录界面和聊天界面,都是用bootstrap做的
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>用户登录</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="renderer" content="webkit"> <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script> <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css"> <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap-theme.min.css"> <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script> <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <style type="text/css"> body { background-color:#f8f6e9; } .mycenter{ margin-top: 100px; margin-left: auto; margin-right: auto; height: 350px; width:500px; padding: 5%; padding-left: 5%; padding-right: 5%; } .mycenter mysign{ width: 440px; } .mycenter input,checkbox,button{ margin-top:2%; margin-left: 10%; margin-right: 10%; } .mycheckbox{ margin-top:10px; margin-left: 40px; margin-bottom: 10px; height: 10px; } </style> <script type="text/javascript"> /* $(function(){ $("#submit").click(function(){ var data = $("#loginform").serialize(); $.post("${pageContext.request.contextPath}/login/setUser.do",data,function(result){ $("span").html(result); }); }); }); */ </script> </head> <body> <form id="loginform" method = "post" action="${pageContext.request.contextPath}/login/setUser.do"> <div class="mycenter"> <div class="mysign"> <div class="col-lg-11 text-center text-info"> <h2>请登录</h2> </div> <div class="col-lg-10"> <input type="text" class="form-control" id = "username" name="username" placeholder="请输入账户名" required autofocus/> </div> <div class="col-lg-10"></div> <div class="col-lg-10"> <input type="password" class="form-control" id = "password" name="password" placeholder="请输入密码" required autofocus/> </div> <div class="col-lg-10"></div> <div class="col-lg-10 mycheckbox checkbox"> <input type="checkbox" class="col-lg-1">记住密码</input> </div> <div class="col-lg-10"></div> <div class="col-lg-10"> <button type="submit" id = "submit" class="btn btn-success col-lg-12">登录</button> </div> </div> </div> </form> </body> </html>
chat.jsp
<%@page import="com.lzl.dao.User"%> <%@ 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>聊天</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="renderer" content="webkit"> <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script> <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css"> <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap-theme.min.css"> <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script> <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <script type="text/javascript"> var websocket; $(function() { // 首先判断是否 支持 WebSocket if('WebSocket' in window) { websocket = new WebSocket("ws://localhost:8080/websocket2/websocket"); } else if('MozWebSocket' in window) { websocket = new MozWebSocket("ws://localhost:8080/websocket2/websocket"); } else { websocket = new SockJS("http://localhost:8080/websocket2/sockjs/websocket"); } // 打开时 websocket.onopen = function(evnt) { $("#tou").html("链接服务器成功!") }; // 处理消息时 websocket.onmessage = function(evnt) { console.log(evnt.data); $("#msg").html($("#msg").html() + "<br/>" + evnt.data); }; websocket.onerror = function(evnt) { console.log(" websocket.onerror "); }; websocket.onclose = function(evnt) { $("#tou").html("与服务器断开了链接!") }; // 点击了发送消息按钮的响应事件 $("#send").click(function(){ // 获取消息内容 var text = $("#message").val(); // 判断 if(text == null || text == ""){ alert(" content can not empty!!"); return false; } var username = $("#username").val(); console.log("username="+username); var msg = { msgContent: text, userID:username }; // 发送消息 websocket.send(JSON.stringify(msg)); $("#message").val(""); }); $("#offline").click(function(){ console.log(websocket.readyState); var state = websocket.readyState; if(1==state){ websocket.close(); $("#offline").html("上线"); }else{ connect(); $("#offline").html("下线"); } }); }); function connect (){ // 首先判断是否 支持 WebSocket if('WebSocket' in window) { websocket = new WebSocket("ws://localhost:8080/websocket2/websocket"); } else if('MozWebSocket' in window) { websocket = new MozWebSocket("ws://localhost:8080/websocket2/websocket"); } else { websocket = new SockJS("http://localhost:8080/websocket2/sockjs/websocket"); } // 打开时 websocket.onopen = function(evnt) { $("#tou").html("链接服务器成功!") }; // 处理消息时 websocket.onmessage = function(evnt) { $("#msg").html($("#msg").html() + "<br/>" + evnt.data); }; websocket.onerror = function(evnt) { console.log(" websocket.onerror "); }; websocket.onclose = function(evnt) { $("#tou").html("与服务器断开了链接!") }; } //给发送按钮绑定回车键事件 $(document).keydown(function(event){ if(event.keyCode == 13){ //绑定回车 $('#send').click(); } }); </script> </head> <body> <input type="hidden" value="${user.username}" id="username"> <div> <div class="page-header" id="tou" style = "text-align:center"> webSocket多终端聊天测试 </div> <div class="well" id="msg" style = "width:800px;margin:0 auto"></div> <div class="col-lg"> <div class="input-group" style = "width:800px;margin:0 auto"> <input type="text" class="form-control" placeholder="发送信息..." id="message"> <span class="input-group-btn"> <button class="btn btn-default" type="button" id="send" >发送</button> </span> </div> </div> </div> <div class="container-fluid"> <div class="row-fluid"> <div class="span12"> <button class="btn btn-warning btn-block" type="button" id= "offline" style = "width:800px;margin:0 auto">下线</button> </div> </div> </div> </body> </html>
4.效果
这是admin这个用户的界面
这是王小二的界面