在2020-03-15日,终于把Netty学完了. 学的好扎心. 中间好多概念不懂,只能硬着头皮看. 之后看了一遍又一遍,熟悉了,再加上写写代码.
也忘不掉了.在这边笔记中,将自己学习的再过一遍.看看能记住多少.记不住的在回头复习即可
Buffer
是Java Nio 的组件之一. 和channel相关联. 每个channel可以有多个或一个Buffer.
Nio的非阻塞 就是通过Buffer实现的
PS: 这话是视频中的韩老师说的,具体的实现个人猜测是通过判断缓冲区是否填满进而实现非阻塞的.
常用的有 capacity,position,mark,limit几个参数. 可以实现读写.
子类
除了String没有对应的Buffer之外,其他的char,double,int,short,long,float都有对应的 Buffer类型
最常用的还是ByteBuffer.
堆外MappedByteBuffer
将部分数据映射到堆外内存,就不用在 JVM 的堆中分配内存了.
RandomAccessFile randomAccessFile = new RandomAccessFile("file01.txt","rw");
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数一 读写模式
* 参数二 可以直接修改的起始位置
* 参数三 映射到内存的大小
* 从文件的第几个字节开始映射到内存,映射多少个.这里是5个. 也只能修改 5 个字节
* 从0开始,一共5个. 0,1,2,3,4 是内存大小,而不是索引位置5
*/
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
// 修改
// TODO 这里修改成功了,但是 需要在硬盘位置查看.这里的是副本
map.put(0,(byte)'H');
map.put(3,(byte)'G');
randomAccessFile.close();
System.out.println("修改成功");
只读Buffer
可以将一个Buffer 转为 asReadOnlyBuffer.
分散聚合
可以有一个Buffer数组.会自动将数据读入到Buffer或写出Buffer中的数据
Selector
有几个不常用的方法记录一下.
wakeup() 唤醒selector selectedKeys(): 有事件发生的通道的key集合
interestOps(): 修改 关注的事件类型
key.interestOps(SelectorKey.OP-WRITE)
attachment(): 得到与之相关联的数据
Channel
有很多子类. 对应不同的情况.比如: DatagramChannel: 用于UDP数据读写. 当然,在学习过程中
还有 transferFrom 和 transferTo,用来实现直接复制文件.不用自己使用Buffer数组,循环读写啊,flip() 切换读模式啊等等
非常方便
Netty IO模型
Netty 是 自己修改的Reactor模型. 编程的标准. IO模型从一开始的 一个线程一个连接 到 一个线程多个连接.
在到 Reactor模型. 都是在慢慢细分的.毕竟以前的公司,根本没那么多的用户.一台电脑 实现几千连接,就是很不错的了
Netty 是 主从Reactor多线程模型. 分为俩部分.
NioEventLoopGroup
Netty 主要是 NioEventLoopGroup. 是一个线程组. 其中的每个线程: NioEventLoop 内部都含有一个selector. 可以接受client 注册到上面.
每个可以通过连接获取对应的channel. 实现接收消息,发送消息等功能
Handler
Netty 的编程相比 Nio 简化了很多. 可以分为三部分来记忆
1. 创建 NioEventLoopGroup
2. 配置Serverbootstrap 对象. 配置 服务器的一些参数
3. 为服务器配置 Handler.用于各种业务的处理
这里的主要配置 childHandler() 实现读写业务处理
ChannelInitializer
ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter 是 Inbound 一脉的类.
childHandler 用来设置worker 线程的业务Handler.
Inbound 代表的是入栈: 数据从client 到 server的方向.
ChannelInboundHandlerAdapter
用户自定义handler 需要 extends ChannelInboundHandlerAdapter. 之后可以重写一些方法.
exceptionCaught(ChannelHandlerContext ctx, Throwable cause): 当发生异常时自动调用该方法
channelReadComplete(ChannelHandlerContext ctx): 数据读取完毕时调用该方法
channelRead(ChannelHandlerContext ctx, Object msg): 当有数据可读时,调用的方法
Future-Listener
Netty 可以是异步线程. 对一些方法可以实现异步.使用Future-Listener机制.编写回调函数. 状态到了自动调用
例如: bind,connect,write等操作都是异步的. 我们可以添加监听器
channelFuture = serverBootStrap.bind(6668).sync();
// 判断监听是否成功
channelFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()) {
System.out.println(" 监听端口 6668 成功 ");
}else{
System.out.println(" 监听端口 6668 失败 ");
}
}
});
// 对关闭的通道进行监听
channelFuture.channel().closeFuture().sync();
绑定端口方法返回的是一个ChannelFuture. 可以通过 addListener添加 ChannelFutureListener实现回调.
只需要编写自己的方法即可:
isDone():
isSuccess(): 判断已完成的操作是否成功
getCause(): 已完成操作失败原因
isCancelled(): 判断已完成的操作是否被取消
编解码器
编解码器也是Handler的一种. Netty自己提供了几种编解码器.
比如: HttpServerCodec. 就是一个编解码器. 无需自己实现任何方法.Netty会自己调用
消息类型
一般来说,client 什么消息都可能发送. 那么我们可以指定用户发的消息类型.
public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {}
我们可以在<> 内写入消息类型,那么当我们实现channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject o)方法时
这里的第二个参数就是你所编写的类型.后面我们可以配合Protobuf 使用消息的传递
工具类
Netty的一些工具类. 比如
Unpooled: 专门读写缓冲区的工具类. 可以使用它发送数据.进行编码等
DefaultFullHttpResponse: 用来响应信息给客户端. 可以设置一些参数
比如头信息,返回信息类型,编码方式.状态码等等
DefaultFullHttpResponse defaultFullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,content);
defaultFullHttpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain;charset=UTF-8");
defaultFullHttpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes());
ByteBuf: Netty 对 ByteBuffer 的包装类. 简化了 Buffer 的使用程度. 内部维护了 readIndex和writerIndex.
用来读写数据.
0-readIndex 是已读区域
readIndex - writeIndex 是可读区域
writeIndex - capacity 是可写区域
hasArray(): 底层是否有一个数组
arayoffset(): 偏移量
readerIndex(): 可读的开始位置
writerIndex(): 可写的开始位置
readableBytes(): 可读的字节数
getCharSequence(): 读取其中的一段字节
私聊实现
private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
全局事件执行器.是单例的. 有一些操作是自动执行的.比如: 当有链接断开时,会自动将所对应的channel移除
心跳机制
在网络编程中,服务端无法主动感知client是否断开了连接. 所以会有心跳包.
IdleStateHandler
// 构造参数:
new IdleStateHandler(3,5,7, TimeUnit.SECONDS)
IdleStateHandler(3,5,7,TimeUtil.SECONDS);
readerIdleTime: 多长时间没读,会发送一个心跳检测包检测是否连接
writerIdleTime: 多长时间没写,会发送一个心跳检测包检测是否连接
allIdleTime: 多长时间没读或写,会发送一个心跳检测包检测是否连接
只需要在自定义handler 中实现 userEventTriggered(ChannelHandlerContext ctx, Object evt) 方法即可
将 evt 强转为 IdleStateHandler 类型. 可以根据 evt.state的状态自定感知,是什么状态.进而实现处理逻辑
日志整合
1. 添加依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.26</version>
<scope>test</scope>
</dependency>
2. 编写配置文件:
log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.Patternlayout
log4j.appender.stdout.layout.Conversionpattern=[%p%] [%C]{1} - %m%n
3. 启动服务,可以看到DEBUG 信息
同步任务队列
当有一些耗时操作时,可以将任务提交到任务队列中,
有2种:
1. handler中添加到任务队列
2. 自定义普通任务
ctx.channel().eventLoop(new Runnable(){
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copieBuffer("你好啊客户端",CharsetUtil.UTF-8));
})
问: 如果有多个自定义任务,之后sleep(10 * 2000) . 那么这俩个任务执行间隔是多少
答: 第一个任务执行10s之后,会sleep 20s. 在TaskQueue中,是在一个线程中. 所以停顿 是在一个线程汇总计时的.
3. 自定义定时任务
ctx.channel().eventLoop().schedule(new Runnable(){
// code
},5,TimeUnit.SECONDS)
// TimeUnit: 定义定时任务schedule需要对第二个参数进行时间单位的说明
这俩种方式是同步模式,添加到任务队列的任务和执行handler的线程是同一个
异步任务队列
1 . Handler添加到异步任务队列
在 handler中定义 static final EventExecutorGroup group = new DefaultEventExecutorLoop(16); 线程池
提交任务到 线程池中:
group.submit(new Callable<Object>(){
@Override
public Object call(){
// 可以在这 处理业务
// 处理服务端响应消息或发送消息到服务端
ctx.writeAndFlush(xxx);
}
});
2 . Context中添加异步任务
这里的context指的是 在ServerBootstarp 配置类中使用
在 服务器配置类中添加 static final EventExcecutorGroup group = new DefaultEventExecutorGroup(2);
在 ChannelInitializer 中 添加Handler的时候, 给该Handler 指定该group.
此后,该handler的所有任务都会在group的线程池中执行
websocket
Netty 实现 websocket 通信
添加 ChunkedWriterHandler(): 数据块处理器. 因为websocket 是基于 frame发送数据的.
pipeline.addLast(new ChunkedWriteHandler());
添加 段处理器.因为 Http请求时分段的.需要将段聚合:
pipeline.addLast(new HttpObjectAggregator(8192));
将 Http连接升级成为 websocket 连接.并且 括号内的 "/hello" 就是 ws 的请求路径
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
添加自己的Handler业务处理器
pipeline.addLast(new MyServerHandler());
handler 添加的逻辑是因为: 首先,是Http连接,是以块的方式发送的. 所以需要 ChunkWriteHandler处理器
而且需要将块聚集起来,所以添加了 HttpObjectAggregator.
之后就可以升级成为 WebSocketServerProtocolHandler 处理器了
页面
<script type="text/javascript">
var socket;
// 判断当前websocket是否支持为websocket编程
if(window.websocket){
socket = new WebSocket("ws://localhost:7000/hello");
// 如果有数据来临,调用此方法
socket.onmessage = function(ev){
var rt = document.getElementById("responseText");
rt.value = ev + "" + 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 + " " + "连接关闭";
}
}else{
alert("当前浏览器不支持websocket");
}
function send(message){
if(!window.socket){
return ;
}
if(socket.readyState == WebSocket.OPEN){
socket.send(message);
}else{
alert("连接没有开启");
}
}
</script>
<form onsubmit="return false">
<textarea name="mes" style="height: 300px; width: 300px"></textarea>
<input type="button" value="发消息" onclick="send(this.form.mes.value)">
<textarea id="responseText" style="height: 300px; width: 300px"></textarea>
<input type="button" value="清空内容" onclick="document.getElementById('responseText').value = '' ">
</form>
Protobuf
因为 Java 本身序列化性能效率不高.所以采用 Protobuf 的方式 发送数据. 服务端接收进行解码
使用
编写 .proto 文件.
引入依赖
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.6.1</version>
</dependency>
生成 Java 文件. 使用 proto 文件生成的对象生成真正的需要的对象
添加 Protobuf 解码器:
pipeline.addLast("proEncoder",new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance()));
添加编码器:
pipeline.addLast("Encoder",new ProtobufEncoder());
生成对象:
myMessage = MyDataInfo.MyMessage.newBuilder()
.setDataType
(
MyDataInfo.MyMessage.DataType.studentType
)
.setStudent
(
MyDataInfo.Student.newBuilder().setId(5).setName("玉麒麟 卢俊义").build()
)
.build();
编写 handler 接收对应的数据.处理业务
粘包 和 拆包
TCP是面向连接,面向流的.使用了 Nagle 算法,将多次间隔较小且数据量小的数据,合成一个大的数据块
然后进行封包.
但是接收端不能分辨出那一部分是一块,完整的数据包
因为面向流的通信是无消息保护边界的
所以会出现一些问题
client发送了 D1 和 D2. 存在以下四种情况
1. 服务端分俩次读取,一次是D1,一次是D2
2. 服务端一次接受了 2个数据包.D1和D2粘合在一起,称为 粘包
3. 服务端读取了2次. 第一次读取D1完毕,加上D2的部分内容. 称为 拆包
4. 服务端读取了2次. 第一次读取了D1 部分内容和D2,第二次读取了D1 的剩下的内容
现象:
//client
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 使用客户端发送 10 条数据
for (int i = 0; i < 10; i++) {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,server ==>" + i, CharsetUtil.UTF_8));
}
}
//server
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
// 转为字符串
String str = new String(bytes, Charset.forName("utf-8"));
System.out.println("客户端发送[ 服务端接收 ]的数据为" + str);
System.out.println("服务端接受到了消息量=" + (++count));
// 回复客户端一个随机ID
ByteBuf byteBufStr = Unpooled.copiedBuffer(UUID.randomUUID().toString(), Charset.forName("utf-8"));
channelHandlerContext.writeAndFlush(byteBufStr);
最终,server可能接收到4次数据.前三次每次3条 "helloserver ==>".最后一次接收了一条
所以我们使用 自定义协议 发送数据
import lombok.*;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class MessageProtocol {
private int len;
private byte[] content;
}
len规定了数据的长度,content 存储了实际的数据.
编写 编解码器
// 解码器:
public class MyServerMessageDecoder extends ReplayingDecoder<MessageProtocol> {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
System.out.println("MessageDecoder 方法被调用");
// 将 二进制字节码转为 messageProtocol 对象
int lenght = byteBuf.readInt();
byte[] bytes = new byte[lenght];
byteBuf.readBytes(bytes);
// 封装成为 message 对象,放入 list 中,传递给下一个 handler 处理
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(lenght);
messageProtocol.setContent(bytes);
list.add(messageProtocol);
}
}
// 编码器
public class MyClientMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MessageProtocol messageProtocol, ByteBuf byteBuf) throws Exception {
System.out.println("messageEncoder 的方法被调用");
//
byteBuf.writeInt(messageProtocol.getLen());
byteBuf.writeBytes(messageProtocol.getContent());
}
}
ByteToMessageDecoder 是最下层的解码器.会对入栈数据进行缓存,知道准备好被处理
MessageToByteEncoder 此类是 OutBound一脉的类.
而 ReplayingDecoder extends <S> extends ByteToMessageDecoder. 我们 可以直接读取字节成为Int 或 Long 类型.
之后 对数据进行处理
总结
我们可以使用 ByteToMessageDecoder 和 MessageToByteEncoder 进行处理. 而ReplayingDecoder 是
ByteToMessageDecoder 的子类.扩展了一些方法. 我们只需要直接使用即可.
其他编码器
其他的编解码器:
1.
ReplayingDecoder<S> extends ByteToMessageDecoder
该类扩展了 ByteToMessageDecoder 类. 我们不需要调用readableBytes() 方法.
其 S 泛型 制定了用户状态管理的类型. 其中Void 代表不需要状态管理
不支持ByteBuf 的所有操作.如果调用了一个不被支持的方法,会抛出 UnsupportedOperationException
ReplayingDecoder 速度慢于 ByteToMessageDecoder. 而在一些情况下,比如网络缓慢并且消息格式复杂,
那么,消息会被拆成多个碎片,速度变慢
案例: MyServerLongToByteDecoder2
2.
LineBaseFrameDecoder: 在netty 内部,使用 行尾控制符 (/r/n) 作为分隔符解析数据
DelimiterBaseFrameDecoder: 使用自定义的特殊字符作为消息的分隔符
HttpObjectDecoder: 一个HTTP 数据的解码器
LengthFieldBasedFrameDecoder: 通过指定长度来标识整包消息,这样可以自动处理 粘包
写在最后
自学过程是痛苦的. 因为可能你遇到一个问题就会半个小时都解决不掉.
又或者,你有什么疑问都没人去问.
看了视频,一些疑问没有解决,没有理解到位,这个感觉如鲠在喉. 会导致你接下来的学习受到影响.
而这个Netty 看到最后是有些糊涂的. 或者说,虽然netty 大大的简化了 网络编程.
就比如: websocket这块. 比如: Protobuf 这块. 还比如: Protocol 这块.
还有一些handler 的应用.讲解.而不是只知道这么写
估计接下来还是需要看好几遍,然后才能够明白,熟记.
本文深入探讨Netty网络编程框架,涵盖Buffer机制、非阻塞I/O、编解码器、粘包拆包处理、WebSocket及Protobuf应用。Netty简化了网络编程,提供高效数据处理解决方案。
3084

被折叠的 条评论
为什么被折叠?



