1.心跳检测机制
1.1 定时断线重连
客户端断线重连机制
客户端数量多,且需要传递的数据量级较大。可以在周期性的发送数据的时候,使用。要求对数据的即时性不高的时候,才可使用。
优点:可以使用数据缓存。不是每条数据进行一次数据交互。可以定时回收资源,对资源利用率高。相对来说,即时性可以通过其他方式保证。如: 120秒自动断线。数据变化1000次请求服务器一次。300秒中自动发送不足1000次的变化数据。
对于长连接的程序断网重连几乎是程序的标配。断网重连具体可以分为两类:
CONNECT失败,需要重连
实现ChannelFutureListener 用来启动时监测是否连接成功,不成功的话重试
// 实现ChannelFutureListener 用来启动时监测是否连接成功,不成功的话重试
//1.lambda 方式
private void doConnect() {
Bootstrap b = ...;
b.connect().addListener((ChannelFuture f) -> {
if (!f.isSuccess()) {
long nextRetryDelay = nextRetryDelay(...); // 延时多少重新发送连接请求
f.channel().eventLoop().schedule(nextRetryDelay, ..., () -> {
doConnect();
});
}
});
}
// 2.自定义类的方式
public class ConnectionListener implements ChannelFutureListener {
private Client client;
public ConnectionListener(Client client) {
this.client = client;
}
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (!channelFuture.isSuccess()) {
System.out.println("Reconnect");
//因为是建立网络连接所以可以共用EventLoop
final EventLoop loop = channelFuture.channel().eventLoop();
loop.schedule(new Runnable() {
@Override
public void run() {
client.createBootstrap(new Bootstrap(), loop);
}
}, 1L, TimeUnit.SECONDS);
}
}
}
程序运行过程中断网、远程强制关闭连接、收到错误包必须重连;
public class Client{
public Bootstrap createBootstrap(Bootstrap bootstrap, EventLoopGroup eventLoop) {
if (bootstrap != null) {
final MyInboundHandler handler = new MyInboundHandler(this);
bootstrap.group(eventLoop);
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(handler);
}
});
bootstrap.remoteAddress("localhost", 8888);
bootstrap.connect().addListener(new ConnectionListener(this));
}
return bootstrap;
}
}
public class MyInboundHandler extends SimpleChannelInboundHandler {
private Client client;
public MyInboundHandler(Client client) {
this.client = client;
}
// 和服务器断开连接时触发
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
final EventLoop eventLoop = ctx.channel().eventLoop();
eventLoop.schedule(new Runnable() {
@Override
public void run() {
client.createBootstrap(new Bootstrap(), eventLoop);
}
}, 1L, TimeUnit.SECONDS);
super.channelInactive(ctx);
}
}
1.2 心跳监测
使用定时发送消息的方式,实现硬件检测,达到心态检测的目的。
心跳监测是用于检测电脑硬件和软件信息的一种技术。如:CPU使用率,磁盘使用率,内存使用率,进程情况,线程情况等。
Netty提供的心跳检测机制分为三种:
- 读空闲,链路持续时间t没有读取到任何消息;
- 写空闲,链路持续时间t没有发送任何消息;
- 读写空闲,链路持续时间t没有接收或者发送任何消息;
心跳检测机制分为三个层面:
- TCP层面的心跳检测,即TCP的Keep-Alive机制,它的作用域是整个TCP协议栈;
- 协议层的心跳检测,主要存在于长连接协议中。例如SMPP协议;
- 应用层的心跳检测,它主要由各业务产品通过约定方式定时给对方发送心跳消息实现。
public class MyServer {
public static void main(String[] args) throws Exception {
//创建两个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//8个NioEventLoop
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//加入一个netty 提供 IdleStateHandler
// 说明
// 1. IdleStateHandler 是netty 提供的处理空闲状态的处理器
// 2. long readerIdleTime : 表示多长时间没有读, 就会发送一个心跳检测包检测是否连接
// 3. long writerIdleTime : 表示多长时间没有写, 就会发送一个心跳检测包检测是否连接
// 4. long allIdleTime : 表示多长时间没有读写, 就会发送一个心跳检测包检测是否连接
//
// 5. 文档说明
// triggers an {@link IdleStateEvent} when a {@link Channel} has not performed
// * read, write, or both operation for a while.
// * 6. 当 IdleStateEvent 触发后 , 就会传递给管道 的下一个handler去处理
// * 通过调用(触发)下一个handler 的 userEventTiggered , 在该方法中去处理 IdleStateEvent(读空闲,写空闲,读写空闲)
pipeline.addLast(new IdleStateHandler(7000, 7000, 10, TimeUnit.SECONDS));
//加入一个对空闲检测进一步处理的handler(自定义)
pipeline.addLast(new MyServerHandler());
}
});
//启动服务器
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class MyServerHandler extends ChannelInboundHandlerAdapter {
/**
* @param ctx 上下文
* @param evt 事件
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
//将 evt 向下转型 IdleStateEvent
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()) {
case READER_IDLE:
eventType = "读空闲";
break;
case WRITER_IDLE:
eventType = "写空闲";
break;
case ALL_IDLE:
eventType = "读写空闲";
break;
default:
throw new IllegalStateException("Unexpected value: " + event.state());
}
System.out.println(ctx.channel().remoteAddress() + "--超时时间--" + eventType);
System.out.println("服务器做相应处理..");
//如果发生空闲,我们关闭通道
// ctx.channel().close();
}
}
}
2.Netty 通过WebSocket 编程实现服务器和客户端长连接
2.1 要求
要求:
- 实现计语 webSocket 的长连接的全双工的交互
- 改变 Http 协议多次请求的约束,实现长连接,服务器可以主动发送消息给浏览器
- 客户端浏览器和服务端会相互感知,比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知
2.2 代码
public class MyServer {
public static void main(String[] args) throws Exception {
//创建两个线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//8个NioEventLoop
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//因为基于http协议,使用http的编码和解码器
pipeline.addLast(new HttpServerCodec());
//是以块方式写,添加ChunkedWriteHandler处理器
pipeline.addLast(new ChunkedWriteHandler());
/*
说明
1. http数据在传输过程中是分段, HttpObjectAggregator ,就是可以将多个段聚合
2. 这就是为什么,当浏览器发送大量数据时,就会发出多次http请求
*/
pipeline.addLast(new HttpObjectAggregator(8192));
/*
说明
1. 对应websocket ,它的数据是以 帧(frame) 形式传递
2. 可以看到WebSocketFrame 下面有六个子类
3. 浏览器请求时 ws://localhost:7000/hello 表示请求的uri
4. WebSocketServerProtocolHandler 核心功能是将 http协议升级为 ws协议 , 保持长连接
5. 是通过一个 状态码 101
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/hello2"));
//自定义的handler ,处理业务逻辑
pipeline.addLast(new MyTextWebSocketFrameHandler());
}
});
//启动服务器
ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
/**
* 这里 TextWebSocketFrame 类型,表示一个文本帧(frame)
*
* @author Administrator
*/
public class MyTextWebSocketFrameHandler 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() + " " + msg.text()));
}
/**
* 当web客户端连接后, 触发方法
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
//id 表示唯一的值,LongText 是唯一的 ShortText 不是唯一
System.out.println("handlerAdded 被调用" + ctx.channel().id().asLongText());
System.out.println("handlerAdded 被调用" + ctx.channel().id().asShortText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved 被调用" + ctx.channel().id().asLongText());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常发生 " + cause.getMessage());
//关闭连接
ctx.close();
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
var socket;
//判断当前浏览器是否支持websocket
if(window.WebSocket) {
//go on
socket = new WebSocket("ws://localhost:7000/hello2");
//相当于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>
<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>
</body>
</html>