nio&netty系列之一nio基础
写在文章初始
很久没有读源码了,近来据说netty的源码很漂亮,而且自身对nio的理解其实一直也不到位,所以有天突然心血来潮,决定沉下心,重新学习nio的知识,并通过读netty的源码来加深理解,但是现实生活中,工作,娱乐,心情等各种原因,导致整个过程很漫长,所以我决定一篇一篇的整理我的笔记内容发布到我的博客。算是一个进步过程吧.
JAVA NIO基础
NIO是jdk1.4推出的新概念,同JDK1.5版本concurrent并发包,是当前java应用中非常火的两个玩意,前者提升了网络传输编码的性能,后者整个将并发编程的高度提升了一个档次。此文首先从NIO的基础入手(jdk7都有nio2了,得赶紧补旧知识的说),然后再去慢慢深入netty的设计与实现,仅当做我最近一段时间从事netty使用这个工作的一个笔记。
NIO有三个重要概念:缓冲区(ByteBuffer),通道(Channel),选择器(Selected),通道传输的数据必须是缓冲区对象,通道注册在选择器上,选择器上有四个事件,op_accept(服务器上特有,接受连接事件),op_connection(客户端特有,建立连接事件),op_writer(写事件),op_read(读事件).
我们先略微介绍下这三个概念,如果对这三个概念需要细究的同学,附件有上传我学习的nio知识的PDF,个人认为已经很详细了.
- 缓冲区:首先在NIO中,我们不再以传统的socket的字符流传递数据,而是以块传递数据,是将数据放到缓存区然后推送到通道的。也就是说通道只接受Buffer对象,通常我们使用ByteBuffer.
- 通道:字面意思,客户端和服务器端连接的一条通道,用于传输数据,只接受Buffer对象。通道可以注册到选择器上。
-
选择器:这个概念较复杂,如果熟悉linux的IO系统的童鞋,那么恭喜,概念是类似的,java的nio的思想应该是来源于linux的异步非阻塞模式select.选择器用于管理所有的通道,是个事件触发模式,当通道就绪的时候,就会触发响应的事件并使对应的key就绪,然后主程序只要获取所有的就绪key(每一个key对应一个唯一的通道),然后做对应的业务逻辑即可。
NIO的过程
JAVA的NIO模型是借鉴了操作系统,应该可以说是直接借助了操作系统的IO模型,由于本人对NIO的源码未曾仔细深入,且对linux太菜,所以这些资料基本都是从别人的博客资料查询而知。因没自信介绍NIO的模型,所以暂时用生活的一个场景来描述NIO的工作过程。
餐厅点菜是一个很好的场景,我记得也有人用过。在以前,我们点菜的时候,服务员会等在你旁边记录你要点的菜,有时候你思考的越久,服务员等待的越久,这就是传统的SOCKET,这个服务员在你点菜的过程中被阻塞了。那么现在点菜就不一样了,服务员给你一张菜单和点菜单,你自己写完菜单然后叫服务员即可,这就是NIO模型,那我们就来分析这个新的点菜过程。
1、客户进入餐厅,餐厅总台指定一个服务员上来迎接,帮你安排座位,给你菜单和点菜单。
2、服务员去总台服务员那里登记有新客户到来,并且由我服务,客户正在准备点餐。
3、客户思考今天要吃什么菜,并且记录到点菜单上,让服务员把点菜单放到总台哪边去
4、总台服务员定时扫描点菜单
5、将点菜内容发送给厨房。
6、厨房厨师开始准备指定的餐
7、总台服务员定时扫描,看那些客户的菜做完了,就通知与客户对应的服务员过来
8、服务员就把菜送到客户那里。
这里我们客户初略的概括为,餐厅总台是服务器,客户是客户端,服务员是通道(channel)。对于餐厅而言,服务员收取点餐单是读事件,送餐是写事件,点菜单和餐都是缓冲区(buffer),总台的服务员是选择器(selector),厨房的厨师就是业务逻辑实现者。所以上述六个过程概括为
1、餐厅与客户创建一个通道,这个通道是指服务员。
2、然后将此通道注册到selector(总台服务员)上,并为此selector注册了一个读事件(告诉总台服务员客户要准备点餐)
3、客户写数据到点菜单,并有通道(服务员)交给餐厅总台哪里。
4、服务器端的选择器selector查看哪边的点菜单写完了,触发服务器的读事件
5、总台读取缓存区(点菜单)数据发送到业务逻辑那里
6、业务逻辑(做菜),把做好的菜放到缓冲区(餐盘)
7、selector扫描哪个客户的数据处理完毕,就通知指定通道
8、通道将数据传输给客户。
NIO与传统socket对比
那么说到此处,概念仍旧模糊,那么这个NIO与传统的Socket编程的优势体现在哪里?我来比较下传统socket编程的流程和nio编程的流程来分析下孰优孰劣.
传统socket编程流程:
1、客户端创建一个socket连接到服务器
2、客户端等待socket建立连接(阻塞)
3、客户端发起请求
4、客户端读取数据(阻塞)
5、结束。
注意:
第一步:创建socket建立连接是个阻塞模式,所以说创建socket的成本很高,以前编程我们会使用一个socket连接池,创建一批socket连接放到连接池中去,从连接池获取socket进行操作,操作完成归还socket回连接池,例如数据库连接池是一样的设计。我们通常称之为长连接。
第三步,客户端读取数据是个阻塞模式,在读取数据的时候我在等待服务器返回数据。如果服务器响应时间为10S,那么我这里要等待10秒才能获取数据。这个很难优化。
NIO编程流程:
1、客户端创建一个通道(SocketChannel)
2、创建一个选择器(Selector,如果没有选择器的话)
3 、通道注册到选择器上,注册事件为OP_CONNECT
4、客户端选择器触发OP_CONNECT事件。(编码中移除该就绪key)
5、捕捉到OP_CONNECT后等待连接创建完毕(阻塞)
6、连接创建完毕,直接发送数据。并且注册当前通道为OP_READ事件。
7、当服务器端有数据返回的时候,触发通道的OP_READ事件
8、读取内容。
其实大家如果换位思考,上面我换成服务器端,那么大家思考下,传统socket编程模式下,客户端与服务器端建立socket后,服务器首先也是在等待客户端传输数据过来,这个过程是一直阻塞的,而服务器端此过程完全没必要使用同步,这是很消耗服务器性能的。在NIO模式下,服务器与客户端建立完通道,服务器注册一个OP_READ事件后,服务器的CPU就可以去忙其它事情了,等客户端数据传输过来后,服务器才会触发读事件并处理相应业务逻辑,所以NIO对于服务器的性能提升是很明显的,并且由于NIO使用了块的传输方式,充分的利用了当今操作系统的性能,在传输性能上也会有明显的提升。
BTW:这是我对传统socket编程和NIO的一些浅薄理解
JAVA NIO 简单实例
//服务器端类
public class NioService {
private static final int port = 4444;
private Selector selector = null;
// 启动服务
public void startUp() {
try {
int channels = 0;
int nKeys = 0;
int currentSelector = 0;
// 创建一个通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel
. open();
// 绑定地址,类似 sokcet的绑定
serverSocketChannel.socket().bind(
new InetSocketAddress(InetAddress.getLocalHost(), 4444));
// 必须设置成非同步模式,在要注册到选择器的前提下,否则在注册的时候会报异常
serverSocketChannel.configureBlocking( false);
// 使用Selector
selector = Selector.open();
// 注册一个感兴趣的事件,这里是OP_ACCEPT事件.
SelectionKey s = serverSocketChannel.register(selector ,
SelectionKey. OP_ACCEPT);
System. out.println("服务器启动成功" );
listen();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void listen() throws IOException {
int nKeys = 0;
while (true ) {
// 阻塞线程,至到有就绪通道,返回值为有多少key已经就绪
nKeys = selector.select();
//获取就绪key的集合
Set selectedKeys = selector.selectedKeys();
for (Iterator keys = selectedKeys.iterator(); keys.hasNext();) {
SelectionKey key = (SelectionKey) keys.next();
//将就绪的key从就绪key中移除掉,否则每次获取都是就绪的,而非更新的就绪key
keys.remove();
if (key.isAcceptable()) {
SocketChannel socketChannel= ((ServerSocketChannel) key.channel()).accept().socket()
.getChannel();
socketChannel.configureBlocking( false);
socketChannel.register( selector,
SelectionKey. OP_READ);
} else if (key.isReadable()) {
Socket socket = ((SocketChannel) key.channel()).socket();
SocketChannel sc = socket.getChannel();
sc.configureBlocking( false);
ByteBuffer bb = ByteBuffer. allocate(1024);
sc.read(bb);
System. out.println("客户端接受到的数据为:" +new String(bb.array()));
sc.register( selector,
SelectionKey. OP_WRITE);
} else if (key.isWritable()) {
Socket socket = ((SocketChannel) key.channel()).socket();
SocketChannel sc = socket.getChannel();
ByteBuffer bb = ByteBuffer. wrap("hello world again!"
.getBytes());
sc.write(bb);
sc.configureBlocking( false);
sc.register( selector,
SelectionKey. OP_READ);
}
}
}
}
public static void main(String[] ben) {
NioService ns = new NioService();
ns.startUp();
}
//客户端类
public class NioClient {
public static void main(String[] ben) throws IOException{
int selectNum;
Selector selector = Selector. open();
SocketChannel socketChannel = SocketChannel. open();
socketChannel.configureBlocking( false);
socketChannel.register(selector, SelectionKey. OP_CONNECT);
socketChannel.connect( new InetSocketAddress(InetAddress.getLocalHost(), 4444));
while(true ){
selectNum = selector.select();
if(selectNum>0){
Set<SelectionKey> keys= selector.selectedKeys();
for(Iterator<SelectionKey> it = keys.iterator();it.hasNext();){
SelectionKey key = it.next();
it.remove();
if(key.isConnectable()){
SocketChannel channel = ((SocketChannel)key.channel());
channel.configureBlocking( false);
// 判断此通道上是否正在进行连接操作。
// 完成套接字通道的连接过程。
if (channel.isConnectionPending()) {
channel.finishConnect();
channel.write(ByteBuffer. wrap("client connection over!".getBytes()));
channel.register(selector, SelectionKey.OP_WRITE );
}
} else if (key.isWritable()){
key.channel().configureBlocking( false);
((SocketChannel)key.channel()).write(ByteBuffer. wrap("hello,i'm from client!".getBytes()));
key.channel().register(selector, SelectionKey.OP_READ );
//客户端线程去处理其它业务逻辑了
System. out.println("cc" );
System. out.println("cc" );
System. out.println("cc" );
System. out.println("cc" );
System. out.println("cc" );
System. out.println("cc" );
} else if (key.isReadable()){
ByteBuffer bb = ByteBuffer.allocate(1024);
((SocketChannel)key.channel()).read(bb);
System. out.println(new String(bb.array()));
key.channel().configureBlocking( false);
// key.channel().register(selector, SelectionKey.OP_WRITE);
//本次模拟的是一次读取过程,所以读完后就关闭
key.channel().close();
selector.close();
}
}
}
}
}
}
上一篇: NIO 编程示例代码
下一篇: 一个NIO的demo