一、前言
该数据据采集中间件需要实现与多个终端的长连接,并定时给所有终端发送指令,终端在接收到相关指令后,返回相关信息给中间件。中间件需要一直监测所有终端的在线状态,并一直监听、接收所有终端的消息,并启动多个定时任务给在线终端发送相关指令。
二、网络通信的相关概念和基础知识
长连接的基本概念:
* 与Http短连接相反,通过某种方式与服务器一直保持连接就叫长连接
长连接的基本原理
* 底层都是基于TCP/IP协议
* 通过Socket、ServerSocket与服务器保持连接
* 服务端一般使用ServerSocket建立监听,监听客户端与之连接
* 客户端使用Socket,指定端口和IP地址,与服务端进行连接
长连接的意义:
* 通过长连接,可以实现服务器主动向客户端推送消息
* 通过长连接,可以减少客户端对服务器的轮询,减小服务器压力
* 通信效率要远高于http
心跳:在TCP网络通信中,经常会出现客户端和服务器之间的非正常断开,需要实时检测查询链接状态。常用的解决方法就是在程序中加入心跳机制。
三、代码实现
SessionBean.java
/**
* IoSession连接信息
*/
public class SessionBean {
/** IoSession 对象 */
private IoSession session;
/** 最后一次请求时间 */
private Long lastTime;
/** ip地址 */
private String ip;
/** 端口号 */
private Integer port;
}
ClientSessionUtil.java
package com.che.utils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.mina.core.session.IoSession;
import com.ym.bean.SessionBean;
/**
* 客户端IoSession管理类
*/
public class ClientSessionUtil {
/** 用于保存所有客户端的连接状态 */
private static Map<Long, SessionBean> clientMap = null;
private ClientSessionUtil(){
}
private static synchronized Map<Long, SessionBean> getClientMap(){
if(clientMap == null){
clientMap = new HashMap<Long, SessionBean>();
}
return clientMap;
}
/**
* 获取某个客户端连接的最后一次请求时间
* @param sessionId
* @return
*/
public static Long getLastTime(Long sessionId){
SessionBean sessionBean = getClientMap().get(sessionId);
if(sessionBean != null){
return sessionBean.getLastTime();
}
return null;
}
/**
* 根据sessionId获取客户端的IoSession对象
* @param sessionId
* @return
*/
public static IoSession getIoSession(Long sessionId){
SessionBean sessionBean = getClientMap().get(sessionId);
if(sessionBean != null){
return sessionBean.getSession();
}
return null;
}
/**
* 新增一个客户端IoSession
* @param session
*/
public static void addSesssion(IoSession session){
SessionBean sessionBean = new SessionBean();
sessionBean.setSession(session);
//设置当前的请求时间
sessionBean.setLastTime(System.currentTimeMillis());
//获取客户端的ip及端口
String address = session.getRemoteAddress().toString();
if(address != null && !address.equals("")){
String[] addArr = address.split(":");
if(addArr != null && addArr.length == 2){
sessionBean.setIp(addArr[0]);
sessionBean.setPort(Integer.parseInt(addArr[1]));
}
}
getClientMap().put(session.getId(), sessionBean);
}
/**
* 获取当前客户端连接数量
* @return
*/
public static int getSessionCount(){
return getClientMap().size();
}
/**
* 断开某个客户端连接
* @param session
*/
public static void removeSession(IoSession session){
getClientMap().remove(session.getId());
}
/**
* 获取当前所有在线的客户端连接IoSession集合
* @return
*/
public static Set<IoSession> getAllSession(){
Set<IoSession> sessions = new HashSet<IoSession>();
Set<Long> idSet = ClientSessionUtil.getClientMap().keySet();
for (Long id : idSet) {
System.out.println("---000--" + id);
IoSession session = ClientSessionUtil.getIoSession(id);
sessions.add(session);
}
return sessions;
}
public static SessionBean getSessionBeanBySessionId(Long sessionId){
if(getClientMap().values() != null && getClientMap().values().size() > 0){
for(SessionBean sessionBean : getClientMap().values()){
if(sessionBean.getSessionId() == sessionId){
return sessionBean;
}
}
}
return null;
}
public static String getMId(Long sessionId){
SessionBean sessionBean = getSessionBeanBySessionId(sessionId);
if(sessionBean != null){
return sessionBean.getMId();
}
return null;
}
}
PropertiesUtil.java
package com.che.utils;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
/**
* 配制参数读取工具类
*/
public class PropertiesUtil {
/** 服务器端口 */
public static final String PORT = "port";
/** 缓冲区大小 */
public static final String READ_BUFFER_SIZE = "readBufferSize";
/** 心跳时间 */
public static final String HEARTBEAT_OUT_TIME = "heartbeatOutTime";
private static PropertiesUtil propertiesUtil = null;
private static Properties prop;
private PropertiesUtil(){
}
public static PropertiesUtil getInstance(){
if(propertiesUtil == null){
propertiesUtil = new PropertiesUtil();
prop = new Properties();
try {
prop.load(new FileInputStream("config.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
return propertiesUtil;
}
/**
* 获取某项配置参数
* @param key
* @return
*/
public static int getProperty(String key){
Object obj = prop.get(key);
return Integer.parseInt(obj + "");
}
}
配制参数文件:config.properties
#服务器端口
port=8002
#缓冲区大小
readBufferSize=102048
#心跳时间
heartbeatOutTime=30000
定时任务管理类:TaskUtils.java
package com.che.utils;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
/**
* 定时任务管理类
*
*/
public class TaskUtils {
@SuppressWarnings("unused")
public static void startTask() {
ScheduledExecutorService newScheduledThreadPool = Executors
.newScheduledThreadPool(2);
//任务1:每隔15分钟读取终端的各项参数
Runnable readDataTask = new Runnable() {
public void run() {
System.out.println("---定时任务--begin--");
//获取当前客户端连接数
int currentSessionCount = ClientSessionUtil.getSessionCount();
System.out.println("===连接数:" + currentSessionCount);
if(currentSessionCount > 0){
//获取当前所有在线客户端连接IoSession集合
Set<IoSession> sessions = ClientSessionUtil.getAllSession();
if(sessions != null && sessions.size() > 0){
//遍历所有在线客户端连接IoSession
for(IoSession session : sessions){
//通过客户端连接IoSession,给每一个在线客户端发送指令
sendMsgTask(session.getId());
}
}
}
System.out.println("---定时任务--end--\n\n");
}
/**
* 通过sessionId,获取到IoSession,然后给某个客户端发送消息
* @param id
*/
private void sendMsgTask(Long id) {
/*
//最初的写法
IoSession session = ClientSessionUtil.getIoSession(id);
String cmd="01 02 03 0A 0B 0C";
String[] cmds = cmd.split(" ");
byte[] aaa = new byte[cmds.length];
int i = 0;
for (String b : cmds) {
if (b.equals("FF")) {
aaa[i++] = -1;
} else {
aaa[i++] = Byte.parseByte(b, 16);
}
}
session.write(IoBuffer.wrap(aaa));
*/
//改进后的写法,将定时任务
String menterId = ClientSessionUtil.getMeterId(session.getId());
String cmd = ProtocolFormatUtils.getCmd(menterId);
TaskCmdQueue.addItem(menterId,cmd);
System.out.println(session.getId() + "-发送数据--000--成功--");
}
};
//每天晚上00:10定时执行的任务
Runnable nightTask = new Runnable() {
@Override
public void run() {
System.out.println("每天晚上执行的任务------");
}
};
//每隔15分钟执行一次
newScheduledThreadPool.scheduleAtFixedRate(readDataTask, 2000,1000 * 60 * 15, TimeUnit.MILLISECONDS);
//每天晚上00:10点执行一次
// long oneDay = 24 * 60 * 60 * 1000;
// long initDelay = getTimeMillis("16:58:00") - System.currentTimeMillis();
// initDelay = initDelay > 0 ? initDelay : oneDay + initDelay;
// newScheduledThreadPool.scheduleAtFixedRate(nightTask, initDelay,oneDay, TimeUnit.MILLISECONDS);
System.out.println("---定时任务--begin-aa-");
}
/**
* 获取指定时间对应的毫秒数
* @param time "HH:mm:ss"
* @return
*/
@SuppressWarnings("unused")
private static long getTimeMillis(String time) {
try {
DateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
DateFormat dayFormat = new SimpleDateFormat("yy-MM-dd");
Date curDate = dateFormat.parse(dayFormat.format(new Date()) + " " + time);
return curDate.getTime();
} catch (ParseException e) {
e.printStackTrace();
}
return 0;
}
}
报文发送队列TaskCmdQueue.java
//保存和获取所有终端的待发送报文的队列信息的工具类
public class TaskCmdQueue {
//保存终端待发送报文队列的map,键是终端id,值是该终端待发送的报文队列信息列表
private static Map<String,Queue<String>> taskCmdQueue = null;
private TaskCmdQueue(){
}
public static synchronized Map<String,Queue<String>> getTaskCmdQueue(){
if(taskCmdQueue == null){
taskCmdQueue = new HashMap<String, Queue<String>>();
}
return taskCmdQueue;
}
//根据终端id,获取该通道最前面的一条待发送报文信息
public static synchronized String getItem(String meterId){
Map<String,Queue<String>> map = getTaskCmdQueue();
if(map != null && meterId != null){
Queue<String> queue = map.get(meterId);
if(queue != null && !queue.isEmpty()){
String result = queue.poll();
map.put(meterId, queue);
return result;
}
}
return null;
}
//在某个终端的通道的消息队列最后面添加一条待发送报文
public static synchronized void addItem(String meterId,String cmdStr){
Map<String,Queue<String>> map = getTaskCmdQueue();
if(map != null){
Queue<String> queue = map.get(meterId);
if(queue == null){
queue = new LinkedList<String>();
}
queue.offer(cmdStr);
map.put(meterId, queue);
}
}
}
服务器端测试类MinaServer.java
package com.che.server;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.stream.StreamWriteFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import com.che.utils.ClientSessionUtil;
import com.che.utils.PropertiesUtil;
import com.che.utils.TaskUtils;
/**
* 服务器端测试类
*/
public class MinaServer {
@SuppressWarnings("static-access")
public static void main(String[] args) {
IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.getFilterChain().addFirst("codec",new StreamWriteFilter());
acceptor.setHandler(new DemoServerHandler());
acceptor.getSessionConfig().setReadBufferSize(PropertiesUtil.getProperty(PropertiesUtil.READ_BUFFER_SIZE));
//读(接收通道)空闲时间:10秒
acceptor.getSessionConfig().setIdleTime(IdleStatus.READER_IDLE, 10);
try {
acceptor.bind(new InetSocketAddress(PropertiesUtil.getProperty(PropertiesUtil.PORT)));
} catch (Exception e) {
}
System.out.println("服务启动...");
//开始执行定时任务
TaskUtils.startTask();
}
private static class DemoServerHandler extends IoHandlerAdapter{
@Override
public void sessionCreated(IoSession session) throws Exception {
super.sessionCreated(session);
System.out.println("------客户端----" + session.getId() + "----sessionCreated-----");
}
@Override
public void sessionOpened(IoSession session) throws Exception {
super.sessionOpened(session);
System.out.println("------客户端----" + session.getId() + "----sessionOpened-----");
//注册客户端
ClientSessionUtil.addSesssion(session);
System.out.println("连接数:" + ClientSessionUtil.getSessionCount());
}
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
super.messageReceived(session, message);
try {
//更新lastTime
ClientSessionUtil.addSesssion(session);
//解析message数据
String engOut = parseMessage(message);
System.out.println("------收到到客户端----" + session.getId() + "---的数据:" + engOut);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 解析客户端发送过来的数据
* @param message
* @return
*/
private String parseMessage(Object message) {
IoBuffer buf = (IoBuffer) message;
IoBuffer.allocate(1024);
buf.setAutoExpand(true);//长度超过会自动翻倍增长
ByteBuffer bf = buf.buf();
System.out.println(bf);
byte[] tmpBuffer = new byte[bf.limit()];
bf.get(tmpBuffer);
String str = "";
for(int i=0; i <tmpBuffer.length; i++) {
String getM = Integer.toHexString(tmpBuffer[i] & 0xFF)+"";//转换16进制
if(getM.length()<2){
getM="0"+getM;
}
str+=getM+" ";
}
return str;
}
@Override
public void messageSent(IoSession session, Object message) throws Exception {
super.messageSent(session, message);
System.out.println("------发送给客户端----" + session.getId() + "----messageSent-----");
System.out.println("===连接数:" + ClientSessionUtil.getSessionCount());
}
@SuppressWarnings("static-access")
@Override
public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
super.sessionIdle(session, status);
System.out.println("------客户端----" + session.getId() + "----sessionIdle-----");
if(ClientSessionUtil.getIoSession(session.getId()) != null){
//某个客户端的最后一次请求时间
Long lastTime = ClientSessionUtil.getLastTime(session.getId());
//获取当前时间
Long nowTime = System.currentTimeMillis();
//获取心跳时间
int heartbeatOutTime = PropertiesUtil.getProperty(PropertiesUtil.HEARTBEAT_OUT_TIME);
//如果某个客户端的最后一次请求时间距离当前时间的间隔大于心跳时间,将该客户端连接移除
if((nowTime - lastTime) > heartbeatOutTime){
System.out.println("------客户端----" + session.getId() + "----超时-----");
ClientSessionUtil.removeSession(session);
}
}
String meterId = ClientSessionUtil.getMId(session.getId());
if(meterId != null && !meterId.equals("")){
//从消息队列中获取一条报文,然后发送
String cmdStr = TaskCmdQueue.getItem(meterId);
if(cmdStr != null && !cmdStr.equals("")){
byte[] senStr = convertCmd(cmdStr);
session.write(IoBuffer.wrap(senStr));
}
}
}
@Override
public void sessionClosed(IoSession session) throws Exception {
super.sessionClosed(session);
System.out.println("------客户端----" + session.getId() + "----sessionClosed-----");
ClientSessionUtil.removeSession(session);
System.out.println("连接数:" + ClientSessionUtil.getSessionCount());
}
}
}
四、小结:
本文用到的知识点及参考
1、mina、nio
http://www.imooc.com/article/19091(mina 客户端与服务器端之间的通信)
2、定时任务使用ScheduledExecutorService替代Timer,上文中也有关于ScheduledExecutorService实现定时任务在某个具体时间点定时执行的实现。
3、客户端测试工具推荐:
USR-TCP232-Test(windows)这个没有mac版
SocketTest 是mac版的,另外mac可以在终端里面使用telnet 127.0.0.1 8002
测试控制台打印信息
服务启动...
---定时任务--begin-aa-
---定时任务--begin--
===连接数:0
---定时任务--end--
---定时任务--begin--
===连接数:0
---定时任务--end--
------客户端----1----sessionCreated-----
------客户端----1----sessionOpened-----
连接数:1
---定时任务--begin--
===连接数:1
---000--1
1-发送数据--000--成功--
---定时任务--end--
------发送给客户端----1----messageSent-----
===连接数:1
------客户端----1----sessionIdle-----
lastTime:1506133144457
nowTime:1506133155309
times:10852
------客户端----2----sessionCreated-----
------客户端----2----sessionOpened-----
连接数:2
---定时任务--begin--
===连接数:2
---000--1
---000--2
1-发送数据--000--成功--
2-发送数据--000--成功--
---定时任务--end--
------发送给客户端----1----messageSent-----
------发送给客户端----2----messageSent-----
===连接数:2
===连接数:2
------客户端----1----sessionIdle-----
lastTime:1506133144457
nowTime:1506133166304
times:21847
------客户端----2----sessionIdle-----
lastTime:1506133158008
nowTime:1506133168308
times:10300
---定时任务--begin--
===连接数:2
---000--1
---000--2
1-发送数据--000--成功--
2-发送数据--000--成功--
---定时任务--end--
------发送给客户端----1----messageSent-----
===连接数:2
------发送给客户端----2----messageSent-----
===连接数:2
java.nio.HeapByteBuffer[pos=0 lim=9 cap=102048]
------收到到客户端----1---的数据:73 65 6e 64 20 31 31 0d 0a
java.nio.HeapByteBuffer[pos=0 lim=9 cap=102048]
------收到到客户端----2---的数据:73 65 6e 64 20 33 33 0d 0a
---定时任务--begin--
===连接数:2
---000--1
---000--2
1-发送数据--000--成功--
------发送给客户端----1----messageSent-----
===连接数:2
------发送给客户端----2----messageSent-----
===连接数:2
2-发送数据--000--成功--
---定时任务--end--
------客户端----1----sessionIdle-----
lastTime:1506133172435
nowTime:1506133183291
times:10856
------客户端----2----sessionIdle-----
lastTime:1506133177724
nowTime:1506133188297
times:10573
---定时任务--begin--
===连接数:2
---000--1
---000--2
1-发送数据--000--成功--
2-发送数据--000--成功--
---定时任务--end--
------发送给客户端----1----messageSent-----
===连接数:2
------发送给客户端----2----messageSent-----
===连接数:2
------客户端----1----sessionIdle-----
lastTime:1506133172435
nowTime:1506133193294
times:20859
5、优化
使用报文发送队列工具类TaskCmdQueue,将定时任务中每个终端的待发送报文队列保存起来,然后在mina的 sessionIdle方法中根据终端id获取待发送的报文,如果有,取一条发送。
本人的另外两篇mina文章:
基于mina实现一个简单数据采集中间件的多客户端在线测试程序
使用Mina实现数据采集时出现的断包、半包的问题处理