> 文章列表 > WebSocket入门

WebSocket入门

WebSocket入门

WebSocket

1.1websoket介绍

websocket是一种网络通信协议,RFC6455定义了它的通信标准

websocketHtml5开始提供的一种在单个TCP连接上进行全双工通讯的协议

Http协议是一种无状态、无连接、单向的应用层协议,它采用了请求/响应模型,通信请求只能有客户端发起,服务端对请求做出应答处理

这种通信模型有一个弊端: http协议无法实现服务器主动向客户端发送消息

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦,大多数Web应用程序将通过频繁的异步ajax请求实现长轮询,轮询的效率低,非常浪费资源(因为必须不停连接,或者Http连接始终打开)

Http协议:

WebSocket入门

Websocket:

WebSocket入门

** 1.2 websocket协议 **

本协议有两部分:握手和传输数据
握手是基于http协议的
来自客户端的握手看起来像如下形式:

WebSocket入门
来自服务器的握手看起来像如下形式:
WebSocket入门

字段说明:

WebSocket入门

1.3 客户端(浏览器)实现

1.3.1 websocket对象
实现Websockets的web浏览器通过WebSocket对象公开所有必需的客户端功能(主要指支持h5的浏览器)

以下API用于创建 Websocket对象:

var ws = new WebSocket(url)

参数url说明: ws: // ip地址: 端口号/资源名称

1.3.2 websocket事件

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接口中明确定义了其生命周期的方法,规范实现者确保生命周期的各个阶段调用实例相关的方法,生命周期方法如下:

WebSocket入门

** 服务端如何接收客户端发送的消息呢?**

通过为 session(websocket协议中的session,不是http协议的session)添加MessageHandler消息处理器来接收消息,当采用注解方式定义Endpoint时,我们还可以通过@OnMessage 注解指定接收消息的方法

服务端如何推送数据给客户端呢?

发送消息则由RemoteEndpoint 完成,其实例由Session维护,根据使用情况,我们可以通过Session.getBasicRemote获取同步消息发送的实例,然后调用其 sendXxx() 方法就可以发送消息,可以通过Session.getAsyncRemote 获取异步消息发送实例

服务端代码:
WebSocket入门

WebSocket入门

下面演示一个简单的聊天系统:

浏览器发送给服务器的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<>();;@Overridepublic 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 {@Autowiredprivate UserInterceptor userInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(userInterceptor).addPathPatterns("/main");}
}

websocket配置类

@Configuration
//websocket要做的配置类
public class WebSocketConfig {@Beanpublic 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;//建立连接时发送系统广播@OnOpenpublic 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();}}//用户之间的信息发送@OnMessagepublic 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();}}//用户断开连接的断后操作@OnClosepublic 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对象@Overridepublic 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";}
}

最终效果:

登陆:

WebSocket入门

WebSocket入门
具体代码地址:码云地址