一:前言
在(一中了解了NIO中的缓冲区和通道),通过本文章你会了解阻塞和非阻塞,选择器,管道
二:完成NIO通信的三要素
* 1.通道(Channel):负责连接
* java.nio.channels.Channel 接口:
* SelectableChannel
* //TCP
* SocketChannel
* ServerSocketChannel
* //UDP
* DatagramChannel
* Pipe.SinkChannel
* Pipe.SourceChannel
*
* 2.缓冲区:(Buffer):数据存取
* 3.选择器:(Selector):(是SelectableChannel的多路复用器)是用于监控SelectableChannel的IO状况
先使用阻塞式通信
客户端
//客户端 @Test public void client() throws IOException { //1.获取通道 SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898)); FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ); //2.分配指定大小的缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); //3.读取本地文件,并发送到服务端 while (inChannel.read(buf)!=-1){ buf.flip(); sChannel.write(buf); buf.clear(); } //关闭通道 inChannel.close(); sChannel.close(); }
服务端
//服务端 @Test public void server() throws IOException { //1.获取通道 ServerSocketChannel ssChannel = ServerSocketChannel.open(); FileChannel outChannel = FileChannel.open(Paths.get("4.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); //2.绑定连接端口号 ssChannel.bind(new InetSocketAddress(9898)); //3.获取客户端的连接的通道 SocketChannel sChannel = ssChannel.accept(); //4.分配指定大小的缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); //5.接受客户端的数据并保存到本地 while (sChannel.read(buf)!=-1){ buf.flip(); outChannel.write(buf); buf.clear(); } sChannel.close(); outChannel.close(); ssChannel.close(); }
阻塞式通信:当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
下面使用选择器完成非阻塞式通信(TCP)
客户端
//客户端 @Test public void client() throws IOException { //1.获取通道 SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898)); //2.切换成非阻塞模式 sChannel.configureBlocking(false); //3.分配指定大小的缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); //4.发送数据到服务端 buf.put(new Date().toString().getBytes()); buf.flip(); sChannel.write(buf); //关闭通道 sChannel.close(); }
服务端
//服务端 @Test public void server() throws IOException { //1.获取通道 ServerSocketChannel ssChannel = ServerSocketChannel.open(); //2.切换到非阻塞模式 ssChannel.configureBlocking(false); //3.绑定连接 ssChannel.bind(new InetSocketAddress(9898)); //4.获取选择器 Selector selector = Selector.open(); //5.将通道注册到选择器上 ssChannel.register(selector, SelectionKey.OP_ACCEPT); //6.轮询式获取选择器上面已经“准备就绪的事件” while(selector.select()>0){ //7.获取当前选择器中所有注册的“选择键(已就绪的监听事件)” Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while(iterator.hasNext()){ //8.获取就绪事件 SelectionKey sk = iterator.next(); //9.判断具体是什么事件准备就绪 if(sk.isAcceptable()){ //10.若“接收就绪”,获取客户端连接 SocketChannel sChannel = ssChannel.accept(); //11.切换到非阻塞模式 sChannel.configureBlocking(false); //12.将该通道注册到选择器上 sChannel.register(selector,SelectionKey.OP_READ); }else if(sk.isReadable()){ //13.若读就绪,获取读就绪状态的通道 SocketChannel sChannel = (SocketChannel)sk.channel(); //14.读取数据 ByteBuffer buf = ByteBuffer.allocate(1024); int len=0; while((len=sChannel.read(buf))>0){ buf.flip(); System.out.println(new String(buf.array(),0,len)); buf.clear(); } } //取消选择键SelectionKey iterator.remove(); } } }
非阻塞式通信:当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
SelectableChannle 的结构如下图:
再来一个例子(DatagramChannel)UDP
客户端
@Test public void send() throws IOException { //通道 DatagramChannel dChannel = DatagramChannel.open(); //设置为非阻塞 dChannel.configureBlocking(false); //设置缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); buf.put(new Date().toString().getBytes()); buf.flip(); dChannel.send(buf,new InetSocketAddress("127.0.0.1",9898)); dChannel.close(); }
服务端
@Test public void receive() throws IOException { DatagramChannel dChannel = DatagramChannel.open(); dChannel.configureBlocking(false); dChannel.bind(new InetSocketAddress(9898)); Selector selector = Selector.open(); dChannel.register(selector, SelectionKey.OP_READ); //6.轮询式获取选择器上面已经“准备就绪的事件” while(selector.select()>0){ //7.获取当前选择器中所有注册的“选择键(已就绪的监听事件)” Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while(iterator.hasNext()){ //8.获取就绪事件 SelectionKey sk = iterator.next(); //9.判断具体是什么事件准备就绪 if(sk.isReadable()){ ByteBuffer buf = ByteBuffer.allocate(1024); dChannel.receive(buf); buf.flip(); System.out.println(new String(buf.array(),0,buf.limit())); buf.clear(); } //取消选择键SelectionKey iterator.remove(); } } }
三:管道
2个线程之间的单向数据连接,管道有一个source通道和一个sink通道,数据写道sink通道,到source通道读取
@Test public void test1() throws IOException { //1.获取管道 Pipe pipe = Pipe.open(); //2.将缓冲区的数据写入管道 ByteBuffer buf = ByteBuffer.allocate(1024); Pipe.SinkChannel sinkChannel = pipe.sink(); buf.put(new Date().toString().getBytes()); buf.flip(); sinkChannel.write(buf); //读取缓冲区里面的数据 Pipe.SourceChannel source = pipe.source(); buf.flip(); int len=source.read(buf); System.out.println(new String(buf.array(),0,len)); source.close(); sinkChannel.close(); }