Netty网络框架学习笔记-8(WebSocket 编程实现服务器和客户端长连接)
实现的目标
实现基于 webSocket 的长连接的全双工的交互
改变 Http 协议多次请求的约束,实现长连接了, 服务器可以发送消息给浏览器
客户端浏览器和服务器端会相互感知,比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知
1.0 服务端
@Slf4j
public class WebSocketServer {
public static void main(String[] args) {
NioEventLoopGroup boosGroup = new NioEventLoopGroup(2);
// cpu核心数*2=线程数(EventLoop)
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boosGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO)) // boosGroup服务器处理程序, 日志
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 因为WebSocket 是独立的、创建在TCP 上的协议。
// Websocket 通过HTTP/1.1 协议的101状态码进行握手。 (HTTP 协议解析,用于握手阶段)
pipeline.addLast(new HttpServerCodec());
// 分块写入, 支持异步写大型数据流,而又不会导致大量的内存消耗
pipeline.addLast(new ChunkedWriteHandler());
//将多段信息聚合起来, 最大支持1024*1024
pipeline.addLast(new HttpObjectAggregator(1024 * 1024));
// WebSocket 数据压缩 ,压缩来自 client
pipeline.addLast(new WebSocketServerCompressionHandler());
// 对应 websocket ,它的数据是以 帧(frame) 形式传递,
// 可以看到 WebSocketFrame 下面有六个子类,
// 核心功能是将 http 协议升级为 ws 协议 , 保持长连接
pipeline.addLast(new WebSocketServerProtocolHandler("/build", null, true));
// 添加自定义处理器
pipeline.addLast(new MyWebSocketServerMessageHandler());
}
});
try {
ChannelFuture channelFuture = serverBootstrap.bind(new InetSocketAddress("localhost", 8888)).sync();
channelFuture.addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
log.info("WebSocketServer-启动成功, 等待客户端连接中!");
}
});
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("WebSocketServer-发生异常! 异常信息:{}", e);
} finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
1.0.1 自定义处理器
// 这里 TextWebSocketFrame 类型,表示一个文本帧(frame)
@Slf4j
public class MyWebSocketServerMessageHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext,
TextWebSocketFrame textWebSocketFrame) throws Exception {
String text = textWebSocketFrame.text();
log.info("读取到的信息:{}",text);
String result = null;
if ("你好".equals(text)){
result = "你好";
}
if ("在干吗".equals(text)){
result = "学习";
}
TextWebSocketFrame webSocketFrame = new TextWebSocketFrame(result);
// 写回数据也是按照协议格式写TextWebSocketFrame
channelHandlerContext.writeAndFlush(webSocketFrame);
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
log.info("MyWebSocketServerMessageHandler-客户端连接成功:{}",ctx.channel().remoteAddress().toString());
super.handlerAdded(ctx);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
log.info("MyWebSocketServerMessageHandler-客户端断开连接:{}",ctx.channel().remoteAddress().toString());
super.handlerRemoved(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.channel().close();
}
}
进行测试
写完客户端后, 可以自己写个页面进行使用
WebSocket
协议进行建立连接发送信息。或者百度搜索在线
WebSocket
进行测试
结果:
19:17:41.580 [nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x066e99a6, L:/127.0.0.1:8888] ACTIVE
19:17:43.463 [nioEventLoopGroup-2-1] INFO com.zhihao.netty.webSocket.WebSocketServer - WebSocketServer-启动成功, 等待客户端连接中!
19:17:50.794 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.webSocket.MyWebSocketServerMessageHandler - MyWebSocketServerMessageHandler-客户端连接成功:/127.0.0.1:49219
19:19:55.845 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.webSocket.MyWebSocketServerMessageHandler - 读取到的信息:你好
19:20:00.266 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.webSocket.MyWebSocketServerMessageHandler - 读取到的信息:在干吗
19:24:15.278 [nioEventLoopGroup-3-1] INFO com.zhihao.netty.webSocket.MyWebSocketServerMessageHandler - MyWebSocketServerMessageHandler-客户端断开连接:/127.0.0.1:49219
扩展:
WebSocket
协议介绍
WebSocket 协议是一种在单个 TCP 连接上进行全双工通信的协议,在建立连接完成握手阶段后,服务端也可以主动推送数据给客户端,使得 Web 浏览器和服务器之间的交互性更强大。
目前 WebSocket 协议应用非常广泛,大部分浏览器均已支持 WebSocket,不仅仅在 Web 应用中,其他很多类型应用(例如游戏)也经常用到 WebSocket 协议。
WebSocket
建立连接的过程
WebSocket 分为握手阶段( handshake )和数据传输阶段( data transfer )。
握手阶段( handshake )
在客户端和服务器建立 WebSocket 连接之前,客户端首先要发送一个 HTTP 协议的握手请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
其中请求头
Connection: Upgrade
和Upgrade: websocket
表示客户端想要升级协议为 WebSocket。服务器进行如下响应完成握手:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
完成握手后,接下来就是双向的数据传输的过程。
数据传输阶段( data transfer )=> (对应 WebSocketFrame
)
数据传输阶段传输的内容以帧( frame )为单位,其中分为控制帧(Control Frame)和数据帧(Data Frame):
- 控制帧(Control Frame):包括
Close
、Ping
、Pong
帧,Close
用于关闭 WebSocket 连接,Ping
和Pong
用于心跳检测 - 数据帧(Data Frame):包括
Text
和Binary
帧,分别用于传输文本和二进制数据
自定义网页:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试WebSocket</title>
</head>
<body>
<form onsubmit="return false">
<textarea name="message" style="height: 300px; width: 300px"></textarea>
<input type="button" value="发生消息" onclick="send(this.form.message.value)">
<textarea id="responseText" style="height: 300px; width: 300px"></textarea>
<input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
</form>
<script>
//判断当前浏览器是否支持
var socket;
if (window.WebSocket) {
// 使用WebSocket连接, 路径定义WebSocketServerProtocolHandler /build
socket = new WebSocket("ws://localhost:8888/build");
//相当于 channelReado, ev 收到服务器端回送的消息
socket.onmessage = function(ev) {
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + ev.data;
}
//相当于连接开启(感知到连接开启)
socket.onopen = function(ev) {
var rt = document.getElementById("responseText");
rt.value = "连接开启了.."
}
//相当于连接关闭(感知到连接关闭)
socket.onclose = function(ev) {
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + "连接关闭了.."
}
} else {
alert("当前浏览器不支持 websocket")
}
//发送消息到服务器
function send(message) {
if (!window.socket) {
//先判断 socket 是否创建好
return;
}
if (socket.readyState == WebSocket.OPEN) {
//通过 socket 发送消息
socket.send(message)
} else {
alert("连接没有开启");
}
}
</script>
</body>
</html>
1