学习使用nio编程
文章参考1:https://blog.csdn.net/haoyuyang/article/details/53231585
bio与nio的区别
socket编程就是使用的bio编程的方式.
socket使用:https://blog.csdn.net/sqlgao22/article/details/102858119
bio模型:
nio模型:
其本质就是阻塞和非阻塞的区别。
阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,那么程序就一直等着,直到传输完毕为止。
非阻塞概念:应用程序直接可以获取已经准备就绪的数据,无需等待。
IO为同步阻塞形式,NIO为同步非阻塞形式。NIO没有实现异步,在JDK1.7之后,升级了NIO库包,支持异步非阻塞通信模型,即NIO2.0(AIO)。
同步和异步:同步和异步一般是面向操作系统与应用程序对IO操作的层面上来区别的。①同步时,应用程序会直接参与IO读写操作,并且应用程序会直接阻塞到某一个方法上,直到数据准备就绪(BIO);或者采用轮询的策略实时检查数据的就绪状态,如果就绪则获取数据(NIO)。②异步时,则所有的IO读写操作都交给操作系统处理,与应用程序没有直接关系,应用程序并不关心IO读写,当操作系统完成IO读写操作时,会向应用程序发出通知,应用程序直接获取数据即可。
同步说的是Server服务端的执行方式,阻塞说的是具体的技术,接收数据的方式、状态(io、nio)。
3.NIO编程介绍
学习NIO编程,首先需要了解几个概念:
(1)Buffer(缓冲区)
Buffer是一个对象,它包含一些需要写入或者读取的数据。在NIO类库中加入Buffer对象,体现了新类库与原IO的一个重要区别。在面向流的IO中,可以直接将数据写入或读取到Stream对象中。在NIO类库中,所有的数据都是用缓冲区处理的(读写)。 缓冲区实质上是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类型的数组。这个数组为缓冲区提供了访问数据的读写等操作属性,如位置、容量、上限等概念,具体的可以参考API文档。
Buffer类型:最常使用的是ByteBuffer,实际上每一种java基本类型都对应了一种缓存区(除了Boolean类型)。
①ByteBuffer②CharBuffer③ShortBuffer④IntBuffer⑤LongBuffer⑥FloatBuffer⑦DoubleBuffer
(2)Channel(管道、通道)
在Java中(Socket类),基本的 I/O 操作(bind()、 connect()、 read()和 write())依赖于底层网络传输所提供的功能。
Channel就像自来水管道一样,网络数据通过Channel读取和写入,通道与流的不同之处在于通道是双向的,而流只能在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行,最关键的是可以和多路复用器集合起来,有多种的状态位,方便多路复用器去识别。通道分为两大类:一类是用于网络读写的SelectableChannel,另一类是用于文件操作的FileChannel,我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。
(3)Selector(选择器、多路复用器)
是NIO编程的基础,非常重要。多路复用器提供选择已经就绪的任务的能力。简单说,就是Selector会不断的轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。一个多路复用器(Selector)可以负责成千上万的通道(Channel),没有上限。这也是JDK使用了epoll代替传统的select实现,获得连接句柄(客户端)没有限制。那也就意味着我们只要一个线程负责Selector的轮询,就可以接入成千上万个客户端,这是JDK NIO库的巨大进步。
Selector线程类似一个管理者(Master),管理了成千上万个管道,然后轮询哪个管道的数据已经准备好了,通知CPU执行IO的读取或写入操作。
Selector模式:当IO事件(管道)注册到选择器以后,Selector会分配给每个管道一个key值,相当于标签。Selector选择器是以轮询的方式进行查找注册的所有IO事件(管道),当IO事件(管道)准备就绪后,Selector就会识别,会通过key值来找到相应的管道,进行相关的数据处理操作(从管道中读取或写入数据,写到缓冲区中)。每个管道都会对选择器进行注册不同的事件状态,以便选择器查找。
事件状态:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
NIO通信模型图解:
数据的中转状态
nio中的基本api
主要用的方法:
//创建一个缓冲区,int/string/byte等类型都有
ByteBuffer buffer = ByteBuffer.allocate(1024);
//清空缓冲区
buffer.clear();
//复位缓冲区
buffer.flip();
//转为字节数组
buffer.array()
//放入数据
byteBuffer.put("第一个nio网络程序".getBytes("utf8"));
api演示:
@Test
public void test01() {
//基本操作
//1.创建指定长度的缓冲区(创建一个可以读写的HeapIntBuffer)[allocate分配]
IntBuffer intBuffer = IntBuffer.allocate(10);
//每put一个intBuffer中的pos(position)就增加一个
intBuffer.put(11);
intBuffer.put(22);
intBuffer.put(33);
//java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
//[位置,限制,容量]
System.out.println(intBuffer);
//2.将pos复位,由2变为0 ,,这个方法很重要
intBuffer.flip();
//flip()方法实际就是 limit = position; position = 0; mark = -1;
//java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
System.out.println(intBuffer);
//3.获取元素, 复位之后原来的的put的依然在 并且get(index)不会改变intbuffer
System.out.println(intBuffer.get(1));
//java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
System.out.println(intBuffer);
//4.替换元素
//将位置为1的元素替换为2222
intBuffer.put(1, 2222);
//获取位置为1的元素是2222
System.out.println(intBuffer.get(1));
//java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
System.out.println(intBuffer);
//5.遍历
for (int i = 0; i < intBuffer.limit(); i++) {
System.out.println(intBuffer.get()+"\n");
}
//使用get()方法,会使pos每次加一 (position++)
//java.nio.HeapIntBuffer[pos=3 lim=3 cap=10]
System.out.println(intBuffer);
}
@Test
public void test02() {
//wrap的使用
IntBuffer intbuffer = IntBuffer.wrap(new int[]{11, 22, 33});
//java.nio.HeapIntBuffer[pos=0 lim=3 cap=3] 数组的长度就是容量
System.out.println(intbuffer);
}
@Test
public void test03() {
//其他的基本操作
IntBuffer intBuffer = IntBuffer.allocate(10);
intBuffer.put(new int[]{11, 22, 33});
//java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
System.out.println(intBuffer);
//1.复制一个一模一样的intBuffer
IntBuffer intBuffer1 = intBuffer.duplicate();
//java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
System.out.println(intBuffer1);
//2.将intbuffer中的数据放在一个int数组中
int remaining = intBuffer.remaining();
//remaining = limit - position,还剩多少地方
System.out.println(remaining);
int[] arr1 = new int[intBuffer.remaining()];
//将缓冲区的数据放入arr1中
intBuffer.get(arr1);
for(Integer i : arr1) {
System.out.print(Integer.toString(i) + ",");
}
}
实现一个简单的server和client发消息功能
注释很详细
/**
* 实现简单的单向通信
*/
public class NioOne {
/*=======================这是服务端start===============================*/
static class Server {
//创建一个缓冲区,大家共用
static ByteBuffer buffer = ByteBuffer.allocate(1024);
public static void main(String[] args) throws IOException, InterruptedException {
/*--------------------初始化一个服务端-----------------------------|*/
//创建一个选择器(多路复用器)
Selector selector = Selector.open();
//打开服务通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//设置服务通道为非阻塞的方式
ssc.configureBlocking(false);
//绑定服务端的监听的端口
ssc.bind(new InetSocketAddress(8888));
//将服务注册到多路复用器上,并设置状态是"接收"状态
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("=====监听端口=8888");
/*--------------------初始化一个服务端-----------------------------|*/
//这里必须不断的轮询状态的改变,采用轮询的方式,查询获取“准备就绪”的注册过的操作
while (selector.select() > 0) {
/获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
//迭代
Iterator<SelectionKey> iterator = selectionKeySet.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//当前的状态是接收状态,需要接收客户端的请求同时注册为可读取标志
if (key.isAcceptable()) {
//获取与客户端交互的通道SocketChannel,服务端的是ServerSocketChannel.注意区分
SocketChannel sc = ssc.accept();
//设置模式为非阻塞模式
sc.configureBlocking(false);
//注册到选择器上,并设置为读取标记
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
//清空缓冲区
buffer.clear();
//获取与客户端连接的通道
SocketChannel sc = (SocketChannel) key.channel();
//将通道中的数据转到缓冲区中
int read = sc.read(buffer);
if (read != -1) {
//收完消息要复位
buffer.flip();
//buffer.array()直接返回byte[]
String msg = new String(buffer.array(), "utf8");
System.out.println("=====收到的消息===" + msg);
}
}
}
//使用过后就移除该key,因为已经没有SocketChannel了
//iterator.remove();
//清理使用过的key,目的同上
selectionKeySet.clear();
//为了观察输出
TimeUnit.SECONDS.sleep(1);
}
}
}
/*=======================这是服务端stop===============================*/
/*=======================这是客户端start===============================*/
static class Client {
//创建一个缓冲区
static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
public static void main(String[] args) throws Exception {
//创建一个端口和ip
InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8888);
//创建一个客户端通道,并且连接网络
SocketChannel sc = SocketChannel.open(socketAddress);
//将数据放入缓冲区
byteBuffer.put("第一个nio网络程序".getBytes("utf8"));
//缓冲区接收数据之后要复位
byteBuffer.flip();
//将缓冲区数据写入通道
sc.write(byteBuffer);
System.out.println("==client消息发送成功===");
//清空数据,下一次使用
byteBuffer.clear();
//关闭
sc.close();
}
}
/*=======================这是客户端stop===============================*/
}
结果
服务端
客户端
实现一个双向通信,一问一答
与上边的区别
在服务端收到消息后,转入writeable状态.将数据返回给client.
在客户端,发出消息后,转入readable状态,接收server的返回数据
/**
* 实现实现双向通信
*/
public class NioTwo {
/*=========================服务端开始============================*/
static class Server {
public static void main(String[] args) throws IOException, InterruptedException {
/*------------------------初始化服务端------------------------*/
ByteBuffer buffer = ByteBuffer.allocate(1024);
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8888));
//服务端初始要注册为阻塞状态
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("===========开始监听8888端口===");
/*------------------------初始化服务端------------------------*/
while (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//有了请求就应该读了,注册为可读
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
buffer.clear();
if (sc.read(buffer) != -1) {
buffer.flip();
System.out.println("--客户端消息是==" + new String(buffer.array()));
//注册为可写状态
sc.register(selector, SelectionKey.OP_WRITE);
}
}
if (key.isWritable()) {
//给客户端回消息
SocketChannel sc = (SocketChannel) key.channel();
buffer.clear();
buffer.put("服务端收到消息了".getBytes("utf8"));
buffer.flip();
sc.write(buffer);
sc.close();
}
}
selectionKeys.clear();
Thread.sleep(500);
}
}
}
/*======================服务端结束==============================*/
/*======================客户端开始==============================*/
static class Client {
public static void main(String[] args) throws IOException, InterruptedException {
/*--------------------客户端初始化-----------------------*/
ByteBuffer buffer = ByteBuffer.allocate(1024);
Selector selector = Selector.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
//创建一个客户端通道,并连接
SocketChannel socketChannel = SocketChannel.open(address);
socketChannel.configureBlocking(false);
//注册为可写
socketChannel.register(selector, SelectionKey.OP_WRITE);
/*--------------------客户端初始化-----------------------*/
while (selector.select()>0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isWritable()) {
SocketChannel sc = (SocketChannel) key.channel();
buffer.clear();
buffer.put("发给服务端的消息".getBytes("utf8"));
buffer.flip();
sc.write(buffer);
System.out.println("=----消息发送成功---");
//改为读状态
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
//接收服务端的回信
SocketChannel sc = (SocketChannel) key.channel();
buffer.clear();
if (sc.read(buffer) != -1) {
buffer.flip();
System.out.println("--服务端回信==" + new String(buffer.array()));
}
//关闭连接
sc.close();
}
}
selectionKeys.clear();
TimeUnit.MILLISECONDS.sleep(500);
}
}
}
/*=========================客户端结束==============================*/
}
结果
服务端
客户端
总结
①创建ServerSocketChannel,为其配置非阻塞模式。
②绑定监听,配置TCP参数,录入backlog大小等。
③创建一个独立的IO线程,用于轮询多路复用器Selector。
④创建Selector,将之前创建的ServerSocketChannel注册到Selector上,并设置监听标识位SelectionKey.OP_ACCEPT。
⑤启动IO线程,在循环体中执行Selector.select()方法,轮询就绪的通道。
⑥当轮询到处于就绪状态的通道时,需要进行操作位判断,如果是ACCEPT状态,说明是新的客户端接入,则调用accept方法接收新的客户端。
⑦设置新接入客户端的一些参数,如非阻塞,并将其继续注册到Selector上,设置监听标识位等。
⑧如果轮询的通道标识位是READ,则进行读取,构造Buffer对象等。
使用nio很麻烦,so,大家经常使用netty框架.
Reactor(一种经典的多路复用I/O模型)模式学习:
https://blog.csdn.net/eric_sunah/article/details/80437025
以及https://www.cnblogs.com/crazymakercircle/p/9833847.html