文章目錄
- WebSocket簡介
- WebSocket通信握手
- Netty為WebSocket資料幀提供的支持
- 實戰
- 測驗
- 總結
WebSocket簡介
WebSocket協議是完全重新設計的協議,旨在為Web上的雙向資料傳輸問題提供一個切實可行的解決方案,使得客戶端和服務器之間可以在任意時刻傳輸訊息,因此,這也就要求它們異步地處理訊息回執
WebSocket特點:
- HTML5 中的協議,實作與客戶端與服務器雙向,基于訊息的文本或二進制資料通信
- 適合于對資料的實時性要求比較強的場景,如通信、直播、共享桌面,特別適合于客戶端與服務端頻繁互動的情況下,如實時共享、多人協作等平臺
- 采用新的協議,后端需要單獨實作
- 客戶端并不是所有瀏覽器都支持
WebSocket通信握手
在從標準的 HTTP 或者 HTTPS協議切換到WebSocket時,將會使用一種稱為握手的機制 ,因此,使用WebSocket的應用程式將始終以HTTP/S作為開始,然后再執行升級,這個升級動作發生的確切時刻特定于應用程式;它可能會發生在啟動時,也可能會發生在請求了某個特定的URL之后
下面是WebSocket請求和回應的標識資訊:

客戶端的請求:
- Connection屬性中標識Upgrade,表示客戶端希望連接升級
- Upgrade屬性中標識為Websocket,表示希望升級成 Websocket 協議
- Sec-WebSocket-Key屬性,表示隨機字串,服務器端會用這些資料來構造出一個 SHA-1 的資訊摘要,把 “Sec-WebSocket-Key” 加上一個特殊字串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后計算 SHA-1 摘要,之后進行 BASE-64 編碼,將結果做為 “Sec-WebSocket-Accept” 頭的值,回傳給客戶端,如此操作,可以盡量避免普通 HTTP 請求被誤認為 Websocket 協議,
- Sec-WebSocket-Version屬性,表示支持的 Websocket 版本,RFC6455 要求使用的版本是 13,之前草案的版本均應當棄用
服務器端回應:
- Upgrade屬性中標識為websocket
- Connection告訴客戶端即將升級的是 Websocket 協議
- Sec-WebSocket-Accept這個則是經過服務器確認,并且加密過后的Sec-WebSocket-Key
Netty為WebSocket資料幀提供的支持
由 IETF 發布的WebSocket RFC,定義了6種幀,Netty為它們每種都提供了一個POJO實作
| 幀型別 | 描述 |
|---|---|
| BinaryWebSocketFrame | 包含了二進制資料 |
| TextWebSocketFrame | 包含了文本資料 |
| ContinuationWebSocketFrame | 包含屬于上一個BinaryWebSocketFrame 或TextWebSocketFrame 的文本或者二進制資料 |
| CloseWebSocketFrame | 標識一個CLOSE請求,包含一個關閉的狀態碼 |
| PingWebSocketFrame | 請求傳輸一個PongWebSocketFrame |
| PongWebSocketFrame | 作為一個對于PingWebSocketFrame的回應被發送 |
實戰
首先,定義WebSocket服務端,其中創建了一個Netty提供ChannelGroup變數用來記錄所有已經連接的客戶端channel,而這個ChannelGroup就是用來完成群發和單聊功能的
//定義websocket服務端
public class WebSocketServer {
private static EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private static EventLoopGroup workerGroup = new NioEventLoopGroup();
private static ServerBootstrap bootstrap = new ServerBootstrap();
private static final int PORT =8761;
//創建 DefaultChannelGroup,用來保存所有已經連接的 WebSocket Channel,群發和一對一功能可以用上
private final static ChannelGroup channelGroup =
new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
public static void startServer(){
try {
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WebSocketServerInitializer(channelGroup));
Channel ch = bootstrap.bind(PORT).sync().channel();
System.out.println("打開瀏覽器訪問: http://127.0.0.1:" + PORT + '/');
ch.closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}finally{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
startServer();
}
}
接下來,初始化Pipeline,向當前Pipeline中注冊所有必需的ChannelHandler,主要包括:用于處理HTTP請求編解碼的HttpServerCodec、自定義的處理HTTP請求的HttpRequestHandler、用于處理WebSocket幀資料以及升級握手的WebSocketServerProtocolHandler以及自定義的處理TextWebSocketFrame資料幀和握手完成事件的WebSocketServerHanlder
public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel>{
/*websocket訪問路徑*/
private static final String WEBSOCKET_PATH = "/ws";
private ChannelGroup channelGroup;
public WebSocketServerInitializer(ChannelGroup channelGroup){
this.channelGroup=channelGroup;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//用于HTTP請求的編解碼
ch.pipeline().addLast(new HttpServerCodec());
//用于寫入一個檔案的內容
ch.pipeline().addLast(new ChunkedWriteHandler());
//用于http請求的聚合
ch.pipeline().addLast(new HttpObjectAggregator(64*1024));
//用于WebSocket應答資料壓縮傳輸
ch.pipeline().addLast(new WebSocketServerCompressionHandler());
//處理http請求,對非websocket請求的處理
ch.pipeline().addLast(new HttpRequestHandler(WEBSOCKET_PATH));
//根據websocket規范,處理升級握手以及各種websocket資料幀
ch.pipeline().addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, "", true));
//對websocket的資料進行處理,主要處理TextWebSocketFrame資料幀和握手完成事件
ch.pipeline().addLast(new WebSocketServerHanlder(channelGroup));
}
}
HttpRequestHandler用來處理HTTP請求,首先會先確認當前的HTTP請求是否指向了WebSocket的URI,如果是那么HttpRequestHandler將呼叫FullHttpRequest物件上的retain方法,并通過呼叫fireChannelRead(msg)方法將它轉發給下一個ChannelInboundHandler(之所以呼叫retain方法,是因為呼叫channelRead0方法完成之后,會進行資源釋放)
接下來,讀取磁盤上指定路徑的index.html檔案內容,將內容封裝成ByteBuf物件,之后,構造一個FullHttpResponse回應物件,將ByteBuf添加進去,并設定請求頭資訊,最后,呼叫writeAndFlush方法沖刷所有寫入的訊息
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest>{
private static final File INDEX = new File("D:/學習/index.html");
private String websocketUrl;
public HttpRequestHandler(String websocketUrl)
{
this.websocketUrl = websocketUrl;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
if(websocketUrl.equalsIgnoreCase(msg.getUri())){
//如果該HTTP請求指向了websocketUrl的URL,那么直接交給下一個ChannelInboundHandler進行處理
ctx.fireChannelRead(msg.retain());
}else{
//生成index頁面的具體內容,并送往瀏覽器
ByteBuf content = loadIndexHtml();
FullHttpResponse res = new DefaultFullHttpResponse(
HTTP_1_1, OK, content);
res.headers().set(HttpHeaderNames.CONTENT_TYPE,
"text/html; charset=UTF-8");
HttpUtil.setContentLength(res, content.readableBytes());
sendHttpResponse(ctx, msg, res);
}
}
public static ByteBuf loadIndexHtml(){
FileInputStream fis = null;
InputStreamReader isr = null;
BufferedReader raf = null;
StringBuffer content = new StringBuffer();
try {
fis = new FileInputStream(INDEX);
isr = new InputStreamReader(fis);
raf = new BufferedReader(isr);
String s = null;
// 讀取檔案內容,并將其列印
while((s = raf.readLine()) != null) {
content.append(s);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
fis.close();
isr.close();
raf.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return Unpooled.copiedBuffer(content.toString().getBytes());
}
/*發送應答*/
private static void sendHttpResponse(ChannelHandlerContext ctx,
FullHttpRequest req,
FullHttpResponse res) {
// 錯誤的請求進行處理 (code<>200).
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),
CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
HttpUtil.setContentLength(res, res.content().readableBytes());
}
// 發送應答.
ChannelFuture f = ctx.channel().writeAndFlush(res);
//對于不是長連接或者錯誤的請求直接關閉連接
if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
}
前面的HttpRequestHandler處理器只是用來管理HTTP請求和回應的,而實際對傳輸的WebSocket資料幀的處理是交由WebSocketServerHanlder 進行(其中只對TextWebSocketFrame型別的資料幀進行處理),
WebSocketServerHanlder 處理時通過重寫userEventTriggered方法,并監聽握手成功的事件,當新客戶端的WebSocket握手成功之后,它將通過把通知訊息寫到ChannelGroup中的所有channel來通知所有已經連接的客戶端,然后它將這個新的channel加入到該ChannelGroup中,并且還為每個channel隨機生成了一個用戶
之后,如果接收到了TextWebSocketFrame訊息時,會先根據當前channel拿到用戶,并決議發送的文本幀資訊,確認是群聊還是單聊,最后,構造TextWebSocketFrame回應內容,通過writeAndFlush進行沖刷
/**
* 對websocket的文本資料幀進行處理
*
*/
public class WebSocketServerHanlder extends SimpleChannelInboundHandler<TextWebSocketFrame>{
private ChannelGroup channelGroup;
public WebSocketServerHanlder(ChannelGroup channelGroup){
this.channelGroup=channelGroup;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
//獲取當前channel用戶名
String userName=UserMap.getUser(ctx.channel().id().asLongText());
//文本幀
String content= msg.text();
System.out.println("Client: "+ userName+" received [ "+content+" ]");
String toName = null;
//判斷是單聊還是群發(單聊會通過 user@ msg 這種格式進行傳輸文本幀)
if(content.contains("@")){
String[] str= content.split("@");
content=str[1];
//獲取單聊的用戶
toName = str[0];
}
if(null!=toName){
Iterator<Channel> it=channelGroup.iterator();
while(it.hasNext()){
Channel channel=it.next();
//找到指定的用戶
if(UserMap.getUser(channel.id().asLongText()).equals(toName)){
//單聊
channel.writeAndFlush(new TextWebSocketFrame(userName+"@"+content));
}
}
}else{
channelGroup.remove(ctx.channel());
//群發實作
channelGroup.writeAndFlush(new TextWebSocketFrame(userName+"@"+content));
channelGroup.add(ctx.channel());
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//檢測事件,如果是握手成功事件,做點業務處理
if(evt==WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE){
String channelId = ctx.channel().id().asLongText();
//隨機為當前channel指定一個用戶名
UserMap.setUser(channelId);
System.out.println("新的客戶端連接:"+UserMap.getUser(channelId));
//通知所有已經連接的 WebSocket 客戶端新的客戶端已經連接上了
channelGroup.writeAndFlush(new TextWebSocketFrame(UserMap.getUser(channelId)+"加入群聊"));
//將新的 WebSocket Channel 添加到 ChannelGroup 中
channelGroup.add(ctx.channel());
}else{
super.userEventTriggered(ctx, evt);
}
}
}
index.html內容
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>基于WebSocket實作網頁版群聊</title>
</head>
<body>
<script type="text/javascript">
var userName= null;
var socket;
var myDate = new Date();
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
socket = new WebSocket("ws://127.0.0.1:8761/ws");
socket.onmessage = function(event) {
var info = document.getElementById("jp-container");
var dataObj=event.data;
if(dataObj.indexOf("@")!=-1){
var arr = dataObj.split('@');
var sendUser;
var acceptMsg;
for(var i=0;i<arr.length;i++){
if(i==0){
sendUser = arr[i];
}else{
acceptMsg =arr[i];
}
}
if(userName==sendUser){
return;
}
var talk= document.createElement("div");
talk.setAttribute("class", "talk_recordboxme");
talk.innerHTML = sendUser+':';
var recordtext= document.createElement("div");
recordtext.setAttribute("class", "talk_recordtextbg");
talk.appendChild(recordtext);
var talk_recordtext=document.createElement("div");
talk_recordtext.setAttribute("class", " talk_recordtext");
var h3=document.createElement("h3");
h3.innerHTML =acceptMsg;
talk_recordtext.appendChild(h3);
var span=document.createElement("span");
span.innerHTML =myDate.toLocaleTimeString();
span.setAttribute("class", "talk_time");
talk_recordtext.appendChild(span);
talk.appendChild(talk_recordtext);
}else{
var talk= document.createElement("div");
talk.style.textAlign="center";
var font = document.createElement("font");
font.color='#212121';
font.innerHTML = dataObj+': '+myDate.toLocaleString( );
talk.appendChild(font);
}
info.appendChild(talk);
};
socket.onopen = function(event) {
console.log("Socket 已打開");
};
socket.onclose = function(event) {
console.log("Socket已關閉");
};
} else {
alert("Your browser does not support Web Socket.");
}
function send(message) {
if (!window.WebSocket) { return; }
if (socket.readyState == WebSocket.OPEN) {
var info = document.getElementById("jp-container");
var talk= document.createElement("div");
talk.setAttribute("class", "talk_recordbox");
var user = document.createElement("div");
user.setAttribute("class", "user");
talk.appendChild(user);
var recordtext= document.createElement("div");
recordtext.setAttribute("class", "talk_recordtextbg");
talk.appendChild(recordtext);
var talk_recordtext=document.createElement("div");
talk_recordtext.setAttribute("class", " talk_recordtext");
var h3=document.createElement("h3");
h3.innerHTML =message;
talk_recordtext.appendChild(h3);
var span=document.createElement("span");
span.innerHTML =myDate.toLocaleTimeString();
span.setAttribute("class", "talk_time");
talk_recordtext.appendChild(span);
talk.appendChild(talk_recordtext);
info.appendChild(talk );
socket.send(message);
} else {
alert("The socket is not open.");
}
}
</script>
<br>
<br>
<div class="talk">
<div class="talk_title"><span>群聊</span></div>
<div class="talk_record" style="background: #EEEEF4;">
<div id="jp-container" class="jp-container">
</div>
</div>
<form onsubmit="return false;">
<div class="talk_word">
<input class="add_face" id="facial" type="button" title="添加表情" value="" />
<input class="messages emotion" autocomplete="off" name="message" value="在這里輸入文字" onFocus="if(this.value=='在這里輸入文字'){this.value='';}" onblur="if(this.value==''){this.value='在這里輸入文字';}" />
<input class="talk_send" type="button" title="發送" value="發送" onclick="send(this.form.message.value)" />
</div>
</form>
</div>
樣式
body{
font-family:verdana, Arial, Helvetica, "宋體", sans-serif;
font-size: 12px;
}
body ,div ,dl ,dt ,dd ,ol ,li ,h1 ,h2 ,h3 ,h4 ,h5 ,h6 ,pre ,form ,fieldset ,input ,P ,blockquote ,th ,td ,img,
INS {
margin: 0px;
padding: 0px;
border:0;
}
ol{
list-style-type: none;
}
img,input{
border:none;
}
a{
color:#198DD0;
text-decoration:none;
}
a:hover{
color:#ba2636;
text-decoration:underline;
}
a{blr:expression(this.onFocus=this.blur())}/*去掉a標簽的虛線框,避免出現奇怪的選中區域*/
:focus{outline:0;}
.talk{
height: 480px;
width: 335px;
margin:0 auto;
border-left-width: 1px;
border-left-style: solid;
border-left-color: #444;
}
.talk_title{
width: 100%;
height:40px;
line-height:40px;
text-indent: 12px;
font-size: 16px;
font-weight: bold;
color: #afafaf;
background:#212121;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: #434343;
font-family: "微軟雅黑";
}
.talk_title span{float:left}
.talk_title_c {
width: 100%;
height:30px;
line-height:30px;
}
.talk_record{
width: 100%;
height:398px;
overflow: hidden;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: #434343;
margin: 0px;
}
.talk_word {
line-height: 40px;
height: 40px;
width: 100%;
background:#212121;
}
.messages {
height: 24px;
width: 240px;
text-indent:5px;
overflow: hidden;
font-size: 12px;
line-height: 24px;
color: #666;
background-color: #ccc;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
}
.messages:hover{background-color: #fff;}
.talk_send{
width:50px;
height:24px;
line-height: 24px;
font-size:12px;
border:0px;
margin-left: 2px;
color: #fff;
background-repeat: no-repeat;
background-position: 0px 0px;
background-color: transparent;
font-family: "微軟雅黑";
}
.talk_send:hover {
background-position: 0px -24px;
}
.talk_record ul{ padding-left:5px;}
.talk_record li {
line-height: 25px;
}
.talk_word .controlbtn a{
margin: 12px;
}
.talk .talk_word .order {
float:left;
display: block;
height: 14px;
width: 16px;
background-repeat: no-repeat;
background-position: 0px 0px;
}
.talk .talk_word .loop {
float:left;
display: block;
height: 14px;
width: 16px;
background-repeat: no-repeat;
background-position: -30px 0px;
}
.talk .talk_word .single {
float:left;
display: block;
height: 14px;
width: 16px;
background-repeat: no-repeat;
background-position: -60px 0px;
}
.talk .talk_word .order:hover,.talk .talk_word .active{
background-position: 0px -20px;
text-decoration: none;
}
.talk .talk_word .loop:hover{
background-position: -30px -20px;
text-decoration: none;
}
.talk .talk_word .single:hover{
background-position: -60px -20px;
text-decoration: none;
}
/*討論區*/
.jp-container .talk_recordbox{
min-height:80px;
color: #afafaf;
padding-top: 5px;
padding-right: 10px;
padding-left: 10px;
padding-bottom: 0px;
}
.jp-container .talk_recordbox:first-child{border-top:none;}
.jp-container .talk_recordbox:last-child{border-bottom:none;}
.jp-container .talk_recordbox .talk_recordtextbg{
float:left;
width:10px;
height:30px;
display:block;
background-repeat: no-repeat;
background-position: left top;}
.jp-container .talk_recordbox .talk_recordtext{
-moz-border-radius:5px;
-webkit-border-radius:5px;
border-radius:5px;
background-color:#b8d45c;
width:240px;
height:auto;
display:block;
padding: 5px;
float:left;
color:#333333;
}
.jp-container .talk_recordbox h3{
font-size:14px;
padding:2px 0 5px 0;
text-transform:uppercase;
font-weight: 100;
}
.jp-container .talk_recordbox .user {
float:left;
display:inline;
height: 45px;
width: 45px;
margin-top: 0px;
margin-right: 5px;
margin-bottom: 0px;
margin-left: 0px;
font-size: 12px;
line-height: 20px;
text-align: center;
}
/*自己發言樣式*/
.jp-container .talk_recordboxme{
display:block;
min-height:80px;
color: #afafaf;
padding-top: 5px;
padding-right: 10px;
padding-left: 10px;
padding-bottom: 0px;
}
.jp-container .talk_recordboxme .talk_recordtextbg{
float:right;
width:10px;
height:30px;
display:block;
background-repeat: no-repeat;
background-position: left top;}
.jp-container .talk_recordboxme .talk_recordtext{
-moz-border-radius:5px;
-webkit-border-radius:5px;
border-radius:5px;
background-color:#fcfcfc;
width:240px;
height:auto;
padding: 5px;
color:#666;
font-size:12px;
float:right;
}
.jp-container .talk_recordboxme h3{
font-size:14px;
padding:2px 0 5px 0;
text-transform:uppercase;
font-weight: 100;
color:#333333;
}
.jp-container .talk_recordboxme .user{
float:right;
height: 45px;
width: 45px;
margin-top: 0px;
margin-right: 10px;
margin-bottom: 0px;
margin-left: 5px;
font-size: 12px;
line-height: 20px;
text-align: center;
display:inline;
}
.talk_time{
color: #666;
text-align: right;
width: 240px;
display: block;
}
測驗
首先,啟動三個視窗

群聊

單聊

總結
本文,基于Netty實戰了一個WebSocket協議實作的網頁版聊天室服務器,從代碼上可以看出,基于Netty的WebSocket的實作還是非常簡單、容易實作的,但是WebSocket協議使用上還是存在局限的,比如需要瀏覽器的支持,但是畢竟WebSocket代表了Web技術的一種重要進展,可以擴寬我們的視野,在一些特定的作業場景中,可以幫助我們解決一些問題
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/230648.html
標籤:其他
