组件整合之websocket
WebSocket,干什么用的?我们有了HTTP,为什么还要用WebSocket?很多同学都会有这样的疑问。我们先来看一个场景,大家的手机里都有微信,在微信中,只要有新的消息,这个联系人的前面就会有一个红点,这个需求要怎么实现呢?最简单,最笨的方法就是客户端轮询,在微信的客户端每隔一段时间(比如:1s或者2s),向服务端发送一个请求,查询是否有新的消息,如果有消息就显示红点。这种方法是不是太笨了呢?每次都要客户端去发起请求,难道就不能从服务端发起请求吗?这样客户端不就省事了吗。再看看股票软件,每个股票的当前价格都是实时的,这我们怎么做,每个一秒请求后台查询当前股票的价格吗?这样效率也太低了吧,而且时效性也很低。这就需要我们今天的主角WebSocket去实现了。
什么是WebSocket
WebSocket协议,它是通过一个TCP连接,在客户端与服务端之间建立的一个全双工、双向的通信渠道。它是一个不同于HTTP的TCP协议,但是它通过HTTP工作。它的默认端口也是80和443,和HTTP是一样的。
一个WebSocket的交互开始于一个HTTP请求,这是一个握手请求,这个请求中包含一个Upgrade请求头,具体如下:
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
我们看到的第3行和第4行就是这个特殊的请求头,既然包含了这个特殊的请求头,那么请求就要升级,升级成WebSocket请求。这个握手请求的响应也比较特殊,它的成功状态码是101,而不是HTTP的200,如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
在这次成功的握手请求以后,在客户端和服务端之间的socket被打开,客户端和服务端可以进行消息的发送和接收。
快速入门
Spring在4.0后将websocket集成进去,要使用Spring的websocket的话,spring的版本要在4.0及以上。
1、pom配置
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
2、spring对websocket的注解配置
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketServerConfig implements WebSocketConfigurer {
@Autowired
private SocketHandler socketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//注册处理拦截器,拦截url为websocket的请求
registry.addHandler(socketHandler, "/websocket").addInterceptors(new WebSocketInterceptor()).setAllowedOrigins("*");
//注册SockJs的处理拦截器,拦截url为/sockjs/websocket的请求
registry.addHandler(socketHandler, "/sockjs/websocket").addInterceptors(new WebSocketInterceptor()).setAllowedOrigins("*").withSockJS();
}
}
3、握手拦截器,继承HttpSessionHandshakeInterceptor类,做一些连接握手或者握手后的一些处理
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import javax.servlet.http.HttpSession;
import java.util.Map;
/**
* @desp websocket拦截器
*
* HandshakeInterceptor
* WebSocket握手请求的拦截器. 检查握手请求和响应, 对WebSocketHandler传递属性
*/
public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor {
// 握手前
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
System.out.println("Before Handshaske");
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
// getSession(boolean create)意思是返回当前request中的HttpSession ,
// 如果当前request中的HttpSession为null,当create为true,就创建一个新的Session,否则返回null;
HttpSession session = servletRequest.getServletRequest().getSession(false);
if (session != null) {
//使用userName区分WebSocketHandler,以便定向发送消息
String userName = (String) session.getAttribute("SESSION_USERNAME");
if (userName==null) {
userName="default-system";
}
attributes.put("WEBSOCKET_USERNAME",userName);
}
}
return super.beforeHandshake(request, response, wsHandler, attributes);
}
// 握手后
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
System.out.println("++++++++++++++++ HandshakeInterceptor: afterHandshake ++++++++++++++");
super.afterHandshake(request, response, wsHandler, ex);
}
}
4、创建SocketHandler类继承WebSocketHandler类(Spring提供的有AbstractWebSocketHandler类、TextWebSocketHandler类、BinaryWebSocketHandler类,看自己需要进行继承),该类主要是用来处理消息的接收和发送
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.ArrayList;
/**
* @desp Socket处理类
*/
@Component
public class SocketHandler implements WebSocketHandler {
//这个会出现性能问题,最好用Map来存储,key用userid
private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();
private static Logger logger = LoggerFactory.getLogger(SocketHandler.class);
public SocketHandler() {
}
/**
* 连接成功时候,会触发页面上onopen方法
*/
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("connect to the websocket success......当前数量:" + users.size());
users.add(session);
//这块会实现自己业务,比如,当用户登录后,会把离线消息推送给用户
//TextMessage returnMessage = new TextMessage("你将收到的离线");
//session.sendMessage(returnMessage);
}
/**
* js调用websocket.send时候,会调用该方法
*/
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
System.err.println(webSocketSession + "---->" + webSocketMessage + ":" + webSocketMessage.getPayload().toString());
String userName = (String) webSocketSession.getAttributes().get("WEBSOCKET_USERNAME");
TextMessage returnMessage = new TextMessage(userName + ": 【" + webSocketMessage.getPayload().toString()+"】");
webSocketSession.sendMessage(returnMessage);
}
/**
* 关闭连接时触发
*/
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
logger.debug("websocket connection closed......");
String username = (String) session.getAttributes().get("WEBSOCKET_USERNAME");
System.out.println("用户" + username + "已退出!");
users.remove(session);
System.out.println("剩余在线用户" + users.size());
}
/**
* 异常时触发
*
* @param session
* @param exception
* @throws Exception
*/
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if (session.isOpen()) {
session.close();
}
logger.debug("websocket connection closed......");
users.remove(session);
}
public boolean supportsPartialMessages() {
return false;
}
/**
* 给某个用户发送消息
*
* @param userName
* @param message
*/
public void sendMessageToUser(String userName, TextMessage message) {
for (WebSocketSession user : users) {
if (user.getAttributes().get("WEBSOCKET_USERNAME").equals(userName)) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
break;
}
}
}
/**
* 给所有在线用户发送消息
*
* @param message
*/
public void sendMessageToUsers(TextMessage message) {
for (WebSocketSession user : users) {
try {
if (user.isOpen()) {
user.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
要进行发送消息的操作,自己可以写方法,利用保存的已经完成连接的WebSocketSession,通过调用sendMessage(WebScoketMessage<?> message)方法进行消息的发送,参数message是发送的消息内容,Spring提供的类型有TextMessage、BinaryMessage、PingMessage、PongMessage。
5、login.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<h2>Hello World!</h2>
<body>
<form action="/websocket/login">
登录名:<input type="text" name="username"/>
<input type="submit" value="登录"/>
</form>
</body>
</body>
</html>
6、websocket.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Insert title here</title>
</head>
<body>
请输入:<textarea rows="5" cols="10" id="inputMsg" name="inputMsg"></textarea>
<button onclick="doSend();">发送</button>
<br/>
<hr/>
<div id="div">
</div>
<script type="text/javascript" src="http://cdn.bootcss.com/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.js"></script>
<script type="text/javascript">
var receiveDiv = document.getElementById('div');
var websocket = null;
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8888/websocket");
} else {
websocket = new SockJS("http://localhost:8888/quicksand/sockjs/websocket");
}
websocket.onopen = function (event) {
console.log(event);
websocket.send('websocket client connect test');
};
websocket.onmessage = function (event) {
console.log(event);
receiveDiv.innerHTML += "<br/>";
receiveDiv.innerHTML += (' @_@ ' + event.data + ' ~_~ ');
};
websocket.onerror = function (event) {
console.log(event);
};
websocket.onclose = function (event) {
console.log(event);
};
function doSend() {
if (websocket.readyState == websocket.OPEN) {
var msg = document.getElementById("inputMsg").value;
websocket.send(msg);//调用后台handleTextMessage方法
alert("发送成功!");
} else {
alert("连接失败!");
}
}
window.close = function () {
websocket.onclose();
}
</script>
</body>
</html>
7、写一个websocket控制器
import com.spring.ws.SocketHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.socket.TextMessage;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* @desp Socket控制器
*
*/
@Controller
public class SocketController{
private static final Logger logger = LoggerFactory.getLogger(SocketController.class);
@Autowired
private SocketHandler socketHandler;
@RequestMapping("/websocket/login")
public ModelAndView login(HttpServletRequest request, HttpServletResponse response) throws Exception {
String username = request.getParameter("username");
System.out.println(username+"登录");
HttpSession session = request.getSession(true);
session.setAttribute("SESSION_USERNAME", username);
//response.sendRedirect("/quicksand/jsp/websocket.jsp");
return new ModelAndView("websocket");
}
@RequestMapping("/websocket/send")
@ResponseBody
public String send(HttpServletRequest request) {
String username = request.getParameter("username");
socketHandler.sendMessageToUser(username, new TextMessage("你好,测试!!!!"));
return "发送成功";
}
}
8、启动项目,访问http://localhost:8888/websocket/login?username=harvey
常见错误
前台403,后台未报错
这种情况需要配置:setAllowedOrigins(*)
//注册处理拦截器,拦截url为websocket的请求
registry.addHandler(socketHandler, "/websocket").addInterceptors(new WebSocketInterceptor()).setAllowedOrigins("*");
//注册SockJs的处理拦截器,拦截url为/sockjs/websocket的请求
registry.addHandler(socketHandler, "/sockjs/websocket").addInterceptors(new WebSocketInterceptor()).setAllowedOrigins("*").withSockJS();