应用场景
弹幕
游戏广播
消息订阅
多玩家游戏
协同编辑
股票基金实时报价
视频会议
在线教育
聊天室
结论
- WebSocket和HTTP都是基于TCP协议。两个完全不同的应用层协议
- WebSocket依赖于HTTP连接进行第一次握手
- Socket不是协议,它是在程序层面上对传输层协议的接口封装,可以理解为一个能够提供端对端的通信的调用接口(API)
实例
springboot
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
内置tomcat注入bean
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
@ConditionalOnWebApplication
public class WebSocketConfig {
//注意:用外置tomcat不需要注入此bean
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
websocket实现类
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@ServerEndpoint(value = "/接口名/{userId}")
@Component
public class MyWebSocketService {
private static final String NULL_KEY = "null";
/**
* 心跳连接有效时间(毫秒)
*/
private static final Long BEAT_HEART_DURATION_TIME_MILLIS = 10 * 60 * 1000L;
/**
* 用来记录当前在线连接数
*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/**
* concurrent包的线程安全Map,用来存放每个客户端对应的Session对象。
*/
public static Map<String, Session> clients = new ConcurrentHashMap<String, Session>();
/**
* concurrent包的线程安全Map,用来存放每个客户端对应的Session对象。
*/
private static Map<Session, String> sessionMap = new ConcurrentHashMap<Session, String>();
private static Map<String, Session> oldClients = new ConcurrentHashMap<String, Session>();
private static Map<Session, Long> sessionBeatheartMap = new ConcurrentHashMap<Session, Long>();
@Autowired
private MessageService messageService;
@OnOpen
public void onOpen(@PathParam("userId") String userId, Session session) {
if (StringUtils.isEmpty(userId) || NULL_KEY.equalsIgnoreCase(userId)) {
try {
log.warn("[key={}]非法,禁止连接!!!", userId);
session.close();
} catch (IOException e) {
}
}
if (clients.containsKey(userId)) {
//删除原有连接
destroyOldSession(userId);
}
//在线数加1
addOnlineCount();
clients.put(userId, session);
sessionMap.put(session, userId);
sessionBeatheartMap.put(session, System.currentTimeMillis());
log.info("新连接[userId={}]加入!当前在线连接数为{}", userId, getOnlineCount());
}
@OnClose
public void onClose(Session session) {
String key = sessionMap.get(session);
if (StringUtils.isNotEmpty(key)) {
if (clients.containsKey(key)) {
clients.remove(key);
//在线数减1
subOnlineCount();
}
sessionMap.remove(session);
sessionBeatheartMap.remove(session);
log.info("连接 [userId={}]关闭!当前在线连接数为{}", key, getOnlineCount());
/**通知系统断开连接**/
destroyOldSession(key);
}
}
@Scheduled(cron = " */5 * * * * ?")
public void processTerminalInformation() {
if (clients.isEmpty()) {
return;
}
clients.forEach((k, v) -> {
try {
List<Message> messages = messageService.getMessageLists(k);
if (CollectionUtil.isNotEmpty(messages)) {
v.getAsyncRemote().sendText(JSON.toJSONString(messages));
}
} catch (Exception e) {
destroyOldSession(k);
}
});
}
@Scheduled(cron = "0 */1 * * * ?")
public void processOnlineTime() {
oldClients.forEach((k, v) -> {
try {
Long lastBeatTime = sessionBeatheartMap.get(v);
if (lastBeatTime == null || (System.currentTimeMillis() - lastBeatTime) > BEAT_HEART_DURATION_TIME_MILLIS) {
//超过90秒未收到空消息,KEY 设备已断开连接
destroyOldSession(k);
}
} catch (Exception e) {
//连接不可用,清理连接
destroyOldSession(k);
}
});
oldClients = clients;
}
private void destroyOldSession(String key) {
Session oldSession = clients.get(key);
if (oldSession != null) {
if (clients.containsKey(key)) {
subOnlineCount();
clients.remove(key);
if (oldSession != null) {
sessionMap.remove(oldSession);
sessionBeatheartMap.remove(oldSession);
}
try {
oldSession.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "已断开连接!"));
} catch (IOException e) {
}
}
}
}
public static synchronized AtomicInteger getOnlineCount() {
return onlineCount;
}
/**
* 增加连接人数
*/
public static synchronized void addOnlineCount() {
onlineCount.incrementAndGet();
}
/**
* 减少连接人数
*/
public static synchronized void subOnlineCount() {
onlineCount.decrementAndGet();
}
}
Vue
用法
created() {
this.initWebSocket();
},
methods: {
initWebSocket() {
let token = localStorage.getItem('token');
const url = 'ws://127.0.0.1:端口号/接口名/' + token;
this.websocket = new WebSocket(url);
this.websocket.onopen = this.websockOpen;
this.websocket.onmessage = this.websocketonmessage;
this.websocket.onclose = this.websocketclose;
},
websockOpen() {
console.log("WebSocket连接成功");
},
websocketonmessage(e) { //数据接收
console.log(e);
},
websocketclose(e) { //关闭
console.log("close..")
},
logout() { //这部分是退出的时候关闭websocket链接
window.localStorage.removeItem('token');
this.$router.push({path: '/login'})
this.websocket.close();
},
}
结论刨析
Ajax、Long poll、Websocket图示
虽然http1.1默认开启了keep-alive长连接保持了这个TCP通道使得在一个HTTP连接中,可以发送多个Request,接收多个Response,但是一个request只能有一个response。而且这个response也是被动的,不能主动发起
协议升级
每个WebSocket连接都始于一个HTTP请求。WebSocket协议在第一次握手连接时,通过HTTP协议在传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Version: 13
注意,关键的地方是,这里面有个Upgrade首部,用来把当前的HTTP请求升级到WebSocket协议,这是HTTP协议本身的内容,是为了扩展支持其他的通讯协议。如果服务器支持新的协议,则必须返回101:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
至此,HTTP请求物尽其用,如果成功出发onopen事件,否则触发onerror事件,后面的传输则不再依赖HTTP协议。
为什么依赖http
- WebSocket设计上就是天生为HTTP增强通信(全双工通信等),所以在HTTP协议连接的基础上是很自然的一件事,并因此而能获得HTTP的诸多便利。
- 这诸多便利中有一条很重要,基于HTTP连接将获得最大的一个兼容支持,比如即使服务器不支持WebSocket也能建立HTTP通信,只不过返回的是onerror而已,这显然比服务器无响应要好的多。