使用Mina模拟短信的收发

完整版见https://jadyer.github.io/2012/10/19/mina-packet/




This is Apache Mina 2.0.4, Let`s drink code....


下面是用于模拟短信协议内容的实体类

package com.mina.model;

/**
 * 模拟短信协议内容的对象
 * @see M sip:wap.fetion.com.cn SIP-C/2.0 //状态行,一般表示协议的名字、版本号等
 * @see S: 1580101xxxx                    //短信的发送号码
 * @see R: 1880202xxxx                    //短信的接收号码
 * @see L: 21                             //短信的字节数
 * @see 你好!!Hello World!!               //短信的内容
 * @see 上面每行的末尾使用ASCII的10(\n)作为换行符
 */
public class SmsObject {
	private String sender;   //短信发送者
	private String receiver; //短信接收者
	private String message;  //短信内容
	/*三个属性的getter和setter略*/
}

下面是Mina编写的服务端主类MyServer.java

package com.mina.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;

import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

import com.mina.factory.CmccSipcCodecFactory;

public class MyServer {
	public static void main(String[] args) throws IOException {
		IoAcceptor acceptor = new NioSocketAcceptor();
		acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
		acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new CmccSipcCodecFactory(Charset.forName("UTF-8"))));
		acceptor.setHandler(new ServerHandler());
		acceptor.bind(new InetSocketAddress(9876));
		System.out.println("Mina Server is Listing on := 9876");
	}
}

下面是服务端的消息处理器ServerHandler.java

package com.mina.server;

import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;

import com.mina.model.SmsObject;

public class ServerHandler extends IoHandlerAdapter {
	@Override
	public void messageReceived(IoSession session, Object message) throws Exception {
		SmsObject sms = (SmsObject)message;
		System.out.println("The message received from Client is [" + sms.getMessage() + "]");
	}
	@Override
	public void sessionOpened(IoSession session) throws Exception{
		System.out.println("InComing Client:" + session.getRemoteAddress());
	}
}

下面是Mina编写的客户端主类MyClient.java

package com.mina.client;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;

import org.apache.mina.core.service.IoConnector;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

import com.mina.factory.CmccSipcCodecFactory;

public class MyClient {
	public static void main(String[] args) {
		IoConnector connector = new NioSocketConnector();
		connector.setConnectTimeoutMillis(3000);
		connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(new CmccSipcCodecFactory(Charset.forName("UTF-8"))));
		connector.setHandler(new ClientHandler());
		connector.connect(new InetSocketAddress("localhost", 9876));
	}
}

下面是客户端的消息处理器ClientHandler.java

package com.mina.client;

import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;

import com.mina.model.SmsObject;

public class ClientHandler extends IoHandlerAdapter {
	@Override
	public void sessionOpened(IoSession session) throws Exception {
		SmsObject sms = new SmsObject();
		sms.setSender("15025302990");
		sms.setReceiver("13716700602");
		sms.setMessage("Hi Jadyer,这是我用Mina2.x发给你的消息....");
		session.write(sms);
	}
}

下面是我们自定义的编解码工厂类CmccSipcCodecFactory.java

package com.mina.factory;

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;

import com.mina.codec.CmccSipcDecoder;
import com.mina.codec.CmccSipcEncoder;

/**
 * 自定义编解码工厂
 * @see 实际上这个工厂类就是包装了编码器、解码器
 * @see 通过接口中的getEncoder()、getDecoder()方法向ProtocolCodecFilter过滤器返回编解码器实例
 * @see 以便在过滤器中对数据进行编解码
 */
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 arg0) throws Exception {
		return decoder;
	}
	@Override
	public ProtocolEncoder getEncoder(IoSession arg0) throws Exception {
		return encoder;
	}
}

重头戏:下面是我们自定义的编码器CmccSipcEncoder.java

package com.mina.codec;

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;

import com.mina.model.SmsObject;

/**
 * 自定义编码器
 * Mina中编写编码器可以实现ProtocolEncoder,其中有encode()、dispose()两个方法需要实现
 * dispose()用于在销毁编码器时释放关联的资源,由于该方法一般我们并不关心,故通常直接继承适配器ProtocolEncoderAdapter
 * @see ==============================================================================================================
 * @see 相比较解码(字节转为JAVA对象,也叫拆包)来说,编码(Java对象转为字节,也叫做打包)就很简单了
 * @see 我们只需要把Java对象转为指定格式的字节流,然后write()就可以了
 * @see ==============================================================================================================
 * @see 解码器的编写有以下几个步骤
 * @see 1、将encode()方法中的message对象强制转换为指定的对象类型
 * @see 2、创建IoBuffer缓冲区对象,并设置为自动扩展
 * @see 3、将转换后的message对象中的各个部分按照指定的应用层协议进行组装,并put()到IoBuffer缓冲区
 * @see 4、数据组装完毕后,调用flip()方法,为输出做好准备
 * @see    切记在write()方法之前调用IoBuffer的flip()方法,否则缓冲区的position的后面是没有数据可以用来输出的
 * @see    你必须调用flip()方法将position移至0,limit移至刚才的position。这个flip()方法的含义请参看java.nio.ByteBuffer
 * @see 5、最后调用ProtocolEncoderOutput的write()方法输出IoBuffer缓冲区实例
 * @see ==============================================================================================================
 */
public class CmccSipcEncoder extends ProtocolEncoderAdapter {
	private final Charset charset;
	public CmccSipcEncoder(Charset charset){
		this.charset = charset;
	}

	/**
	 * 依据传入的字符集类型对message对象进行编码
	 * 编码的方式就是按照短信协议拼装字符串到IoBuffer缓冲区,然后调用ProtocolEncoderOutput的write()方法输出字节流
	 */
	@Override
	public void encode(IoSession session, 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);
		//使用String类与Byte[]类型之间的转换方法获得转为字节流后的字节数
		buffer.putString("L: "+smsContent.getBytes(charset).length+'\n', ce);
		buffer.putString(smsContent, ce);
		buffer.flip();
		out.write(buffer);
	}
}

重头戏:最后是我们自定义的解码器CmccSipcDecoder.java

package com.mina.codec;

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.AttributeKey;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;

import com.mina.model.SmsObject;

/**
 * 自定义解码器
 * Mina中编写解码器,可以实现ProtocolDecoder接口,其中有decode()、finishDecode()、dispose()三个方法
 * finishDecode()用于处理IoSession关闭时剩余的未读取数据,该方法通常不会被用到,不过也可忽略处理剩余的数据
 * 同样的,一般情况下,我们只需要继承适配器ProtocolDecoderAdapter,关注decode()方法即可
 * @see =============================================================================================================
 * @see 解码器相对编码器来说,最麻烦的是数据发送过来的规模。比如聊天室中每隔一段时间都会有聊天内容发送过来
 * @see 此时	decode()方法会被往复调用,这样处理时就会非常麻烦,幸好Mina提供了CumulativeProtocolDecoder类
 * @see 它是累积性的协议解码器。即只要有数据发送过来,该类就会去读取数据,然后累积到内部的IoBuffer缓冲区
 * @see 但具体的拆包(把累积到缓冲区的数据解码为Java对象)则交由子类的doDecode()方法完成
 * @see 实际上CumulativeProtocolDecoder就是在decode()中反复的调用暴漏给子类实现的doDecode()方法
 * @see =============================================================================================================
 * @see 具体执行过程如下所示
 * @see 1、这里的doDecode()方法返回true时
 * @see    CumulativeProtocolDecoder的decode()方法首先会判断你是否在doDecode()方法中从内部的IoBuffer缓冲区读取了数据
 * @see    如果没有,则会抛出非法的状态异常
 * @see    即doDecode()返回true就表示你已经消费了本次数据(相当于聊天室中一个完整的消息已经读取完毕)
 * @see    进一步说,也就是此时你必须已经消费过内部的IoBuffer缓冲区的数据(哪怕是消费了一个字节的数据)
 * @see    如果验证通过,那么CumulativeProtocolDecoder会检查缓冲区内是否还有数据未读取
 * @see    如果有就继续调用doDecode()方法,没有则停止对doDecode()方法的调用,直到有新的数据被缓冲
 * @see 2、当doDecode()方法返回false时,CumulativeProtocolDecoder会停止对doDecode()的调用
 * @see    但这时,若本次数据还有未读取完的,那么就将含有剩余数据的IoBuffer缓冲区保存到IoSession中
 * @see    以便下一次数据到来时可以从IoSession中提取合并
 * @see    若发现本次数据全都读取完毕,则清空IoBuffer缓冲区
 * @see =============================================================================================================
 * @see 简而言之,当你认为读取到的数据已经够解码了,那么就返回true,否则就返回false
 * @see 这个CumulativeProtocolDecoder其实最重要的工作就是帮你完成了数据的累积,因为这个工作是很烦琐的
 * @see =============================================================================================================
 * @see 假如数据的发送被拆成了多次(譬如:短信协议的短信内容、消息报头被拆成了两次数据发送),那么上面的代码势必就会存在问题
 * @see 因为当第二次调用doDecode()方法时,状态变量i、matchCount势必会被重置,也就是原来的状态值并没有被保存
 * @see 那么我们如何解决状态保存的问题呢
 * @see 答案就是将状态变量保存在IoSession中或者是Decoder实例自身,但推荐使用前者
 * @see 因为虽然Decoder是单例的,其中的实例变量保存的状态在Decoder实例销毁前始终保持
 * @see 但Mina并不保证每次调用doDecode()方法时都是同一个线程
 * @see 这也就是说第一次调用doDecode()是IoProcessor-1线程,第二次有可能就是IoProcessor-2线程
 * @see 这就会产生多线程中的实例变量的可视性(Visibility,具体请参考Java的多线程知识)问题
 * @see 而IoSession中使用一个同步的HashMap保存对象,所以我们就不需要担心多线程带来的问题
 * @see =============================================================================================================
 * @see 使用IoSession保存解码器的状态变量通常的写法如下所示
 * @see 1、在解码器中定义私有的内部类Context,然后将需要保存的状态变量定义在Context中存储
 * @see 2、在解码器中定义方法获取这个Context的实例,这个方法的实现要优先从IoSession中获取Context
 * @see =============================================================================================================
 * @see 这里做了如下的几步操作
 * @see 1、所有记录状态的变量移到了Context内部类中,包括记录读到短信协议的哪一行的line
 * @see    每一行读取了多少个字节的matchCount,还有记录解析好的状态行、发送者、接受者、短信内容、累积数据的innerBuffer等
 * @see    这样就可以在数据不能完全解码,等待下一次doDecode()方法的调用时,还能承接上一次调用的数据
 * @see 2、在doDecode()方法中主要的变化是各种状态变量首先是从Context中获取,然后操作之后,将最新的值setXXX()到Context中保存
 * @see 3、这里注意doDecode()方法最后的判断,当认为不够解码为一条短信息时,返回false,即在本次数据流解码中不要再调用doDecode()方法
 * @see   当认为已解码出一条短信息时,输出消息并重置所有状态变量,返回true,即若本次数据流解码中还有没解码完的数据,则继续调用doDecode()
 * @see =============================================================================================================
 */
//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 i = 1;          //记录解析到了短信协议中的哪一行(\n)
//		int matchCount = 0; //记录在当前行中读取到了哪一个字节
//		String statusLine="", sender="", receiver="", length="", sms="";
//		while(in.hasRemaining()){
//			byte b = in.get();
//			buffer.put(b);
//			if(10==b && 5>i){ //10==b表示换行:该短信协议解码器使用\n(ASCII的10字符)作为分解点
//				matchCount++;
//				if(1 == i){
//					buffer.flip(); //limit=position,position=0
//					statusLine = buffer.getString(matchCount, cd);
//					statusLine = statusLine.substring(0, statusLine.length()-1); //移除本行的最后一个换行符
//					matchCount = 0; //本行读取完毕,所以让matchCount=0
//					buffer.clear();
//				}
//				if(2 == i){
//					buffer.flip();
//					sender = buffer.getString(matchCount, cd);
//					sender = sender.substring(0, sender.length()-1);
//					matchCount = 0;
//					buffer.clear();
//				}
//				if(3 == i){
//					buffer.flip();
//					receiver = buffer.getString(matchCount, cd);
//					receiver = receiver.substring(0, receiver.length()-1);
//					matchCount = 0;
//					buffer.clear();
//				}
//				if(4 == i){
//					buffer.flip();
//					length = buffer.getString(matchCount, cd);
//					length = length.substring(0, length.length()-1);
//					matchCount = 0;
//					buffer.clear();
//				}
//				i++;
//			}else if(5 == i){
//				matchCount++;
//				if(Long.parseLong(length.split(": ")[1]) == matchCount){
//					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; //告诉Mina:本次数据已全部读取完毕,故返回false
//	}
//}
/**
 * 以上注释的解码器,适用于客户端发送的数据是一次全部发送完整的情况
 * 下面的这个解码器,适用于客户端发送的数据被拆分为多次后发送的情况
 */
public class CmccSipcDecoder extends CumulativeProtocolDecoder {
	private final Charset charset;
	private final AttributeKey CONTEXT = new AttributeKey(getClass(), "context");
	public CmccSipcDecoder(Charset charset){
		this.charset = charset;
	}
	private Context getContext(IoSession session){
		Context context = (Context)session.getAttribute(CONTEXT);
		if(null == context){
			context = new Context();
			session.setAttribute(CONTEXT, context);
		}
		return context;
	}
	
	@Override
	protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
		CharsetDecoder cd = charset.newDecoder();
		Context ctx = this.getContext(session);
		IoBuffer buffer = ctx.innerBuffer;
		int matchCount = ctx.getMatchCount();
		int line = ctx.getLine();
		String statusLine = ctx.getStatusLine();
		String sender = ctx.getSender();
		String receiver = ctx.getReceiver();
		String length = ctx.getLength();
		String sms = ctx.getSms();
		while(in.hasRemaining()){
			byte b = in.get();
			matchCount++;
			buffer.put(b);
			if(10==b && line<4){
				if(0 == line){
					buffer.flip();
					statusLine = buffer.getString(matchCount, cd);
					statusLine = statusLine.substring(0, statusLine.length()-1);
					matchCount = 0;
					buffer.clear();
					ctx.setStatusLine(statusLine);
				}
				if(1 == line){
					buffer.flip();
					sender = buffer.getString(matchCount, cd);
					sender = sender.substring(0, sender.length()-1);
					matchCount = 0;
					buffer.clear();
					ctx.setSender(sender);
				}
				if(2 == line){
					buffer.flip();
					receiver = buffer.getString(matchCount, cd);
					receiver = receiver.substring(0, receiver.length()-1);
					matchCount = 0;
					buffer.clear();
					ctx.setReceiver(receiver);
				}
				if(3 == line){
					buffer.flip();
					length = buffer.getString(matchCount, cd);
					length = length.substring(0, length.length()-1);
					matchCount = 0;
					buffer.clear();
					ctx.setLength(length);
				}
				line++;
			}else if(4 == line){
				if(Long.parseLong(length.split(": ")[1]) == matchCount){
					buffer.flip();
					sms = buffer.getString(matchCount, cd);
					ctx.setSms(sms);
					ctx.setMatchCount(matchCount); //由于下面的break,这里需要调用else外面的两行代码
					ctx.setLine(line);
					break;
				}
			}
			ctx.setMatchCount(matchCount);
			ctx.setLine(line);
		}
		//判断本次是否已读取完毕:要求读取到最后一行,且读取的字节数与前一行指定的字节数相同
		if(4==ctx.getLine() && Long.parseLong(ctx.getLength().split(": ")[1])==ctx.getMatchCount()){
			SmsObject smsObject = new SmsObject();
			smsObject.setSender(sender.split(": ")[1]);
			smsObject.setReceiver(receiver.split(": ")[1]);
			smsObject.setMessage(sms);
			out.write(smsObject);
			ctx.reset();
			return true;
		}else{
			return false;
		}
	}
	
	
	private class Context{
		private final IoBuffer innerBuffer; //用于累积数据的IoBuffer
		private String statusLine = "";     //记录解析好的状态行
		private String sender = "";         //记录解析好的发送者
		private String receiver = "";       //记录解析好的接受者
		private String length = "";
		private String sms = "";            //记录解析好的短信内容
		private int matchCount = 0;         //记录每一行读取了多少个字节
		private int line = 0;               //记录读到短信协议的哪一行
		public Context(){
			innerBuffer = IoBuffer.allocate(100).setAutoExpand(true);
		}
		public String getStatusLine() {
			return statusLine;
		}
		public void setStatusLine(String statusLine) {
			this.statusLine = statusLine;
		}
		public String getSender() {
			return sender;
		}
		public void setSender(String sender) {
			this.sender = sender;
		}
		public String getReceiver() {
			return receiver;
		}
		public void setReceiver(String receiver) {
			this.receiver = receiver;
		}
		public String getLength() {
			return length;
		}
		public void setLength(String length) {
			this.length = length;
		}
		public String getSms() {
			return sms;
		}
		public void setSms(String sms) {
			this.sms = sms;
		}
		public int getMatchCount() {
			return matchCount;
		}
		public void setMatchCount(int matchCount) {
			this.matchCount = matchCount;
		}
		public int getLine() {
			return line;
		}
		public void setLine(int line) {
			this.line = line;
		}
		public void reset(){
			this.innerBuffer.clear();
			this.statusLine = "";
			this.sender = "";
			this.receiver = "";
			this.length = "";
			this.sms = "";
			this.matchCount = 0;
			this.line = 0;
		}
	}
}
一句话总结:最为核心部分就是编码器和解码器

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值