nj4x-ts简介
nj4x-ts是NJ4X项目中的终端服务器,参考我的上篇博客NJ4X源码阅读分析笔记系列(一)——项目整体分析中的图:
方框中就是nj4x-ts的角色。
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 java
static {
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,不知道为什么,现在没有计划研究专家系统,以后有可能。
//部署专家系统webservice
deployEaWs(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
文件里面就有一句话,不知道什么意思:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>Properties removed at Thu Nov 17 16:28:14 CST 2016</comment>
</properties>
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进行一次6
spaceMonitoringJob = 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.日志压缩任务
6.断线终端的关闭任务
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 HashMap<String, TerminalParams> tp = 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(
// "<html><b>" + ((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<String, TerminalParams> 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(
//"<html><b>" + ((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);
此外,还有其他的任务,后面会提到