参考视频:https://www.bilibili.com/video/av76223318?p=5
I/O模型简单的解释:用什么样的通道进行数据的发送和接收,很大程度上决定了程序通讯的性能
Java共支持三种网络编程模型:BIO,NIO,AIO
BIO:Blocking IO
同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时,服务器端就需要启动一个线程进行处理,如果这个连续不做任何事情会造成不必要的线程开销。可以通过线程池机制改善(实现多个客户连接服务器)。
放在java.io包下
适用场景:
连接数目小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中。jdk1.4之前的唯一选择,但程序简单易理解
BIO简单流程:
1 服务器端启动一个ServerSockert
2 客户端启动Socket对服务器进行通讯,默认情况下服务器端需要对每个客户建立一个线程与之通讯
3 客户端发送请求后,先咨询服务器是否有线程响应
3.1 如果没有响应,则会等待,或者被拒绝
3.2 如果有响应,客户端线程会等待请求结束后,再继续执行
public class BioServerSocket {
public static void main(String[] args) throws Exception{
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
//用线程池来管理线程
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
System.out.println("===== 启动ServerSocket");
//启用并监听
while (true){
//等待并监听
Socket socket = serverSocket.accept();
//获得监听后启用线程来处理
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
byte[] bytes = new byte[1024];
try {
InputStream inputStream = socket.getInputStream();
int read;
while ((read = inputStream.read(bytes)) != -1){
System.out.println(new String(bytes,0,read));
}
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
}
}
利用命令窗口的telnet模仿客户端发送请求
telnet命令若需要启动:https://jingyan.baidu.com/article/7908e85c6ec355af491ad265.html
连接命令:telnet 127.0.0.1 6666
作为客户端发送命令:Ctrl ]
发送内容命令:send XXXXX
实际发送内容为XXXX
NIO:Non-blocking/New IO
同步非阻塞,服务器实现为一个线程处理多个请求,即客户端发送的连接请求都会注册到多路复用器(Selector选择器)上,多路复用器轮询到连接有I/O请求就进行处理
放在java.nio包下
适用场景:
连接数目多且连接时间短(轻操作)的架构,比如聊天服务器,弹屏系统,服务期间通讯等。编程比较复杂,jdk1.4之后开始
三大核心部分:Channel通道,Buffer缓冲区,Selector选择器
NIO是面向缓冲区,或者面向块编程,是Channel的事件Event驱动的
BIO 和 NIO 比较:
1 BIO以流的方式处理数据,NIO以块的方式处理数据,块IO的效率比流IO的效率要高很多
2 BIO是阻塞的,NIO是非阻塞的
3 BIO是基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件,因此使用单个线程可以监听多个客户端通道
Buffer缓冲区:
本质是一个可以读写数据的内存块,可以理解成一个容器对象(含数组),该对象提供一组方法,可以更轻松的使用内存块。缓冲区对象内置了一些机制,可以跟踪和记录缓冲区的状态变化情况。Channel提供可以从文件,网络读取数据的渠道,但读取和写入的数据必须经由Buffer。
子类都有四个重要属性:
Capatity: 容量,即可以容纳的最大数据量。在缓冲区创建时被设定并且不能改变。
Limit: 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写,且极限是可以修改的
Position: 位置,下一个要被读或者写的元素的索引,每次读写缓冲区时都会改变该值,为下次读写做准备
Mark:标记
static void Test(){
ByteBuffer byteBuffer = ByteBuffer.allocate(5); //创建缓冲区
ByteBuffer direct = ByteBuffer.allocateDirect(5);//创建直接缓冲区
byteBuffer.put("h".getBytes()[0]);
byteBuffer.put("s".getBytes()[0]);
byteBuffer.put("s".getBytes()[0]);
byteBuffer.put("n".getBytes()[0]);
byteBuffer.put("j".getBytes()[0]);
byteBuffer.flip(); //切换读写
while (byteBuffer.hasRemaining()){
System.out.println(byteBuffer.get());
}
System.out.println(" ================ ");
byteBuffer.put(1,"k".getBytes()[0]);
System.out.println("第一个元素:"+byteBuffer.get(1));
System.out.println("第二个元素:"+byteBuffer.get(2));
System.out.println("容量:"+byteBuffer.capacity());
System.out.println("位置:"+byteBuffer.position());
System.out.println(".. 具体诸多其他方法搜搜就好了");
}
Channel通道:
类似于流,但有区别
1 通道能同时进行读写,而流只能进行读或者只能写
2 通道能异步进行读写数据
3 通道能从缓冲读取数据,也能写入缓冲
Channel在java.nio中是接口,具体常用的实现类:FileChannel(文件数据读写), DatagramChannel(UDP的数据读写), ServerSocketChannel(TCP数据读写), SocketChannel(TCP数据读写)
FileChannel:
方法:
read(ByteBuffer des); 通道读取数据,放到缓存区
write(ByteBuffer tar); 读取缓冲区数据,放到通道
transferFrom(....); 从目标通道复制数据到当前通道
transferTo(....); 从当前通道复制数据到目标通道
static void test2() throws Exception {
//发送数据
String str = "Hi,女孩";
//封装的Buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(str.getBytes());
//buffer反转
byteBuffer.flip();
//最终存储地方
FileOutputStream fileInputStream = new FileOutputStream("d://hiGirl.text");
//获得通道
FileChannel channel = fileInputStream.getChannel();
//缓冲区读取数据到通道
channel.write(byteBuffer);
//关闭流
fileInputStream.close();
}
static void test4() throws Exception{
File file = new File("d://hiGirl.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream("d://hiGir2.txt");
FileChannel channel = fileInputStream.getChannel();
FileChannel outputStreamChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
while (true){
byteBuffer.clear();
int read = channel.read(byteBuffer);
if (read == -1) break;
byteBuffer.flip();
outputStreamChannel.write(byteBuffer);
byteBuffer.flip();
}
fileInputStream.close();
fileOutputStream.close();
}
static void test5() throws Exception{
File file = new File("d://hiGirl.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream("d://hiGir4.txt");
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileChannel outputStreamChannel = fileOutputStream.getChannel();
inputStreamChannel.transferTo(0,inputStreamChannel.size(),outputStreamChannel);
// 或 outputStreamChannel.transferFrom(inputStreamChannel,0,inputStreamChannel.size());
inputStreamChannel.close();
outputStreamChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
MapperedByteBuffer:
可以让文件直接在内存(对外内存)中进行修改(操作系统不需要copy),而如何同步到文件由NIO完成
static void test6() throws Exception{
RandomAccessFile randomAccessFile = new RandomAccessFile("d://hiGirl.txt","rw");
FileChannel channel = randomAccessFile.getChannel();
/**
* FileChannel.MapMode
* READ_ONLY: 只读
* READ_WRITE:读写
* PRIVATE: private (copy-on-write)
*
* 定义可以修改的范围
* position: 可以修改的起始位置
* size: 映射内存大小
*/
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
map.put(0,(byte) 'H');
map.put(2,(byte) 'L');
randomAccessFile.close();
}
scattering:将数据写入到Buffer时,可以采用Buffer数组,依次写入
gatthering:将数据读取到Buffer时,可以采用Buffer数组,依次读取
static void test7() throws Exception{
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
SocketChannel accept = serverSocketChannel.accept();
//设置数组Buffer
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(3);
byteBuffers[1] = ByteBuffer.allocate(5);
int mesLength = 3+5;
while (true){
long byteRead = 0;
while (byteRead < mesLength){
long read = accept.read(byteBuffers);
byteRead += read;
System.out.println("byteRead: "+byteRead);
Arrays.asList(byteBuffers).stream().map(buffer -> "position: "+buffer.position()+" ,limit: "+buffer.limit())
.forEach(System.out::println);
}
//将所有buffer进行反转可以进行其他操作
Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.flip());
//将数据读出显示到客户端
long byteWrite = 0;
while (byteWrite < mesLength){
long write = accept.write(byteBuffers);
byteWrite += write;
}
//复位
Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());
System.out.println("byteRead = "+byteRead + " byteWrite = "+byteWrite);
}
}
selector:
selector能够检测多个注册的通道上是否有事件发生(多个Channel可以以事件的方式注册到注册到同一个Selector),如果有事件发生便获取事件,然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
1 Netty的IO线程聚合了Selector选择器,可以同时并发处理成百上千个并发请求
2 当线程从某客户端Socket通道进行读写数据时,若没有线程可用时,可进行其他操作
3 线程通常将阻塞IO的空闲时间用于其他通道上执行IO操作,所以单个线程可以管理多个输入和输出通道
4 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起
5 一个IO线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞IO一连接一线程模型,架构的性能,弹性伸缩能力和可靠性都得到了极大的提升
Selector抽象类
public abstract class Selector implements Closeable {
//得到一个选择器对象
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
//监控所有注册通道,当其中有IO操作可进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
//select()为阻塞方法,至少有一个事件发生才会返回
//select(long timeout) 非阻塞,若无事件发生,超时时间后也会返回
//selectNow() 非阻塞,有事件发生立马返回
//wakeup() 唤醒selector
public abstract int select(long timeout)
throws IOException;
//从内部集合中得到所有SelectionKey
public abstract Set<SelectionKey> selectedKeys();
}
原理
1 当有客户端连接时,会通过ServerSocketChannel得到对应的SocketChannel
2 对应的SocketChannel注册倒Selector上,一个Selector上可以注册多个SocketChannel
----- SelectionKey register(Selector sel, int ops,Object att)
3 注册后返回一个SelectionKey,会和该Selector关联
4 Selector进行监听,用select()方法 ,会返回有事件发生的通道的个数
5 进一步得到各个SelectionKey
6 再通过SelectionKey反向获取SocketChannel channel()
7 可以通过得到的channel,完成业务处理
OP_ACCEPT: 有新的网络连接可以accept, 1<<4 = 16
OP_CONNECT:代表连接已经建立:1<<3=8
OP_READ:代表读操作:1<<0 = 1
OP_WRITE:代表写操作:1<<2=4
零拷贝:零拷贝不是不拷贝,而是没有CPU拷贝
零拷贝是网络编程的关键,很多性能优化都离不开。在java程序中,常用的零拷贝mmap(内存映射)和sendFile。那么它们在OS里到底是一个怎样的设计?
1 我们说的零拷贝,是从操作系统角度来说。因为内核缓冲区之间,没有数据是重复的(只有kernel buffer有一份数据)
2 零拷贝不仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算
(用户态,kernel[内核态],硬件)
mmap:
文件映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数
SendFile:
Linux2.1提供了sendFile函数,基本原理:数据不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时由于和用户态完全无关,就减少了一次上下文切换
Linux2.4对sendFile函数进行了优化,避免了从内核缓冲区拷贝到SocketBuffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝
mmap 和sendfile区别
1 mmap适合小数据量读写,sendfile适合大文件传输
2 mmap需要3次上下文切换,3次数据拷贝;sendfile需要2次上下文切换,最少2次数据拷贝
3 sendfile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)
传统拷贝:
4次拷贝:
硬件 -> DMA拷贝到 -> 内核态(kernelBuffer)
内核态(kernelBuffer) -> cpu拷贝到 ->用户buffer
用户buffer -> cpu拷贝到 -> socket buffer
socket buffer -> DMA拷贝到 -> 协议栈
3次切换:硬件,内核态, 用户
MMAP拷贝:
3次拷贝:
硬件 -> DMA拷贝到 -> 内核态(kernelBuffer)
内核态(kernelBuffer) -> cpu拷贝到 -> socket buffer
socket buffer -> DMA拷贝到 -> 协议栈
3次切换:硬件,内核态, 用户
SendFile拷贝:
linux2.1版的sendfile中还有一次CPU拷贝:3次拷贝,2次切换
linux2.4版的才是零拷贝:2次拷贝,2次切换
零拷贝代码Test:
server:
public class NewIOServiceSocket {
public static void main(String[] args) throws Exception{
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open().bind(new InetSocketAddress(7000));
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
int readLength = 0;
while (readLength != -1){
readLength = socketChannel.read(byteBuffer);
byteBuffer.rewind();//倒带
}
}
}
}
client
public class NewIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",7000));
FileInputStream fileInputStream = new FileInputStream(new File("C:\\Users\\chinaoly\\Desktop\\12345.txt"));
FileChannel fileChannel = fileInputStream.getChannel();
long start = System.currentTimeMillis();
/**
* linux 环境下 transferTo 一次即可完成
* windows环境下,transferTo 一次最多传8M,大于8M分段传输,需要记住传输时的位置
*/
long count = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println(" 时间 : "+(System.currentTimeMillis() - start));
}
}
AIO(NIO.2):
异步非阻塞,AIO引入异步通道的概念,采用Proactor模式,简化了程序编写,有校的请求才启用线程,它的特点是先有操作系统完成后才通知服务器端启动线程去处理,一般适用于连接数较多且连接时间较长的应用。jdk1.7以后引入,但目前还未得到广泛运用
适用场景:
连接数目多且连接时间长(重操作)的架构。比如相册服务器,充分调用OS参与并发操作。编程负责,jdk7开始