来吧,一步一步搭建mina服务端,原理往后再说!
参考博客:矢落叶の博客
首先利用springboot的插件新建一个maven项目
首先加入mina核心依赖
<dependency>
<groupId>org.apache.minagroupId>
<artifactId>mina-coreartifactId>
<version>2.1.3version>
dependency>
为了进行socket通信,还需要加入另一个依赖:
<dependency>
<groupId>org.apache.minagroupId>
<artifactId>mina-integration-beansartifactId>
<version>2.1.3version>
dependency>
此时pom文件会报错Missing artifact org.apache.mina:mina-core:bundle:2.1.3
需要添加如下插件:
<plugin>
<groupId>org.apache.felixgroupId>
<artifactId>maven-bundle-pluginartifactId>
<extensions>trueextensions>
plugin>
mina需要哪些配置类呢?一个一个分析。
设置I/O接收器,指定接收到请求后交给handler处理
此部分被 NioSocketAcceptor 隐式使用,无此则会报字符串无法转换成 InetSocketAddress
从字符串到 SocketAddress 的转换,会偿试使用该自定义属性编辑器
具体解释参照
Mina 配置中的 CustomEditorConfigurer
上代码
/**
* 设置I/O接收器
* @return
*/
private static Map<Class<?>, Class<? extends PropertyEditor>> customEditors = new HashMap<>();
@Bean
public static CustomEditorConfigurer customEditorConfigurer() {
customEditors.put(SocketAddress.class, InetSocketAddressEditor.class);
CustomEditorConfigurer configurer = new CustomEditorConfigurer();
configurer.setCustomEditors(customEditors);
return configurer;
}
如果不加mina-integration-beans
依赖,会找不到InetSocketAddressEditor
这个类
在处理流程中加入线程池,可以较好的提高服务器的吞吐量,但也带来了新的问题:请求的处理顺序问题。在单线程的模型下,可以保证IO请求是挨个顺序地处理的。加入线程池之后,同一个IoSession的多个IO请求可能被ExecutorFilter并行的处理,这对于一些对请求处理顺序有要求的程序来说是不希望看到的。比如:数据库服务器处理同一个会话里的prepare,execute,commit请求希望是能按顺序逐一执行的。
Mina里默认的实现是有保证同一个IoSession中IO请求的顺序的。具体的实现是,ExecutorFilter默认采用了Mina提供的OrderedThreadPoolExecutor作为内置线程池。后者并不会立即执行加入进来的Runnable对象,而是会先从Runnable对象里获取关联的IoSession(这里有个down cast成IoEvent的操作),并将Runnable对象加入到session的任务列表中。OrderedThreadPoolExecutor会按session里任务列表的顺序来处理请求,从而保证了请求的执行顺序。
对于没有顺序要请求的情况,可以为ExecutorFilter指定一个Executor来替换掉默认的OrderedThreadPoolExecutor,让同一个session的多个请求能被并行地处理,来进一步提高吞吐量。
参考文章:Mina工作原理分析
/**
* 线程池filter
*/
@Bean
public ExecutorFilter executorFilter() {
return new ExecutorFilter();
}
/**
* 日志信息注入过滤器,MDC(Mapped Diagnostic Context有译作线程映射表)是日志框架维护的一组信息键值对,可向日志输出信息中插入一些想要显示的内容。
*
*/
@Bean
public MdcInjectionFilter mdcInjectionFilter() {
return new MdcInjectionFilter(MdcInjectionFilter.MdcKey.remoteAddress);
}
/**
* 日志filter
*/
@Bean
public LoggingFilter loggingFilter() {
return new LoggingFilter();
}
/**
* 编解码器filter
*/
@Bean
public ProtocolCodecFilter protocolCodecFilter() {
return new ProtocolCodecFilter(new MyProtocolCodecFactory());
}
此时迎来了第一个重点,编解码器。
稍后单独列出这个自定义编解码工厂的实现。
@Bean
public KeepAliveFactoryImpl keepAliveFactoryImpl() {
return new KeepAliveFactoryImpl();
}
/**
* 心跳filter
*/
@Bean
public KeepAliveFilter keepAliveFilter(KeepAliveFactoryImpl keepAliveFactory) {
// 注入心跳工厂,读写空闲
KeepAliveFilter filter = new KeepAliveFilter(keepAliveFactory, IdleStatus.BOTH_IDLE);
// 设置是否forward到下一个filter
filter.setForwardEvent(true);
// 设置心跳频率,单位是秒,我这里设置的180,也就是3分钟
filter.setRequestInterval(Const.IDELTIMEOUT);
return filter;
}
心跳包是第二大重点,稍后单独列出心跳包的实现。
本例列举两个业务处理类:BindHandler
、TimeCheckHandler
心跳包业务是单独实现的
BindHandler
负责处理登录业务
TimeCheckHandler
负责获取服务器的系统时间
private HashMap<Integer, BaseHandler> handlers = new HashMap<>();
@Bean
public ServerHandler serverHandler() {
handlers.put(Const.AUTHEN, new BindHandler());
handlers.put(Const.TIME_CHECK, new TimeCheckHandler());
ServerHandler serverHandler = new ServerHandler();
serverHandler.setHandlers(handlers);
return serverHandler;
}
BaseHandler
接口稍后展示。
handlers
集合是为了方便处理业务时,根据不同模块获取相应的处理类,稍后看具体的实现
主要用于拦截和过滤网络传输中I/O操作的各种消息,是在应用层和我们业务员层之间的过滤层
主要作用:
记录事件的日志(Mina默认提供了LoggingFilter)
测量系统性能
信息验证
过载控制
信息的转换(主要就是编码和解码)
和其他更多的信息
最后将心跳包检测加入到过滤器链中
@Bean
public DefaultIoFilterChainBuilder defaultIoFilterChainBuilder(ExecutorFilter executorFilter,
MdcInjectionFilter mdcInjectionFilter, ProtocolCodecFilter protocolCodecFilter, LoggingFilter loggingFilter,
KeepAliveFilter keepAliveFilter) {
DefaultIoFilterChainBuilder filterChainBuilder = new DefaultIoFilterChainBuilder();
Map<String, IoFilter> filters = new LinkedHashMap<>();
filters.put("mdcInjectionFilter", mdcInjectionFilter);
filters.put("protocolCodecFilter", protocolCodecFilter);
filters.put("executor", executorFilter);
filters.put("keepAliveFilter", keepAliveFilter);
filterChainBuilder.setFilters(filters);
return filterChainBuilder;
}
注意:
当你使用自定的ProtocolCodecFactory时候一定要将线程池配置在该过滤器之后,因为你自己实现的ProtocolCodecFactory直接读取和转换的是二进制数据,这些数据都是由和CPU绑定的I/O Processor来读取和发送的,因此为了不影响系统的性能,也应该将数据的编解码操作绑定到I/O Processor线程中,因为在Java中创建和线程切换都是比较耗资源的,因此建议将ProtocolCodecFactory配置在ExecutorFilter的前面
/**
* 创建连接
* @return
*/
@Bean(initMethod = "init", destroyMethod = "dispose")
public NioSocketAcceptor nioSocketAcceptor(ServerHandler serverHandler,
DefaultIoFilterChainBuilder defaultIoFilterChainBuilder) {
NioSocketAcceptor acceptor = new NioSocketAcceptor();
acceptor.getSessionConfig().setReadBufferSize(2048);
acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, Const.IDELTIMEOUT);
// 绑定过滤器链
acceptor.setFilterChainBuilder(defaultIoFilterChainBuilder);
acceptor.setHandler(serverHandler);
return acceptor;
}
底层的传输与交互都是采用二进制的方式。
如何判断发送的消息已经结束,就需要通过协议来规定,比如收到换行符等标识时,判断为结束等。
根据协议,把二进制数据转换成Java对象称为解码(也叫做拆包),把Java对象转换为二进制数据称为编码(也叫做打包)。
常用的协议制定方法:
实际应用中,采用最多的还是定长报文头法。
本例采用的是定长报文头法。
首先来实现一个自定义协议:
协议组成: 数据长度(4个字节) + 协议编号(1字节)+ 模块代码(4字节)+ 序列号(4字节)+ 真实数据。
实体类如下:
public class MyPack {
// 数据总长度
private int len;
// 数据传输类型:0x00-设备到服务端 0x01-服务端到设备
private byte type = 0x01;
// 模块代码
private int module;
/**
* 此域表示一个序列号,使用在异步通信模式下,由消息发起者设定,应答者对应给回此序列号。
* 序列号范围:0000-9999,循环使用。
* 同步方式下该域保留。
**/
private String seq;
// 包体
private String body;
/**
* 0x00表示客户端到服务端
*/
public static final byte REQUEST = 0x00;
/**
* 0x01表示服务端到客户端
*/
public static final byte RESPONSE = 0x01;
// 包头长度
public static final int PACK_HEAD_LEN = 13;
// 最大长度
public static final int MAX_LEN = 9999;
public MyPack(int module, String seq, String body) {
this.module = module;
this.seq = seq;
this.body = body;
// 总长度
this.len = PACK_HEAD_LEN + (StringUtils.isBlank(body) ? 0 : body.getBytes().length);
}
// getter/setter...
}
在添加自定义编码器ProtocolCodecFilter时,需要注入一个ProtocolCodecFactory编解码工厂,查看ProtocolCodecFactory接口,发现需要实现2个方法,该接口的两个方法需要返回ProtocolDecoder和ProtocolEncoder的实现类对象:
public interface ProtocolCodecFactory {
ProtocolEncoder getEncoder(IoSession session) throws Exception;
ProtocolDecoder getDecoder(IoSession session) throws Exception;
}
这就需要我们对编码器、解码器进行实现。
自定义编解码器工厂类的实现如下:
public class MyProtocolCodecFactory implements ProtocolCodecFactory {
private final ProtocolEncoder encoder;
private final ProtocolDecoder decoder;
public MyProtocolCodecFactory() {
this(Charset.forName("UTF-8"));
}
public MyProtocolCodecFactory(Charset charset) {
this.encoder = new MyProtocolEncoder(charset);
this.decoder = new MyProtocolDecoder(charset);
}
@Override
public ProtocolEncoder getEncoder(IoSession session) throws Exception {
return encoder;
}
@Override
public ProtocolDecoder getDecoder(IoSession session) throws Exception {
return decoder;
}
}
编码器的作用是将JAVA对象转换成二进制流,然后发送给客户端。
可以通过继承ProtocolEncoderAdapter类或实现ProtocolEncoder接口来实现自定义编码。
本例采用的方式是实现ProtocolEncoder
接口。
public class MyProtocolEncoder implements ProtocolEncoder {
private final Charset charset;
public MyProtocolEncoder() {
this.charset = Charset.defaultCharset();
}
public MyProtocolEncoder(Charset charset) {
this.charset = charset;
}
@Override
public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception {
MyPack myPack = (MyPack) message;
IoBuffer ioBuffer = IoBuffer.allocate(myPack.getLen()).setAutoExpand(true);
System.out.println("encode length: " + myPack.getLen());
ioBuffer.putInt(myPack.getLen());
ioBuffer.put(myPack.getType());
ioBuffer.putInt(myPack.getModule());
ioBuffer.putString(myPack.getSeq(), charset.newEncoder());
if (StringUtils.isNotBlank(myPack.getBody())) {
System.out.println("encoder body length:" + myPack.getBody().getBytes().length);
ioBuffer.putString(myPack.getBody(), charset.newEncoder());
}
ioBuffer.flip();
out.write(ioBuffer);
}
@Override
public void dispose(IoSession session) throws Exception {
// TODO Auto-generated method stub
}
}
注意,在write前,结束iobuffer操作后,必须将ioBuffer进行flip操作,具体原因查看IoBuffer的使用方法
解码器的作用是将二进制流转换成JAVA对象,通过服务端的解析将数据转化成需要的协议包。
可以通过实现ProtocolDecoder接口或继承ProtocolDecoderAdapter类来实现自定义解码,但是难以解决半包、粘包问题。
一般都是采用继承CumulativeProtocolDecoder类,重写doDecode方法,可以解决半包、粘包问题
public class MyProtocolDecoder extends CumulativeProtocolDecoder {
static final Logger logger = LoggerFactory.getLogger(MyProtocolDecoder.class);
private final Charset charset;
public MyProtocolDecoder() {
this.charset = Charset.defaultCharset();
}
public MyProtocolDecoder(Charset charset) {
this.charset = charset;
}
@Override
protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
if (in.remaining() < MyPack.PACK_HEAD_LEN) {
return false;
}
if (in.remaining() > MyPack.MAX_LEN) {
// 数据长度大于最大长度,出现数据错误,丢弃
clearData(in);
return false;
}
else {
in.mark();
try {
// 数据长度4字节
int length = in.getInt();
logger.info("decode length:" + length);
if (length < MyPack.PACK_HEAD_LEN) {
// 数据长度小于包头长度,出现数据错误,丢弃
clearData(in);
return false;
}
int i = in.remaining();
logger.info("remaing:" + i);
if (i < (length - 4)) {
// 内容不够, 重置position到操作前,进行下一轮接受新数据
in.reset();
return false;
} else {
in.reset();
// 数据传输类型
byte type = in.get();
if (type != MyPack.REQUEST) {
// 不是客户端发送的数据,出现数据错误,丢弃
clearData(in);
return false;
}
// 模块id
int module = in.getInt();
// 序列号
String seq_no = in.getString(4, charset.newDecoder());
String body = in.getString(length - MyPack.PACK_HEAD_LEN, charset.newDecoder());
MyPack myPack = new MyPack(module, seq_no, body);
logger.info(">>>>>> server decode result: " + myPack.toString());
out.write(myPack);
return in.remaining() > 0;
}
} catch (Exception e) {
// 解析有异常,抛弃异常数据,不能影响正常通信
logger.error(">>>>>> decode error: " + e.toString());
clearData(in);
return false;
}
}
}
private void clearData(IoBuffer in) {
byte[] bytes = new byte[in.remaining()];
in.get(bytes);
bytes = null;
}
}
doDecode方法说明:
简而言之,当你认为读取到的数据已经够解码了,那么就返回true,否则就返回false。这个CumulativeProtocolDecoder 其实最重要的工作就是帮你完成了数据的累积,因为这个工作是很烦琐的。也就是说返回true,那么CumulativeProtocolDecoder会再次调用decoder,并把剩余的数据发下来返回false就不处理剩余的,当有新数据包来的时候把剩余的和新的拼接在一起然后再调用decoder。
clearData(IoBuffer in)
方法是为了清空IoBuffer中的数据,防止再次读取