一 Websocket简介
1.1 是什么
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
1.2 出现背景
很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。
在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
1.3 优点
-
较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
-
更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
-
保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
-
更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
-
可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
-
更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
推送消息数目 即时通讯 直播间弹幕 等
二 Websocket入门-springboot
websocket是支持一对一(用户发给用户,管理员发给特定用户),一对多(管理员推送给一组用户)消息推送的,故接下来我们对这两种场景进行入门测试。
2.1 步骤分析
搭建springboot环境
springboot支持websocket
websocket服务端实现-一对一
websocket客户端实现-一对一
websocket服务端实现-一对多
websocket客户端实现-一对多
2.2 实现
2.2.1 搭建springboot环境
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
server:
port: 18092
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class,args);
}
}
2.2.2 springboot支持websocket
<dependency>
<!-- websocket -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<!-- fastjson -->
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
@Configuration
public class WebSocketConfig {
/**
* 注入一个ServerEndpointExporter,该Bean会自动注册
* 使用@ServerEndpoint注解申明的websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2.2.3 websocket一对一测试
//后台
package cn.itsource.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 前后端交互的类实现消息的接收推送(自己发送给自己)
*
* @ServerEndpoint(value = "/test/one") 前端通过此URI和后端交互,建立连接
*/
@Slf4j
@ServerEndpoint(value = "/test/one")
@Component
public class OneWebSocket {
/**
* 记录当前在线连接数
*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
onlineCount.incrementAndGet(); // 在线数加1
log.info("有新连接加入:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
onlineCount.decrementAndGet(); // 在线数减1
log.info("有一连接关闭:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("服务端收到客户端[{}]的消息:{}", session.getId(), message);
this.sendMessage("Hello, " + message, session);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 服务端发送消息给客户端
*/
private void sendMessage(String message, Session toSession) {
try {
log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);
toSession.getBasicRemote().sendText(message);
} catch (Exception e) {
log.error("服务端发送消息给客户端失败:{}", e);
}
}
}
//前台 resources/static/index.html
<!DOCTYPE HTML>
<html>
<head>
<title>My WebSocket</title>
</head>
<body>
<input id="text" type="text" />
<button onclick="send()">Send</button>
<button onclick="closeWebSocket()">Close</button>
<div id="message"></div>
</body>
<script type="text/javascript">
var websocket = null;
//判断当前浏览器是否支持WebSocket, 主要此处要更换为自己的地址
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:18092/test/one");
// websocket = new WebSocket("ws://localhost:18092/test/oneToMany");
} else {
alert('Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function() {
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(event) {
//setMessageInnerHTML("open");
}
//接收到消息的回调方法
websocket.onmessage = function(event) {
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function() {
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function() {
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
//发送消息
function send() {
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</html>
启动测试
2.2.4 websocket一对多测试
package cn.itsource.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* 前后端交互的类实现消息的接收推送(自己发送给所有人(不包括自己))
*
* @ServerEndpoint(value = "/test/oneToMany") 前端通过此URI 和后端交互,建立连接
*/
@Slf4j
@ServerEndpoint(value = "/test/oneToMany")
@Component
public class OneToManyWebSocket {
/** 记录当前在线连接数 */
private static AtomicInteger onlineCount = new AtomicInteger(0);
/** 存放所有在线的客户端 */
private static Map<String, Session> clients = new ConcurrentHashMap<>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
onlineCount.incrementAndGet(); // 在线数加1
clients.put(session.getId(), session);
log.info("有新连接加入:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
onlineCount.decrementAndGet(); // 在线数减1
clients.remove(session.getId());
log.info("有一连接关闭:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 收到客户端消息后调用的方法
*
* @param message
* 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("服务端收到客户端[{}]的消息:{}", session.getId(), message);
this.sendMessage(message, session);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 群发消息
*
* @param message
* 消息内容
*/
private void sendMessage(String message, Session fromSession) {
for (Map.Entry<String, Session> sessionEntry : clients.entrySet()) {
Session toSession = sessionEntry.getValue();
// 排除掉自己
if (!fromSession.getId().equals(toSession.getId())) {
log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);
toSession.getAsyncRemote().sendText(message);
}
}
}}
前台 //修改上面的一个访问地址为多个访问地址就ok
websocket = new WebSocket("ws://localhost:18092/test/oneToMany");
//启动测试
2.3 小结
三 Websocket实战-消息推送
3.1 非集群环境Websocket
3.1.1 需求分析
环境:一个websocket服务器
实现:
用户1给用户2发送站内信,如果用户2在线实时显示消息总数!
管理员给用户1,用户2发站内信,如果他们在线显示消息总数!
3.1.2 方案设计
3.1.3 代码实现
package cn.itsource.msg.websocket;
import cn.itsource.basic.util.LoginContext;
import cn.itsource.basic.util.SpringUtil;
import cn.itsource.msg.domain.Message;
import cn.itsource.msg.service.IMessageService;
import cn.itsource.user.domain.LoginInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* 前后端交互的类实现消息的接收推送(自己发送给所有人(不包括自己))
*
* @ServerEndpoint(value = "/test/oneToMany") 前端通过此URI 和后端交互,建立连接
*/
@Slf4j
@ServerEndpoint(value = "/msg/{token}")
@Component
public class MsgWebSocket {
/** 记录当前在线连接数 */
private static AtomicInteger onlineCount = new AtomicInteger(0);
/** 存放所有在线的客户端 */
private static Map<String, Session> clients = new ConcurrentHashMap<>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) {
RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("redisTemplate");
LoginInfo info = (LoginInfo) redisTemplate.opsForValue().get(token);
System.out.println(info);
//已连接上就获取信息数目发送
IMessageService messageService = SpringUtil.getBean(IMessageService.class);
Integer count = messageService.queryMsgTotal(info.getId());
session.getAsyncRemote().sendText(count+"");
onlineCount.incrementAndGet(); // 在线数加1
clients.put(info.getId()+"", session);
log.info("有新连接加入:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session,@PathParam("token") String token) {
onlineCount.decrementAndGet(); // 在线数减1
clients.remove(token);
log.info("有一连接关闭:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 收到客户端消息后调用的方法
*
* @param message
* 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("服务端收到客户端[{}]的消息:{}", session.getId(), message);
this.sendMessage(message, session);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
public void pushMsg(Message message){
IMessageService messageService = SpringUtil.getBean(IMessageService.class);
//发消息
Session session = clients.get(message.getRec_id() + "");
session.getAsyncRemote().sendText(messageService.queryMsgTotal(message.getRec_id())+"");
}
}
发送接口
controller
@PostMapping("/push/{id}")
public AjaxResult pushMsg(@PathVariable("id") Long id){
try {
messageService.push(id);
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("发送失败!"+e.getMessage());
}
}
service
@Autowired
private MsgWebSocket msgWebSocket;
@Autowired
private MessageMapper messageMapper;
@Override
public void push(Long id) {
Message message = messageMapper.loadById(id);
//非集群环境
msgWebSocket.pushMsg(message);
}
3.1.4 小结
3.2 集群环境Websocket
3.2.1 需求分析
环境:多个websocket服务器进行集群
实现:
用户1给用户2发送站内信,如果用户2在线实时显示消息总数!
管理员给用户1,用户2发站内信,如果他们在线显示消息总数!
3.2.2 方案设计
3.2.3 代码实现
rabbitmq支持
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
yml
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
发送配置
@Configuration
public class SendConfig {
@Bean("queue")
public Queue queueMsg(){
return new Queue("queue");
}
@Bean("exchange")
public TopicExchange exchange(){
return new TopicExchange("exchange");
}
@Bean
Binding bindingExchangeAndQueuemsg(@Qualifier("queue") Queue queue, @Qualifier("exchange") TopicExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("a");
}
}
websocket
package cn.itsource.msg.websocket;
import cn.itsource.basic.util.LoginContext;
import cn.itsource.basic.util.SpringUtil;
import cn.itsource.msg.domain.Message;
import cn.itsource.msg.service.IMessageService;
import cn.itsource.user.domain.LoginInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* 前后端交互的类实现消息的接收推送(自己发送给所有人(不包括自己))
*
* @ServerEndpoint(value = "/test/oneToMany") 前端通过此URI 和后端交互,建立连接
*/
@Slf4j
@ServerEndpoint(value = "/msg/{token}")
@Component
public class MsgWebSocket {
/** 记录当前在线连接数 */
private static AtomicInteger onlineCount = new AtomicInteger(0);
/** 存放所有在线的客户端 */
private static Map<String, Session> clients = new ConcurrentHashMap<>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) {
RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("redisTemplate");
LoginInfo info = (LoginInfo) redisTemplate.opsForValue().get(token);
System.out.println(info);
//已连接上就获取信息数目发送
IMessageService messageService = SpringUtil.getBean(IMessageService.class);
Integer count = messageService.queryMsgTotal(info.getId());
session.getAsyncRemote().sendText(count+"");
onlineCount.incrementAndGet(); // 在线数加1
clients.put(info.getId()+"", session);
log.info("有新连接加入:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session,@PathParam("token") String token) {
onlineCount.decrementAndGet(); // 在线数减1
clients.remove(token);
log.info("有一连接关闭:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 收到客户端消息后调用的方法
*
* @param message
* 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("服务端收到客户端[{}]的消息:{}", session.getId(), message);
this.sendMessage(message, session);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
@RabbitListener(queues = "queue")
public void recive(Message message) throws Exception{
System.out.println(message);
IMessageService messageService = SpringUtil.getBean(IMessageService.class);
//发消息
Session session = clients.get(message.getRec_id() + "");
if (session!=null){
session.getAsyncRemote().sendText(messageService.queryMsgTotal(message.getRec_id())+"");
}
}
发送接口
controller
@PostMapping("/push/{id}")
public AjaxResult pushMsg(@PathVariable("id") Long id){
try {
messageService.push(id);
return AjaxResult.me();
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("发送失败!"+e.getMessage());
}
}
service
@Autowired
private AmqpTemplate amqpTemplate;
@Autowired
private MessageMapper messageMapper;
@Override
public void push(Long id) {
Message message = messageMapper.loadById(id);
//集群环境
amqpTemplate.convertAndSend("exchange","a",message);
}