接上一节:springboot+mina框架服务端的实现(一) ------ pom依赖、mina配置类、自定义协议以及编解码器的实现
参考博客:矢落叶の博客
先简单介绍下keepAlive的机制:
首先,需要搞清楚TCP keepalive是干什么用的。从名字理解就能够知道,keepalive就是用来检测一个tcp connection是否还连接正常。当一个tcpconnection建立好之后,如果双方都不发送数据的话,tcp协议本身是不会发送其它的任何数据的,也就是说,在一个idle的connection上,两个socket之间不产生任何的数据交换。从另一个方面讲,当一个connection建立之后,链接双方可以长时间的不发送任何数据,比如几天,几星期甚至几个月,但该connection仍然存在。
所以,这就可能出现一个问题。举例来说,server和client建立了一个connection,server负责接收client的request。当connection建立好之后,client由于某种原因机器停机了。但server端并不知道,所以server就会一直监听着这个connection,但其实这个connection已经失效了。
keepalive就是为这样的场景准备的。当把一个socket设置成了keepalive,那么这个socket空闲一段时间后,它就会向对方发送数据来确认对方仍然存在。放在上面的例子中,如果client停机了,那么server所发送的keepalive数据就不会有response,这样server就能够确认client完蛋了(至少从表面上看是这样)。
MINA本身提供了一个过滤器类: org.apache.mina.filter.keepalive.KeepAliveFilter ,该过滤器用于在IO空闲的时候发送并且反馈心跳包(keep-alive request/response)。
该类构造函数中参数有三个分别是:
(1)KeepAvlieMessageFactory: 该实例引用用于判断接受与发送的包是否是心跳包,以及心跳请求包的实现
(2)IdleStatus: 该过滤器所关注的空闲状态,默认认为读取空闲。 即当读取通道空闲的时候发送心跳包
(3)KeepAliveRequestTimeoutHandler: 心跳包请求后超时无反馈情
先看KeepAliveMessageFactory
接口:
public interface KeepAliveMessageFactory {
boolean isRequest(IoSession session, Object message);
boolean isResponse(IoSession session, Object message);
Object getRequest(IoSession session);
Object getResponse(IoSession session, Object request);
}
实现这个接口:
public class KeepAliveFactoryImpl implements KeepAliveMessageFactory {
static final Logger logger = LoggerFactory.getLogger(KeepAliveFactoryImpl.class);
@Resource
private BaseHandler keepAliveHandler;
// 用来判断接收到的消息是不是一个心跳请求包,是就返回true[接收端使用]
@Override
public boolean isRequest(IoSession session, Object message) {
if (message instanceof MyPack) {
MyPack pack = (MyPack) message;
if (Const.HEART_BEAT == pack.getModule()) {
return true;
}
}
return false;
}
// 用来判断接收到的消息是不是一个心跳回复包,是就返回true[发送端使用]
@Override
public boolean isResponse(IoSession session, Object message) {
// TODO Auto-generated method stub
return false;
}
// 在需要发送心跳时,用来获取一个心跳请求包[发送端使用]
@Override
public Object getRequest(IoSession session) {
// TODO Auto-generated method stub
return null;
}
// 在需要回复心跳时,用来获取一个心跳回复包[接收端使用]
@Override
public Object getResponse(IoSession session, Object request) {
MyPack attendPack = (MyPack) request;
if (null == session.getAttribute(Const.SESSION_KEY)) {
// 需要先进行登录
return new MyPack(Const.AUTHEN, attendPack.getSeq(), "fail");
}
// 将超时次数置为0
session.setAttribute(Const.TIME_OUT_KEY, 0);
return new MyPack(Const.HEART_BEAT, attendPack.getSeq(), "success");
}
}
超时后的处理放在业务处理类的sessionIdle
方法中实现,稍后详细介绍
方便对session会话进行管理,方便对session会话集合获取和删除
服务端接收到新的Session后,构造一个封装类,实现session 的部分方法,并额外实现方法
就一个处理业务的方法,使用自定义Session代替IoSession
public class MySession implements Serializable {
private static final long serialVersionUID = 1L;
// 不参与序列化
private transient IoSession session;
// session在本机器的ID
private Long nid;
// session绑定的服务ip
private String host;
// 访问端口
private int port;
// session绑定的设备
private String account;
public MySession() {
}
public MySession(IoSession session) {
this.session = session;
this.host = ((InetSocketAddress) session.getRemoteAddress()).getAddress().getHostAddress();
this.port = ((InetSocketAddress) session.getRemoteAddress()).getPort();
this.nid = session.getId();
}
/**
* 将key-value自定义属性,存储到IO会话中
*/
public void setAttribute(String key, Object value) {
if (null != session) {
session.setAttribute(key, value);
}
}
/**
* 从IO的会话中,获取key的value
*/
public Object getAttribute(String key) {
if (null != session) {
return session.getAttribute(key);
}
return null;
}
/**
* 在IO的会话中,判断是否存在包含key-value
*/
public boolean containsAttribute(String key) {
if (null != session) {
return session.containsAttribute(key);
}
return false;
}
/**
* 从IO的会话中,删除key
*/
public void removeAttribute(String key) {
if (null != session) {
session.removeAttribute(key);
}
}
/**
* 获取IP地址
*/
public SocketAddress getRemoteAddress() {
if (null != session) {
return session.getRemoteAddress();
}
return null;
}
/**
* 将消息对象 message发送到当前连接的对等体(异步)
* 当消息被真正发送到对等体的时候,IoHandler.messageSent(IoSession,Object)会被调用。
* @param msg 发送的消息
*/
public void write(MyPack msg) {
if (null != session) {
session.write(msg).isWritten();
}
}
/**
* 会话是否已经连接
*/
public boolean isConnected() {
if (null != session) {
return session.isConnected();
}
return false;
}
/**
* 关闭当前连接。如果参数 immediately为 true的话
* 连接会等到队列中所有的数据发送请求都完成之后才关闭;否则的话就立即关闭。
*/
public void close(boolean immediately) {
if (null != session) {
if (immediately) {
session.closeNow();
} else {
session.closeOnFlush();
}
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (null == obj) {
return false;
}
if (this.getClass() != obj.getClass()) {
return false;
}
// 强转为当前类
MySession session = (MySession) obj;
if (session.nid != null && nid != null) {
return session.nid.longValue() == nid.longValue() && session.host.equals(host) && session.port == port;
}
return false;
}
public String toString() {
return "session host:" + this.host + " port:" + this.port + " nid:" + this.nid;
}
// getter/setter...
}
public interface SessionManager {
/**
* 添加session
*/
void addSession(String device, MySession session);
/**
* 获取session
*/
MySession getSession(String device);
/**
* 替换Session
*/
void replaceSession(String device, MySession session);
/**
* 删除session
*/
void removeSession(String device);
/**
* 删除session
*/
void removeSession(MySession session);
}
实现类:
@Configuration
public class DefaultSessionManagerImpl extends Observable implements SessionManager {
/**
* 存放session的线程安全的map集合
*/
private static ConcurrentHashMap<String, MySession> sessions = new ConcurrentHashMap<>();
/**
* 线程安全的自增类,用于统计连接数
*/
private static final AtomicInteger connectionsCounter = new AtomicInteger(0);
/**
* 添加session
*/
@Override
public void addSession(String account, MySession session) {
if (null != session) {
sessions.put(account, session);
connectionsCounter.incrementAndGet();
// 被观察者方法,拉模型
setChanged();
notifyObservers();
}
}
/**
* 获取session
*/
@Override
public MySession getSession(String account) {
return sessions.get(account);
}
/**
* 替换session,通过账号
*/
@Override
public void replaceSession(String account, MySession session) {
sessions.put(account, session);
// 被观察者方法,拉模型
setChanged();
notifyObservers();
}
/**
* 移除session通过账号
*/
@Override
public void removeSession(String account) {
sessions.remove(account);
connectionsCounter.decrementAndGet();
// 被观察者方法,拉模型
setChanged();
notifyObservers();
}
/**
* 移除session通过session
*/
@Override
public void removeSession(MySession session) {
String account = (String) session.getAttribute(Const.SESSION_KEY);
removeSession(account);
}
public static ConcurrentHashMap<String, MySession> getSessions() {
return sessions;
}
}
业务处理类都需要实现该接口,该接口定义了一个处理业务逻辑的方法:
/**
* Mina的请求处理接口,必须实现此接口
*
*/
public interface BaseHandler {
String process(MySession mySession, String content);
}
NioSocketAcceptor
设置handler后,会调用messageReceived
方法处理业务逻辑。
当读写空闲时间超过定义的时间后,会调用该handler的sessionIdle
方法。
public class ServerHandler extends IoHandlerAdapter {
static final Logger logger = LoggerFactory.getLogger(ServerHandler.class);
private HashMap<Integer, BaseHandler> handlers = new HashMap<>();
@Autowired
SessionManager sessionManager;
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
MySession MySession = new MySession(session);
MyPack MyPack = (MyPack) message;
logger.info(MySession.toString() + ">>>>>> server received:" + message);
MyPack response;
// 如果是心跳包接口,则说明处理失败
if (Const.HEART_BEAT == MyPack.getModule()) {
logger.info(MySession.toString() + ">>>>>> server handler heartbeat error!");
response = new MyPack(Const.AUTHEN, MyPack.getSeq(), "authen fail");
MySession.write(response);
MySession.close(false);
return;
}
// 终端在未认证时连接进来,SERVER端要发送认证失败的包给终端,然后再断开连接,防止未知设备连到服务器
if (null == MySession.getAttribute(Const.SESSION_KEY) && Const.AUTHEN != MyPack.getModule()) {
logger.info(MySession.toString() + ">>>>>> need device authen!");
response = new MyPack(Const.AUTHEN, MyPack.getSeq(), "authen fail");
MySession.write(response);
MySession.close(false);
return;
}
BaseHandler handler = handlers.get(MyPack.getModule());
String result = handler.process(MySession, MyPack.getBody());
if (result == null) {
logger.info(MySession.toString() + ">>>>>> need authen!");
response = new MyPack(Const.AUTHEN, MyPack.getSeq(), "deal error");
MySession.write(response);
MySession.close(false);
} else {
logger.info(MySession.toString() + ">>>>>> succeed!");
response = new MyPack(MyPack.getModule(), MyPack.getSeq(), result);
MySession.write(response);
}
}
/**
* 心跳包超时处理
*/
@Override
public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
if (session.getAttribute(Const.TIME_OUT_KEY) == null) {
session.closeNow();
logger.error(
session.getAttribute(Const.SESSION_KEY) + " nid: " + session.getId() + " >>>>>> time_out_key null");
return;
}
try {
int isTimeoutNum = (int) session.getAttribute(Const.TIME_OUT_KEY);
isTimeoutNum++;
// 没有超过最大次数,超时次数加1
if (isTimeoutNum < Const.TIME_OUT_NUM) {
session.setAttribute(Const.TIME_OUT_KEY, isTimeoutNum);
} else {
// 超过最大次数,关闭会话连接
String account = (String) session.getAttribute(Const.SESSION_KEY);
// 移除device属性
session.removeAttribute(Const.SESSION_KEY);
// 移除超时属性
session.removeAttribute(Const.TIME_OUT_KEY);
sessionManager.removeSession(account);
session.closeOnFlush();
logger.info(">>>>>> client user: " + account + " more than " + Const.TIME_OUT_NUM
+ " times have no response, connection closed! >>>>>>");
}
} catch (Exception e) {
logger.error(
session.getAttribute(Const.SESSION_KEY) + " nid: " + session.getId() + " >>>>>> " + e.getMessage());
session.closeNow();
}
}
@Override
public void sessionClosed(IoSession session) throws Exception {
logger.info(session.getAttribute(Const.SESSION_KEY) + " nid: " + session.getId() + " >>>>>> sessionClosed ");
// 移除account属性
session.removeAttribute(Const.SESSION_KEY);
// 移除超时属性
session.removeAttribute(Const.TIME_OUT_KEY);
String account = (String) session.getAttribute(Const.SESSION_KEY);
sessionManager.removeSession(account);
session.closeNow();
}
@Override
public void sessionCreated(IoSession session) throws Exception {
InetSocketAddress isa = (InetSocketAddress) session.getRemoteAddress();
// IP
String address = isa.getAddress().getHostAddress();
session.setAttribute("address", address);
logger.info(">>>>>> 来自" + address + " 的终端上线,sessionId:" + session.getId());
}
@Override
public void sessionOpened(IoSession session) throws Exception {
logger.info("Open a session ...");
}
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
logger.error(
">>>>>> 终端用户:" + session.getAttribute(Const.SESSION_KEY) + "连接发生异常,即将关闭连接,原因:" + cause.getMessage());
}
@Override
public void messageSent(IoSession session, Object message) throws Exception {
logger.info(">>>>>>>>>>>>>>>>>>>> 发送消息成功 >>>>>>>>>>>>>>>>>>>>");
}
public HashMap<Integer, BaseHandler> getHandlers() {
return handlers;
}
public void setHandlers(HashMap<Integer, BaseHandler> handlers) {
this.handlers = handlers;
logger.info(">>>>>> server handlers set success!");
}
}
绑定账号handler:
public class BindHandler implements BaseHandler {
static final Logger logger = LoggerFactory.getLogger(BindHandler.class);
// 获取会话管理类
@Autowired
private SessionManager sessionManager;
@Override
public String process(MySession mySession, String content) {
if (StringUtils.isBlank(content)) {
return null;
}
try {
JSONObject data = JSONObject.parseObject(content);
// 检查账号是否存在
String account = data.getString("account");
// 可增加数据库、redis之类的认证
if (StringUtils.isBlank(account)) {
return null;
}
// 检查软件版本号
String version = data.getString("version");
// 可增加数据库、redis之类的认证
if (!Const.VERSION.equals(version)) {
return null;
}
mySession.setAttribute(Const.SESSION_KEY, account);
mySession.setAttribute(Const.TIME_OUT_KEY, 0); // 超时次数设为0
// 由于客户端断线服务端可能会无法获知的情况,客户端重连时,需要关闭旧的连接
MySession oldSession = sessionManager.getSession(account);
if (oldSession != null && !oldSession.equals(mySession)) {
// 移除account属性
oldSession.removeAttribute(Const.SESSION_KEY);
// 移除超时时间
oldSession.removeAttribute(Const.TIME_OUT_KEY);
// 替换oldSession
sessionManager.replaceSession(account, mySession);
oldSession.close(false);
logger.info(">>>>>> oldsession close!");
}
if (oldSession == null) {
sessionManager.addSession(account, mySession);
}
logger.info(">>>>>> bind success: " + mySession.getNid());
} catch (Exception e) {
logger.error(">>>>>> bind error: " + e.getMessage());
return null;
}
return "bind success";
}
}
验证服务器时间handler:
public class TimeCheckHandler implements BaseHandler {
static final Logger logger = LoggerFactory.getLogger(TimeCheckHandler.class);
@Autowired
@Override
public String process(MySession mySession, String content) {
if (StringUtils.isBlank(content)) {
return null;
}
try {
// 平台系统时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String time = sdf.format(new Date());
return time;
} catch (Exception e) {
logger.error(">>>>>> time check error: " + e.getMessage());
return null;
}
}
}
在Spring Boot中给我们提供了两个接口来帮助我们实现这样的需求-CommandLineRunner和ApplicationRunner
@Component
public class MinaServerRun implements CommandLineRunner{
private static final Logger logger = LoggerFactory.getLogger(MinaServerRun.class);
@Autowired
private NioSocketAcceptor acceptor;
public MinaServerRun(NioSocketAcceptor acceptor) {
this.acceptor = acceptor;
}
@Override
public void run(String... args) throws Exception {
acceptor.bind(new InetSocketAddress(Const.PORT));
logger.info("---springboot mina server start---");
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
logger.info("---server acceptor unbind---");
acceptor.unbind();
logger.info("---server acceptor dispose---");
acceptor.dispose();
}
});
}
}
额外添加了一个钩子方法,保证在使用kill指令杀掉tomcat进程时,可以关闭NioSocketAcceptor
public class Const {
public static final int PORT = 8090;
// idel时间,单位秒
public static final int IDELTIMEOUT = 180;
// session_key采用设备编号
public static final String SESSION_KEY = "account";
// 超时KEY
public static final String TIME_OUT_KEY = "time_out";
// 超时次数
public static final int TIME_OUT_NUM = 3;
// 登录验证
public static final int AUTHEN = 1;
// 验证服务器时间
public static final int TIME_CHECK = 2;
// 心跳包
public static final int HEART_BEAT = 3;
// 版本
public static final String VERSION = "V1.0";
}
项目启动后,控制台信息如下:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.6.RELEASE)
2019-07-06 17:05:32.543 INFO 15572 --- [ main] org.my.SpringbootMinaApplication : Starting SpringbootMinaApplication on PC-20161221CSLG with PID 15572 (D:\workspace\springboot_mina\target\classes started by Administrator in D:\workspace\springboot_mina)
2019-07-06 17:05:32.551 INFO 15572 --- [ main] org.my.SpringbootMinaApplication : No active profile set, falling back to default profiles: default
2019-07-06 17:05:34.474 INFO 15572 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-07-06 17:05:34.518 INFO 15572 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-07-06 17:05:34.518 INFO 15572 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.21]
2019-07-06 17:05:34.711 INFO 15572 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-07-06 17:05:34.711 INFO 15572 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2078 ms
2019-07-06 17:05:34.849 INFO 15572 --- [ main] org.my.mina.handler.ServerHandler : >>>>>> server handlers set success!
2019-07-06 17:05:35.284 INFO 15572 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-07-06 17:05:35.628 INFO 15572 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-07-06 17:05:35.639 INFO 15572 --- [ main] org.my.SpringbootMinaApplication : Started SpringbootMinaApplication in 3.729 seconds (JVM running for 4.901)
2019-07-06 17:05:35.649 INFO 15572 --- [ main] org.my.mina.MinaServerRun : ---springboot mina server start---
测试稍后奉上,敬请期待~
项目github地址:
https://github.com/gavinL93/springboot_mina
欢迎大家关注