出現(xiàn)的背景
WebSocket是一種規(guī)范靴寂,是Html5規(guī)范的一部分磷蜀,websocket解決什么問題呢?解決http協(xié)議的一些不足百炬。我們知道褐隆,http協(xié)議是一種無狀態(tài)的,基于請求響應(yīng)模式的協(xié)議剖踊。
網(wǎng)頁聊天的程序(基于http協(xié)議的)庶弃,瀏覽器客戶端發(fā)送一個(gè)數(shù)據(jù),服務(wù)器接收到這個(gè)瀏覽器數(shù)據(jù)之后德澈,如何將數(shù)據(jù)推送給其他的瀏覽器客戶端呢歇攻?
這就涉及到服務(wù)器的推技術(shù)。早年為了實(shí)現(xiàn)這種服務(wù)器也可以像瀏覽器客戶端推送消息的長連接需求梆造,有很多方案缴守,比如說最常用的采用一種輪詢技術(shù),就是客戶端每隔一段時(shí)間镇辉,比如說2s或者3s向服務(wù)器發(fā)送請求屡穗,去請求服務(wù)器端是否還有信息沒有響應(yīng)給客戶端,有就響應(yīng)給客戶端摊聋,當(dāng)然沒有響應(yīng)就只是一種無用的請求鸡捐。
這種長輪詢技術(shù)的缺點(diǎn)有:
1)響應(yīng)數(shù)據(jù)不是實(shí)時(shí)的,在下一次輪詢請求的時(shí)候才會(huì)得到這個(gè)響應(yīng)信息麻裁,只能說是準(zhǔn)實(shí)時(shí)箍镜,而不是嚴(yán)格意義的實(shí)時(shí)。
2)大多數(shù)輪詢請求的空輪詢煎源,造成大量的資源帶寬的浪費(fèi)色迂,每次http請求攜帶了大量無用的頭信息,而服務(wù)器端其實(shí)大多數(shù)都不關(guān)注這些頭信息手销,而實(shí)際大多數(shù)情況下這些頭信息都遠(yuǎn)遠(yuǎn)大于body信息歇僧,造成了資源的消耗。
拓展
比較新的技術(shù)去做輪詢的效果是Comet锋拖。這種技術(shù)雖然可以雙向通信诈悍,但依然需要反復(fù)發(fā)出請求。而且在Comet中兽埃,普遍采用的長鏈接侥钳,也會(huì)消耗服務(wù)器資源。
WebSocket是什么柄错?
WebSocket一種在單個(gè) TCP 連接上進(jìn)行全雙工通訊的協(xié)議舷夺。WebSocket通信協(xié)議于2011年被IETF定為標(biāo)準(zhǔn)RFC 6455苦酱,并被RFC7936所補(bǔ)充規(guī)范。WebSocket API也被W3C定為標(biāo)準(zhǔn)给猾。
WebSocket 使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單疫萤,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)。在 WebSocket API 中敢伸,瀏覽器和服務(wù)器只需要完成一次握手扯饶,兩者之間就直接可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸详拙。
websocket的出現(xiàn)就是解決了客戶端與服務(wù)端的這種長連接問題帝际,這種長連接是真正意義上的長連接∪恼蓿客戶端與服務(wù)器一旦連接建立雙方就是對等的實(shí)體蹲诀,不再區(qū)分嚴(yán)格意義的客戶端和服務(wù)端。長連接只有在初次建立的時(shí)候弃揽,客戶端才會(huì)向服務(wù)端發(fā)送一些請求脯爪,這些請求包括請求頭和請求體,一旦建立好連接之后矿微,客戶端和服務(wù)器只會(huì)發(fā)送數(shù)據(jù)本身而不需要再去發(fā)送請求頭信息痕慢,這樣大量減少了
網(wǎng)絡(luò)帶寬。websocket協(xié)議本身是構(gòu)建在http協(xié)議之上的升級協(xié)議涌矢,客戶端首先向服務(wù)器端去建立連接掖举,這個(gè)連接本身就是http協(xié)議只是在頭信息中包含了一些websocket協(xié)議的相關(guān)信息,一旦http連接建立之后娜庇,服務(wù)器端讀到這些websocket協(xié)議的相關(guān)信息就將此協(xié)議升級成websocket協(xié)議塔次。websocket協(xié)議也可以應(yīng)用在非瀏覽器應(yīng)用,只需要引入相關(guān)的websocket庫就可以了名秀。
HTML5定義了WebSocket協(xié)議励负,能更好的節(jié)省服務(wù)器資源和帶寬,并且能夠更實(shí)時(shí)地進(jìn)行通訊匕得。Websocket使用ws或wss的統(tǒng)一資源標(biāo)志符继榆,類似于HTTPS,其中wss表示在TLS之上的Websocket汁掠。如:
ws://example.com/wsapi
wss://secure.example.com/
優(yōu)點(diǎn)
- 較少的控制開銷:相對與http請求的頭部信息略吨,websocket信息明顯減少。
- 更強(qiáng)的實(shí)時(shí)性:由于協(xié)議是全雙工的考阱,所以服務(wù)器可以隨時(shí)主動(dòng)給客戶端下發(fā)數(shù)據(jù)晋南。相對于HTTP請求需要等待客戶端發(fā)起請求服務(wù)端才能響應(yīng),延遲明顯更少羔砾;即使是和Comet等類似的長輪詢比較负间,其也能在短時(shí)間內(nèi)更多次地傳遞數(shù)據(jù)。
- 保持連接狀態(tài)姜凄。于HTTP不同的是政溃,Websocket需要先創(chuàng)建連接,這就使得其成為一種有狀態(tài)的協(xié)議态秧,之后通信時(shí)可以省略部分狀態(tài)信息董虱。而HTTP請求可能需要在每個(gè)請求都攜帶狀態(tài)信息(如身份認(rèn)證等)。
- 更好的二進(jìn)制支持申鱼。Websocket定義了二進(jìn)制幀愤诱,相對HTTP,可以更輕松地處理二進(jìn)制內(nèi)容捐友。
- 可以支持?jǐn)U展淫半。Websocket定義了擴(kuò)展,用戶可以擴(kuò)展協(xié)議匣砖、實(shí)現(xiàn)部分自定義的子協(xié)議科吭。如部分瀏覽器支持壓縮等。
- 更好的壓縮效果猴鲫。相對于HTTP壓縮对人,Websocket在適當(dāng)?shù)臄U(kuò)展支持下,可以沿用之前內(nèi)容的上下文拂共,在傳遞類似的數(shù)據(jù)時(shí)牺弄,可以顯著地提高壓縮率。
netty對websocket協(xié)議的支持
demo
瀏覽器頁面向服務(wù)器發(fā)送消息宜狐,服務(wù)器將當(dāng)前消息發(fā)送時(shí)間反饋給瀏覽器頁面势告。
服務(wù)器端
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import java.net.InetSocketAddress;
//websocket長連接示例
public class MyServer {
public static void main(String[] args) throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup wokerGroup = new NioEventLoopGroup();
try{
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,wokerGroup).channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new WebSocketChannelInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(new InetSocketAddress(8899)).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
wokerGroup.shutdownGracefully();
}
}
}
服務(wù)器端初始化連接
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//websocket協(xié)議本身是基于http協(xié)議的,所以這邊也要使用http解編碼器
pipeline.addLast(new HttpServerCodec());
//以塊的方式來寫的處理器
pipeline.addLast(new ChunkedWriteHandler());
//netty是基于分段請求的肌厨,HttpObjectAggregator的作用是將請求分段再聚合,參數(shù)是聚合字節(jié)的最大長度
pipeline.addLast(new HttpObjectAggregator(8192));
//ws://server:port/context_path
//ws://localhost:9999/ws
//參數(shù)指的是contex_path
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
//websocket定義了傳遞數(shù)據(jù)的6中frame類型
pipeline.addLast(new TextWebSocketFrameHandler());
}
}
WebSocketServerProtocolHandler
:參數(shù)是訪問路徑培慌,這邊指定的是ws,服務(wù)客戶端訪問服務(wù)器的時(shí)候指定的url是:ws://localhost:8899/ws
柑爸。
它負(fù)責(zé)websocket握手以及處理控制框架(Close吵护,Ping(心跳檢檢測request),Pong(心跳檢測響應(yīng)))表鳍。 文本和二進(jìn)制數(shù)據(jù)幀被傳遞到管道中的下一個(gè)處理程序進(jìn)行處理馅而。
楨:
WebSocket規(guī)范中定義了6種類型的楨,netty為其提供了具體的對應(yīng)的POJO實(shí)現(xiàn)譬圣。
WebSocketFrame:所有楨的父類瓮恭,所謂楨就是WebSocket服務(wù)在建立的時(shí)候,在通道中處理的數(shù)據(jù)類型厘熟。本列子中客戶端和服務(wù)器之間處理的是文本信息屯蹦。所以范型參數(shù)是TextWebSocketFrame维哈。
自定義Handler
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.time.LocalDateTime;
//處理文本協(xié)議數(shù)據(jù),處理TextWebSocketFrame類型的數(shù)據(jù)登澜,websocket專門處理文本的frame就是TextWebSocketFrame
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{
//讀到客戶端的內(nèi)容并且向客戶端去寫內(nèi)容
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("收到消息:"+msg.text());
/**
* writeAndFlush接收的參數(shù)類型是Object類型阔挠,但是一般我們都是要傳入管道中傳輸數(shù)據(jù)的類型,比如我們當(dāng)前的demo
* 傳輸?shù)木褪荰extWebSocketFrame類型的數(shù)據(jù)
*/
ctx.channel().writeAndFlush(new TextWebSocketFrame("服務(wù)時(shí)間:"+ LocalDateTime.now()));
}
//每個(gè)channel都有一個(gè)唯一的id值
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
//打印出channel唯一值脑蠕,asLongText方法是channel的id的全名
System.out.println("handlerAdded:"+ctx.channel().id().asLongText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved:" + ctx.channel().id().asLongText());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("異常發(fā)生");
ctx.close();
}
}
頁面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket客戶端</title>
</head>
<body>
<script type="text/javascript">
var socket;
//如果瀏覽器支持WebSocket
if(window.WebSocket){
//參數(shù)就是與服務(wù)器連接的地址
socket = new WebSocket("ws://localhost:8899/ws");
//客戶端收到服務(wù)器消息的時(shí)候就會(huì)執(zhí)行這個(gè)回調(diào)方法
socket.onmessage = function (event) {
var ta = document.getElementById("responseText");
ta.value = ta.value + "\n"+event.data;
}
//連接建立的回調(diào)函數(shù)
socket.onopen = function(event){
var ta = document.getElementById("responseText");
ta.value = "連接開啟";
}
//連接斷掉的回調(diào)函數(shù)
socket.onclose = function (event) {
var ta = document.getElementById("responseText");
ta.value = ta.value +"\n"+"連接關(guān)閉";
}
}else{
alert("瀏覽器不支持WebSocket购撼!");
}
//發(fā)送數(shù)據(jù)
function send(message){
if(!window.WebSocket){
return;
}
//當(dāng)websocket狀態(tài)打開
if(socket.readyState == WebSocket.OPEN){
socket.send(message);
}else{
alert("連接沒有開啟");
}
}
</script>
<form onsubmit="return false">
<textarea name = "message" style="width: 400px;height: 200px"></textarea>
<input type ="button" value="發(fā)送數(shù)據(jù)" onclick="send(this.form.message.value);">
<h3>服務(wù)器輸出:</h3>
<textarea id ="responseText" style="width: 400px;height: 300px;"></textarea>
<input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空數(shù)據(jù)">
</form>
</body>
</html>
啟動(dòng)服務(wù)器,然后運(yùn)行客戶端頁面谴仙,當(dāng)客戶端和服務(wù)器端連接建立的時(shí)候迂求,服務(wù)器端執(zhí)行handlerAdded
回調(diào)方法,客戶端執(zhí)行onopen
回調(diào)方法
服務(wù)器端控制臺:
handlerAdded:acde48fffe001122-00005c11-00000001-4ce4764fffa940fe-df037eb5
頁面:
客戶端發(fā)送消息晃跺,服務(wù)器端進(jìn)行響應(yīng)揩局,
服務(wù)端控制臺打印:
收到消息:websocket程序
客戶端也收到服務(wù)器端的響應(yīng):
打開開發(fā)者工具:
在從標(biāo)準(zhǔn)的HTTP或者HTTPS協(xié)議切換到WebSocket時(shí)哼审,將會(huì)使用一種升級握手的機(jī)制谐腰。因此,使用WebSocket的應(yīng)用程序?qū)⑹冀K以HTTP/S作為開始涩盾,然后再執(zhí)行升級十气。這個(gè)升級動(dòng)作發(fā)生的確定時(shí)刻特定與應(yīng)用程序;它可能會(huì)發(fā)生在啟動(dòng)時(shí)候春霍,也可能會(huì)發(fā)生在請求了某個(gè)特定的IURL之后砸西。
參考技術(shù)
java web 服務(wù)器推送技術(shù)--comet4j
Comet:基于 HTTP 長連接的“服務(wù)器推”技術(shù)