目录
1. Mina概述
-
Apache的Mina(Multipurpose Infrastructure Networked Applications)是一个基于java nio的网络通信框架。主要屏蔽了网络通信的一些细节,对Socket进行封装,并且是NIO的一个实现架构,可以帮助我们快速的开发网络通信,常用于游戏的开发、中间件服务端的程序中。
-
Mina最主要的工作就是把底层传输的字节码转换为Java对象,提供给应用程序;或者把应用程序返回的结果转换为字节码,交给底层传输。
-
Mina 的API 将真正的网络通信与我们的应用程序隔离开来,我们只需要关心我们要发送、接收的数据以及业务逻辑即可。
2. Mina组件
-
IOService:最底层的是IOService,负责具体的IO相关工作。用于描述我们的客户端和服务端接口,(IoAccept、IoConnector是其子类),分别用于服务端和客户端使用;这一层的典型代表有IOSocketAcceptor和 IOSocketChannel。每当有数据到达时,IOService会先调用底层IO接口读取数据,封装成IoBuffer,之后以事件的形式通知上层代码,从而将Java NIO的同步IO接口转化成了异步IO。
-
IOprocessor:是多线程的环境来处理我们的连接请求;IoProcessor 负责调用注册在 IoService 上的过滤器,并在过滤器链之后调用IoHandler。
-
IOFilter: 这个接口定义一组拦截器,用于数据的过滤工作(包含编解码、日志等信息的过滤),其中数据的encode与decode是最为重要的、也是你在使用Mina时最主要关注的地方。
-
IOHandler:这个接口负责编写业务逻辑,也就是接收、发送数据的地方。需要有开发者自己来实现这个接口。
-
IOsession: 是对底层连接(服务器与客户端的特定连接,该连接由服务器地址、端口以及客户端地址、端口来决定)的封装。
3. Mina工作流程
3.1 服务端流程
-
通过SocketAcceptor 同客户端建立连接;
-
连接建立之后 I/O的读写交给了I/O Processor线程,I/O Processor是多线程的;
-
通过I/O Processor 读取的数据经过IoFilterChain里所有配置的IoFilter,IoFilter进行消息的过滤,格式的转换,在这个层面可以制定一些自定义的协议;
-
最后IoFilter将数据交给 Handler 进行业务处理,完成了整个读取的过程;
写入过程也是类似,只是刚好倒过来, 通过IoSession.write 写出数据,然后Handler进行写入的业务处理,处理完成后交给IoFilterChain,进行消息过滤 和协议的转换,最后通过 I/O Processor 将数据写出到 socket 通道。
3.2 客户端流程
-
客户端首先创 建一个IOConnector 用来和服务端通信,顾名思义这就是建立的一个连接对象;
-
在这个连接上创建一个session, 客 户端中的业务方法可以向session中写入数据,数据经过Filter Chain的过滤后会发送给服务端;
-
从服务端发回的数据也会首先经过Filter Chain的过滤,然后交给IOHandler做进一步的处理
4. Mina长链接与短连接
-
长连接:通信双方长期的保持一个连接状态不断开,一旦建立连接后,就不断开,除非发生异常,比较消耗IO资源。
-
短连接:通信双方不是保持一个长期的连接状态,比如Http协议,当客户端发起http请求,服务器处理http请求,当服务器处理完成后,返回客户端数据后就断开链接。
默认是长连接的,处于监听会话状态,可以改成短连接。如下图 服务端设置
5. Mina示例
5.1 SpringBoot基础工程搭建
5.2 引入Mina依赖
<!--MINA-->
<dependency>
<groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId>
<version>2.0.0-RC1</version>
</dependency>
5.3 IoHandler类,顾名思义即接收到消息的处理类
package com.miaxis.controller;
import lombok.extern.slf4j.Slf4j;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import java.util.Date;
/**
* IoHandler继承类,socket服务器端处理类
* @author chen.gs
* @date - 2024-01-14
*/
@Slf4j
public class ServerHandler extends IoHandlerAdapter {
/**
* 当一个新客户端连接后触发此方法.
*/
@Override
public void sessionCreated(IoSession session) throws Exception{
super.sessionCreated(session);
// To do
//log.info("连接创建: " + session.getRemoteAddress().toString());
}
/**
* 当连接后打开时触发此方法,一般此方法与 sessionCreated 会被同时触发
*/
@Override
public void sessionOpened(IoSession session) throws Exception{
super.sessionOpened(session);
// To do
log.info("连接打开: " + session.getRemoteAddress().toString());
}
/**
* 当连接被关闭时触发
*/
@Override
public void sessionClosed(IoSession session) throws Exception {
super.sessionClosed(session);
// To do
log.info("连接关闭 : " + session.getRemoteAddress().toString());
int size = session.getService().getManagedSessions().values().size();
log.info("连接关闭时session数量==》" + size);
}
/**
* 当连接空闲时触发此方法.
*/
@Override
public void sessionIdle(IoSession session, IdleStatus status) throws Exception{
super.sessionIdle(session,status);
// To do
if (status == IdleStatus.READER_IDLE) {
log.info("进入读空闲状态");
session.close(true);
} else if (status == IdleStatus.BOTH_IDLE) {
log.info("BOTH空闲");
session.close(true);
}
}
/**
* 抛出异常未被捕获触发此方法
*/
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception{
super.exceptionCaught(session,cause);
// To do
log.error("出现异常 :" + session.getRemoteAddress().toString() + " : " + cause.toString());
//cause.printStackTrace();
session.write("exceptionCaught");
session.close(true);
}
/**
* 接收到客户端的请求信息触发此方法.
*/
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
super.messageReceived(session, message);// 消息的接受
// To do
//String server_ip = session.getLocalAddress().toString();
String client_ip = session.getRemoteAddress().toString();
String text = String.valueOf(message);
log.info("接受到客户端["+client_ip+"]数据 :" + text);
log.info("数据业务处理开始...... ");
String result = analyzeData(session,text);
log.info("数据业务处理结束...... ");
session.write(result);
}
private String analyzeData(IoSession session,String text) {
String address = session.getLocalAddress().toString();
// 返回数据
Date date = new Date();
String responseMessage = date.toString()+"||"+text;
return responseMessage;
}
/**
* 当信息已经传送给客户端后触发此方法.
*/
@Override
public void messageSent(IoSession session, Object message)throws Exception{
super.messageSent(session,message);
// To do
log.info("返回客户端[" + session.getRemoteAddress().toString() +"]信息:"+message);
}
}
5.4 Mina配置类
package com.miaxis.config;
import com.miaxis.controller.ServerHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.InetSocketAddress;
@Configuration
@Slf4j
public class MinaConfig {
private static final int PORT = 9123;
/**
* 拦截器, Mina事件类ServerHandler
*/
@Bean
public IoHandler ioHandlerG() {
return new ServerHandler();
}
@Bean
public IoAcceptor ioAcceptorG() throws Exception {
IoAcceptor acceptor = new NioSocketAcceptor();
// 增加编码过滤器,统一编码UTF-8,TextLineCodecFactory基于文本的,根据回车换行来断点传输数据
// 在mina中,一般的应用场景用TextLine的Decode和Encode就够用了
// TextLine的默认分割符虽然是\n,但其实分隔符是可以自己指定的,如:newTextLineDecoder(charset, decodingDelimiter);
acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory()));
// 使用自定义编码解码工厂类
//acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new SocketFactory(Charset.forName("utf-8"))));
// 设置服务端逻辑处理器
acceptor.setHandler(ioHandlerG());
// 设置读缓存大小
acceptor.getSessionConfig().setReadBufferSize(2048);
// 设置指定类型的空闲时间(单位:秒),空闲时间超过这个值将触发sessionIdle方法
acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
// 绑定端口
acceptor.bind(new InetSocketAddress(PORT));
//System.out.println("Mina服务已启动,端口:" + PORT);
log.info("Mina服务已启动,端口:" + PORT);
return acceptor;
}
}
5.5 测试
package com.miaxis.mina;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
@SpringBootTest
public class MinaClientTest {
@Test
public void mina() {
// 创建客户端连接器.
NioSocketConnector connector = new NioSocketConnector();
connector.getFilterChain().addLast("logger", new LoggingFilter());
// 设置编码过滤器
connector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("utf-8"))));
// connector.getFilterChain().addLast("codec",
// new ProtocolCodecFilter(new SocketEncoder(),new SocketDecoder()));
// 设置事件处理器
connector.setHandler(new ClientHandler());
// 建立连接
ConnectFuture cf = connector.connect(new InetSocketAddress("127.0.0.1", 9123));
// 等待连接创建完成
cf.awaitUninterruptibly();
// 发送消息,中英文符号都有
cf.getSession().write("hello,d的十多年弗兰克萨洛克{id:丹参粉!!!}");
try {
Thread.sleep(1000); //1000 毫秒,也就是1秒.
} catch(InterruptedException ex) {
Thread.currentThread().interrupt();
}
cf.getSession().close(true);
}
}