io_Nio_netty核心知识点和面试笔记
IO流
二:File类
java.io.File类:文件和目录路径名的抽象表示形式,与平台无关。File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。File对象可以作为参数传递给流的构造函数。
File类的常见构造方法:
public File(String pathname)
以pathname为路径创建File对象,可以是绝对路径或者相对路径,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储(可以通过system.getproperty("user.dir")查看)。
public File(String parent,String child)
以parent为父路径,child为子路径创建File对象
字符流以字符为单位,字节流以byte为单位
因此BufferedReader和BufferedWriter可以包装在FileReader和FileWriter上加快文件读写速度。
3.1 输入输出流的基类InputStream和Reader,OutputStram和Writer
package IO; import org.junit.Test; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class TestInputOutput { @Test public void test1() {//读取文件,并把结果输出到控制台上面 FileInputStream fis = null; try { fis = new FileInputStream(new File("src/IO/config.txt")); byte[] b = new byte[5]; int len; while ((len = fis.read(b)) != -1) { System.out.print(new String(b, 0, len)); } } catch (IOException e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void test2() { // 1.创建一个File对象,表明要写入的文件位置。 // 输出的物理文件可以不存在,当执行过程中,若不存在,会自动的创建。若存在,会将原有的文件覆盖 FileOutputStream fos = null; try { fos = new FileOutputStream(new File("src/IO/b.txt")); fos.write("你好,我在发消息".getBytes()); } catch (IOException e) { e.printStackTrace(); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Test public void test3(){// 从硬盘读取一个文件,并写入到另一个位置。(相当于文件的复制) FileInputStream fis = null; FileOutputStream fos = null; long start = System.currentTimeMillis(); try { fis = new FileInputStream(new File("a.jpg")); fos = new FileOutputStream(new File("a1.jpg")); byte[] b = new byte[1000];//会发现数组的大小影响文件写出的效率,文件太小多次写入浪费时间,太大则浪费空间。 int len; while ((len = fis.read(b))!=-1){ fos.write(b,0,len);//注意使用len而不是b.length的原因是防止在后一次循环若数组为读满导致多余的数据被写入的错误 } } catch (IOException e) { e.printStackTrace(); } finally { if(fos!=null){ try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } if(fis!=null){ try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } long end = System.currentTimeMillis(); System.out.println("时间共花了"+(end-start)); } }
3.5转换流
3.7 打印流
@Test
public void test3() throws IOException {
FileOutputStream fos = new FileOutputStream(new File("b.txt"));
PrintStream printStream = new PrintStream(fos);
if(printStream != null){
System.setOut(printStream);//改变流的方向
}
System.out.println("这句话将输入到b.txt文件而不是控制台");
printStream.close();
InputStream inputStream = new FileInputStream("b.txt");
System.setIn(inputStream);//输入被调整为从文件里面输入
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
System.out.println(scanner.nextLine());
}
inputStream.close();
}
3.8数据流
@Test
public void test4() throws Exception {//数据流:用来处理基本数据类型、String、字节数组的数据:DataInputStream DataOutputStream
DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream("data.txt"));
dataOutputStream.writeUTF("我爱你,而你不知道");
dataOutputStream.writeBoolean(true);
dataOutputStream.writeInt(12);
dataOutputStream.close();//会乱码,但是再次读的时候会恢复
DataInputStream dataInputStream = new DataInputStream(new FileInputStream(new File("data.txt")));
// byte[] b =new byte[10];
// int len;
// while((len = dataInputStream.read(b))!=-1){
// System.out.println(new String(b,0,len));
// }
System.out.println(dataInputStream.readUTF());
System.out.println(dataInputStream.readBoolean());
System.out.println(dataInputStream.readInt());
dataInputStream.close();
}
3.9对象流
package IO; import org.junit.Test; import java.io.*; public class TestObjectInputOutputStream { @Test public void test() throws IOException {// 对象的序列化过程:将内存中的对象通过ObjectOutputStream转换为二进制流,存储在硬盘文件中 Person person = new Person("花花", 12, new Pet("哈哈")); Person person1 = new Person("瓜瓜", 22, new Pet("大大")); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt")); oos.writeObject(person); oos.flush(); oos.writeObject(person1); oos.flush(); oos.close(); } @Test public void test1() throws IOException, ClassNotFoundException {// // 对象的反序列化过程:将硬盘中的文件通过ObjectInputStream转换为相应的对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt")); Person person = (Person) ois.readObject(); Person person1 = (Person) ois.readObject(); System.out.println(person); System.out.println(person1); } } /* * 要实现序列化的类: 1.要求此类是可序列化的:实现Serializable接口 * 2.要求类的属性同样的要实现Serializable接口 * 3.提供一个版本号:private static final long serialVersionUID * 4.使用static或transient修饰的属性,不可实现序列化 */ class Person implements Serializable{ private static final long serialVersionUID = 23456789L; String name; Integer age; Pet pet; public Person(String name, Integer age,Pet pet) { this.name = name; this.age = age; this.pet = pet; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", pet=" + pet + '}'; } } class Pet implements Serializable{ String name; public Pet(String name) { this.name = name; } @Override public String toString() { return "Pet{" + "name='" + name + '\'' + '}'; } }
transient是 Java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程
3.10 randomAccessFile类
package IO; import org.junit.Test; import java.io.File; import java.io.RandomAccessFile; public class TestRandomAccessFile { @Test//RandomAccessFile进行文件的读写 public void test1() throws Exception { RandomAccessFile randomAccessFile = new RandomAccessFile(new File("src/IO/a.txt"),"r"); RandomAccessFile randomAccessFile1 = new RandomAccessFile(new File("src/IO/aa.txt"),"rw"); int len; byte[] b = new byte[10]; while ((len = randomAccessFile.read(b))!=-1){ randomAccessFile1.write(b,0,len); } randomAccessFile1.close(); randomAccessFile.close(); } @Test//实现的实际上是覆盖的效果 public void test2() throws Exception { RandomAccessFile randomAccessFile1 = new RandomAccessFile(new File("src/IO/aa.txt"),"rw"); randomAccessFile1.seek(4); randomAccessFile1.write("xy".getBytes());//在第四个字符出写入xy,实则是覆盖,注意汉字一个字占两个字符,如26个字符覆盖后abcdxyghijklmnopqrstuvwxyz randomAccessFile1.close(); } @Test//处理覆盖 public void test3() throws Exception { RandomAccessFile randomAccessFile1 = new RandomAccessFile(new File("src/IO/aa.txt"),"rw"); randomAccessFile1.seek(4); String str = randomAccessFile1.readLine();//弊端是只能处理一行 randomAccessFile1.seek(4);//指针已经移动,需要调整 randomAccessFile1.write("xy".getBytes()); randomAccessFile1.write(str.getBytes());//拿出来在追加上 randomAccessFile1.close();//abcdxyefghijklmnopqrstuvwxyz } @Test//更通用 public void test4() throws Exception { RandomAccessFile randomAccessFile1 = new RandomAccessFile(new File("src/IO/aa.txt"),"rw"); randomAccessFile1.seek(4); byte[] b = new byte[10]; int len; StringBuffer sb = new StringBuffer(); while((len = randomAccessFile1.read(b)) != -1){ sb.append(new String(b,0,len)); } randomAccessFile1.seek(4);//指针已经移动,需要调整 randomAccessFile1.write("xy".getBytes()); randomAccessFile1.write(sb.toString().getBytes());//拿出来在追加上 randomAccessFile1.close();//abcdxyefghijklmnopqrstuvwxyz } }
BIO-NIO-AIO
-=
Netty
Netty - 你的雷哥 - 博客园 (cnblogs.com)
面试题
java Nio
首先我们分别画图来看看,BIO、NIO、AIO,分别是什么?
BIO:传统的网络通讯模型,就是BIO,同步阻塞IO
它其实就是服务端创建一个ServerSocket, 然后就是客户端用一个Socket去连接服务端的那个ServerSocket, ServerSocket接收到了一个的连接请求就创建一个Socket和一个线程去跟那个Socket进行通讯。
接着客户端和服务端就进行阻塞式的通信,客户端发送一个请求,服务端Socket进行处理后返回响应。
在响应返回前,客户端那边就阻塞等待,上门事情也做不了。
这种方式的缺点:每次一个客户端接入,都需要在服务端创建一个线程来服务这个客户端
这样大量客户端来的时候,就会造成服务端的线程数量可能达到了几千甚至几万,这样就可能会造成服务端过载过高,最后崩溃死掉。
BIO模型图:
Acceptor:
传统的IO模型的网络服务的设计模式中有俩种比较经典的设计模式:一个是多线程, 一种是依靠线程池来进行处理。
如果是基于多线程的模式来的话,就是这样的模式,这种也是Acceptor线程模型。
NIO:
NIO是一种同步非阻塞IO, 基于Reactor模型来实现的。
其实相当于就是一个线程处理大量的客户端的请求,通过一个线程轮询大量的channel,每次就获取一批有事件的channel,然后对每个请求启动一个线程处理即可。
这里的核心就是非阻塞,就那个selector一个线程就可以不停轮询channel,所有客户端请求都不会阻塞,直接就会进来,大不了就是等待一下排着队而已。
这里面优化BIO的核心就是,一个客户端并不是时时刻刻都有数据进行交互,没有必要死耗着一个线程不放,所以客户端选择了让线程歇一歇,只有客户端有相应的操作的时候才发起通知,创建一个线程来处理请求。
NIO:模型图
Reactor模型:
AIO
AIO:异步非阻塞IO,基于Proactor模型实现。
每个连接发送过来的请求,都会绑定一个Buffer,然后通知操作系统去完成异步的读,这个时间你就可以去做其他的事情
等到操作系统完成读之后,就会调用你的接口,给你操作系统异步读完的数据。这个时候你就可以拿到数据进行处理,将数据往回写
在往回写的过程,同样是给操作系统一个Buffer,让操作系统去完成写,写完了来通知你。
这俩个过程都有buffer存在,数据都是通过buffer来完成读写。
这里面的主要的区别在于将数据写入的缓冲区后,就不去管它,剩下的去交给操作系统去完成。
操作系统写回数据也是一样,写到Buffer里面,写完后通知客户端来进行读取数据。
AIO:模型图
聊完了BIO,NIO,AIO的区别之后,现在我们再结合这三个模型来说下同步和阻塞的一些问题。
同步阻塞
为什么说BIO是同步阻塞的呢?
其实这里说的不是针对网络通讯模型而言,而是针对磁盘文件读写IO操作来说的。
因为用BIO的流读写文件,例如FileInputStrem,是说你发起个IO请求直接hang死,卡在那里,必须等着搞完了这次IO才能返回。
同步非阻塞:
为什么说NIO为啥是同步非阻塞?
因为无论多少客户端都可以接入服务端,客户端接入并不会耗费一个线程,只会创建一个连接然后注册到selector上去,这样你就可以去干其他你想干的其他事情了
一个selector线程不断的轮询所有的socket连接,发现有事件了就通知你,然后你就启动一个线程处理一个请求即可,这个过程的话就是非阻塞的。
但是这个处理的过程中,你还是要先读取数据,处理,再返回的,这是个同步的过程。
异步非阻塞
为什么说AIO是异步非阻塞?
通过AIO发起个文件IO操作之后,你立马就返回可以干别的事儿了,接下来你也不用管了,操作系统自己干完了IO之后,告诉你说ok了
当你基于AIO的api去读写文件时, 当你发起一个请求之后,剩下的事情就是交给了操作系统
当读写完成后, 操作系统会来回调你的接口, 告诉你操作完成
在这期间不需要等待, 也不需要去轮询判断操作系统完成的状态,你可以去干其他的事情。
同步就是自己还得主动去轮询操作系统,异步就是操作系统反过来通知你。所以来说, AIO就是异步非阻塞的。
NIO核心组件详细讲解
学习NIO先来搞清楚一些相关的概念,NIO通讯有哪些相关组件,对应的作用都是什么,之间有哪些联系?
多路复用机制实现Selector
首先我们来了解下传统的Socket网络通讯模型。
传统Socket通讯原理图
为什么传统的socket不支持海量连接?
每次一个客户端接入,都是要在服务端创建一个线程来服务这个客户端的
这会导致大量的客户端的时候,服务端的线程数量可能达到几千甚至几万,几十万,这会导致服务器端程序负载过高,不堪重负,最终系统崩溃死掉。
接着来看下NIO是如何基于Selector实现多路复用机制支持的海量连接。
NIO原理图
多路复用机制是如何支持海量连接?
NIO的线程模型对Socket发起的连接不需要每个都创建一个线程,完全可以使用一个Selector来多路复用监听N多个Channel是否有请求,该请求是对应的连接请求,还是发送数据的请求
这里面是基于操作系统底层的Select通知机制的,一个Selector不断的轮询多个Channel,这样避免了创建多个线程
只有当莫个Channel有对应的请求的时候才会创建线程,可能说1000个请求, 只有100个请求是有数据交互的
这个时候可能server端就提供10个线程就能够处理这些请求。这样的话就可以避免了创建大量的线程。
NIO如何通过Buffer来缓冲数据的
NIO中的Buffer是个什么东西 ?
学习NIO,首当其冲就是要了解所谓的Buffer缓冲区,这个东西是NIO里比较核心的一个部分
一般来说,如果你要通过NIO写数据到文件或者网络,或者是从文件和网络读取数据出来此时就需要通过Buffer缓冲区来进行。Buffer的使用一般有如下几个步骤:
写入数据到Buffer,调用flip()方法,从Buffer中读取数据,调用clear()方法或者compact()方法。
Buffer中对应的Position, Mark, Capacity,Limit都啥?
-
capacity:缓冲区容量的大小,就是里面包含的数据大小。
-
limit:对buffer缓冲区使用的一个限制,从这个index开始就不能读取数据了。
-
position:代表着数组中可以开始读写的index, 不能大于limit。
-
mark:是类似路标的东西,在某个position的时候,设置一下mark,此时就可以设置一个标记
后续调用reset()方法可以把position复位到当时设置的那个mark上。去把position或limit调整为小于mark的值时,就丢弃这个mark
如果使用的是Direct模式创建的Buffer的话,就会减少中间缓冲直接使用DirectorBuffer来进行数据的存储。
如何通过Channel和FileChannel读取Buffer数据写入磁盘的
NIO中,Channel是什么?
Channel是NIO中的数据通道,类似流,但是又有些不同
Channel既可从中读取数据,又可以从写数据到通道中,但是流的读写通常是单向的。
Channel可以异步的读写。Channel中的数据总是要先读到一个Buffer中,或者从缓冲区中将数据写到通道中。
FileChannel的作用是什么?
Buffer有不同的类型,同样Channel也有好几个类型。
-
FileChannel
-
DatagramChannel
-
SocketChannel
-
ServerSocketChannel
这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。而FileChannel就是文件IO对应的管道, 在读取文件的时候会用到这个管道。
下面给一个简单的NIO实现读取文件的Demo代码
1
|
NIOServer端和Client端代码案例 |
最后,给大家一个NIO客户端和服务端示例代码,简单感受下NIO通讯的方式。
Netty面试题
最近经常会再面试中碰到 Netty 相关的问题。
全文采用大家喜欢的与面试官对话的形式展开。 如果大家觉得总结的不错的话,不妨点一个赞鼓励一下!这是我继续坚持很重要的动力来源。
概览:
Netty 是什么?为什么要用 Netty?Netty 应用场景了解么?Netty 核心组件有哪些?分别有什么作用?EventloopGroup 了解么?和 EventLoop 啥关系?Bootstrap 和 ServerBootstrap 了解么?NioEventLoopGroup 默认的构造函数会起多少线程?Netty 线程模型了解么?Netty 服务端和客户端的启动过程了解么?Netty 长连接、心跳机制了解么?Netty 的零拷贝了解么?Netty 是什么?
面试官:介绍一下自己对 Netty 的认识吧!小伙子。
我:好的!那我就简单用 3 点来概括一下 Netty 吧!
Netty 是一个 基于 NIO 的异步非堵塞的基于事件驱动的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。它极大地简化并优化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
支持多种协议 如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。
用官方的总结就是:Netty 成功地找到了一种在不妥协可维护性和性能的情况下实现易于开发,性能,稳定性和灵活性的方法。
除了上面介绍的之外,很多开源项目比如我们常用的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。
网络编程我愿意称 Netty 为王 。
为什么要用 Netty?
面试官:为什么要用 Netty 呢?能不能说一下自己的看法。
我:因为 Netty 具有下面这些优点,并且相比于直接使用 JDK 自带的 NIO 相关的 API 来说更加易用。
解决了nio的epoll空循环导致cpu100%的问题。
面试官:能不能通俗地说一下使用 Netty 可以做什么事情?
我:凭借自己的了解,简单说一下吧!理论上来说,NIO 可以做的事情 ,使用 Netty 都可以做并且更好。Netty 主要用来做网络通信 :
作为 RPC 框架的网络通信工具 :我们在分布式系统中,不同服务节点之间经常需要相互调用,这个时候就需要 RPC 框架了。不同服务节点之间的通信是如何做的呢?可以使用 Netty 来做。比如我调用另外一个节点的方法的话,至少是要让对方知道我调用的是哪个类中的哪个方法以及相关参数吧!
实现一个自己的 HTTP 服务器 :通过 Netty 我们可以自己实现一个简单的 HTTP 服务器,这个大家应该不陌生。说到 HTTP 服务器的话,作为 Java 后端开发,我们一般使用 Tomcat 比较多。一个最基本的 HTTP 服务器可要以处理常见的 HTTP Method 的请求,比如 POST 请求、GET 请求等等。
实现一个即时通讯系统 :使用 Netty 我们可以实现一个可以聊天类似微信的即时通讯系统,这方面的开源项目还蛮多的,可以自行去 Github 找一找。
实现消息推送系统 :市面上有很多消息推送系统都是基于 Netty 来做的。......Netty 核心组件有哪些?分别有什么作用?
面试官:Netty 核心组件有哪些?分别有什么作用?
我:表面上,嘴上开始说起 Netty 的核心组件有哪些,实则,内心已经开始 mmp 了,深度怀疑这面试官是存心搞我啊!
1.Channel
Channel 接口是 Netty 对网络操作抽象类,它除了包括基本的 I/O 操作,如 bind()、connect()、read()、write() 等。
比较常用的Channel接口实现类是NioServerSocketChannel(服务端)和NioSocketChannel(客户端),这两个 Channel 可以和 BIO 编程模型中的ServerSocket以及Socket两个概念对应上。Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。
2.EventLoop
这么说吧!EventLoop(事件循环)接口可以说是 Netty 中最核心的概念了!
《Netty 实战》这本书是这样介绍它的:
“EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。
是不是很难理解?说实话,我学习 Netty 的时候看到这句话是没太能理解的。
说白了,EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
那 Channel 和 EventLoop 直接有啥联系呢?
Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的Channel 处理 I/O 操作,两者配合参与 I/O 操作。
3.ChannelFuture
Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。
因此,我们不能立刻得到操作是否执行成功,但是,你可以通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
并且,你还可以通过ChannelFuture 的 channel() 方法获取关联的Channel
public interface ChannelFuture extends Future<Void> { Channel channel(); ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1); ...... ChannelFuture sync() throws InterruptedException;}
另外,我们还可以通过 ChannelFuture 接口的 sync()方法让异步的操作变成同步的。
4.ChannelHandler 和 ChannelPipeline
下面这段代码使用过 Netty 的小伙伴应该不会陌生,我们指定了序列化编解码器以及自定义的 ChannelHandler 处理消息。
b.group(eventLoopGroup) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class)); ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class)); ch.pipeline().addLast(new KryoClientHandler()); } });
ChannelHandler 是消息的具体处理器。他负责处理读写操作、客户端连接等事情。
ChannelPipeline 为 ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。
我们可以在 ChannelPipeline 上通过 addLast() 方法添加一个或者多个ChannelHandler ,因为一个数据或者事件可能会被多个 Handler 处理。当一个 ChannelHandler 处理完之后就将数据交给下一个 ChannelHandler 。
EventloopGroup 了解么?和 EventLoop 啥关系?
面试官:刚刚你也介绍了 EventLoop。那你再说说 EventloopGroup 吧!和 EventLoop 啥关系?
我:
EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程),上面我们已经说了 EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
并且 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,即 Thread 和 EventLoop 属于 1 : 1 的关系,从而保证线程安全。
上图是一个服务端对 EventLoopGroup 使用的大致模块图,其中 Boss EventloopGroup 用于接收连接,Worker EventloopGroup 用于具体的处理(消息的读写以及其他逻辑处理)。
从上图可以看出:当客户端通过 connect 方法连接服务端时,bossGroup 处理客户端连接请求。当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 IO 相关操作。
Bootstrap 和 ServerBootstrap 了解么?
面试官:你再说说自己对 Bootstrap 和 ServerBootstrap 的了解吧!
我:
Bootstrap 是客户端的启动引导类/辅助类,具体使用方法如下:
EventLoopGroup group = new NioEventLoopGroup(); try { //创建客户端启动引导/辅助类:Bootstrap Bootstrap b = new Bootstrap(); //指定线程模型 b.group(group). ...... // 尝试建立连接 ChannelFuture f = b.connect(host, port).sync(); f.channel().closeFuture().sync(); } finally { // 优雅关闭相关线程组资源 group.shutdownGracefully(); }
ServerBootstrap 客户端的启动引导类/辅助类,具体使用方法如下:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //2.创建服务端启动引导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给引导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup). ...... // 6.绑定端口 ChannelFuture f = b.bind(port).sync(); // 等待连接关闭 f.channel().closeFuture().sync(); } finally { //7.优雅关闭相关线程组资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }
从上面的示例中,我们可以看出:
Bootstrap 通常使用 connet() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端。ServerBootstrap通常使用 bind() 方法绑定本地的端口上,然后等待客户端的连接。Bootstrap 只需要配置一个线程组— EventLoopGroup ,而 ServerBootstrap需要配置两个线程组— EventLoopGroup ,一个用于接收连接,一个用于具体的处理。NioEventLoopGroup 默认的构造函数会起多少线程?
面试官:看过 Netty 的源码了么?NioEventLoopGroup 默认的构造函数会起多少线程呢?
我:嗯嗯!看过部分。
回顾我们在上面写的服务器端的代码:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();
为了搞清楚NioEventLoopGroup 默认的构造函数 到底创建了多少个线程,我们来看一下它的源码。
/** * 无参构造函数。 * nThreads:0 */ public NioEventLoopGroup() { //调用下一个构造方法 this(0); } /** * Executor:null */ public NioEventLoopGroup(int nThreads) { //继续调用下一个构造方法 this(nThreads, (Executor) null); } //中间省略部分构造函数 /** * RejectedExecutionHandler():RejectedExecutionHandlers.reject() */ public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,final SelectStrategyFactory selectStrategyFactory) { //开始调用父类的构造函数 super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()); }
一直向下走下去的话,你会发现在 MultithreadEventLoopGroup 类中有相关的指定线程数的代码,如下:
// 从1,系统属性,CPU核心数*2 这三个值中取出一个最大的 //可以得出 DEFAULT_EVENT_LOOP_THREADS 的值为CPU核心数*2 private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); // 被调用的父类构造函数,NioEventLoopGroup 默认的构造函数会起多少线程的秘密所在 // 当指定的线程数nThreads为0时,使用默认的线程数DEFAULT_EVENT_LOOP_THREADS protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) { super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args); }
综上,我们发现 NioEventLoopGroup 默认的构造函数实际会起的线程数为 CPU核心数*2。
另外,如果你继续深入下去看构造函数的话,你会发现每个NioEventLoopGroup对象内部都会分配一组NioEventLoop,其大小是 nThreads, 这样就构成了一个线程池, 一个NIOEventLoop 和一个线程相对应,这和我们上面说的 EventloopGroup 和 EventLoop关系这部分内容相对应。
Netty 线程模型了解么?
面试官:说一下 Netty 线程模型吧!
我:大部分网络框架都是基于 Reactor 模式设计开发的。
“Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的 Handler 处理,非常适合处理海量 IO 的场景。
在 Netty 主要靠 NioEventLoopGroup 线程池来实现具体的线程模型的 。
我们实现服务端的时候,一般会初始化两个线程组:
bossGroup :接收连接。workerGroup :负责具体的处理,交由对应的 Handler 处理。下面我们来详细看一下 Netty 中的线程模型吧!
1.单线程模型:
一个线程需要执行处理所有的 accept、read、decode、process、encode、send 事件。对于高负载、高并发,并且对性能要求比较高的场景不适用。
对应到 Netty 代码是下面这样的
“使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2。
//1.eventGroup既用于处理客户端连接,又负责具体的处理。 EventLoopGroup eventGroup = new NioEventLoopGroup(1); //2.创建服务端启动引导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); boobtstrap.group(eventGroup, eventGroup) //......
2.多线程模型
一个 Acceptor 线程只负责监听客户端的连接,一个 NIO 线程池负责具体处理:accept、read、decode、process、encode、send 事件。满足绝大部分应用场景,并发连接量不大的时候没啥问题,但是遇到并发连接大的时候就可能会出现问题,成为性能瓶颈。
对应到 Netty 代码是下面这样的:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try { //2.创建服务端启动引导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给引导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup) //......
3.主从多线程模型
从一个 主线程 NIO 线程池中选择一个线程作为 Acceptor 线程,绑定监听端口,接收客户端连接的连接,其他线程负责后续的接入认证等工作。连接建立完成后,Sub NIO 线程池负责具体处理 I/O 读写。如果多线程模型无法满足你的需求的时候,可以考虑使用主从多线程模型 。
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workerGroup = new NioEventLoopGroup();try { //2.创建服务端启动引导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给引导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup) //......
Netty 服务端和客户端的启动过程了解么?
服务端
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //2.创建服务端启动引导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给引导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup) // (非必备)打印日志 .handler(new LoggingHandler(LogLevel.INFO)) // 4.指定 IO 模型 .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { ChannelPipeline p = ch.pipeline(); //5.可以自定义客户端消息的业务处理逻辑 p.addLast(new HelloServerHandler()); } }); // 6.绑定端口,调用 sync 方法阻塞知道绑定完成 ChannelFuture f = b.bind(port).sync(); // 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法) f.channel().closeFuture().sync(); } finally { //8.优雅关闭相关线程组资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); }
简单解析一下服务端的创建过程具体是怎样的:
1.首先你创建了两个 NioEventLoopGroup 对象实例:bossGroup 和 workerGroup。
bossGroup : 用于处理客户端的 TCP 连接请求。workerGroup :负责每一条连接的具体读写数据的处理逻辑,真正负责 I/O 读写操作,交由对应的 Handler 处理。举个例子:我们把公司的老板当做 bossGroup,员工当做 workerGroup,bossGroup 在外面接完活之后,扔给 workerGroup 去处理。一般情况下我们会指定 bossGroup 的 线程数为 1(并发连接量不大的时候) ,workGroup 的线程数量为 CPU 核心数 *2 。另外,根据源码来看,使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 *2 。
2.接下来 我们创建了一个服务端启动引导/辅助类:ServerBootstrap,这个类将引导我们进行服务端的启动工作。
3.通过 .group() 方法给引导类 ServerBootstrap 配置两大线程组,确定了线程模型。
通过下面的代码,我们实际配置的是多线程模型,这个在上面提到过。
EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup();
4.通过channel()方法给引导类 ServerBootstrap指定了 IO 模型为NIO
NioServerSocketChannel :指定服务端的 IO 模型为 NIO,与 BIO 编程模型中的ServerSocket对应NioSocketChannel : 指定客户端的 IO 模型为 NIO, 与 BIO 编程模型中的Socket对应5.通过 .childHandler()给引导类创建一个ChannelInitializer ,然后制定了服务端消息的业务处理逻辑 HelloServerHandler 对象6.调用 ServerBootstrap 类的 bind()方法绑定端口客户端
//1.创建一个 NioEventLoopGroup 对象实例 EventLoopGroup group = new NioEventLoopGroup(); try { //2.创建客户端启动引导/辅助类:Bootstrap Bootstrap b = new Bootstrap(); //3.指定线程组 b.group(group) //4.指定 IO 模型 .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); // 5.这里可以自定义消息的业务处理逻辑 p.addLast(new HelloClientHandler(message)); } }); // 6.尝试建立连接 ChannelFuture f = b.connect(host, port).sync(); // 7.等待连接关闭(阻塞,直到Channel关闭) f.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); }
继续分析一下客户端的创建流程:
1.创建一个 NioEventLoopGroup 对象实例
2.创建客户端启动的引导类是 Bootstrap
3.通过 .group() 方法给引导类 Bootstrap 配置一个线程组
4.通过channel()方法给引导类 Bootstrap指定了 IO 模型为NIO
5.通过 .childHandler()给引导类创建一个ChannelInitializer ,然后制定了客户端消息的业务处理逻辑 HelloClientHandler 对象
6.调用 Bootstrap 类的 connect()方法进行连接,这个方法需要指定两个参数:
inetHost : ip 地址inetPort : 端口号public ChannelFuture connect(String inetHost, int inetPort) { return this.connect(InetSocketAddress.createUnresolved(inetHost, inetPort)); } public ChannelFuture connect(SocketAddress remoteAddress) { ObjectUtil.checkNotNull(remoteAddress, "remoteAddress"); this.validate(); return this.doResolveAndConnect(remoteAddress, this.config.localAddress()); }
connect 方法返回的是一个 Future 类型的对象
public interface ChannelFuture extends Future<Void> { ......}
也就是说这个方是异步的,我们通过 addListener 方法可以监听到连接是否成功,进而打印出连接信息。具体做法很简单,只需要对代码进行以下改动:
ChannelFuture f = b.connect(host, port).addListener(future -> { if (future.isSuccess()) { System.out.println("连接成功!"); } else { System.err.println("连接失败!"); }}).sync();
什么是 TCP 粘包/拆包?有什么解决办法呢?
面试官:什么是 TCP 粘包/拆包?
我:TCP 粘包/拆包 就是你基于 TCP 发送数据的时候,出现了多个字符串“粘”在了一起或者一个字符串被“拆”开的问题。比如你多次发送:“你好,你真帅啊!哥哥!”,但是客户端接收到的可能是下面这样的:
面试官:那有什么解决办法呢?
我:
1.使用 Netty 自带的解码器
LineBasedFrameDecoder : 发送端发送数据包的时候,每个数据包之间以换行符作为分隔,
LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断是否有换行符,然后进行相应的截取。
DelimiterBasedFrameDecoder : 可以自定义分隔符解码器,
LineBasedFrameDecoder 实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器。
FixedLengthFrameDecoder: 固定长度解码器,它能够按照指定的长度对消息进行相应的拆包。
LengthFieldBasedFrameDecoder:2.自定义序列化编解码器
在 Java 中自带的有实现 Serializable 接口来实现序列化,但由于它性能、安全性等原因一般情况下是不会被使用到的。
通常情况下,我们使用 Protostuff、Hessian2、json 序列方式比较多,另外还有一些序列化性能非常好的序列化方式也是很好的选择:
专门针对 Java 语言的:Kryo,FST 等等跨语言的:Protostuff(基于 protobuf 发展而来),ProtoBuf,Thrift,Avro,MsgPack 等等“由于篇幅问题,这部分内容会在后续的文章中详细分析介绍~~~
Netty 长连接、心跳机制了解么?
面试官:TCP 长连接和短连接了解么?
我:我们知道 TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。
所谓,短连接说的就是 server 端 与 client 端建立连接之后,读写完成之后就关闭掉连接,如果下一次再要互相发送消息,就要重新连接。短连接的有点很明显,就是管理和实现都比较简单,缺点也很明显,每一次的读写都要建立连接必然会带来大量网络资源的消耗,并且连接的建立也需要耗费时间。
长连接说的就是 client 向 server 双方建立连接之后,即使 client 与 server 完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接的可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的依赖,节约时间。对于频繁请求资源的客户来说,非常适用长连接。
面试官:为什么需要心跳机制?Netty 中心跳机制了解么?
我:
在 TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题, 我们就需要引入 心跳机制。
心跳机制的工作原理是: 在 client 与 server 之间在一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器就会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。所以, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性.
TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:SO_KEEPALIVE。但是,TCP 协议层面的长连接灵活性不够。所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler 。
Netty 的零拷贝了解么?
面试官:讲讲 Netty 的零拷贝?
我:
维基百科是这样介绍零拷贝的:
“零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU 周期和内存带宽。
在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据。而在 Netty 层面 ,零拷贝主要体现在对于数据操作的优化。
Netty 中的零拷贝体现在以下几个方面:
使用 Netty 提供的 CompositeByteBuf 类, 可以将多个ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝。ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.