netty对websocket的支持
什么是WebSocket?
WebSocket是一种在2011年被互联网工程任务组(IETF)标准化的协议。WebSocket解决了一个长期存在的问题:既然底层的协议(HTTP)是一个请求/响应模式的交互序列(半双工),那么如何实时地发布信息呢?AJAX提供了一定程度上的改善,但是数据流仍然是由客户端所发送的请求驱动的,还有其他的一些或多或少的取巧方式(Comet)。
WebSocket规范以及它的实现代表了对一种更加有效的解决方案的尝试。简单地说,WebSocket提供了在一个单个的TCP连接上提供双向的通信,它为网页和远程服务器之间的双向通信提供了一种替代HTTP轮询的方案。也就是说,WebSocket在客户端和服务器之间提供了真正的双向数据交换,WebSocket连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket只需要建立一次连接,就可以一直保持连接状态,这相比于轮询方式的不停建立连接显然效率要大大提高。
特点:
- HTML5中的协议,实现与客户端与服务器双向通信,基于消息的文本或二进制数据通信。
- 适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户。
- 与服务频繁交互的情况下,如实时共享、多人协作等平台。
缺点:
- 采用新的协议,前台和后端都需要实现websocket协议。
- 客户端并不是所有浏览器都支持。
WebSocket通信握手
Websocket借用了HTTP的协议来完成一部分握手。
要使用WebSocket,客户端的请求头中必须携带如下信息:
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: eKx07sBdizAQzCa6Hq4Bvw==
Sec-WebSocket-Version: 13
说明:
Connection: Upgrade
:表示客户端希望连接升级。Upgrade: websocket
:Upgrade字段必须设置Websocket,表示希望升级到Websocket协议。Sec-WebSocket-Key
:是一个随机的字符串。Sec-WebSocket-Version
:表示支持的Websocket版本。RFC6455要求使用的版本是13,之前草案的版本均应当弃用。
服务器端返回的响应头:
connection: upgrade
sec-websocket-accept: tUrLqjHcbpMSjRS6Lqcns5SN8Ac=
upgrade: websocket
Upgrade: websocket
:与客户端一致。Connection: Upgrade
:与客户端一致。sec-websocket-accept
:服务器端把客户端端请求头中的Sec-WebSocket-Key
加上一个特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算SHA-1摘要,之后进行BASE-64编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通HTTP请求被误认为Websocket协议。
至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行通信。
netty代码实现
服务器端代码如下:
Server.java
package com.morris.netty.protocol.websocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
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.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class Server {
private static final int port = 8899;
public static void main(String[] args) 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(new HttpServerCodec()); // 请求消息编解码器
pipeline.addLast(new HttpObjectAggregator(65536));// 目的是将多个消息转换为单一的request或者response对象
pipeline.addLast(new WebSocketServerProtocolHandler("/ws")); // websocket协议支持,url为/ws
pipeline.addLast(new ServerHandler());
}
});
ChannelFuture future = b.bind("127.0.0.1", port).sync();
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
ServerHandler.java
package com.morris.netty.protocol.websocket;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
@Slf4j
public class ServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
log.info("receive from client: {}", msg.text());
ctx.writeAndFlush(new TextWebSocketFrame(LocalDateTime.now().toString()));
}
}
客户端采用html,代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Socket</title>
<script type="text/javascript">
var websocket;
//如果浏览器支持WebSocket
if(window.WebSocket){
websocket = new WebSocket("ws://localhost:8899/ws"); //获得WebSocket对象
//当有消息过来的时候触发
websocket.onmessage = function(event){
var respMessage = document.getElementById("respMessage");
respMessage.value += "\n" + event.data;
}
//连接关闭的时候触发
websocket.onclose = function(event){
var respMessage = document.getElementById("respMessage");
respMessage.value += respMessage.value + "\n断开连接";
}
//连接打开的时候触发
websocket.onopen = function(event){
var respMessage = document.getElementById("respMessage");
respMessage.value = "建立连接";
}
}else{
alert("浏览器不支持WebSocket");
}
function sendMsg(msg) { //发送消息
if(window.WebSocket){
if(websocket.readyState == WebSocket.OPEN) { //如果WebSocket是打开状态
websocket.send(msg); //send()发送消息
}
}else{
return;
}
}
</script>
</head>
<body>
<form onsubmit="return false">
<textarea style="width: 300px; height: 200px;" name="message"></textarea>
<input type="button" onclick="sendMsg(this.form.message.value)" value="发送"><br>
<h3>信息</h3>
<textarea style="width: 300px; height: 400px;" id="respMessage"></textarea>
<input type="button" value="清空" onclick="javascript:document.getElementById('respMessage').value = ''">
</form>
</body>
</html>
测试结果如下:
可见Websocket需要借助HTTP的协议来完成握手,在数据的传输过程中不再需要http头部了,只有数据部分,真正的实现了实时和高效。
注意要使用websocket的压缩功能,要将WebSocketServerProtocolHandler的扩展参数改为true。
pipeline.addLast(new WebSocketServerCompressionHandler()); // 支持ws数据的压缩传输
pipeline.addLast(new WebSocketServerProtocolHandler("/ws", null ,true)); // websocket协议支持,url为/ws