Netty WebSocket协议栈开发
- 背景介绍
- HTTP 协议的弊端
- WebSocket 介绍
- WebSocket 特点
- WebSocket 连接建立
- Socket生命周期
- WebSocket关闭
- 开发
- WebSocket 服务端功能介绍
- 流程图
- 代码实现
- jar 依赖
- WebSocket 服务端启动类 WebSocketServer
- 服务端业务处理类 WebSocketServerHandler
- WebSocketServer.html
- 说明
- 测试
- 打开html页面
- 用Goole浏览器打开 WebSocketServer.html .
- 启动服务端。
- 服务端打印:
- 刷新下html. 打印信息如下;
- 点击 发送 WebSocket 请求消息,
- 关闭服务器
- 浏览器截图
- 总结
背景介绍
一直以来,网络在很大程度都是围绕着HTTP的请求/响应模式而构建的。客户端加载一个网页,然后直到用户点击,否则什么都不会发送。后来 AJAX 开始让网络变得更加动态,但 HTTP通信仍然是由客户端控制,需要用户进行互动或定期轮询。各种技术让服务器得知有新数据时,立即将数据发送到客户端。比如 "推送"或 Comet,长轮询。利用长轮询,客户端可以打开指向服务器的HTTP连接,而服务器会一直保持连接打开,直到发送响应。
但是,由于HTTP协议的开销,导致它们不适合低延迟应用。比如 IM 系统。
为了解决这些问题,WebSocket 将网络套接字引入到了客户端和服务端,浏览器和服务器之间可以通过套接字建立持久的连接,双方随时都可以互发数据给对方,而不是之前由客户端控制的一请求一应答模式。
HTTP 协议的弊端
(1)HTTP协议为半双工协议。半双工协议指数据可以在客户端和服务端两个方向传输,但是不能同时传输。意味着在同一个时刻,只有一个方向的数据传输。
(2)HTTP消息冗长而繁琐。HTTP 消息包含消息头,消息体,换行符。通常情况下采用文本方式传输,相比于其他的二进制通信协议,冗长繁琐。
(3)容易受到攻击。长时间轮询。
为了解决HTTP协议效率低下的问题。HTML5 定义了WebSocket协议,能更好的节省服务器资源和宽带并达到实时通信。
WebSocket 介绍
WebSocket 是 HTML5 开始提供的一种浏览器与服务器间进行全双工通信的网络技术。WebSocket通信协议于2011年被IETF定为标准 RFC6455,WebSocket API 被W3C定位标准。
在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后 浏览器和服务器之间就形成了一条快速通道,两者就可以直接互相传送数据了。WebSocket基于TCP双向全双工进行消息传递,在同一时空,即可以发送消息,也可以接收消息。相比于HTTP的半双工协议,性能得到大大提升。
WebSocket 特点
(1)单一的TCP连接,采用全双工模式通信
(2)对代理,防火墙和路由器透明
(3)五头部信息,Cookie和身份验证
(4)五安全开销
(5)通过 “ping/pong” 帧保持链路激活
(6)服务器可以主动传递消息给客户端,不再需要客户端轮询
WebSocket 设计出来就是要取代轮询和Comet技术。使客户端浏览器具备像C/S架构下桌面系统一样的实时通信能力。
WebSocket 连接建立
客户端和服务端建立连接。
请求消息中的Sec-WebSocket-Key 是随机的,服务端会用这些数据构造出一个SHA-1 的信息摘要。服务端会用这些数据来构造一个SHA-1的信息摘要。使用SHA-1 加密,然后进行BASE-64 编码,将结果作为"Sec-WebSocket-Accept"头的值,返回给客户端。
如图:
Socket生命周期
握手成功之后,服务端和客户端就可以通过"message"的方式进行通信了,一个消息由一个或多个帧组成,WebSocket 的消息不一定对应一个特定网络层的帧,它可以被分割成多个帧或者被合并。
帧都有自己对应的类型,属于同一个消息的多个帧具有相同类型的数据。从广义上将,数据类型可以是文本数据,二进制数据和控制帧。
Socket 生命周期图:
WebSocket关闭
为了关闭WebSocket 连接,客户端和服务端需要通过一个安全的方法关闭底层TCP连接以及TL会话。如果合适,丢弃任何可能已经接收的字节;必要时(比如受到攻击),可以通过任何可用的手段关闭连接。底层的TCP连接,在正常情况下,应该首先由服务器关闭。在异常情况下(例如在一个合理的时间周期后没有收到服务器的TCP Close),客户端可用发起 TCP Close。因此,当服务器被指示关闭WebSocket 连接时,他应该立即发起一个TCP Close 操作。客户端应该等待服务器的TCP Close。
WebSocket 的握手关闭消息带有一个状态码和一个可选的关闭原因,它必须按照协议要求发送一个Close控制帧,当对端接收到关闭控制帧指令时,需要主动关闭WebSocket连接。
开发
Netty 基于HTTP协议栈开发了WebSocket 协议栈,利用Netty的WebSocket 协议栈可以非常方便的开发出WebSocket客户端和服务端。
WebSocket 服务端功能介绍
支持WebSocket 的浏览器通过 WebSocket 协议发送请求消息给服务端,服务端对请求消息进行判断,如果时合法的WebSocket请求,则获取请求消息体,并在后面追加字符串 "欢迎使用Netty WebSocket 服务,现在是 "+系统时间。
客户端端 HTML通过内嵌的JS脚本创建WebSocket连接,如果握手成功,在文本框中打印 “打开WebSocket 服务正常,浏览器支持WebSocket!” 。
如果浏览器不支持WebSocket ,回提示 sorry,您的浏览器不支持WebSocket 协议。
流程图
代码实现
jar 依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId> <!-- Use 'netty5-all' for 5.0-->
<version>5.0.0.Alpha1</version>
<scope>compile</scope>
</dependency>
WebSocket 服务端启动类 WebSocketServer
public class WebSocketServer {
public void run(int port){
EventLoopGroup bossGroup=new NioEventLoopGroup();
EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
ServerBootstrap bootstrap=new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//将请求和应答消息编码或者解码为HTTP消息
socketChannel.pipeline()
.addLast("http-codec",new HttpServerCodec());
//将多个部分组合成一条完整的HTTP消息
socketChannel.pipeline()
.addLast("aggregator",new HttpObjectAggregator(65536));
//向客户端发送HTML5文件,主要用于支持浏览器和服务器进行WebSocket
socketChannel.pipeline()
.addLast("http-chunked",new ChunkedWriteHandler());
socketChannel.pipeline()
.addLast(new WebSocketServerHandler());
}
});
//服务端绑定端口
ChannelFuture future=bootstrap.bind(port).sync();
System.out.println("server is started.");
System.out.println("open your browser and navigate to http://127.0.0.1:"+port+"/");
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//优雅关闭
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
new WebSocketServer().run(8080);
}
}
服务端业务处理类 WebSocketServerHandler
注释里写了详细的说明。
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
private static final Logger logger = Logger.getLogger(WebSocketServerHandler.class.getName());
private WebSocketServerHandshaker handshaker;
@Override
protected void messageReceived(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception {
//如果是传统的HTTP接入
if (msg instanceof FullHttpRequest) {
handleHttpRequest(channelHandlerContext, (FullHttpRequest) msg);
} else if (msg instanceof WebSocketFrame) {
handleWebSocketFrame(channelHandlerContext, (WebSocketFrame) msg);
}
}
public void channelReadComplete(ChannelHandlerContext context) {
context.flush();
}
/**
* 第一次握手请求消息 由HTTP协议承载,
* @param context
* @param request
*/
private void handleHttpRequest(ChannelHandlerContext context, FullHttpRequest request) {
//如果Http解码失败,返回HTTP异常
if (!request.getDecoderResult().isSuccess()
|| (!"websocket".equals(request.headers().get("Upgrade")))) {
sendHttpResponse(context, request,
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
//校验通过,开始构造握手工厂,然后构造握手处理类WebSocketServerHandshaker。
//构造握手响应返回,本机测试
WebSocketServerHandshakerFactory factory =
new WebSocketServerHandshakerFactory("ws://127.0.0.1:8080/websocket",
null, false);
handshaker=factory.newHandshaker(request);
if (handshaker==null){
WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(context.channel());
}else {
//将WebSocket 相关的编码和解码类动态添加到ChannelPipeline中,用于WebSocket消息的编解码
// 有了WebSocket Encoder 和 WebSocket Decoder之后。服务端就可以自动对WebSocket消息进行编解码了。
// 源码可以查看 WebSocketServerHandshaker.handshake
handshaker.handshake(context.channel(),request);
}
}
private void handleWebSocketFrame(ChannelHandlerContext context, WebSocketFrame socketFrame) {
//判断是否关闭链路的指令
if (socketFrame instanceof CloseWebSocketFrame){
handshaker.close(context.channel(),(CloseWebSocketFrame) socketFrame.retain());
return;
}
//判断是否是ping消息
if (socketFrame instanceof PingWebSocketFrame){
context.channel().write(new PongWebSocketFrame(socketFrame.content().retain()));
return;
}
//本demo 只支持文本消息,不支持二进制消息
//其他消息直接抛出异常
if (!(socketFrame instanceof TextWebSocketFrame)){
throw new UnsupportedOperationException(
String.format("%s frame types not supported",socketFrame.getClass().getName()));
}
//返回应答消息
//从 TextWebSocketFrame 中获取请求消息字符串
String requestStr=((TextWebSocketFrame) socketFrame).text();
if (logger.isInfoEnabled()){
logger.info(String.format("%s received %s",context.channel(),requestStr));
}
//通过处理后,通过构造新的TextWebSocketFrame 消息返回给客户端。
// 之前添加了TextWebSocketFrame的编码类。
// 所以可以直接发送TextWebSocketFrame对象
String timeStr=new SimpleDateFormat("YYYY-MM-DD dd:mm:ss").format(new Date());
context.channel().write(new TextWebSocketFrame(
requestStr+" ,欢迎使用Netty WebSocket 服务,现在是 : "+timeStr));
}
private static void sendHttpResponse(ChannelHandlerContext context, FullHttpRequest request,
FullHttpResponse response) {
//返回应答给客户端
if (response.getStatus().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(response.toString().toString(), CharsetUtil.UTF_8);
response.content().writeBytes(buf);
buf.release();
setContentLength(response, response.content().readableBytes());
}
//如果是非Keep-Alive ,关闭连接
ChannelFuture future = context.channel().writeAndFlush(response);
if (!isKeepAlive(request) || response.getStatus().code() != 200) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
public void exceptionCaught(ChannelHandlerContext context, Throwable cause) {
cause.printStackTrace();
context.close();
}
}
WebSocketServer.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Netty WebSocket</title>
</head>
<body>
<br>
<script type="text/javascript">
var socket;
if(!window.WebSocket){
window.WebSocket=window.MozWebSocket;
}
//判断浏览器是否支持WebSocket
if(window.WebSocket){
// 进行握手链接
socket=new WebSocket("ws://127.0.0.1:8080/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!";
};
//如果服务器关闭,显示 WebSocket 关闭
socket.onclose=function(event){
var ta=document.getElementById('responseText');
ta.value="WebSocket 关闭!!";
};
}else{
alert("sorry,您的浏览器 不支持 WebSocket 协议! ");
}
function send(message){
if(!window.WebSocket){
return ;
}
if(socket.readyState==WebSocket.OPEN){
socket.send(message);
}else{
alert("WebSocket 连接没有建立成功!");
}
}
</script>
<form onsubmit="return false;">
<input type="text" name="message" value="Netty learning WebSocket"/>
<br><br>
<input type="button" value=" 发送 WebSocket 请求消息 " onclick="send(this.form.message.value)"/>
<hr color="blue"/>
<h3> 服务端返回应答消息</h3>
<textarea id="responseText" style="width:500px;height:300px;"></textarea>
</form>
</body>
</html>
说明
总体代码比较简单。特别是客户端的html代码,但是 很方便我们进行测试。各种打印信息,都能帮助我们更好的理解 WebSocket协议。
测试
打开html页面
用Goole浏览器打开 WebSocketServer.html .
此时浏览器的显示界面。能正确显示 服务器是关闭的。
启动服务端。
服务端打印:
刷新下html. 打印信息如下;
点击 发送 WebSocket 请求消息,
此时可以鼠标无限制点击,发现时间在变化,说明服务端在不停的返回数据。此时 http请求还是101。
和我们常见的HTTP请求显然不同
关闭服务器
此时服务端会给客户端发送 TCP Close。浏览器客户端接收到了消息后,会进行对应打印信息展示。
浏览器截图
总结
本章首先介绍了HTTP协议的弊端和产生WebSocket 的一些技术背景,随后介绍了下WebSocket的基本信息。然后写了一个小demo。但是 WebSocket 是很复杂的,这个demo 只能算一个入门的小例子。如果想深入学习 WebSocket。还需要不断的深入学习。