Netty
一、Netty介绍和应用场景
1.1 Netty的介绍
Netty
是由JBOSS
提供的一个Java
开源框架,现为Github
上的独立项目。Netty
是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO
程序。Netty
主要针对在TCP
协议下,面向Clients
端的高并发应用,或者Peer-to-Peer
场景下的大量数据持续传输的应用。Netty
本质是一个NIO
框架,适用于服务器通讯相关的多种应用场景。
1.2 Netty的应用场景
1.2.1 互联网行业
- 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的
RPC
框架必不可少,Netty
作为异步高性能的通信框架,往往作为基础通信组件被这些RPC
框架使用。 - 典型的应用有:阿里分布式服务框架
Dubbo
的RPC
框架使用Dubbo
协议进行节点间通信,Dubbo
协议默认使用Netty
作为基础通信组件,用于实现各进程节点之间的内部通信。
1.2.2 游戏行业
- 无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。
Netty
作为高性能的基础通信组件,提供了TCP/UDP
和HTTP
协议栈,方便定制和开发私有协议栈,账号登录服务器。- 地图服务器之间可以方便的通过
Netty
进行高性能的通信。
1.2.3 大数据领域
- 经典的
Hadoop
的高性能通信和序列化组件Avro
的RPC
框架,默认采用Netty
进行跨界点通信。 - 它的
NettyService
基于Netty
框架二次封装实现。
三、JavaBIO编程
2.1 I/O模型
I/O
模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。Java
共支持3种网络编程模型I/O
模式:BIO
、NIO
、AIO
。JavaBIO
:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
JavaNIO
:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O
请求就进行处理。
JavaAIO(NIO.2)
:异步非阻塞,AIO
引入异步通道的概念,采用了Proactor
模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
2.2 BIO、NIO、AIO适用场景分析
BIO
方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4
以前的唯一选择,但程序简单易理解。NIO
方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4
开始支持。AIO
方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS
参与并发操作,编程比较复杂,JDK7
开始支持。
2.3 JavaBIO基本介绍
JavaBIO
就是传统的javaio
编程,其相关的类和接口在java.io
。BIO(blockingI/O)
:同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。BIO
方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4
以前的唯一选择,程序简单易理解。
2.4 JavaBIO工作机制
- 服务器端启动一个
ServerSocket
。 - 客户端启动
Socket
对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯。 - 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
- 如果有响应,客户端线程会等待请求结束后,在继续执行。
2.5 JavaBIO应用实例
- 使用
BIO
模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯。 - 要求使用线程池机制改善,可以连接多个客户端。
- 服务器端可以接收客户端发送的数据(
telnet
方式即可)。
public class BIOServer {
public static void main(String[] args) throws Exception {
//1.创建一个线程池
//2.如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true) {
System.out.println("线程信息id=" + Thread.currentThread().getId() + "名字=" +
Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接....");
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//就创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() {
//我们重写
//可以和客户端通讯
handler(socket);
}
});
}
}
/**
* 编写一个handler方法,和客户端通讯
* @param socket
*/
public static void handler(Socket socket) {
try {
System.out.println("线程信息id=" + Thread.currentThread().getId() + "名字=" +
Thread.currentThread().getName());
byte[] bytes = new byte[1024];
//通过socket获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true) {
System.out.println("线程信息id=" + Thread.currentThread().getId() + "名字=" +
Thread.currentThread().getName());
System.out.println("read....");
int read = inputStream.read(bytes);
if (read != -1) {
System.out.println(new String(bytes, 0, read));
//输出客户端发送的数据
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("关闭和client的连接");
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//启动结果
服务器启动了
线程信息id=1名字=main
等待连接....
//进入命令行窗口 telnet 127.0.0.1 6666
连接到一个客户端
线程信息id=1名字=main
等待连接....
线程信息id=12名字=pool-1-thread-1
线程信息id=12名字=pool-1-thread-1
read....
//在客户端输入 ctrl+]
//send client1
client1
线程信息id=12名字=pool-1-thread-1
read....
//send client ok
client ok
线程信息id=12名字=pool-1-thread-1
read....
//新打开一个命令行窗口 telnet 127.0.0.1 6666
连接到一个客户端
线程信息id=1名字=main
等待连接....
线程信息id=13名字=pool-1-thread-2
线程信息id=13名字=pool-1-thread-2
read....
//在客户端输入 ctrl+]
//send client1
client2
线程信息id=13名字=pool-1-thread-2
read....
client2 ok
线程信息id=13名字=pool-1-thread-2
read....
2.6 JavaBIO问题分析
- 每个请求都需要创建独立的线程,与对应的客户端进行数据
Read
,业务处理,数据Write
。 - 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在
Read
操作上,造成线程资源浪费。
三、JavaNIO编程
3.1 JavaNIO基本介绍
-
JavaNIO
全称java non-blockingIO
,是指JDK
提供的新API
。从JDK1.4
开始,Java
提供了一系列改进的输入/输出的新特性,被统称为NIO
(即NewIO
),是同步非阻塞的。 -
NIO
相关类都被放在java.nio
包及子包下,并且对原java.io
包中的很多类进行改写。 -
NIO
有三大核心部分:Channel
(通道),Buffer
(缓冲区),Selector
(选择器)。 -
NIO
是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。 -
JavaNIO
的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 -
通俗理解:
NIO
是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO
那样,非得分配10000个。 -
HTTP2.0
使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1
大了好几个数量级。public class BasicBuffer { public static void main(String[] args) { //举例说明Buffer的使用(简单说明) //创建一个Buffer,大小为5,即可以存放5个int IntBuffer intBuffer = IntBuffer.allocate(5); //向buffer存放数据 for (int i = 0; i < intBuffer.capacity(); i++) { intBuffer.put(i * 2); } //如何从buffer读取数据 //将buffer转换,读写切换(!!!) intBuffer.flip(); while (intBuffer.hasRemaining()) { System.out.println(intBuffer.get()); } } } //结果 0 2 4 6 8
3.2 NIO和BIO的比较
BIO
以流的方式处理数据,而NIO
以块的方式处理数据,块I/O
的效率比流I/O
高很多。BIO
是阻塞的,NIO
则是非阻塞的。BIO
基于字节流和字符流进行操作,而NIO
基于Channel
(通道)和Buffer
(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector
(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
3.3 NIO三大核心原理示意图
- 每个
channel
都会对应一个Buffer
。 Selector
对应一个线程,一个线程对应多个channel
(连接)。- 该图反应了有三个
channel
注册到该selector
程序。 - 程序切换到哪个
channel
是由事件决定的,Event
就是一个重要的概念。 Selector
会根据不同的事件,在各个通道上切换。Buffer
就是一个内存块,底层是一个数组。- 数据的读取写入是通过
Buffer
,BIO
中要么是输入流,或者是输出流,不能双向,但是NIO
的Buffer
是可以读也可以写,需要flip
方法切换channel
是双向的,可以返回底层操作系统的情况,比如Linux
,底层的操作系统通道就是双向的。
3.4 缓冲区(Buffer)
3.4.1基本介绍
- 缓冲区(
Buffer
):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel
提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer
。
3.4.2 Buffer类及其子类
-
在
NIO
中,Buffer
是一个顶层父类,它是一个抽象类。
Buffer子类 描述 ByteBuffer
存储字节数据到缓冲区 ShortBuffer
存储字符串数据到缓冲区 CharBuffer
存储字符数据到缓冲区 IntBuffer
存储整数数据到缓冲区 LongBuffer
存储长整型数据到缓冲区 DoubleBuffer
存储小数到缓冲区 FloatBuffer
存储小数到缓冲区 -
Buffer类
public abstract class Buffer { // Invariants: mark <= position <= limit <= capacity private int mark = -1; //标记 private int position = 0;//位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变值,为下次读写做准备 private int limit;//表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的 private int capacity; //容量,即可以容纳的最大数据量,在缓冲区创建时被设定不能改变 //JDK1.4 引入的API //返回该缓冲区的容量 public final int capacity() { return capacity; } //返回该缓冲区的位置 public final int position() { return position; } //设置此缓冲区的位置 public final Buffer position(int newPosition) { if ((newPosition > limit) || (newPosition < 0)) throw new IllegalArgumentException(); position = newPosition; if (mark > position) mark = -1; return this; } //返回这个缓冲区的限制 public final int limit() { return limit; } //设置此缓冲区的限制 public final Buffer limit(int newLimit) { if ((newLimit > capacity) || (newLimit < 0)) throw new IllegalArgumentException(); limit = newLimit; if (position > limit) position = limit; if (mark > limit) mark = -1; return this; } //在其位置设置缓冲区的标记 public final Buffer mark() { mark = position; return this; } //将缓冲区的位置重置为之前标记的位置 public final Buffer reset() { int m = mark; if (m < 0) throw new InvalidMarkException(); position = m; return this; } //清除此缓冲区,即将各个标记恢复到初始状态,但是数据并没有真正擦除 public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; } //反转此缓冲区 public final Buffer flip() { limit = position; position = 0; mark = -1; return this; } //重绕此缓冲区 public final Buffer rewind() { position = 0; mark = -1; return this; } //返回当前位置和限制之间的元素数 public final int remaining() { return limit - position; } //判断当前位置和限制之间是否有元素 public final boolean hasRemaining() { return position < limit; } //告诉这个缓冲区是否为只读 public abstract boolean isReadOnly(); //JDK1.6 引入的API //告知此缓冲区是否具有可访问的底层实现数组 public abstract boolean hasArray(); //返回此缓冲区的底层实现数据 public abstract Object array(); //返回此缓冲区的底层实现数组中的第一个缓冲区的偏移量 public abstract int arrayOffset(); //告知此缓冲区是否为直接缓冲区 public abstract boolean isDirect(); }
3.5 ByteBuffer
public abstract class IntBuffer extends Buffer implements Comparable<IntBuffer>{
//设置缓冲区的初始容量
public static IntBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapIntBuffer(capacity, capacity);
}
//构造初始化位置和上界length的缓冲区
public static IntBuffer wrap(int[] array,int offset, int length){
try {
return new HeapIntBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}
//把一个数组放到缓冲区中使用
public static IntBuffer wrap(int[] array) {
return wrap(array, 0, array.length);
}
//创建一个新的int缓冲区,其内容是该缓冲区内容的共享子序列
public abstract IntBuffer slice();
//创建一个新的int缓冲区,共享这个缓冲区的内容
public abstract IntBuffer duplicate();
//创建一个新的int缓冲区,共享这个缓冲区的内容
public abstract IntBuffer asReadOnlyBuffer();
//从当前位置poposition上get,get之后,position会自动+1
public abstract int get();
//将给定的int值写入缓冲区的当前位置,然后对该位置加1
public abstract IntBuffer put(int i);
//绝对的get方法。读取给定下标处的int值
public abstract int get(int index);
//在给定的索引处将给定的int写入缓冲区
public abstract IntBuffer put(int index, int i);
//此方法将此缓冲区的数据转换到给定的目标数组
public IntBuffer get(int[] dst, int offset, int length) {
checkBounds(offset, length, dst.length);
if (length > remaining())
throw new BufferUnderflowException();
int end = offset + length;
for (int i = offset; i < end; i++)
dst[i] = get();
return this;
}
//此方法将此缓冲区的数据转换到给定的目标数组。
public IntBuffer get(int[] dst) {
return get(dst, 0, dst.length);
}
//此方法将给定源缓冲区中剩余的整数转移到此缓冲区
public IntBuffer put(IntBuffer src) {
if (src == this)
throw new IllegalArgumentException();
if (isReadOnly())
throw new ReadOnlyBufferException();
int n = src.remaining();
if (n > remaining())
throw new BufferOverflowException();
for (int i = 0; i < n; i++)
put(src.get());
return this;
}
//此方法将给定源数组的数据转换到此缓冲区
public IntBuffer put(int[] src, int offset, int length) {
checkBounds(offset, length, src.length);
if (length > remaining())
throw new BufferOverflowException();
int end = offset + length;
for (int i = offset; i < end; i++)
this.put(src[i]);
return this;
}
//这个方法将给定源int数组的全部内容传输到这个缓冲区中
public final IntBuffer put(int[] src) {
return put(src, 0, src.length);
}
//说明该缓冲区是否由可访问的int数组支持
public final boolean hasArray() {
return (hb != null) && !isReadOnly;
}
//返回该缓冲区的int数组
public final int[] array() {
if (hb == null)
throw new UnsupportedOperationException();
if (isReadOnly)
throw new ReadOnlyBufferException();
return hb;
}
//返回该缓冲区的后退数组中第一个元素的偏移量
public final int arrayOffset() {
if (hb == null)
throw new UnsupportedOperationException();
if (isReadOnly)
throw new ReadOnlyBufferException();
return offset;
}
//压缩这个缓冲区
public abstract IntBuffer compact();
//说明这个int缓冲区是否直接
public abstract boolean isDirect();
//返回此缓冲区状态的字符串
public String toString() {}
//返回此缓冲区的当前哈希码
public int hashCode() {}
//告诉缓冲区是否等于另一个对象
public boolean equals(Object ob) {
}
private static boolean equals(int x, int y) {
return x == y;
}
//将这个缓冲区与另一个缓冲区进行比较
public int compareTo(IntBuffer that) {
int n = this.position() + Math.min(this.remaining(), that.remaining());
for (int i = this.position(), j = that.position(); i < n; i++, j++) {
int cmp = compare(this.get(i), that.get(j));
if (cmp != 0)
return cmp;
}
return this.remaining() - that.remaining();
}
private static int compare(int x, int y) {
return Integer.compare(x, y);
}
}
3.6 通道(Channel)
-
NIO
的通道类似于流,但有些区别:- 通道可以同时进行读写,而流只能读或者只能写。
- 通道可以实现异步读写数据。
- 通道可以从缓冲读数据,也可以写数据到缓冲。
-
BIO
中的stream
是单向的,例如FileInputStream
对象只能进行读取数据的操作,而NIO
中的通道(Channel
)是双向的,可以读操作,也可以写操作。 -
Channel
在NIO
中是一个接口。public interface Channel extends Closeable { }
-
常用的
Channel
类有:FileChannel
、DatagramChannel
、ServerSocketChannel
和SocketChannel
。 -
FileChannel
用于文件的数据读写,DatagramChannel
用于UDP
的数据读写,ServerSocketChannel
和SocketChannel
用于TCP
的数据读写。
3.6.1 FileChannel类
FileChannel
主要用来对本地文件进行IO
操作,常见的方法有:
public intread(ByteBuffer dst)
,从通道读取数据并放到缓冲区中。public intwrite(ByteBuffer src)
,把缓冲区的数据写到通道中。public long transferFrom(ReadableByteChannel src,long position,long count)
,从目标通道中复制数据到当前通道。publicl ongt ransferTo(long position,long count,WritableByteChannel target)
,把数据从当前通道复制给目标通道。
3.6.2 应用实例1-本地文件写数据
-
使用
ByteBuffer
(缓冲)和FileChannel
(通道),将"hello,残影"
写入到file01.txt
中。 -
文件不存在就创建。
public class NIOFileChannel01 { public static void main(String[] args) throws Exception { String str = "hello,残影"; //创建一个输出流->channel FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\24891\\Desktop\\TestNetty\\file\\file01.txt"); //通过fileOutputStream获取对应的FileChannel //这个fileChannel真实类型是FileChannelImpl FileChannel fileChannel = fileOutputStream.getChannel(); //创建一个缓冲区ByteBuffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //将str放入byteBuffer byteBuffer.put(str.getBytes()); //对byteBuffer进行flip byteBuffer.flip(); //将byteBuffer数据写入到fileChannel fileChannel.write(byteBuffer); fileOutputStream.close(); } }
3.6.3 应用实例2-本地文件读数据
-
使用
ByteBuffer
(缓冲)和FileChannel
(通道),将file01.txt
中的数据读入到程序,并显示在控制台。 -
假定文件已经存在。
public class NIOFileChannel02 { public static void main(String[] args) throws Exception { //创建文件的输入流 File file = new File("C:\\Users\\24891\\Desktop\\TestNetty\\file\\file01.txt"); FileInputStream fileInputStream = new FileInputStream(file); //通过fileInputStream获取对应的FileChannel->实际类型FileChannelImpl FileChannel fileChannel = fileInputStream.getChannel(); //创建缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length()); //将通道的数据读入到Buffer fileChannel.read(byteBuffer); //将byteBuffer的字节数据转成String System.out.println(new String(byteBuffer.array())); fileInputStream.close(); } } //结果 hello,残影
3.6.4 应用实例3-使用一个Buffer完成文件读取、写入
-
使用FileChannel(通道)和方法read,write,完成文件的拷贝
-
拷贝一个文本文件1.txt
public class NIOFileChannel03 { public static void main(String[] args) throws IOException { FileInputStream fileInputStream = new FileInputStream("file\\1.txt"); FileChannel fileChannel01 = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("file\\2.txt"); FileChannel fileChannel02 = fileOutputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(512); //循环读取 while (true) { //清空buffer,不清空会导致Buffer的position等于limit,read会永远为0,进入死循环 byteBuffer.clear(); int read = fileChannel01.read(byteBuffer); System.out.println("read=" + read); if (read == -1) { //表示读完 break; } //将buffer中的数据写入到fileChannel02--2.txt byteBuffer.flip(); fileChannel02.write(byteBuffer); } // 关闭相关的流 fileInputStream.close(); fileOutputStream.close(); } } //结果 read=87 read=-1
3.6.5 应用实例4-拷贝文件transferFrom方法
-
使用
FileChannel
(通道)和方法transferFrom
,完成文件的拷贝。public class NIOFileChannel04 { public static void main(String[] args) throws IOException { FileInputStream fileInputStream = new FileInputStream("file\\img.png"); FileOutputStream fileOutputStream = new FileOutputStream("file\\img2.png"); //获取各个流对应的filechannel FileChannel sourceCh = fileInputStream.getChannel(); FileChannel destCh = fileOutputStream.getChannel(); //使用transferForm完成拷贝 destCh.transferFrom(sourceCh, 0, sourceCh.size()); //关闭相关通道和流 sourceCh.close(); destCh.close(); fileInputStream.close(); fileOutputStream.close(); } }
3.6.6 关于Buffer和Channel的注意事项和细节
-
ByteBuffer
支持类型化的put
和get
,put
放入的是什么数据类型,get
就应该使用相应的数据类型来取出,否则可能有BufferUnderflowException
异常。 -
可以将一个普通
Buffer
转成只读Buffer
。ByteBuffer readOnlyBuffer=buffer.asReadOnlyBuffer()
-
NIO
还提供了MappedByteBuffer
,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO
来完成。public class MappedByteBufferTest { public static void main(String[] args) throws IOException { RandomAccessFile randomAccessFile = new RandomAccessFile("file\\1.txt", "rw"); //获取对应的通道 FileChannel channel = randomAccessFile.getChannel(); /** * 参数1: FileChannel.MapMode.READ_WRITE使用的读写模式 * 参数2:0:可以直接修改的起始位置 * 参数3: 5:是映射到内存的大小(不是索引位置),即将1.txt的多少个字节映射到内存 * 可以直接修改的范围就是0-5 * 实际类型DirectByteBuffer */ MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); mappedByteBuffer.put(0, (byte) 'i'); mappedByteBuffer.put(1, (byte) 'f'); //IndexOutOfBoundsException //mappedByteBuffer.put(5, (byte) 'Y'); randomAccessFile.close(); System.out.println("修改成功~~"); } } //结果 //注意:修改的文件在本地文件打开,才会发现变化 修改成功~~
-
前面读写操作,都是通过一个
Buffer
完成的,NIO
还支持通过多个Buffer
(即Buffer
数组)完成读写操作,即Scattering
和Gathering
。public class ScatteringAndGatheringTest { public static void main(String[] args) throws IOException { //使用ServerSocketChannel和SocketChannel网络 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress(7000); //绑定端口到socket,并启动 serverSocketChannel.socket().bind(inetSocketAddress); //创建buffer数组 ByteBuffer[] byteBuffers = new ByteBuffer[2]; byteBuffers[0] = ByteBuffer.allocate(5); byteBuffers[1] = ByteBuffer.allocate(3); //等客户端连接(telnet) SocketChannel socketChannel = serverSocketChannel.accept(); int messageLength = 8; //假定从客户端接收8个字节 //循环的读取 while (true) { int byteRead = 0; while (byteRead < messageLength) { long l = socketChannel.read(byteBuffers); //累计读取的字节数 byteRead += l; System.out.println("byteRead=" + byteRead); //使用流打印,看看当前的这个buffer的position和limit Arrays.asList(byteBuffers).stream().map(buffer -> "postion=" + buffer.position() + ", limit=" + buffer.limit()).forEach(System.out::println); } //将所有的buffer进行flip Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip()); //将数据读出显示到客户端 long byteWirte = 0; while (byteWirte < messageLength) { long l = socketChannel.write(byteBuffers); byteWirte += l; } //将所有的buffer进行clear Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear()); System.out.println("byteRead=" + byteRead + ", byteWrite=" + byteWirte + ", messagelength=" + messageLength); } } }
3.7 Selector(选择器)
3.7.1 基本介绍
Java
的NIO
,用非阻塞的IO
方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector
(选择器)。Selector
能够检测多个注册的通道上是否有事件发生(注意:多个Channel
以事件的方式可以注册到同一个Selector
),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。- 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
- 避免了多线程之间的上下文切换导致的开销。
3.7.2 Selector示意图和特点说明
Netty
的IO
线程NioEventLoop
聚合了Selector
(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。- 当线程从某客户端
Socket
通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。 - 线程通常将非阻塞
IO
的空闲时间用于在其他通道上执行IO
操作,所以单独的线程可以管理多个输入和输出通道。 - 由于读写操作都是非阻塞的,这就可以充分提升
IO
线程的运行效率,避免由于频繁I/O
阻塞导致的线程挂起。 - 一个
I/O
线程可以并发处理N
个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O
一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
3.7.3 Selector类相关方法
public abstract class Selector implements Closeable {
//打开一个选择器
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
//告诉选择器是否打开
public abstract boolean isOpen();
//返回创建此通道的提供程序
public abstract SelectorProvider provider();
//返回这个选择器的键集
public abstract Set<SelectionKey> keys();
//从内部集合中获取所有的SelectionKey
public abstract Set<SelectionKey> selectedKeys();
//返回由选择操作更新其准备操作集的键的数目(可能为零)
public abstract int selectNow() throws IOException;
//监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
public abstract int select(long timeout) throws IOException;
//选择一组键,其对应的通道已准备好进行I/O操作。
public abstract int select() throws IOException;
//使第一个尚未返回的选择操作立即返回
public abstract Selector wakeup();
//关闭这个选择器
public abstract void close() throws IOException;
}
3.7.4 注意事项
NIO
中的ServerSocketChannel
功能类似ServerSocket
,SocketChannel
功能类似Socket
。selector
相关方法说明:selector.select()
//阻塞selector.select(1000);
//阻塞1000毫秒,在1000毫秒后返回selector.wakeup();
//唤醒selector.selectNow();
//不阻塞,立马返还
3.8 NIO非阻塞网络编程原理分析图
- 当客户端连接时,会通过
ServerSocketChannel
得到SocketChannel
。 Selector
进行监听select
方法,返回有事件发生的通道的个数。- 将
socketChannel
注册到Selector
上,register(Selectorsel,intops)
,一个selector
上可以注册多个SocketChannel
。 - 注册后返回一个
SelectionKey
,会和该Selector
关联(集合)。 - 进一步得到各个
SelectionKey
(有事件发生)。 - 再通过
SelectionKey
反向获取SocketChannel
,方法channel()
。 - 可以通过得到的
channel
,完成业务处理。
3.9 NIO非阻塞网络编程快速入门
-
编写一个
NIO
入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)。public class NIOServer { public static void main(String[] args) throws Exception { //创建ServerSocketChannel->ServerSocket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //得到一个Selecor对象 Selector selector = Selector.open(); //绑定一个端口6666,在服务器端监听 serverSocketChannel.socket().bind(new InetSocketAddress(6666)); //设置为非阻塞 serverSocketChannel.configureBlocking(false); //把serverSocketChannel注册到selector 关心事件为OP_ACCEPT serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //循环等待客户端连接 while (true) { //这里我们等待1秒,如果没有事件发生,返回 if (selector.select(1000) == 0) { //没有事件发生 System.out.println("服务器等待了1秒,无连接"); continue; } //如果返回的>0,就获取到相关的selectionKey集合 //1.如果返回的>0,表示已经获取到关注的事件 //2.selector.selectedKeys()返回关注事件的集合 //通过selectionKeys反向获取通道 Set<SelectionKey> selectionKeys = selector.selectedKeys(); //遍历Set<SelectionKey>,使用迭代器遍历 Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()) { //获取到SelectionKey SelectionKey key = keyIterator.next(); //根据key对应的通道发生的事件做相应处理 if (key.isAcceptable()) { //如果是OP_ACCEPT,有新的客户端连接 //该该客户端生成一个SocketChannel SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("客户端连接成功生成了一个socketChannel" + socketChannel.hashCode()); //将SocketChannel设置为非阻塞 socketChannel.configureBlocking(false); //将socketChannel注册到selector,关注事件为OP_READ,同时给socketChannel关联一个Buffer socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } //发生OP_READ if (key.isReadable()) { //通过key反向获取到对应channel SocketChannel channel = (SocketChannel) key.channel(); //获取到该channel关联的buffer ByteBuffer buffer = (ByteBuffer) key.attachment(); channel.read(buffer); System.out.println("form客户端" + new String(buffer.array())); } //手动从集合中移动当前的selectionKey,防止重复操作 keyIterator.remove(); } } } }
public class NIOClient { public static void main(String[] args) throws Exception { //得到一个网络通道 SocketChannel socketChannel = SocketChannel.open(); //设置非阻塞 socketChannel.configureBlocking(false); //提供服务器端的ip和端口 //连接服务器 InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666); if (!socketChannel.connect(inetSocketAddress)) { while (!socketChannel.finishConnect()) { System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作.."); } } //...如果连接成功,就发送数据 String str = "hello, 残影~"; //Wraps a byte array into a buffer ByteBuffer buffer = ByteBuffer.wrap(str.getBytes()); //发送数据,将buffer数据写入channel socketChannel.write(buffer); System.in.read(); } }
3.10 SelectionKey
-
SelectionKey
,表示Selector
和网络通道的注册关系, 共四种:int OP_ACCEPT
:有新的网络连接可以 accept,值为 16 。int OP_CONNECT
:代表连接已经建立,值为 8。int OP_READ
:代表读操作,值为 1。int OP_WRITE
:代表写操作,值为 4。
public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4;
-
SelectionKey
相关方法:public abstract class SelectionKey { protected SelectionKey() { } //得到与之关联的通道对象 public abstract SelectableChannel channel(); //得到与之关联的selector对象 public abstract Selector selector(); //判断此键是否有效 public abstract boolean isValid(); //如果这个键已经被取消,那么调用这个方法没有效果。一旦取消,密钥将永远无效 public abstract void cancel(); //检索此键的兴趣集 public abstract int interestOps(); //设置或改变监听事件 public abstract SelectionKey interestOps(int ops); //检索此键的就绪操作集 public abstract int readyOps(); //用于读操作的操作设置位 public static final int OP_READ = 1 << 0; //操作设置位,用于写操作 public static final int OP_WRITE = 1 << 2; //套接字连接操作的操作设置位 public static final int OP_CONNECT = 1 << 3; //socket-accept操作的操作设置位 public static final int OP_ACCEPT = 1 << 4; //是否可以读 public final boolean isReadable() { return (readyOps() & OP_READ) != 0; } //是否可以写 public final boolean isWritable() { return (readyOps() & OP_WRITE) != 0; } //是否可以连接 public final boolean isConnectable() { return (readyOps() & OP_CONNECT) != 0; } //测试此密钥的通道是否已准备好接受新的套接字连接 public final boolean isAcceptable() { return (readyOps() & OP_ACCEPT) != 0; } // -- Attachments -- private volatile Object attachment = null; private static final AtomicReferenceFieldUpdater<SelectionKey,Object> attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater( SelectionKey.class, Object.class, "attachment" ); //将给定对象附加到这个键上 public final Object attach(Object ob) { return attachmentUpdater.getAndSet(this, ob); } //得到与之关联的数据 public final Object attachment() { return attachment; } }
3.11 ServerSocketChannel
-
ServerSocketChannel
在服务器端监听新的客户端Socket
连接。 -
相关方法:
public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel{ //初始化该类的新实例 protected ServerSocketChannel(SelectorProvider provider) { super(provider); } //得到一个ServerSocketChannel通道 public static ServerSocketChannel open() throws IOException { return SelectorProvider.provider().openServerSocketChannel(); } //返回标识此通道支持的操作的操作集 public final int validOps() { return SelectionKey.OP_ACCEPT; } //设置服务器的端口号 public final ServerSocketChannel bind(SocketAddress local) throws IOException{ return bind(local, 0); } public abstract ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException; public abstract <T> ServerSocketChannel setOption(SocketOption<T> name, T value) throws IOException; //检索与此通道相关联的服务器套接字 public abstract ServerSocket socket(); //接受一个连接,返回代表这个连接的通道对象 public abstract SocketChannel accept() throws IOException; @Override public abstract SocketAddress getLocalAddress() throws IOException; }
3.12 SocketChannel
-
SocketChannel
,网络IO
通道,具体负责进行读写操作。NIO
把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。 -
相关方法:
public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel{ //初始化该类的新实例 protected SocketChannel(SelectorProvider provider) { super(provider); } //得到一个SocketChannel通道 public static SocketChannel open() throws IOException { return SelectorProvider.provider().openSocketChannel(); } //打开一个套接字通道并将其连接到一个远程地址 public static SocketChannel open(SocketAddress remote) throws IOException{ SocketChannel sc = open(); try { sc.connect(remote); } catch (Throwable x) { try { sc.close(); } catch (Throwable suppressed) { x.addSuppressed(suppressed); } throw x; } assert sc.isConnected(); return sc; } //返回标识此通道支持的操作的操作集 public final int validOps() { return (SelectionKey.OP_READ | SelectionKey.OP_WRITE | SelectionKey.OP_CONNECT); } @Override public abstract SocketChannel bind(SocketAddress local) throws IOException; @Override public abstract <T> SocketChannel setOption(SocketOption<T> name, T value) throws IOException; //在不关闭通道的情况下关闭读取连接 public abstract SocketChannel shutdownInput() throws IOException; //在不关闭通道的情况下关闭写入连接 public abstract SocketChannel shutdownOutput() throws IOException; //检索与此通道关联的socket public abstract Socket socket(); //判断此通道的网络socket是否已连接 public abstract boolean isConnected(); //说明该通道上是否正在进行连接操作 public abstract boolean isConnectionPending(); //连接服务器 public abstract boolean connect(SocketAddress remote) throws IOException; //如果通过上面的方法连接失败,接下来就要通过该方法完成连接操作 public abstract boolean finishConnect() throws IOException; //返回此通道socket连接到的远程地址 public abstract SocketAddress getRemoteAddress() throws IOException; //从通道里读数据 public abstract int read(ByteBuffer dst) throws IOException; public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException; public final long read(ByteBuffer[] dsts) throws IOException { return read(dsts, 0, dsts.length); } //往通道里写数据 public abstract int write(ByteBuffer src) throws IOException; public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException; public final long write(ByteBuffer[] srcs) throws IOException { return write(srcs, 0, srcs.length); } @Override public abstract SocketAddress getLocalAddress() throws IOException; }
3.13 NIO网络编程应用实例-群聊系统
-
编写一个
NIO
群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)。 -
实现多人群聊。
-
服务器端:可以监测用户上线,离线,并实现消息转发功能。
-
客户端:通过
channel
可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)。 -
目的:进一步理解
NIO
非阻塞网络编程机制。 -
服务端
public class GroupChatServer { //定义属性 private Selector selector; private ServerSocketChannel listenChannel; private static final int PORT = 6667; //初始化工作 public GroupChatServer() { try { //得到选择器 selector = Selector.open(); //ServerSocketChannel listenChannel = ServerSocketChannel.open(); //绑定端口 listenChannel.socket().bind(new InetSocketAddress(PORT)); //设置非阻塞模式 listenChannel.configureBlocking(false); //将该listenChannel注册到selector listenChannel.register(selector, SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } //监听 public void listen() { try { //循环处理 while (true) { int count = selector.select(); if (count > 0) {//有事件处理 //遍历得到selectionKey集合 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { //取出selectionkey SelectionKey key = iterator.next(); //监听到accept if (key.isAcceptable()) { SocketChannel sc = listenChannel.accept(); sc.configureBlocking(false); //将该sc注册到seletor sc.register(selector, SelectionKey.OP_READ); //提示 System.out.println(sc.getRemoteAddress() + " 上线 "); } //通道发送read事件,即通道是可读的状态 if (key.isReadable()) { //处理读 (专门写方法..) readData(key); } //当前的key删除,防止重复处理 iterator.remove(); } } else { System.out.println("等待...."); } } } catch ( Exception e) { e.printStackTrace(); } finally { //发生异常处理.... } } //读取客户端消息 private void readData(SelectionKey key) { //取到关联的channel SocketChannel channel = null; try { //得到channel channel = (SocketChannel) key.channel(); //创建buffer ByteBuffer buffer = ByteBuffer.allocate(1024); int count = channel.read(buffer); //根据count的值做处理 if (count > 0) { //把缓存区的数据转成字符串 String msg = new String(buffer.array()); //输出该消息 System.out.println("form 客户端: " + msg); //向其它的客户端转发消息(去掉自己), 专门写一个方法来处理 sendInfoToOtherClients(msg, channel); } } catch (IOException e) { try { System.out.println(channel.getRemoteAddress() + " 离线了.."); //取消注册 key.cancel(); //关闭通道 channel.close(); } catch (IOException e2) { e2.printStackTrace(); } } } //转发消息给其它客户(通道) private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException { System.out.println("服务器转发消息中..."); //遍历所有注册到selector上的SocketChannel,并排除self for (SelectionKey key : selector.keys()) { //通过key取出对应的SocketChannel Channel targetChannel = key.channel(); //排除自己 if (targetChannel instanceof SocketChannel && targetChannel != self) { //转型 SocketChannel dest = (SocketChannel) targetChannel; //将msg存储到buffer ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); //将buffer的数据写入通道 dest.write(buffer); } } } public static void main(String[] args) { //创建服务器对象 GroupChatServer groupChatServer = new GroupChatServer(); groupChatServer.listen(); } }
-
客户端
public class GroupChatClient { //定义相关的属性 //服务器的ip private final String HOST = "127.0.0.1"; //服务器端口 private final int PORT = 6667; private Selector selector; private SocketChannel socketChannel; private String username; //构造器, 完成初始化工作 public GroupChatClient() throws IOException { selector = Selector.open(); //连接服务器 socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT)); //设置非阻塞 socketChannel.configureBlocking(false); //将channel注册到selector socketChannel.register(selector, SelectionKey.OP_READ); //得到username username = socketChannel.getLocalAddress().toString().substring(1); System.out.println(username + " is ok..."); } //向服务器发送消息 public void sendInfo(String info) { info = username + " 说:" + info; try { socketChannel.write(ByteBuffer.wrap(info.getBytes())); } catch (IOException e) { e.printStackTrace(); } } //读取从服务器端回复的消息 public void readInfo() { try { int readChannels = selector.select(); //有可以用的通道 if (readChannels > 0) { Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); //得到相关的通道 if (key.isReadable()) { SocketChannel sc = (SocketChannel) key.channel(); //得到一个 Buffer ByteBuffer buffer = ByteBuffer.allocate(1024); //读取 sc.read(buffer); //把读到的缓冲区的数据转成字符串 String msg = new String(buffer.array()); System.out.println(msg.trim()); } } //删除当前的selectionKey, 防止重复操作 iterator.remove(); } else { //System.out.println("没有可以用的通道..."); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { //启动我们客户端 GroupChatClient chatClient = new GroupChatClient(); //启动一个线程, 每隔3秒,读取从服务器发送数据 new Thread() { public void run() { while (true) { chatClient.readInfo(); try { Thread.currentThread().sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); //发送数据给服务器端 Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { String s = scanner.nextLine(); chatClient.sendInfo(s); } } }
3.14 NIO与零拷贝
3.14.1 零拷贝基本介绍
- 零拷贝是网络编程的关键,很多性能优化都离不开。
- 在
Java
程序中,常用的零拷贝有mmap
(内存映射) 和sendFile
。那么,他们在OS
里,到底是怎么样的一个的设计?我们分析mmap
和sendFile
这两个零拷贝。
3.14.2 传统IO数据读写
File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file,"rw");
byte[] arr = new byte[(int)file.length()];
raf.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
3.14.3 传统IO模型
DMA: direct memory access
直接内存拷贝(不使用CPU
)。
3.14.4 mmap 优化
mmap
通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网 络传输时,就可以减少内核空间到用户空间的拷贝次数。mmap
示意图:
3.14.5 sendFile优化
Linux 2.1
版本 提供了sendFile
函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到Socket Buffer
,同时,由于和用户态完全无关,就减少了一次上下文切换。- 示意图和小结
- 提示:零拷贝从操作系统角度,是没有
cpu
拷贝。 Linux
在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到Socket buffer
的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。
- 这里其实有一次
cpu
拷贝kernel buffer -> socket buffer
但是,拷贝的信息很少,比如lenght
,offset
, 消耗低,可以忽略。
3.14.6 零拷贝的再次理解
- 零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有
kernel buffer
有一份数据)。 - 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的
CPU
缓存伪共享以及无CPU
校验和计算。
3.14.7 mmap 和 sendFile 的区别
mmap
适合小数据量读写,sendFile
适合大文件传输。mmap
需要 4 次上下文切换,3 次数据拷贝;sendFile
需要 3 次上下文切换,最少 2 次数据拷贝。sendFile
可以利用DMA
方式,减少CPU
拷贝,mmap
则不能(必须从内核拷贝到Socket
缓冲区)。
3.14.8 NIO 零拷贝案例
- 使用传统的
IO
方法传递一个大文件。 - 使用
NIO
零拷贝方式传递(transferTo
)一个大文件。 - 看看两种传递方式耗时时间分别是多少。
3.15 JavaAIO 基本介绍
JDK 7
引入了Asynchronous I/O
,即AIO
。在进行I/O
编程中,常用到两种模式:Reactor
和Proactor
。Java
的NIO
就是Reactor
,当有事件触发时,服务器端得到通知,进行相应的处理。AIO
即NIO2.0
,叫做异步不阻塞的IO
。AIO
引入异步通道的概念,采用了Proactor
模式,简化了程序编写, 有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接 数较多且连接时间较长的应用。- 目前
AIO
还没有广泛应用,Netty
也是基于NIO
,而不是AIO
。