Netty权威指南之Websocket协议开发

阿里 阅读:591 2021-03-31 22:28:24 评论:0

本章主要学习内容如下:

1、HTTP协议弊端

2、WebSocket入门

3、Netty WebSocket协议开发


第一节:HTTP协议弊端

将HTTP协议的主要弊端总结如下:

1、HTTP协议为半双工协议。半双工协议指数据可以在客户端和服务端两个方向上传输,但不能同时传输。它意味着在同一时刻,只有一个方向上的数据传输。

2、HTTP消息繁琐。HTTP消息包含消息头、消息体、换行符等,通常情况下采用文本方式传输,相比于其他的二进制通信消息协议,显得繁琐。

3、针对服务器推送的黑客攻击。比如:长时间轮训。


第二节:WebSocket入门

WebSocket是HTML5开始提供的一种浏览器与服务器进行全双通信的网络技术,WebSocket通信协议与2011年被IEIF定位标准RFC6455,WebsSocket api被W3c定为标准。

在WebSocket api中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间形成了一条快速通道,两者就可以之间互相传输消息数据了。WebSocket基于TCP双向全双工进行消息传递,同一时刻,既可以发送消息,也可以接受消息,相比HTTP的半双工协议,性能得到很大的提升。

下面总结一下WebSocket的特点

1、单一TCP连接,采用全双工模式通信。

2、对代理、防火墙和路由器透明。

3、无头部信息、Cookie和身份验证

4、无安全开销

5、通过ping/pong 帧保持链路激活状态

6、服务器可以主动传递消息给客户端,不需要客户端轮训。

WebSocket背景

WebSocket设计出来的目的就是取代轮训和Comet技术,使客户端浏览器具备向C/S架构下桌面系统一样的实时通讯能力。浏览器通过javascript 向服务器发出建立WebSocket连接的请求,连接成功后,客户端和服务端可以通过TCP直接交互数据。因为WebSocket连接本质上就是一个TCP连接,所以在数据传输的稳定性和数据传输量的大小方面,和轮训即Comet技术相比,具有很大的优势。WebSocket.org网站对传统的轮训方式和WebSocket 调用方式作了一个详细的测试和比较,将一个简单的web应用分别通过轮训方式和websocket方式来实现,在这里引用一下测试结果,如图所示:


WebSocket 连接建立

客户端和服务端建立连接的示意图如下:


建立WebSocket连接时,需要通过客户端或者浏览器发出握手请求,请求消息如图所示:


为了建立一个WebSocket 连接,客户端浏览器首先向服务器发起一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加头信息,其中附加头信息“Upgrade:Websocket”表明这是一个申请协议升级的HTTP请求。服务端解析这些附加头信息,然后生成应答信息返回客户端,客户端和服务端的WebSoket连接就建立起来了,双方可以通过这个连接通道自由传递信息,并且这个连接会持续存在直到客户端或服务器端的某一方主动关闭连接。


请求消息中的"sec-websocket-key"是随机的,服务器会用这些数据构造出一个SHA-1的消息摘要,把“sec-websocket-key”加上一个魔幻字符串“*************”。使用SHA-1加密,然后进行Base64编码,将结果作为"sec-websocket-accept"头的值,返回给客户端。


WebSocket 生命周期

握手成功之后,服务端和客户端就可以通过“message”的方式进行通信了,一个消息有一个或多个帧组成,WebSocket的消息并不一定对应一个特定网络层的帧,他可以被分割成多个帧或者被合并。

帧都有自己对应的类型,属于同一消息的多个帧具有相同的数据类型。从广义上讲,数据类型可以使文本数据、二进制数据和控制帧。



WebSocket 连接关闭

为了关闭WebSocket 连接,客户端和服务端需要通过一个安全的方法关闭底层TCP连接以及TLS会话。如果合适,丢弃任何可能已经接受的字节:必要时,可以通过任何手段关闭连接。

底层的TCP连接,在正常的情况下,应该首先由服务器关闭。在异常的情况下,客户端可以发起TCP close 指令。因为,当服务器被指示关闭WebSocket连接时,它应该立即发起一个TCP Close操作;客户端应该等待服务器的TCP close.

