版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_33223299/article/details/86678480
一 前言
前面一篇大致讲解了webSocket的定义以及配置,那么这一篇通过简单案例对webSocket更好的理解与使用。使用的是spring-servlet-webSocket的整合形式。因为这里主要是以webSocket为重点,主要展示的是webSocket如何使客户端与服务器端的交互变得简单。
二 配置工作
1. 导入需要的maven包(这里version忽略了)
spring jar包:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
java.servlet jar包:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<scope>provided</scope>
</dependency>
json jar包:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
log jar 包:
<!-- log begin -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.logback-extensions</groupId>
<artifactId>logback-ext-spring</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
<!-- log end -->
2.配置spring-servlet.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: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.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 启动注解服务 -->
<mvc:annotation-driven/>
<mvc:resources location="/resources/" mapping="/resources/**"/>
<context:component-scan base-package="com.chp.spring"/>
<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/jsp/"/>
<property name="suffix" value=".jsp"/>
<property name="order" value="1"/>
</bean>
</beans>
3.配置web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 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_3_0.xsd">
<display-name>spring-notes-helloworld</display-name>
<!-- SERVLET BEGIN -->
<servlet>
<servlet-name>spring-servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring-servlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- SERVLET END -->
<!-- FILTER BEGIN -->
<!-- 转换字符编码 -->
<filter>
<filter-name>encodingFilter</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>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
<!-- FILTER END -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
三 主要代码
模拟qq聊天无非两个主要因素 人与聊天信息 ,所以这里写两个类,用户类与聊天信息类
User:
public class User {
private Long id;
private String name;
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Message:
public class Message {
// 发送者
private Long from;
// 发送者名称
private String fromName;
// 接收者
private Long to;
// 发送的文本
private String text;
// 发送日期
private 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;
}
}
WebSocket握手请求的拦截器:
public class HandShake implements HandshakeInterceptor {
/**
* 在握手之前执行该方法, 继续握手返回true, 中断握手返回false. 通过attributes参数设置
*/
@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")
+ "]已经建立连接");
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) {
}
}
webSocket处理类
@Component
public class MyWebSocketHandler implements WebSocketHandler {
private static final Logger logger = LoggerFactory.getLogger(MyWebSocketHandler.class);
private static Map<Long, Set<WebSocketSession>> userSocketSessionMap = new ConcurrentHashMap<>();
/**
* 建立连接后
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Long uid = (Long) session.getAttributes().get("uid");
logger.info("Session {} connected.", uid);
// 如果是新 Session,记录进 Map
boolean isNewUser = true;
for (Object o : userSocketSessionMap.entrySet()) {
Entry entry = (Entry) o;
Long key = (Long) entry.getKey();
if (key.equals(uid)) {
userSocketSessionMap.get(uid).add(session);
isNewUser = false;
break;
}
}
if (isNewUser) {
Set<WebSocketSession> sessions = new HashSet<>();
sessions.add(session);
userSocketSessionMap.put(uid, sessions);
}
logger.info("当前在线用户数: {}", userSocketSessionMap.values().size());
}
/**
* 消息处理,在客户端通过Websocket API发送的消息会经过这里,然后进行相应的处理
*/
@Override
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)));
}
/**
* 消息传输错误处理
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if (session.isOpen()) {
session.close();
}
// 移除Socket会话
for (Set<WebSocketSession> item : userSocketSessionMap.values()) {
if (item.contains(session)) {
// 删除连接 session
item.remove(session);
// 如果 userId 对应的 session 数为 0 ,删除该 userId 对应的记录
if (0 == item.size()) {
userSocketSessionMap.values().remove(item);
}
break;
}
}
}
/**
* 关闭连接后
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
logger.info("Session {} disconnected. Because of {}", session.getId(), closeStatus);
for (Set<WebSocketSession> item : userSocketSessionMap.values()) {
if (item.contains(session)) {
// 删除连接 session
item.remove(session);
// 如果 userId 对应的 session 数为 0 ,删除该 userId 对应的记录
if (0 == item.size()) {
userSocketSessionMap.values().remove(item);
}
break;
}
}
logger.info("当前在线用户数: {}", userSocketSessionMap.values().size());
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 给所有在线用户发送消息
*
* @param message
* @throws IOException
*/
public void broadcast(final TextMessage message) throws IOException {
// 多线程群发
for(Set<WebSocketSession> item : userSocketSessionMap.values()) {
for (final WebSocketSession session : item) {
if (session.isOpen()) {
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("socket-schedule-pool-%d").daemon(true).build());
for (int i = 0; i < 3; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
if (session.isOpen()) {
logger.debug("broadcast output:" + message.toString());
session.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
}
}
}
/**
* 给某个用户发送消息
*
* @param userName
* @param message
* @throws IOException
*/
private void sendMessageToUser(Long uid, TextMessage message) throws IOException {
for (Long id : userSocketSessionMap.keySet()) {
if (id.equals(uid)) {
for (WebSocketSession session : userSocketSessionMap.get(uid)) {
try {
logger.info("SendAll: {}", message);
session.sendMessage(message);
} catch (Exception e) {
logger.error(e.toString());
}
}
}
}
}
}
webSocket入口:
@Configuration
@EnableWebSocket
public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
@Bean
public WebSocketHandler myHandler() {
return new MyWebSocketHandler();
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//允许连接的域,只能以http或https开头
String[] allowsOrigins = {"http://www.xxx.com"};
registry.addHandler(myHandler(), "/ws").addInterceptors(new HandShake());
registry.addHandler(myHandler(), "/ws/sockjs").addInterceptors(new HandShake()).
setAllowedOrigins(allowsOrigins).addInterceptors(new HandShake()).withSockJS();
}
}
- 实现WebSocketConfigurer接口,重写registerWebSocketHandlers方法,这是一个核心实现方法,配置websocket入口,允许访问的域、注册Handler、SockJs支持和拦截器。
- registry.addHandler注册和路由的功能,当客户端发起websocket连接,把/path交给对应的handler处理,而不实现具体的业务逻辑,可以理解为收集和任务分发中心。
- setAllowedOrigins(String[] domains),允许指定的域名或IP(含端口号)建立长连接,如果只允许自家域名访问,这里轻松设置。如果不限时使用"*"号,如果指定了域名,则必须要以http或https开头。
- addInterceptors,顾名思义就是为handler添加拦截器,可以在调用handler前后加入我们自己的逻辑代码。
MsgController控制层
@Controller
@RequestMapping("/msg")
public class MsgController {
@Resource
MyWebSocketHandler handler;
Map<Long, User> users = new HashMap<>();
// 模拟一些数据
@ModelAttribute
public void setReqAndRes() {
User u1 = new User();
u1.setId(1L);
u1.setName("张三");
users.put(u1.getId(), u1);
User u2 = new User();
u2.setId(2L);
u2.setName("李四");
users.put(u2.getId(), u2);
}
// 用户登录
@RequestMapping(value = "login", method = RequestMethod.POST)
public ModelAndView doLogin(User user, HttpServletRequest request) {
request.getSession().setAttribute("uid", user.getId());
request.getSession().setAttribute("name", users.get(user.getId()).getName());
return new ModelAndView("redirect:talk");
}
// 跳转到交谈聊天页面
@RequestMapping(value = "talk", method = RequestMethod.GET)
public ModelAndView talk() {
return new ModelAndView("talk");
}
// 跳转到发布广播页面
@RequestMapping(value = "broadcast", method = RequestMethod.GET)
public ModelAndView broadcast() {
return new ModelAndView("broadcast");
}
// 发布系统广播(群发)
@ResponseBody
@RequestMapping(value = "broadcast", method = RequestMethod.POST)
public void broadcast(String text) throws IOException {
Message msg = new Message();
msg.setDate(new Date());
msg.setFrom(-1L);
msg.setFromName("系统广播");
msg.setTo(0L);
msg.setText(text);
handler.broadcast(new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg)));
}
// 发布系统广播(群发)
@ResponseBody
@RequestMapping(value = "test", method = RequestMethod.GET)
public void test(@RequestParam("text") String text) throws IOException {
Message msg = new Message();
msg.setDate(new Date());
msg.setFrom(-1L);
msg.setFromName("系统广播");
msg.setTo(0L);
msg.setText(text);
String output = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg);
System.out.println("output:" + output);
handler.broadcast(new TextMessage(output));
}
}
前端JS
<script>
var path = '<%=basePath%>';
var uid=${uid eq null?-1:uid};
if(uid==-1){
location.href="<%=basePath2%>";
}
var from=uid;
var fromName='${name}';
var to=uid==1?2:1;
var websocket;
if ('WebSocket' in window) {
websocket = new WebSocket("ws://" + path + "/ws?uid="+uid);
} else if ('MozWebSocket' in window) {
websocket = new MozWebSocket("ws://" + path + "/ws"+uid);
} else {
websocket = new SockJS("http://" + path + "/ws/sockjs"+uid);
}
websocket.onopen = function(event) {
console.log("WebSocket:已连接");
console.log(event);
};
websocket.onmessage = function(event) {
var data=JSON.parse(event.data);
console.log("WebSocket:收到一条消息",data);
var textCss=data.from==-1?"sfmsg_text":"fmsg_text";
$("#content").append("<div class='fmsg'><label class='name'>"+data.fromName+" "+data.date+"</label><div class='"+textCss+"'>"+data.text+"</div></div>");
scrollToBottom();
};
websocket.onerror = function(event) {
console.log("WebSocket:发生错误 ");
console.log(event);
};
websocket.onclose = function(event) {
console.log("WebSocket:已关闭");
console.log(event);
}
function sendMsg(){
var v=$("#msg").val();
if(v==""){
return;
}else{
var data={};
data["from"]=from;
data["fromName"]=fromName;
data["to"]=to;
data["text"]=v;
websocket.send(JSON.stringify(data));
$("#content").append("<div class='tmsg'><label class='name'>我 "+new Date().Format("yyyy-MM-dd hh:mm:ss")+"</label><div class='tmsg_text'>"+data.text+"</div></div>");
scrollToBottom();
$("#msg").val("");
}
}
function scrollToBottom(){
var div = document.getElementById('content');
div.scrollTop = div.scrollHeight;
}
Date.prototype.Format = function (fmt) { //author: meizz
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
function send(event){
var code;
if(window.event){
code = window.event.keyCode; // IE
}else{
code = e.which; // Firefox
}
if(code==13){
sendMsg();
}
}
function clearAll(){
$("#content").empty();
}
</script>