Netty学习系列
lyowish
回顾一下传统的HTTP服务器的原理 :
1、创建一个ServerSocket,监听并绑定一个端口
2、一系列客户端来请求这个端口
3、服务器使用Accept,获得一个来自客户端的Socket连接对象
4、启动一个新线程处理连接
4.1、读Socket,得到字节流
4.2、解码协议,得到Http请求对象
4.3、处理Http请求,得到一个结果,封装成一个HttpResponse对象
4.4、编码协议,将结果序列化字节流写Socket,将字节流发给客户端
5、继续循环步骤3
HTTP服务器
之所以称为HTTP服务器
,是因为编码解码
协议是HTTP协议
,如果协议是Redis协议,那它就成了Redis服务器,如果协议是WebSocket,那它就成了WebSocket服务器,等等。使用Netty
你就可以定制编解码协议
,实现自己的特定协议的服务器
。
NIO代表的一个词汇叫着IO多路复用
。Netty
就是基于Java NIO
技术封装的一套框架。为什么要封装呢?原生的Java NIO使用没那么方便。Netty把它封装后,提供一个易于操作的使用模式和接口,用户使用起来就更加容易了。
为什么使用Netty?
- 使用JDK原生API比较复杂。
- 需要要掌握更多的基础知识,比如NIO涉及到的Reactor模式。
- 高可用的话,需要出路断路重连,半包读写,失败缓存等问题。
- JDK NIO的bug
- 对Netty来说,它的API简单,性能高而且社区活跃。
NIO基础了解
在BIO模式下,Socket通信从头到尾都是阻塞的,这些线程只能等着,占用系统cpu资源,什么事也不做。
NIO如何做到非阻塞呢?NIO使用事件驱动机制。可以用一个线程把accept,读写操作,请求处理逻辑都给干了。如果什么事都没有,它也不会死循环,它会将线程休眠起来,知道下一个事情来了再继续干活。
NIO的特点:
- 一个线程可以处理多个通道,减少创建线程数量。
- 读写非阻塞,节约资源。没有可读/可写数据时,不会发生阻塞导致线程资源的浪费。
阻塞与非阻塞
阻塞:系统调用内核发现数据还没有准备好,就一直阻塞等待,不会去做其他事情。
非阻塞:系统调用内核发现数据还没有准备好,不会等待。如果数据已经准备好,也直接返回。
同步与异步
同步:应用程序直接参与IO读写的操作
异步:IO读写交给操作系统处理,应用程序只需要等待通知
Java Nio中主要包含三个概念。缓存区(Buffer)
,通道(channel)
,选择器(selector)
。
缓存区(Buffer)
缓存区概念是对java原生数组对象的封装,它除了包含数组外,还包含四个描述缓冲区特征属性以及一组用来操作缓冲区的API。
四个属性:
容量:缓存区能够容纳的数据元素的最大数量。初始化后不能更改。
上界:缓存区中第一个不能被读或者写的元素位置。或者说,缓存区内现存元素的上界。
位置:缓存区内下一个将要被读或写的元素位置。在进行读写缓存区时,位置会自动更新。
标记:一个备忘录。初始时为"未定义",调用mark时mark = position,调用reset时,position=mark。
四个属性关系:mark <= position <= limit <= capacity
写入数据:
position表示下一个要写入的坐标
读数据:
读数据前先调用flip方法,flip
方法完成两件事
(1)将limit设置为当前的position值
(2)把position设置为0
Clear()方法能够把所有的状态变化设置为初始化时的值。
remaining方法是返回缓冲区中目前存储的元素个数。
hasRemaining()的含义是查询缓存区中是否还有元素。
compact()压缩方法是为了将读取了一部分的buffer,其剩下的部分整体移动到buffer的头部。
duplicate复制缓冲区,两个缓冲区对象实际上指向了同一个内部数组,但分别管理各自的属性。
lice缓冲区切片,缓冲区切片,将一个大缓冲区的一部分切出来,作为一个单独的缓冲区,但是它们公用同一个内部数组。切片从原缓冲区的position位置开始,至limit为止。
直接缓冲区 DirectByteBuffer
。使用的内存是直接调用了操作系统api分配的,绕过了JVM堆栈。直接缓冲区通过ByteBuffer.allocateDirect()
方法创建,并可以调用isDirect()
来查询一个缓冲区是否为直接缓冲区。
Channel
通常来说NIO中的所有IO
都是从Channel(通道)
开始的。
- 从通道进行数据读取。创建一个缓存区(Buffer),然后请求通道数据到缓存区。
- 从通道进行数据写入。创建一个缓存区(Buffer),填充数据,然后请求通道将缓存区数据写入。
与流的区别:
1. 通道可以读也可以写。流是单向的(只能读或者写,所以之前我们用流进行IO操作的时候,需要分别创建一个输入流和一个输出流)。
2. 通道可以异步读写。
3. 通道总是基于缓存区(Buffer)来读写。
Java NIO 中最重要的几个Channel
的实现:
1. FileChannel,用于文件的数据读写。
2. DatagramChannel,用于UDP的数据读写。
3. SocketChannel,用与TCP的数据读写,一般是客户端实现。
4. ServerSocketChannel,允许我们监听TCP链接请求,每个请求会创建一个SocketChannel,一般是服务器实现。
SocketChannel
的使用:
1. 通过SocketChannel链接到远程服务器。
2. 创建读数据/写数据缓冲区对象来读取服务端数据或向服务端写入数据。
3. 关闭SocketChannel。
public class WebClient {
public static void main(String[] args) throws IOException {
//1.通过SocketChannel的open()方法创建一个SocketChannel对象
SocketChannel socketChannel = SocketChannel.open();
//2.连接到远程服务器(连接此通道的socket)
socketChannel.connect(new InetSocketAddress("127.0.0.1", 3333));
// 3.创建写数据缓存区对象
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
writeBuffer.put("hello WebServer this is from WebClient".getBytes());
writeBuffer.flip();
socketChannel.write(writeBuffer);
//创建读数据缓存区对象
ByteBuffer readBuffer = ByteBuffer.allocate(128);
socketChannel.read(readBuffer);
//String 字符串常量,不可变;StringBuffer 字符串变量(线程安全),可变;StringBuilder 字符串变量(非线程安全),可变
StringBuilder stringBuffer=new StringBuilder();
//4.将Buffer从写模式变为可读模式
readBuffer.flip();
while (readBuffer.hasRemaining()) {
stringBuffer.append((char) readBuffer.get());
}
System.out.println("从服务端接收到的数据:"+stringBuffer);
socketChannel.close();
}
}
ServerSocketChannel的实现:
1. 通过ServerSocketChannel 绑定ip地址和端口号。
2. 通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel
对象用户从客户端读/写数据。
3. 创建读数据/写数据缓冲区对象来读取客户端数据或向客户端发送数据。
4. 关闭SocketChannel和ServerSocketChannel。
public class WebServer {
public static void main(String args[]) throws IOException {
try {
//1.通过ServerSocketChannel 的open()方法创建一个ServerSocketChannel对象,open方法的作用:打开套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.通过ServerSocketChannel绑定ip地址和port(端口号)
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 3333));
//通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据
SocketChannel socketChannel = ssc.accept();
//3.创建写数据的缓存区对象
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
writeBuffer.put("hello WebClient this is from WebServer".getBytes());
writeBuffer.flip();
socketChannel.write(writeBuffer);
//创建读数据的缓存区对象
ByteBuffer readBuffer = ByteBuffer.allocate(128);
//读取缓存区数据
socketChannel.read(readBuffer);
StringBuilder stringBuffer=new StringBuilder();
//4.将Buffer从写模式变为可读模式
readBuffer.flip();
while (readBuffer.hasRemaining()) {
stringBuffer.append((char) readBuffer.get());
}
System.out.println("从客户端接收到的数据:"+stringBuffer);
socketChannel.close();
ssc.close();
} catch (IOException e) {
e.printStackTrace();
}
}}
在Java NIO中如果一个channel是FileChannel
类型的,那么他可以直接把数据传输到另一个channel
。
- transferFrom() :transferFrom方法把数据从通道源传输到FileChannel
- transferTo() :transferTo方法把FileChannel数据传输到另一个channel
Selector
使用Selector来实现单线程控制多路非阻塞IO
。Selector需要其他两种对象配合使用。即SelectionKey
(选择key) 和 SelectableChannel
(可选择的通道)。
SelectableChannel
是一类可以与Selector
进行配合的通道,例如Socket相关通道以及Pipe产生的通道都属于SelectableChannel。这类通道可以将自己感兴趣的操作(例如read、write、accept和connect)注册到一个Selector上,并在Selector的控制下进行IO相关操作。
Selector是一个控制器,它负责管理已注册的多个SelectableChannel,当这些通道的某些状态改变时,Selector会被唤醒(从select()方法的阻塞中),并对所有就绪的通道进行轮询操作。
SelectionKey
是一个用来记录SelectableChannel
和Selector
之间关系的对象,它由SelectableChannel
的register()
方法返回,并存储在Selector
的多个集合中。它不仅记录了两个对象的引用
,还包含了SelectableChannel感兴趣的操作
,即OP_READ
,OP_WRITE
,OP_ACCEPT
和OP_CONNECT
。
- Register方法,SelectalbeChannel的register方法
SelectionKey register(Selector sel, int ops)
-
Selector的三个集合
(1)keys
集合,存储了所有与Selector关联的SelectionKey
对象;
(2)selectedKeys
集合,存储了在一次select()方法调用后,所有状态改变的通道
关联的SelectionKey
对象;
(3)cancelledKeys
集合,存储了一轮select()方法调用过程中,所有被取消但还未从keys中删除的SelectionKey对象。 -
select 方法
Selector类的select()
方法是一个阻塞方法,它有两种形式:
int select()。不带参数的方法会一直阻塞,直到至少有一个注册的通道状态改变,才会被唤醒。
int select(long timeout),带有timeout参数的方法会一直阻塞,直到时间耗尽,或者有通道的状态改变。 -
轮询处理
在一次select()方法返回后,应对selectedKeys
集合中的所有SelectionKey
对象进行轮询操作,并在操作完成后手动将SelectionKey
对象从selectedKeys
集合中删除。
服务端代码:
public class SelectorServer {
private static final int PORT = 1234;
private static ByteBuffer buffer = ByteBuffer.allocate(1024);
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(PORT));
ssc.configureBlocking(false);
//1.register()
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("REGISTER CHANNEL , CHANNEL NUMBER IS:" + selector.keys().size());
while (true) {
//2.select()
int n = selector.select();
if (n == 0) {
continue;
}
//3.轮询SelectionKey
Iterator<SelectionKey> iterator = (Iterator) selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//如果满足Acceptable条件,则必定是一个ServerSocketChannel
if (key.isAcceptable()) {
ServerSocketChannel sscTemp = (ServerSocketChannel) key.channel();
//得到一个连接好的SocketChannel,并把它注册到Selector上,兴趣操作为READ
SocketChannel socketChannel = sscTemp.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("REGISTER CHANNEL , CHANNEL NUMBER IS:" + selector.keys().size());
}
//如果满足Readable条件,则必定是一个SocketChannel
if (key.isReadable()) {
//读取通道中的数据
SocketChannel channel = (SocketChannel) key.channel();
readFromChannel(channel);
}
//4.remove SelectionKey
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void readFromChannel(SocketChannel channel) {
buffer.clear();
try {
while (channel.read(buffer) > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println("READ FROM CLIENT:" + new String(bytes));
}
} catch (IOException e) {
e.printStackTrace();
}
}}
客户端代码:
public class SelectorClient {
static class Client extends Thread {
private String name;
private Random random = new Random(47);
Client(String name) {
this.name = name;
}
@Override
public void run() {
try {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(1234));
while (!channel.finishConnect()) {
TimeUnit.MILLISECONDS.sleep(100);
}
ByteBuffer buffer = ByteBuffer.allocate(1024);
for (int i = 0; i < 5; i++) {
TimeUnit.MILLISECONDS.sleep(100 * random.nextInt(10));
String str = "Message from " + name + ", number:" + i;
buffer.put(str.getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer);
}
buffer.clear();
}
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(new Client("Client-1"));
executorService.submit(new Client("Client-2"));
executorService.submit(new Client("Client-3"));
executorService.shutdown();
}}
Netty整体认识
Netty是一个优秀的网络编程框架。
整体结构
core
核心层。提供底层网络交互的抽象和实现。事件模型,通用通信api,支持零拷贝的buffer等。protocol support
协议支持层。覆盖主流协议的编码,解码实现。transport service
传输服务层。提供了网络传输的定义和实现方法。
网络通信层
封装了底层socket操作。它的职责是执行网络I/O操作,支持多种网络协议和I/O模型的连接操作。 支持提供了bootstrap,serverbootstrap,channel等组件。
bootstrap,serverbootstrap分别负责客户端,服务端启动。
channel是网络通信的载体,提供了与底层socket交互的能力。如register,bind,connect,read,write,flush等。Netty自己实现的channle是以JDK NIO Channel为基础的。
Channel会有多种状态,如连接建立,连接注册,数据读写,连接销毁等。
事件调度层
通过Reactor线程模型对个各类事件进行聚合处理。通过Selector主循环线程集成多种事件。
事件调度层的核心组件是:EventLoopGroup,EventLooop。
EventLoopGroup是Netty Reactor线程模型的具体实现方式。
单线程模型
EventLoopGroup只包含一个EventLoop,Boss 和 Worker使用同一个EventLoopGroup。
多线程模型
EventLoopGroup包含多个EventLoop,Boss 和 Worker使用同一个EventLoopGroup。
主从多线程模型
EventLoopGroup 包含多个EventLoop,Boss是主Reactor,Worker是从Reactor。主Reactor负责新的网络连接Channel创建,然后Channel注册到从Reactor。
服务编排层
负责组装各类服务,用以实现网络事件的动态编排和有序传播。
服务编排层的核心组件有,ChannelPipeline,ChannelHandler,ChannelHandlerContext。
ChannelPipeline,ChannelHandlerContext,ChannelHandler
ChannelPipeline负责组装各种ChannelHandler。实际数据的编解码
以及加工处理操作
由ChannelHandler完成。
当I/O读写事件触发时,ChannelPipeline会一次调用ChannelHandler列表对Channel的数据进行拦截和处理。
ChannelPipeline是线程安全的。每一个Channel会对应绑定
一个新的ChannelPipeline。
一个ChannelPipeline关联
一个EventLoop,一个EventLoop仅会绑定
一个线程。
每个ChannelHandler绑定ChannelHandlerContext的作用是什么呢?
(1)ChannelHandleContext保存ChannelHandler上下文。
(2)实现ChannelHandler之间的交互
(3)包含ChannelHandler声明周期的所有事件,如Connet,bind,read,flush,write,close等
每个ChannelHanler的通用的逻辑,如果没有ChannelContext抽象,会造成代码的重复。