应用场景
弹幕
游戏广播
消息订阅
多玩家游戏
协同编辑
股票基金实时报价
视频会议
在线教育
聊天室
结论
- 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@ConditionalOnWebApplicationpublic class WebSocketConfig {//注意:用外置tomcat不需要注入此bean@Beanpublic 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}")@Componentpublic 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>();@Autowiredprivate MessageService messageService;@OnOpenpublic 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);}//在线数加1addOnlineCount();clients.put(userId, session);sessionMap.put(session, userId);sessionBeatheartMap.put(session, System.currentTimeMillis());log.info("新连接[userId={}]加入!当前在线连接数为{}", userId, getOnlineCount());}@OnClosepublic void onClose(Session session) {String key = sessionMap.get(session);if (StringUtils.isNotEmpty(key)) {if (clients.containsKey(key)) {clients.remove(key);//在线数减1subOnlineCount();}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而已,这显然比服务器无响应要好的多。
