刚开始用这个玩意的时候,很多东西不是很了解,所以走了很多弯路,大概记录一下。
推荐大家去看看这篇博客,当初也是因为看了这篇博客,才知道自己理解错误的地方:点击打开链接
1.mina框架的编码解码工厂
mina和netty都有这么一个怪毛病,他的编码解码工厂都是单例的,至今未搞懂这么设计的目的,如果有哪位大神知道,可以留言教教我哦。
虽然mina的编码解码工厂都是单例的,但是,当每次有一个客户端连接到服务器的时候,mina服务器都会创建一个会话,我们可以借助这个会话来分离每个客户端之间的消息编码解码处理。
一.客户端编码解码的处理
如大家所了解,一般这种管理类都要是单例的,所以大家都要注意实现类的单例化哦,不会的话可以自行百度。
类里面就存放一个Map,用于存放客户端的会话与对应的编码解码处理类,当客户端连接成功,并开始发送消息的时候,当进入自定义实现mina解码接口类的时候,首先在管理。工厂中去找自定义的解码类,若找不到,新建一个并保存在管理类中。
解码器中三个比较重要的方法:
a. in.remaining():表示未处理数据的字节长度
b. in.limit(); 表示本条数据的长度
c. in.position(); 表示本条数据当前状态已经处理的数据长度(通过in.positon(int x)方法所标记的位置)
还有一点需要注意的,mina解码类的返回值问题:
1.打标记:in.position(int x);这个方法是给本次接收到的消息打一个标记,当返回true的时候,就会把标记后面的数据重新发送一次
2.return true;表示本次接收到的数据出现粘包问题,要把标记位置后面的数据重新发送一次
3.return false;表示本条数据处理成功,等待继续处理吓一跳数据,此时标记位应该在本条数据的末尾,也就是in.limit。
自定义解码类:
/**
* 解码线程
*
* @author 郭鹏飞
*
*/
public class DecodeHandler {
private IoSession session; // 客户端连接类
public String allData = ""; // 完整数据包
public String remainData = ""; // 剩余的数据
private boolean dataHead = true; // 是否验证了数据长度
private byte[] head = {}; // 获取到数据头
private int msgLength = 0; // 获取到数据的长度
private boolean recHand = false; // 接收到握手包
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public DecodeHandler(IoSession session) {
this.session = session;
}
/**
* 消息编码处理
*
* @param data
* @return
*/
public boolean handleData(String data) {
//自定义处理逻辑
remainData = "";
try {
String str = HexUtil.hex2String(data);
/* RTU心跳截取 */
if (str.length() >= 2 && str.substring(0, 2).equals("FE")) {
String tem = data;
tem = data.substring(0, 4);
remainData = data.substring(4);
allData = tem;
return true;
}
/* RTU开闸确认指令 */
if (data.length() == 16 || data.length() == 26) {
allData += data;
return true;
}
byte[] msgByte = HexUtil.hexString2Bytes(data);
/* 未接收到握手包的设备信息 */
if (!recHand && msgByte[0] == '[') {
int x = data.indexOf("5D");
String tem = data;
tem = data.substring(0, x + 2);
remainData = data.substring(x + 2);
allData = tem;
recHand = true;
return true;
}
// 如果未验证数据长度
if (dataHead) {
//验证数据长度
}
allData += data;
if (msgLength <= 0 || msgLength > 150) {
initDecode();
return true;
}
// 接收完整数据
else{
if (allData.length() / 2 >= msgLength) {
String tem = allData;
tem = allData.substring(0, msgLength * 2);
remainData = allData.substring(msgLength * 2);
allData = tem;
initDecode();
return true;
} else {
return false;
}
}
} catch (Exception e) {
e.printStackTrace();
System.out.println(sdf.format(new Date()) + "========================RTU解码器异常=====================");
return true;
}
/**
* 初始化数据
*/
private void initDecode() {
this.msgLength = 0;
this.dataHead = true;
}
}
客户端解码类管理工厂:
/**
* RTU解码工厂
*
* @author 郭鹏飞
*
*/
public class MyProtocalDecoder extends CumulativeProtocolDecoder {
private final String charset;
public MyProtocalDecoder(String charset) {
this.charset = charset;
}
/**
* 解码接收到的消息
*/
@Override
protected synchronized boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out)
throws Exception {
String nowData = in.getHexDump(); // 接收16进制数据
nowData = nowData.replace(" ", "");
System.out.println("本次接收到的数据:" + nowData);
DecodeHandler dh = DecodeManage.getInstance().getValueBykey(session);
// 存在该客户端的解码处理方法
if (dh == null) {
dh = new DecodeHandler(session);
DecodeManage.getInstance().putValue(session, dh);
}
if (dh.handleData(nowData)) {
out.write(dh.allData);
System.out.println("RTU平台剩余数据长度:"+dh.remainData.length());
if (dh.remainData != null && !dh.remainData.equals("")) {
in.position(in.limit() - dh.remainData.length() / 2);
dh.allData = "";
return true;
} else {
in.position(in.limit());
dh.allData = "";
return false;
}
} else {
in.position(in.limit());
return false;
}
}
}
自定义mina解码接口实现类:
/**
* 解码器线程管理类
* @author guopengfei
*
*/
public class DecodeManage {
private static DecodeManage dm;
private DecodeManage(){}
public static DecodeManage getInstance(){
if(dm == null){
synchronized (DecodeManage.class) {
if(dm == null)
dm = new DecodeManage();
}
}
return dm;
}
public static Map<IoSession, DecodeHandler> map = new HashMap<IoSession, DecodeHandler>(); // 客户端连接池
/**
* 根据值获取键
* @param dt
* @return
*/
public IoSession getKeyByValue(DecodeHandler dt) {
for (IoSession key : map.keySet()) {
DecodeHandler is = map.get(key);
if (is.equals(dt))
return key;
}
return null;
}
/**
* 获取一个客户端解码线程
*
* @param key
* @return
*/
public synchronized DecodeHandler getValueBykey(IoSession key) {
return map.get(key);
}
/**
* 删除一个客户端解码线程
*
* @param key
*/
public synchronized void removebyKey(IoSession key) {
map.remove("");
map.remove(key);
}
/**
* 添加一个客户端解码线程
*
* @param key
* @param dt
*/
public synchronized void putValue(IoSession key, DecodeHandler dt) {
map.put(key, dt);
}
/**
* 查看容器数量
*
* @return
*/
public int getMapSize() {
return map.size();
}
}
二. 客户端处理类的管理
到这里的时候就很轻车熟路了,每个客户端的消息应该如何处理才能保证不同客户端发送的消息分开处理,并且管理所有客户端连接,可以实现向指定客户端发送消息。
1. 编写一个消息处理管理类,用来存放每个客户端的连接,key中存放已知的连接唯一标识
注册抱机制:
在注册包中携带一个客户端连接的唯一标识,以后可以通过这个标识来找到对应的连接消息。
当每个连接接收到第一条消息,或者携带有指定包头或者其他自定义标识位的数据为注册包,将此注册包的信息与连接的IoSession存放在管理类中。
2. 客户端断电断网等异常掉线状态的检测
当每个客户端连接的时候,为每个客户端连接设置一个超时时间,当达到超时时间的时候,会自动进入sessionIdle()方法,可以在此方法中主动调用断开连接的方法断开与客户端的连接。
此处有一个小小的坑,直接调用session.close()是不能断开连接的(新版本的mina中貌似弃用了close方法,我目前用的是closeNow方法),需要提前设置一个参数,才能彻底关闭连接。
SocketSessionConfig cfg = (SocketSessionConfig) session.getConfig();
session.getConfig().setIdleTime(IdleStatus.BOTH_IDLE, 300);
cfg.setSoLinger(0);
mina作为客户端时,关闭连接的方法也不一样,不能用close();方法,要使用IoService的dispose方法才可以。
3. 当关闭客户端连接的时候,需要从解码管理工厂和连接管理工厂中干掉该连接的信息。然后关闭客户端的连接。
@Override
public void sessionClosed(IoSession session) throws Exception {
System.out.println(session.getRemoteAddress()+"===================的连接关闭了");
String eqNo = RTUClientManage.getInstance().getKeyByValue(session);
RTUClientManage.getInstance().removebyKey(eqNo);
DecodeManage.getInstance().removebyKey(session);
System.out.println(sdf.format(new Date()) + " RTU设备连接断开,设备的地址:" + session.getRemoteAddress() + "\n");
if (session != null) {
session.closeNow();
}
}
客户端消息处理类:
/**
* 接收到消息
*
* @param session
* @param message
* @throws Exception
*/
@Override
public synchronized void messageReceived(IoSession session, Object message) throws Exception {
String msg = message.toString();
if (msg != null) {
String head = HexUtil.subStr(data);
if (head != null && !head.equals("") && data.length() < 20 && data.startsWith("[") && data.endsWith("]")) {
handData = head;
// 添加到客户端管理类中
RTUClientManage.getInstance().putValue(handData, session);
System.out.println(sdf.format(new Date()) + " 接收到RTU设备" + session.getRemoteAddress() + "握手包了,握手包为为:"
+ handData + "=================绑定成功================");
System.out.println("================================================ 正在连接的RTU客户端数量为:"
+ RTUClientManage.getInstance().getMapSize());
} else {
//业务逻辑处理
}
}
}
客户端连接管理工厂
package com.ts.handler;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.apache.mina.core.session.IoSession;
import com.ts.util.HexUtil;
/**
* 客户端管理类
*
* @author 郭鹏飞
*
*/
public class RTUClientManage {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private volatile static RTUClientManage rc;
private RTUClientManage() {
}
/**
* 单例化管理类
*
* @return
*/
public static RTUClientManage getInstance() {
if (rc == null) {
synchronized (RTUClientManage.class) {
if (rc == null) {
rc = new RTUClientManage();
}
}
}
return rc;
}
public static Map<String, IoSession> map = new HashMap<String, IoSession>(); // 客户端连接池
public String getKeyByValue(IoSession session) {
for (String key : map.keySet()) {
IoSession is = map.get(key);
if (is.equals(session))
return key;
}
return null;
}
/**
* 获取一个客户端
*
* @param key
* @return
*/
public synchronized IoSession getValueBykey(String key) {
return map.get(key);
}
/**
* 删除一个客户端
*
* @param key
*/
public synchronized void removebyKey(String key) {
map.remove("");
map.remove(key);
}
/**
* 添加一个客户端
*
* @param key
* @param rsh
*/
public synchronized void putValue(String key, IoSession rsh) {
IoSession session = map.get(key);
if(session != null){
removebyKey(key);
session.closeNow();
}
map.put(key, rsh);
}
/**
* 查看容器数量
*
* @return
*/
public int getMapSize() {
return map.size();
}
}