上一篇中有一行代码
//设定这个过滤器将一行一行地读取数据 chain.addLast("myChain", new ProtocolCodecFilter(new TextLineCodecFactory()));
这句代码其实并不简单,这句代码的意思是设置编码码工厂(就是一个能得到编码器和解码器的东西),这里我们用的是Mina自己提供的字符行编解码工厂,就是把一行文本认为是一条消息。
如果我们想要实现自己的通信协议,就需要自己编写编解码器了。这里我们定义这样一个协议
M sip:wap.fetion.com.cn SIP-C/2.0
S: 1580101xxxx
R: 1889020xxxx
L: 21
Hello World!
第一行代表协议类型,表示是一个飞信信息
第二行代表飞信的发送者
第三行代表飞信的接收者
第四行代表文本信息的长度
第五行开始代表实际的文本信息(注意这里不能用换行作为分隔符,因为实际的飞信内容里面也是有换行符的)
首先,我们定义一个实体类代表一条信息
package mina.common; /** * 协议的实体类 * @author 尹定宇 * @Email [email protected] * @version 2013-5-1 上午9:54:44 * @info */ public class SmsObject { private String sender;// 短信发送者 private String receiver;// 短信接受者 private String message;// 短信内容 /***getters & setters***/ }
编解码工厂,必须实现 ProtocolCodecFactory接口,这个工厂的作用就是得到具体的编码器和解码器对象
package mina.common; import java.nio.charset.Charset; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFactory; import org.apache.mina.filter.codec.ProtocolDecoder; import org.apache.mina.filter.codec.ProtocolEncoder; /** * 编解码工厂 * @author 尹定宇 * @Email [email protected] * @version 2013-5-2 上午10:22:51 * @info */ public class CmccSipcCodecFactory implements ProtocolCodecFactory { private final CmccSipcEncoder encoder; private final CmccSipcDecoder decoder; public CmccSipcCodecFactory() { this(Charset.defaultCharset()); } public CmccSipcCodecFactory(Charset charSet) { this.encoder = new CmccSipcEncoder(charSet); this.decoder = new CmccSipcDecoder(charSet); } @Override public ProtocolDecoder getDecoder(IoSession session) throws Exception { return decoder; } @Override public ProtocolEncoder getEncoder(IoSession session) throws Exception { return encoder; } }
编码器类,编码器类必须实现ProtocolEncoder接口,或者继承ProtocolEncoderAdapter接口,而且必须重写encode方法,它的第一个参数表示一个会话(还是和Java Web中的session有点像,可以存取上下文,有妙用,在下一篇介绍)第二个参数表示消息实体,第三个参数表示它要输出的信息
package mina.common; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolEncoderAdapter; import org.apache.mina.filter.codec.ProtocolEncoderOutput; /** * 继承ProtocolEncoderAdapter, * 而此类已经实现ProtocolEncoder其他不太需要我们关注的方法 * 我们只需重写编码方法 * @author 尹定宇 * @Email [email protected] * @version 2013-5-1 上午10:00:35 * @info */ public class CmccSipcEncoder extends ProtocolEncoderAdapter { private final Charset charset; public CmccSipcEncoder(Charset charset) { this.charset = charset; } /** * 必须实现的编码方法 */ @Override public void encode(IoSession arg0, Object message, ProtocolEncoderOutput out) throws Exception { SmsObject sms = (SmsObject) message; CharsetEncoder ce = charset.newEncoder(); IoBuffer buffer = IoBuffer.allocate(100).setAutoExpand(true); String statusLine = "M sip:wap.fetion.com.cn SIP-C/2.0"; String sender = sms.getSender(); String receiver = sms.getReceiver(); String smsContent = sms.getMessage(); buffer.putString(statusLine + '\n', ce); buffer.putString("S: " + sender + '\n', ce); buffer.putString("R: " + receiver + '\n', ce); buffer .putString("L: " + (smsContent.getBytes(charset).length) + "\n", ce); buffer.putString(smsContent, ce); buffer.flip(); out.write(buffer); } }
编码器的逻辑很清晰,就是拿到一个smsObject对象,然后按照协议规定那么一行一行地输出。
之后是解码器
package mina.common; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.CumulativeProtocolDecoder; import org.apache.mina.filter.codec.ProtocolDecoderOutput; /** * 解码器类 * @author 尹定宇 * @Email [email protected] * @version 2013-5-2 上午10:10:03 * @info */ public class CmccSipcDecoder extends CumulativeProtocolDecoder { private final Charset charset; public CmccSipcDecoder(Charset charset) { this.charset = charset; } @Override protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { IoBuffer buffer = IoBuffer.allocate(100).setAutoExpand(true); CharsetDecoder cd = charset.newDecoder(); int matchCount = 0; String statusLine = "", sender = "", receiver = "", length = "", sms = ""; int i = 1; while (in.hasRemaining()) { byte b = in.get(); buffer.put(b); if (b == 10 && i < 5) { matchCount++; if (i == 1) { buffer.flip(); statusLine = buffer.getString(matchCount, cd); statusLine = statusLine.substring(0, statusLine.length() - 1); matchCount = 0; buffer.clear(); } if (i == 2) { buffer.flip(); sender = buffer.getString(matchCount, cd); sender = sender.substring(0, sender.length() - 1); matchCount = 0; buffer.clear(); } if (i == 3) { buffer.flip(); receiver = buffer.getString(matchCount, cd); receiver = receiver.substring(0, receiver.length()-1); matchCount = 0; buffer.clear(); } if (i == 4) { buffer.flip(); length = buffer.getString(matchCount, cd); length = length.substring(0, length.length() - 1); matchCount = 0; buffer.clear(); } i++; } else if (i == 5) { matchCount++; if (matchCount == Long.parseLong(length.split(": ")[1])) { buffer.flip(); sms = buffer.getString(matchCount, cd); i++; break; } } else { matchCount++; } } SmsObject smsObject = new SmsObject(); smsObject.setSender(sender.split(": ")[1]); smsObject.setReceiver(receiver.split(": ")[1]); smsObject.setMessage(sms); out.write(smsObject); return false; } }
解码器的逻辑就不如编码器那么清晰,这里专门介绍一下。解码器里有个Byte b,表示当前从输入中读到的字节,int i,用来表示当时是在信息的第几行,变量matchCount表示是一行中的第几个字节。我们从输入中一个字节一个字节的读,根据i判断当前如果不是第五行,而且不是换行符(10),那么只将matchCount++,如果是换行符,就降这一行解码出来,设置为一个smsObject对象的对应属性;对于第五行之后则是不同的逻辑,因为实际信息内容中可能包括换行符,第五行之后是根据第四行中解码出的信息长度来截取的。
然后是服务器端的IoHandler示例了,这个很容易看懂
package mina.server; import mina.common.SmsObject; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IoSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyServerHandler extends IoHandlerAdapter{ //日志 private final static Logger log = LoggerFactory.getLogger(MyServerHandler.class); /** * 当一个客户端连接进入时 */ public void sessionOpened(IoSession session) throws Exception{ System.out.println("新接入的客户端"+session.getRemoteAddress()); } /** * 当一个客户端断开连接时 */ public void sessionClosed(IoSession session){ System.out.println("客户端断开连接"); } /** * 当有信息发来时 */ public void messageReceived(IoSession session,Object message){ SmsObject sms = (SmsObject)message; log.info("收到短信,内容是:【"+sms.getMessage()+"】"); } private int count = 0; }
客户端的IoHandler示例
package mina.client; import mina.common.SmsObject; import org.apache.mina.core.service.IoHandlerAdapter; import org.apache.mina.core.session.IoSession; public class MyClientHandler extends IoHandlerAdapter { public void sessionOpened(IoSession session) throws Exception{ System.out.println("连接到服务器"); SmsObject sms = new SmsObject(); sms.setSender("15580046143"); sms.setReceiver("10010"); sms.setMessage("今天天气不错,适合开例会"); session.write(sms); } public void sessionClosed(IoSession session){ System.out.println("与服务器断开连接"); } public void messageReceived(IoSession session,Object message){ } }
两边的主函数在这边就省略了,只需要将本篇开头中的编解码工厂换成我们自己编写的编解码工厂就好了。
最后讨论一个细节,编码器实现的是ProtocolEecoder接口,而解码器实现的是CumulativeProtocolDecoder接口,它为什么要使用这个接口呢?我们可以试着分析一下。在实际使用过程中,信息并不是一条一条发来的,很可能某一刻很多信息累积在一起,如果我们用传统的解码器,调用一次decode方法,解码出第一条信息之后,后面的信息就被我们废弃了,这是一个令我们不能容忍的问题。好在Mina早想到了这个问题,提供了CumulativeProtocolDecoder接口,它的函数dodecode工作原理是解码完成后如果后面还有后续的信息就把它作为这个方法的输入再调用一次自己,这样就很好的解决了上面所描述的问题。