学习笔记第二部分,继续摘抄《MINA2学习笔记》。
(三)调试的一点小结 ps写在前面。
今天实际调试例子代码,给两天来的工作做个小结:1、按照例子来弄,通信是比较简单的,基本上直接套用就可以了;2、没有Java编程基础,要回头重新掌握Charset、String等的一些常用文本字符串处理类型;3、协议分层处理,基本上:codec编解码器就是处理字符收发,把帧的问题解决掉,把一串二进制流转换成消息结构(解码)或者反过来(编码);IOHandler部分处理业务,实际处理消息内容;4、协议分层也是一个限制。尝试不注册任何的Filter,这是能够接收到消息,但是在发送时,会出Encode为空指针的异常。5、 传递给codec的实际是Simpbuffer,可以按照IoBuffer来处理哦,但不能按照字符串来处理。decoder里面用write写入的发送消息encoder先接收到,然后才到发送部分。
下面继续抄:
2.2 如何自定义协议编解码器
协议编解码器是在使用Mina 的时候最需要关注的对象,因为网络传输的数据都是二进制数据(byte),而在程序中面向的是JAVA 对象,这就需要在发送数据时将JAVA 对象编码二进制数据,接收数据时将二进制数据解码为JAVA 对象。
协议编解码器是通过ProtocolCodecFilter过滤器构造的,看它的构造方法,它需要一个ProtocolCodecFactory对象:
publicProtocolCodecFilter(ProtocolCodecFactory factory) {
if (factory ==null) {
thrownew NullPointerException("factory");
}
this.factory = factory;
}
ProtocolCodecFactory接口非常直接,通过ProtocolEncoder和
ProtocolDecoder对象来构建!
publicinterfaceProtocolCodecFactory{
/**
* Returns a new(orreusable)instanceof{@link ProtocolEncoder}which
* encodes message objects into binary or protocol-specificdata.
*/
ProtocolEncoder getEncoder(IoSession session)throws Exception;
/**
* Returns a new(orreusable)instanceof{@link ProtocolDecoder}which
* decodes binary or protocol-specificdatainto messageobjects.
*/
ProtocolDecoder getDecoder(IoSession session)throws Exception;
}
制定协议的方法:
定长消息法:这种方式是使用长度固定的数据发送,一般适用于指令发送。
字符定界法:这种方式是使用特殊字符作为数据的结束符,一般适用于简单数据的发送。
定长报文头法:使用定长报文头,在报文头的某个域指明报文长度。
根据协议,把二进制数据转换成Java对象称为解码(也叫做拆包);把Java对象转换为二进制数据称为编码(也叫做打包)
2.3 IoBuffer常用方法:
协议解析这块主要就是将Buffer中的二进制数据转换成字符串,方便Handler做业务层面的处理。
Mina中传输的所有二进制信息都存放在IoBuffer中,IoBuffer是对Java NIO中ByteBuffer的封装(Mina2.0以前版本这个接口也是ByteBuffer),提供了更多操作二进制数据,对象的方法,并且存储空间可以自增长,用起来非常方便;简单理解,它就是个可变长度的byte数组!
1. static IoBufferallocate(int capacity,boolean useDirectBuffer)
创建IoBuffer实例,第一个参数指定初始化容量,第二个参数指定使用直接缓冲区还是JAVA 内存堆的缓存区,默认为false。
2.IoBuffer setAutoExpand(boolean autoExpand)
这个方法设置IoBuffer 为自动扩展容量,也就是前面所说的长度可变,那么可以看出长度可变这个特性默认是不开启的。
1.IoBuffer flip()
limit=position ,position=0,重置mask,为了读取做好准备,一般是结束buf操作,将buf写入输出流时调用;这个必须要调用,否则极有可能position!=limit,导致position后面没有数据;每次写入数据到输出流时,必须确保position=limit。 备注:flip的中文意义类似于翻转,翻筋斗。
2.IoBuffer clear()与IoBuffer reset()
clear:limit=capacity ,position=0,重置mark;它是不清空数据,但从头开始存放数据做准备---相当于覆盖老数据。
reset就是clear()+ 清空数据
5. int remaining()与boolean hasRemaining()
这两个方法一般是在调用了flip()后使用的,remaining()是返回limt-position的值!hasRemaining()则是判断当前是否有数据,返回position <limit的boolean值!
为了更好理解,引用ByteBuffer的一些解释:(IoBuffer - 博客频道 - CSDN.NET http://blog.csdn.net/chengzhaoan2010/article/details/54909571)
缓冲区都有4个属性:capacity、limit、position、mark,并遵循:mark <= position <= limit <= capacity:
重要属性描述Capacity | 容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变 |
Limit | 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的 |
Position | 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备 |
Mark | 标记,调用mark()来设置mark=position,再调用reset()可以让position恢复到标记的位置 |
重要方法描述
| limit(), limit(10): 其中读取和设置这4个属性的方法的命名和jQuery中的val(),val(10)类似,一个负责get,一个负责set |
reset() :把position设置成mark的值,相当于之前做过一个标记,现在要退回到之前标记的地方 | |
clear():position = 0;limit = capacity;mark = -1; 有点初始化的味道,但是并不影响底层byte数组的内容 | |
| flip():limit = position;position = 0;mark = -1; 翻转,也就是让flip之后的position到limit这块区域变成之前的0到position这块,翻转就是将一个处于存数据状态的缓冲区变为一个处于准备取数据的状态。 做好输出状态转换到读取状态准备,一般Send后要做这个调用。 |
| rewind() :把position设为0,mark设为-1,不改变limit的值 |
| remaining(): return limit - position;返回limit和position之间相对位置差 |
| hasRemaining():return position < limit返回是否还有未读内容 |
a)底层(编解码器)是按照字节来处理接收的内容的,可能发送了200个字节到缓冲区,但是消息还没有接收完,这个就是笔记里面说的要考虑“解码器所解码时未完成的数据”,多个线程时可能出现混乱的问题。
b)业务层是在底层(编解码器)的基础上工作的,这个就是接收到消息,按照消息来处理了,不出意外的情况下,就是按照协议定义的消息。只要考虑业务就行了,这个也就是分层的好处了。
3、IoHandler接口
IoHandler是Mina实现其业务逻辑的顶级接口;它相当简单,你就理解它是根据事件触发的简单应用程序即可。
在IoHandler中定义了7个方法,根据I/O事件来触发对应的方法:
import java.io.IOException;
public interface IoHandler {
void sessionCreated(IoSessionsession)throws Exception;
void sessionOpened(IoSessionsession)throws Exception;
void sessionClosed(IoSessionsession)throws Exception;
void sessionIdle(IoSession session,IdleStatus status)throws Exception;
void exceptionCaught(IoSessionsession, Throwable cause)throws Exception;
void messageReceived(IoSessionsession, Object message)throws Exception;
void messageSent(IoSession session,Object message)throws Exception;
}
sessionCreated:当一个新的连接建立时,由I/Oprocessor thread调用;
sessionOpened:当连接打开是调用;
messageReceived:当接收了一个消息时调用;
messageSent:当一个消息被(IoSession#write)发送出去后调用;
sessionIdle:当连接进入空闲状态时调用;
sessionClosed:当连接关闭时调用;
exceptionCaught:当实现IoHandler的类抛出异常时调用;
一般情况下,我们最关心的只有messageReceived方法,接收消息并处理,然后调用IoSession的write方法发送出消息!(注意:这里接收到的消息都是Java对象,在IoFilter中所有二进制数据都被解码啦!)
一般情况下很少有人实现IoHandler接口,而是继承它的一个实现类IoHandlerAdapter,这样不用覆盖它的7个方法,只需要根据具体需求覆盖其中的几个方法就可以!
三、MINA解析
3.1 通信服务器模型
Mina是一个Java NIO框架;而NIO的基本思想是:服务器程序只需要一个线程就能同时负责接收客户的连接、客户发送的数据,以及向各个客户发送响应数据。服务器程序的处理流程如下:
//阻塞
while(一直等待,直到有接收连接就绪事件、读就绪事件或写就绪事件发生){
if(有客户连接)
接收客户的连接; //非阻塞
if(某个Socket的输入流中有可读数据)
从输入流中读数据; //非阻塞
if(某个Socket的输出流可以写数据)
向输出流写数据; //非阻塞
}
而传统的并发型服务器则是采用多线程的模式响应用户请求的;
//阻塞
while(一直等待){
if(有客户连接)
启动新线程,与客户的通信; //可能会阻塞
}
但是,无论如何,服务端共同的结构如下:
1. Read Request; 接受请求
2. Decode Request;请求值解码(读)
3. Process Service;请求处理
4. Encode Reply; 响应值编码(写)
5. Send Reply; 发送响应
举例类型:单个阻塞服务器(缺点:只处理一个连接)--》多线程阻塞服务器(缺点:连接数不受控制)--》线程池阻塞服务器(连接受线程池控制,稳定)。 实际开发者使用JDK自带的线程池:java.util.concurrent包提供了现成的线程池的实现.
3.2 JAVA NIO服务器
更进一步: 使用JAVA NIO服务器,比较SDK的自带的线程池更进一步:
Java NIO引入了两个新的概念:通道Channel和选择器Selector;
通道是服务端和客户端进行通信的接口-----原来是直接的IO操作,客户端发信息给服务端,服务端从OutputStream中读取,然后向InputStream中写数据;现在则直接从Channel中读取或写入数据;
选择器是一个多路复用器:所有的通道向它注册事件,因此它管理了所有的通道信息,并轮询各个通道的状态,一旦某个通道某事件发生(比如有数据读或可以写入数据),则通知该管道对应事件的处理器去处理它;
客户端连接上服务端后,首先每个客户端都要与服务端建立一个通道(SocketChannel);然后每个通道向选择器(Selector)注册事件,注册器就会轮询查看每个通道是否有事件发生,一旦某通道有事件发生,比如Client1的SocketChannel有数据了,就触发了读就绪事件,可以进行读取的操作啦。
SocketChannel可看作是Socket的替代类,但它比Socket具有更多的功能;SocketChannel不仅从SelectableChannel父类中继承了configureBlocking()和register()方法,而且实现了ByteChannel接口,因此具有用于读写数据的read(ByteBuffer dst)和write(ByteBuffer src)方法;
SocketChannel没有public类型的构造方法,必须通过它的静态方法open()来创建SocketChannel对象。
SelectionKey中定义了四种事件,分别用4个int类型的常量来表示:
l SelectionKey.OP_ACCEPT:接收连接就绪事件,表示服务器监听到了客户连接,服务器可以接收这个连接了。常量值为16
l SelectionKey.OP_CONNECT:连接就绪事件,表示客户与服务器的连接已经建立成功。常量值为8。
l SelectionKey.OP_READ:读就绪事件,表示通道中已经有了可读数据,可以执行读操作了。常量值为1。
l SelectionKey.OP_WRITE:写就绪事件,表示已经可以向通道写数据了。常量值为4。
总结Selector工作流程如下:
a.Channel向Selector注册连接就绪/读/写事件;
b.获得就绪事件的SelectionKey列表,遍历列表;
c.处理每个就绪事件;
仔细看代码,遍历 SelectionKey列表时,每拿出一个 SelectionKey,处理都是 线性执行的: (很重要,是线性执行哦)