1.JAVA NIO 基本介绍
1)Java NIO 全称 java non-blocking IO,是指 JDK提供的新API。从JDK1.4 开始,JAVA提供了一系列改进的输入/输出的新特性,被统称为NIO(即 New IO )
,是同步非阻塞的
2)NIO相关类都被放在java.nio 包及子包下,并且对原java.io 包中的很多类进行改写
3)NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
4)NIO 是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中灵活性,使用它可以提供非阻塞式
的高伸缩性网络
5)JAVA NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,
所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的
事情。
6)通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50到100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。
7)HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1 大了好几个数量级
8)案例说明NIO的Buffer
package com.gezongyang.bio;
import java.nio.IntBuffer;
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 的读写切换
intBuffer.flip();
//设置缓存的位置
intBuffer.position(0);
System.out.println(intBuffer.get());
intBuffer.limit(3);
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
2.NIO 和 BIO的比较
1)BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多
2)BIO是阻塞的,NIO则是非阻塞的
3)BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
3.NIO 三大核心原理示意图
一张图描述NIO的 Selector、channel和Buffer的关系
1)每个channel 都对应一个Buffer
2)Selector 对应一个线程,一个线程对应多个channel(连接)
3)该图反应了有三个channel注册到该selector程序
4)selector切换到哪个channel是由事件(Event)决定的
5)Buffer是一个内存块,底层由数组实现
6)数据的读取写入是通过Buffer,NIO中的Buffer即可以读也可以写,需要flip方法切换
channel(双向),BIO中要么输入流,要么输出流,不能双向。
4.缓冲区(Buffer)
1)基本介绍
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成一个容器对象(含数组),该对象提供了一组方法,可以
更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,
但是读取或写入的数据都必须通过buffer
5.通道(Channel)
1)基本介绍
NIO的通道类似于流,但有些区别如下:
-
通道可以同时进行读写,而流只能读或者只能写
-
通道可以实现异步读写数据
-
通道可以从缓冲区读数据,也可以写数据到缓冲区
2)BIO中的stream 是单向的,例如FileInputStream 对象只能进行读取数据的操作,而NIO中的通道(Channel)
是双向的,可以读操作,也可以写操作
3)Channel 在NIO中是一个接口
4)常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel 和 SocketChannel,FileChannel
用于文件的数据读写;DatagramChannel 用于UDP的数据读写;ServerSocketChannel和SocketChannel 用于TCP数据读写。
6.FileChannel类
代码实例 1
1)使用前面学习后的ByteBuffer(缓冲)和FileChannel,将“hello”写入到file01.txt 中
2)文件不存在就创建
package com.gezongyang.bio;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel01 {
public static void main(String[] args) throws IOException {
String str = "hello";
//创建一个输出流-> channel
FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
//通过fileOutputString 获取对应的FileChannel
FileChannel channel = fileOutputStream.getChannel();
//创建一个缓冲区 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将str 放入byteBuffer
byteBuffer.put(str.getBytes());
byteBuffer.flip();
//将byteBuffer 数据写入到fileChannel
channel.write(byteBuffer);
fileOutputStream.close();
}
}
代码实例2;(本地文件读数据)
1)将file01.txt中的数据读入到程序,并显示在控制台屏幕
package com.gezongyang.bio;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel02 {
public static void main(String[] args) throws IOException {
//创建文件的输入流
File file = new File("d:\\file01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel channel = fileInputStream.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
channel.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
}
}
代码实例3:
1)拷贝文件file01.txt,放在项目下
package com.gezongyang.bio;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel03 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("d:\\file01.txt");
FileChannel channel1 = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("file02.txt");
FileChannel channel2 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
//循环读取
while (true) {
byteBuffer.clear();//清空buffer
int read = channel1.read(byteBuffer);
System.out.println("read=" + read);
//表示读完
if (read == -1) {
break;
}
//将buffer 中的数据写入到 Channel2
byteBuffer.flip();
channel2.write(byteBuffer);
}
}
}
代码实例4:
1)使用FileChannel(通道)和方法 transferFrom,完成文件的拷贝
package com.gezongyang.bio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class NIOFileChannel04 {
public static void main(String[] args) throws IOException {
FileInputStream inputStream = new FileInputStream("d:\\file01.txt");
FileOutputStream outputStream = new FileOutputStream("d:\\file03.txt");
FileChannel channel1 = inputStream.getChannel();
FileChannel channel2 = outputStream.getChannel();
//使用transterFrom 完成拷贝
channel2.transferFrom(channel1, 0, channel1.size());
channel1.close();
channel2.close();
inputStream.close();
outputStream.close();
}
}
代码示例5:
1)NIO 提供MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成
package com.gezongyang.bio;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class MappedByteBufferTest {
public static void main(String[] args) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
//获取对应的通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数1:使用读写模式
* 参数2:可以直接修改的起始位置
* 参数3:映射到内存的大小,即将1.txt的
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'H');
randomAccessFile.close();
System.out.println("修改成功~~");
}
}
代码示例6:前面我们讲的读写操作,都是通过一个Buffer完成的,NIO还支持通过Buffer(即Buffer数组)完成读写操作,即Scattering 和Gathering 【举例说明】
package com.gezongyang.bio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
/**
* Scattering:将数据写入buffer时,可以采用buffer数组,依次写入
* Gathering:从buffer读取数据时,可以采用buffer数组,依次读
*/
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;
//循环的读取
while (true) {
int byteRead = 0;
while (byteRead < messageLength) {
long l = socketChannel.read(byteBuffers);
//累计读取的字节数
byteRead += 1;
System.out.println("byteRead=" + byteRead);
//使用流打印,看看当前的这个buffer 的position 和 limit
Arrays.asList(byteBuffers).stream().map(buffer -> "position=" + buffer.position() +
",limit=" + buffer.limit()).forEach(System.out::println);
}
//将所有的buffer进行flip
Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.flip());
//将数据读出显示到客户端
long byteWrite = 0;
while (byteWrite < messageLength) {
long l = socketChannel.write(byteBuffers);
byteWrite += 1;
}
//将所有的buffer进行clear
Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());
System.out.println("byteRead=" + byteRead + "byteWrite=" + byteWrite + ",messagelength" + messageLength);
}
}
}
7.Selector(选择器)
7.1 基本介绍
1)Java 的NIO,用非阻塞的IO方式。可以用一个线程,处理多个客户端连接,就会使用到Selector(选择器)
2)Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel 以事件的方式可以注册到同一个Selector),
如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
3)只有在连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
4)避免了多线程之间的上下文切换导致的开销
7.2 Selector 示意图和特点说明
说明如下:
1)Netty 的IO线程NioEventLoop 聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
2)当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
3)线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
4)由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起。
5)一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
7.3 Selector 类相关方法
Selector 类是一个抽象类,常用方法和说明如下:
public abstract class Selector implements Closeable {
public static Selector open(); //得到一个选择器对象
public int select(long timeout); //监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
public Set<SelectionKey> selectedKeys(); //从内部集合中得到所有的SelectionKey
}
7.4 注意事项
1)NIO中的ServerSocketChannel 功能类似ServerSocket,SocketChannel 功能类似Socket
2)selector 相关方法说明
selector.select() //阻塞
selector.select(); //阻塞1000毫秒,在1000毫秒后返回
selector.wakeup(); //唤醒 selector
selector.selectNow(); //不阻塞,立马返还
8.NIO 非阻塞网络编程原理分析图
对上图的说明:
1)当客户端连接时,会通过ServerSocketChannel 得到SocketChannel
2)将socketChannel 注册到Selector上,register(Selector sel, int ops),一个selector上可以注册多个SocketChannel
3)注册后返回一个SelectionKey,会和该Selector 关联
4)Selector 进行监听,返回有事件发生的通道的个数,进一步获取有事件发生的SelectionKey,再通过SelectionKey反向获取
SocketChannel
5)通过得到的channel,完成业务处理
9 NIO非阻塞网络编程案例
案例要求:
1)编写一个NIO入门案例,实现服务器端和客户端之间的数据简单通讯
2)目的:理解NIO非阻塞网络编程机制
3)代码演示
NIOServer
-
创建ServerSocketChannel
-
获取一个Selector对象
-
绑定6666端口,在服务端监听
-
将serverSocketChannel 注册到Selector
-
循环等待客户端连接
-
获取关注事件的集合
-
通过key 反向获取对应的channel
-
处理业务逻辑
package com.gezongyang.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selector 对象
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()));
}
keyIterator.remove();
}
}
}
}
NIOClient
package com.gezongyang.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws IOException {
//获取网络通道
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();
}
}
SelectionKey
1)SelectionKey, 表示Selector 和网络通道的注册关系,共四种:
int OP_ACCEPT: 有新的网络连接可以accept,值为16
int OP_CONNECT: 代表连接已经建立,值为8
int OP_READ: 代表读操作,值为1
int OP_WRITE: 代表写操作,值为4
10.NIO 网络编程应用实例-群聊系统
实例要求:
1)编写一个 NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
2)实现多人群聊
3)服务器端:可以监测用户上线,离线,并实现消息转发功能
4)客户端:通过channel 可以无阻塞发送消息给其他所有用户,同时可以接受其他用户发送的消息(有服务器转发得到)
代码实现:
package com.gezongyang.chat;
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;
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;
System.out.println("send info is " + 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 (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
//启动我们客户端
GroupChatClient chatClient = new GroupChatClient();
//启动一个线程,每个3秒。读取从服务器发送数据
new Thread(){
public void run() {
while (true) {
chatClient.readInfo();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
//发送数据给服务器端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String s = scanner.nextLine();
chatClient.sendInfo(s);
}
}
}
package com.gezongyang.chat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
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) {
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 注册到selector
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("from 客户端:" + msg);
//向其它的客户端转发消息,专门写一个方法来处理
sendInfoToOtherClients(msg, channel);
}
} catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress() + " 离线了..");
//取消注册
key.cancel();
//关闭通道
channel.close();
} catch (IOException e1) {
e1.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();
}
}
11.NIO与零拷贝
11.1 传统 IO 数据读写
DMA:direct memory access 直接内存拷贝(不使用CPU)
11.2 mmap 优化
1)mmap 通过内存映射,将文件映射到内核缓冲区,同时用户空间可以共享内核空间的数据。在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数
2)mmap 示意图
11.3 sendFile 优化
1)Linux 2.1 版本提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时由于和用户态完全无关,就减少了一次上下文切换
2)示意图
3)提示:零拷贝从操作系统角度来看是没有cpu拷贝
4)Linux 在2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝协议栈,从而再一次减少了数据拷贝。具体如下图和小结:
5)这里其实有一次cpu 拷贝
kernel buffer -> socket buffer
但是,拷贝的信息很少,比如 lengh,offset,消耗低,可以忽略
11.4 零拷贝总结
1)我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有kernel buffer 有一份数据)。
2)零拷贝不仅仅带来了更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算。
mmap和 sendFile的区别
1)mmap 适合小数据量读写,sendFile适合大文件传输。
2)mmap 需要4次上下文切换,3次数据拷贝;sendFile需要3次上下文切换,最少两次数据拷贝。
3)sendFile 可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)。
11.5 BIO、NIO、AIO对比表
BIO | NIO | AIO | |
IO模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
编程难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |