文章目录
前言
在了解BIO和NIO后开始初探Netty的实现;Netty是基于NIO实现的高性能服务器,在多路复用器的基础上能够提供多client的快速响应。
Netty 是基于 Java NIO 的异步事件驱动的网络应用框架,使用 Netty 可以快速开发网络应用,Netty 提供了高层次的抽象来简化 TCP 和 UDP 服务器的编程
一、多线程下的多路复用
1.1 基础代码
场景描述
在server 和 client的链接当中,可以存在多个selector,对于每一个client或者server需要考虑将事件注册到哪个selector中
为什么使用多线程?
即使是NIO的场景,在selecotr()阻塞结束后的其他操作也都是顺序执行的,因此一旦某一次链接中执行的任务量过多导致长时间无法处理结束,那么整个selector便会受到影响,其他链接的消息也无法正常执行。
代码实现
既然是多线程环境,需要多个selector,因此使用Runnable创建线程
public class SelectorThread implements Runnable{
// 多路复用的线程
Selector selector = null;
SelectorThread() {
try {
selector = Selector.open(); // 初始化一个selector
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
while (true) {
int num = selector.select(); // 阻塞, 等待绑定事件
if (num > 0) { // 处理事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) { // 线性处理
SelectionKey key = iter.next();
iter.remove(); // 删除监听
if (key.isAcceptable()) { // 多线程环境clien注册到哪个客户端
handleAcceptable(key);
} else if (key.isReadable()) {
handleReadable(key);
}
}
}
// 可以处理其他task
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void handleReadable(SelectionKey key) {
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel client = (SocketChannel) key.channel(); // 获取客户端的channel
// 读数据
buffer.clear();
while (true) {
try {
int num = client.read(buffer);
if (num > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
buffer.clear();
} else if (num == 0) {
break;
} else {
System.out.println(client.getRemoteAddress() + " 链接关闭了");
key.cancel(); // 取消事件
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleAcceptable(SelectionKey key) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
try {
SocketChannel client = server.accept();
client.configureBlocking(false); // 配置非阻塞
// chooose a select and register
} catch (IOException e) {
e.printStackTrace();
}
}
}
考虑:想要拥有一个多线程版本的selector集合,使用多线程创建即可;但是多路复用中涉及事件的触发,需要绑定到一个selector等其监听,因此对于每一个连接下的fd我们应该绑定到哪个selector上呢?
我们需要一个集合用来管理创建的selectors,可以通过轮询的方式为每一个fd分配selector
public class ThreadGroup {
SelectorThread[] selectorThreads = null; // selector的集合
AtomicInteger ac = new AtomicInteger(); // 用来取模获取集合中的具体下标
ThreadGroup(int num) {
selectorThreads = new SelectorThread[num];
for (int i = 0; i < num; i++) {
selectorThreads[i] = new SelectorThread(); // 创建新的线程
new Thread(selectorThreads[i]).start(); // 启动线程
}
}
public SelectorThread next() { // 选择一个selector
// 分配一个Selector
int index = ac.incrementAndGet() % selectorThreads.length;
return selectorThreads[index];
}
public void bind(int port) { // 用于主线程中绑定端口
ServerSocketChannel server = null;
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port)); // 绑定一个server端口
System.out.println("server already run ...");
// 分配server一个selector进行监听
nextSelector(server);
} catch (IOException e) {
e.printStackTrace();
}
}
// 由于会为client, server都会分配selector,因此这里可以使用多态,父类:Channel
private void nextSelector(Channel channel) {
SelectorThread selectorThread = next(); // 分配server一个selector
if (channel instanceof ServerSocketChannel) { // 此时为serverSocketChannel,监听Acceptable事件
ServerSocketChannel ss = (ServerSocketChannel) channel;
try {
selectorThread.selector.wakeup(); // 唤醒selector,避免阻塞
ss.register(selectorThread.selector, SelectionKey.OP_ACCEPT); // 为选定的selector上面注册接受链接的事件
System.out.println("事件绑定结束");
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
}
}
创建一个server用来等待链接请求, 主进程
public static void main(String[] args) {
// 主线程,用来生产SelectorThread
ThreadGroup group = new ThreadGroup(1); // 定义一个容量num的线程组
group.bind(9999);
}
以上就是多路复用的代码结构
代码中wakeup的作用:当创建一个SelectorThread,在run()中会走到select()方法进行阻塞,只有当事件的数据准备完毕才会继续往后走。
不同于单线程版本,我们需要在选择一个selector后才会去考虑注册事件的操作,因此需要想办法先打破阻塞状态,然后为选定的selector绑定一个Acceptable事件。
selectorThread.selector.wakeup(); // 唤醒selector,避免阻塞
SelectorThread中的run()方法, 这里使用比较low的方式,用Thread.sleep的方式保证事件一定注册成功
while (true) {
System.out.println(Thread.currentThread().getName() + " before connect ..." + selector.keys().size());
int num = selector.select(); // 阻塞, 等待绑定事件
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " after connect ..." + selector.keys().size());
if (num > 0) { // 处理事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) { // 线性处理
SelectionKey key = iter.next();
iter.remove(); // 删除监听
if (key.isAcceptable()) { // 多线程环境clien注册到哪个客户端
handleAcceptable(key);
} else if (key.isReadable()) {
handleReadable(key);
}
}
}
// 可以处理其他task
}
运行结果
这里使用Linux命令模拟客户端连接:
// 如果nc没有找到,先使用yum进行安装
yum install nc
// 模拟客户端
nc ip port // 如: nc 192.168.xxx.xxx 9999
分割线,明天继续
1.2 划分角色的多路复用
场景 上述的Group角色划分不明确,每一个Group都可以接受Acceptable或读写操作,比较混杂;现在想分为Boss和Worker,Boss名下会有Worker,连接到Boss下的读写操作将交付给Worker处理
代码实现:
首先会为每一个ThreadGroup定义角色,初始每一个ThreadGroup都标识Boss;
增加的代码:next1(), nextSelector1()
public class ThreadGroup {
SelectorThread[] selectorThreads = null;
AtomicInteger ac = new AtomicInteger();
ThreadGroup worker = this; // 工作的线程组
ThreadGroup(int num) {
selectorThreads = new SelectorThread[num];
for (int i = 0; i < num; i++) {
selectorThreads[i] = new SelectorThread(this); // 创建新的线程
new Thread(selectorThreads[i]).start(); // 启动线程
}
}
public SelectorThread next() {
// 分配一个Selector
int index = ac.incrementAndGet() % selectorThreads.length;
return selectorThreads[index];
}
// 由工作组进行分配进程,当处理的Channel为SocketChannel的时候由worker处理
public SelectorThread next1() {
// 分配一个Selector
int index = worker.ac.incrementAndGet() % worker.selectorThreads.length;
return worker.selectorThreads[index];
}
public void bind(int port) {
ServerSocketChannel server = null;
try {
server = ServerSocketChannel.open();
server.configureBlocking(false); // 非阻塞
server.bind(new InetSocketAddress(port));
System.out.println("server already run ..."); // boss负责连接
// 分配server一个selector进行监听
nextSelector1(server);
} catch (IOException e) {
e.printStackTrace();
}
}
// 由于会为client, server都会分配selector,因此这里可以使用多态,父类:Channel
public void nextSelector1(Channel channel) {
if (channel instanceof ServerSocketChannel) {
SelectorThread st = this.next();
st.setWorker(worker);
st.lbq.add(channel);
st.selector.wakeup();
} else if (channel instanceof SocketChannel) {
SelectorThread st = next1();
st.lbq.add(channel);
st.selector.wakeup();
}
}
public void setWorker(ThreadGroup worker) {
this.worker = worker;
}
}
SelectorThread中的修改
在设计SelectorThread的时候,对于读事件的绑定会在监听到Acceptable的时候触发,因此需要重新定义一个成员变量ThearGroup,在这个里面指定selector
public class SelectorThread implements Runnable{
// 多路复用的线程
Selector selector = null;
LinkedBlockingQueue<Channel> lbq = new LinkedBlockingQueue<>();
ThreadGroup threadGroup = null;
SelectorThread(ThreadGroup threadGroup) {
try {
this.threadGroup = threadGroup;
selector = Selector.open(); // 初始化一个selector
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
// 。。。
}
private void handleReadable(SelectionKey key) {
// 。。。。
}
private void handleAcceptable(SelectionKey key) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
try {
SocketChannel client = server.accept();
client.configureBlocking(false); // 配置非阻塞
// chooose a select and register
// 给客户端绑定一个selector
this.threadGroup.nextSelector(client); // 这里根据ThreadGroup进行划分
} catch (IOException e) {
e.printStackTrace();
}
}
public void setWorker(ThreadGroup worker) {
this.threadGroup = worker;
}
}
二、Netty的API使用【模拟S/C】
2.1 ByteBuf
相比于NIO中的ByteBuffer,Netty的ByteBuf的读写操纵更加方便,不需要进行指针的filp(), 使用单独的方法便可以直接操作。
使用默认分配的时候需要指定初始大小和最大上限;当初始大小满的时候会进行二倍扩容,直到达到最大上限。
@Test
public void testByteBuffer() {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(8, 20);
print(buffer);
buffer.writeBytes(new byte[] {1, 2, 3, 4});
print(buffer);
buffer.writeBytes(new byte[] {1, 2, 3, 4});
print(buffer);
buffer.writeBytes(new byte[] {1, 2, 3, 4});
print(buffer);
buffer.writeBytes(new byte[] {1, 2, 3, 4});
print(buffer);
buffer.writeBytes(new byte[] {1, 2, 3, 4});
print(buffer);
}
public void print(ByteBuf buffer) {
System.out.println("buffer.isReadable():" + buffer.isReadable());
System.out.println("buffer.readerIndex():" + buffer.readerIndex());
System.out.println("buffer.readableBytes()" + buffer.readableBytes());
System.out.println("buffer.isWritable():" + buffer.isWritable());
System.out.println("buffer.writerIndex():" + buffer.writerIndex());
System.out.println("buffer.capacity():" + buffer.capacity());
System.out.println("buffer.maxCapacity():" + buffer.maxCapacity());
System.out.println("buffer.isDirect()" + buffer.isDirect());
System.out.println("-------------------------------------");
}
2.2 模拟Netty客户端
public void testNettyClient() {
// 创建一个SocketChannel
NioSocketChannel client = new NioSocketChannel();
// 创建一个连接
client.connect(new InetSocketAddress("192.168.189.130", 9090));
// 生成一个指定字符串大小的buffer
ByteBuf buffer = Unpooled.copiedBuffer("hello world".getBytes(StandardCharsets.UTF_8));
client.writeAndFlush(buffer);
}
netty中的消息处理属于异步通信,因此为了保证连接,消息到成功处理,需要在某些地方等待数据回馈,即阻塞进程。
@Test
public void testNettyClient() throws InterruptedException {
// 创建一个事件循环
NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
// 创建一个SocketChannel
NioSocketChannel client = new NioSocketChannel();
// 在事件循环器上注册channel
eventExecutors.register(client);
// 绑定一个异步事件处理器,可以反馈服务端的响应消息
ChannelPipeline pipeline = client.pipeline();
// Inserts {@link ChannelHandler}s at the last position of this pipeline.
pipeline.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf info = (ByteBuf) msg;
// netty中没有NIO中的flip的方法,使用readable会把指针搜索,这样在没有翻转后的写操作便无法写入
// CharSequence charSequence = info.readCharSequence(info.readableBytes(), StandardCharsets.UTF_8);
CharSequence charSequence = info.getCharSequence(0, info.readableBytes(), StandardCharsets.UTF_8);
System.out.println("server:" + charSequence);
ByteBuf buffer = Unpooled.copiedBuffer("pong".getBytes(StandardCharsets.UTF_8));
ctx.writeAndFlush(buffer);
}
});
// 创建一个连接
ChannelFuture connect = client.connect(new InetSocketAddress("192.168.189.130", 9090));
// 阻塞等待异步连接建立
connect.sync();
System.out.println("connect already is created");
// 生成一个指定字符串大小的buffer
ByteBuf buffer = Unpooled.copiedBuffer("hello world".getBytes(StandardCharsets.UTF_8));
ChannelFuture channelFuture = client.writeAndFlush(buffer);
// 阻塞等待消息发送
channelFuture.sync();
System.out.println("infomation was send");
connect.channel().closeFuture().sync(); // 可以在发送消息后处理一些其他操作,阻塞然后去执行
System.out.println("client closed");
}
2.3 模拟Netty服务端
场景:模拟Netty的服务端接受,注意,服务端应该适用多连接
基础代码
@Test
public void testNettyServer() throws InterruptedException {
// 4. 发生报错,没有event loop
NioEventLoopGroup eventExecutors = new NioEventLoopGroup(1);
// 1.创建一个serverSocket
NioServerSocketChannel server = new NioServerSocketChannel();
// 5. 将server注册到eventExecutor上,分配一个线程处理
eventExecutors.register(server);
// 7. 定义一个异步处理器
ChannelPipeline pipeline = server.pipeline();
pipeline.addLast(new ServerInHandler(eventExecutors));
// 2.绑定一个端口
ChannelFuture connect = server.bind(new InetSocketAddress(9090));
// 3. 等待连接建立
connect.sync();
System.out.println("server connect");
// 6. 响应式的可以阻塞在这里,然后server可以通过popline()处理一些其他操作,比如通信
// 当客户端发送消息的时候,应该像写客户端的时候配置一个pipline(),定义一个处理器
connect.channel().closeFuture().sync();
}
自定义类
class ServerInHandler extends ChannelInboundHandlerAdapter {
private final NioEventLoopGroup evenExecutors;
public ServerInHandler(NioEventLoopGroup eventExecutors) {
this.evenExecutors = eventExecutors;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 8. 当一个连接建立的时候,客户端发送过来的msg应该是他自己的Channel,可以从这里面获取信息
/*
之前写NIO的时候,写过:SocketChannel scChnnel = ssChannel.accept(); 用于接受客户端的连接
那么在netty的时候,什么时候处理的accept, 非阻塞怎么设置的
*/
System.out.println("channelRead ...");
SocketChannel client = (SocketChannel) msg;
// 9. 模仿客户端的写法,在获取client的channel后,我们应该为这个连接添加一些处理器
/*
写完这步之后,发现连接建立后,客户端不能正常通信?
问题:
1. 我是用eventExecutor给server注册了accept事件,能够正常监听;到那时我在handler中获取的client是
哪个selector监听的 -》 没有注册
2. 然后我们目前注册了一个eventExecutor,所以应该成员变量获取
*/
// 10. 给client注册一个selector
this.evenExecutors.register(client);
ChannelPipeline pipeline = client.pipeline();
pipeline.addLast(new MyInHandler());
}
}
class MyInHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf info = (ByteBuf) msg;
/* netty中没有NIO中的flip的方法,使用readable会把指针搜索,这样在没有翻转后的写操作便无法写入
CharSequence charSequence = info.readCharSequence(info.readableBytes(), StandardCharsets.UTF_8);
*/
CharSequence charSequence = info.getCharSequence(0, info.readableBytes(), StandardCharsets.UTF_8);
System.out.println("server:" + charSequence);
// ByteBuf buffer = Unpooled.copiedBuffer("pong".getBytes(StandardCharsets.UTF_8));
ctx.writeAndFlush(info);
}
}
错误模拟:实现MyInHandler的共享错误问题,将MyInHandler的创建通过传参决定
class ServerInHandler extends ChannelInboundHandlerAdapter {
private final NioEventLoopGroup evenExecutors;
private final ChannelHandler handler;
public ServerInHandler(NioEventLoopGroup eventExecutors, MyInHandler myInHandler) {
this.evenExecutors = eventExecutors;
this.handler = myInHandler;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 8. 当一个连接建立的时候,客户端发送过来的msg应该是他自己的Channel,可以从这里面获取信息
/*
之前写NIO的时候,写过:SocketChannel scChnnel = ssChannel.accept(); 用于接受客户端的连接
那么在netty的时候,什么时候处理的accept, 非阻塞怎么设置的
*/
System.out.println("channelRead ...");
SocketChannel client = (SocketChannel) msg;
// 9. 模仿客户端的写法,在获取client的channel后,我们应该为这个连接添加一些处理器
/*
写完这步之后,发现连接建立后,客户端不能正常通信?
问题:
1. 我是用eventExecutor给server注册了accept事件,能够正常监听;到那时我在handler中获取的client是
哪个selector监听的 -》 没有注册
2. 然后我们目前注册了一个eventExecutor,所以应该成员变量获取
*/
// 10. 给client注册一个selector
this.evenExecutors.register(client);
ChannelPipeline pipeline = client.pipeline();
pipeline.addLast(handler);
super.channelRead(ctx, msg);
}
}
思考:MyInHandler是用来处理读写操作的,那一定是用户自定义的,当业务场景复杂的时候,我可以在读写操作中添加一些计数器等成员变量,但是这玩意为什么能被共享呢? 并且我总不能要求用户定义的时候不准定义变量吧。。。
所以呢,我们可以把他进一步封装,像我最开始没有演示出错误那样,由用户自定义才好;但是我上面自定义的时候是写到 ServerInHandler 里了,因此有了下面的逻辑
@ChannelHandler.Sharable
class ChannelInit extends ChannelInboundHandlerAdapter {
private final NioEventLoopGroup evenExecutors;
public ChannelInit(NioEventLoopGroup eventExecutors) {
this.evenExecutors = eventExecutors;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("channelRead ...");
SocketChannel client = (SocketChannel) msg;
this.evenExecutors.register(client);
ChannelPipeline pipeline = client.pipeline();
pipeline.addLast(new MyInHandler());
pipeline.remove(this);
}
}
三、Netty的API使用【Netty的Boostrap】
3.1 客户端实现
可以看前面模拟的客户端,整体思路都是一样的;这里的ChannelInitializer就是上面实现的ChannelInit类,都是起到中间者的作用,避免共享hanler问题
/**
* Netty API情况下的client实现
*/
@Test
public void testNettyClient() throws InterruptedException {
Bootstrap bs = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup(); // 1.创建一个事件循环 event loop
ChannelFuture connect = bs.group(group)
.channel(NioSocketChannel.class) // 指定监听的channel类型,这里是客户端使用SocketChannel
.handler(new ChannelInitializer<SocketChannel>() { // 绑定Handler
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyInHandler());
}
})
.connect(new InetSocketAddress("192.168.189.130", 9090)); // 绑定端口
Channel client = connect.sync().channel();
ByteBuf buffer = Unpooled.copiedBuffer("hello server".getBytes(StandardCharsets.UTF_8));
ChannelFuture channelFuture = client.writeAndFlush(buffer);
channelFuture.sync();
connect.channel().closeFuture().sync();
}
class MyInHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf info = (ByteBuf) msg;
CharSequence charSequence = info.getCharSequence(0, info.readableBytes(), StandardCharsets.UTF_8);
System.out.println("server:" + charSequence);
ctx.writeAndFlush(info);
}
}
3.2 服务端实现
@Test
public void testNettyServer() throws InterruptedException {
ServerBootstrap bs = new ServerBootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
ChannelFuture connect = bs.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
System.out.println(ch.remoteAddress().getAddress() + ":" + ch.remoteAddress().getPort() + " connect...");
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyInHandler());
}
})
.bind(new InetSocketAddress("192.168.189.1", 9090));
ChannelFuture sync = connect.sync();
System.out.println("connect already created");
connect.channel().closeFuture().sync();
}