WebSocket简介
目的
HTML5 WebSocket设计出来的目的就是取代轮询和长连接,使客户端浏览器具备像C/S框架下桌面系统的即时通讯能力,实现了浏览器和服务器全双工通信,建立在TCP之上,虽然WebSocket和HTTP一样通过TCP来传输数据,但WebSocket可以主动的向对方发送或接收数据,就像Socket一样;并且WebSocket需要类似TCP的客户端和服务端通过握手连接,连接成功后才能互相通信。
优点
双向通信、事件驱动、异步、使用ws或wss协议的客户端能够真正实现意义上的推送功能。
缺点
少部分浏览器不支持。
示例
社交聊天(微信、QQ)、弹幕、多玩家玩游戏、协同编辑、股票基金实时报价、体育实况更新、视频会议/聊天、基于位置的应用、在线教育、智能家居等高实时性的场景。
WebSocket请求响应客户端服务器交互图
WebSocket方式减少了很多TCP打开和关闭连接的操作,WebSocket的资源利用率高。
java WebSocket实现
Oracle 发布的 java 的 WebSocket 的规范是 JSR356规范 ,Tomcat从7.0.27开始支持WebSocket,从7.0.47开始支持JSR-356。
1、初始化一个springboot项目
2、加入websocket依赖
org.springframework.boot
spring-boot-starter-websocket
pom.xml如下:
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-websocket
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.0
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-tomcat
provided
org.springframework.boot
spring-boot-starter-test
test
3、编写websocket的服务端
3.1 WebSocketEndPoint是websocket服务端的核心
@PathParam是javax.websocket.server下的注解,是将路径中绑定的占位符的值取出来
在url中使用key和name,是想通过key和name对websocket的连接进行访问控制,这个key可以是用户登录后服务器给用户的令牌,通过令牌和和name进行权限验证(自己写拦截器或者继承权限框架实现),还可以通过key和name生成唯一值来进行在线websocket
连接的维护<(key+name), websocketSession>, 当然,我在这里没有这样做。
package com.geniuses.sewage_zero_straight.net.websocket;
import com.geniuses.sewage_zero_straight.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import static com.geniuses.sewage_zero_straight.net.websocket.WebSocketPool.*;
import static com.geniuses.sewage_zero_straight.net.websocket.WebSocketHandler.createKey;
@Slf4j
@Component
@ServerEndpoint("/net/websocket/{key}/{name}")//表明这是一个websocket服务的端点
public class WebSocketEndPoint {
private static UserService userService;
@Autowired
public void setUserService(UserService userService){
WebSocketEndPoint.userService = userService;
}
@OnOpen
public void onOpen(@PathParam("key") String key, @PathParam("name") String name, Session session){
log.info("有新的连接:{}", session);
add(createKey(key, name), session);
WebSocketHandler.sendMessage(session, key + name);
log.info("在线人数:{}",count());
sessionMap().keySet().forEach(item -> log.info("在线用户:", item));
for (Map.Entry item : sessionMap().entrySet()){
log.info("12: {}", item.getKey());
}
}
@OnMessage
public void onMessage(String message){
log.info("有新消息: {}", message);
}
@OnClose
public void onClose(@PathParam("key") String key, @PathParam("name") String name,Session session){
log.info("连接关闭: {}", session);
remove(createKey(key, name));
log.info("在线人数:{}",count());
sessionMap().keySet().forEach(item -> log.info("在线用户:", (item.split("@"))[1]));
for (Map.Entry item : sessionMap().entrySet()){
log.info("12: {}", item.getKey());
}
}
@OnError
public void onError(Session session, Throwable throwable){
try {
session.close();
} catch (IOException e) {
log.error("onError Exception: {}", e);
}
log.info("连接出现异常: {}", throwable);
}
}
3.2、WebSocketPool是websocket的在线连接池
package com.geniuses.sewage_zero_straight.net.websocket;
import lombok.extern.slf4j.Slf4j;
import javax.websocket.Session;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class WebSocketPool {
//在线用户websocket连接池
private static final Map ONLINE_USER_SESSIONS = new ConcurrentHashMap<>();
/**
* 新增一则连接
* @param key
* @param session
*/
public static void add(String key, Session session){
if (!key.isEmpty() && session != null){
ONLINE_USER_SESSIONS.put(key, session);
}
}
/**
* 根据Key删除连接
* @param key
*/
public static void remove(String key){
if (!key.isEmpty()){
ONLINE_USER_SESSIONS.remove(key);
}
}
/**
* 获取在线人数
* @return
*/
public static int count(){
return ONLINE_USER_SESSIONS.size();
}
/**
* 获取在线session池
* @return
*/
public static Map sessionMap(){
return ONLINE_USER_SESSIONS;
}
}
3.3、WebSocketHandler是websocket的动作处理工具
package com.geniuses.sewage_zero_straight.net.websocket;
import lombok.extern.slf4j.Slf4j;
import javax.websocket.RemoteEndpoint;
import javax.websocket.Session;
import java.io.IOException;
import static com.geniuses.sewage_zero_straight.net.websocket.WebSocketPool.sessionMap;
@Slf4j
public class WebSocketHandler {
/**
* 根据key和用户名生成一个key值,简单实现下
* @param key
* @param name
* @return
*/
public static String createKey(String key, String name){
return key + "@" + name;
}
/**
* 给指定用户发送信息
* @param session
* @param msg
*/
public static void sendMessage(Session session, String msg){
if (session == null)
return;
final RemoteEndpoint.Basic basic = session.getBasicRemote();
if (basic == null)
return;
try {
basic.sendText(msg);
} catch (IOException e) {
log.error("sendText Exception: {}", e);
}
}
/**
* 给所有的在线用户发送消息
* @param message
*/
public static void sendMessageAll(String message){
log.info("广播:群发消息");
sessionMap().forEach((key, session) -> sendMessage(session, message));
}
}
4、前端访问实现
4.1、index.html,页面引用了jquery和bootstrap样式,请自行应用
chat room websocket
聊天室
<
4.2、页面访问控制器,由此来访问index.html
package com.geniuses.sewage_zero_straight.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/view")
@Controller
public class ViewController {
/**
* 返回首页
* @return
*/
@GetMapping("/index")
public String index(){
return "index";
}
}
5、websocket配置
package com.geniuses.sewage_zero_straight.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
@EnableWebSocket
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
6、注意:
在使用了@ServerEndpoint注解的类是无法直接使用@Autowired的,因为@ServerEndpoint表明当前类是websocket的服务端点,在spring容器启动时会初始化一次该类,当有新的websocket连接的时候,也会进行该类实例的创建(每一次连接时都会创建一个实例),所以在第二次往后创建该类实例的时候,就无法进行有效的@Autowired了,此时发现,即便第一次注入是有效的,但是也没有什么用。这个时候,将需要注入的变量置为类的变量,提供一个set方法(该方法为实例方法),在set方法上面进行依赖注入,这样就可以进行有效的注入了。