[{TableOfContents title=''}]

!!Tomcat 服务器监控软件 [{$latestVersion}] by beansoft@126.com
帮我自动重启无响应的 Tomcat(实际情况哦, 所有的程序都是有 BUG 的), 定时重启 Tomcat.//
后记: 推荐使用开源的Tomcat监控工具 [LambdaProbe|http://www.lambdaprobe.org/] 来全面了解 Tomcat Server 状况, 再配合本站的自动重启软件, 人工+自动+定时重启, 万无一失! 截屏: [http://www.lambdaprobe.org/d/screenshots.shtml]

!! 系统需求
JDK 1.4 或更高//
Tomcat 4 或更高(BAT方式安装)//
Windows 2K 或更高 或者 Linux (已测试过 RedHat 9 图形模式)

!!下载(含源码):
!最新版[{$latestVersion}]
[TomcatServerMonitor1_3.zip|http://www.blogjava.net/Files/beansoft/TomcatServerMonitor1_3.zip] 1.19 MB

!1.2版
[TomcatServerMonitor1_2.zip|http://www.blogjava.net/Files/beansoft/TomcatServerMonitor1_2.zip] (Windows, Linux RedHat 9)1155KB

!1.1版
[TomcatServerMonitor1_1.zip|http://www.blogjava.net/Files/beansoft/TomcatServerMonitor1_1.zip] (老版本, Windows Only)889KB
!1.2版EXE文件
[tsm_launcher.zip|http://www.blogjava.net/Files/beansoft/tsm_launcher.zip] (Windows EXE 启动文件,可选)10KB

!! 快速开始
假设你要在 Windows 下开始监控, 请按照下列说明进行, 只需要记事本即可.

1. 修改 conf/monitor.properties
[{Code lang=properties

######################################################
# 监控的通用选项配置文件
#Server Monitor Configuration File
######################################################
#
# 报警邮件接收人地址, 多人以分号隔开, 发送邮件的账户请修改 MailSender.ini
AlertEmails=username@test.com
# 需要探测的网页的地址
WebPageUrl=http/://localhost/:8080/
# 网页连接请求超时时间(单位: 毫秒)
DefaultTimeOut=100
# 需要重启服务器的网页连接请求超时时间(单位: 毫秒), 也就是说读取页面超过了这
# 个时间就认为是故障了, 然后就会重启服务器
RestartTimeOut=2000
# 监控探测时间间隔, 单位: 分钟
QueryTimeInternal=1
# 定时重启的第一次执行时间(格式: HH:mm:ss), 如果为0或者空, 则设置为 00:00:00
# 改动这个值需要重启应用才能生效
ScheduledRestartTime=01/:00/:00
# 定时重启时间间隔, 单位:分钟, 如果 <= 0, 则不需要定时重启
# 默认值是1440(60*24)分钟(一天), 改动这个值需要重启应用才能生效
ScheduledRestartPeriod=1440
}]

2. 修改 conf/tomcat_windows.properties 来指定 Tomcat 安装路径等信息
[{Code lang=properties

######################################################
# servermon.launcher.plugin.TomcatWindowsLauncher 类所使用的
# Windows 版本的 Tomcat 配置文件
# Laucher property file for servermon.launcher.plugin.TomcatWindowsLauncher
######################################################
#
# 服务器名称(可以任意取)
AppServerName=Tomcat
#服务器根目录(不含bin等路径)
AppServerHome=E/://_PortableJava//jakarta-tomcat-5.0.30
#服务器启动脚本名称
StartupScript=startup.bat
#服务器关闭脚本名称
StopScript=shutdown.bat
#默认等待关闭时间(秒为单位),即执行StopScript后等待的时间
ShutDownWaitTime=30
}]

3. 修改 conf/MailSender.ini 来启用邮件发送功能, 这一步可选, 不想发邮件忽略即可;

4. 配置好 Tomcat  BAT 方式启动所需的 JAVA_HOME 等变量, 如果已经配置好了, 忽略即可;

5. 运行 AutoMonitorWithGUI.bat 开始工作, 测试第一次监控, 测试成功后可以直接运行 TomcatMonitor.exe 开始自动后头监控;

6. 命令行参数说明://
__-autostart__ 启动后自动开始监控;//
__-nogui__ 不显示托盘图标和主窗体, 以隐藏的方式运行;//
__不带参数__ 启动后等待用户点击开始监控的菜单项后才开始监控.//

7. 可以用 javaw.exe 来避免显示 DOS 窗口.

!! Windows 下监控/重启 Tomcat/Weblogic 服务
2007-02-12//
用 Tomcat 的安装程序(EXE版本)安装后, 记得选中安装服务, 例如 jakarta-tomcat-5.0.30.exe, 安装后再系统控制面板里可以看到服务显示名为 tomcat5. 这时候修改 conf/tomcat_windows.properties 的内容如下所示即可用服务的方式开始,停止 Tomcat 了.

[{Code lang=properties

######################################################
# servermon.launcher.plugin.TomcatWindowsLauncher 类所使用的
# Windows 版本的 Tomcat 配置文件 - 服务模式
# Laucher property file for servermon.launcher.plugin.TomcatWindowsLauncher
######################################################
#
# 服务器名称(可以任意取)
AppServerName=Tomcat 5 Service
#服务器根目录(不含bin等路径)
AppServerHome=D://Program Files//Apache Software Foundation//Tomcat 5.0
#服务器启动脚本名称
StartupScript=net start tomcat5
#服务器关闭脚本名称
StopScript=net stop tomcat5
#默认等待关闭时间(秒为单位),即执行StopScript后等待的时间
ShutDownWaitTime=30
}]

上面的 tomcat5 即为服务的名称.

如果是 Weblogic, 同样的也会有个服务名称, 例如 beawls8, 其实这时候服务已经和 Java 没有关系了. 所要注意的是 AppServerHome 下面必须有个 bin 目录, 否则命令执行会失败, 因为原来实现的时候的起始目录是 $AppServerHome/bin, 也就是说这时候的 AppServerHome 的真实路径不会影响服务的执行, 完全可以设置成 JDK 的目录, 例如 d:/jdk1.5.0 .

原理很简单, 就是 net 命令可以用来开始和停止系统服务.//
开始服务: net start 服务名 //
停止服务: net stop 服务名

!! TODO
进一步分离, 加入事件监听机制?//

!!TomcatServerMonitor 1.3 的 UML 图
在线浏览: [http://beansoft.java-cn.org/ajax/tomcatmonitor/]
下载后离线浏览:

[TomcatServerMonitorUML.7z.zip|http://www.blogjava.net/Files/beansoft/TomcatServerMonitorUML.7z.zip] 126KB

详细请关注 servermon 包的内容. 其它的都是工具类.



注: 需要 JRE (Applet) 和 SVG 查看器(如 Adobe SVG Viewer).

开发工具: Together Community Edition for Eclipse v6.3(免费)

需要 Eclipse 3.0

下载: [ftp://ftpd.borland.com/download/together/tec63/TG_ECLIPSE_6_3_WIN_SETUP.ZIP]

!!更新历史//

!2007.02.10
在主界面里加入了更改配置文件的菜单, 然后启动可视化的属性文件编辑器, 这样所有工作都可以在本软件中完成; 参考[1];//
新增定时重启功能, 可以指定第一次重启的时间以及每次重启的时间间隔.//
当然定时重启可以使用 Windows 的计划任务或者 Linux 下的 [CronTab] 来完成.

!2006.12.17
1. Linux 版本, 测试过的是 RedHat 9 中文版.

要启用 Linux 支持, 首先记得给3个.sh文件执行权限, 自行配置好 Tomcat 的启动时候需要的 CATALINA_HOME 变量. 配置文件请关注:

conf/appserverplugin.properties, 修改文件内容为:

[{Code lang=properties

######################################################
# Tomcat 5, Linux
# 要使用的服务器启动类
AppServerLauncherPlugIn=servermon.launcher.plugin.TomcatLinuxLauncher
# 使用的启动参数文件
AppServerLauncherPlugIn.ConfigFile=tomcat_linux.properties
}]

然后修改 conf/tomcat_linux.properties 中的文件即可, 该文件中的参数说明:

[{Code lang=properties

# 服务器名称(可以任意取)
AppServerName=Tomcat
#服务器根目录(不含bin等路径)
AppServerHome=/opt/tomcat-5.0.30-test
#服务器启动脚本名称
StartupScript=startup.sh
#服务器关闭脚本名称
StopScript=shutdown.sh
#默认等待关闭时间(秒为单位),即执行StopScript后等待的时间
ShutDownWaitTime=30
}]


2. 引入插件机制, 只要您实现 servermon.launcher.IAppServerLauncher, 即可监控各个版本的服务器. 最简单的做法可以参考代码 servermon.launcher.plugin.TomcatWindowsLauncher, 基本上只需要实现

public void startServer();//
public void stopServer();

即可. 目前实现的有 Tomcat 的 Windows, Linux 版本, 以及 Weblogic 9 的 Windows 版本, Weblogic 8 的 Windows 版本支持正在开发中, 初步考虑用直接执行 java 命令的方式来做, 因为 Wls 8 缺省不带 shutdown 的脚本.

3. 纯命令行模式支持, 适用于 Linux 文本方式下, 停止监控请使用 ps 命令然后kill监控进程. 不过目前尚未在 Linux 文本模式下测试过, 请测试过的朋友反馈. 相关文件: AutoMonitorNoGUI.sh, AutoMonitorNoGUI.bat

Linux 图形模式下的截屏:

[{Image src = 'tomcat_monitor_redhat9_gui.png'}]

!2006.12.11
增加了一个 EXE 格式的启动文件, 带源码.//
解压缩, 把 EXE 文件复制到 TomcatServerMonitor 目录下.//
这对不喜欢用BAT格式启动的朋友们是个好消息, 也期待对希望自己做个 EXE 启动文件的兄弟们有所帮助. 解压缩后如果不需要源码只留下 TomcatMonitor.exe 10.0 KB 即可, 它执行了下列命令://
javaw -cp ./jdic/jdic.jar;.;./classes/;lib/activation.jar;lib/commons-dbutils-1.0.jar;lib/jspsmartupload.jar;lib/mail.jar;lib/servlet.jar; servermon.MonitorFrame -autostart//
因此需要事先设置 javaw.exe 在类路径中. 它相当于自动后台运行监控程序.bat 的命令行版本.//
本项目源码修改自 FreeMind [http://freemind.sourceforge.net/|http://freemind.sourceforge.net/] 的 EXE 启动文件源码, 使用 Dev-C++ 5 (一个免费 C++ 开发环境 [http://www.bloodshed.net/devcpp.html|http://www.bloodshed.net/devcpp.html]) 编译, 并用 ASPACK 压缩(这是商业软件...俺只不过想让EXE变的更小一点, 没压缩的时候是 20KB). 安装后打开项目文件编译即可.//
!2006.12.10
更新:

1. 增加托盘图标功能, 最小化主窗口到托盘, 便于监控同一机器上的多个 Server(需要手工复制多份主程序)//
2. 定位监控的 Tomcat 目录, 使用内置 IE 浏览监控页面功能(在托盘菜单中)//
3. 自动监控(用 -autostart 参数即可), 使用启动文件 "自动后台运行监控程序.bat" 可以不带 DOS 窗口启动并自动开始监控//
4. 在托盘气泡中显示报警和出错信息, 便于随时掌握 Server 状态, 并根据监控状态显示不同的状态图标//
4. 小改动://
更改按钮和菜单默认字体大小, 使更容易看清楚;//
发送邮件的配置文件路径移动到 conf 下面, 便于手工修改;//
监控页面的超时单位改为毫秒

!2005.07.09
1.0 版本推出, 用于监控远程托管的 Tomcat 网站服务器, 只能用来监控 Windows 平台下面的 Tomcat.
----

!!使用说明(请参考更新历史)

本软件每隔固定时间就监测一次给定的 Web 站点的页面是否可以访问, 如果请求超时或者失败, 就发送彩E/邮件到给定的手机/信箱(手机的话必须自行申请了手机邮箱, 例如彩E, 和普通邮箱一样发送), 进行通知, 在系统托盘区显示气泡进行警告,//
并尝试根据配置的 Tomcat 服务器路径重启本机的 Tomcat 进程, 首先尝试执行 "shutdown.bat", 然后等待 30 秒后执行 "startup.bat" 完成操作.//
更有定时重启的功能, 可以指定第一次重启的时间以及每次重启的时间间隔.//

首先请设置要监测的内容和参数, 然后再点击按钮"开始监测"即可.//
可以点击 "浏览监测日志" 查看以往监控结果.//
点击 "立即监测" 可以查看当前的网页访问情况.

软件启动后, 在工具栏上点击按钮"设置"就可以修改所有的监控和邮件等参数了.

目录结构//
/src  源代码目录//
/classes 编译后的类文件//
/conf  系统配置文件参数所在地//
    appserverplugin.properties 服务器启动插件类//
    MailSender.ini 发邮件配置//
    monitor.properties 监控配置, 可以使用图形界面来修改(注: TomcatHomePath 变量已经不再此处设置)//
    其它: 启动插件对应的配置文件   //
/lib  用到的第三方类库//
/jdic  Sun 提供的 JDIC(JDesktop Integration Components, [https://jdic.dev.java.net/|https://jdic.dev.java.net/]) 组件(托盘, 浏览器), Win, Linux 版本//
/images  图片//
/logs  监控日志文件目录//
/AutoMonitorNoGUI.bat, AutoMonitorNoGUI.sh 不带图形窗口自动监控//
/AutoMonitorWithGUI.bat, AutoMonitorWithGUI.sh 带图形窗口自动监控//
/ManMonitorWithGUI.bat, ManMonitorWithGUI.sh  带图形窗口人工启动监控//
/COPYING 许可协议文件

!!许可协议: GPL
请阅读 [LICENSE]//
开发工具: Eclipse + Jigloo//
后记: 用这个软件可以监控 Weblogic 9(Windows版本), 请修改配置文件 conf/appserverplugin.properties 和 conf/weblogic9_windows.properties, 因为 Weblogic 9 也是有两个启动脚本的. Weblogic 8 的监控功能正在开发中.

!! 实现
首先定时重启我用 java.util.Timer, 当前版本的主要流程位于类 servermon.MonitorThread.

[{QuickJavaDocPlugin

/*
* @(#)MonitorThread.java 1.00 2005-7-19
*
* Copyright 2005 beansoft studio. All rights reserved.
* PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package servermon;

import java.io.InputStream;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;


import servermon.launcher.AppServerPluginManager;
import servermon.launcher.IAppServerLauncher;
import servermon.persistance.ConfigurationManager;
import servermon.persistance.LogManager;
import servermon.tray.TrayIndicator;
import servermon.valuebean.Configuration;
import beansoft.jsp.StringUtil;
import beansoft.smtp.MailSender;
import beansoft.util.thread.ThreadPool;

/**
* MonitorThread, 监控器线程类, 处理所有监控操作.
*
* @author BeanSoft
* @version 1.3 2007-02-11
* version 1.00 2005-7-19
*/
public class MonitorThread extends Thread {
/** 暂停标志 */
private boolean paused = false;
/** 邮件发送器 */
private MailSender sender = new MailSender();
/** 线程池 */
private ThreadPool threadPool = new ThreadPool(new ThreadGroup("Tomcat 监控线程"));

/** 托盘指示图标 */
TrayIndicator tray = TrayIndicator.getInstance();

/** If all data is read from url done successfully,
* then this value is set to true
* Added at 2006-12-27
*/
protected static boolean dataReaded = false;
/**
* 监控时尝试读取服务器页面所用的 HTTP 输入流.
*/
protected static InputStream httpIn = null;

/**
* 是否正在重启操作中, 使用此变量避免监控线程和定时重启线程同时进行重启操作.
* @since 1.3
*/
protected static boolean isRestartInProgress = false;

/**
* 执行一次监控操作并返回日志报告.
* @return 操作结果日志
*/
public String doMonitor() {
String msg = "";
final Configuration cfg = ConfigurationManager.getConfiguration();

final long startTime = System.currentTimeMillis();

dataReaded = false;

// Add time out restart support, such as server is core but not down
final Timer timer = new Timer();

// 检查数据读取是否超时的定时任务
TimerTask checkReadTimeTask = new TimerTask() {
public void run() {
System.out.println( System.currentTimeMillis() - startTime);

if(!dataReaded) {
long deltaTime = System.currentTimeMillis() - startTime;

try {
httpIn.close();
} catch (Exception e) {
// TODO: handle exception
}

String msg = StringUtil.dateToChineseString(new java.util.Date()) + " : " +
"[错误] 访问服务器页面超过最大限时, 用时" + deltaTime + "毫秒, 即将重启 Tomcat/n";
tray.displayErrorTomcat(msg);
dataReaded = true;
// 重新启动 Tomcat
restartTomcat();
// 发送报警邮件
sendMail("服务器出错", msg, cfg);
} else {
this.cancel();
timer.cancel();
}
}
};

try {

URL url = new URL(cfg.getWebPageUrl());
// Schedule the max timeout reader check task
timer.schedule(checkReadTimeTask, cfg.getRestartTimeOut());

int data;
InputStream in = url.openStream();
httpIn = in;

while((data = in.read()) != -1) {
// if(dataReaded) {
// break;
// }
}


try {
in.close();
} catch (RuntimeException e1) {
e1.printStackTrace();
}

dataReaded = true;

long deltaTime = System.currentTimeMillis() - startTime;

if(cfg.getDefaultTimeOut() != 0 && deltaTime > cfg.getDefaultTimeOut()) {
msg += StringUtil.dateToChineseString(new java.util.Date()) + " : " +
"[警告] 访问服务器页面超时, 用时" + deltaTime + "毫秒/n";

tray.displayWarningTomcat(msg);

// 发送报警邮件
sendMail("服务器可能故障", msg, cfg);
} else {
msg += StringUtil.dateToChineseString(new java.util.Date()) + " : "
+ "[正常] 访问" + cfg.getWebPageUrl() + "用时" + deltaTime + "毫秒/n";
tray.displayRunningTomcat(msg);
}

} catch (Exception e) {
long deltaTime = System.currentTimeMillis() - startTime;
msg += StringUtil.dateToChineseString(new java.util.Date()) + " : " +
"[错误] 无法访问服务器页面, 用时" + deltaTime + "毫秒, 即将重启 Tomcat/n";
tray.displayErrorTomcat(msg);
dataReaded = true;
// 重新启动 Tomcat
restartTomcat();
// 发送报警邮件
sendMail("服务器出错", msg, cfg);
}

LogManager.writeLog(msg);

return msg;
}

/**
* 初始化重启的计划任务.
* 如果给定的重启的时间为空或者<= 0, 就不启动重启定时任务.
* @since 1.3
*/
protected void initScheduledRestartTimer() {
Configuration cfg = ConfigurationManager.getConfiguration();

// Parse scheduled time
SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");

Date today = new Date();

try {
LogManager.writeLog("第一次重启的时间为:" + cfg.getScheduledRestartTime() + "/n");

Date scheduledDate = df.parse(cfg.getScheduledRestartTime());

// TODO Fix these warnings
today.setHours(scheduledDate.getHours());
today.setMinutes(scheduledDate.getMinutes());
today.setSeconds(scheduledDate.getSeconds());

System.out.println(today.toLocaleString());
} catch (java.text.ParseException e1) {
today.setHours(0);
today.setMinutes(0);
today.setSeconds(0);
}

if(cfg.getScheduledRestartPeriod() > 0) {
LogManager.writeLog("重启的时间间隔为:" + cfg.getScheduledRestartPeriod() + "分钟/n");

Timer scheduledRestartTimer = new Timer();
TimerTask scheduledRestartTimerTask = new TimerTask() {

public void run() {
String msg = StringUtil.dateToChineseString(new java.util.Date()) + " : " +
"[警告] 即将定时重启服务器/n";

tray.displayWarningTomcat(msg);
LogManager.writeLog(msg);

if(!isPaused()) {
restartTomcat();
}
}
};

scheduledRestartTimer.schedule(scheduledRestartTimerTask, today,
cfg.getScheduledRestartPeriod() * 1000 * 60);// 分钟为单位
}
}

/**
* 监控的主循环流程.
* 首先初始化定时重启的任务;
* 接着定时进行监控, 如果访问页面超时, 就发送邮件报警, 并在系统
* 托盘指示状态;
* 如果打开访问时间太久, 就尝试重启服务器, 注意这里有可能会造成
* 死循环;
* 所有的参数都从配置文件中读取.
* TODO 是否用 Spring 显的更灵活???
*
* @see #initScheduledRestartTimer()
* @see #doMonitor()
*/
public void run() {
initScheduledRestartTimer();

while(true) {

// 未暂停时执行监控操作
if(!isPaused()) {
doMonitor();
} else {
tray.displayPausedTomcat("监控已暂停");
}

Configuration cfg = ConfigurationManager.getConfiguration();

// 默认休眠 10 分钟后检测
try {
if(cfg.getQueryTimeInternal() > 0) {
Thread.sleep(cfg.getQueryTimeInternal() * 60 * 1000);
} else {
Thread.sleep(10 * 60 * 1000);
}
} catch (Exception e) {
// e.printStackTrace();
}
}
}

/**
* 发送邮件到监控人的信箱.
* @param subject 信件主题
* @param msg 信件内容
* @param cfg 配置参数
*/
private void sendMail(String subject, String msg, Configuration cfg) {
String mailCfg = MailSender.readConfigurationString();


String from = null;// 发件人地址

try {
Properties props = new Properties();
props.load(new java.io.ByteArrayInputStream(mailCfg.getBytes()));

from = props.getProperty("username", "beansoft@beansoft studio");
} catch (Exception e) {
// TODO: handle exception
}

sender.setFrom(from);

String toStr = cfg.getAlertEmails();

StringTokenizer token = new StringTokenizer(toStr, ";,");

while(token.hasMoreElements()) {
String value = token.nextElement().toString();

sender.setTo(value);
sender.setSubject(subject + new Date().toLocaleString());
sender.setBody(msg);

sender.sendMail();
}
}

/**
* 重新启动 Tomcat 服务器. 首先执行 shutdown.bat 关闭服务器, 等待30秒后运行 startup.bat.
* @param cfg 服务器监控配置参数
*/
private void restartTomcat() {
if(isRestartInProgress == true) {
return;
}

final IAppServerLauncher launcher = AppServerPluginManager.getAppServerLauncher();

Runnable run = new Runnable() {
public void run() {
isRestartInProgress = true;
launcher.stopServer();

// 等待一段时间来确保服务器已经停止
try {
Thread.currentThread().sleep(launcher.getShutDownWaitTime() * 1000);
} catch (Exception e) {
}

// 启动服务器
launcher.startServer();

isRestartInProgress = false;
}
};

threadPool.execute(run, "Restart " + launcher.getAppServerName());
}


/**
* @return returns 是否暂停线程.
*/
public boolean isPaused() {
return paused;
}

/**
* @param paused The 是否暂停线程 to set.
*/
public void setPaused(boolean paused) {
this.paused = paused;
}

}
}]

!!截屏
[{Image src = 'tomcat_monitor_main.gif'}]//
图1: 主界面//
[{Image src = 'TomcatMonitor-Tray.png'}]//
图2:气泡和托盘图标//
[{Image src = 'tomcat_monitor_trays.png'}]//
图3: 托盘图标状态//
[{Image src = 'tomcat_monitor.gif'}]//
图4: 启动和运行动画(Windows 2003 Server, English)//
[propedit.gif]//
图5: [#1]属性文件编辑器


----

     AutoMonitorNoGUI.sh脚本有问题,Linux下AutoMonitorNoGUI.sh脚本好像使用Windows的配置文件。希望出一个Linux下稳定的
无GUI版本,必定用tomcat的还是Linux服务器多,且大多数无GUI界面。
      建议程序写成客户端/服务器模式,因为很少有人爬在Tomcat服务器上盯着服务运行!可以在Tomcat所在服务器上部署服务器端
,并发送Tomcat监控信息到客户端。这样相对较好。

--[220.231.31.203|mailto:toakee@iwmusic.com], 27-Feb-2007


----

开源软件, 稍微看看代码修改一下即可实现, 大部分的监控代码已经写好了, 你提的这些算是远程记录 log 的功能, 可以考虑 log4j. 正因为是无担保无技术支持的开源软件, 所以你的这些需求我暂时还没打算做.

--[beansoft|], 06-Mar-2007
 
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