WebSocket 的握手关闭消息带有一个状态码和一个可选关闭原因,它必须按照协议要求发送一个Close 控制帧,当对端接受到关闭帧指令时,需要主动关闭Websocket 连接。


第三节:Netty WebSocket协议开发

Websocket 服务端功能介绍

Websocket服务端的功能如下:支持websocket的浏览器通过websocket协议发送请求消息给客户端,服务端对请求消息进行判断,如果是合法的websocket 请求,则获取请求消息文本,并在后面追加字符串“欢迎使用Netty websocket 服务”。

客户端HTML通过内嵌的js 脚本创建websocke 连接,如果握手成功,在文本框中打印“打开websocket 服务正常,浏览器支持websocket ”。客户端界面如图所示:



WebSocket 服务端开发

首先对WebSocket服务端的功能进行简单的讲解。WebSocket服务端接受到请求消息之后,先对消息的类型进行判断。如果不是WebSocket 握手的请求消息,则返回HTT 400 响应客户端。客户端的握手请求信息如图所示:


服务端对握手请求消息进行处理,构造握手响应返回,双方的socket 连接正式建立,服务端返回握手的应答消息如图所示

连接成功后,到关闭之前,双方都可以主动向对方发送消息,这点跟HTTP的请求/应答模式存在很大的差别。相比与HTTP,它的网络利用率更高,可以通过全双工的方式进行消息的接收和发送。


Netty WebSocket 服务端源代码:

WebSocketServer.java

package com.nio.server; 
 
import com.nio.handler.WebSocketServerHandler; 
import io.netty.bootstrap.ServerBootstrap; 
import io.netty.channel.Channel; 
import io.netty.channel.ChannelInitializer; 
import io.netty.channel.ChannelPipeline; 
import io.netty.channel.EventLoopGroup; 
import io.netty.channel.nio.NioEventLoopGroup; 
import io.netty.channel.socket.SocketChannel; 
import io.netty.channel.socket.nio.NioServerSocketChannel; 
import io.netty.handler.codec.http.HttpObjectAggregator; 
import io.netty.handler.codec.http.HttpServerCodec; 
import io.netty.handler.stream.ChunkedWriteHandler; 
 
/** 
 * Created by vixuan-008 on 2015/6/25. 
 */ 
public class WebSocketServer { 
    public void run(int port) throws Exception { 
        EventLoopGroup bossGroup = new NioEventLoopGroup(); 
        EventLoopGroup workerGroup = new NioEventLoopGroup(); 
        try { 
            ServerBootstrap b = new ServerBootstrap(); 
            b.group(bossGroup, workerGroup) 
                    .channel(NioServerSocketChannel.class) 
                    .childHandler(new ChannelInitializer<SocketChannel>() { 
 
                        @Override 
                        protected void initChannel(SocketChannel ch) 
                                throws Exception { 
                            ChannelPipeline pipeline = ch.pipeline(); 
                            pipeline.addLast("http-codec", 
                                    new HttpServerCodec()); 
                            pipeline.addLast("aggregator", 
                                    new HttpObjectAggregator(65536)); 
                            ch.pipeline().addLast("http-chunked", 
                                    new ChunkedWriteHandler()); 
                            pipeline.addLast("handler", 
                                    new WebSocketServerHandler()); 
                        } 
                    }); 
 
            Channel ch = b.bind(port).sync().channel(); 
            System.out.println("Web socket server started at port " + port 
                    + '.'); 
            System.out 
                    .println("Open your browser and navigate to http://localhost:" 
                            + port + '/'); 
 
            ch.closeFuture().sync(); 
        } finally { 
            bossGroup.shutdownGracefully(); 
            workerGroup.shutdownGracefully(); 
        } 
    } 
 
    public static void main(String[] args) throws Exception { 
        int port = 17888; 
        new WebSocketServer().run(port); 
    } 
}
WebSocketServerHandler.java

package com.nio.handler; 
 
import io.netty.channel.ChannelHandlerContext; 
import io.netty.channel.SimpleChannelInboundHandler; 
import io.netty.handler.codec.http.FullHttpRequest; 
import io.netty.handler.codec.http.websocketx.*; 
 
