在Java NIO(一)从操作系统角度对比IO与NIO的对比中已经介绍了传统IO(也叫BIO,基本IO的意思),下面主要介绍NIO关于网络编程的部分。
网络编程的基本模型都是Server/Client模型,也就是两个进程之间相互通讯,其中服务端提供连接入口(IP地址和端口信息),客户端向服务端发起连接请求,通过三次握手,连接成功后可以利用Socket套接字进行通信。下图是Netty权威指南中关于同步、异步、阻塞、非阻塞的说明。
传统BIO(同步阻塞式IO)
Server
public class BIOTimeServer {
public static void main(String[] args){
ServerSocket server = null;
try {
server = new ServerSocket(55533);//创建socket服务
Socket socket = null;
while (true){
socket = server.accept();//等待客户端连接。
new Thread(new BIOTimeServerHandler(socket)).start();//每收到一个连接就创建一个新的线程。
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
以下为ServerSocket的处理类。
public class BIOTimeServerHandler implements Runnable{
private Socket socket;
public BIOTimeServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
System.out.println("服务器已启动.");
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));//获取当前socket的输入流
out = new PrintWriter(this.socket.getOutputStream(),true);//向输socket里面输出
String currentTime = null;
String body = null;
while (true){
body = in.readLine();
if(body == null){
break;
}
System.out.println("服务器收到消息:" + body);
//如果客户端传来的请求是query,就将当前时间发给客户端。
currentTime = "query".equalsIgnoreCase(body)?new java.util.Date(System.currentTimeMillis()).toString():"error";
out.println(currentTime);//注意:此处的打印一定要用println,而不能用print,因为PrintWriter会根据换行符进行判断
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
in.close();
out.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Client
public class BIOTimeClient {
public static void main(String[] args) throws IOException {
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("127.0.0.1",55533);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(),true);
out.println("query");
String response = in.readLine();
System.out.println("Now time is " + response);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
in.close();
out.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
上面的BIO(基本IO)可以发现,每当服务端收到客户端的一个连接,就会创建一个新的线程处理当前的连接。一个线程只能处理一个客户端的请求。显然不适用与高并发高性能的服务器。
基于上面的BIO的改进办法,引入了线程池或消息队列的概念。也就是将上面的“一线程处理一连接”改为“多线程处理多连接”。但是底层仍然是同步阻塞式IO,所以这种处理被称作是“伪异步”。
伪异步IO
伪异步的实现是通过服务器对线程的优化,防止线程耗尽。假设客户端的数量是M个,处理客户端连接的线程数量是N个,M可以远大于N。通过线程池可以灵活的调用线程资源,也可以设置线程数量的最大值。
Server改成用线程池来处理
public class FakeIOTimeServer {
public static void main(String[] args){
ServerSocket server = null;
try {
server = new ServerSocket(55533);//创建socket服务
Socket socket = null;
FakeIOThreadExecutePool singleExacutor = new FakeIOThreadExecutePool(50,10000);
while (true){
socket = server.accept();//等待客户端连接。
//此处改为利用自定义的线程池来处理。处理类还是用的BIO的server处理类BIOTimeServerHandler。
singleExacutor.execute(new BIOTimeServerHandler(socket));
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
FakeIOThreadExecutePool的实现
public class FakeIOThreadExecutePool {
private ExecutorService executor;
public FakeIOThreadExecutePool(int maxPoolSize, int queueSize){
executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),maxPoolSize,120L,TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(queueSize));
}
public void execute(Runnable task){
executor.execute(task);
}
}
伪异步的弊端:当线程池的里面的线程用完时,并且被使用的线程所处理的客户端的请求或者应答的时间较长时,线程就会一直阻塞着,这期间其他的的请求就会一直在消息队列中等着。所以伪异步并没有从根本上结局BIO的弊端,只是进行了优化而已。
NIO(同步非阻塞式IO)
JavaNIO编程的基础的Selector,也就是多路复用,Selector会不断的轮询注册在Selector上的Channel,如果某个Channel上面发生读或写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取已经准备就绪的Channel集合,进行后续的I/O操作。
JDK使用了epoll()代替传统的select实现,所以他并没有最大连接句柄的限制。也就是说,一个线程可以负责处理成千上万的客户端。
Server
private Selector selector = Selector.open();;//声明选择器,用来监视每个通道的事件。
public NIOExampleServer(int port) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();//获取一个ServerSocket通道,用于接收客户端连接
serverChannel.configureBlocking(false);//则此通道将被置于非阻塞模式
serverChannel.socket().bind(new InetSocketAddress("localhost",port));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);//将通道注册到selector,只有注册以后才会被监听
}
//处理通道事件
public void listen() throws IOException{
//使用轮询访问selector
while(true){
selector.select();//当有注册的事件到达通道时,继续执行下面代码,否则阻塞。
Iterator<SelectionKey> ite=selector.selectedKeys().iterator(); //获取selector中的迭代器,选中项为注册的事件
while(ite.hasNext()){
SelectionKey key = ite.next();
//删除已选key,防止重复处理
ite.remove();
//客户端请求连接事件
if(key.isAcceptable()){
handleAccept(key);
}else if(key.isReadable()){//有可读数据事件
handleRead(key);
}
}
}
}
//处理通道连接事件(处理客户端发来的连接请求)
public void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel)key.channel();
SocketChannel channel = server.accept();//获得客户端连接通道
channel.configureBlocking(false);
channel.write(ByteBuffer.wrap(new String("我收到了你的连接请求!").getBytes()));//向客户端发消息
channel.register(selector, SelectionKey.OP_READ);//在与客户端连接成功后,为客户端通道注册SelectionKey.OP_READ事件。
}
//处理通道的读取事件(处理客户端发来的数据)
public void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel)key.channel();//获取客户端传输数据可读取消息通道。
ByteBuffer buffer = ByteBuffer.allocate(100);//创建读取数据缓冲器
channel.read(buffer);
byte[] data = buffer.array();
String message = new String(data);
System.out.println("收到客户端的消息内容是: " + message);
}
public static void main(String[] args) throws IOException {
new NIOExampleServer(8087).listen();
}
Client
private Selector selector = Selector.open();;//声明选择器,用来监视每个通道的事件。
public NIOExampleClient(int port) throws IOException {
SocketChannel channel = SocketChannel.open();//获取一个ServerSocket通道,用于请求服务端
channel.configureBlocking(false);//则此通道将被置于非阻塞模式
channel.connect(new InetSocketAddress("localhost", port));
channel.register(selector, SelectionKey.OP_CONNECT);//将通道,以及通道的“接受连接”的事件注册到selector,只有注册以后才会被监听。
}
//处理通道事件
public void listen() throws IOException{
//使用轮询访问selector
while(true){
selector.select();//当有注册的事件到达通道时,继续执行下面代码,否则阻塞。
Iterator<SelectionKey> ite=selector.selectedKeys().iterator(); //获取selector中的迭代器,选中项为注册的事件
while(ite.hasNext()){
SelectionKey key = ite.next();
//删除已选key,防止重复处理
ite.remove();
//如果连接成功
if(key.isConnectable()){
handleConnect(key);
}else if(key.isReadable()){//有可读数据事件
handleRead(key);
}
}
}
}
//处理通道连接事件(处理服务端发来的连接请求)
public void handleConnect(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel)key.channel();
//如果正在连接,则完成连接
if(channel.isConnectionPending()){
channel.finishConnect();
}
channel.configureBlocking(false);
channel.write(ByteBuffer.wrap(new String("我是客户端!").getBytes()));//向服务端发消息
channel.register(selector, SelectionKey.OP_READ);//在与服务端连接成功后,为服务端通道注册SelectionKey.OP_READ事件。
}
//处理通道的读取事件(处理服务端发来的数据)
public void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel)key.channel();//获取服务端传输数据可读取消息通道。
ByteBuffer buffer = ByteBuffer.allocate(100);//创建读取数据缓冲器
channel.read(buffer);
byte[] data = buffer.array();
String message = new String(data);
System.out.println("收到服务端的消息内容是: " + message);
}
public static void main(String[] args) throws IOException {
new NIOExampleClient(8087).listen();
}
AIO(异步非阻塞式IO)
NIO2.0以后提出了AIO,AIO不需要多路复用。AIO的实现主要是基于回调函数,每个操作在执行时,要传入一个类,该类封装操作了完成以后根据执行结果是否成功的灰调函数。这个类必须是CompletionHandler接口的子类,该接口面有两个方法需要实现。该接口源码如下:
public interface CompletionHandler<V,A> {
//操作执行成功以后执行这部分代码。
void completed(V result, A attachment);
//操作执行失败以后执行这部分代码。
void failed(Throwable exc, A attachment);
}
如果熟悉JavaScript语言常用的闭包和回调,对这部分应该很好理解。下面但是基于AIO实现的Server/Client。(实现的业务是客户端如果发来“query”字符串以后,就将当前系统时间发送给客户端)
//Server
public class AsyncIOServer {
public static void main(String[] args){
new Thread(new AsyncServerHandler(55550),"AIO-AsyncServerHandler-001").start();//每收到一个连接就创建一个新的线程。
}
}
//Client
public class AsyncIOClient {
public static void main(String[] args) {
new Thread(new AsyncClientHandler("localhost",55550),"AIO-AsyncClientHandler-001").start();
}
}
上面的实现很简单,重点都在相应的处理类面,下面看一下服务端的AsyncServerHandler实现类:
public class AsyncServerHandler implements Runnable{
CountDownLatch latch;
AsynchronousServerSocketChannel asynChannel;
public AsyncServerHandler(int port) {
try {
//打开一个通道
asynChannel = AsynchronousServerSocketChannel.open();
//接收相应端口的连接
asynChannel.bind(new InetSocketAddress(port));
System.out.println("The server is start in port:" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
latch = new CountDownLatch(1);
//准备接收客户端的连接,同时传入连接成功以后的回调。
asynChannel.accept(this,new AcceptCompletionHandler());
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面代码在准备接收连接时,传入了AcceptCompletionHandler,该类作为服务端连接成功以后的回调。下面是该类的实现。
public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncServerHandler> {
//连接完成后执行此部分回调
@Override
public void completed(AsynchronousSocketChannel result, AsyncServerHandler attachment) {
attachment.asynChannel.accept(attachment,this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
//如果连接成功以后,准备读取客户发来的消息,同事传入一个,读取成功的回调。
result.read(buffer,buffer,new ReadCompletionHandler(result));
}
//连接失败后执行此部分回调
@Override
public void failed(Throwable exc, AsyncServerHandler attachment) {
exc.printStackTrace();
attachment.latch.countDown();
}
}
上面的代码可以看到,服务端与客户端连接成功以后,就会读取客户端发来的消息,在读取消息的同时,又传入了一个读取消息成功的回调ReadCompletionHandler,该类的实现如下:
public class ReadCompletionHandler implements CompletionHandler<Integer,ByteBuffer> {
private AsynchronousSocketChannel asynchannel;
public ReadCompletionHandler(AsynchronousSocketChannel asynchannel) {
if(this.asynchannel == null){
this.asynchannel = asynchannel;
}
}
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
byte[] body = new byte[buffer.remaining()];
buffer.get(body);
try {
//解析客户端发来的消息。
String req = new String(body,"UTF-8");
System.out.println("收到客户端消息:" + req);
//要发给客户端的消息。
String currentTime = "query".equalsIgnoreCase(req)?new java.util.Date(System.currentTimeMillis()).toString():"error";
//给客户端发消息。
doWrite(currentTime);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
//向客户端写消息
private void doWrite(String currentTime){
if(currentTime != null && currentTime.trim().length() > 0){
System.out.println("发送给客户端的消息是:" +currentTime);
byte[] bytes = currentTime.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
//向客户端管道写入数据的同时,传入一个写入成功的回调类,该回调是以匿名内部类的形式传递的,到此服务端一些列的回调结束。
asynchannel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer writeBuffer) {
//如果没有发送完继续发送
if(writeBuffer.hasRemaining()){
asynchannel.write(writeBuffer,writeBuffer,this);
}
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
try {
asynchannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
try {
this.asynchannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端的实现与服务端的原理一样,客户端在向服务端发送连接请求时连接成功以后同时传入一个连接成功的回调(连接成功就将“query”发送给服务端),在向服务端管道写数据时又传入一个写数据成功的回调(如果“query”被成功写入,则读取服务端发来的消息),AsyncClientHandler的实现如下:
public class AsyncClientHandler implements CompletionHandler<Void,AsyncClientHandler>,Runnable {
private AsynchronousSocketChannel client;
private String host;
private int port;
private CountDownLatch latch;
public AsyncClientHandler(String host, int port) {
this.host = host;
this.port = port;
try {
client = AsynchronousSocketChannel.open();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
latch = new CountDownLatch(1);
//向该端口发送连接请求。
client.connect(new InetSocketAddress(host,port),this,this);
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void completed(Void result, AsyncClientHandler attachment) {
byte[] req = "query".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
//连接成功以后将字符串“query”发送给服务端。
client.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
//如果管道的内容还没写完就继续写。
if(buffer.hasRemaining()){
client.write(buffer,buffer,this);
}else {
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
//如果向服务端写入成功了以后就读取服务端发的消息。到此客户端回调结束。
client.read(readBuffer, readBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body;
try{
body = new String(bytes,"UTF-8");
System.out.println("Now is : " + body);
latch.countDown();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
client.close();
latch.countDown();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
try {
client.close();
latch.countDown();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
public void failed(Throwable exc, AsyncClientHandler attachment) {
try {
client.close();
latch.countDown();
} catch (IOException e) {
e.printStackTrace();
}
}
}