之前的文章主要讲解了Reactor模式、多路复用器select、以及Netty框架,这篇文章主要是java Nio的代码,但我们知道java的Nio是使用了Reactor模式和多路复用器来实现的,所以下面看看java nio的demo。
一、java Nio 客户端
package com.test2;
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.SocketChannel;
import java.util.Iterator;
public class Client {
//多路复用器、选择器(具体看使用的操作系统以及jdk版本,1.5有可能就是select而1.7就是epoll)
private Selector selector;
public Client init(String serverIp, int port) throws IOException {
// 获取socket通道、
// 在reactor模式中的资源
// select、epoll函数中的fd(个人理解如有错误请求指正)
SocketChannel channel = SocketChannel.open();
// 将该通道设置为非阻塞
channel.configureBlocking(false);
// 获取多路复用器实例
selector = Selector.open();
// 客户端连接服务器,需要调用channel.finishConnect();才能实际完成连接。
channel.connect(new InetSocketAddress(serverIp, port));
// 为该通道注册SelectionKey.OP_CONNECT事件,也就是将channel的fd和感兴趣的事件添加到多路复用器中
channel.register(selector, SelectionKey.OP_CONNECT);
return this;
}
public void listen() throws IOException {
System.out.println("客户端启动");
// 轮询访问selector
while (true) {
// 选择注册过的io操作的事件(第一次为SelectionKey.OP_CONNECT)
// 看过之前文章的话,就明白这是获取注册到该复用器中的通道的相关事件,获取的过程也就是select()方法遍历获取事件不同的系统、jdk也不同,如果是epoll则是从一个fd就绪队列获取
// 而select和poll则是遍历存放channel和事件的集合所以效率低下一些,还有其他的效率问题可以看看之前epoll和select的讲解
selector.select();
//获取注册在该复用器上的channel和channelEvent
Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = ite.next();
// 删除已选的key,防止重复处理
ite.remove();
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
// 如果正在连接,则完成连接
if (channel.isConnectionPending()) {
channel.finishConnect();
}
// channel.configureBlocking(false);
// 向服务器发送消息
channel.write(ByteBuffer.wrap(new String("send message to server.").getBytes()));
// 连接成功后,注册接收服务器消息的事件
channel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接成功");
} else if (key.isReadable()) { // 判断该channel的channelEvent事件类型,也就是reactor模式中的分发器,如果把里面处理过程进行封装就是处理器了
// 因为selectionKey中存放的是channel和channelEvent所以可以通过selectionKey就能获取channel
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
byte[] data = buffer.array();
String message = new String(data);
System.out.println("recevie message from server:, size:"
+ buffer.position() + " msg: " + message);
ByteBuffer outbuffer = ByteBuffer.wrap(("client.".concat(message)).getBytes());
channel.write(outbuffer);
}
}
}
}
public static void main(String[] args) throws IOException {
new Client().init("127.0.0.1", 9981).listen();
}
}
二、java Nio服务端
package com.test2;
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;
/**
* 客户端还是服务端资源、复用器、分发器、处理器整个流程使用都是一样的……
* @author sykj
*
*/
public class Server {
//多路复用器
private Selector selector;
public Server init(int port) throws IOException {
// 获取一个ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
// 获取多路复用器对象
selector = Selector.open();
// 将通道管理器与通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,
// 只有当该事件到达时,Selector.select()会返回,否则一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
return this;
}
public void listen() throws IOException {
System.out.println("服务器端启动成功");
// 使用轮询访问selector
while (true) {
selector.select();
Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = ite.next();
ite.remove();
// 客户端请求连接事件
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获得客户端连接通道
SocketChannel channel = server.accept();
channel.configureBlocking(false);
// 向客户端发消息
channel.write(ByteBuffer.wrap(new String("send message to client").getBytes()));
// 在与客户端连接成功后,为客户端通道注册SelectionKey.OP_READ事件。
channel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端请求连接事件");
} else if (key.isReadable()) {// 有可读数据事件
// 获取客户端传输数据可读取消息通道。
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取数据缓冲器
ByteBuffer buffer = ByteBuffer.allocate(10);
int read = channel.read(buffer);
byte[] data = buffer.array();
String message = new String(data);
ByteBuffer outbuffer = ByteBuffer.wrap(message.getBytes());
channel.write(outbuffer);
}
}
}
}
public static void main(String[] args) throws IOException {
new Server().init(9981).listen();
}
}
OK 有代码有注释应该能看明白,个人感觉javaNio和Reactor模式对应起来理解Reactor 模式还是比较好,Reactor模式是资源、选择器、分发器和处理器这几个模块组成.
在java Nio中个人理解的对应关系:
这就是个人理解,如果感觉不会好过于简单的可以查看前面文章对reactor有详细分析……
java Bio对比java Nio
Bio和Nio底层的实现模型不同,所以区别也就是从这里开始,Bio即阻塞同步IO而Nio即非阻塞同步IO他们最大的区别就是一个是阻塞一个非阻塞的,阻塞非阻塞就是对请求响应的一种形态。在效率问题上的Nio相比Bio最大的区别就是Reactor模式的使用Reactor模式中的多路复用在多并发访问以及高并发访问的情况下有更少的资源消耗,因为就复用器一个线程能管理多个或所有客户端的连接关系,相比一个客户端请求一个线程资源的消耗很小。
Bio的阻塞是在accept阻塞的,而Nio的阻塞是在事件上阻塞的,不是说Nio是非阻塞的吗(那是对客户端请求来说是非阻塞的),而服务端对获取客户端请求的等待是阻塞的。
这么说吧,在服务端获得客户端请求不管是Bio还是Nio都是阻塞的,但他们阻塞的时间地点不同,之后说的所有情况都是在客户端和服务端之间已经成功建立tcp链接。
Bio 每当有请求来的时候都是通过服务端的accept()方法来获取客户端的请求,服务端获取请求后对这个请求进行后续的处理,在将处理结果返回给客户端
而在Nio中(epoll)使用了多路复用器,所以当某个客户端请求来的时候,会把该客户端连接和客户端请求事件保存到 FD事件队列,在程序中轮询select时是对有事件的channel和事件进行了轮询,他的阻塞是在select()时阻塞的,而客户端不用同步等待服务端消息,服务端获得select()中的事件后能立马先给客户端返回一个成功消息,将处理后的信息之后再传过去
Bio 和 Nio服务端阻塞的事件和时间点不同、Bio 和 Nio对客户端的响应也是不同以及对客户端请求事件的轮询查询不同。
总的来说Nio相比Bio消耗资源少,获取客户端请求事件循环的队列小,也就是使用复用器带来的好处,其他的区别……