https://netty.io/
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.
Netty是一个异步事件驱动的网络应用框架。
用于快速开发可维护的高性能协议服务器和客户端。
Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.
‘Quick and easy’ doesn’t mean that a resulting application will suffer from a maintainability or a performance issue. Netty has been designed carefully with the experiences earned from the implementation of a lot of protocols such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, Netty has succeeded to find a way to achieve ease of development, performance, stability, and flexibility without a compromise.
Netty是一个NIO客户服务器框架,它能够快速和容易地开发网络应用,如协议服务器和客户端。它大大简化和精简了网络编程,如TCP和UDP套接字服务器。
快速和简单 "并不意味着开发出来的应用程序会出现可维护性或性能问题。Netty的设计是经过精心设计的,其经验来自于许多协议的实施,如FTP、SMTP、HTTP以及各种基于二进制和文本的遗留协议。因此,Netty成功地找到了一种方法来实现开发的简易性、性能、稳定性和灵活性,而没有任何妥协。
怎么理解同步/异步呢?对于Netty来讲,Boos线程接收请求,Wroker线程处理请求,Worker线程处理比较复杂的请求时,会影响处理其他的请求,Netty是这样做的,当接收到客户端请求之后,Worker会马上给与响应,但是事情并没有做完,Worker会开辟其他线程处理这件事,自己又可以处理新的请求,其他线程处理完请求之后,会将处理结果返回给客户端 。思考一下:子线程完成任务后,如何将结果返回给客户端?通过回调返回,类似于AJAX。
1. Netty是完成网络通信的框架,底层封装了NIO
2. NIO存在很多问题
API 复杂难用,尤其是 Buffer 的指针切来切去的 [ByteBuf]
需要掌握丰富的知识,比如多线程和网络编程
可靠性无法保证,断线重连、半包粘包、网络拥塞统统需要自己考虑
空轮询 bug,CPU 又 100%
1. Trustin Lee 2004年开发了Netty,成功入职了Arreo通信公司
2. 2008年,Trustin Lee,加入JBoss,发布了Netty3
3. 2012年,Trustin Lee,单干,发布了Netty4
4. 2013年,发布了Netty5。 引入JDK的新特性,比如 ForkJoinPool等
使用 ForkJoinPool 提升了复杂性
没有带来明显的性能提升
同时维护太多分支太耗费精力
即使是一台机器,在进程通信中,也需要网络通信。
1. 框架,gRPC、Dubbo、Spring WebFlux、Spring Cloud Gateway
2. 大数据,Spark、Hadoop、Flink
3. 消息队列,RocketMQ、ActiveMQ
4. 搜索引擎,Elasticsearch
5. 分布式协调器,Zookeeper
6. 数据库,Cassandra、Neo4j
7. 负载均衡,Ribbon
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
<version>4.1.45.Finalversion>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.13.2version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
<version>1.7.32version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.9version>
dependency>
Handler处理器,当我们监控到读写操作后,进行处理。Handler一般包括解码、编码、业务处理。Netty对于Handler的封装,设计各司其职,编码对应一个类、解码对应一个类、业务处理多个类(自定义)。所以处理这些需要多个Handler一起使用,在Netty中有一个专业术语PipeLine(流水线),建立连接的操作被Netty所封装了。
public class MyNettyServer {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
//创建了一组线程 通过死循环 监控状态 accept read write
serverBootstrap.group(new NioEventLoopGroup());
//ServerSocketChannel SocketChannel
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
/*
channel 接通 监控 accept rw 处理 通过流水线 用handler进行处理 。
*/
protected void initChannel(NioSocketChannel ch) throws Exception {
//ByteBuf 字节--->字符
//ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = (ByteBuf) msg;
String result = ((ByteBuf) msg).toString(Charset.defaultCharset());
System.out.println("result = " + result);
}
});
}
});
serverBootstrap.bind(8000);
}
}
public class MyNettyClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
// 为什么client 引入事件 循环 组
// client Netty 做多线程 异步
// 连接服务端 一个线程
// 通信 做成一个线程
// 异步处理 连接 --->
// IO操作
bootstrap.group(new NioEventLoopGroup());
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
});
//创建了新的线程 进行写操作
ChannelFuture connect = bootstrap.connect(new InetSocketAddress(8000));
connect.sync();
Channel channel = connect.channel();
channel.writeAndFlush("hello suns");
}
}
connect.sync();为什么做同步?开辟了一个新的线程等待连接,必须等到连接之后才能进行后面的操作
既可以accept,也可以read、write。
1. 事件(Accept READ WRITE) 循环(死循环)
2. 曾经讲过的worker
1. 独立线程
2. 通过死循环 监控 状态 进行操作 --》 run
while(true){
selector.select
SelectionKeys 遍历
}
3. Selector
EventLoop worker 线程 select ---> READ WRITE
boss 线程 select ---> Accept
开发中 如何过的EventLoop
1. 不会通过构造方法 让程序员创建。
2. 通过EventLoopGroup创建
在编程的过程中,开放的编程接口是 EventLoopGroup, EventLoopGroup 创建EventLoop(一个线程) 多个EventLoop(多个线程), EventLoopGroup是EventLoop的工厂。可以通过数字指定创建线程,
public static void main(String[] args) {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup(2);
// System.out.println("NettyRuntime.availableProcessors() = " + NettyRuntime.availableProcessors());
EventLoop el1 = eventLoopGroup.next();
EventLoop el2 = eventLoopGroup.next();
EventLoop el3 = eventLoopGroup.next();
System.out.println("el1 = " + el1);
System.out.println("el2 = " + el2);
System.out.println("el3 = " + el3);
}
DefaultEventLoop就是一个普通的线程,里面的执行任务,是由程序员指定的。获取方式和NioEventLoop一样。
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
EventLoop defaultEventLoop = defaultEventLoopGroup.next();
defaultEventLoop.submit(()->{
log.debug("defaultEventLoop",defaultEventLoop);
});
1. NioEventLoop 是一个线程 IO Write Read 事件监控
2. DefaultEventLooop 就是一个普通的线程,内容工作可以由程序员决定,他不做 IO监控 读写的处理.
注意:后续再Netty进行多线程开发,推荐大家优先考虑DefaultEventLoop -->普通线程。
EventLoop是会绑定channel,EventLoop可以支持多个channel访问的。就是说当前客户端连接了这个
EventLoop那么下次,还是会访问这个EventLoop,一个EventLoop可以支持多个channel访问。服务端 进行EventLoop的分工 —> 主从Reactor模式(NIO,Netty…)
在Reactor模式中,Boss只有一个,用NIO我们已经实现过,用Netty如何符合Reactor模式开发?
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup(3);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
// serverBootstrap.group(new NioEventLoopGroup());
serverBootstrap.group(boss,worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
//Read write
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("msg = " + msg);
}
});
}
});
serverBootstrap.bind(8000);
}
当Worker线程执行比较复杂的操作时,我们应该把复杂的操作交给其他线程,为了更好的融入Netty的体系中,可以使用DefaultEventLoop,辅助NIOEventLoop完成普通业务操作。
我们发现Handler中addLast()方法,有不同的重载,将我们创建的默认线程放进去,意味着Handler中的代码将在这个线程中执行,从而实现了Worker交付给其他线程执行任务
future.sync();会发生阻塞,当客户端与服务器连接之后,阻塞消失,执行后面的代码,就是等待双方连接上,连接不上怎么通信啊。
异步和多线程有什么关系?
首先异步也是多线程编程,多线程中线程直接是平等的,而异步多线程,有一个主要的线程,和其他线程一起完成任务,而主线程往往需要其他线程把结果返回。
处理过程中
1. 阻塞 主线程 完成异步操作的配合。
future.sync();
2. 异步处理 (新线程 异步线程完成的)
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
log1.debug(" add Listerner .....");
Channel channel = future.channel();
channel.writeAndFlush("hello suns");
}
});
只要是异步的操作 只能通过上述的2种方式的一种处理。
在Netty只要涉及到 网络 IO的相关操作 那么Netty都会涉及异步处理。
.connect() netty 异步
.writeAndFlush 异步
.close() 异步
Netty为什么要把网络、IO的操作做成异步?
提高系统的吞吐量,worker负责接收客户端的读写,当客户端的读写操作比较耗时,就会影响客户端的吞吐量,这个时候,如果增加新的线程去帮worker处理,worker接收到客户端请求之后,告诉客户端,而不需要worker阻塞,等结果什么时候处理好了,线程会告诉客户端中的回调方法。
Netty中大量使用了Promise。
JDK Future
Netty Future
Netty Promise
JDK Future 只能通过阻塞的方式获取异步线程返回值,不能通过回调监听的方式。
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
//可以获取异步线程的返回值
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("异步工作");
TimeUnit.SECONDS.sleep(1);
return 1;
}
});
log.debug("处理结果{}",future.get());
}
Netty中的Future对JDK中的Future进行增强,可以基于监听的方式获取返回值。
public static void main(String[] args) throws ExecutionException, InterruptedException {
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup(2);
EventLoop eventLoop = defaultEventLoopGroup.next();
Future<Integer> future = eventLoop.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 10;
}
});
// log.debug("处理结果{}",future.get());
future.addListener(new GenericFutureListener<Future<? super Integer>>() {
@Override
public void operationComplete(Future<? super Integer> future) throws Exception {
log.debug("处理结果{}",future.get());
}
});
}
Promise又在Netty的Future基础上,增加了获取异步处理的结果。通过Callable接口的获取异步处理结果的有什么问题?如果没有用Callable,无法获取异步处理结果。Promise可以方便的获取异步处理结果。另外,通过Callable只能获取到处理结果,不能说明返回值是否是正常的,还是异常的。就是不知道异步线程是成功还是失败。
public static void main(String[] args) throws ExecutionException, InterruptedException {
EventLoop eventLoop = new DefaultEventLoop().next();
DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);
new Thread(()->{
log.debug("异步处理....");;
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
promise.setSuccess(5);
}).start();
// log.debug("等待异步处理结果");
// log.debug("结果是{}",promise.get());
promise.addListener(new GenericFutureListener<Future<? super Integer>>() {
@Override
public void operationComplete(Future<? super Integer> future) throws Exception {
log.debug("等待异步处理结果");
log.debug("结果是{}",promise.get());
}
});
}
如果1代码执行完了,后面才执行2,还能监听得到吗?其实是可以的Netty已经做了处理
1. JDK ServerSocketChannel SocketChannel
2. Netty原有JDK中channel进行 统一的封装。
1. 统一channel的编程模型,通过他的封装 不在让客户区分SocketChannel ServerSocketChannel
2. Netty 一旦封装了Channel,更好的和Netty框架结合起来。 I/0 pipeline
配置 Channel TCP Socket缓冲区 滑动窗口的大小。。
客户端并不会在我们完成IO操作后,把客户端关闭,因为IO操作只是其中一个线程,还有其他线程可能在做其他操作。eventLoopGroup.shutdownGracefully();所有的线程工作完成,才关闭客户端。
1. Netty封装Channel 提供哪些API (方法)
channel.writeAndFlush("xiaohei");
.write
writeAndFlush 写出数据后 刷新缓冲区 ---》 写出去了
write不会立即发出去的,存在缓冲区中,手工.flush.
.close() 顾名思义 作用 channel.close() ---> Socket
ChannelFuture close = channel.close();//异步化操作 启动一个新的线程
//其他资源的释放,其他事,close()方法执行完之后,运行后面这些代码
//main主线程完成
//close.sync();
close.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
log.debug("channel.close()执行完后,操作后续的一些工作...");//不行
}
});
eventLoopGroup.shutdownGracefully();
2. 如何封装的Channel(源码)
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
//Read write
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("handle1",new ChannelInboundHandlerAdapter(){
@Override
//msg ButeBuf
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("handle1");
System.out.println("msg = " + msg);
super.channelRead(ctx,msg);
}
});
ch.pipeline().addLast("handle2",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("handle2");
super.channelRead(ctx, msg);
}
});
}
});
msg就是ByteBuf。多个handler数据传递,super。channelRead(ctx,msg)传递给下一个handler。
OutboundHandler的执行顺序和代码的书写顺序相反。
ch.writeAndFlush(“hello suns”);会向全局去找OutboundHandler,是否有输出。而ctx.writeAndFlush(“hello xiaojr”);会从当前的环境向上去找OutboundHandler
1. Handler作用,用于处理接受 或者发送前的数据。程序员使用netty最重要的战斗场地
2. 通过Pipeline 把多个handler有机整合成了一个整体。
读取数据 ChannleInboundHandler 子类
写出数据 ChannelOutboundHandler 子类
3. Pipleline中 执行Handler有固定顺序 (类似一个栈)双向链表
4. Handler传递数据
super.channelRead(ctx, msg);
最后一个Handler不需要传递数据时,那么上述方法 也就无需调用。
底层 ctx.fireChannelRead(s);
5. 最后一个handler如果不需要传递数据了
无需调用
super.channelRead(ctx, msg);
ctx.fireChannelRead(s);
6. Head Tail 2个默认的Handler在Pipeline
head --> handler1 ---> StingDecoder ---> handler2 ---> handler3 --> tail
7. OutboundHandler 特性 编码顺序 和 运行顺序 相反
8. 同一种Handler的编码顺序 和 运行顺序 相关 。 h1 h2 h3
h1 h3 h2
不同种的Handler,顺序有所不同,不会影响运行效果。
9. 注意
ch.writeAndFlush("hello suns");
全局的查找输出OutboundHandler 依次运行
ctx.writeAndFlush("hello xiaojr");
从当前Handler,往前查找,没有输出的OutboundHandler。
10. 方便测试Handller
EmbeddedChannel
serverBootstrap.childHandler和serverBootstrap.handler的区别,一个是处理读写的,一个是接收Accept。我们发现当我们没有写handler的时候程序也是照样运行,写不写好像没什么区别。实际上handler里面也有一套pipline,当我们想要做复杂的操作时,可以在接收连接之后,做一些事情。
//处理客户端连接
serverBootstrap.handler(new ChannelInitializer<NioServerSocketChannel>() {
@Override
protected void initChannel(NioServerSocketChannel ch) throws Exception {
}
});
//处理读写的
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
//Read write
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("msg:{}",msg);
super.channelRead(ctx, msg);
}
});
}
});
pipline对于SocketChannel来说,每一个SocketChannel独有一份,是为了保证数据不混乱。一个EventLoop可以接收多个客户端请求,但是Pipline是不一样的。
输入的时候Head和Tail都会运行,输出的时候只走Head。
ByteBufUtil.prettyHexDump(buffer)可以输出ByteBuf的内存结构。
1. 类似NIO ByteBuffer --- Netty网络通信的过程中,底层数据存储ByteBuf。
2. ByteBuf --> ByteBuffer封装
1. 自动扩容
2. 读写的指针,方便操作。 ByteBuffer没有读写指针的。读模式.flip 写模式 clear compact()
3. 内存的池化。(连接池,线程池)
4. 0copy相关内容。netty 0copy 尽可能少占用内存。
1. //如何获得ByteBuf 1. 支持自动扩容 2. 制定byteBuf初始化大小 3.256
3. 最大值 Integer.max,在构造方法中指定最大值
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
2.自动扩容 什么规律? 扩充到最大的ByteBuf的值
4的n次方 ---> 4 二次方 16 4 三次方 64 ---> 一旦超过64就在原有空间之上*2 64、128、256...
Netty为ByteBuf做了池化,提高创建的效率,不需要频繁的开辟内存。合理的使用资源,用完内存之后,并没有销毁,放到了池子中。减少内存溢出的可能,内存不够用,使用池化就相当于设定了内存的上限。
池化是默认开启的。
1. 堆内存
创建和销毁 代价小。读写效率(低),GC压力大
2. 直接内存
创建和销毁 代价大。GC压力小。
3. 引入了ByteBuf池化思想
1. 站在调用者提高创建的效率。
2. 合理的使用了资源 —> jemalloc
3. 减少内存溢出的可能 (内存不够用,内存使用过多)
4. Netty池化默认开启的
4.1以后 ByteBuf池化
4.1以前 关闭
-Dio.netty.allocator.type=pooled unpooled
1. ByteBuffer 读写模式切换 。 clear() compact() flip()
2. 读写指针
默认只能读一遍数据,可以通过其他方式多次读取。
当读写指针重合的时候,就不再输出buffer结构了。再读的话就会报错。
重复读取,类似于NIO中的API,get(i),不会影响指针的顺序。
public static void main(String[] args) {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
buffer.writeByte(5);
System.out.println("buffer = " + buffer);
System.out.println(ByteBufUtil.prettyHexDump(buffer));
buffer.markReaderIndex();//标记
byte b = buffer.readByte();
System.out.println("buffer = " + buffer);
System.out.println(ByteBufUtil.prettyHexDump(buffer));
buffer.resetReaderIndex();//返回
byte b1 = buffer.readByte();
System.out.println("b1 = " + b1);
}
1. 是不是回收,清空?销毁?
不是,如果ByteBuf池化的,内存释放。---》放回byteBuf的池子。
及使不是ByteBuf池化的 释放,不一定立即销毁。堆内存,垃圾回收。
2. Netty在处理内存释放时,因为内存释放情况种类繁多,所以netty对于编程人员来讲,
设计了统一的内存释放接口。
3. RefrenceCounted(引用计数器)--》
ByteBuf
创建一个ByteBuf —> 1
ByteBuf —> retain +1
ByteBuf —> release -1 …0
4. ByteBuf什么时候需要释放?
1. ByteBuf一定只能应用在pipeline中,在handler中进行释放数据最为理想且稳妥。
2. tailContext 会对读到的数据进行ByteBuf释放。
headContext 会对写的数据进行ByteBuf的释放。
最后一次程序员在handler中使用ByteBuf时候,做ByteBuf释放。
继承了ReferencceCounted类,可以完成内存的释放,原理就是引用计数器,每被使用一次引用次数加1,当引用次数为0是,内存就被释放。
只有ByteBuf才能被自动释放,所以当我们pipline中不是ByteBuf,或者handler没有向下传递的时候,都没法自动释放内存。需要程序员在最后一次使用ByteBuf的时候做释放。
分片 slice,并不是为其创建新的内存,还是用的原来的,只不过为每一个slice()创建新的指针。
public static void main(String[] args) {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
buffer.writeBytes(new byte[]{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'});
System.out.println("buffer = " + buffer);
System.out.println(ByteBufUtil.prettyHexDump(buffer));
ByteBuf s1 = buffer.slice(0, 6);
s1.retain(); // 保留引用计数
ByteBuf s2 = buffer.slice(6, 4);
s2.retain();
buffer.release();// 释放引用计数
System.out.println(ByteBufUtil.prettyHexDump(s1));
System.out.println(ByteBufUtil.prettyHexDump(s2));
}
TCP消息传递中,消息是无边际的,必须对消息进行边界化的处理。UDP不会出现半包粘包。
一次接收客户端的数据过大,没有接收完整,这就是半包。
1. 半包粘包
本质 接受数据时,数据有可能接受不完整,也可能接受过多。
1. 网络通信过程中,什么情况可以导致接受数据不完整或者过多?
1. ByteBuf
Socket缓冲区有接收和发送缓冲区,这两个是独立的。Socket的缓冲区是大于用户缓冲区的,等攒够了再发送给另一方。
滑动窗口,一次性发送多少数据帧,滑动窗口的大小等同于Socket缓冲区(空余空间的)的大小,因为Socket缓冲区也有可能接收了数据,被ByteBuf读取了一部分,。
这里有三个地方会出现半包粘包,ByteBuf、Socket缓冲区、滑动窗口。
我们以前写的代码中,write()返回值是0,说明Socket缓冲区满了,没有做刷新操作,就是没有往滑动窗口里面写,没有空间了。
1. Netty通信过程中,用于网络通信的ByteBuf是Netty创建。
1. 如何获得?
Handler 第一个非head得inboundhandler 获得 参数 就是Netty读取数据后,封装ByteBuf。
2. Netty创建的ByteBuf默认大小多大?
接受数据所创建的ByteBuf 大小默认 1024
//设置Socket缓冲区的大小 相当于设置滑动窗口的大小 默认65535
serverBootstrap.option(ChannelOption.SO_RCVBUF,100);
//netty创建ByteBuf时 执行大小 默认1024
serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR,new AdaptiveRecvByteBufAllocator(16,16,16));
ByteToMessageDecoder是所有解决半包粘包的父类,转换成Message之后,就是完整的消息。
固定长度的解码器
解决的问题是 固定长度消息的 半包粘包问题。 新问题 10 client —> abc_________ 不好的地方 占用空间
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
//Read write
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new FixedLengthFrameDecoder(10));
pipeline.addLast(new LoggingHandler());
}
});
通过’ \n \r\n’分隔符来解决半包粘包。每一个完整的消息必须有一个分隔符。
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
//Read write
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//maxlength 如果在10个字节中还找不到分隔符就不再处理了 失败
pipeline.addLast(new LineBasedFrameDecoder(1024));
pipeline.addLast(new LoggingHandler());
}
});
客户端
channel.writeAndFlush("liu\nshi\nli\n");
也可以自定义分隔符DelimiterBasedFrameDecoder
头体分离的方式解决。lengthFieldOffset长度偏移量,从哪个位子开始找头
lengthFieldLength找到头之后,找几位。
lengthAdjustment,如果Length后面直接是内容的话,lengthAdjustment就是0,如果Length和内容中间有东西的话,lengthAdjustment就代表跳过几个字节。
public static void main(String[] args) throws InterruptedException {
//统一编程模型
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
bootstrap.group(eventLoopGroup);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler());
ch.pipeline().addLast(new StringEncoder());
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
String message ="hello liusir";
//length 0 4 消息的长度
buf.writeInt(message.length());
// 特殊的头 1 1
buf.writeByte(1);
//具体内容
buf.writeBytes(message.getBytes());
String message1 ="hello liusir";
//length 0 4 消息的长度
buf.writeInt(message1.length());
// 特殊的头 1 1
buf.writeByte(1);
//具体内容
buf.writeBytes(message1.getBytes());
channel.writeAndFlush(buf);
}
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup(3);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss,worker);
//处理读写的
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
//Read write
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024,0,4,1,5));
pipeline.addLast(new LoggingHandler());
}
});
serverBootstrap.bind(8000);
}
1. ByteBuf--->handler--->第一个非head得handler,默认的大小是1024
2. decoder|encoder ---> 原始字节 Byte ---> Message[半包 粘包 都解决完成了]
3. ByteToMessageDecoder 完成 ByteBuf ---> Message(完整的消息 不应该半包粘包)
解决半包粘包的方式
3. LengthFieldBasedFrameDecoder
lengthFieldOffset 从第几个字节开始找 length 位
lengthFieldLength length位的长度
lengthAdjustment length位后面第几个字节是内容
initailByteToStrip 去头
1. 编码 Java对象 ---》 二进制 (Byte)
2. 解码 二进制 ---》 Java对象
广义编解码 (codec) 2部分
1. encode Java对象 ---》 二进制 (Byte)
2. decode 二进制 ---》 Java对象
ByteToMessageDecoder是抽象的,无法进行实例化,
Netty中编解码的体现
1.
1. encode Java对象 ---》 二进制 (Byte)
2. decode 二进制 ---》 Java对象
2. Netty中编解码工作 谁做的呢?
Handler --> ChannelHandler
ByteToMessageCodec
ByteToMessageDecoder --- 实际开发解码器 就作为这个类型的子类
MessageToByteEncoder --- 实际开发编码器 就作为这个类型的子类
3. 编解码器 在使用过程中 2部分核心内容
1. 序列化协议(编码格式)(传输数据的格式)
1. java序列化 和 反序列化
1. 类 implements Seriliazable接口 (标识性接口)接口里面没有方法 实现的 Spring ThrowsAdvice
2. ObjectOutputStream ---> Java 转换成 二进制的内容 ---> 文件|通过网络进行传输
ObjectInputStream ---> 二进制的内容 转换成 Java
3. serialVersionUID private static final long
1. serialVersionUID --> 如果不显示创建 java在序列化的过程中会不会创建?默认创建的 hashCode
问题:
1. 无法跨语言的
2. 可读性差 ---> 二进制
3. java序列化后的数据大小 大 ---》 ByteBuffer 5倍 传输效率 低
4. java序列化操作的时间 ---》ByteBuffer 5倍
2. XML
c -------------------------------------------- s
1
sunshuai
3. JSON
可读性好 {"name":"sunshuai","age":10},数据量 > 二进制的内容。 HTTP协议 + JSON ->SpringCloud
4. msgpack
类似于JSON,二进制传输。效率高 数据体量小 ---》支持多语言的 【bson ---> mongodb】4倍与protobuf
5. protobuf google
二进制 支持多种编程语言 -> 自己的编译器,把数据格式编译成中间语言 ---> go python.. 更小 更快 可读性差。
Hadoop
2. 具体的编解码器
ByteToMessageDecoder --- 实际开发解码器 就作为这个类型的子类
MessageToByteEncoder --- 实际开发编码器 就作为这个类型的子类
MessageToMessage和ByteToMessage有什么区别?ByteToMessage更底层,通过decode方法参数,ByteToMessage直接就是ByteBuf,而MessageToMessage是一个泛型,意味着可以传递其他类型的,已经被转换过。
ByteToMessage解决半包粘包,而MessageToMessage不解决半包粘包。所以使用MessageToMessage之前,需要自己解决半包粘包。
Netty对Java的序列化进行了改进。提供了ObjectEncoder和ObjectDecoder。
1. StringDecoder StringEncoder
功能 --- String转换
注意:
Netty的编解码体系中
1. ByteToMessage
MessageToByte
MessageToMessageDecoder
MessageToMessageEncoder
2. 区别
1. Byte系列的编解码 更底层
decode方法参数 观察到的
2. ByteToMessage体系
解决封帧的问题 (半包 粘包)
MessageToMessage不自己解决。
FixedLengthFrameDecoder
LineBasedFrameDecoder
LengthFieldBasedFrameDecoder
2. 封帧相关的
FixedLengthFrameDecoder
LineBasedFrameDecoder
LengthFieldBasedFrameDecoder
3. Java序列化相关的编解码
ObjectEncoder
ObjectDecoder() LengthFieldBasedFrameDecoder子类 不会有半包粘包
编解码器 Netty。做了一定的优化,数据小 ---> 幻术
4. JSON相关的编解码器
Netty JSON编码器(String ---> ) 只提供了JSON解码器 JsonObjectDecoder
针对于JSON数据 做封帧的(半包 粘包),根据{}区分。
5. Http协议编解码操作
Netty支持对Http协议进行编解码 ---》 Netty可以作为Web服务器 ---》 SpringWebFlex 底层就是基于Netty. --->Gateway
1. HttpServerCodec()
2. HttpObject子类进行编程了。
1. pipeline的调用与 Message的个事相关
2. 可以通过SimpleChannelInboundHandler限定消息的类型
3. channelRead0 和 channelRead区别是什么?
channelRead底层调用channelRead0
在channelRead中已经对于msg做了类型的转换。
4. HttpObjectAggregator
HttpRequest 和 HttpContent 聚合在一起。
6. Http协议的解码器,会出现半包粘包问题么?
Http协议Content-Length ---> 数据 不会出现。
使用Netty做web开发服务器,为什么一次会执行两次pipline?数据是一次性接收到msg中,但是请求头请求行被封装成HttpRequest,请求体被封装成HttpContent,存储到List中,被当做一个message。
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup());
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
//
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
//与http协议相关的编解码器
// 接受请求的时候,解码 http协议转换httpObject
// 提供相应的时候,编码 httpObject转换成http协议响应给client
pipeline.addLast(new HttpServerCodec());
/* pipeline.addLast(new HttpRequestDecoder());
pipeline.addLast(new HttpResponseEncoder());*/
pipeline.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug(" msg is {} ", msg);
if (msg instanceof HttpRequest) {
HttpRequest request = (HttpRequest) msg;
//请求头
HttpHeaders headers = request.headers();
//请求行
String uri = request.uri();
HttpVersion httpVersion = request.protocolVersion();
HttpMethod method = request.method();
log.debug("uri {} " + uri);
//状态行
DefaultFullHttpResponse response = new DefaultFullHttpResponse(httpVersion, HttpResponseStatus.OK);
byte[] bytes = "Hello Suns
".getBytes();
//response.setHeader(Content-Length,10);
response.headers().set(CONTENT_LENGTH, bytes.length);
response.content().writeBytes(bytes);
ctx.writeAndFlush(response);
} else if (msg instanceof HttpContent) {
//因为get请求,不通过请求体传递数据,所以这块内容是空。
HttpContent content = (HttpContent) msg;
//请求体
ByteBuf content1 = content.content();
}
}
});
}
});
//client浏览器 http://localhost:8000/xxx
serverBootstrap.bind(8000);
}
可以使用SimpleChannelInboundHandler来限定接收的类型。
还有一种聚合的Handler,包括了头和体
按照下面的要求,自定义编解码器
当ByteBuf中的数据没有读取完成时,会一直调用decode方法.
服务端
客户端
自定义编码器
上面的代码需要我们自己做安全校验,就是读取错了数据,需要手动还原,当我们解码器实现ReplayingDecoder之后,就不需要自己还原了。
1. 编码器
extends MessageToByteEncoder
encode
2. 编码器
extends ByteToMessageDecoder
1. 如果ByteBuf的数据一次解码没有处理完成,则Netty会重复调用decode方法
3. extends ByteToMessageCodec
编解码器 合二为一
4. Netty提供的一个特殊的解码器 ReplayingDecoder
他的实现类 中的decode方法 不需要做任何安全设置。
Netty编程
1. Bytebuf读写指针
2. Bytebuf释放
1. 设计协议
client 与 服务器端 传输数据的格式
协议头
幻数(魔术)suns版本号 公司唯一,标识,不让其他人访问
指令类型 登录 注册 xxx 业务操作的编号 类似于http中的uri代表不同的功能,我们的专有协议
是没有uri的。
序列化的方式:1 json 2 protobuf 3 hession
正文长度
协议正文(协议体)消息正文
{name:"sunshuai",password:"xiaohei"}
3. 编解码
参看代码
实际上pipline中处理会默认加上tail和head之外,还有ChannelInitalizer,相当于有三个默认的handler。
我们想知道上下文ChannelHandlerContext是否同一个,但有时候toString方法被覆盖,并不能看到对象的内存地址,当一个对象的toString方法没有被覆盖的时候,输入的是这个对象的全限定类名@hashcode(16进制),所以看是否同一个对象,只需要输出这个对象的hashcode。实际发现并不是同一个
1. Handler作用:
1. 网络连接(Channel)已经建立,通过Handler IO相关的操作。 处理IO中的数据。
ChannelHandler是Netty中最为重要的一个组件。与开发息息相关。
2. Pipeline中Handler执行流程?
1. Pipleine底层数据结构 双向列表 输入(入栈) 输出(出栈)
2. Pipeline中
head ChannelInitializer hander1 hander2 hander3.. tail
3. channel.writeAndFlush 与 ChannelHandlerContext.writeAndFlush()区别
1. channel.writeAndFlush 从整个pipeline的最后一个输出的handler(OutboundHandler)开始依次执行OutboundHandler
2. channelHandlerContext.writeAndFlush() 从当前位置依次查找前面的OutBoundHandler进行依次执行。
4. ByteBuf的使用和释放
//不建议使用
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
//建议
ctx.alloc().buffer();
ByteBuf用完后一定 realase()
5. channel的生命周期方法 或者 ChannelHandler中回调
SocketChannel IO操作 不负责accpet
initchannel
0. addHandler
1.channelRegistered
当连接(SocketChannel)被分配到了具体的worker线程上,回调这个方法。
Boss负责连接,Worker负责IO读写,当Boss线程连接上之后,SocketChannel被分配到worker之后。
问题:有没有可能Channel被accept但是没有分配worker?
答 存在这个可能性,client并发多与worker的数量。
2. channelActive 【常用】
channel的准备工作基本完成了。所有的pipeline上面的handler添加完成。 channel准备就绪了。
channelActive方法被回调用,就因为这个client和服务端 可以进行通信了。
应用过程中,可以通过channelActive回调,像另一方写数据,客户端可以往服务器,服务器也可以往
客户端写数据。
3.channelRead【常用】
接受数据操作,每一次发过来数据都会回调这个方法。
4. channelReadComplete
读操作结束。目的 资源性的释放,类似于finally
下面的2个方法 ,都会在channel关闭的时候 调用
服务端调用close,则直接回调 后面的2个方法
客户端调用close方法,发送消息到服务端。
5.channelInactive
channel连接断掉,TCP连接没有了
6.channelUnregistered
对应分配给我们的worker线程 换回 EventLoopGroup
7. removeHandler
为什么了解回调 ---Channel相关一些事件 。Netty (基于事件的异步通信框架)
6. 异常的处理
前一个handler出的异常,在下一个handler也可以处理。
exceptionCaught
如果本handler出现的异常 这个方法可以处理
如果本handler中前面handler出现的未处理异常,他也可以处理。
开发的过程中 一定要设置日志,没有日志,无法挑错。
8. Handler相关内容
监听服务器的空闲检查
作用:空闲检查
1. 注意: 常用于服务端监控的一个Handler
读空闲 ,写空闲 ,读写空闲 (空闲指的就是没有通信)
开发场景:
心跳 : 在一段时间内 读写都空闲 更能说明,网络有可能不通。
读写空闲
注意: 1. 心跳检测是二种方式:
1. IdleStateHandler 方式 空闲时间 最够长 死了 一次性处理。
2. 一定的时间间隔 一定次数 死
2. 监控到了空闲之后,核心关闭channel。完成业务处理的善后工作。
3. 客户端角度:如果发现 channel关了,但是client本身没事,重试的机制。
定时任务。
TCP协议是场链接,HTTP虽然依附于TCP协议,但是短连接,一次响应,主动关闭TCP连接。
1. 增加 Netty处理WebSocket能力..
常规的问题
1. WebSocket干什么?
协议:全双工的
传统的Http1.0协议 短连接协议, 请求 《---》 响应 结束 连接(断了) 无状态协议 (用户会话追踪,cookie --> session)
传统的Http1.1协议 有限的长连接 目的 减少tcp 连接 握手的次数 keepalive 决定连接的时长
HTTP 持久性连接(也称为HTTP keep-alive或HTTP 连接重复使用)是一种概念,允许单个TCP 连接发送和接收多个HTTP 请求/响应,而不是为每个请 求/响应对打开新连接。 Apigee 使用持久性连接与后端服务进行通信。 默认情况下,连接保持活动状态60 秒。
不能做推送 服务端 推数据
服务器端 发生变化了 主动告知 client
2. WebSocket协议 Http协议关系是什么?
WebSocket在Http协议之上的。
3. Netty如何支持WebScoket
WebSocketServerProtocalHandler
什么是有状态的对象,什么是无状态的?有没有成员变量,就算有成员变量也是不可写的。MessageToMessasge可以被共用,因为这个是在ByteToMessage之后执行的,已经获取到了完整的消息。
MessageToByte可以被共享,因为客户端这边只是一个线程,不会有并发问题。
多个Pipeline中共用Handler的方式
1. 不要在pipeline中new Handler,把new Handler的过程提取到外面,这样多个Pipeline就可以共用Handler.
2. 如果共用Handler就意味着 Handler会被多线程访问。
Handler处理过程中 什么情况下可以被多个pipeline共用
1. 无状态的Handler 或者 有状态但是 加锁 Handler 可以被Pipeline共用。
2. MessageToMessasgeDcoder 也可共用。能被共用的Handler 会通过一个 注解生命 @Sharable.
3. ByteToMessageDecoder 及其子类 不能。
注意:
开发能被共用的@Sharable ,不能被共用的不加@Sharable(ByteToMessageDecoder)
能加的就提取出来共用,不能加的就Pipeline 去创建了。
开发建议:
自定义的过程中,Handler处理成无状态 (没有成员变量),可以使用Sharable。
package com.suns.netty14;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
// channel 什么关闭?
// handler一些回调方法进行处理
// IdleStateHandler处理 关闭channel
// 异常发生 也会关闭channel
// 程序正常退出 。。
// 写内容 都要在Handler中完成。
public class MyNettyClient {
private static final Logger log = LoggerFactory.getLogger(MyNettyClient.class);
public static void main(String[] args) throws InterruptedException, JsonProcessingException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
LoggingHandler loggingHandler = new LoggingHandler();
StringEncoder stringEncoder = new StringEncoder();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
Bootstrap group = bootstrap.group(eventLoopGroup);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("logging", loggingHandler);
ch.pipeline().addLast("stringEnoder", stringEncoder);
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush("xiaohei");
}
});
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
//监控channel的关闭
channel.closeFuture().sync();
} catch (InterruptedException e) {
log.error("client error ", e);
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
package com.suns.netty14;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyNettyServer {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer.class);
public static void main(String[] args) {
LoggingHandler loggingHandler = new LoggingHandler();
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//workerGroup默认 后续结合内存的压力进行调整。
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(loggingHandler);
//共用的独立的使用 ,不能共用的自己创建
}
});
Channel channel = serverBootstrap.bind(8000).sync().channel();
channel.closeFuture().sync();
} catch (Exception e) {
log.error("server error", e);
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
1. netty参数的设置位置
client
bootstrap.option()
server
serverBootstrap.option(); //ServerScoketChannel
serverBootstrap.childOption() //ScoketChannel
2. netty中
ChannelOption(ChannelOption进行配置修改) --- ChannelConfig(核心配置) ---DefaltChannelConfig(核心配置的默认值)
ChannelOption.XXXX,对应内容
1. RCVBUF_ALLOCATOR SocketChannel
作用:设置服务端 ByteBuf缓冲区大小,这个IO ByteBuf在netty设计中,只能是直接内存。
默认值:
最小64 初始值1024 最大65535
主动设置
serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16,16,16));
2. SO_RCVBUF & SO_SNDBUF [了解即可]
*SO_RCVBUF ServerSocketChannl option() 接受Socket缓冲区的大小 。
SO_SNDBUF SocketChannel 客户端 option() 服务端 childOption 发送Socket缓冲区的大小。
2点注意:
1. 目前的开发中,操作系统都比较智能了。定义合理的发送Socket缓冲 接受Socket缓冲区的大小。
2. SO开头的参数 都是和操作系统底层 TCP协议相关的参数。所以这些参数 1. 可以在netty中通过参数设置 2. 直接修改操作系统对应的文件
3. ALLOCATOR
SocketChannel childOption()
ByteBuf内存分配器
ctx.alloc()
设置方式
serverBootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
区别
4. TCP_NODELAY 默认情况下 就是不延迟
不积攒,有多少发多少。
SocketChannel
Nagle算法 :
6. CONNECT_TIMEOUT_MILLIS
SocketChannel
客户端在建立连接时,如果超过了参数指定的时间,抛出超时异常。
*6. SO_BACKLOG
ServerScoketChannel参数 .option()
含义:
SO_BACKLOG 决定了3次握手之后,全连接队列的大小。
系统层面 /proc/sys/net/core/somaxconn 在这个文件中设置
Netty运行过程中 全连接队列的大小 ,min(SO_BACKLOG,somaxconn)
补充注意:半连接队列 没有最大值 无限大 可以不用考虑
tcp建立连接3次握手
*7. SO_REUSEADDR
ServerScoketChannel参数 .option()
端口复用:可以使用别的进程使用的端口
为什么需要这个方式:Server非正常关闭,或者正常关闭过程中出现意外,都有可能导致端口被占用,后续需要重新启动服务时,就会出现端口被占用异常。
为了解决这个问题,需要端口复用
tcp关闭连接4次握手
8. SO_KeepAlive
1. 目前的开发中 没有必要使用TCP keepalive 。还是需要心跳解决 活跃的问题。
KeepAlive TCP协议:7200 9 75秒 一次
net.ipv4.tcp_keeplive_time = 7200
net.ipv4.tcp_keeplive_xxx
2. 心跳站在应用层的角度解决问题
KeepAlive站在TCP层次解决问题。
3. Http协议1.1,保证 有限长连接 KeepAlive头 60秒
TCP协议中KeepAlive 是什么关系呢?TCP keepAlive保证网络连接,两者是或者的。
半毛钱的关系都没有。