一、前言
該數據據採集中間件需要實現與多個終端的長連接,並定時給所有終端發送指令,終端在接收到相關指令後,返回相關信息給中間件。中間件需要一直監測所有終端的在線狀態,並一直監聽、接收所有終端的消息,並啓動多個定時任務給在線終端發送相關指令。
二、網絡通信的相關概念和基礎知識
長連接的基本概念:
* 與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實現數據採集時出現的斷包、半包的問題處理