/** 
 * Created by vixuan-008 on 2015/6/25. 
 */ 
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> { 
    private WebSocketServerHandshaker handshaker; 
    @Override 
    protected void messageReceived(ChannelHandlerContext channelHandlerContext, Object o) throws Exception { 
 
        // WebSocket接入 
         if (o instanceof WebSocketFrame) { 
             System.out.println("request meesage body:"+o); 
            handleWebSocketFrame(channelHandlerContext, (WebSocketFrame) o); 
        }else if(o instanceof FullHttpRequest){ 
             System.out.println("server receiver message is:"+o); 
             handleHttpRequest(channelHandlerContext, (FullHttpRequest) o); 
 
         } 
    } 
 
    @Override 
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 
        ctx.flush(); 
    } 
    private void handleHttpRequest(ChannelHandlerContext ctx, 
                                   FullHttpRequest req) throws Exception { 
 
 
 
        // 构造握手响应返回,本机测试 
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( 
                "ws://localhost:17888/websocket", null, false); 
        handshaker = wsFactory.newHandshaker(req); 
        if (handshaker == null) { 
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); 
        } else { 
            handshaker.handshake(ctx.channel(), req); 
        } 
    } 
 
    private void handleWebSocketFrame(ChannelHandlerContext ctx, 
                                      WebSocketFrame frame) { 
 
        // 判断是否是关闭链路的指令 
        if (frame instanceof CloseWebSocketFrame) { 
            handshaker.close(ctx.channel(), 
                    (CloseWebSocketFrame) frame.retain()); 
            return; 
        } 
        // 判断是否是Ping消息 
        if (frame instanceof PingWebSocketFrame) { 
            ctx.channel().write( 
                    new PongWebSocketFrame(frame.content().retain())); 
            return; 
        } 
        // 本例程仅支持文本消息,不支持二进制消息 
        if (!(frame instanceof TextWebSocketFrame)) { 
            throw new UnsupportedOperationException(String.format( 
                    "%s frame types not supported", frame.getClass().getName())); 
        } 
 
        // 返回应答消息 
        String request = ((TextWebSocketFrame) frame).text(); 
        System.out.println("server receiver message is:"+request); 
 
        ctx.channel().write( 
                new TextWebSocketFrame(request 
                        + " , welocme to:" 
                        + new java.util.Date().toString())); 
    } 
 
    @Override 
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 
        ctx.close(); 
    } 
} 

客户端代码:

<!DOCTYPE html> 
<html> 
<head> 
<meta charset="UTF-8"> 
Netty WebSocket 时间服务器 
</head> 
<br> 
<body> 
<br> 
<script type="text/javascript"> 
var socket; 
if (!window.WebSocket)  
{ 
	window.WebSocket = window.MozWebSocket; 
} 
if (window.WebSocket) { 
	socket = new WebSocket("ws://localhost:17888/websocket"); 
	socket.onmessage = function(event) { 
		var ta = document.getElementById('responseText'); 
		ta.value=""; 
		ta.value = event.data 
	}; 
	socket.onopen = function(event) { 
		var ta = document.getElementById('responseText'); 
		ta.value = "打开WebSocket服务正常,浏览器支持WebSocket!"; 
	}; 
	socket.onclose = function(event) { 
		var ta = document.getElementById('responseText'); 
		ta.value = ""; 
		ta.value = "WebSocket 关闭!";  
	}; 
} 
else 
	{ 
	alert("抱歉,您的浏览器不支持WebSocket协议!"); 
	} 
 
function send(message) { 
	if (!window.WebSocket) { return; } 
	if (socket.readyState == WebSocket.OPEN) { 
		socket.send(message); 
	} 
	else 
		{ 
		  alert("WebSocket连接没有建立成功!"); 
		} 
} 
</script> 
<form οnsubmit="return false;"> 
<input type="text" name="message" value="Netty最佳实践"/> 
<br><br> 
<input type="button" value="发送WebSocket请求消息" οnclick="send(this.form.message.value)"/> 
<hr color="blue"/> 
<h3>服务端返回的应答消息</h3> 
<textarea id="responseText" style="width:500px;height:300px;"></textarea> 
</form> 
</body> 
</html>

执行结果:

服务端:


客户端:


声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

关注我们

一个IT知识分享的公众号