目录
3、聊天室案例
视频地址:https://www.bilibili.com/video/BV1py4y1E7oA?p=108&vd_source=3db50b368637ed14b6374cc45e14b74f
空闲监测
连接假死
原因
- 网络设备出现故障,例如网卡,机房等,底层的 TCP 连接已经断开了,但应用程序没有感知到,仍然占用着资源。
- 公网网络不稳定,出现丢包。如果连续出现丢包,这时现象就是客户端数据发不出去,服务端也一直收不到数据,就这么一直耗着
- 应用程序线程阻塞,无法进行数据读写
问题
- 假死的连接占用的资源不能自动释放
- 向假死的连接发送数据,得到的反馈是发送超时
服务器端解决
- 怎么判断客户端连接是否假死呢?如果能收到客户端数据,说明没有假死。因此策略就可以定为,每隔一段时间就检查这段时间内是否接收到客户端数据,没有就可以判定为连接假死
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 5s 内如果没有收到 channel 的数据,会触发一个 IdleState#READER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(5, 0, 0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
// 用来触发特殊事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{
IdleStateEvent event = (IdleStateEvent) evt;
// 触发了读空闲事件
if (event.state() == IdleState.READER_IDLE) {
log.debug("已经 5s 没有读到数据了");
ctx.channel().close();
}
}
});
客户端定时心跳
- 客户端可以定时向服务器端发送数据,只要这个时间间隔小于服务器定义的空闲检测的时间间隔,那么就能防止前面提到的误判,客户端可以定义如下心跳处理器
// 用来判断是不是 读空闲时间过长,或 写空闲时间过长
// 3s 内如果没有向服务器写数据,会触发一个 IdleState#WRITER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(0, 3, 0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
// 用来触发特殊事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{
IdleStateEvent event = (IdleStateEvent) evt;
// 触发了写空闲事件
if (event.state() == IdleState.WRITER_IDLE) {
// log.debug("3s 没有写数据了,发送一个心跳包");
ctx.writeAndFlush(new PingMessage());
}
}
});
4、扩展与源码
4.1、扩展
4.1.1、扩展序列化算法
/**
* 用于扩展序列化、反序列化算法
*/
public interface Serializer {
/** 反序列化方法 */
<T> T deserialize(Class<T> clazz, byte[] bytes);
/** 序列化方法 */
<T> byte[] serialize(T object);
/** 序列化枚举类实现 */
enum Algorithm implements Serializer {
/** Java 序列化 */
Java {
@Override
public <T> T deserialize(Class<T> clazz, byte[] bytes) {
try {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
return (T) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("反序列化失败", e);
}
}
@Override
public <T> byte[] serialize(T object) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(object);
return bos.toByteArray();
} catch (IOException e) {
throw new RuntimeException("序列化失败", e);
}
}
},
/** Gson 序列化 */
Json {
@Override
public <T> T deserialize(Class<T> clazz, byte[] bytes) {
Gson gson = new GsonBuilder().registerTypeAdapter(Class.class, new ClassCodec()).create();
String json = new String(bytes, StandardCharsets.UTF_8);
return gson.fromJson(json, clazz);
}
@Override
public <T> byte[] serialize(T object) {
Gson gson = new GsonBuilder().registerTypeAdapter(Class.class, new ClassCodec()).create();
String json = gson.toJson(object);
return json.getBytes(StandardCharsets.UTF_8);
}
}
}
// 处理 Gson().toJson(String.class) 序列化失败问题
class ClassCodec implements JsonSerializer<Class<?>>, JsonDeserializer<Class<?>> {
@Override
public Class<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
try {
String str = json.getAsString();
return Class.forName(str);
} catch (ClassNotFoundException e) {
throw new JsonParseException(e);
}
}
@Override // String.class
public JsonElement serialize(Class<?> src, Type typeOfSrc, JsonSerializationContext context) {
// class -> json
return new JsonPrimitive(src.getName());
}
}
}
Gson 需要的依赖
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
4.2、参数调优
1)CONNECT_TIMEOUT_MILLIS
- 基于 SocketChannel 参数
- 用在客户端建立连接时,如果在指定毫秒内无法连接,会抛出 TimeOut 异常
SO_TIMEOUT
主要用在阻塞 IO,阻塞 IO 中 accept、read 等都是无限等待的,如果不希望永远阻塞,使用它调整超时时间
@Slf4j
public class TimeOutClient {
public static void main(String[] args) {
// 1、客户端
// new Bootstrap().option(); 给 SocketChannel 配置参数
// 2、服务端
// new ServerBootstrap().option(); 给 ServerSocketChannel 配置参数
// new ServerBootstrap().childOption(); 给 SocketChannel 配置参数
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(worker)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 500)
.channel(NioSocketChannel.class)
.handler(new LoggingHandler());
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888);
channelFuture.sync().channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
log.error("time out");
} finally {
worker.shutdownGracefully();
}
}
}
抛出的异常:
io.netty.channel.ConnectTimeoutException: connection timed out: /127.0.0.1:8888
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe$1.run(AbstractNioChannel.java:265)
at io.netty.util.concurrent.PromiseTask$RunnableAdapter.call(PromiseTask.java:38)
at io.netty.util.concurrent.ScheduledFutureTask.run(ScheduledFutureTask.java:120)
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:400)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:401)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:805)
at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:145)
at java.lang.Thread.run(Thread.java:745)
AbstractNioChannel.java:265
源码:
// Schedule connect timeout. 了解线程之间的通信方式
int connectTimeoutMillis = config().getConnectTimeoutMillis(); // 设置的超时时间参数
if (connectTimeoutMillis > 0) {
connectTimeoutFuture = eventLoop().schedule(new Runnable() { // 定时任务
@Override
public void run() {
ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise; // Promise 和主线程是同一个对象
ConnectTimeoutException cause =
new ConnectTimeoutException("connection timed out: " + remoteAddress);
if (connectPromise != null && connectPromise.tryFailure(cause)) { // 抛出异常
close(voidPromise());
}
}
}, connectTimeoutMillis, TimeUnit.MILLISECONDS);
}
2)SO_BACKLOG
- 属于 ServerSocketChannel 参数
1、第一次握手,client 发送 SYN 到 server ,状态修改为 SYN_SEND ,server 收到,状态改变为 SYN_REVD,并将该请求放入 sync queue 队列
2、第二次握手,server 回复 SYN + ACK 给 client,client 收到,状态改变为 ESTABLISHED ,并且发送 ACK 给 server
3、第三次握手,server 收到 ACK,状态改变为 ESTABLISHED ,该请求从 sync queue 队列 放入 accept queue 队列
其中
- 在 Linux 2.2 之前,backlog 大小包括了两个队列的大小,在 Linux 2.2 之后分别用以下两个参数控制
- sync queue - 半连接队列
- 大小通过
/proc/sys/net/ipv4/tcp_max_syn_backlog
指定,在syncookies
启用的情况下,逻辑上没有最大限制,这个设置便被忽略
- 大小通过
- accept queue - 全连接队列
- 其大小通过
/proc/sys/net/core/somaxconn
指定,在使用 listen 函数时,内核会根据传入的 backlog 参数与系统参数,取两者的较小者 - 如果 accept queue 队列满了,server 将发送一个拒绝连接的错误信息带 client
- 其大小通过
Netty 中 可以通过 option(ChannelOption.SO_BACKLOG, 值)
来设置大小(NIO 通过 bind(端口,backlog)
)
3)ulimit -n
- 属于操作系统参数 (文件句柄数限制)
4)TCP_NODELAY
- 属于 SocketChannel 参数 (默认是开启 false 开启了 nagle 算法,数据包攒到一定量再发送。建议设置为 true)
5)SO_SNDBUF & SO_RCVBUF
- SO_SNDBUF 属于 SocketChannel 参数
- SO_RCVBUF 即可用于 SocketChannel 参数,也可以用于 ServerSocketChannel 参数 (建议设置到 ServerSocketChannel 上)
6)ALLOCATOR
-
属于 SocketChannel 参数
-
用来分配 ByteBuf,
ctx.alloc()
7)RCVBUF_ALLOCATOR
- 属于 SocketChannel 参数
- 控制 Netty 接收缓冲区的大小
- 负责入站数据的分配,决定入站缓冲区的大小(并可动态调整),统一采用 direct 直接内存,具体池化还是非池化由 allocator 决定
4.3、Netty 实现 RPC 框架
远程方法调用
地址:
https://www.bilibili.com/video/BV1py4y1E7oA?p=130&vd_source=3db50b368637ed14b6374cc45e14b74f
5、 源码分析
地址:
https://www.bilibili.com/video/BV1py4y1E7oA?p=139&vd_source=3db50b368637ed14b6374cc45e14b74f
2.1 启动剖析
我们就来看看 netty 中对下面的代码是怎样进行处理的
// 1 netty 中使用 NioEventLoopGroup (简称 nio boss 线程)来封装线程和 selector
Selector selector = Selector.open();
// 2 创建 NioServerSocketChannel,同时会初始化它关联的 handler,以及为原生 ServerSocketChannel 存储 config
NioServerSocketChannel attachment = new NioServerSocketChannel();
// 3 创建 NioServerSocketChannel 时,创建了 java 原生的 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 4 启动 nio boss 线程执行接下来的操作
// 5 注册(仅关联 selector 和 NioServerSocketChannel),未关注事件
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, attachment);
// 6 head -> 初始化器 -> ServerBootstrapAcceptor -> tail,初始化器是一次性的,只为添加 acceptor
// 7 绑定端口
serverSocketChannel.bind(new InetSocketAddress(8080));
// 8 触发 channel active 事件,在 head 中关注 op_accept 事件
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
主线:
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, attachment);
serverSocketChannel.bind(new InetSocketAddress(8080));
selectionKey.interestOps(SelectionKey.OP_ACCEPT);