nj4x-ts简介
nj4x-ts是NJ4X项目中的终端服务器,参考我的上篇博客中的图:
nj4x-ts起着承上启下的作用,nj4x-ts提供了面向中断的网络通讯接口,操作底层的C++函数操作mt4的程序。然后让整个系统跑起来。
包结构
包结构如图:
- gui 界面
- io 引用dll库,文件操作
- jmx JMX服务器
- net 网络通讯相关
- xml xml操作类
编译
编译的时候,会出现问题,大致就是找不到dll,到时候要注意调整dll的路径,编译会直接生成课执行jar
包,exe
可执行程序程序,如下图:
其中antrun
文件夹下是ant
的编译脚本。
这样就编译完了整个项目。
启动过程
nj4x-ts的启动过程非常值得一提。
入口函数
入口函数在com.jfx.ts.net.TerminalServer
类中。
首先开始读取main
函数的参数args
,我们一般不给这个写参数。
if (args.length % 2 == 0) { for (int i = 0; i < args.length; i++) { String pn = args[i++]; String pv = args[i]; System.setProperty(pn, pv); //指定系统的全局属性 } }
然后加载dll库:
LibrariesUtil.initEmbeddedLibraries();
initEmbeddedLibraries()
加载dll之前,LibrariesUtil
有一个静态块,静态块在构造函数之前运行,用以判断操作系统的位数,以及初始化程序的相关路径:
static { String osName = System.getProperty("os.name"); isWindows = osName.toLowerCase().contains("windows"); String arch = System.getProperty("os.arch"); isX64 = arch.contains("amd64") || arch.contains("x64"); LIBS_DIR = System.getProperty("program_data_dir", "C:\\ProgramData\\nj4x") + "\\bin";// LIBS_DIR = System.getProperty("user.dir"); }
然后再调用initEmbeddedLibraries()
函数,根据位数选择相应的dll。奇怪的是,作者并没有直接复制dll,而是直接二进制读取原路径的dll,然后在目标路径新建一个dll,把二进制写入新的dll文件,然后再System.load()
,很奇怪,也许作者这样做是想确保每次的dll都是最新的。代码如下:
final File libFile = new File(libFileName); // C:\ProgramData\nj4x\bin\PSUtils_x64.dll//final InputStream in = nativeLibraryUrl.openStream();final OutputStream out = new BufferedOutputStream(new FileOutputStream(libFile));//int len;byte[] buffer = new byte[160000];while ((len = in.read(buffer)) > -1) out.write(buffer, 0, len); //写入dll文件out.close();in.close();//System.load(libFile.getAbsolutePath()); //加载PSUtil_X64.dll这个dll
至此,dll加载完毕。
接下来就判断程序是不是以管理员身份登录的:
asAdministrator = PSUtils.asAdministrator();
具体方法在com.jfx.ts.io.PSUtils
中:
public static boolean asAdministrator() { if (!isAdministrator()) { String compat_layer = System.getenv().get("__COMPAT_LAYER"); if (compat_layer == null || !compat_layer.contains("RunAsAdmin")) { return false; } } return true;}
而isAdministrator()
是一个native
静态方法:
public static native boolean isAdministrator();
这个方法是使用的PSUtils_x64.dll
的native方法。因为com.jfx.ts.io.PSUtils
中有一个静态块:
// Load the dll that exports functions callable from javastatic { if (!LibrariesUtil.IS_OK) { String dllFileName = null; try { if (LibrariesUtil.isX64) { dllFileName = "PSUtils_x64.dll"; } else { dllFileName = "PSUtils.dll"; } String libFileName = LibrariesUtil.LIBS_DIR + File.separator + System.load(libFileName); } catch (Throwable t) { t.printStackTrace(); try { System.load(dllFileName); } catch (Exception e) { System.exit(2); } } }}
没错,这就是战斗民族的代码。
接下来就是确定最大线程数,不知道战斗民族为什么把代码写的如此复杂:
//最大线程数,不知道为什么要搞得这么复杂 MAX_TERMINAL_STARTUP_THREADS = Integer.parseInt(System.getProperty("max_terminal_connection_threads", "" + (AVAILABLE_PROCESSORS >= 24 ? AVAILABLE_PROCESSORS / 2 : (AVAILABLE_PROCESSORS >= 12 ? AVAILABLE_PROCESSORS / 3 : (AVAILABLE_PROCESSORS > 3 ? AVAILABLE_PROCESSORS - 2 : AVAILABLE_PROCESSORS)))));
然后判断,要不要启动专家系统,默认是不启动的。
IS_DEPLOY_EA_WS = System.getProperty("deploy_EA_WS", "false").equals("true");
作者在代码中大量的使用System.getProperty()
方法操作参数,思路不错。
真正的部署程序在这个方面里面
deploy(port);
deploy(port)
方法首先打印出所有的系统环境变量和程序设置的参数:
Logger logger = TS.LOGGER;//logger.info("--");logger.info("--");logger.info("--");logger.info("-- TS " + TS.NJ4X + " STARTUP --");logger.info("-- " + TS.NJ4X_UUID + " --");logger.info("--");logger.info("--");logger.info("--");logger.info("-- System properties --");for (Map.Entry e : new TreeMap<>(System.getProperties()).entrySet()) { logger.info("" + e.getKey() + "=" + e.getValue());}logger.info("-- Environment --");for (Map.Entry e : new TreeMap<>(System.getenv()).entrySet()) { logger.info("" + e.getKey() + "=" + e.getValue());}logger.info("-- Deployment --");
然后实例化TS
类,TS
作用下一节再分析。
接下来就部署WebService服务,具体由TsWS
类实现。
关于nj4x-ts的网络通信部分,会在下一篇博客中做介绍,在此略过。
但是作者在最后又默认启动专家系统的webserive,不知道为什么,现在没有计划研究专家系统,以后有可能。
//部署专家系统webservicedeployEaWs(true);
总之,启动后的样子:
这个界面就不介绍了,懂的人自然懂。
TS
类的分析
TS
类要单独拿出来分析,因为,以上的启动过程都是表面上的,程序启动真正干活的是TS
类。
TS
类中的各个静态变量,可以自己看。
首先,TS
类有三个静态块:
第一个我不知道是干嘛用的,目的不明确:
static { try { //不知道为什么要外部程序,不知道是干什么的,跑起来之后看看hostname是个什么鬼 //但是这个是简化外部当前JVM进程的执行。 ExternalProcess p = new ExternalProcess("hostname"); p.run(); TS.hostname = p.getOut().trim(); } catch (Exception e) { TS.LOGGER.error("hostname cmd error", e); TS.hostname = System.getenv("COMPUTERNAME"); }}
第二个初始化JFX的HOME路径:
static { //貌似是初始化JFX的HOME路径 JFX_HOME = System.getProperty("home", System.getProperty("user.hom System.setProperty("home", JFX_HOME); String tmout = System.getenv("JFX_TERM_IDLE_TMOUT_SECONDS"); if (tmout == null) { JFX_TERM_IDLE_TMOUT_SECONDS = 3600 * 6; } else { JFX_TERM_IDLE_TMOUT_SECONDS = Long.parseLong(tmout); }}
第三个非常重要,其作用是初始化程序运行的各种路径文件:
static { //mkdir函数,如果存在就返回false new File(getTargetTermDir()).mkdirs(); // new File(JFX_HOME_CONFIG).mkdirs(); new File(JFX_HOME_SRV_DIR).mkdirs(); new File(JFX_HOME_EA_DIR).mkdirs(); new File(JFX_HOME_EXPERTS_DIR).mkdirs(); new File(JFX_HOME_INDICATORS_DIR).mkdirs(); new File(JFX_HOME_CHR_DIR).mkdirs(); // TerminalClient terminalClient = null; try { terminalClient = new TerminalClient("127.0.0.1", Integer.parseInt(System.getProperty("port", "7788"))); TS.P_GUI_ONLY = true; if (!TS.P_USE_MSTSC) { String mode = terminalClient.ask(ClientWorkerThread.GETMODE).toLowerCase(); if (mode.contains("mstsc=true")) { TS.P_USE_MSTSC = true; } } } catch (IOException e) { //ignore, seems this is 1st TS instance or port is used by another app } finally { if (terminalClient != null) { try { terminalClient.close(); } catch (IOException ignore) { } } } // LOGGING_CONFIG_XML = JFX_HOME_CONFIG + File.separatorChar + (P_GUI_ONLY ? "gui_" : "") + "logging.xml"; if (!new File(LOGGING_CONFIG_XML).exists()) { try { String loggingXml = ResourceReader.getClassResourceReader(TerminalServer.class, true).getProperty("logging.xml"); if (P_GUI_ONLY) { loggingXml = loggingXml.replace("jfx_term.log", "gui_jfx_term.log"); } writeFile(LOGGING_CONFIG_XML, loggingXml.replace("./jfx_term/", JFX_HOME.replace('\\', '/') + "/").getBytes()); } catch (IOException e) { e.printStackTrace(); } } // if (!new File(JMX_CONFIG_XML).exists()) { try { writeFile(JMX_CONFIG_XML, ResourceReader.getClassResourceReader(TerminalServer.class).getProperty("mbean_config.xml").getBytes()); } catch (IOException e) { e.printStackTrace(); } } // DOMConfigurator.configureAndWatch(LOGGING_CONFIG_XML); LOGGER = Logger.getLogger(TS.class); // String termDirLn = System.getenv("SystemDrive") + "\\." + System.getProperty("port", "7788"); File tmpDirLnk = new File(termDirLn); if (tmpDirLnk.exists() && !P_GUI_ONLY) { String linkDir = getLinkDir(tmpDirLnk); if (linkDir == null || !new File(getTargetTermDir()).equals(new File(linkDir))) { tmpDirLnk.delete(); } } if (!tmpDirLnk.exists()) { tmpDirLnk.delete(); ExternalProcess mklink = new ExternalProcess("cmd", "/C", "mklink", "/J", termDirLn, getTargetTermDir()); try { mklink.run(); TERM_DIR = tmpDirLnk.exists() ? termDirLn : null; } catch (Exception e) { e.printStackTrace(); } } else { TERM_DIR = termDirLn; } // tsConfig = new TSConfig();}
要注意几个路径:
C:\ProgramData\nj4x\bin
目前看来只包含PSUtils_x64.dll
文件。
C:\Users\Micheal\jfx_term
文件夹比较复杂,总之就是包含了jfx所有的配置文件,如图:
chr
文件夹是空的,不知道干嘛的。 config
文件夹中,是一些配置文件,logging.xml
是log4j
的配置文件。配置方式都不一样,战斗民族;mbean-config.xml
文件是网络配置文件,下次博客再讲;ts_config.xml
文件里面就有一句话,不知道什么意思:
Properties removed at Thu Nov 17 16:28:14 CST 2016
log
文件夹中是日志。
srv
文件夹中是各个交易商的配置文件,这个和交易商有关:
zero_term
文件夹中存放着一份mt4
的程序。没有提到的文件夹就是空的。
C:\Users\Micheal\.jfx_terminals
这个文件夹中存放的是mt4的程序,每一个账号,每一个交易商的不同服务器都会创建一份新的程序,应该是配置文件不同。
TS.scheduledExecutorService
分析。
作者在Ts
类中定义了一个scheduledExecutorService
:
/** * The constant scheduledExecutorService. */ public static ScheduledExecutorService scheduledExecutorService = java.util.concurrent.Executors.newScheduledThreadPool(64);
据我统计,有10个地方用到了这个scheduledExecutorService
,也就是至少有10个定时任务。
1.MT4程序连接监控,监控有没有网络连接到MT4程序。这个里面有两个,不知道为什么里面还有一个,这两个任务是一样的:
if (mt4Module.isCheckRequired && mt4Module.checkFuture == null) { mt4Module.checkFuture = scheduledExecutorService.schedule(new Runnable() { @Override public void run() { Mt4Module mt4Module = incomingConnectionModule.get(tp.strategy); if (mt4Module.isCheckRequired) { try { String status = tp.checkTerminal(clientWorker); if (status.startsWith("OK")) { mt4Module.checkFuture = scheduledExecutorService.schedule(this, 15, TimeUnit } else { mt4Module.checkFuture = null; incomingConnectionError.put(tp.strategy, status); } } catch (NoSrvConnection e) { String m = "No connection to server: " + e; TS.LOGGER.error(m, e); incomingConnectionError.put(tp.strategy, m); } catch (SrvFileNotFound e) { String m = "SRV file not found: " + e; TS.LOGGER.error(m, e); incomingConnectionError.put(tp.strategy, m); } catch (MaxNumberOfTerminalsReached e) { String m = "Reached max number of terminals: " + e; TS.LOGGER.error(m, e); incomingConnectionError.put(tp.strategy, m); } catch (InvalidUserNamePassword e) { String m = "Invalid user name or password: " + e; TS.LOGGER.error(m, e); incomingConnectionError.put(tp.strategy, m); } catch (TerminalInstallationIsRequired e) { String m = e.getMessage(); TS.LOGGER.error(m, e); incomingConnectionError.put(tp.strategy, m); } catch (Throwable e) { e.printStackTrace(); String m = "Unexpected error: " + e; TS.LOGGER.error(m, e); incomingConnectionError.put(tp.strategy, m); } } } }, 15, TimeUnit.SECONDS);
2.磁盘监控,从这个就能看出对磁盘的操作
//这个磁盘监控每60s进行一次6spaceMonitoringJob = scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() {}},5, 60, TimeUnit.SECONDS);
3.监控新的session
TS.scheduledExecutorService.submit(newSessionCreator);
4.监控终端变化
TS.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { public void run() { if (Log4JUtil.isConfigured() && TS.LOGGER.isTraceEnabled()) { TS.LOGGER.trace("Timer: update session's load level"); } if (TS.P_USE_MSTSC) { try { loadExistingSessions(); } catch (Throwable e) { TS.LOGGER.error("Error loading current sessions", e); } } updateTerminals(); }}, 30, 60, TimeUnit.SECONDS);
4.监控google网盘设置变化
TS.scheduledExecutorService.schedule(new Runnable() { @Override public void run() { int nextSyncInSeconds = nextSyncInSeconds(); if (nextSyncInSeconds > 0) { TS.scheduledExecutorService.schedule(this, 1/*Math.min(nextSyncInSeconds, 5)*/, TimeUnit.SECONDS); return; } try { checking = true; boolean somethingDone = false; for (DownloadSetup ds : downloads.values()) { somethingDone |= ds.run(); } if (somethingDone) { TS.LOGGER.info("Google Drive: All Downloads Complete!"); } checking = false; } finally { lastSyncTime = System.currentTimeMillis(); TS.scheduledExecutorService.schedule(this, 1/*Math.max(nextSyncInSeconds(), 5)*/, TimeUnit.SECONDS); } }
5.日志压缩任务
ScheduledFuture schedule = TS.scheduledExecutorService.schedule(new Runnable() { public void run() { if (Log4JUtil.isConfigured() && TS.LOGGER.isInfoEnabled()) { TS.LOGGER.info("Timer: Stop terminal " + processName); } ts.killProcess(processName, true); }}, TS.JFX_TERM_IDLE_TMOUT_SECONDS, TimeUnit.SECONDS);TS.terminations.put(processName, schedule);
7.更新终端状态任务
TS.scheduledExecutorService.schedule(new Runnable() { public void run() { ts.updateTerminals(); }}, 2, TimeUnit.SECONDS);
8.监控GUI中的网盘设置项变化
TS.scheduledExecutorService.schedule(new Runnable() { @Override public void run() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { if (!text.equals(updateInterval.getText())) { return; } if (text.replace("0", "").length() > 0) { try { if (Integer.parseInt(text) <= 0) { JOptionPane.showMessageDialog(null, "Error: Please enter number bigger than 0", "Error Message", JOptionPane.ERROR_MESSAGE); } else { TS.setConfigurationValue("cloud_update_interval", text); TS.LOGGER.info("Google Drive update interval set to " + text + " sec"); } } catch (NumberFormatException e) { JOptionPane.showMessageDialog(null, "Error: Please enter valid number >0", "Error Message", JOptionPane.ERROR_MESSAGE); } } } catch (HeadlessException e) { TS.LOGGER.error("At GUI", e); } } }); }}, KEY_PRESSED_ACTION_DELAY, TimeUnit.MILLISECONDS);
9.貌似是界面的刷新
TS.scheduledExecutorService.schedule(new Runnable() { @Override public void run() { SwingUtilities.invokeLater(x); }}, 1, TimeUnit.SECONDS);
10.监控正在进行连接操作的终端
TS.scheduledExecutorService.schedule(new Runnable() { public void run() { 将正在连接的终端列表下这个map final HashMaptp = new HashMap<>(); synchronized (connectionsInProgress) { for (TerminalParams p : connectionsInProgress) { tp.put(p.getTerminalDirPathName(), p); } } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { synchronized (connectionsInProgressTable) { DefaultTableModel model = (DefaultTableModel) connectionsInProgressTable.getModel(); Vector dataVector = model.getDataVector(); int sz = dataVector.size(); boolean allInPlace = sz > 0; for (int row = 0; row < sz; row++) { String path = (String) model.getValueAt(row, model.findColumn(PATH_COLUMN)); TerminalParams terminalParams = tp.get(path); if (terminalParams == null) { allInPlace = false; break; } else { model.setValueAt( // " " + ((System.curre 设置连接的时间 new Integer((int) ((System.currentTimeMillis() - terminalParams.start.getT row, model.findColumn(DURATION_COLUMN) ); } } 设置好时间之后开始显示 if (!allInPlace) { dataVector.clear(); // new Object[]{"Broker", "Account", "Path", "Start Time", "Duration (s)"} for (Map.Entry p : tp.entrySet()) { Vector row = new Vector(); TerminalParams terminalParams = p.getValue(); row.add(terminalParams.getSrv()); row.add(terminalParams.getUser()); row.add(new SimpleDateFormat("MMM d, HH:mm:ss").format(terminalParams.start)); row.add( //" " + ((System.currentTimeMillis() - terminalParams.start.getTim new Integer((int) ((System.currentTimeMillis() - terminalParams.start.getT ); row.add(terminalParams.getTerminalDirPathName()); // dataVector.add(row); } model.fireTableDataChanged(); } //这个才是真真的显示 SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { TitledBorder border = (TitledBorder) connectionsInProgressPanel.getBorder(); border.setTitle(tp.size() > 0 ? (tp.size() == 1 ? "1 Connection In Progress" : "" + tp.size() + " Co : "Connections In Progress" ); connectionsInProgressPanel.repaint(); } catch (Exception e) { TS.LOGGER.error("At GUI", e); } } }); } } catch (Exception e) { TS.LOGGER.error("At GUI", e); } } }); //不知道这句话是干什么用的,为什么要this,this指的是这个new runable类,不知道加不加有什么区别 TS.scheduledExecutorService.schedule(this, 1, TimeUnit.SECONDS); }}, 1, TimeUnit.SECONDS);
11.监控日志显示列表的变化
//监听日志限制输入框的输入变化TS.scheduledExecutorService.schedule(new Runnable() { @Override public void run() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { if (!text.equals(limitRowsField.getText())) { return; } if (text.replace("0", "").length() > 0) { try { if (Integer.parseInt(text) <= 0) { JOptionPane.showMessageDialog(null, "Error: Please enter number bigger than 0", "Error Message", JOptionPane.ERROR_MESSAGE); } else { applyNewRowsLimit(); } } catch (NumberFormatException e) { JOptionPane.showMessageDialog(null, "Error: Please enter valid number bigger than 0", "Error Message", JOptionPane.ERROR_MESSAGE); } } } catch (HeadlessException e) { TS.LOGGER.error("At GUI", e); } } }); }}, KEY_PRESSED_ACTION_DELAY, TimeUnit.MILLISECONDS);
此外,还有其他的任务,后面会提到