运用的场景
WebSocket通常运用在此类情况。服务端数据变化的时候,需要立即通知前端。
为什么采用WebSocket而不是其他协议呢,例如HTTP?
首先,HTTP有比较大的局限性。
- HTTP请求只能由客户端发起请求,服务器才能产生响应,一个服务器响应只能对应一个客户端请求。
- HTTP1.0中每一次请求都需要建立一次连接,HTTP1.1中,请求头中Connection: keep-alive,这个属性使HTTP请求可以在一次连接的基础上,发生多个请求。
- 为了获取服务端是否有数据更新,客户端只能采用轮询或者长连接的方式,这两种方式不能保证好的时效性以及合理的服务器开销。
WebSocket则可以在建立WebSocket连接之后保持一个长连接,服务器与客户端都可以主动发送数据。
WebSocket协议与HTTP协议的关系
我们知道HTTP协议是建立在TCP连接之上的。HTTP是应用层的协议,TCP是传输层的协议。而WebSocket连接在建立连接之前都需要客户端首先发送一个HTTP请求给服务器。但是这一次的请求头属性是这样子的Connection: Upgrade。Upgrade表明此次请求会被升级。请求头属性Upgrade: websocket,这表明此次请求被升级成了WebSocket协议。其中说明一下几个相关的属性,Sec-WebSocket-Key 属性是浏览器生成用来验证服务器是不是WebSocket代理的。Sec-WebSocket-Version属性表示的是协议的版本号。
下面是一个上知乎时候的请求头
GET wss://messaging.zhihu.com/ws?udid=ADCCTzJorgyPTs8Wf4XDRIU9AxdiumMdLUI%3D HTTP/1.1
Host: messaging.zhihu.com
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: https://www.zhihu.com
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3346.9 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: _zap=d9799872-2f14-453a-8046-3fcb52fbc31e; d_c0="ADCCTzJorgyPTs8Wf4XDRIU9AxdiumMdLUI=|1510642687"; z_c0="2|1:0|10:1519470533|4:z_c0|92:Mi4xcmxlMEFRQUFBQUFBTUlKUE1taXVEQ1lBQUFCZ0FsVk54WlYtV3dDSUlHMWY3b2thWHprOHFMUnVGMnExQ1hneExR|aa3f4aa167abfcf79a130d9dbc79b9db9939fca637a912b97dead6e83a1248cd"; __utma=51854390.2029141864.1525242282.1525242282.1525242282.1; __utmz=51854390.1525242282.1.1.utmcsr=baidu|utmccn=(organic)|utmcmd=organic; __utmv=51854390.100-1|2=registration_date=20150520=1^3=entry_date=20150520=1; q_c1=f1f572658785464ba36fae7942eaaf67|1527147861000|1510377437000; _xsrf=aa18cc1d-c3f3-4307-8dd3-02c9c156e249
Sec-WebSocket-Key: Ef+CnxJwdZAIEqovO+ia3A==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
最后说明,在HTTP协议中,普通的URL为http://x.x.x,加密的HTTP则为https://x.x.x;在WebSocket协议中,普通的URL为ws:x.x.x,加密的WebSocket则为wss:x.x.x。再补充一点,不支持WebSocket的浏览器是没有办法使其支持的。只能采用其他方式。
实现直播弹幕
先说明一下实现WebSocket的两种方式
-
使用tomcat的WebSocket实现,需要tomcat7以上版本,jdk7以上版本。
-
使用spring的WebSocket实现,需要spring4.X以上版本。
本文采用的是tomcat实现的WebSocket。只需要服务端建立一个类。
/**
* Created by ding on 2018/5/31.
*/
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
* 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
*/
@ServerEndpoint("/websocket/{liveId}")
public class WebSocket {
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static int onlineCount = 0;
/**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识*/
// private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<WebSocket>();
/**
* 用来存放每个直播相对应的WebSocket的set集合
*/
private static Map<String, CopyOnWriteArraySet> liveMap = new ConcurrentHashMap<String, CopyOnWriteArraySet>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据。
*/
private Session session;
private String liveId;
/**
* 连接建立成功调用的方法
*
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(@PathParam("liveId") String liveId, Session session) {
this.liveId = liveId;
this.session = session;
CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<WebSocket>();
//若map中存在该live,取出WebSocket的set,并将新的WebSocket加入set,否则直接将WebSocket对象加入Set
if (liveMap.containsKey(liveId)) {
webSocketSet = liveMap.get(liveId);
} else {
}
webSocketSet.add(this); //加入set中
liveMap.put(liveId, webSocketSet);
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
CopyOnWriteArraySet<WebSocket> webSocketSet = liveMap.get(liveId);
webSocketSet.remove(this); //从set中删除
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);
}
/**
* 发生错误时调用
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
/**
* 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
*
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* @Description: 群发消息
* @Author: jiang-weirong
* @Date: 2018/6/4 14:20
*/
public void messageGroup(String message, String liveId){
if(liveMap.containsKey(liveId)){
CopyOnWriteArraySet<WebSocket> webSocketSet = liveMap.get(liveId);
for (WebSocket item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}else {
}
}
public static synchronized int getOnlineCount(String liveId) {
if (liveMap.containsKey(liveId)) {
CopyOnWriteArraySet<WebSocket> webSocketSet = liveMap.get(liveId);
onlineCount = webSocketSet.size();
} else {
return 0;
}
return onlineCount;
}
}
类中的liveId表示的是直播房间编号,实现的主要功能:
- 可以通过该类获取各个直播房间的观众人数。
- 可以发送信息给在某个直播房间的所有观众。
前端的js建立WebSocket的代码:
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/websocket/" + liveId);
}
else {
alert('当前浏览器 Not support websocket')
}