概述
Netty的应用场景中,socket通讯占了大多数,而WebSocket作为于H5推出内容中的重要功能点,Netty也做了很好的支持。在学习WebSocket的时候,首先要明确几个问题,WebSocket是什么,为什么要用推出WebSocket,其应用场景是什么,主要解决什么问题。
什么是WebSocket
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
摘抄于百度百科
简单的来讲,WebSocket是可以让客户端和服务端进行双向通讯的应用层协议。用于在Web浏览器和服务器之间进行任意的双向数据传输的一种技术。WebSocket协议基于TCP协议实现,包含初始的握手过程,以及后续的多次数据帧双向传输过程。WebSocket目前支持两种统一资源标志符ws
和wss
,类似于HTTP和HTTPS。
- 发送一个GET请求,关键:Upgrade: websocket & Connection: Upgrade,这两个就告诉服务器,我要发起websocket协议,我不是HTTP
- 服务器收到了协议,返回一个 Switching Protocol, 这样就连接成功了,http升级为WebSocket
- 接下来的通信都是websocket, 这样就很好的连接了
解决什么问题&其应用场景
在早期的程序中,基于Http1.0的请求只能是客户端来发起的,服务端处于”被动“的状态,且协议无状态,请求与请求之间并没有什么联系,需要靠cookie或者session来维护用户信息。当需要有服务端向客户端发起请求(推送数据)的时候,就很麻烦。常用的做法是每一个客户端都对服务端进行轮询请求,而轮询的弊端显而易见,会有很多轮询的请求是无意义的,并且由于Http协议规则,导致每一次请求需要有请求头和请求体,请求的数据文本很大,设置很多情况下请求到的数据还没有请求头的长,这样就造成了服务端带宽的压力,即使之后推出的Http1.1有了keeplive的能力,由于其状态保持的时间很短,也只能视为是一种没有办法的办法。常见的”双向通讯“场景有消息推送、即时通讯类应用等。
WebSocket优势
-
较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
-
更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
-
保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
-
更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
-
可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
-
更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
示例
服务端:
package com.leolee.netty.fifthExample;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import java.net.InetSocketAddress;
/**
* @ClassName MyServer
* @Description: webSocket
* @Author LeoLee
* @Date 2020/8/30
* @Version V1.0
**/
public class MyServer {
public static void main(String[] args) throws InterruptedException {
//定义线程组 EventLoopGroup为死循环
//boss线程组一直在接收客户端发起的请求,但是不对请求做处理,boss会将接收到的请i交给worker线程组来处理
//实际可以用一个线程组来做客户端的请求接收和处理两件事,但是不推荐
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//启动类定义
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
//子处理器,自定义处理器,服务端可以使用childHandler或者handler,handlerr对应接收线程组(bossGroup),childHandler对应处理线程组(workerGroup)
.handler(new LoggingHandler(LogLevel.INFO))//日志处理器
.childHandler(new WebSocketChannelInitializer());
//绑定监听端口
ChannelFuture channelFuture = serverBootstrap.bind(new InetSocketAddress(8899)).sync();
//定义关闭监听
channelFuture.channel().closeFuture().sync();
} finally {
//Netty提供的优雅关闭
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
package com.leolee.netty.fifthExample;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
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.handler.stream.ChunkedWriteHandler;
/**
* @ClassName WebSocketChannelInitializer
* @Description: TODO
* @Author LeoLee
* @Date 2020/8/30
* @Version V1.0
**/
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//Http处理
pipeline.addLast("httpServerCodec", new HttpServerCodec());
pipeline.addLast("chunkedWriteHandler", new ChunkedWriteHandler());//之后再学习
//Netty对于http请求是分块或者分段的方式,比如一个请求发送的数据长度是1000,被切成了10段,该处理器就按照8192最大长度,去聚合这些请求数据
pipeline.addLast("httpObjectAggregator", new HttpObjectAggregator(8192));
//websocket处理
//负责websocket的连接,以及控制frames(close Ping Pong)的处理,文字和二进制数据传递给下一个处理器处理,websocket的数据基于各种frames
pipeline.addLast("webSocketServerProtocolHandler", new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new TextWebSocketFrameHandler());
}
}
package com.leolee.netty.fifthExample;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.time.LocalDateTime;
/**
* @ClassName TextWebSocketFrameHandler
* @Description: 处理websocket文本数据
* @Author LeoLee
* @Date 2020/8/30
* @Version V1.0
**/
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("收到客户端消息:" + msg.text());
//写数据给客户端
ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间:" + LocalDateTime.now()));
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerAdded:" + ctx.channel().id().asLongText());//channel的全局唯一id
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved:" + ctx.channel().id().asLongText());//channel的全局唯一id
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("发生异常");
ctx.close();
}
}
服务端的代码和之前学习中的基本一致,需要注意的是TextWebSocketFrameInitializer中继承SimpleChannelInboundHandler时候,泛型传递的是TextWebSocketFrame,TextWebSocketFrame是继承于WebSocketFrame,这里为什么不像之前socket通讯的时候写String类型呢,是因为WebSocket协议的相关规定,WebSocket可以传递的数据类型有6种,可以在WebSocketFrame子类中查看。
客户端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket-client</title>
</head>
<body>
<form onsubmit="return false;">
<textarea name="message" style="width: 300px;height: 150px"></textarea>
<input type="button" value="发送" onclick="sendMessage(this.form.message.value)"/>
<h3>服务端输出:</h3>
<textarea id="responseText" style="width: 300px;height: 150px"></textarea>
<input type="button" onclick="javascript: document.getElementById('responseText').value=''" value="clear"/>
</form>
</body>
<script type="text/javascript">
var socket;
if (window.WebSocket) {
//建立于服务端的连接
socket = new WebSocket("ws://127.0.0.1:8899/ws")
var serverTextArea = document.getElementById("responseText");
//收到服务端消息的时候的回调
socket.onmessage = function (event) {
serverTextArea.value = serverTextArea.value + "\n" + event.data;
}
//连接建立成功回调
socket.onopen = function (event) {
serverTextArea.value = "连接建立成功";
}
//连接断开
socket.onclose = function (event) {
serverTextArea.value = serverTextArea.value + "\n" + "连接断开";
}
} else {
alert("浏览器不支持WebSocket")
}
function sendMessage(message) {
if (!window.WebSocket) {
return;
} else {
if (socket.readyState == WebSocket.OPEN) {
socket.send(message);
} else {
alert("连接尚未建立");
}
}
}
</script>
</html>
启动服务端,如下输出:
八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0x1e87326c] REGISTERED
八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0x1e87326c] BIND: 0.0.0.0/0.0.0.0:8899
八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0x1e87326c, L:/0:0:0:0:0:0:0:0:8899] ACTIVE
启动客户端,在idea中对html文件右键,run!,idea会帮我们启动一个服务在浏览器中可访问:
客户端在浏览器初始化完成后会自动建立与服务端的 连接,服务端的handlerAdded触发:
八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0x1e87326c] REGISTERED
八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0x1e87326c] BIND: 0.0.0.0/0.0.0.0:8899
八月 30, 2020 2:51:54 下午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0x1e87326c, L:/0:0:0:0:0:0:0:0:8899] ACTIVE
八月 30, 2020 3:00:06 下午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0x1e87326c, L:/0:0:0:0:0:0:0:0:8899] READ: [id: 0xf219f5ab, L:/127.0.0.1:8899 - R:/127.0.0.1:52817]
八月 30, 2020 3:00:06 下午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0x1e87326c, L:/0:0:0:0:0:0:0:0:8899] READ COMPLETE
handlerAdded:9cb6d0fffedebcb5-00006ec0-00000001-0822684926b0801c-f219f5ab
尝试发送消息给服务端:
收到客户端消息:hello server
服务端收到客户端的消息并写信息到客户端。
关闭客户端页面服务端触发handlerRemove:
handlerRemoved:9cb6d0fffedebcb5-00006ec0-00000001-0822684926b0801c-f219f5ab
关闭服务端,客户端触发socket.onclose回调:
由于不是网络原因断开的连接,所以会触发客户端的回调,同理服务端也是可以收到客户端的断开回调。如果是因为网络原因造成的断开,就需要心跳机制了。
分析
Status Code:101 Switching Protocols 表示协议的切换,证明WebSocket建立服务端连接时候,需要http握手,建立成功后,切换协议到WebSocket。
Upgrade:websocket,表示协议升级
chrome network控制台中会实时监控服务的收发情况
需要代码的来这里拿嗷:demo项目地址