WebSocket
1.1websoket介绍
websocket
是一种网络通信协议,RFC6455定义了它的通信标准
websocket
是Html5
开始提供的一种在单个TCP
连接上进行全双工通讯的协议
Http协议
是一种无状态、无连接、单向的应用层协议,它采用了请求/响应模型,通信请求只能有客户端发起,服务端对请求做出应答处理
这种通信模型有一个弊端: http协议无法实现服务器主动向客户端发送消息
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦,大多数Web应用程序将通过频繁的异步ajax请求实现长轮询,轮询的效率低,非常浪费资源(因为必须不停连接,或者Http连接始终打开)
Http协议:
Websocket:
** 1.2 websocket协议 **
本协议有两部分:握手和传输数据
握手是基于http协议的
来自客户端的握手看起来像如下形式:
来自服务器的握手看起来像如下形式:
字段说明:
1.3 客户端(浏览器)实现
1.3.1 websocket对象
实现Websockets的web浏览器通过WebSocket对象公开所有必需的客户端功能(主要指支持h5的浏览器)
以下API用于创建 Websocket对象:
var ws = new WebSocket(url)
参数url说明: ws: // ip地址: 端口号/资源名称
1.3.2 websocket事件
1.4 服务端实现
Tomcat的7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356)
Java Websocket
应用由一系列的WebsocketEndPoint
组成,EndPoint是一个Java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口,就像Servlet之与http请求一样
我们可以通过两种方式定义Endpoint:
- 第一种是编程式,即继承类 javax.websocket.Endpoint并实现其方法
- 第二种是注解类,即定义一个pojo,并添加@ServerEndpoint相关注解
EndPoint 实例在WebSocket握手时创建,并在客户端与服务端连接过程中有效,最后在连接关闭时结束,在Endpoint接口中明确定义了其生命周期的方法,规范实现者确保生命周期的各个阶段调用实例相关的方法,生命周期方法如下:
** 服务端如何接收客户端发送的消息呢?**
通过为 session(websocket协议中的session,不是http协议的session)
添加MessageHandler
消息处理器来接收消息,当采用注解方式定义Endpoint
时,我们还可以通过@OnMessage
注解指定接收消息的方法
服务端如何推送数据给客户端呢?
发送消息则由RemoteEndpoint
完成,其实例由Session
维护,根据使用情况,我们可以通过Session.getBasicRemote
获取同步消息发送的实例,然后调用其 sendXxx()
方法就可以发送消息,可以通过Session.getAsyncRemote
获取异步消息发送实例
服务端代码:
下面演示一个简单的聊天系统:
浏览器发送给服务器的WebSocket数据:
/**
* 浏览器发送给服务器的WebSocket数据
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Message {
private String toName;
private String message;
private String fromName;
}
//登录时用到的信息
// 用于登陆响应给浏览器的数据
@Data
@AllArgsConstructor
@NoArgsConstructor
//登录时用到的信息
// 用于登陆响应给浏览器的数据
public class Result {
private boolean flag;
private String message;
}
//用户间传送的消息
// 服务器发送给浏览器的WebSocket数据
package com.websocket.websocketdemo.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
//用户间传送的消息
// 服务器发送给浏览器的WebSocket数据
public class ResultMessage {
private boolean isSystem;
private String fromName;
//private String toName;
private Object message; // 如果是系统消息是数组
}
配置拦截器:
package com.websocket.websocketdemo.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Slf4j
public class UserInterceptor implements HandlerInterceptor {
//没用数据库,暂且用集合来存储已登录等用户,进行拦截。
public static Map<String, String> onLineUsers = new ConcurrentHashMap<>();;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession httpSession = request.getSession();
String username = (String) httpSession.getAttribute("user");
log.info("进入拦截器"+"==="+"进入拦截器的用户是:"+username);
if(username != null && !onLineUsers.containsKey(username)){
onLineUsers.put(username,username);
log.info("已进入拦截器判断");
log.info("已存储的用户01"+onLineUsers);
return true;
}else {
log.info("已存储的用户02" + onLineUsers);
log.info("未进入判断,进行重定向");
httpSession.removeAttribute("user");
response.sendRedirect("/loginerror");
return false;
}
}
}
使拦截器生效:
//配置拦截路径
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {
@Autowired
private UserInterceptor userInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor)
.addPathPatterns("/main");
}
}
websocket配置类
@Configuration
//websocket要做的配置类
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
//封装发送的消息内容
/**
- 用来封装消息的工具类
*/
//封装发送的消息内容
/**
* 用来封装消息的工具类
*/
public class MessageUtils {
public static String getMessage(boolean isSystemMessage,String fromName,Object message){
try{
ResultMessage resultMessage = new ResultMessage();
resultMessage.setSystem(isSystemMessage);
resultMessage.setMessage(message);
if(fromName != null){
resultMessage.setFromName(fromName);
}
// if(toName !=null ){
// resultMessage.setToName(toName);
// }
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(resultMessage);
}catch (JsonProcessingException e){
e.printStackTrace();
}
return null;
}
}
websocket对应客户端某个对象:
@Slf4j
@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfigurator.class)
@Component
public class ChatEndPoint {
//用线程安全的map来保存当前用户
private static Map<String,ChatEndPoint> onLineUsers = new ConcurrentHashMap<>();
//声明一个session对象,通过该对象可以发送消息给指定用户,不能设置为静态,每个ChatEndPoint有一个session才能区分.(websocket的session)
private Session session;
//保存当前登录浏览器的用户
private HttpSession httpSession;
//建立连接时发送系统广播
@OnOpen
public void onOpen(Session session, EndpointConfig config){
this.session = session;
HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;
String username = (String) httpSession.getAttribute("user");
log.info("上线用户名称:"+username);
onLineUsers.put(username,this);
String message = MessageUtils.getMessage(true,null,getNames());
broadcastAllUsers(message);
}
//获取当前登录的用户
private Set<String> getNames(){
return onLineUsers.keySet();
}
//发送系统消息
private void broadcastAllUsers(String message){
try{
Set<String> names = onLineUsers.keySet();
for(String name : names){
ChatEndPoint chatEndPoint = onLineUsers.get(name);
chatEndPoint.session.getBasicRemote().sendText(message);
}
}catch (Exception e){
e.printStackTrace();
}
}
//用户之间的信息发送
@OnMessage
public void onMessage(String message,Session session){
try{
ObjectMapper mapper = new ObjectMapper();
Message mess = mapper.readValue(message,Message.class);
String toName = mess.getToName();
String data = mess.getMessage();
String username = (String) httpSession.getAttribute("user");
log.info(username + "向"+toName+"发送的消息:" + data);
String resultMessage = MessageUtils.getMessage(false,username,data);
if(StringUtils.hasLength(toName)) {
onLineUsers.get(toName).session.getBasicRemote().sendText(resultMessage);
}
}catch (Exception e){
e.printStackTrace();
}
}
//用户断开连接的断后操作
@OnClose
public void onClose(Session session){
String username = (String) httpSession.getAttribute("user");
log.info("离线用户:"+ username);
if (username != null){
onLineUsers.remove(username);
UserInterceptor.onLineUsers.remove(username);
}
httpSession.removeAttribute("user");
String message = MessageUtils.getMessage(true,null,getNames());
broadcastAllUsers(message);
}
}
import org.springframework.context.annotation.Configuration;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
//@Configuration
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
//此方法用来获取httpssion对象
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
if(httpSession != null) {
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
}
}
}
控制层:
package com.websocket.websocketdemo.controller;
import com.websocket.websocketdemo.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
@RestController
//模拟登录操作
@Slf4j
public class CertificationController {
@RequestMapping("/toLogin")
public Result toLogin(String user, String pwd, HttpSession httpSession){
Result result = new Result();
httpSession.setMaxInactiveInterval(30*60);
log.info(user+"登录验证中..");
if(httpSession.getAttribute("user") != null){
result.setFlag(false);
result.setMessage("不支持一个浏览器登录多个用户!");
return result;
}
if ("张三".equals(user)&&"123".equals(pwd)){
result.setFlag(true);
log.info(user+"登录验证成功");
httpSession.setAttribute("user",user);
}else if ("李四".equals(user)&&"123".equals(pwd)){
result.setFlag(true);
log.info(user+"登录验证成功");
httpSession.setAttribute("user",user);
}else if ("田七".equals(user)&&"123".equals(pwd)){
result.setFlag(true);
log.info(user+"登录验证成功");
httpSession.setAttribute("user",user);
}
else if ("王五".equals(user)&&"123".equals(pwd)){
result.setFlag(true);
log.info(user+"登录验证成功");
httpSession.setAttribute("user",user);
}else {
result.setFlag(false);
log.info(user+"验证失败");
result.setMessage("登录失败");
}
return result;
}
@RequestMapping("/getUsername")
public String getUsername(HttpSession httpSession){
String username = (String) httpSession.getAttribute("user");
return username;
}
}
package com.websocket.websocketdemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
//页面跳转
public class PageController {
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/main")
public String main(){
return "main";
}
@RequestMapping("/loginerror")
public String longinError(){
return "loginerror";
}
}
最终效果:
登陆:
具体代码地址:码云地址