WebSocket是一种在单个TCP连接上进行全双工通信的协议。它最初于2008年被提出,后来由IETF标准化。WebSocket协议旨在解决HTTP协议的一些限制,例如HTTP请求只能由客户端发起,服务器不能主动向客户端发送数据等。
早期,很多网站为了实现推送技术,所用的技术都是轮询。轮询是指由浏览器每隔一段时间向服务器发出HTTP请求,然后服务器返回最新的数据给客户端。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求与回复可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源。
在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
如上图所示,从上图看出可以,WebSocket和HTTP轮询本质上都依赖于TCP的握手,他们二者都是应用层的协议,事实上,WebSocket和HTTP的工作端口都是80和443,WebSocket可以使用HTTP代理和中介,兼容HTTP协议。
不同的是HTTP轮询获取信息每次都需要客户端向服务端发送请求建立连接,而WebScoket经过第一次建立连接后,连接就被持久化下来, 不需要重复建立连接,而且可以由服务端主动向客户端发送信息。
WebSocket的优点包括:
WebSocket的缺点包括:
下面用一个简单的示例,展示Spring boot如何集成WebSocket,然后实现简单的建立连接后服务器向客户端发送信息并向其他在线用户广播。
新建Spring boot项目,在pom文件加入以下代码
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-websocket
org.projectlombok
lombok
true
com.alibaba
fastjson
1.2.83
创建如下文件夹:
在interceptor文件家新建WebSocketInterceptor类文件,代码如下:
@Component
public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
return super.beforeHandshake(request, response, wsHandler, attributes);
}
}
在handler文件夹中新建WebSocketHandler类文件
@Component
public class WebSocketHandler extends TextWebSocketHandler implements InitializingBean {
@Autowired
private ApplicationContext applicationContext;
@Override
public void afterPropertiesSet() {
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
super.afterConnectionEstablished(session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
super.handleTextMessage(session,message);
}
@Override
protected void handlePongMessage(WebSocketSession session, PongMessage message) {
super.handlePongMessage(session, message);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
super.handleTransportError(session, exception);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
super.afterConnectionClosed(session, status);
}
@Override
public boolean supportsPartialMessages() {
return super.supportsPartialMessages();
}
}
在config文件夹新建WebSocketConfig类文件
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private WebSocketHandler webSocketHandler;
@Autowired
private WebSocketInterceptor webSocketInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//配置拦截器、处理器,设置跨域
registry.addHandler(webSocketHandler, "/")
.addInterceptors(webSocketInterceptor)
.setAllowedOrigins("*");
}
}
到这里,启动Spring boot项目,网站通过ws命令就可以连接到WebSocket。不过为了展示WebSocket的通信功能,我们要先实现WebSocket的信息处理相关机制。
在message文件夹定义Message接口类:
public interface Message {
String getType();
}
为了实现建立连接后服务端向客户端发送消息,我们需要实现一个连接认证的请求消息,一个请求的回复消息。
@Data
public class AuthRequestMessage implements Message{
public static final String TYPE = "AUTH_REQUEST";
private String token;
@Override
public String getType() {
return TYPE;
}
}
@Data
public class AuthResponseMessage implements Message{
public static final String TYPE = "AUTH_RESPONSE";
private Integer code;
private String data;
@Override
public String getType() {
return TYPE;
}
}
创建以上消息后,在handler文件夹中创建MessageHandler接口类:
public interface MessageHandler {
void execute(WebSocketSession webSocketSession, T message);
String getType();
}
然后在utils文件夹创建WebSocketUtils工具类用来管理WebSocket消息的发送。
@Component
public class WebSocketUtils {
private static final Map USER_SESSION_MAP = new ConcurrentHashMap<>();
private static final Map SESSION_USER_MAP = new ConcurrentHashMap<>();
public static void addSession(WebSocketSession session, String user) {
USER_SESSION_MAP.put(user, session);
SESSION_USER_MAP.put(session, user);
}
public static void removeSession(WebSocketSession session) {
String user = SESSION_USER_MAP.remove(session);
if (StringUtils.isNotEmpty(user)) {
USER_SESSION_MAP.remove(user);
}
}
public static void send(WebSocketSession session, String type, T message) throws JSONException, IOException {
String messageText = buildTextMessage(type, message);
sendTextMessage(session, messageText);
}
public static boolean send(String user, String type, T message) throws JSONException, IOException {
WebSocketSession session = USER_SESSION_MAP.get(user);
if (session == null) {
return false;
}
send(session, type, message);
return true;
}
private static String buildTextMessage(String type, T message) throws JSONException {
JSONObject messageObject = new JSONObject();
messageObject.put("type", type);
messageObject.put("body", message);
return messageObject.toString();
}
private static void sendTextMessage(WebSocketSession session, String messageText) throws IOException {
if (session == null) {
return;
}
session.sendMessage(new TextMessage(messageText));
}
}
在handler文件夹创建连接认证消息的处理类。
@Component
public class AuthRequestMessageHandler implements MessageHandler {
@Override
public void execute(WebSocketSession webSocketSession, AuthRequestMessage message) {
try {
if (StringUtils.isEmpty(message.getToken())) {
AuthResponseMessage authResponseMessage = new AuthResponseMessage();
authResponseMessage.setCode(1);
authResponseMessage.setData("token未传入");
WebSocketUtils.send(webSocketSession, AuthResponseMessage.TYPE, authResponseMessage);
return;
}
WebSocketUtils.addSession(webSocketSession, message.getToken());
AuthResponseMessage authResponseMessage = new AuthResponseMessage();
authResponseMessage.setCode(0);
authResponseMessage.setData("登录成功");
WebSocketUtils.send(webSocketSession, AuthResponseMessage.TYPE, authResponseMessage);
} catch (JSONException | IOException e) {
throw new RuntimeException(e);
}
}
@Override
public String getType() {
return AuthRequestMessage.TYPE;
}
}
从上面的代码可以看到有个token的概念,那么这个是从哪里获取的呢?答案是在前面的WebSocketInterceptor和WebSocketHandler中,修改如下:
WebSocketInterceptor:
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request;
attributes.put("token", servletServerHttpRequest.getServletRequest().getParameter("token"));
}
return super.beforeHandshake(request, response, wsHandler, attributes);
}
WebSocketHandler:
private final Map HANDLERS = new HashMap<>();
private final Map> MESSAGE_CLASS = new HashMap<>();
@Override
public void afterPropertiesSet() {
applicationContext.getBeansOfType(MessageHandler.class).values()
.forEach(messageHandler -> HANDLERS.put(messageHandler.getType(), messageHandler));
applicationContext.getBeansOfType(Message.class).values()
.forEach(message -> MESSAGE_CLASS.put(message.getType(), message.getClass()));
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String token = (String) session.getAttributes().get("token");
AuthRequestMessage authRequestMessage = new AuthRequestMessage();
authRequestMessage.setToken(token);
MessageHandler messageHandler = HANDLERS.get(AuthRequestMessage.TYPE);
if (messageHandler == null) {
return;
}
messageHandler.execute(session, authRequestMessage);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
try {
JSONObject jsonMessage = JSON.parseObject(message.getPayload());
String messageType = jsonMessage.getString("type");
MessageHandler messageHandler = HANDLERS.get(messageType);
if (messageHandler == null) {
return;
}
Class extends Message> messageClass = MESSAGE_CLASS.get(messageType);
if (messageClass == null) {
return;
}
Message messageObj = JSON.parseObject(jsonMessage.getString("body"), messageClass);
messageHandler.execute(session, messageObj);
} catch (Exception ignored) {
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
WebSocketUtils.removeSession(session);
}
这样建立连接服务端向客户端回复的机制就实现了,我们还要实现一个建立连接通知所有在线用户的功能,有了前面的铺垫,这个功能实现很简单。
在message文件夹下创建UserLoginNoticeMessage:
@Data
public class UserLoginNoticeMessage implements Message{
public static final String TYPE = "USER_LOGIN_NOTICE";
private String nickname;
@Override
public String getType() {
return TYPE;
}
}
修改WebSocketUtils,增加broadcast方法
public static void broadcast(String type, T message) throws IOException, JSONException {
String messageText = buildTextMessage(type, message);
for (WebSocketSession session : SESSION_USER_MAP.keySet()) {
sendTextMessage(session, messageText);
}
}
修改AuthRequestMessageHandler的excute方法:
@Override
public void execute(WebSocketSession webSocketSession, AuthRequestMessage message) {
try {
if (StringUtils.isEmpty(message.getToken())) {
AuthResponseMessage authResponseMessage = new AuthResponseMessage();
authResponseMessage.setCode(1);
authResponseMessage.setData("token未传入");
WebSocketUtils.send(webSocketSession, AuthResponseMessage.TYPE, authResponseMessage);
return;
}
WebSocketUtils.addSession(webSocketSession, message.getToken());
AuthResponseMessage authResponseMessage = new AuthResponseMessage();
authResponseMessage.setCode(0);
authResponseMessage.setData("登录成功");
WebSocketUtils.send(webSocketSession, AuthResponseMessage.TYPE, authResponseMessage);
UserLoginNoticeMessage userLoginNoticeMessage = new UserLoginNoticeMessage();
userLoginNoticeMessage.setNickname(message.getToken());
WebSocketUtils.broadcast(UserLoginNoticeMessage.TYPE, userLoginNoticeMessage);
} catch (JSONException | IOException e) {
throw new RuntimeException(e);
}
}
为了方便测试,我们直接选择用在线的WebSocket测试工具。
WebSocket在线测试工具
启动服务,然后同时启动三个测试工具,用以下代码开启链接:
ws://127.0.0.1:8080/?token=1
ws://127.0.0.1:8080/?token=2
ws://127.0.0.1:8080/?token=3
开启链接,接收到信息如下:
可以看到,每个WebSocket客户端链接成功都收到了服务端返回的信息,然后每次新客户端链接成功时,已存在的老客户端会收到新客户端登录的信息,说明我们实现了链接后返回信息并给其他在线用户广播的功能。
首先定义一个websocketServer类
@Component
@ServerEndpoint("你自己想要定义的ws路径")
public class WebSocketServer {
}
在此类中分别添加自定义方法然后分别打上@OnOpen、@OnClose、@OnMessage、@OnError注解
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session){
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session){
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@OnMessage
public void onMessage(String message, Session session) {
}
/**
* 发生错误时调用
*
* @param session 可选的参数
* @param error 错误
*/
@OnError
public void onError(Session session, Throwable error) {
}
最后定义config类
@Configuration
@EnableWebSocket
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpoint(){
return new ServerEndpointExporter();
}
}