Netty
Java NIO三大组件:Selector,Buffer,Channel
- 每个channel都对应一个Buffer
- Selector对应一个线程
- Buffer是一个内存块,底部有一个数组
Buffer
//标记
private int mark = -1;
//当前位置
private int position = 0;
//缓冲区的位置
private int limit;
//缓冲区的容量大小
private int capacity;
Selector
常用方法
//Open方法,返回一个选择器对象
public static Selector open();
//监控注册的通道,当有IO操作时,将对应的SelectionKey加入集合并返回
public abstract int selectNow()
//监控注册的通道,当有IO操作时,将对应的SelectionKey加入集合并返回,参数用来设置超时时间
public abstract int select(long timeout)
//查看所有SelectionKey
public abstract Set<SelectionKey> selectedKeys();
NIO的简单网络编程
服务端
package com.kklll.leetcode.nio.server;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* @Author DeepBlue
* @Date 2020/10/11 15:25
*/
public class NioServer {
public static void main(String[] args) throws Exception {
//serverSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//先获取Socket然后绑定端口号
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//非阻塞
serverSocketChannel.configureBlocking(false);
//创建一个Selector
Selector selector = Selector.open();
//把ServerSocketChannel注册到Selector事件为Accept
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if (selector.select(1000) == 0) {
System.out.println("我干别的去了");
continue;
}
//获取到Accept发生的集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
SelectionKey key1 = null;
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
key1 = iterator.next();
if (key1.isAcceptable()) {
SocketChannel accept = serverSocketChannel.accept();
accept.configureBlocking(false);
System.out.println("客户端连接成功!Channel的hashcode是:"+accept.hashCode());
//再注册,并关联一个Buffer
accept.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key1.isReadable()) {
SocketChannel channel = (SocketChannel) key1.channel();
ByteBuffer buffer = (ByteBuffer) key1.attachment();
channel.read(buffer);
System.out.println("客户端发送的是:" + new String(buffer.array()));
buffer.clear();
channel.close();
}
iterator.remove();
}
}
}
}
客户端
package com.kklll.leetcode.nio.client;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @Author DeepBlue
* @Date 2020/10/11 15:25
*/
public class NioClient {
public static void main(String[] args) throws IOException {
SocketChannel clientChannel = null;
try {
clientChannel = SocketChannel.open();
} catch (IOException e) {
e.printStackTrace();
}
try {
clientChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
}
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
try {
if (!clientChannel.connect(inetSocketAddress)) {
while (!clientChannel.finishConnect()) {
System.out.println("连接需要时间,客户端不会阻塞,可以做其他工作!");
}
}
} catch (IOException e) {
e.printStackTrace();
}
//连接成功
String s = "Hello,Netty And NIO2";
ByteBuffer buffer = ByteBuffer.wrap(s.getBytes());
try {
clientChannel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
try {
System.in.read();
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
NIO实现的多人聊天室
服务端
package com.kklll.leetcode.nio.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* @Author DeepBlue
* @Date 2020/10/11 17:17
*/
public class GroupCharServer {
private Selector selector;
private ServerSocketChannel listenChannel;
public static long BLOCK_TIMEOUT = 2000;
public static int PORT = 6667;
public GroupCharServer() {
try {
selector = Selector.open();
listenChannel = ServerSocketChannel.open();
listenChannel.bind(new InetSocketAddress(PORT));
listenChannel.configureBlocking(false);
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
e.printStackTrace();
}
}
public void listen() {
try {
while (true) {
int count = selector.select(BLOCK_TIMEOUT);
if (count > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
SelectionKey key = null;
while (iterator.hasNext()) {
key = iterator.next();
handleRequest(key);
iterator.remove();
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
}
}
public void handleRequest(SelectionKey key) {
try {
//如果是连接
if (key.isAcceptable()) {
accept();
}
//如果是read方法
if (key.isReadable()) {
readData(key);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void accept() {
try {
SocketChannel sc = listenChannel.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
System.out.println(sc.getRemoteAddress() + "上线了");
} catch (Exception e) {
e.printStackTrace();
}
}
public void readData(SelectionKey key) {
SocketChannel channel = null;
try {
channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
if (count > 0) {
String s = new String(buffer.array());
System.out.println("FROM 客户端" + s);
forward(s, channel);
} else {
System.out.println(channel.getRemoteAddress());
}
} catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress() + "离线");
key.cancel();
channel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
public void forward(String msg, SocketChannel self) throws IOException {
System.out.println("转发消息中..............");
for (SelectionKey key : selector.keys()) {
Channel target = key.channel();
if (target instanceof SocketChannel && target != self) {
SocketChannel dest = (SocketChannel) target;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
dest.write(buffer);
}
}
}
public static void main(String[] args) {
GroupCharServer server=new GroupCharServer();
server.listen();
}
}
客户端
package com.kklll.leetcode.nio.client;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
/**
* @Author DeepBlue
* @Date 2020/10/11 17:17
*/
public class GroupChatClient {
private static final String HOST = "127.0.0.1";
private static final int SERVER_PORT = 6667;
private Selector selector;
private SocketChannel socketChannel;
private String username = null;
public GroupChatClient() {
try {
selector = Selector.open();
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, SERVER_PORT));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
username = socketChannel.socket().getLocalAddress().getHostAddress()+socketChannel.socket().getPort();
System.out.println("客户端 IS OK");
} catch (Exception e) {
e.printStackTrace();
}
}
public void sendInfo(String msg) {
msg = username + "说:" + msg;
try {
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
public void readInfo() {
try {
int select = selector.select();
if (select > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer allocate = ByteBuffer.allocate(1024);
channel.read(allocate);
String s = new String(allocate.array());
System.out.println(s);
}
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
GroupChatClient groupChatClient = new GroupChatClient();
new Thread(() -> {
while (true) {
groupChatClient.readInfo();
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
String next = sc.next();
groupChatClient.sendInfo(next);
}
}
}
Netty架构
Netty服务端工作原理
Netty客户端工作原理
重要元素
(1)NioSocketChannel/NioServerSocketChannel
NioSocketChannel/NioServerSocketChannel可以理解成java中的Socket/ServerSocket。既可以输入又可以输出,可以是设置成阻塞和非阻塞。
一个NioSocketChannel对应一个连接,一个NioSocketChannel/NioServerSocketChannel对应一个Pipeline。
(2)NioEventLoopGroup
NioEventLoopGroup相当于是一个线程池,维护一个NioEventLoop[]数组。
NioEventLoopGroup可以设置线程个数,线程默认个数是NettyRuntime.availableProcessors() * 2。
对于Server来说,parent Group一般只有1个线程,child Group有NettyRuntime.availableProcessors() * 2个线程。
对于Client来说,一般也是一个线程。
每次有新Channel注册时,NioEventLoopGroup会按顺序选择一个NioEventLoop来处理这个Channel。对于服务端,NioServerSocketChannel是扔给parent Group 里唯一的线程处理的,而新接入的NioSocketChannel则是由child Group按顺序选择的NioEventLoop来处理。客户端类似。
(3)NioEventLoop
NioEventLoop相当于一个线程,一个NioEventLoop包含一个Selector。
NioEventLoop是用来执行任务的。主要包括3种类型的任务:定时任务、普通任务、IO任务。
在需要执行任务的时候,会启动NioEventLoop线程,NioEventLoop的run()实现如下:
// NioEventLoop
protected void run() {
while(true) {
switch(hasTasks() ? selector.selectNow() : -1) {
case CONTINUE: //-2
continue;
case SELECT: //-1
select(wakenUp.getAndSet(false));
if(this.wakenUp.get()) {
selector.wakeup();
}
default:
processSelectedKeys();
runAllTasks();
break;
}
}
}
1.select():
轮询是否有到点的定时任务
或普通任务
或IO任务
可以执行。如果有任一种任务可执行就会跳出循环。如果任何任务都没有,则会调用selector.select(timeoutMillis)等待可执行的IO任务。
定时任务:PriorityQueue scheduledTaskQueue,按timeout时间排序
普通任务:LinkedBlockingQueue taskQueue
IO任务:SelectionKey
2.processSelectedKeys():
处理IO任务OP_CONNECT/OP_ACCEPT/OP_READ/OP_WRITE。
// NioEventLoop
public void processSelectedKeys() {
for(int i = 0; i < selectedKeys.size; ++i) {
SelectionKey k = selectedKeys.keys[i];
AbstractNioChannel ch = k.attachment();
NioUnsafe unsafe = ch.unsafe();
//处理连接事件
int readyOps = k.readyOps();
if ((readyOps & OP_CONNECT) != 0) {
unsafe.finishConnect();
}
//处理写事件
if((readyOps & SelectionKey.OP_WRITE) != 0) {
unsafe.forceFlush();
}
//处理读事件或处理NioServerSocketChannel的ACCEPT事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
}
}
3.runAllTasks():
处理到点的定时任务和普通任务。
对于到点的定时任务,会添加到普通任务队列,并执行普通任务队列的任务。
IO任务执行时间比例:ioRatio = 50
其他任务执行时间比例:100 - ioRatio
如果ioRatio设置为0的话,则是顺序执行,先执行完IO任务再执行完其他任务。
(4)Pipeline
Pipeline相当于一个管道,维护一个ChannelHandlerContext的双向链表,头结点是HeadContext,尾结点是TailContext。
HeadContext,既是ChannelInBoundHandler也是ChannelOutBoundHandler,是outBound类型。
TailContext,是ChannelInBoundHandler,是inBound类型。
当进行读操作,即数据从外部读到自己这边的Channel时,数据会先传给Pipeline,然后从HeadContext开始依次传给inBound类型的ChannelInBoundHandler,直到TailContext结束。
当进行写操作,即数据从自己这边的Channel往外部写时,数据会先传给Pipeline,然后从TailContext开始依次传给outBound类型的ChannelOutBoundHandler,直到HeadContext结束。
异常事件是从HeadContext往TailContext传递。
对于Client端:
除HeadContext和TailContext外,Pipeline中的handler还包括Bootstrap.handler()中的handler。
对于Server端:
除HeadContext和TailContext外,NioServerSocketChannel对应的Pipeline中的handler包括ServerBootstrap.handler()中的handler和ServerBootstrapAcceptor。 接入新连接后,NioSocketChannel对应的Pipeline中的handler包括ServerBootstrap.childHandler()中的childHandler
如果使用了ChannelInitializer
这个handler的话,由于在它的initChannel()回调方法中又remove了它自己,所以最终对应的handler是initChannel()里的handler。因此ChannelInitializer仅仅起到初始化作用,不参与实际数据处理。
PS: 数据传递过程是一个链式传递
,因为之前稍微看过OkHttp源码,有点类似。
(5)ChannelHandlerContext
unsafe:
NioServerSocketChannel对应NioMessageUnsafe NioSocketChannel对应NioByteUnsafe
HeadContext的工作:
1、通过unsafe处理bind/connect/read/write/flush/deregister/disconnect/close等具体事件
2、链式传递exceptionCaught/channelRegistered/channelUnregistered/channelActive/channelInactive/channelRead/channelReadComplete等事件。
TailContext的工作:
负责userEventTriggered/exceptionCaught/channelRead等一些异常捕获或未处理消息的日志提醒和资源释放工作。
用户自定义的ChannelHandlerContext可以选择是否将消息继续往下传递,默认是传递的,如果不传递将ctx.fireChannelXXX();这行代码覆盖掉即可。比如ServerBootstrapAcceptor在channelRead的时候就没有继续往下传。
传递的参数可以是Channel,比如在Server端监听到OP_ACCEPT事件时,ServerSocketChannel.accept()会得到一个NioSocketChannel,这个连接会放到list,然后经由ServerSocketChannel对应的Pipeline的channelRead递归传给ServerBootstrapAcceptor,NioSocketChannel进行一些初始化处理后再交由child Group进行下一步操作。
我的个人博客地址:https://dlddw.xyz/
所有文章均在个人博客中首发。欢迎小伙伴们访问和留言!