如今,不少网站为了实现推送技术,所用的技术都是 Ajax 轮询或者long poll 。这种传统的模式带来很明显的缺点,即浏览器须要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费不少的带宽等资源。javascript
ajax轮询 的原理很是简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。
场景再现:
客户端:啦啦啦,有没有新信息(Request)
服务端:没有(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:好啦好啦,有啦给你。(Response)
客户端:啦啦啦,有没有新消息(Request)
服务端:。。。。。没。。。。没。。。没有(Response) ---- loop前端
long poll 其实原理跟 ajax轮询 差很少,都是采用轮询的方式,不过采起的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起链接后,若是没消息,就一直不返回Response给客户端。直到有消息才返回,返回完以后,客户端再次创建链接,周而复始。
场景再现:
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)
服务端:额。。 等待到有消息的时候。。来 给你(Response)
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loopjava
从上面能够看出其实这两种方式,都是在不断地创建HTTP链接,而后等待服务端处理,能够体现HTTP协议的另一个特色,被动性。何为被动性呢,其实就是,服务端不能主动联系客户端,只能有客户端发起。web
从上面很容易看出来,无论怎么样,上面这两种都是很是消耗资源的。
ajax轮询 须要服务器有很快的处理速度和资源。(速度)
long poll 须要有很高的并发,也就是说同时接待客户的能力。(场地大小)
因此ajax轮询 和long poll 缺点很是明显。ajax
WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说不要紧,但HTTP是不支持持久链接的(长链接,循环链接的不算)json
首先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,可是Websocket实际上是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充,能够经过这样一张图理解浏览器
首先,相对于HTTP这种非持久的协议来讲,Websocket是一个持久化的协议。安全
因此在这种状况下出现了,Websocket出现了。他解决了HTTP的难题。服务器
WebSocket并非全新的协议,而是利用了HTTP协议来创建链接。咱们来看看WebSocket链接是如何建立的。websocket
首先,WebSocket链接必须由浏览器发起,由于请求协议是一个标准的HTTP请求,格式以下:
GET ws://localhost:3000/ws/chat HTTP/1.1 Host: localhost Upgrade: websocket Connection: Upgrade Origin: http://localhost:3000 Sec-WebSocket-Key: client-random-string Sec-WebSocket-Version: 13
该请求和普通的HTTP请求有几点不一样:
/path/
,而是以ws://
开头的地址;Upgrade: websocket
和Connection: Upgrade
表示这个链接将要被转换为WebSocket链接;Sec-WebSocket-Key
是用于标识这个链接,并不是用于加密数据;Sec-WebSocket-Version
指定了WebSocket的协议版本。服务器若是接受该请求,就会返回以下响应:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: server-random-string
该响应代码101
表示本次链接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket
指定的WebSocket协议。
这里开始就是HTTP最后负责的区域了,告诉客户端,已经成功切换协议啦~
var ws = new WebSocket("wss://localhost:8080/ws/asset"); //链接创建成功调用的方法 ws.onopen = function(evt) { console.log("Connection open ..."); //向服务端发送消息 ws.send("Hello WebSockets!"); }; //收到服务端消息后调用的方法 ws.onmessage = function(evt) { console.log( "Received Message: " + evt.data); ws.close(); }; //链接关闭调用的方法 ws.onclose = function(evt) { console.log("Connection closed."); };
@ServerEndpoint(value = "/ws/asset") @Component @Slf4j public class WebSocketServer { private static AtomicInteger OnlineCount = new AtomicInteger(0); // concurrent包的线程安全Set,用来存放每一个客户端对应的Session对象。 private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<Session>(); /** * 链接创建成功调用的方法 */ @OnOpen public void onOpen(Session session) { SessionSet.add(session); int cnt = OnlineCount.incrementAndGet(); // 在线数加1 log.info("有链接加入,当前链接数为:{}", cnt); SendMessage(session, "链接成功"); } /** * 链接关闭调用的方法 */ @OnClose public void onClose(Session session) { SessionSet.remove(session); int cnt = OnlineCount.decrementAndGet(); log.info("有链接关闭,当前链接数为:{}", cnt); } /** * 收到客户端消息后调用的方法 * * @param message * 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, Session session) { log.info("来自客户端的消息:{}",message); System.out.println(session.toString()); SendMessage(session, "收到消息,消息内容:"+message+session.getId()); } /** * 出现错误 * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误:{},Session ID: {}",error.getMessage(),session.getId()); error.printStackTrace(); } /** * 发送消息,实践代表,每次浏览器刷新,session会发生变化。 * @param session * @param message */ public static void SendMessage(Session session, String message) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error("发送消息出错:{}", e.getMessage()); e.printStackTrace(); } } /** * 群发消息 * @param message * @throws IOException */ public static void BroadCastInfo(String message) throws IOException { for (Session session : SessionSet) { if(session.isOpen()){ SendMessage(session, message); } } } /** * 指定Session发送消息 * @param sessionId * @param message * @throws IOException */ public static void SendMessage(String message,String sessionId) throws IOException { Session session = null; for (Session s : SessionSet) { if(s.getId().equals(sessionId)){ session = s; break; } } if(session!=null){ SendMessage(session, message); } else{ log.warn("没有找到你指定ID的会话:{}",sessionId); } } }
websockt心跳机制,不得不说很形象;那何为心跳机制,就是代表client与server的链接是否还在的检测机制;
若是不存在检测,那么网络忽然断开,形成的后果就是client、server可能还在傻乎乎的发送无用的消息,浪费了资源;怎样检测呢?原理就是定时向server发送消息,若是接收到server的响应就代表链接依旧存在;
这个心跳机制在分布式中也很常见,
(1)Client:客户端说明
客户端的代码主要是使用H5的WebSocket进行实现,在前端网页中使用WebSocket进行链接服务端,而后创建Socket链接进行通信。
(2)Server:服务端说明
服务端主要是创建多个客户端的关系,进行消息的中转等。客户端成功链接到服务端以后,就能够经过创建的通道进行发送消息到服务端,服务端接收到消息以后在群发给全部的客户端。
(3)客户端和服务端链接
var websocket = new WebSocket("ws://localhost:8080/myWs");
(4)客户端和服务端怎么发送消息?
客户端可使用webSocket提供的send()方法,以下代码:
var message = document.getElementById('text').value; websocket.send(message);
服务端怎么发送消息呢?主要是使用在成功创建链接的时候,建立的Session对象进行发送,以下代码:
session.getAsyncRemote().sendText("恭喜您成功链接上WebSocket");
(5)客户端和服务端怎么接受消息?
客户端接收消息消息使用的是websocket的onmessage回调方法,以下代码:
websocket.onmessage = function(event) { //文本信息直接显示,若是是json信息,须要转换下在显示. var data = event.data; document.getElementById('message').innerHTML += data; }
服务端:
@OnMessage public void onMessage(String message, Session session) { System.out.println("来自客户端的消息:" + message); }
(6)群聊原理(群发消息)
服务端在和客户端创建链接的时候,会建立一个webSocket对象,咱们会将每一个链接建立的对象进行报错到一个列表中,好比:CopyOnWriteArraySet(这是线程安全的);在要进行群发的时候,编写咱们的列表对象进行群发消息。
(7)单聊原理(一对一消息)
聊的时候,就无需遍历列表,而是须要知道发送者和接受者各自的Session对象,这个Session对象怎么获取呢?Session能够获取到sessionId,发送者在发送消息的时候,携带接收消息的sessionId,那么问题就演变成了:发送者怎么知道接受者的sessionId,那就是加入一个在线用户列表便可,在线用户列表中有用户的基本信息,包括sessionId。
对比聊天室的demo,不一样之处在于,客户端连入服务器时候,会开启一个线程,在线程中对客户端进行推送数据。
关键代码:
/** * 接收到消息 * * @param text */ @OnMessage public void onMsg(Session session,@PathParam("param") String param) throws IOException { //记录客户端 webSocketMaps.put(session, param); //实例化工做任务 Operater operater =new Operater(session,param); //开启线程 Thread thread = new Thread(operater); thread.start(); logger.info("发送线程启动成功"); }
目前业务还不是很复杂,后期功能添加时候,再进行扩展,关于这个实时推送,大概开了50个窗口就链接失败了。关于websocket的高并发,能够考虑。