前言
WebSocket是一种在单个TCP连接上进行全双工的通信协议,WebSocket使得客户端和服务器之间的数据交换变的更加简单,允许服务器主动向客户端推送数据,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
HTTP协议的弊端
很多网站为了实现推送技术,所用的技术都是轮询,轮询是在特定的时间间隔,由浏览器对服务器发送HTTP请求,然后由服务器返回最新的数据给客户端的浏览器,这种方式的主要弊端如下。
(1)HTTP协议为半双工协议,半双工协议指数据可以在客户端和服务端两个方向上传输,但是不能同时传输,它意味着在同一个时刻,只有一个方向上的数据传送。
(2)HTTP消息可能包含较长的消息头、消息体、其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
(3)针对服务器推送的黑客攻击,例如长时间轮询。
而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。
在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
WebSocket优点
在WebSocket中,浏览器和服务器只需要完成一次握手的过程,之后,浏览器和服务器之间就形成了一条快速通道,两者就可以直接互相传送数据了,WebSocket基于TCP双向全双工进行消息传递,在同一时刻,既可以发送消息,也可以接收消息,相比HTTP的半双工协议,性能得到很大提升。
具体优点如下:
(1)较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
(2)更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
(3)保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
(4)更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
(5)可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
(6)更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
Netty WebSocket开发
我们将通过使用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.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
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.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.util.concurrent.ImmediateEventExecutor;
public class ChatServer {
/*
创建DefaultChannelGroup,其将保存所有已经连接的WebSocket Channel
*/
private static final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
public static void main(String[] args) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
/*
HttpServerCodec同时包含了(HttpRequestDecoder, HttpResponseEncoder),
通过HttpServerCodec将请求和响应编码或者解码为HTTP消息
*/
pipeline.addLast(new HttpServerCodec());
//聚合http为一个完整的报文
pipeline.addLast(new HttpObjectAggregator(65536));
/*
指定"/ws"为WebSocket升级请求的路径。
并按照 WebSocket 规范的要求,处理 WebSocket 升级握手、
PingWebSocketFrame 、 PongWebSocketFrame 和 CloseWebSocketFrame
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
/*
处理FullHttpRequest请求。
在InitChatRoomHandler中,生成聊天室的HTML页面,并指定使用WS协议通信。
*/
pipeline.addLast(new InitChatRoomHandler());
//处理 TextWebSocketFrame 和握手完成事件
pipeline.addLast(new TextWebSocketFrameHandler(channelGroup));
}
});
ChannelFuture future = b.bind(8088).sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
InitChatRoomHandler
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
public class InitChatRoomHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
//只接收http://127.0.0.1:8088/请求
if ("/".equals(req.uri())) {
//生成一个HTML页面,并指定使用WebSocket协议访问,最后地址为/ws,服务器将会处理WebSocket升级
ByteBuf content = MakeIndexPage.getContent("ws://127.0.0.1:8088/ws");
//构建HTTP请求
FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
HttpUtil.setContentLength(res, content.readableBytes());
sendHttpResponse(ctx, req, res);
} else {
//如果是其他访问,直接返回404
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND));
}
}
private static void sendHttpResponse(ChannelHandlerContext ctx,
FullHttpRequest req,
FullHttpResponse res) {
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);
//如果非Keep-Alive,或者200,则关闭连接
if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
}
TextWebSocketFrameHandler
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import java.util.Locale;
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private final ChannelGroup group;
public TextWebSocketFrameHandler(ChannelGroup group) {
this.group = group;
}
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
String request = frame.text();
//把消息写到ChannelGroup中所有已经连接的客户端
group.writeAndFlush(
new TextWebSocketFrame(
"Client " + ctx.channel() + " say: " + request.toUpperCase(Locale.CHINA)
));
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx,
Object evt) throws Exception {
//如果是握手事件,则通知所有已经连接上的客户端
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
//从pipeline中移除InitChatRoomHandler,因为将不会收到任何HTTP消息了
ctx.pipeline().remove(InitChatRoomHandler.class);
group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel() + " joined"));
//把新的websocket channel添加到ChannelGroup中,这样它就可以接收到所有的消息了。
group.add(ctx.channel());
} else {
super.userEventTriggered(ctx, evt);
}
}
}
MakeIndexPage
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
public final class MakeIndexPage {
private static final String NEWLINE = "\r\n";
public static ByteBuf getContent(String webSocketLocation) {
return Unpooled.copiedBuffer(
"<html><head><title>Web Socket Test</title></head>"
+ NEWLINE +
"<body>" + NEWLINE +
"<script type=\"text/javascript\">" + NEWLINE +
"var socket;" + NEWLINE +
"if (!window.WebSocket) {" + NEWLINE +
" window.WebSocket = window.MozWebSocket;" + NEWLINE +
'}' + NEWLINE +
"if (window.WebSocket) {" + NEWLINE +
" socket = new WebSocket(\"" + webSocketLocation + "\");"
+ NEWLINE +
" socket.onmessage = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = ta.value + '\\n' + event.data" + NEWLINE +
" };" + NEWLINE +
" socket.onopen = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = \"Web Socket opened!\";" + NEWLINE +
" };" + NEWLINE +
" socket.onclose = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = ta.value + \"Web Socket closed\"; "
+ NEWLINE +
" };" + NEWLINE +
"} else {" + NEWLINE +
" alert(\"Your browser does not support Web Socket.\");"
+ NEWLINE +
'}' + NEWLINE +
NEWLINE +
"function send(message) {" + NEWLINE +
" if (!window.WebSocket) { return; }" + NEWLINE +
" if (socket.readyState == WebSocket.OPEN) {" + NEWLINE +
" socket.send(message);" + NEWLINE +
" } else {" + NEWLINE +
" alert(\"The socket is not open.\");" + NEWLINE +
" }" + NEWLINE +
'}' + NEWLINE +
"</script>" + NEWLINE +
"<form οnsubmit=\"return false;\">" + NEWLINE +
"<input type=\"text\" name=\"message\" " +
"value=\"Hello, World!\"/>" +
"<input type=\"button\" value=\"Send Web Socket Data\""
+ NEWLINE +
" οnclick=\"send(this.form.message.value)\" />"
+ NEWLINE +
"<h3>服务端返回的消息</h3>" + NEWLINE +
"<textarea id=\"responseText\" " +
"style=\"width:500px;height:300px;\"></textarea>"
+ NEWLINE +
"</form>" + NEWLINE +
"</body>" + NEWLINE +
"</html>" + NEWLINE, CharsetUtil.UTF_8);
}
}
运行结果
ChannelPipeline的状态图
WebSocket协议升级之前的ChannelPipeline的状态如下图:
当WebSocket协议升级完成之后,WebSocketServerProtocalHandler将会把HttpRequestDecoder替换为WebSocketFrameDecoder,把HttpResponseEncoder替换为WebSocketFrameEncoder,为了性能最大化,还将移除任何不再被WebSocket连接所需要的ChannelHandler。
升级为WebSocket之后ChannelPipeline的状态如下图: