互联网行业
游戏行业
大数据领域
I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
Java 共支持 3 种网络编程模型 I/O 模式:BIO、NIO、AIO。
Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理
Java AIO(NIO.2):异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
public static void main(String[] args) throws IOException {
//线程池机制
//思路
//1. 创建一个线程池
// 2. 如果有客户端连接 就创建 一个客户端与之通信
ExecutorService threadPool = Executors.newCachedThreadPool();
//创建一个ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器端启动了");
while(true)
{
//监听 等待客户端连接
final Socket accept = serverSocket.accept();
System.out.println("连接到一个客户端");
//创建一个线程与之通信
threadPool.execute(new Runnable() {
@Override
public void run() {
//和客户端进行通信
handler(accept);
}
});
}
}
//编写一个handler方法,与客户端通信
public static void handler (Socket socket)
{
try {
byte[] bytes=new byte[1024];
//获取socket输入流
try {
InputStream inputStream = socket.getInputStream();
//循环读取客户端发送的数据
while (true)
{
System.out.println("线程id:"+Thread.currentThread().getId()+"线程名字"+Thread.currentThread().getName());
//读取客户端发送的数据
int read = inputStream.read(bytes);
if(read!=-1)
{
String s = new String(bytes, 0, read);
System.out.println(s);
}
else
{
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
finally {
System.out.println("关闭和client的连接");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
import java.nio.IntBuffer;
public class BasicBuffer {
public static void main(String[] args) {
//举例说明buffer的使用
//创建一个大小为5的buffer 即可以存放5个int
IntBuffer buffer = IntBuffer.allocate(5);
for(int i=0;i<buffer.capacity();i++)
{
buffer.put(i*2);
}
//如何从buffer中获取数据
//将buffer进行读写转换
buffer.flip();
while (buffer.hasRemaining())
{
System.out.println(buffer.get());
}
}
}
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
从前面可以看出对于 Java 中的基本数据类型(boolean 除外),都有一个 Buffer 类型与之相对应,最常用的自然是 ByteBuffer 类(二进制数据),该类的主要方法如下:
FileChannel 主要用来对本地文件进行 IO 操作,常见的方法有
方法 | 说明 |
---|---|
public int read(ByteBuffer dst) | 从通道读取数据并放到缓冲区中 |
public int write(ByteBuffer src) | 把缓冲区的数据写到通道中 |
public long transferFrom(ReadableByteChannel src, long position, long count) | 从目标通道中复制数据到当前通道 |
public long transferTo(long position, long count, WritableByteChannel target) | 把数据从当前通道复制给目标通道 |
实例要求:
import io.netty.buffer.ByteBuf;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
public class NIOFileChannel01 {
public static void main(String[] args) throws IOException {
String s="hollow word";
//创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("./1.txt");
//通过fileOutputStream获取对应的通道
FileChannel channel = fileOutputStream.getChannel();
//创建一个缓存字节流
ByteBuffer buffer = ByteBuffer.allocate(1024);
//向缓冲流中写入数据
buffer.put(s.getBytes(StandardCharsets.UTF_8));
//对缓存流进行flip
buffer.flip();
channel.write(buffer);
fileOutputStream.close();
}
}
实例要求:
public static void main(String[] args) throws IOException {
//创建文件输入流
File file = new File("./1.txt");
FileInputStream fileInputStream = new FileInputStream(file);
//通过FileInputStream获取对应的FileChannel
FileChannel channel = fileInputStream.getChannel();
//创建缓存区
ByteBuffer buffer = ByteBuffer.allocate((int)file.length());
//通过通道将数据读到缓存区
channel.read(buffer);
String s = new String(buffer.array());
System.out.println(s);
fileInputStream.close();
}
实例要求:
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("./1.txt");
FileChannel channel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("./2.txt");
FileChannel channel1 = fileOutputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true)
{
//情况buffer
buffer.clear();
int read = channel.read(buffer);
if(read==-1)
{
break;
}
buffer.flip();
channel1.write(buffer);
}
fileInputStream.close();
fileOutputStream.close();
}
}
实例要求:
使用 FileChannel(通道)和方法 transferFrom,完成文件的拷贝
拷贝一张图片
代码演示
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class NIOFileChannel04 {
public static void main(String[] args) throws IOException {
//创建相关的流
FileOutputStream fileOutputStream = new FileOutputStream("./b.jpg");
FileInputStream fileInputStream = new FileInputStream("./a.jpg");
//获取各个流对应的通道
FileChannel channel = fileOutputStream.getChannel();
FileChannel channel1 = fileInputStream.getChannel();
//使用transferFrom完成拷贝
channel.transferFrom(channel1,0,channel1.size());
//关闭相应的流和通道
channel.close();
channel1.close();
fileInputStream.close();
fileOutputStream.close();
}
}
import java.nio.ByteBuffer;
public class NIOByteBufferPutGet {
public static void main(String[] args) {
//创建一个 Buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
//类型化方式放入数据
buffer.putInt(100);
buffer.putLong(9);
buffer.putChar('尚');
buffer.putShort((short) 4);
//取出
buffer.flip();
System.out.println();
System.out.println(buffer.getInt());
System.out.println(buffer.getLong());
System.out.println(buffer.getChar());
System.out.println(buffer.getShort());
}
}
import java.nio.ByteBuffer;
public class ReadOnlyBuffer {
public static void main(String[] args) {
//创建一个 buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
for (int i = 0; i < 64; i++) {
buffer.put((byte) i);
}
//读取
buffer.flip();
//得到一个只读的 Buffer
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println(readOnlyBuffer.getClass());
//读取
while (readOnlyBuffer.hasRemaining()) {
System.out.println(readOnlyBuffer.get());
}
readOnlyBuffer.put((byte) 100); //ReadOnlyBufferException
}
}
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* 说明 1.MappedByteBuffer 可让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次
*/
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
//获取对应的通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数 1:FileChannel.MapMode.READ_WRITE 使用的读写模式
* 参数 2:0:可以直接修改的起始位置
* 参数 3:5: 是映射到内存的大小(不是索引位置),即将 1.txt 的多少个字节映射到内存
* 可以直接修改的范围就是 0-5 5为大小而不是索引
* 实际类型 DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'H');
mappedByteBuffer.put(3, (byte) '9');
mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException
randomAccessFile.close();
System.out.println("修改成功~~");
}
}
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 Exception {
//使用 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 -> "position = " + 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);
}
}
}
注意事项
NIO
中的ServerSocketChannel
功能类似ServerSocket
、SocketChannel
功能类似 Socket
。Selector
相关方法说明
selector.select();
//阻塞selector.select(1000);
//阻塞 1000 毫秒,在 1000 毫秒后返回selector.wakeup();
//唤醒 selectorselector.selectNow();
//不阻塞,立马返还NIO 非阻塞网络编程相关的(Selector、SelectionKey、ServerScoketChannel 和 SocketChannel)关系梳理图
对上图的说明:
案例:
编写一个 NIO 入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)
目的:理解 NIO 非阻塞网络编程机制
NIOServer
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
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();
//绑定服务器端监听的端口
serverSocketChannel.bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把ServerSocketChannel注册到selector 时间为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while (true)
{
//等待一秒如果没有时间发生,返回
if(selector.select(1000)==0)
{
System.out.println("服务器等待一秒,无连接");
continue;
}
/**
* 返回值>0 获取相关的selectKey集合
* 1. 如何返回值> 0 表示已经获取到关注的事件
* 2. selector.selectedKeys() 返回关注事件的集合
* 通过selectionKeys 反向获取通道
*/
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历Set 使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext())
{
//获取SelectKey
SelectionKey key = keyIterator.next();
//根据key对应的通道做出响应的处理
if(key.isAcceptable())//如果是OP_ACCEPT 表示有新的客户端进行连接
{
//该客户端生成一个SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//将SocketChannel设置为非阻塞
socketChannel.configureBlocking(false);
//将客户端注册到selector 关注时间为OP_READ 同时给SocketChannel关联一个buffer
System.out.println("客户端连接成功");
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if(key.isReadable())//发生 OP_READ
{
//通过key获取对应的channel
SocketChannel channel =(SocketChannel) key.channel();
//获取与之相关联的buffer
ByteBuffer buffer =(ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("from 客户端 "+new String(buffer.array()));
}
//收到从几个中基础当前的selectionKey防止重复操作
keyIterator.remove();
}
}
}
}
NIOClient
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
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 s="hollow word";
ByteBuffer buffer = ByteBuffer.wrap(s.getBytes(), 0, s.length());
//发送数据 将buffer数据写入channel
socketChannel.write(buffer);
System.in.read();
}
}
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;
实例要求:
public class GroupChatServer {
//定义属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6667;
//构造器
//初始化工作
public GroupChatGroup() {
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() + " 上线 ");
}
if (key.isReadable()) {//通道发送read事件,即通道是可读的状态
// 处理读(专门写方法..)
readData(key);
}
//当前的 key 删除,防止重复处理
iterator.remove();
}
} else {
System.out.println("等待....");
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//发生异常处理....
}
}
//读取客户端消息
public void readData(SelectionKey key) {
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) {
//创建服务器对象
GroupChatGroup groupChatServer = new GroupChatGroup();
groupChatServer.listen();
}
}
客户端
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.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
public class GroupChatClient {
//定义相关的属性
private final String HOST = "127.0.0.1";//服务器的ip
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(HOST, 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());
}
}
iterator.remove(); //删除当前的 selectionKey,防止重复操作
} 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);
}
}
}
零拷贝是网络编程的关键,很多性能优化都离不开。
在 Java 程序中,常用的零拷贝有 mmap(内存映射)和 sendFile。那么,他们在 OS 里,到底是怎么样的一个的设计?我们分析 mmap 和 sendFile 这两个零拷贝
零拷贝是从操作系统上面看的即没有CPU拷贝
另外我们看下 NIO 中如何使用零拷贝
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);
传统IO模型
DMA:direct memory access 直接内存拷贝(不使用 CPU)
Hard drive:硬盘
Kernel buffer:内核缓存区
user buffer:用户缓存区
protocol engine:协议及协议栈
mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据(kernel buffer和user buffer共享数据)。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据)。
零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。
BIO | NIO | AIO | |
---|---|---|---|
IO模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非 阻塞 |
编程难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |