1.学习基本概念、传统的同步阻塞式I/O编程、伪异步IO实现
2.学习基于NIO的同步非阻塞式编程
3.了解基于NIO2.0的异步非阻塞(AIO)编程
1.1 基本概念
Socket又称“套接字”,应用程序通常通过“套接字”向网络发出请求或者应答网络请求。
Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是建立网络连接时使用的,在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的回话。对于一个网络连接来书,套接字是平等的,不因为在服务器端或在客户端而产生不同级别。不管是Socket还是ServerSocket它们的工作都是通过SocketImpl类及其子类完成的。
套接字之间的连接过程可以分为四个步骤:服务器监听,客户端请求服务器,服务器确认,客户端确认,进行通信
(1)服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监听网络状态。
(2)客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
(3)服务器端连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端。
(4)客户端连接确认:一旦客户端确认了此描述,连接就建立好了。双方开始进行通信,而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
传统的同步阻塞式I/O编程
Client.java

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; public class Client { final static String ADDRESS = "127.0.0.1"; final static int PORT = 8765; public static void main(String[] args) { Socket socket = null; BufferedReader in = null; PrintWriter out = null; try { socket = new Socket(ADDRESS, PORT); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(), true); //向服务器端发送数据 out.println("接收到客户端的请求数据..."); String response = in.readLine(); System.out.println("Client: " + response); } catch (Exception e) { e.printStackTrace(); } finally { if(in != null){ try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if(out != null){ try { out.close(); } catch (Exception e) { e.printStackTrace(); } } if(socket != null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } socket = null; } } }
Server.java

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class Server { final static int PROT = 8765; public static void main(String[] args) { ServerSocket server = null; try { server = new ServerSocket(PROT); System.out.println(" server start .. "); //进行阻塞 Socket socket = server.accept(); //新建一个线程执行客户端的任务 new Thread(new ServerHandler(socket)).start(); } catch (Exception e) { e.printStackTrace(); } finally { if(server != null){ try { server.close(); } catch (IOException e) { e.printStackTrace(); } } server = null; } } }
ServerHandler.java

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; public class ServerHandler implements Runnable{ private Socket socket ; public ServerHandler(Socket socket){ this.socket = socket; } @Override public void run() { BufferedReader in = null; PrintWriter out = null; try { in = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); out = new PrintWriter(this.socket.getOutputStream(), true); String body = null; while(true){ body = in.readLine(); if(body == null) break; System.out.println("Server :" + body); out.println("服务器端回送响的应数据."); } } catch (Exception e) { e.printStackTrace(); } finally { if(in != null){ try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if(out != null){ try { out.close(); } catch (Exception e) { e.printStackTrace(); } } if(socket != null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } socket = null; } } }
伪异步IO编程
为了解决同步阻塞IO面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化,后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。 下面,我们结合连接模型图和源码,对伪异步IO进行分析,看它是否能够解决同步阻塞IO面临的问题。
伪异步IO模型图
采用线程池和任务队列可以实现一种叫做伪异步的IO通信框架,它的模型图如下:
当有新的客户端接入的时候,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
Client.java

import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; public class Client { final static String ADDRESS = "127.0.0.1"; final static int PORT =8765; public static void main(String[] args) { Socket socket = null; BufferedReader in = null; PrintWriter out = null; try { socket = new Socket(ADDRESS, PORT); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(), true); out.println("Client request"); String response = in.readLine(); System.out.println("Client:" + response); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { if(in != null){ try { in.close(); } catch (Exception e1) { e1.printStackTrace(); } } if(out != null){ try { out.close(); } catch (Exception e2) { e2.printStackTrace(); } } if(socket != null){ try { socket.close(); } catch (Exception e3) { e3.printStackTrace(); } } socket = null; } } }
Server.java

import java.io.BufferedReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; public class Server { final static int PORT = 8765; public static void main(String[] args) { ServerSocket server = null; BufferedReader in = null; PrintWriter out = null; try { server = new ServerSocket(PORT); System.out.println("server start"); Socket socket = null; HandlerExecutorPool executorPool = new HandlerExecutorPool(50, 1000); while(true){ socket = server.accept(); executorPool.execute(new ServerHandler(socket)); } } catch (Exception e) { e.printStackTrace(); } finally { if(in != null){ try { in.close(); } catch (Exception e1) { e1.printStackTrace(); } } if(out != null){ try { out.close(); } catch (Exception e2) { e2.printStackTrace(); } } if(server != null){ try { server.close(); } catch (Exception e3) { e3.printStackTrace(); } } server = null; } } }
ServerHandler.java

import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; public class ServerHandler implements Runnable { private Socket socket; public ServerHandler (Socket socket){ this.socket = socket; } @Override public void run() { BufferedReader in = null; PrintWriter out = null; try { in = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); out = new PrintWriter(this.socket.getOutputStream(), true); String body = null; while(true){ body = in.readLine(); if(body == null) break; System.out.println("Server:" + body); out.println("Server response"); } } catch (Exception e) { e.printStackTrace(); } finally { if(in != null){ try { in.close(); } catch (Exception e1) { e1.printStackTrace(); } } if(out != null){ try { out.close(); } catch (Exception e2) { e2.printStackTrace(); } } if(socket != null){ try { socket.close(); } catch (Exception e3) { e3.printStackTrace(); } } socket = null; } } }
HandlerExecutorPool.java

import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class HandlerExecutorPool { private ExecutorService executor; public HandlerExecutorPool(int maxPoolSize, int queueSize){ this.executor = new ThreadPoolExecutor( Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize)); } public void execute(Runnable task){ this.executor.execute(task); } }
我们首先创建一个处理类的线程池,当接收到新的客户端连接的时候,将请求Socket封装成一个Task,然后调用线程池的execute方法执行,从而避免了每个请求接入都创建一个新的线程。
由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出,相比于传统的一连接一线程模型,是一种改良。
伪异步IO通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。
public int read(byte b[]) throws IOException { return read(b, 0, b.length); public void write(byte b[]) throws IOException
当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三种事件: 1) 有数据可读 2) 可用数据已经读取完毕 3) 发生空指针或者IO异常 这意味着当对方发送请求或者应答消息比较缓慢、或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方60S才能够将数据发送完成,读取一方的IO线程也将会被同步阻塞60S,在此期间,其它接入消息只能在消息队列中排队。
当调用OutputStream的write方法写输出流的时候,它将会被阻塞直到所有要发送的字节全部写入完毕,或者发生异常。学习过TCP/IP相关知识的都知道,当消息的接收方处理缓慢的时候,将不能及时的从TCP缓冲区读取数据,这将会导致发送方的TCP window size不断减小,直到为0,双方处于Keep-Alive状态,消息发送方将不能再向TCP缓冲区写入消息,这时如果采用的是同步阻塞IO,write操作将会被无限期阻塞,直到TCP window size大于0或者发生IO异常。
通过对输入和输出流的API文档进行分析,我们了解到读和写操作都是同步阻塞的,阻塞的时间取决于对方IO线程的处理速度和网络IO的传输速度。本质上来讲,我们无法保证生产环境的网络状况和对端的应用程序能够足够快,如果我们的应用程序依赖对方的处理速度,它的可靠性就非常差。也许在实验室进行的性能测试结果令大家满意,但是一旦上线运行,面对恶劣的网络环境和良莠不齐的第三方系统,问题就会如火山一样喷发。
伪异步IO实际上仅仅只是对之前IO线程模型的一个简单优化,它无法从根本上解决同步IO导致的通信线程阻塞问题。下面我们就简单分析下如果通信对方返回应答时间过长引起的级联故障:
- 服务端处理缓慢,返回应答消息耗费60S,平时只需要10MS;
- 采用伪异步IO的线程正在读取故障服务节点的响应,由于读取输入流是阻塞的,因此,它将会被同步阻塞60S;
- 假如所有的可用线程都被故障服务器阻塞,那后续所有的IO消息都将在队列中排队;
- 由于线程池采用阻塞队列实现,当队列积满之后,后续入队列的操作将被阻塞;
- 由于前端只有一个Accptor线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时;
- 由于几乎所有的连接都超时,调用者会认为系统已经崩溃,无法接收新的请求消息。
(摘自:http://ifeve.com/netty-definitive-guide-2-2/)
IO(BIO)和NIO的区别:其本质就是阻塞和非阻塞的区别。
阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,那么程序就一直等着,直到传输完毕为止。
非阻塞概念:应用程序直接可以获取已经准备就绪好的数据,无需等待。
IO为同步阻塞形式,NIO为同步非阻塞形式。NIO并没有实现异步,在JDK1.7之后,升级了NIO库包,支持异步非阻塞通信模式即NIO2.0(AIO)
同步和异步:同步和异步一般是面向操作系统与应用程序对IO操作的层面上来区别的。
同步时,应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某一个方法上,直到数据准备就绪;或者采用轮询的策略实时检查数据的就绪状态,如果就绪则获取数据。
异步时,则所有的IO读写操作交给操作系统处理,与我们的应用程序没有直接关系,我们程序不需要关心IO读写,当操作系统完成了IO读写操作时,会给我们应用程序发送通知,我们的应用程序直接拿走数据即可。
同步说的是你的server服务器端的执行方式
阻塞说的是具体技术,接收数据的方式、状态(io、nio)
NIO
在介绍NIO之前,先澄清一个概念,有的人叫NIO为new IO,有的人把NIO叫做Non-block IO,这里我们还是习惯说后者,即非阻塞IO
NIO的本质
NIO的本质就是避免原始的TCP建立连接使用3次握手的操作,减少连接的开销
学习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
Channel是一个通道,它就像自来水管道一样,网络数据通过Channel读取和写入,通道与流不同之处在于通道是双向的,而流只是一个方向上移动(一个流必须是inputStream或者outputStream的子类),而通道可以用于读,写或者二者同时进行,最关键的是可以与多路复用器结合起来,有多种的状态位,方便多路复用器去识别。事实上通道分为两大类,一类是网络读写的(SelectableChannel),一类是用于文件操作的(FileChannel),我们使用的SocketChannel和ServerSockerChannel都是SelectableChannel的子类
3.多路复用器 Selector
他是NIO编程的基础,非常重要,多路复用器提供选择已经就绪的任务的能力。
简单说,就是Selector会不断地轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。
Selector线程就类似一个管理者(Master),管理了成千上万个管道,然后轮询哪个管道的数据已经准备好,通知CPU执行IO的读取或写入操作。
Selector模式:当IO事件(管理)注册到选择器以后,selector会分配给每个管道一个key值,相当于标签。selector选择器是以轮询的方式进行查找注册的所有IO事件(管道)
当我们的IO事件(管道)准备就绪后,select就会识别,会通过key值来找到相应的管道,进行相关的数据处理操作(从管道里读或写数据,写道我们的数据缓冲区中)。
一个多路复用器Selecor可以同时轮询多个Channel,由于JDK使用epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。所以只需要一个线程负载Selector的轮询,就可以接入很多客户端
每个管道都会对选择器进行注册不同的事件状态,以便选择器查找。
SelectionKey.OP_CONNECT 连接状态
SelectionKey.OP_ACCEPT 阻塞状态
SelectionKey.OP_READ 可读状态
SelectionKey.OP_WRITE 可写状态
代码如下:
Server.java

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; public class Server implements Runnable{ //1 多路复用器(管理所有的通道) private Selector seletor; //2 建立缓冲区 private ByteBuffer readBuf = ByteBuffer.allocate(1024); //3 //private ByteBuffer writeBuf = ByteBuffer.allocate(1024); public Server(int port){ try { //1 打开路复用器 this.seletor = Selector.open(); //2 打开服务器通道 ServerSocketChannel ssc = ServerSocketChannel.open(); //3 设置服务器通道为非阻塞模式 ssc.configureBlocking(false); //4 绑定地址 ssc.bind(new InetSocketAddress(port)); //5 把服务器通道注册到多路复用器上,并且监听阻塞事件 ssc.register(this.seletor, SelectionKey.OP_ACCEPT); System.out.println("Server start, port :" + port); } catch (IOException e) { e.printStackTrace(); } } @Override public void run() { while(true){ try { //1 必须要让多路复用器开始监听 this.seletor.select(); //2 返回多路复用器已经选择的结果集 Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator(); //3 进行遍历 while(keys.hasNext()){ //4 获取一个选择的元素 SelectionKey key = keys.next(); //5 直接从容器中移除就可以了 keys.remove(); //6 如果是有效的 if(key.isValid()){ //7 如果为阻塞状态 if(key.isAcceptable()){ this.accept(key); } //8 如果为可读状态 if(key.isReadable()){ this.read(key); } //9 写数据 if(key.isWritable()){ //this.write(key); //ssc } } } } catch (IOException e) { e.printStackTrace(); } } } private void write(SelectionKey key){ //ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //ssc.register(this.seletor, SelectionKey.OP_WRITE); } private void read(SelectionKey key) { try { //1 清空缓冲区旧的数据 this.readBuf.clear(); //2 获取之前注册的socket通道对象 SocketChannel sc = (SocketChannel) key.channel(); //3 读取数据 int count = sc.read(this.readBuf); //4 如果没有数据 if(count == -1){ key.channel().close(); key.cancel(); return; } //5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位) this.readBuf.flip(); //6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据 byte[] bytes = new byte[this.readBuf.remaining()]; //7 接收缓冲区数据 this.readBuf.get(bytes); //8 打印结果 String body = new String(bytes).trim(); System.out.println("Server : " + body); // 9..可以写回给客户端数据 } catch (IOException e) { e.printStackTrace(); } } private void accept(SelectionKey key) { try { //1 获取服务通道 ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //2 执行阻塞方法 SocketChannel sc = ssc.accept(); //3 设置阻塞模式 sc.configureBlocking(false); //4 注册到多路复用器上,并设置读取标识 sc.register(this.seletor, SelectionKey.OP_READ); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { new Thread(new Server(8765)).start();; } }
Client.java

import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class Client { //需要一个Selector public static void main(String[] args) { //创建连接的地址 InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8765); //声明连接通道 SocketChannel sc = null; //建立缓冲区 ByteBuffer buf = ByteBuffer.allocate(1024); try { //打开通道 sc = SocketChannel.open(); //进行连接 sc.connect(address); while(true){ //定义一个字节数组,然后使用系统录入功能: byte[] bytes = new byte[1024]; System.in.read(bytes); //把数据放到缓冲区中 buf.put(bytes); //对缓冲区进行复位 buf.flip(); //写出数据 sc.write(buf); //清空缓冲区数据 buf.clear(); } } catch (IOException e) { e.printStackTrace(); } finally { if(sc != null){ try { sc.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
创建NIO服务器的主要步骤
(1)打开ServerSocketChannel,监听客户端连接
(2)绑定监听端口,设置连接为非阻塞模式
(3)创建Reactor线程,创建多路复用器并启动线程
(4)将ServerSocketChannel注册到Reactor线程中的Selector上,监听ACCEPT事件
(5)Selector轮询准备就绪的key
(6)Selector监听到新的客户端接入,处理新的接入请求,完成TCP三次握手,简历物理链路
(7)设置客户端链路为非阻塞模式
(8)将新接入的客户端连接注册到Reactor线程的Selector上,监听读操作,读取客户端发送的网络消息
(9)异步读取客户端消息到缓冲区
(10)对Buffer编解码,处理半包消息,将解码成功的消息封装成Task
(11)将应答消息编码为Buffer,调用SocketChannel的write将消息异步发送给客户端
因为应答消息的发送,SocketChannel也是异步非阻塞的,所以不能保证一次能吧需要发送的数据发送完,此时就会出现写半包的问题。我们需要注册写操作,不断轮询Selector将没有发送完的消息发送完毕,然后通过Buffer的hasRemain()方法判断消息是否发送完成
推荐NIO底层讲解网址:
http://mp.weixin.qq.com/s/_vcT6KJNgcy1FruhSlA5zg
https://mp.weixin.qq.com/s/BZJiUMR60YwpW1krN3Me_g
AIO编程
在NIO基础之上引入了异步通道的概念,并提供了异步分局和异步套接字通道的实现,从而在真正意义上实现了异步非阻塞,之前我们学习的NIO只是非阻塞而
并非异步。而AIO它不需要通过多路复用器对注册的通道进行轮训操作即可实现异步读写,从而简化了NIO编程模型。也可以称之为NIO2.0,这种模式才真正属于我们异步非阻塞的模型。
AsynchronousServerScoketChannel
AsynchronousScoketChanel
代码
Server.java

import java.net.InetSocketAddress; import java.nio.channels.AsynchronousChannelGroup; import java.nio.channels.AsynchronousServerSocketChannel; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Server { //线程池 private ExecutorService executorService; //线程组 private AsynchronousChannelGroup threadGroup; //服务器通道 public AsynchronousServerSocketChannel assc; public Server(int port){ try { //创建一个缓存池 executorService = Executors.newCachedThreadPool(); //创建线程组 threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1); //创建服务器通道 assc = AsynchronousServerSocketChannel.open(threadGroup); //进行绑定 assc.bind(new InetSocketAddress(port)); System.out.println("server start , port : " + port); //进行阻塞 assc.accept(this, new ServerCompletionHandler()); //一直阻塞 不让服务器停止 Thread.sleep(Integer.MAX_VALUE); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { Server server = new Server(8765); } }
Client.java

import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; import java.util.concurrent.ExecutionException; public class Client implements Runnable{ private AsynchronousSocketChannel asc ; public Client() throws Exception { asc = AsynchronousSocketChannel.open(); } public void connect(){ asc.connect(new InetSocketAddress("127.0.0.1", 8765)); } public void write(String request){ try { asc.write(ByteBuffer.wrap(request.getBytes())).get(); read(); } catch (Exception e) { e.printStackTrace(); } } private void read() { ByteBuffer buf = ByteBuffer.allocate(1024); try { asc.read(buf).get(); buf.flip(); byte[] respByte = new byte[buf.remaining()]; buf.get(respByte); System.out.println(new String(respByte,"utf-8").trim()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } @Override public void run() { while(true){ } } public static void main(String[] args) throws Exception { Client c1 = new Client(); c1.connect(); Client c2 = new Client(); c2.connect(); Client c3 = new Client(); c3.connect(); new Thread(c1, "c1").start(); new Thread(c2, "c2").start(); new Thread(c3, "c3").start(); Thread.sleep(1000); c1.write("c1 aaa"); c2.write("c2 bbbb"); c3.write("c3 ccccc"); } }
ServerCompletionHandler.java

import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; import java.util.concurrent.ExecutionException; public class ServerCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Server> { @Override public void completed(AsynchronousSocketChannel asc, Server attachment) { //当有下一个客户端接入的时候 直接调用Server的accept方法,这样反复执行下去,保证多个客户端都可以阻塞 attachment.assc.accept(attachment, this); read(asc); } private void read(final AsynchronousSocketChannel asc) { //读取数据 ByteBuffer buf = ByteBuffer.allocate(1024); asc.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer resultSize, ByteBuffer attachment) { //进行读取之后,重置标识位 attachment.flip(); //获得读取的字节数 System.out.println("Server -> " + "收到客户端的数据长度为:" + resultSize); //获取读取的数据 String resultData = new String(attachment.array()).trim(); System.out.println("Server -> " + "收到客户端的数据信息为:" + resultData); String response = "服务器响应, 收到了客户端发来的数据: " + resultData; write(asc, response); } @Override public void failed(Throwable exc, ByteBuffer attachment) { exc.printStackTrace(); } }); } private void write(AsynchronousSocketChannel asc, String response) { try { ByteBuffer buf = ByteBuffer.allocate(1024); buf.put(response.getBytes()); buf.flip(); asc.write(buf).get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, Server attachment) { exc.printStackTrace(); } }
适用场景
(1)BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
(2)NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
(3)AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
另外,I/O属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。