4.NIO 基础知识
Java NIO(new IO)是JDK1.4引入的非阻塞IO机制,具体它如何的好,我就不说啦,百度一下看看就了解啦。
Java NIO引入了两个新的概念:通道Channel和选择器Selector。
通道是服务端和客户端进行通信的接口-----原来是直接的IO操作,客户端发信息给服务端,服务端从OutputStream中读取,然后向InputStream中写数据;现在则直接从Channel中读取或写入数据。
选择器是一个多路复用器:所有的通道向它注册事件,因此它管理了所有的通道信息,并轮询各个通道的状态,一旦某个通道某事件发生(比如有数据读或可以写入数据),则通知该管道对应事件的处理器去处理它。
如下图所示:
客户端连接上服务端后,首先每个客户端都要与服务端建立一个通道(SocketChannel);然后每个通道向选择器(Selector)注册事件,注册器就会轮询查看每个通道是否有事件发生,一旦某通道有事件发生,比如Client1的SocketChannel有数据了,就触发了读就绪事件,可以进行读取的操作。
选择器(Selector)是个典型的反应器模式(Reactor Pattern),它的实现原理可以参考该文章:http://bijian1013.iteye.com/blog/2277792
先介绍下NIO的几个常用类:
(1).Buffer 缓冲区
Java NIO的缓冲区Buffer基本和Mina的IoBuffer一样,但是准确的说,Mina的IoBuffer就是对Java NIO buffer的二次封装,使得它的功能更加强大。
数据输入和输出往往是比较耗时的操作。缓冲区从两个方面提高I/O操作的效率:
1).减少实际的物理读写次数
这一点比较容易理解,我们经常使用的缓冲数组就是这个道理:
public void ioRead(String filePath) throws IOException { FileInputStream in = new FileInputStream(filePath); byte[] b = new byte[1024]; int i = 0; while ((i = in.read(b)) != -1) { logger.info(new String(b, 0, i)); } }
2).缓冲区在创建时被分配内存,这块内存区域一直被重用,这可以减少动态分配和回收内存区域的次数。
这一点是Buffer的优势,也是buffer性能较高的一个原因。
java.nio.Buffer类是一个抽象类,不能被实例化。共有8个具体的缓冲区类,其中最基本的缓冲区是ByteBuffer,它存放的数据单元是字节。ByteBuffer类并没有提供公开的构造方法,但是提供了两个获得ByteBuffer实例的静态工厂方法:
a.allocate(int capacity):返回一个ByteBuffer对象,参数capacity指定缓冲区的容量。
b.directAllocate(int capacity): 返回一个ByteBuffer对象,参数capacity指定缓冲区的容量。该方法返回的缓冲区称为直接缓冲区,它与当前操作系统能够更好的耦合,因此能进一步提高I/O操作的速度。但是分配直接缓冲区的系统开销很大,因此只有在缓冲区较大并且长期存在,或者需要经常重用时,才使用这种缓冲区。
同样常用的是CharBuffer,使用基本和上面一样,所有不多说啦!其他实现类我也基本不怎么用。
常用的方法也不多做解释了,翻看API文档吧。
(2).Charset 字符编码
Charset就是根据指定的编码个数进行编解码的一个接口,不多作解释。
a.Charset类的静态forName(String encode)方法返回一个Charset对象,它代表参数encode指定的编码类型。
b.ByteBuffer encode(String str):对参数str指定的字符串进行编码,把得到的字节序列存放在一个ByteBuffer对象中,并将其返回。
c.ByteBuffer encode(CharBuffer cb):对参数cb指定的字符缓冲区中的字符进行编码,把得到的字节序列存放在一个ByteBuffer对象中,并将其返回。
d.CharBuffer decode(ByteBuffer bb):把参数bb指定的ByteBuffer中的字节序列进行解码,把得到的字符序列存放在一个CharBuffer对象中,并将其返回。
(3).Channel 通道
通道在Java NIO的开始就做个解释,它是服务端和客户端进行通信的接口;通道Channel用来连接缓冲区与数据源或数据汇(即数据目的地)。如下图所示,数据源的数据经过通道到达缓冲区,缓冲区的数据经过通道到达数据汇。
但是在实际的开发中,我们基本都是使用Channel的实现类:ServerSocketChannel和SocketChannel,类图如下:
ServerSocketChannel从SelectableChannel中继承了configureBlocking()和register()方法。ServerSocketChannel是ServerSocket的替代类,也具有负责接收客户连接的accept()方法。
ServerSocketChannel并没有public类型的构造方法,必须通过它的静态方法open()来创建ServerSocketChannel对象。
每个ServerSocketChannel对象都与一个ServerSocket对象关联。ServerSocketChannel的socket()方法返回与它关联的ServerSocket对象。
private Selector selector; private int PORT = 3015; private ServerSocketChannel serverSocketChannel = null; // 创建一个Selector对象 selector = Selector.open(); // 创建一个ServerSocketChannel对象 serverSocketChannel = ServerSocketChannel.open(); // 使得在同一个主机上关闭了服务器程序,紧接着再启动该服务器程序时, // 可以顺利绑定到相同的端口 serverSocketChannel.socket().setReuseAddress(true); // 使ServerSocketChannel工作于非阻塞模式 serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
SocketChannel可看作是Socket的替代类,但它比Socket具有更多的功能;SocketChannel不仅从SelectableChannel父类中继承了configureBlocking()和register()方法,而且实现了ByteChannel接口,因此具有用于读写数据的read(ByteBuffer dst)和write(ByteBuffer src)方法。
SocketChannel没有public类型的构造方法,必须通过它的静态方法open()来创建SocketChannel对象。
private String HOST = "127.0.0.1"; private int PORT = 3015; private SocketChannel socketChannel = null; // 创建一个SocketChannel对象 socketChannel = SocketChannel.open(); // 使SocketChannel工作于非阻塞模式 socketChannel.configureBlocking(false); // InetAddress ia = InetAddress.getLocalHost(); // InetSocketAddress isa = new InetSocketAddress(ia, 3015); InetSocketAddress isa = new InetSocketAddress(HOST, PORT); socketChannel.connect(isa); logger.info("与服务器建立连接成功...."); // 创建一个Selector对象 selector = Selector.open();
其他不多做解释了,请参看NIO的API。
(4).Selector 选择器
选择器在Java NIO的开始就做个解释,它是一个多路复用器,所有的通道向它注册事件;只要ServerSocketChannel以及SocketChannel向Selector注册了特定的事件,Selector就会监控这些事件是否发生。
ServerSocketChannel以及SelectableChannel的register()方法负责注册事件,该方法返回一个SelectionKey对象,该对象是用于跟踪这些被注册事件的句柄。
在SelectionKey对象的有效期间,Selector会一直监控与SelectionKey对象相关的事件,如果事件发生,就会把SelectionKey对象加入到selected-keys集合中。
在以下情况,SelectionKey对象会失效,这意味着Selector再也不会监控与它相关的事件。
a.程序调用SelectionKey的cancel()方法;
b.关闭与SelectionKey关联的Channel;
c.与SelectionKey关联的Selector被关闭;
在SelectionKey中定义了四种事件,分别用4个int类型的常量来表示:
a.SelectionKey.OP_ACCEPT:接收连接就绪事件,表示服务器监听到了客户连接,服务器可以接收这个连接了,常量值为16。
b.SelectionKey.OP_CONNECT:连接就绪事件,表示客户与服务器的连接已经建立成功,常量值为8。
c.SelectionKey.OP_READ:读就绪事件,表示通道中已经有了可读数据,可以执行读操作了,常量值为1。
d.SelectionKey.OP_WRITE:写就绪事件,表示已经可以向通道写数据了,常量值为4。
最常用的就是后三种事件:连接就绪事件和读写就绪事件;下面就是它们的具体应用啦,请结合实例理解NIO的用法。
5.基于NIO的阻塞服务器
ServerSocketChannel与SocketChannel采用默认的阻塞模式,因此我们用NIO提供的API做一个阻塞服务器,和以前的阻塞服务器做个对比,以加深它们的区别;
服务端代码:
package com.bijian.study.mina.server; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.log4j.Logger; import com.bijian.study.mina.handler.Server05Handler; /* * 使用NIO的ServerSocketChannel创建阻塞的Socket服务端 * 使用JDK自带的线程池ExecutorService,多线程处理客户端请求 */ public class EchoServer05 { private Logger logger = Logger.getLogger(EchoServer05.class); private int PORT = 3015; private ServerSocketChannel serverSocketChannel = null; private ExecutorService executorService; // 线程池 private static final int POOL_MULTIPLE = 4; // 单个CPU时线程池中的工作线程个数 public EchoServer05() throws IOException { // 创建线程池 // Runtime的availableProcessors()方法返回当前系统的CPU格式 // 系统的CPU越多,线程池中工作线程的数目也越多 executorService = Executors.newFixedThreadPool(Runtime.getRuntime() .availableProcessors() * POOL_MULTIPLE); // ServerSocketChannel并没有public类型的构造方法, // 必须通过它的静态方法open()来创建ServerSocketChannel对象 // 默认是阻塞模式的,通过configureBlocking(false)设置为非阻塞模式 serverSocketChannel = ServerSocketChannel.open(); // 使得在同一个主机上关闭了服务器程序,紧接着再启动该服务器程序时, // 可以顺利绑定到相同的端口 serverSocketChannel.socket().setReuseAddress(true); // 每个ServerSocketChannel对象都与一个ServerSocket对象关联 // ServerSocketChannel的socket()方法返回与它关联的ServerSocket对象 serverSocketChannel.socket().bind(new InetSocketAddress(PORT)); logger.info("服务端启动.... 端口号:" + PORT); } public void service() { while (true) { // 阻塞 SocketChannel socketChannel = null; try { socketChannel = serverSocketChannel.accept(); // 等待连接 // 多线程处理 executorService.execute(new Server05Handler(socketChannel)); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String args[]) throws IOException { new EchoServer05().service(); } }服务器端的业务处理代码:
package com.bijian.study.mina.handler; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.nio.channels.SocketChannel; import java.util.Date; import org.apache.log4j.Logger; public class Server05Handler implements Runnable { private Logger logger = Logger.getLogger(Server05Handler.class); private SocketChannel socketChannel; public Server05Handler(SocketChannel socketChannel) { this.socketChannel = socketChannel; } public void run() { try { Socket socket = socketChannel.socket(); logger.info("一个新的请求达到并创建 " + socket.getInetAddress() + ":" + socket.getPort()); InputStream socketIn = socket.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(socketIn)); OutputStream socketOut = socket.getOutputStream(); PrintWriter pw = new PrintWriter(socketOut, true); String msg = null; while ((msg = br.readLine()) != null) { logger.info("服务端受到的信息为:" + msg); pw.println(new Date()); // 给客户端响应日期字符串 if (msg.equals("bye")) break; } } catch (IOException e) { e.printStackTrace(); } finally { try { if (socketChannel != null) socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } } }客户端代码:
package com.bijian.study.mina.client; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; import org.apache.log4j.Logger; /* * 使用NIO的SocketChannel创建阻塞的客户端 */ public class EchoClient05 { private Logger logger = Logger.getLogger(EchoClient05.class); private String HOST = "localhost"; private int PORT = 3015; private SocketChannel socketChannel; public EchoClient05() throws IOException { socketChannel = SocketChannel.open(); // InetAddress ia = InetAddress.getLocalHost(); InetSocketAddress isa = new InetSocketAddress(HOST, PORT); // socketChannel.connect()与远程主机建立连接 // 默认采用阻塞模式 socketChannel.connect(isa); } public void talk() throws IOException { try { // 通过socketChannel.socket()方法获得与SocketChannel关联的Socket对象, // 然后从这个Socket中获得输出流与输入流,再一行行的发送和接受数据。 // 获得服务端响应信息的输入流 InputStream socketIn = socketChannel.socket().getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader( socketIn)); // 给服务端发送信息的输出流 OutputStream socketOut = socketChannel.socket().getOutputStream(); PrintWriter pw = new PrintWriter(socketOut, true); BufferedReader localReader = new BufferedReader( new InputStreamReader(System.in)); String msg = null; while ((msg = localReader.readLine()) != null) { pw.println(msg); logger.info(br.readLine()); if (msg.equals("bye")) break; } } catch (IOException e) { e.printStackTrace(); } finally { try { socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) throws IOException { new EchoClient05().talk(); } }启动服务端和客户端,测试,无疑是成功的!
是不是感觉很别扭呀,明明两个ServerSocket和Socket解决的问题,却创建了一大堆的对象。NIO编程刚开始的却是感觉不爽呀,不过慢慢就适应啦。但如果使用Mina框架,你会发现,根本不需要关心什么NIO,它已经给你封装好啦。
6.基于NIO的非阻塞服务器
待完善……
7.多线程的基于NIO的非阻塞服务器
NIO有效解决了多线程服务器存在的线程开销问题,但在使用上略显得复杂一些。许多基于 NIO 的多线程服务器程序往往直接基于选择器(Selector)的 Reactor 模式实现。这种简单的事件机制对于较复杂的服务器应用,显然缺乏扩展性和可维护性, 而且缺乏直观清晰的结构层次。
二.异步操作分析
待完善……
三.Mina内部实现分析
待完善……
四.Mina的线程模型配置
先看官方文档的描述:(这里我就纯粹翻译一下吧,注意Mina的线程模型配置是针对Mina2.0以前的版本而言的,使用2.0以后版本的可以跳过)
1.禁止缺省的ThreadModel设置
MINA2.0及以后版本已经没有ThreadModel了,如果使用这些版本的话,可以跳过本节。
ThreadModel设置是在MINA1.0以后引入的,但是使用ThreadModel增加了配置的复杂性,推荐禁止掉缺省的TheadModel配置。
IoAcceptor acceptor = new SocketAcceptor(); SocketAcceptorConfig cfg = new SocketAcceptorConfig(); cfg.setThreadModel(ThreadModel.MANUAL);// 禁止掉ThreadModel的缺省配置
2.配置I/O工作线程的数量
这节只是NIO实现相关的,NIO数据包以及虚拟机管道等的实现没有这个配置。
在MINA的NIO实现中,有三种I/O工作线程:
a.Acceptor线程:接受进入连接,并且转给I/O处理器线程来进行读写操作。每一个SocketAcceptor产生一个Acceptor线程,线程的数目不能配置。
b.Connector线程:尝试连接远程对等机,并且将成功的连接转给I/O处理器线程来进行读写操作。每一个SocketConnector产生一个Connector线程,这个的数目也不可以配置。
c.I/O处理器线程:执行实际上的读写操作直到连接关闭。每一个SocketAcceptor或SocketConnector都产生它们自己的I/O处理线程。这个数目可以配置,缺省是1。
因此,对于每个IoService,可以配置的就是I/O处理线程的数目。下面的代码产生一个有四个I/O处理线程的SocketAcceptor:
IoAcceptor acceptor = new SocketAcceptor(4,Executors.newCachedThreadPool());
没有单凭经验来决定I/O处理线程数目的方法,一般设置为当前服务器CPU个数+1:
IoAcceptor acceptor = new SocketAcceptor(Runtime.getRuntime().availableProcessors() + 1, Executors.newCachedThreadPool());
3.增加一个ExecutorFilter到IoFilterChain中
ExecutorFilter是一个IoFilter,用于将进入的I/O事件转到一个 java.util.concurrent.Executor实现。事件会从这个Executor转到下一个IoFilter,通常是一个线程池。可以在 IoFilterChain的任何地方增加任意数目的ExecutorFilter,实现任何类型的线程模型,从简单的线程池到复杂的SEDA。
到现在为止我们还没有增加ExecutorFilter,如果没有增加ExecutorFilter,事件会通过方法调用转到一个 IoHandler,这意味着在IoHandler实现中的业务逻辑会在I/O处理线程里运行。我们叫这种线程模型为"单线程模型"。单线程模型可以用来就会低反应网络应用程序,受CPU限制的业务逻辑(如,游戏服务器)。
典型的网络应用需要一个ExecutorFilter插入到IoFilterChain中,因为业务逻辑和I/O处理线程有不同的资源使用模式。如果你用IoHandler的实现来执行数据库操作,而没有增加一个ExecutorFilter的话,那么,你整个服务器会在执行数据库操作的时候锁定,特别是数据库性能低的时候。下面的例子配置一个IoService在一个新的IoSession建立时增加一个ExecutorFilter。
cfg.getFilterChain().addLast("threadPool",new ExecutorFilter(Executors.newCachedThreadPool()));
如果server关闭,则execute也需要关闭。
使用一个ExecutorFilter通常不意味着要用一个线程池,对于Executor的实现没有任何限制。
4.应该把ExecutorFilter放在IoFilterChain的什么地方
这个要根据于具体应用的情况来定。如果一个应用有一个ProtocolCodecFilter实现和一个常用的有数据库操作的IoHandler实现的话,那么就建议在ProtocolCodecFilter实现的后面增加一个ExecutorFilter,这是因为大部分的协议解码实现的性能特性是受CPU限制的,和I/O处理线程是一样的。
// Add CPU-bound job first cfg.getFilterChain().addLast( "codec", new ProtocolCodecFilter(new VamsCodecFactory(Charset.forName("utf-8"), true))); // and then a thread pool cfg.getFilterChain().addLast("threadPool",new ExecutorFilter(Executors.newCachedThreadPool()));
5.选择IoService的线程池类型时要小心
Executors.newCachedThreadPool()经常是IoService首选的。因为如果使用其它类型的话,可能会对 IoService产生不可预知的性能方面的影响。一旦池中的所有线程都在使用中,IoService会在向池尝试请求一个线程时开始锁定,然后会出现一个奇怪的性能下降,这有时是很难跟踪的。
6.不推荐IoServices和ExecutorFilters共享一个线程池
你可以想让IoServices和ExecutorFilters共享一个线程池,而不是一家一个。这个是不禁止的,但是会出现很多问题,在这种情况下,除非你为IoServices建立一个缓冲线程池。
PS:Socket编程俗称Java网络编程,是Java Web开发的精髓!做J2EE的人可能很少关心多线程,NIO等等这些东西,但是不可否认它却实时与我们打交道,比如常用的web容器Tomcat。
Mina解析完整代码见附件MinaDemo05.rar。
相关推荐
### Mina 2.0快速入门与源码解析 #### Mina 2.0 快速入门 Mina 2.0 是一个基于Java NIO技术的高效、可伸缩的网络通信框架,广泛应用于服务器端开发。Mina 2.0 的设计目标是为开发者提供一个易于使用的API,使得...
在深入解析Mina源码的过程中,我们可以从以下几个关键知识点入手: 1. **Mina的过滤器机制实现**: Mina的核心设计理念之一是过滤器链(Filter Chain),它借鉴了Servlet的过滤器模型。每个过滤器都可以在数据传输...
你可以定义自定义的协议解析器,将接收到的原始字节流转换为有意义的业务对象,比如XML或JSON报文。在接收端,这些对象会被反序列化,从而可以进行业务逻辑处理。Mina的ProtocolDecoder和ProtocolEncoder接口是实现...
《Mina 框架源码解析 - 构建简单通信程序》 Mina(Minimal Asynchronous Network)是一个用于构建高性能、高可扩展性的网络应用程序的框架,它为开发者提供了低级的网络通信抽象,使其能够专注于业务逻辑,而不是...
这份手册可能详细解析了MINA服务器的搭建步骤、配置选项,以及如何处理各种网络事件。它还可能包含示例代码,展示如何处理客户端连接、读写数据、异常处理等常见任务,让开发者能快速上手MINA服务器开发。 学习MINA...
Apache Mina是一个强大的网络通信框架,专为基于TCP/IP和UDP/IP协议栈的应用设计。它提供了JAVA对象的序列化和虚拟机内部通信的功能,使得开发者能够迅速构建高性能、高可扩展性的网络应用。Mina的核心特性是其事件...
在Android平台上,MINA(Java Miniature Network Application Framework)是一个高度可扩展的网络应用程序框架,主要用作构建高性能、高效率的服务端和客户端应用程序。它提供了丰富的网络通信API,支持TCP/IP和UDP/...
Apache Mina是一个用Java编写的网络通信框架,它简化了创建高性能、高可用性的网络服务的过程。Mina的核心思想是提供一个与传输层协议无关的API,使得开发者可以专注于业务逻辑,而不需要关心底层的网络通信细节。在...
本文将深入探讨mina编解码器的工作原理,提供一个典型编解码器的示例,并解析其代码。 1. **mina框架基础** - Mina提供了一个高效的、事件驱动的网络应用程序框架,简化了网络编程,尤其是TCP和UDP通信。 - 它...
自定义编解码器可以实现特定的数据压缩、加密或者特定协议的解析,如XML、JSON或者其他二进制协议。这句话还强调了开源精神,表示技术应当被分享和共同进步,这也是MINA作为开源项目的一大优点。 在标签中,"mina...
描述中的"实现客户端输入数据的返回"意味着,当客户端发送数据到服务端时,服务端的Handler会接收到这些数据,进行解析或者处理,然后将处理后的结果返回给客户端。这种交互模式在很多网络应用中非常常见,例如FTP、...
这些jar文件包含了处理I/O操作、网络连接、协议解析等任务的类和方法。 描述中重复提到"mina.jar 包,内涵mina所需的所有jar包,解压即可,测试可用",意味着这个"mina.jar"可能是一个集成了所有依赖的单一文件,...
此外,Mina支持多种编码和解码器,如LineDelimiterFilter(按行分割数据)、ProtobufDecoder(解析Google Protobuf格式数据)等。在实际应用中,根据数据格式选择合适的过滤器,可以有效地处理网络传输中的数据。 ...
Mina框架解析** Mina(Minimal Asynchronous Network Library)是Apache软件基金会的一个开源项目,它提供了一个简单、高性能的API来处理网络通信。Mina支持多种协议,如TCP、UDP、HTTP、FTP等,可以用于构建服务器...
3. **HTTP协议解析**:在MINA中,我们需要编写代码来解析HTTP请求头和请求体,包括方法(GET、POST等)、URL、状态码、头部和内容。同时,对于响应,我们也要构造相应的HTTP响应头和体。 4. **编码与解码**:MINA...
1. **FTP协议**:理解FTP的基本工作流程,包括控制连接和数据连接,以及ASCII和二进制模式的区别。 2. **Apache Mina API**:熟悉Mina的核心概念,如Session、Filter和ProtocolHandler,以及如何使用它们来构建网络...
MINA的目标是简化网络编程,无论你是在处理TCP/IP协议如HTTP、FTP,还是UDP协议,或者是自定义的二进制协议,MINA都能提供统一的接口。 MINA的核心特性包括: 1. **非阻塞IO**:MINA基于NIO(Non-blocking I/O)...
1. 高效数据序列化:Protocol Buffers序列化后的数据占用空间小,解析速度快,与MINA结合可以提升网络传输效率。 2. 易于维护:通过定义.proto文件,数据结构的变化不会影响到网络通信代码,降低了维护成本。 3. 跨...