1 netty 是 NIO 的一个封装,把NIO 关于接受请求建立连接,循环处理可以事件,然后请求交给工作线程的过程。我们只需要重点关心工作线程后面的业务逻辑,别的重复逻辑由netty 框架来做了。
2 要连接 netty 之前先要了解NIO的编程模型,NIO 能够一个线程 处理多个请求 BIO 一个请求需要一个线程来处理,但是NIO 只能提高IO 零拷节省下来的时间然后去做别的事情,如果这个线程被业务阻塞,那么它是不能去做别的事情的,是用NIO 不一定能提高程序的执行效率,当主要耗时在业务阻塞而不是IO阻塞的时候,NIO用处就不大了。就如同多线程不一定能提高程序执行的效率一样(当多线程上下文切换带来成本大于多线程带来的收益的时候,多线程就没必要了)。
3 BIO 的编程模型例子:
BioServer
package com.lomi.io.bio;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Bio的 服务器端
*
* @Author ZHANGYUKUN
* @Date 2022/9/24
*/
public class BioServer {
public static void main(String[] args) throws IOException {
//如果把请求线程改成1,超过一个客户端就会等待连接
ExecutorService executorService = Executors.newFixedThreadPool(1);
ServerSocket serverSocket = new ServerSocket(6666);
while(true){
System.out.println("等待客户端连接中.........");
Socket socket = serverSocket.accept();
System.out.println("收到一个客户端连接.........");
executorService.execute( ()->{
System.out.println("处理客户端请求中.........");
InputStream inputStream = null;
try {
socket.getOutputStream().write("你可以给我发信息了。。。。。。。。".getBytes());
inputStream = socket.getInputStream();
while(true){
byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
if( read == -1 ){
System.out.println("程序退出");
break;
}
System.out.println("收到的信息是 :" + new String(bytes,0,read));
String msg = "收到的信息是:" + new String(bytes,0,read);
socket.getOutputStream().write( msg.getBytes() );
}
} catch (Exception e) {
System.out.println("一个客户端退出。。。。。。");
}
System.out.println("处理完成一个客户端连接.........");
} );
}
}
}
BioClient
package com.lomi.io.bio;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Bio的 客户端
*
* @Author ZHANGYUKUN
* @Date 2022/9/24
*/
public class BioClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket();
socket.connect( new InetSocketAddress("127.0.0.1",6666) );
boolean quite = false;
//启动一个线程读取消息
new Thread(()->{
try{
//读取服务器端返回的信息,并且输出
byte[] bytes = new byte[1024];
while (true) {
int read = socket.getInputStream().read(bytes);
System.out.println( new String( bytes,0,read ) );
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
//向服务器端写入数据
Scanner scanner = new Scanner(System.in);
while( !quite ){
String inputString = scanner.nextLine();
if( "quite".equals( inputString ) ){
quite = true;
}
//输出数据
System.out.println( "我发送的信息:"+ inputString );
socket.getOutputStream().write(inputString.getBytes());
}
//退出
socket.close();
}
}
上诉例子客户端和服务器建立连接,然后客户端可以想服务端写入数据,服务器端回复相同的数据。
如果BioServer 只有一个线程池来接受Client 的请求,那么只有第一个客户端可以正常连接发送数据,第二个开始 在服务器端accept 阶段被阻塞,这时候第二个客户端依旧可以想服务器发送数据,只是连接没建立,只有等连接建立后才能得到响应,并且tcp 请求没有明显的边界,第二个阻塞的客户端分别发送了a,b,c,在后面又资源建立的时候被被当成一次请求adc 处理。返回 “收到的信息是: abc”
并且
bio 一个一个请求必须占用一个线程,这里的一个请求必须占用一个线程指的是能正常的并行通讯,如果只用一个线程把完成接受请求,并且读写数据,也是可以做到的,但是这样程序没什么意义,任意阻塞都会导致整个系统的阻塞。
4 NIO编程例子:
服务器端代码:
客户端写入什么就回写什么
package com.lomi.io.nio.chat1;
import org.apache.ibatis.annotations.SelectKey;
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 ZHANGYUKUN
* @Date 2022/9/24
*/
public class NioServerBootStrap {
public static void main(String[] args) throws IOException {
NioServer nioServer = new NioServer(8888);
nioServer.listen();
}
}
class NioServer{
Selector selector;
ServerSocketChannel serverSocketChannel;
public NioServer(int prot) throws IOException {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind( new InetSocketAddress( prot ) );
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
public void listen() throws IOException {
while (true) {
if (selector.select(10000) == 0) {
System.out.println("暂时没有客户端连接");
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> selectionKeysIterator = selectionKeys.iterator();
while (selectionKeysIterator.hasNext()) {
SelectionKey selectionKey = selectionKeysIterator.next();
if ( selectionKey.isAcceptable()) {
System.out.println("isAcceptable");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}else if ( selectionKey.isConnectable() ) {
System.out.println("isConnectable");
}else if ( selectionKey.isReadable()) {
System.out.println("isReadable");
try{
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketChannel channel = (SocketChannel) selectionKey.channel();
int readCount = channel.read(byteBuffer);
if( readCount != -1 ){
byte[] content = new byte[byteBuffer.position()];
byteBuffer.flip();
for(int i = 0; byteBuffer.hasRemaining() ;i++){
content[i] = byteBuffer.get();
}
String msg = "收到的消息是:" + new String( content )+":end";
System.out.println(msg);
//回写客户端数据
channel.write( ByteBuffer.wrap( msg.getBytes() ) );
System.out.println("回写数据完成..........");
}else{
System.out.println("一个客户端退出");
channel.close();
}
}catch (Exception e){
e.printStackTrace();
SocketChannel channel = (SocketChannel) selectionKey.channel();
channel.close();
}
}else if ( selectionKey.isWritable()) {
System.out.println("isWritable");
}
selectionKeysIterator.remove();
}
}
}
}
客户端代码:
一个线程不定的读取服务器端数据,并且打印,另外一个线程阻塞等待输入,并且传递给服务器
package com.lomi.io.nio.chat1;
import io.netty.channel.nio.NioEventLoopGroup;
import javax.sound.midi.Soundbank;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
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;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 客户端
*
* @Author ZHANGYUKUN
* @Date 2022/9/24
*/
public class NioClinetBootStrap {
public static void main(String[] args) throws IOException, InterruptedException {
NioClinet nioClinet = new NioClinet("127.0.0.1",8888 );
nioClinet.listen();
}
}
class NioClinet{
SocketChannel socketChannel;
Selector selector;
Boolean stop = false;
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(128);
public NioClinet(String hostName,int prot) throws IOException {
selector = Selector.open();
socketChannel = SocketChannel.open( new InetSocketAddress(hostName,prot) );
/*SocketChannel socketChannel = SocketChannel.open();
if( !socketChannel.connect(inetSocketAddress) ){
System.out.println("客户端连接中........");
}*/
socketChannel.configureBlocking(false);
socketChannel.register( selector, SelectionKey.OP_CONNECT);
socketChannel.register( selector, SelectionKey.OP_READ);
//socketChannel.register( selector, SelectionKey.OP_WRITE);
//获取输入
new Thread(()->{
Scanner scanner = new Scanner(System.in);
while(true){
try {
String inputString = scanner.nextLine();
//键入quite关闭
if( "quite".equals( inputString ) ){
stop = true;
socketChannel.close();
return;
}
socketChannel.write( ByteBuffer.wrap( inputString.getBytes() ) );
} catch ( IOException e) {
e.printStackTrace();
}
}
}).start();
}
public void listen() throws IOException, InterruptedException {
while (!stop) {
if (selector.select() == 0) {
System.out.println("没有可读消息");
Thread.sleep(100);
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> selectionKeysIterator = selectionKeys.iterator();
System.out.println( "selectionKeys:" + selectionKeys.size() );
while (selectionKeysIterator.hasNext()) {
SelectionKey selectionKey = selectionKeysIterator.next();
if ( selectionKey.isAcceptable()) {
System.out.println("isAcceptable");
}else if ( selectionKey.isConnectable() ) {
System.out.println("isConnectable");
}else if ( selectionKey.isReadable()) {
try{
System.out.println("isReadable");
ByteBuffer bf = ByteBuffer.allocate(1024);
int readLen = ((SocketChannel)selectionKey.channel()).read(bf);
System.out.println("服务器返回的消息长度:" + readLen );
System.out.println("服务器返回的消息:" + new String(bf.array()) );
}catch (Exception e){
e.printStackTrace();
selectionKey.channel().close();
}
}else if ( selectionKey.isWritable()) {
System.out.println("isWritable");
}
selectionKeysIterator.remove();
}
}
}
}
5 netty 封装课NIO编程模型,并且提供了多个主线程(boss 线程或者说接受请求的线程),多子线程(work 线程,或者处理读写时间的线程 )的编程模型。并且由Handler 来处理读写消息和业务逻辑。
6 netty编程模型例子:
7 eventLoop 是和当前 EventLoop 相关的 一个任务线程池,或者是一个任务池,在这里面允许的任务都会使用 EventLoop 的线程来异步的执行,可以通过handler的上下文来获当前的eventLoop
//使用eventLoop的线程,执行异步任务
ctx.channel().eventLoop().execute( ()->{
System.out.println("异步任务执行了");
} );
//定时执行异步任务
ctx.channel().eventLoop().schedule(()->{
System.out.println("异步任务执行了");
} ,10, TimeUnit.SECONDS);
eventLoop 的继承图,上层的 EventExecutorGroup 继承的java 的 ScheduledExecutorService,再上层是 ExecutorService 线程池对象
8 因为 eventLoop 依旧占用的 work 线程的资源,我们可以考虑建立业务线程池,避免业务逻辑阻塞work线程的分配读写
可以在添加handler 的时候指定 业务线程组,或者在handler 内部考虑使用自定义线程池执行 业务代码。
channel.pipeline().addLast(EventExecutorGroup var1, ChannelHandler... var2)
9 netty 的监听器,可以监听 连接服务器状态的
ChannelFuture channelFuture = serverBootstrap.bind(9999);
channelFuture.addListener(new MyServerGenericFutureListener());
/**
* future监听器
*
* @Author ZHANGYUKUN
* @Date 2022/9/27
*/
public class MyServerGenericFutureListener implements GenericFutureListener<ChannelFuture> {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
System.out.println("MyGenericFutureListener:" + future );
if( future.isDone() ){
System.out.println( "isDone" );
}else if( future.isSuccess() ){
System.out.println( "isSuccess" );
}else if( future.isCancellable() ){
System.out.println( "isCancellable" );
}else if( future.isCancelled() ){
System.out.println( "isCancelled" );
}
}
}
10 netty 的 pipeline
pileline 是 netty 里面的 handler 链的封装结构,一个请求过来accpet 以后得到一个SocketChanel 并且被封装成 NioSocketChanel ,一个 NioSocketChanel 对应一个 pipeline,
pipeline 里面放的有一个链表装着 被ChannelHandlerContext包裹的Handler,pipeline里面的 handler 有2种类型一种是入栈(InboundHandler),一种是出站(outoundHandler),读取数据叫做入栈,回写数据叫做出站。
一个Handler可以即是出站Handler又是入站Handler。在入站过程中,只有入站Handler 会被执行,从head 节点开始遍历,出站的时候只有出站Handler会被执行,从tail节点开始遍历。
11 netty 的心跳机制
netty 的心跳分成三种,分别对应读超时,写超时,和读写超时,指定时间没有发生对应的操作的时候触发
IdleStateHandler,有三个参数,对应 三种超时,当这个心跳事件发生以后,会被下一个handler的userEventTriggered方法感知
添加一个心跳的handler: channel.pipeline().addLast(new IdleStateHandler(5,5,8));
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
System.out.println("userEventTriggered");
if( evt instanceof IdleStateEvent){
IdleStateEvent idleStateEvent = (IdleStateEvent)evt;
if( IdleState.READER_IDLE.equals( idleStateEvent.state() ) ){
System.out.println("读超时");
}
if( IdleState.WRITER_IDLE.equals( idleStateEvent.state() ) ){
System.out.println("写读超时");
}
if( IdleState.ALL_IDLE.equals( idleStateEvent.state() ) ){
System.out.println("读或者写超时");
}
}
}
IdleState 枚举:
package io.netty.handler.timeout;
public enum IdleState {
READER_IDLE,
WRITER_IDLE,
ALL_IDLE;
private IdleState() {
}
}
12 netty 的编码器和解码器
请求入栈的时候一般需要解码器把网络传过来的二进制数据内容转成我们需要的对象,出站的时候需要把返回对象装换成io流里里面二进制数据。
如果只是简单的传输String类型数据,我们可以使用 StringDecoder和StringEncoder。编码器很多,对应不同情况,比如HttpDecoder和 HttpEncoder,ProtoBuff的编码器和解码器等等。
18 netty 是长链接的tcp 协议,所以没有明确的终止符号,自定义码编码和解码器的时候,可以在前面4个字节写入后面内容的长度,然后再写入对应的数据,读取数据的时候先读取4字节的len字段,然后读取对应len长度的字节的数据(http协议也是建立在tcp 协议上的,并且总是带有数据的长度)。如果不确定长度,那么会出现粘包和拆包问题(几次写入的数据合并位一次接收,或者一次写入的数据被分成几次读取)。
19 Protobuf 一种google 的协议,可以实现跨平台,跨语言的消息内容装换。
通过指定语法描述消息体结构,然后使用google的 protoBuf 工具编译成 Java的 类文件(也可以编译成其他语言的类文件),然后不同语言用这个消息编译成的类文件就能按照google定义的规范来统一的解析这个消息(个人觉得还是用json 类型比较通用容易)。
20 netty 使用 ProtoBuf的例子
21 netty 做 httpServer 的例子
22 netty 实现 webSocket 长连接的例子
23 netty 里面 Unpooled 有关于ByteBuf的封装,返回一个 ByteBuf,
ByteBuf 是一个类型 ByteBuffer的一个东西,通过readerIndex,writerIndex 来简化了 ByteBuffer 需要倒装才能切换读写的用法。
0到 readerIndex 的位置是可以读的区间,readerIndex 不能大于 writerIndex。
readerIndex 到 writerIndex 是未读的区间。
writerIndex 到 capacity 是可以写的区间。
netty 里面 通过 Unpool可以很方便的创建一个 ByteBuf
ByteBuf byteBuf = Unpooled.copiedBuffer("服务器返回的数据".getBytes());
24 ByteBuffer 的使用和原理
25 NioEventLoopGroup 介绍
NioEventLoopGroup 封装了一堆线程组,里面 有多个 EventLoop,一个 EventLoop 对应一个线程。
boss 线程中这个线程可以处理 请求接受,轮询可用的accpet事件,建立连接,然后把连接交给 work 线程组。
work 线程中,这个线程处理 轮询其他事件,读写IO,然后把连接交处理业务或者把业务交给业务线程。
26 serverBootstrap 说明
serverBootstrap管理者 boss 和 work 线程组,serverBootstrap.option(...),serverBootstrap.childOption(...) 分别是个boss 线程设置参数和个work 线程设置参数。(没有child 开头的方法都是设置 boss 线程也有人叫做master线程,child 开头都是设置work 线程,也有人叫做从线程)