websocket协议
一、何为websocket协议
- WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
- 何谓全双工:全双工(Full Duplex)是通讯传输的一个术语。双方在通信时允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合。全双工指可以同时进行信号的双向传输。指A→B的同时B→A,就像是双向车道。
- 单工就像是汽车的单行道,是在只允许甲方向乙方传送信息,而乙方不能向甲方传送 。
参考资料:https://baike.baidu.com/item/%E5%85%A8%E5%8F%8C%E5%B 7%A5/310007?fr=aladdin
- 在 WebSocket中,浏览器和服务器只需要完成一次握手,就可以创建持久性的连接,并进行双向数据传输。
- 在推送功能的实现技术上,相比使用Ajax 定时轮询的方式(setInterval),WebSocket 更节省服务器资源和带宽。
- 服务器向客户端发送数据的功能是websocket协议的典型使用场景

二、websocket常用事件方法
以下 API 用于创建 WebSocket 对象。
var Socket = new WebSocket(url, [protocol] );
WebSocket 事件
以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:

WebSocket 方法

三、演示WebSocket
spring websocket实现前后端通信(服务器端)
1. 配置文件
pom.xml
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>shenzhanwang</groupId> <artifactId>SSM</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>Spring-websocket</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId> <version>4.0.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>4.0.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.0.4.RELEASE</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.16</version> </dependency> <!-- servlet API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1-b09</version> <scope>provided</scope> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.30</version> </dependency> <!-- mchange C3P0 数据源--> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5-pre2</version> </dependency> <!-- junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!-- fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.1.29</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.0.4.RELEASE</version> </dependency> <!-- spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>4.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>4.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>4.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>4.0.4.RELEASE</version> </dependency> <!-- aspectjweaver --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.7.2</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.1</version> </dependency> <dependency> <groupId>ognl</groupId> <artifactId>ognl</artifactId> <version>3.0.6</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.14</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.2.2</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>4.1.6</version> </dependency> </dependencies> <build> <finalName>Spring-websocket</finalName> </build> <!-- 指定maven编译方式为jdk1.8版本 --> <profiles> <profile> <id>jdk-1.8</id> <activation> <activeByDefault>true</activeByDefault> <jdk>1.8</jdk> </activation> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion> </properties> </profile> </profiles> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> </project>
spring-mybatis.xml
<?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:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close" p:driverClass="com.mysql.jdbc.Driver" p:jdbcUrl="jdbc:mysql://192.168.240.133:3306/sakila" p:user="root" p:password="root" p:maxPoolSize="40" p:minPoolSize="2" p:initialPoolSize="2" p:maxIdleTime="30"/> <!-- mybatis文件配置,扫描所有mapper文件 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:dataSource-ref="dataSource" p:configLocation="classpath:conf/mybatis-config.xml" p:mapperLocations="classpath:mapper/*.xml"/> <!-- configLocation为mybatis属性 mapperLocations为所有mapper--> <!-- spring与mybatis整合配置,扫描所有mapper --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" p:basePackage="mapper" p:sqlSessionFactoryBeanName="sqlSessionFactory"/> <!-- 对数据源进行事务管理 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>
spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <context:component-scan base-package="controller,service.impl,websocket" /> <!-- 处理请求时返回json字符串的中文乱码问题 --> <mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>application/json;charset=UTF-8</value> </list> </property> </bean> </mvc:message-converters> </mvc:annotation-driven> <!-- 静态资源访问(不拦截此目录下的东西的访问) --> <mvc:resources location="/js/" mapping="/js/**" /> <mvc:resources location="/css/" mapping="/css/**" /> <!--配置拦截器, 多个拦截器,顺序执行 --> <mvc:interceptors> <mvc:interceptor> <!-- 匹配的是url路径, 如果不配置或/**,将拦截所有的Controller --> <mvc:mapping path="/**" /> <mvc:exclude-mapping path="/login" /> <mvc:exclude-mapping path="/authImg"/> <mvc:exclude-mapping path="/loginvalidate"/> <bean class="interceptor.LoginIntercepter"></bean> </mvc:interceptor> <!-- 当设置多个拦截器时,先按顺序调用preHandle方法,然后逆序调用每个拦截器的postHandle和afterCompletion方法 --> </mvc:interceptors> <!-- 页面转向解析 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/content/" /> <property name="suffix" value=".jsp" /> </bean> <!-- 文件上传 --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean> </beans>
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <plugins> <!-- com.github.pagehelper为PageHelper类所在包名 --> <plugin interceptor="com.github.pagehelper.PageHelper"> <!-- 4.0.0以后版本可以不设置该参数 --> <property name="dialect" value="mysql"/> <!-- 该参数默认为false --> <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 --> <!-- 和startPage中的pageNum效果一样--> <property name="offsetAsPageNum" value="true"/> <!-- 该参数默认为false --> <!-- 设置为true时,使用RowBounds分页会进行count查询 --> <property name="rowBoundsWithCount" value="true"/> <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 --> <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)--> <property name="pageSizeZero" value="true"/> <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 --> <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 --> <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 --> <property name="reasonable" value="false"/> <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 --> <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 --> <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 --> <!-- 不理解该含义的前提下,不要随便复制该配置 --> <property name="params" value="pageNum=pageHelperStart;pageSize=pageHelperRows;"/> <!-- 支持通过Mapper接口参数来传递分页参数 --> <property name="supportMethodsArguments" value="false"/> <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page --> <property name="returnPageInfo" value="none"/> </plugin> </plugins> </configuration>
log4j.properties
### set log levels ###
#log4j.rootLogger = debug , stdout , D , E
log4j.rootLogger = info , stdout , D
### output to the console ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
#log4j.appender.stdout.layout.ConversionPattern = %d{ABSOLUTE} %5p %c{ 1 }:%L - %m%n
log4j.appender.stdout.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] %m%n
### Output to the log file ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = ${webapp.root}/WEB-INF/logs/error.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = ERROR
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <welcome-file-list> <welcome-file>/content/login.jsp</welcome-file> </welcome-file-list> <!-- 读取spring配置文件 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:conf/spring-mybatis.xml</param-value> </context-param> <!-- Spring字符集过滤器 --> <filter> <filter-name>SpringEncodingFilter</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>SpringEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 日志记录 --> <context-param> <!-- 日志配置文件路径 --> <param-name>log4jConfigLocation</param-name> <param-value>classpath:conf/log4j.properties</param-value> </context-param> <context-param> <!-- 日志页面的刷新间隔 --> <param-name>log4jRefreshInterval</param-name> <param-value>6000</param-value> </context-param> <listener> <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class> </listener> <!-- 启动spring --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- spring mvc配置 --> <servlet> <servlet-name>sakila</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:conf/spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>sakila</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
2. websocket连接相关类
HandShake
package websocket; import java.util.Map; import javax.servlet.http.HttpSession; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; /** * Socket建立连接(握手)和断开 * 握手拦截器类:创建websocket连接时的拦截器,记录建立连接的用户的session以便根据不同session来通信 */ public class HandShake implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { System.out.println("Websocket:用户[ID:" + ((ServletServerHttpRequest) request).getServletRequest().getSession(false).getAttribute("uid") + "]已经建立连接"); //在握手之前将HttpSession中的用户,copy放到WebSocket Session中 if (request instanceof ServletServerHttpRequest) { ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request; HttpSession session = servletRequest.getServletRequest().getSession(false); // 标记用户 Long uid = (Long) session.getAttribute("uid"); if(uid!=null){ attributes.put("uid", uid); }else{ return false; } } return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { System.out.println("after hand"); } }
MyWebSocketHandler
package websocket; import java.io.IOException; import java.util.Date; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; 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 po.Message; import service.LoginService; import com.google.gson.Gson; import com.google.gson.GsonBuilder; /** * Socket处理器 * 消息处理类 */ @Component public class MyWebSocketHandler implements WebSocketHandler { //用于保存HttpSession与WebSocketSession的映射关系 public static final Map<Long, WebSocketSession> userSocketSessionMap; @Autowired LoginService loginservice; static { userSocketSessionMap = new ConcurrentHashMap<Long, WebSocketSession>(); } /** * 建立连接后,把登录用户的id写入WebSocketSession */ public void afterConnectionEstablished(WebSocketSession session) throws Exception { //从 WebSocket Session中取得用户 Long uid = (Long) session.getAttributes().get("uid"); String username=loginservice.getnamebyid(uid); if (userSocketSessionMap.get(uid) == null) { userSocketSessionMap.put(uid, session); Message msg = new Message(); msg.setFrom(0L);//0表示上线消息 msg.setText(username); this.broadcast(new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg))); } } /** * 消息处理,在客户端通过Websocket API发送的消息会经过这里,然后进行相应的处理 */ public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { if(message.getPayloadLength()==0) return; Message msg=new Gson().fromJson(message.getPayload().toString(),Message.class); msg.setDate(new Date()); sendMessageToUser(msg.getTo(), new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg))); } /** * 消息传输错误处理 */ public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { if (session.isOpen()) { session.close(); } Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator(); // 移除当前抛出异常用户的Socket会话 while (it.hasNext()) { Entry<Long, WebSocketSession> entry = it.next(); if (entry.getValue().getId().equals(session.getId())) { userSocketSessionMap.remove(entry.getKey()); System.out.println("Socket会话已经移除:用户ID" + entry.getKey()); String username=loginservice.getnamebyid(entry.getKey()); Message msg = new Message(); msg.setFrom(-2L); msg.setText(username); this.broadcast(new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg))); break; } } } /** * 关闭连接后 */ public void afterConnectionClosed(WebSocketSession session,CloseStatus closeStatus) throws Exception { System.out.println("Websocket:" + session.getId() + "已经关闭"); Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator(); // 移除当前用户的Socket会话 while (it.hasNext()) { Entry<Long, WebSocketSession> entry = it.next(); if (entry.getValue().getId().equals(session.getId())) { userSocketSessionMap.remove(entry.getKey()); System.out.println("Socket会话已经移除:用户ID" + entry.getKey()); String username=loginservice.getnamebyid(entry.getKey()); Message msg = new Message(); msg.setFrom(-2L);//下线消息,用-2表示 msg.setText(username); this.broadcast(new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg))); break; } } } public boolean supportsPartialMessages() { return false; } /** * 给所有在线用户发送消息 * @param message * @throws IOException */ public void broadcast(final TextMessage message) throws IOException { Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator(); //多线程群发 while (it.hasNext()) { final Entry<Long, WebSocketSession> entry = it.next(); if (entry.getValue().isOpen()) { // entry.getValue().sendMessage(message); new Thread(new Runnable() { public void run() { try { if (entry.getValue().isOpen()) { entry.getValue().sendMessage(message); } } catch (IOException e) { e.printStackTrace(); } } }).start(); } } } /** * 给某个用户发送消息 * * @param uid * @param message * @throws IOException */ public void sendMessageToUser(Long uid, TextMessage message) throws IOException { WebSocketSession session = userSocketSessionMap.get(uid); if (session != null && session.isOpen()) { //输出消息到客户端 session.sendMessage(message); } } }
WebSocketConfig
package websocket;
import javax.annotation.Resource;
import org.springframework.stereotype.Component;
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;
/**
* WebScoket配置处理器
*/
@Component
//声明该类支持WebSocket
@EnableWebSocket
public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
@Resource
MyWebSocketHandler handler;
/**
websocket入口,允许访问的域、注册Handler、SockJs支持和拦截器。
registerWebSocketHandlers方法是向spring容器注册一个handler地址,我把他理解成requestMapping
addInterceptors:拦截器,当建立websocket连接的时候,我们可以通过继承spring的HttpSessionHandshakeInterceptor来搞事情
withSockJS: 这个是应对浏览器不支持websocket协议的时候降级为轮询的处理
setAllowedOrigins:跨域设置,*表示所有域名都可以,不限制, 域包括ip:port, 指定*可以是任意的域名,不加的话默认localhost+本服务端口
*/
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(handler, "/ws").addInterceptors(new HandShake());
registry.addHandler(handler, "/ws/sockjs").addInterceptors(new HandShake()).withSockJS();
//setAllowedOrigins()方法的支持spring4.1.5之后的版本才支持
//setAllowedOrigins(String... val),允许指定的域名或IP(含端口号)建立长连接,可以只设置允许自家域名访问,如果不限时使用"*"号,如果指定了域名,则必须要以http或https开头。
}
}
Message
package po;
import java.util.Date;
public class Message {
//发送者
public Long from;
//发送者名称
public String fromName;
//接收者
public Long to;
//发送的文本
public String text;
//发送日期
public Date date;
public Long getFrom() {
return from;
}
public void setFrom(Long from) {
this.from = from;
}
public Long getTo() {
return to;
}
public void setTo(Long to) {
this.to = to;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getFromName() {
return fromName;
}
public void setFromName(String fromName) {
this.fromName = fromName;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
}
3. 登录聊天室
WEB-INF/content/login.jsp登录页面
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>login</title> <script src="./js/jquery-1.12.3.min.js"></script> <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <style type="text/css"> .vertical-center{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } </style> </head> <body> <div class="container vertical-center"> <div class="col-md-6 col-md-offset-3"> <form action="loginvalidate" method="post"> <h2 >登录聊天室</h2> <label for="inputEmail" class="sr-only">userid</label> <input type="text" name="username" id="inputEmail" class="form-control" placeholder="userid" required autofocus> <label for="inputPassword" class="sr-only">Password</label> <input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required> <label for="inputEmail" class="sr-only">userid</label> <input type="text" name="pic" id="pic" class="form-control" placeholder="验证码" required> 验证码:<img src="authImg" width="120" height="40"> <button class="btn btn-lg btn-primary btn-block" type="submit">login</button> </form> </div> </div> <!-- /container --> </body> </html>
WEB-INF/content/failcode.jsp验证码错误页面
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <script> alert("验证码错误"); window.history.go(-1); </script>
WEB-INF/content/chatroom.jsp登录成功后跳转页面
//连接建立时触发 websocket.onopen = function(event) { // debugger // alert('连接成功!'); console.log("连接成功!"); }; $.post("onlineusers",function(data){ console.log("onlineusers!!!"); for(var i=0;i<data.length;i++) $("#users").append('<a href="#" onclick="talk(this)" class="list-group-item">'+data[i]+'</a>'); });
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>聊天室</title> <script src="./js/jquery-1.12.3.min.js"></script> <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <style> body{ margin-top:5px; } </style> </head> <body> <div class="container"> <div class="row"> <div class="col-md-3"> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">当前登录用户</h3> </div> <div class="panel-body"> <div class="list-group"> <a href="#" class="list-group-item">你好,${sessionScope.username}</a> <a href="logout" class="list-group-item">退出</a> </div> </div> </div> <div class="panel panel-primary" id="online"> <div class="panel-heading"> <h3 class="panel-title">当前在线的其他用户</h3> </div> <div class="panel-body"> <div class="list-group" id="users"> </div> </div> </div> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">群发系统广播</h3> </div> <div class="panel-body"> <input type="text" class="form-control" id="msg" /><br> <button id="broadcast" type="button" class="btn btn-primary">发送</button> </div> </div> </div> <div class="col-md-9"> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title" id="talktitle"></h3> </div> <div class="panel-body"> <div class="well" id="log-container" style="height:400px;overflow-y:scroll"> </div> <input type="text" id="myinfo" class="form-control col-md-12" /> <br> <button id="send" type="button" class="btn btn-primary">发送</button> </div> </div> </div> </div> </div> <script> $(document).ready(function() { // 指定websocket路径 var websocket; if ('WebSocket' in window) { websocket = new WebSocket("ws://localhost:8080/Spring-websocket/ws?uid="+${sessionScope.uid}); } else if ('MozWebSocket' in window) { websocket = new MozWebSocket("ws://localhost:8080/Spring-websocket/ws"+${sessionScope.uid}); } else { websocket = new SockJS("http://localhost:8080/Spring-websocket/ws/sockjs"+${sessionScope.uid}); } //var websocket = new WebSocket('ws://localhost:8080/Spring-websocket/ws'); websocket.onmessage = function(event) { var data=JSON.parse(event.data); if(data.from>0||data.from==-1){//用户或者群消息 // 接收服务端的实时消息并添加到HTML页面中 $("#log-container").append("<div class='bg-info'><label class='text-danger'>"+data.fromName+" "+data.date+"</label><div class='text-success'>"+data.text+"</div></div><br>"); // 滚动条滚动到最低部 scrollToBottom(); }else if(data.from==0){//上线消息 if(data.text!="${sessionScope.username}") { $("#users").append('<a href="#" onclick="talk(this)" class="list-group-item">'+data.text+'</a>'); //alert(data.text+"上线了"); } }else if(data.from==-2){//下线消息 if(data.text!="${sessionScope.username}") { $("#users > a").remove(":contains('"+data.text+"')"); //alert(data.text+"下线了"); } } }; websocket.onopen = function(event) { // debugger alert('连接成功!'); }; websocket.onclose = function(event) { debugger //alert('连接关闭!'); }; websocket.onerror = function(event) { //alert('连接出错!'); }; $.post("onlineusers",function(data){ for(var i=0;i<data.length;i++) $("#users").append('<a href="#" onclick="talk(this)" class="list-group-item">'+data[i]+'</a>'); }); $("#broadcast").click(function(){ $.post("broadcast",{"text":$("#msg").val()}); }); $("#send").click(function(){ $.post("getuid",{"username":$("body").data("to")},function(d){ var v=$("#myinfo").val(); if(v==""){ return; }else{ var data={}; data["from"]="${sessionScope.uid}"; data["fromName"]="${sessionScope.username}"; data["to"]=d.uid; data["text"]=v; websocket.send(JSON.stringify(data)); $("#log-container").append("<div class='bg-success'><label class='text-info'>我 "+new Date()+"</label><div class='text-info'>"+v+"</div></div><br>"); scrollToBottom(); $("#myinfo").val(""); } }); }); }); function talk(a){ $("#talktitle").text("与"+a.innerHTML+"的聊天"); $("body").data("to",a.innerHTML); } function scrollToBottom(){ var div = document.getElementById('log-container'); div.scrollTop = div.scrollHeight; } </script> </body> </html>
WEB-INF/content/fail.jsp登录失败页面
<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <script> alert("密码错误"); window.history.go(-1); </script>
LoginIntercepter登录拦截器类
package interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
public class LoginIntercepter extends HandlerInterceptorAdapter{
/**
* 在业务处理器处理请求之前被调用
* 如果返回false
* 从当前的拦截器往回执行所有拦截器的afterCompletion(),再退出拦截器链
* 如果返回true
* 执行下一个拦截器,直到所有的拦截器都执行完毕
* 再执行被拦截的Controller
* 然后进入拦截器链,
* 从最后一个拦截器往回执行所有的postHandle()
* 接着再从最后一个拦截器往回执行所有的afterCompletion()
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String username = (String)request.getSession().getAttribute("username");
if(username == null){
request.getRequestDispatcher("/WEB-INF/content/login.jsp").forward(request, response);
return false;
}else
return true;
}
/**
* 在业务处理器处理请求执行完成后,生成视图之前执行的动作
* 可在modelAndView中加入数据,比如当前时间
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 在DispatcherServlet完全处理完请求后被调用,可用于清理资源等
*
* 当有拦截器抛出异常时,会从当前拦截器往回执行所有的拦截器的afterCompletion()
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
导入sakila库staff表数据

Staff实体类
package po;
public class Staff {
private byte staff_id;
private String first_name;
private String last_name;
private short address_id;
private String email;
private String username;
private String password;
private String last_update;
public String getLast_update() {
return last_update;
}
public byte getStaff_id() {
return staff_id;
}
public void setStaff_id(byte staff_id) {
this.staff_id = staff_id;
}
public void setLast_update(String last_update) {
this.last_update = last_update;
}
public String getFirst_name() {
return first_name;
}
public void setFirst_name(String first_name) {
this.first_name = first_name;
}
public String getLast_name() {
return last_name;
}
public void setLast_name(String last_name) {
this.last_name = last_name;
}
public short getAddress_id() {
return address_id;
}
public void setAddress_id(short address_id) {
this.address_id = address_id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
AuthImg类封装登录验证码图片
package web; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.annotation.*; import java.io.*; import java.awt.*; import java.awt.image.*; import java.util.*; import javax.imageio.*; @WebServlet(urlPatterns={"/WEB-INF/content/authImg.jsp"}) public class AuthImg extends HttpServlet { private final Font mFont = new Font("Arial Black", Font.PLAIN, 16); private final int IMG_WIDTH = 100; private final int IMG_HEIGTH = 18; private Color getRandColor(int fc,int bc) { Random random = new Random(); if(fc > 255) fc = 255; if(bc > 255) bc=255; int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r , g , b); } public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setHeader("Pragma","No-cache"); response.setHeader("Cache-Control","no-cache"); response.setDateHeader("Expires", 0); response.setContentType("image/jpeg"); BufferedImage image = new BufferedImage (IMG_WIDTH , IMG_HEIGTH , BufferedImage.TYPE_INT_RGB); Graphics g = image.getGraphics(); Random random = new Random(); g.setColor(getRandColor(200 , 250)); g.fillRect(1, 1, IMG_WIDTH - 1, IMG_HEIGTH - 1); g.setColor(new Color(102 , 102 , 102)); g.drawRect(0, 0, IMG_WIDTH - 1, IMG_HEIGTH - 1); g.setColor(getRandColor(160,200)); for (int i = 0 ; i < 30 ; i++) { int x = random.nextInt(IMG_WIDTH - 1); int y = random.nextInt(IMG_HEIGTH - 1); int xl = random.nextInt(6) + 1; int yl = random.nextInt(12) + 1; g.drawLine(x , y , x + xl , y + yl); } g.setColor(getRandColor(160,200)); for (int i = 0 ; i < 30 ; i++) { int x = random.nextInt(IMG_WIDTH - 1); int y = random.nextInt(IMG_HEIGTH - 1); int xl = random.nextInt(12) + 1; int yl = random.nextInt(6) + 1; g.drawLine(x , y , x - xl , y - yl); } g.setFont(mFont); String sRand = ""; for (int i = 0 ; i < 4 ; i++) { String tmp = getRandomChar(); sRand += tmp; g.setColor(new Color(20 + random.nextInt(110) ,20 + random.nextInt(110) ,20 + random.nextInt(110))); g.drawString(tmp , 15 * i + 10,15); } HttpSession session = request.getSession(true); session.setAttribute("rand" , sRand); // System.out.println("写入session"+sRand); g.dispose(); ImageIO.write(image, "JPEG", response.getOutputStream()); } private String getRandomChar() { int rand = (int)Math.round(Math.random() * 2); long itmp = 0; char ctmp = '\u0000'; switch (rand) { case 1: itmp = Math.round(Math.random() * 25 + 65); ctmp = (char)itmp; return String.valueOf(ctmp); case 2: itmp = Math.round(Math.random() * 25 + 97); ctmp = (char)itmp; return String.valueOf(ctmp); default : itmp = Math.round(Math.random() * 9); return itmp + ""; } } }
controller/Pic类映射验证码请求
package controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class Pic {
@RequestMapping(value="/authImg")
public String getpic(){
return "authImg";
}
}
controller/Login类封装登录相关方法
package controller; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import service.LoginService; @Controller public class Login { @Autowired LoginService loginservice; @RequestMapping("/loginvalidate") public String loginvalidate(@RequestParam("username") String username,@RequestParam("pic") String pic,@RequestParam("password") String pwd,HttpSession httpSession){ String picode=(String) httpSession.getAttribute("rand"); if(!picode.equalsIgnoreCase(pic)) return "failcode"; if(username==null) return "login"; String realpwd=loginservice.getpwdbyname(username); if(realpwd!=null&&pwd.equals(realpwd)) { long uid=loginservice.getUidbyname(username); httpSession.setAttribute("username", username); httpSession.setAttribute("uid", uid); return "chatroom"; }else return "fail"; } @RequestMapping("/login") public String login(){ return "login"; } @RequestMapping("/logout") public String logout(HttpSession httpSession){ httpSession.removeAttribute("username"); httpSession.removeAttribute("uid"); return "login"; } }
ChatController类
package controller; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.socket.WebSocketSession; import service.LoginService; import websocket.MyWebSocketHandler; @Controller public class ChatController { @Autowired LoginService loginservice; @RequestMapping("/onlineusers") @ResponseBody public Set<String> onlineusers(HttpSession session){ Map<Long, WebSocketSession> map=MyWebSocketHandler.userSocketSessionMap; Set<Long> set=map.keySet(); Iterator<Long> it = set.iterator(); Set<String> nameset=new HashSet<String>(); while(it.hasNext()){ Long entry = it.next(); String name=loginservice.getnamebyid(entry); String user=(String)session.getAttribute("username"); if(!user.equals(name)) nameset.add(name); } return nameset; } }
LoginService类
package service;
public interface LoginService {
String getpwdbyname(String name);
Long getUidbyname(String name);
String getnamebyid(long id);
}
LoginServiceImpl类
package service.impl;
import mapper.LoginMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import po.Staff;
import service.LoginService;
@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,timeout=5)
@Service("loginservice")
public class LoginServiceImpl implements LoginService{
@Autowired
LoginMapper loginmapper;
public String getpwdbyname(String name) {
Staff s=loginmapper.getpwdbyname(name);
if(s!=null)
return s.getPassword();
else
return null;
}
public Long getUidbyname(String name) {
Staff s=loginmapper.getpwdbyname(name);
if(s!=null)
return (long) s.getStaff_id();
else
return null;
}
public String getnamebyid(long id) {
Staff s=loginmapper.getnamebyid(id);
if(s!=null)
return s.getUsername();
else
return null;
}
}
LoginMapper类
package mapper;
import po.Staff;
public interface LoginMapper {
Staff getpwdbyname(String name);
Staff getnamebyid(long id);
}
resources/mapper/LoginMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="mapper.LoginMapper"> <resultMap type="po.Staff" id="staffMap"> <id column="staff_id" property="staff_id" ></id> <result column="username" property="username"/> <result column="password" property="password"/> <result column="last_name" property="last_name"/> </resultMap> <select id="getpwdbyname" parameterType="String" resultMap="staffMap"> select * from staff where binary username=#{name} </select> <select id="getnamebyid" parameterType="long" resultMap="staffMap"> select * from staff where staff_id=#{id} </select> </mapper>
启动tomcat(local),虚拟目录配置/Spring-websocket,浏览器访问http://localhost:8080/Spring-websocket/login

使用staff表的用户名和密码登录,输入错误的验证码,输入错误的密码

登录成功,检查页面展示

4. 群发系统广播
4.1 聊天室页面输入群发消息,发送按钮绑定点击事件
<div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">群发系统广播</h3> </div> <div class="panel-body"> <input type="text" class="form-control" id="msg" /><br> <button id="broadcast" type="button" class="btn btn-primary">发送</button> </div> </div>
$("#broadcast").click(function(){
$.post("broadcast",{"text":$("#msg").val()});
});
4.2 Controller
执行controller之前会先调用LoginIntercepter拦截器的preHandle方法,然后调用controller中的broadcast方法
@Autowired
MyWebSocketHandler handler;
// 发布系统广播(群发)
@ResponseBody
@RequestMapping(value = "broadcast", method = RequestMethod.POST)
public void broadcast(@RequestParam("text") String text) throws IOException {
Message msg = new Message();
msg.setDate(new Date());
msg.setFrom(-1L);//-1表示系统广播
msg.setFromName("系统广播");
msg.setTo(0L);
msg.setText(text);
handler.broadcast(new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg)));
}
4.3 MyWebSocketHandler类
/** * 给所有在线用户发送消息 * @param message * @throws IOException */ public void broadcast(final TextMessage message) throws IOException { Iterator<Entry<Long, WebSocketSession>> it = userSocketSessionMap.entrySet().iterator(); //多线程群发 while (it.hasNext()) { final Entry<Long, WebSocketSession> entry = it.next(); if (entry.getValue().isOpen()) { // entry.getValue().sendMessage(message); new Thread(new Runnable() { public void run() { try { if (entry.getValue().isOpen()) { entry.getValue().sendMessage(message); } } catch (IOException e) { e.printStackTrace(); } } }).start(); } } }
浏览器发送群发消息,其他用户登录成功可以看到这条消息

5. 发送消息给一个用户
5.1 给发送绑定点击事件chatroom.jsp
<div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title" id="talktitle"></h3> </div> <div class="panel-body"> <div class="well" id="log-container" style="height:400px;overflow-y:scroll"> </div> <input type="text" id="myinfo" class="form-control col-md-12" /> <br> <button id="send" type="button" class="btn btn-primary">发送</button> </div> </div>
$("#send").click(function(){ $.post("getuid",{"username":$("body").data("to")},function(d){ var v=$("#myinfo").val(); if(v==""){ return; }else{ var data={}; data["from"]="${sessionScope.uid}"; data["fromName"]="${sessionScope.username}"; data["to"]=d.uid; data["text"]=v; websocket.send(JSON.stringify(data)); $("#log-container").append("<div class='bg-success'><label class='text-info'>我 "+new Date()+"</label><div class='text-info'>"+v+"</div></div><br>"); scrollToBottom(); $("#myinfo").val(""); } }); });
5.2 User实体类
package po;
public class User {
Long uid;
public Long getUid() {
return uid;
}
public void setUid(Long uid) {
this.uid = uid;
}
}
5.3 Controller
@Autowired
LoginService loginservice;
@RequestMapping("getuid")
@ResponseBody
public User getuid(@RequestParam("username")String username){
Long a=loginservice.getUidbyname(username);
User user=new User();
user.setUid(a);
return user;
}
浏览器登录成功,点击“当前在线的其他用户”中的用户名,右侧输入信息内容,发送


浙公网安备 33010602011771号