漫谈NIO(2)之Java的NIO
1.前言
上章提到过Java的NIO采取的是多路IO复用模式,其衍生出来的模型就是Reactor模型。多路IO复用有两种方式,一种是select/poll,另一种是epoll。在windows系统上使用的是select/poll方式,在linux上使用的是epoll方式,主要是由于DefaultSelectorProvider具体选择的selector决定。epoll是在linux2.6之后才支持的,select的方式时间复杂度为O(N),最大fd限制是1024。epoll没有数量限制,时间复杂度是O(1)。
再温习一遍多路IO复用的基本思路,阻塞发生在select上面,select管理所有注册在其上的socket请求,socket准备完全就会交由用户程序处理。下面结合java的nio例子,来更细致的讲解一下这种模式,强化理解一下。要写出java的nio不难但要完全正确绝不容易,相关概念不清楚就会产生难以理解的bug,这里有一些相关的陷阱。
另外说明一下,这个例子不一定完全正确,用于演示足够了。对于Java的NIO而言,有几个概念比较重要,这里先提两个channel和buffer。不管是客户端发送服务端接收,还是服务端发送客户端接收,基本的流程都是:发送方发送数据->buffer->发送方channel->接收方channel->buffer->接收方接收数据。
2.具体实现
2.1 服务端
对于服务端而言首先需要的就是确定监听的端口,其次是与之对应的channel,而后就是selector,最后还需要一个线程池。为什么会需要线程池呢?道理很简单,select模式获取了所有channel的change,对于服务端而言,change的可能有非常多的客户端channel,而用户程序只有一个线程,如果这么多个channel一个个顺序执行,如果有耗时严重的操作,那么后果是非常糟糕的,所有客户端都会延时处理,这也是多路IO复用的一个糟糕点。线程池就是为每个客户端分配一个线程去处理,减缓这种情况的后果。Server的基本四个内容就出来了:
private int port;
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private ExecutorService executorService;
接下来就是初始化服务端。初始化的步骤也是一般化:1.初始化连接池;2.初始化Selector;3.初始化绑定端口;4.将socket注册到select上。大致步骤就是这些,但是还有些额外的细节。具体代码如下:
1. executorService = Executors.newCachedThreadPool();
2. selector = Selector.open();
3. serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port));
4. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
这里的一个细节就是socket必须是非阻塞模式。初始化完成之后就是正式的逻辑了,再来回忆一下多路IO复用的逻辑,管理多个IO的change事件,阻塞在select上,如果有change事件,select就能继续执行下去,选出change了的IO,只对这部分IO进行操作。这段描述就下面这段简单的代码了:
int event = selector.select();
if(event != 0) {
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()) {
SelectionKey key = it.next();
it.remove();
}
}
这里就是调用selector.select()方法进行阻塞,如果change事件不为0(这个判断应该去掉好点),获取当前所有change事件。遍历处理,移除该事件。不移除,下次该事件依旧存在,相当于认为是没处理,会出现多次触发错误。
下面详细介绍一下事件的类型,Java定义了4种类型:
1.针对服务端的ACCEPT事件,接收到客户端的连接请求;
2.针对客户端的CONNECT事件,发起对服务端的连接请求;
3.针对获取对端发送的数据的READ事件;
4.针对请求发送数据给对端时准备好了缓冲区的WRITE事件。
其中WRITE事件一般不进行使用,因为大部分情况缓冲区都是空闲的,会立刻触发该事件,这个浪费CPU的性能,还会造成bug。下面代码就是server端处理的一个基本逻辑,也是有些要注意的点。
if(key.isValid() && key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if(key.isValid() && key.isReadable()) {
key.interestOps(0);
executorService.execute(new Task(key));
}
服务端的事件就3个,write事件不用管,所以只需要关注accept和read事件。有请求进来,就接收这个请求,设置成非阻塞式,再注册到selector中,监听该请求的读事件。读事件到来,先将监听的时间改成无,这里是因为异步执行,可能没有读完数据,再次触发了该channel的读事件,重复读取,造成问题。Task就是一个runnabel任务,处理读取,发送应答,这里还需要重新将监听的事件改成读事件,即处理完了本次内容,等待下次内容。
Task的具体内容如下:
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int size = -1;
try {
while((size = channel.read(buffer)) > 0) {
buffer.flip();
baos.write(buffer.array(), 0, size);
buffer.clear();
}
if(baos.size() == 0) {
key.cancel();
} else {
// 协议解析
String msg = new String(baos.toByteArray(), "UTF-8");
// 返回该数据
String reply = "get client msg: " + msg;
ByteBuffer re = ByteBuffer.wrap(reply.getBytes());
while(re.hasRemaining()) {
channel.write(re);
}
// 处理完毕后后设置成读状态,继续获取相关数据
key.interestOps(SelectionKey.OP_READ);
key.selector().wakeup();
}
} catch (Exception e) {
key.cancel(); // 异常连接中断
}
这里的逻辑就是使用buffer将数据取出来了。取出为0,或者抛出异常,意味着客户端断开了连接,直接取消掉该channel的管理。回写了一个数据。之后就是将事件监听设置回监听读取事件,最后一步需要wakeup一下。wakeup是为了唤醒一下select,原因如下:这个是由于前面先将监听的事件改成了0,后面才改回了read事件。不管是怎么修改,都不是立刻生效的,需要下次select事件触发才能生效,问题也只会出在多线程中。试想一下下面这个过程:
1.A通道有数据了,A先置为0了,开始读取数据,因为是异步的,所以又走到了select阻塞了;
2.B连接进来,触发的select方法,这时A的0才正式生效,这也是我们想要的,因为A之前的数据还在处理,并不是新的数据到来,不需要再次触发读操作。这里先置为0的动作是正确的。
3.此时主线程又走到了select方法阻塞了,注意,此时A生效的是0,A结束此次读操作,等待下次读事件。问题就出在这里,如果不触发一下select方法,此时A即使有新的读事件,其也不会触发,因为重置为read并没有生效,要等select触发才能生效。这就相当于A没接到消息了,如果B有读事件,触发了select方法,则A才能接到消息。wakeup在这里必须添加的目的就是强制触发一下select,使A更新回read事件,而不是不关系任何事件。
实际上触发没有这么麻烦,在客户端还会说到这个问题,有更简单的触发方法。
上面的代码也可以看出nio都是基于buffer操作的。buffer也有很多陷阱,使用正确不容易。下面给出一个我的完整例子,可以运行试试,不保证没bug。了解了上面的知识,测出bug调试应该也不难。
import java.io.ByteArrayOutputStream;
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;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; public class NioServer { private int port;
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private ExecutorService executorService; public NioServer(int port) {
this.port = port;
} public void open() {
this.executorService = Executors.newCachedThreadPool();
try {
this.selector = Selector.open();
this.serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("server端启动...");
for(;;) {
System.out.println("======>select的keys数量:" + selector.keys().size());
int event = selector.select();
System.out.println("======>select的keys数量:" + selector.keys().size() + ", change事件数量:" + event);
if(event != 0) {
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
// System.out.println("======>真实未处理的change事件数量:" + keys.size());
while(it.hasNext()) {
SelectionKey key = it.next();
it.remove(); // 移除这个key
if(key.isValid() && key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("===>获取client连接,准备读取数据:"+ client.socket().getRemoteSocketAddress());
} else if(key.isValid() && key.isReadable()) {
// 先置为0,防止异步线程未处理完该事件又被select
// key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
key.interestOps(0);
executorService.execute(new Task(key));
} else {
System.out.println("其它事件:" + key.interestOps());
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
} private class Task implements Runnable { private SelectionKey key; public Task(SelectionKey key) {
this.key = key;
} @Override
public void run() {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int size = -1;
try {
// System.out.println("===>开始读取数据");
while((size = channel.read(buffer)) > 0) {
buffer.flip();
baos.write(buffer.array(), 0, size);
buffer.clear();
}
if(baos.size() == 0) {
key.cancel();
System.out.println("======<client断开连接:"+ channel.socket().getRemoteSocketAddress());
} else {
// 协议解析
String msg = new String(baos.toByteArray(), "UTF-8");
System.out.println("===>获取client数据: " + msg);
// 返回该数据
String reply = "get client msg: " + msg;
ByteBuffer re = ByteBuffer.wrap(reply.getBytes());
while(re.hasRemaining()) {
channel.write(re);
}
// 处理完毕后后设置成读状态,继续获取相关数据
// key.interestOps((key.interestOps() | SelectionKey.OP_READ));
key.interestOps(SelectionKey.OP_READ);
key.selector().wakeup();
System.out.println("===<返回server的获取结果");
}
} catch (Exception e) {
key.cancel(); // 异常连接中断
System.out.println("======<异常client断开连接:"+ channel.socket().getRemoteSocketAddress());
}
}
} public static void main(String[] args) {
NioServer nioServer = new NioServer(7777);
nioServer.open();
}
}
2.2 客户端
第一节说过,在单个连接的时候,多路IO复用方式甚至没有阻塞式IO性能好,多路IO复用是针对了多个IO操作。这里还是给出客户端的NIO写法。同样的,客户端需要上面的内容,不包括线程池,我们只处理一个客户端连接。需要增加的一个字段就是服务端地址,所以总共也是4个内容:服务端地址、端口、连接通道、select。
private String host;
private int port;
private SocketChannel socketChannel;
private Selector selector;
初始化也是基本操作:1.获取select;2.建立连接;3.注册到select
1. selector = Selector.open();
2. socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.socket().setTcpNoDelay(true);
socketChannel.connect(new InetSocketAddress(host, port));
3. socketChannel.register(selector, SelectionKey.OP_CONNECT);
这里要注意的也就是要以非阻塞式的方式进行。后面的步骤也一样,进行select,获取change事件,根据不同的事件处理不同。write事件不使用,客户端关注的就connect和read事件了。
int event = selector.select();
if(event != 0) {
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()) {
SelectionKey sk = it.next();
it.remove();
if(sk.isValid() && sk.isConnectable()) {
if(socketChannel.isConnectionPending()) {
if(socketChannel.finishConnect()) {
sk.interestOps(SelectionKey.OP_READ);
} else {
sk.cancel();
}
}
} else if(sk.isValid() && sk.isReadable()) {
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int size = -1;
while((size = sc.read(buffer)) > 0) {
buffer.flip();
baos.write(buffer.array(), 0, size);
buffer.clear();
}
}
}
}
这里要注意的是connect并没有真正连上,要触发了connect事件,执行finishConnect才会连接成功。连接成功后更新成read事件。这里会有一个疑惑,server端的时候intersetOps设置成0或者read不是直接生效,要select执行后才能生效,为什么这边connect设置成read事件就能直接改过来???...这是一个思维陷阱:不是要执行后才能改变状态,而是select认准的状态是select操作之前一瞬间的状态。server端的例子,哪怕不需要两个线程,单个线程也能触发,只要是异步操作。主线程先接收到A的读取操作,设置A成0,然后又进行select了,此一瞬间A的状态是0,后面A处理完后,再来一条消息就没用了,因为此时select阻塞时检测的状态是0,后续改过来也没用,所以才需要wakeup一下,让其认识到其状态应该修改后的read。而上述例子为什么不需要,就是因为这是一个同步的过程,此次connect事件,下次再select的时候一定变成了read。
其他的也没有什么值得一提的了,下面是客户端的完整代码。
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set; public class NioClient { private String host;
private int port;
private SocketChannel socketChannel;
private Selector selector; public NioClient(String host, int port) {
this.host = host;
this.port = port;
} public void open() {
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.socket().setTcpNoDelay(true);
socketChannel.connect(new InetSocketAddress(host, port));
socketChannel.register(selector, SelectionKey.OP_CONNECT);
System.out.println("client端启动...");
for(;;) {
System.out.println("======>select的keys数量:" + selector.keys().size());
int event = selector.select();
System.out.println("======>select的keys数量:" + selector.keys().size() + ", change事件数量:" + event);
if(event != 0) {
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()) {
SelectionKey sk = it.next();
it.remove();
if(sk.isValid() && sk.isConnectable()) {
if(socketChannel.isConnectionPending()) {
if(socketChannel.finishConnect()) {
sk.interestOps(SelectionKey.OP_READ);
System.out.println("连接上远程服务器:" + socketChannel.getRemoteAddress());
} else {
sk.cancel();
System.out.println("连接未建立...");
}
}
} else if(sk.isValid() && sk.isReadable()) {
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int size = -1;
while((size = sc.read(buffer)) > 0) {
buffer.flip();
baos.write(buffer.array(), 0, size);
buffer.clear();
}
System.out.println("接收服务器消息:" + new String(baos.toByteArray(), "UTF-8"));
} else {
System.out.println("其它事件:" + sk.interestOps());
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
} public void close() {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
} public void send(String msg) {
byte[] b = msg.getBytes();
ByteBuffer buffer = ByteBuffer.wrap(b);
try {
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
} public static void main(String[] args) {
NioClient client = new NioClient("127.0.0.1", 7777);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
client.open();
}
});
thread.setDaemon(true);
thread.start();
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()) {
String msg = scanner.nextLine();
if("close".equals(msg)) {
client.close();
System.out.println("退出成功");
break;
} else {
client.send(msg);
}
}
}
}
3.总结
此章结合java nio的实际demo加强一下对多路IO复用的理解,理解Java的nio基本流程,对于理解后面的netty设计的结构有很大的帮助。
漫谈NIO(2)之Java的NIO的更多相关文章
- java的nio之:java的nio的服务器实现模型
[nio服务端序列图]
- java的nio之:java的nio的原理
转载:http://weixiaolu.iteye.com/blog/1479656 Java NIO原理图文分析及代码实现 前言: 最近在分析hadoop的RPC(Remote Procedure ...
- java的nio之:java的nio系列教程之FileChannel
一:Java NIO的FileChannel===>Java NIO中的FileChannel是一个连接到文件的通道.可以通过文件通道读写文件. ===>FileChannel无法设置为非 ...
- java的nio之:java的nio系列教程之buffer的概念
一:java的nio的buffer==>Java NIO中的Buffer用于和NIO通道Channel进行交互.==>数据是从通道channel读入缓冲区buffer,从缓冲区buffer ...
- java的nio之:java的nio系列教程之channel的概念
一:java的nio的channel Java NIO的通道类似流,但又有些不同: ==>既可以从通道中读取数据,又可以写数据到通道.但流的读写通常是单向的. ==>通道可以异步地读写. ...
- java的nio之:java的nio系列教程之概述
一:java的nio的核心组件?Java NIO 由以下几个核心部分组成: ==>Channels ==>Buffers ==>Selectors 虽然Java NIO 中除此之外还 ...
- java的nio之:java的nio系列教程之java的io和nio的区别
当学习了Java NIO和IO的API后,一个问题马上涌入脑海: 我应该何时使用IO,何时使用NIO呢?在本文中,我会尽量清晰地解析Java NIO和IO的差异.它们的使用场景,以及它们如何影响您的代 ...
- java的nio之:java的nio系列教程之pipe
Java NIO 管道是2个线程之间的单向数据连接.Pipe有一个source通道和一个sink通道.数据会被写到sink通道,从source通道读取. 这里是Pipe原理的图示:
- java的nio之:java的nio系列教程之DatagramChannel
Java NIO中的DatagramChannel是一个能收发UDP包的通道.因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入.它发送和接收的是数据包. 打开 DatagramChann ...
随机推荐
- 【转】ORACLE 表空间扩展方法
转载地址:http://blog.itpub.net/28950170/viewspace-763139/ 第一步:查看表空间的名字及文件所在位置: select tablespace_name, f ...
- c语言学生信息管理系统-学习结构体
#include<stdio.h> #include<stdlib.h> //结构体可以存放的学生信息最大个数,不可变变量 ; //学生信息结构体数组,最多可以存放100个学生 ...
- 简单的cxf-ws 基于web容器
pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w ...
- html5获取经纬度
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- 开源投影工具Proj——进行坐标转换
proj.4 is a standard UNIX filter function which converts geographic longitude and latitude coordinat ...
- SSH整合 第四篇 Spring的IoC和AOP
这篇主要是在整合Hibernate后,测试IoC和AOP的应用. 1.工程目录(SRC) 2.IoC 1).一个Service测试类 /* * 加入spring容器 */ private Applic ...
- 求解1^2+2^2+3^2+4^2+...+n^2的方法(求解1平方加2平方加3平方...加n平方的和)
利用公式 (n-1)3 = n3 -3n2 +3n-1 设 S3 = 13 +23 +33 +43 +...+n3 及 S2 = 12 +22 +32 +42 +...+n2 及 S1 = 1 +2 ...
- 理解ValueStack的基本机制 OGNL表达式
ValueStack基础:OGNL(Object Graphic Navigatino Language) OGNL是Struts2中使用的一种表达式语言. 它可以用于,在JSP页面,使用标签方便的访 ...
- DBCC--OPENTRAN
返回最早开始的但仍在运行的事务 数据库 'DB1' 的事务信息. 最早的活动事务: SPID (服务器进程 ID): 60 UID (用户 ID): -1 名称 : user_tra ...
- [翻译]NUnit---Sequential and SetCulture and SetUICulture Attributes(十八)
Sequential特性用于在测试用例上指定NUnit通过为测试提供的参数选择单一值生产测试用例,并且不会生产额外的组合. Note:如果参数数据由多个特性提供,那么NUnit使用数据项的顺序是随机的 ...