Apache Commons Net 3.13.0 快速入门与集成示例
以 SubnetUtils 最小验证为起点,逐个实测 FTPClient、SMTPClient、TelnetClient 与真实服务的联动,给出 Apache Commons Net 3.13.0 的快速上手与集成路径
Apache Commons Net 3.13.0 快速入门与集成示例
目标:10 分钟跑通第一个 Commons Net 程序,再依次与真实 FTP 服务器、SMTP 服务、Telnet 服务完成端到端联动。已在 Ubuntu 24.04.5 LTS(x86_64)+ OpenJDK 17.0.20.1 + Maven 3.9.16 实测通过。
一、环境信息
- OS:Ubuntu 24.04.5 LTS
- 架构:x86_64
- JDK:OpenJDK 17.0.20.1
- 构建工具:Apache Maven 3.9.16
- 组件版本:Apache Commons Net 3.13.0(传递依赖 commons-io 2.21.0)
- Maven 坐标:
commons-net:commons-net:3.13.0 - 联动服务:vsftpd 3.0.5-0ubuntu3.1、python3 3.12.3 自研 SMTP sink、netcat-openbsd 1.226
- 工作目录:
/root/commons-net-demo
本文实验机同时充当三个服务的宿主:FTP 服务器(vsftpd,127.0.0.1:21)、SMTP 演示服务(127.0.0.1:2525)、Telnet 演示服务(127.0.0.1:2323),客户端与服务端同机直连,回显均来自真实网络交互。
二、10 分钟最小可用
创建工程目录与 POM:
mkdir -p /root/commons-net-demo/src/main/java
cd /root/commons-net-demo
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>commons-net-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>8</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.13.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
</plugins>
</build>
</project>
第一个程序不碰网络服务,用工具类 SubnetUtils 解析 CIDR 网段——这是 Commons Net 里除协议客户端外最常用的一族工具:
import org.apache.commons.net.util.SubnetUtils;
import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
public class SubnetDemo {
public static void main(String[] args) {
SubnetUtils su = new SubnetUtils("192.168.56.0/26");
SubnetInfo info = su.getInfo();
System.out.println("[不含网络号/广播号] low=" + info.getLowAddress()
+ " high=" + info.getHighAddress()
+ " 可用地址数=" + info.getAddressCountLong());
su.setInclusiveHostCount(true);
info = su.getInfo();
System.out.println("[含网络号/广播号] low=" + info.getLowAddress()
+ " high=" + info.getHighAddress()
+ " 可用地址数=" + info.getAddressCountLong());
System.out.println("network=" + info.getNetworkAddress()
+ " netmask=" + info.getNetmask()
+ " broadcast=" + info.getBroadcastAddress());
System.out.println("isInRange(192.168.56.100) = " + info.isInRange("192.168.56.100"));
System.out.println("isInRange(192.168.57.100) = " + info.isInRange("192.168.57.100"));
System.out.println("commons-net OK");
}
}
编译并查看依赖树:
mvn -B compile
mvn -q dependency:tree -DoutputFile=deptree.txt
cat deptree.txt
实测回显(工作区里还有本系列后续章节要用的另外几个演示类,共 10 个源文件):
[INFO] Compiling 10 source files with javac [debug release 8] to target/classes
[INFO] BUILD SUCCESS
[INFO] Total time: 1.256 s
demo:commons-net-demo:jar:1.0.0
\- commons-net:commons-net:jar:3.13.0:compile
\- commons-io:commons-io:jar:2.21.0:compile
依赖树证实两点:Commons Net 3.13.0 无第三方传递依赖之外的负担;commons-io 2.21.0 是它的正式传递依赖(FTP 传输路径实测必用,缺了会 NoClassDefFoundError,见第六节)。
运行:
java -Dfile.encoding=UTF-8 -cp target/classes:$HOME/.m2/repository/commons-net/commons-net/3.13.0/commons-net-3.13.0.jar:$HOME/.m2/repository/commons-io/commons-io/2.21.0/commons-io-2.21.0.jar SubnetDemo
实测回显:
[不含网络号/广播号] low=192.168.56.1 high=192.168.56.62 可用地址数=62
[含网络号/广播号] low=192.168.56.0 high=192.168.56.63 可用地址数=64
network=192.168.56.0 netmask=255.255.255.192 broadcast=192.168.56.63
isInRange(192.168.56.100) = false
isInRange(192.168.57.100) = false
commons-net OK
验证结果:192.168.56.100 属于 64127 段,所以对 63)判定为 /26(0false,行为正确。到这里 Commons Net 已经可用;下面三节把它接入真实网络服务。
三、与 FTP 服务器联动实测
3.1 准备 vsftpd 服务
apt-get install -y vsftpd
输出节选(实测):
vsftpd is already the newest version (3.0.5-0ubuntu3.1).
创建测试用户(密码 q1w2e3r4)并允许其作为 FTP 登录用户:
echo /usr/sbin/nologin >> /etc/shells
useradd -m -s /usr/sbin/nologin ftpuser
echo "ftpuser:q1w2e3r4" | chpasswd
id ftpuser
实测回显:
uid=1001(ftpuser) gid=1001(ftpuser) groups=1001(ftpuser)
写入 /etc/vsftpd.conf(本地用户可登录、可写、chroot 到家目录、固定被动模式端口段):
listen=YES
listen_ipv6=NO
anonymous_enable=NO
local_enable=YES
write_enable=YES
local_umask=022
dirmessage_enable=NO
use_localtime=YES
xferlog_enable=NO
connect_from_port_20=YES
chroot_local_user=YES
allow_writeable_chroot=YES
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
seccomp_sandbox=NO
pasv_enable=YES
pasv_min_port=40000
pasv_max_port=40100
启动并验证服务端横幅:
mkdir -p /var/run/vsftpd/empty
nohup /usr/sbin/vsftpd /etc/vsftpd.conf >/var/log/vsftpd-demo.log 2>&1 &
curl -s telnet://127.0.0.1:21 --max-time 3 | head -1
实测回显:
220 (vsFTPd 3.0.5)
3.2 FTPClient 登录、上传、列目录、下载
准备待上传文件与演示程序:
printf "Hello Apache Commons Net FTPClient\n" > /root/commons-net-demo/hello-ftp.txt
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class FtpDemo {
public static void main(String[] args) throws Exception {
FTPClient ftp = new FTPClient();
ftp.connect("127.0.0.1", 21);
System.out.println("连接回显: " + ftp.getReplyString().trim());
if (!ftp.login("ftpuser", "q1w2e3r4")) {
throw new IllegalStateException("登录失败: " + ftp.getReplyString());
}
System.out.println("登录成功, 当前目录=" + ftp.printWorkingDirectory()
+ " 服务端类型=" + ftp.getSystemType());
ftp.enterLocalPassiveMode();
ftp.setFileType(FTP.BINARY_FILE_TYPE);
File local = new File("/root/commons-net-demo/hello-ftp.txt");
try (InputStream in = new FileInputStream(local)) {
System.out.println("storeFile = " + ftp.storeFile("hello-ftp.txt", in)
+ " 回复码=" + ftp.getReplyCode());
}
System.out.println("listNames 目录列表:");
for (String name : ftp.listNames()) {
System.out.println(" " + name);
}
File down = new File("/root/commons-net-demo/hello-ftp-download.txt");
try (OutputStream out = new FileOutputStream(down)) {
System.out.println("retrieveFile = " + ftp.retrieveFile("hello-ftp.txt", out));
}
System.out.println("下载文件字节数=" + down.length()
+ " 与上传一致=" + (down.length() == local.length()));
ftp.logout();
ftp.disconnect();
System.out.println("断开后 isConnected = " + ftp.isConnected());
}
}
java -Dfile.encoding=UTF-8 -cp target/classes:$HOME/.m2/repository/commons-net/commons-net/3.13.0/commons-net-3.13.0.jar:$HOME/.m2/repository/commons-io/commons-io/2.21.0/commons-io-2.21.0.jar FtpDemo
实测回显:
连接回显: 220 (vsFTPd 3.0.5)
登录成功, 当前目录=/ 服务端类型=UNIX Type: L8
storeFile = true 回复码=226
listNames 目录列表:
hello-ftp.txt
retrieveFile = true
下载文件字节数=35 与上传一致=true
断开后 isConnected = false
验证结果:登录后 printWorkingDirectory() 返回 /(vsftpd 把用户 chroot 到了家目录 /home/ftpuser);storeFile/retrieveFile 均返回 true 且回复码 226(传输完成);下载文件与上传文件字节数一致。服务端落盘复核:
ls -l /home/ftpuser/
实测回显:
total 4
-rw-r--r-- 1 ftpuser ftpuser 35 Oct 15 01:55 hello-ftp.txt
💡 技术拓展:FTP 每次文件传输都要另开一条数据连接,分主动与被动两种建立方向:
维度 主动模式(PORT) 被动模式(PASV/EPSV) 数据连接由谁发起 服务端连客户端 客户端连服务端 客户端环境要求 必须可被服务端直连(公网 IP、开放端口) 无 服务端环境要求 无 需开放被动端口段 典型问题 客户端在内网/NAT 后必失败 服务端漏放行端口段 Commons Net API enterLocalActiveMode()enterLocalPassiveMode()本文客户端与 vsftpd 同机部署,两种模式都能通;演示程序与生产实践默认采用被动模式,主动模式的实测对比见《常用 API 与网络客户端用法详解》。
四、与 SMTP 服务联动实测
4.1 准备本地 SMTP sink
sink 是只收不发、把对话打印到日志的"黑洞"服务,用 60 行左右的标准库 Python 实现(无第三方依赖),保存为 /root/smtp_sink.py:
#!/usr/bin/env python3
# SMTP sink:本地收信黑洞,打印与客户端的完整对话
import base64
import socket
HOST, PORT = "127.0.0.1", 2525
def reply(conn, line):
conn.sendall((line + "\r\n").encode())
def handle(conn, addr):
print("connection from %s:%d" % addr, flush=True)
rd = conn.makefile("rb")
reply(conn, "220 net-demo SMTP sink ready")
while True:
line = rd.readline()
if not line:
break
text = line.decode("utf-8", "replace").rstrip("\r\n")
print("C: %s" % text, flush=True)
u = text.upper()
if u.startswith("HELO"):
reply(conn, "250 sink greets %s" % text.split(" ", 1)[1])
elif u.startswith("EHLO"):
reply(conn, "250-sink greets %s" % text.split(" ", 1)[1])
reply(conn, "250-AUTH LOGIN PLAIN")
reply(conn, "250 8BITMIME")
elif u.startswith("AUTH LOGIN"):
reply(conn, "334 VXNlcm5hbWU6") # base64("Username:")
user = base64.b64decode(rd.readline().strip()).decode()
print("C: (base64 decoded) user=%s" % user, flush=True)
reply(conn, "334 UGFzc3dvcmQ6") # base64("Password:")
pwd = base64.b64decode(rd.readline().strip()).decode()
print("C: (base64 decoded) pass=%s" % pwd, flush=True)
if user == "smtpuser" and pwd == "q1w2e3r4":
reply(conn, "235 auth ok")
else:
reply(conn, "535 auth failed")
elif u.startswith("MAIL FROM"):
reply(conn, "250 OK")
elif u.startswith("RCPT TO"):
reply(conn, "250 OK")
elif u.startswith("DATA"):
reply(conn, "354 end with <CRLF>.<CRLF>")
print("---- message body ----", flush=True)
while True:
l = rd.readline()
if not l or l.rstrip(b"\r\n") == b".":
break
print("M: %r" % l.rstrip(b"\r\n"), flush=True)
print("---- end of body ----", flush=True)
reply(conn, "250 OK queued")
elif u.startswith("QUIT"):
reply(conn, "221 bye")
break
else:
reply(conn, "250 OK")
conn.close()
print("connection closed", flush=True)
def main():
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((HOST, PORT))
srv.listen(5)
print("SMTP sink listening on %s:%d ..." % (HOST, PORT), flush=True)
while True:
conn, addr = srv.accept()
try:
handle(conn, addr)
except Exception as e:
print("error: %r" % e, flush=True)
try:
conn.close()
except Exception:
pass
if __name__ == "__main__":
main()
启动并验证:
nohup python3 /root/smtp_sink.py >/var/log/smtp-sink.log 2>&1 &
curl -s telnet://127.0.0.1:2525 --max-time 3 | head -1
实测回显:
220 net-demo SMTP sink ready
💡 技术拓展:真实投递邮件除 SMTP 协议本身外还要选认证方式,常见 SMTP 认证扩展对比:
方式 原理 安全性 不认证(HELO 直发) 仅信任来源 IP 低,仅内网/中继白名单可用 AUTH LOGIN 用户名密码 Base64 编码传输 需 TLS 保护,裸跑可被窃听 AUTH PLAIN 一次提交明文对 同上 AUTH CRAM-MD5 服务端发挑战、密码不落网 高,但现代服务端已少用 本文 sink 是本地演示服务,4.2 用无认证流程发信;带 AUTH LOGIN 认证的完整实测见《常用 API 与网络客户端用法详解》。
4.2 SMTPClient 真实发信
import org.apache.commons.net.smtp.SMTPClient;
import org.apache.commons.net.smtp.SMTPReply;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
public class SmtpDemo {
public static void main(String[] args) throws Exception {
SMTPClient client = new SMTPClient("UTF-8");
client.connect("127.0.0.1", 2525);
if (!SMTPReply.isPositiveCompletion(client.getReplyCode())) {
client.disconnect();
throw new IllegalStateException("SMTP 服务器拒绝连接: " + client.getReplyString());
}
System.out.println("连接回显: " + client.getReplyString().trim());
client.login("client.demo.local");
System.out.println("HELO 回显: " + client.getReplyString().trim());
client.setSender("sender@demo.local");
client.addRecipient("rcpt@demo.local");
try (Writer w = client.sendMessageData()) {
w.write("From: sender@demo.local\r\n");
w.write("To: rcpt@demo.local\r\n");
w.write("Subject: commons-net smtp demo\r\n");
w.write("\r\n");
w.write("hello from Apache Commons Net SMTPClient\r\n");
w.write("第二行:中文测试\r\n");
}
System.out.println("DATA 送达: " + client.completePendingCommand()
+ " 回复码=" + client.getReplyCode());
client.disconnect();
System.out.println("断开后 isConnected = " + client.isConnected());
}
}
java -Dfile.encoding=UTF-8 -cp target/classes:$HOME/.m2/repository/commons-net/commons-net/3.13.0/commons-net-3.13.0.jar:$HOME/.m2/repository/commons-io/commons-io/2.21.0/commons-io-2.21.0.jar SmtpDemo
实测回显:
连接回显: 220 net-demo SMTP sink ready
HELO 回显: 250 sink greets client.demo.local
DATA 送达: true 回复码=250
断开后 isConnected = false
sink 侧收到的完整报文(日志节选):
connection from 127.0.0.1:35452
C: HELO client.demo.local
C: MAIL FROM:<sender@demo.local>
C: RCPT TO:<rcpt@demo.local>
C: DATA
---- message body ----
M: b'From: sender@demo.local'
M: b'To: rcpt@demo.local'
M: b'Subject: commons-net smtp demo'
M: b''
M: b'hello from Apache Commons Net SMTPClient'
M: b'\xe7\xac\xac\xe4\xba\x8c\xe8\xa1\x8c\xef\xbc\x9a\xe4\xb8\xad\xe6\x96\x87\xe6\xb5\x8b\xe8\xaf\x95'
---- end of body ----
connection closed
验证结果:\xe7\xac\xac\xe4\xba\x8c\xe8\xa1\x8c\xef\xbc\x9a\xe4\xb8\xad\xe6\x96\x87\xe6\xb5\x8b\xe8\xaf\x95 正是"第二行:中文测试"的 UTF-8 字节序列,sink 完整还原了报文;completePendingCommand() 拿到 250 表示 DATA 会话按协议正常闭合。
4.3 中文正文的编码坑(实测对照)
SMTPClient 的写通道默认编码是 ISO-8859-1(源码常量 SMTP.DEFAULT_ENCODING,经反射实测值),该编码无法表示汉字。用无参构造发同一封中文邮件,sink 收到的字节是:
M: b'????????'
8 个汉字变成 8 个字面问号——不是显示问题,是编码时就丢掉了。另外实测 setCharset(StandardCharsets.UTF_8) 对 SMTP 的写通道无效(编码字段在构造时即固定),正确做法是构造时指定:new SMTPClient("UTF-8")。4.2 的回显即该构造下 sink 收到的正确 UTF-8 字节。
五、与 Telnet 服务联动实测
5.1 准备 netcat 演示服务
用 netcat 监听 2323 端口,stdin 喂入应答脚本(-i 1 每行间隔 1 秒),保存为 /root/telnet-script.txt:
WELCOME TO NET-TELNET-DEMO
READY
ECHO: HELLO
BYE
nc -l 127.0.0.1 2323 -i 1 -q 3 < /root/telnet-script.txt
5.2 TelnetClient 真实交互
import org.apache.commons.net.telnet.TelnetClient;
import java.io.InputStream;
import java.io.OutputStream;
public class TelnetDemo {
public static void main(String[] args) throws Exception {
TelnetClient telnet = new TelnetClient();
telnet.connect("127.0.0.1", 2323);
System.out.println("已连接 127.0.0.1:2323");
InputStream in = telnet.getInputStream();
OutputStream out = telnet.getOutputStream();
System.out.println("服务端欢迎行: " + readLine(in).trim());
System.out.println("服务端就绪行: " + readLine(in).trim());
out.write("HELLO\r\n".getBytes());
out.flush();
System.out.println("发送 HELLO, 服务端回复: " + readLine(in).trim());
out.write("QUIT\r\n".getBytes());
out.flush();
System.out.println("发送 QUIT, 服务端回复: " + readLine(in).trim());
telnet.disconnect();
System.out.println("会话关闭, isConnected = " + telnet.isConnected());
}
static String readLine(InputStream in) throws Exception {
StringBuilder sb = new StringBuilder();
int ch;
while ((ch = in.read()) != -1 && ch != '\n') {
sb.append((char) ch);
}
return sb.toString();
}
}
java -Dfile.encoding=UTF-8 -cp target/classes:$HOME/.m2/repository/commons-net/commons-net/3.13.0/commons-net-3.13.0.jar:$HOME/.m2/repository/commons-io/commons-io/2.21.0/commons-io-2.21.0.jar TelnetDemo
实测回显:
已连接 127.0.0.1:2323
服务端欢迎行: WELCOME TO NET-TELNET-DEMO
服务端就绪行: READY
发送 HELLO, 服务端回复: ECHO: HELLO
发送 QUIT, 服务端回复: BYE
会话关闭, isConnected = false
服务端侧同样留了痕迹——nc 把 TelnetClient 实际发来的字节打印了出来:
HELLO
QUIT
验证结果:双向回显对上了,TelnetClient 对普通 ASCII 字节是透传的(Telnet 协议的 IAC 转义只影响协商字节,详见《常用 API 与网络客户端用法详解》)。
六、集成常见坑(全部实测复现)
-
缺 commons-io 直接
NoClassDefFoundError。去掉 classpath 中的commons-io再跑 3.2 的 FTP 上传:连接回显: 220 (vsFTPd 3.0.5) 登录成功, 当前目录=/ 服务端类型=UNIX Type: L8 Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/io/output/ProxyOutputStream at java.base/java.lang.ClassLoader.defineClass1(Native Method) at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1017) at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150) at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862) at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760) at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681) at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525) at org.apache.commons.net.ftp.FTPClient._storeFile(FTPClient.java:978) at org.apache.commons.net.ftp.FTPClient.storeFile(FTPClient.java:3302) at org.apache.commons.net.ftp.FTPClient.storeFile(FTPClient.java:3323) at FtpDemo.main(FtpDemo.java:25) Caused by: java.lang.ClassNotFoundException: org.apache.commons.io.output.ProxyOutputStream at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525) ... 13 more3.13.0 把传输流包装实现迁移到了 commons-io,Maven 方案自动带入;手拼 classpath 时两个 JAR 都要有。
SubnetUtils、TelnetClient等纯工具类不受影响。 -
FTP 控制通道编码与 JVM 编码是两回事。FTP 默认控制编码
ISO-8859-1,文件名含中文时须setControlEncoding("UTF-8")(参数细节见《配置参数详解》);SMTP 则要在构造器传"UTF-8"(4.3)。 -
FTP 一次只能创建一级目录。
makeDirectory("data/2026")在data不存在时被 vsftpd 拒绝(回复码 550),需先建data再建data/2026,实测过程见《常用 API 与网络客户端用法详解》。 -
中文环境下的 vsftpd 列表解析。3.13.0 的 RELEASE-NOTES 明确修复了"FTP fails to parse listings for Linux vsftpd in Chinese or Japanese(NET-740)",升级到 3.13.0 可避免中文 locale 下
listFiles()解析失败的旧坑。 -
Telnet 演示服务要留好退出通道。netcat 脚本式服务与
TelnetClient联调时,读阻塞发生在服务端不再输出之后,务必像 5.2 那样按服务端约定读到终止行再disconnect()。
七、与其他网络方案选型对比
| 需求 | Commons Net 方案 | 替代方案 | 选型建议 |
|---|---|---|---|
| FTP/FTPS 客户端 | FTPClient | JDK 无内置;JSch/sshj 是 SFTP(另一协议) | 传统 FTP/FTPS 首选 Commons Net |
| SMTP 发信 | SMTPClient(协议级) | Jakarta Mail(JavaMail,功能全) | 只要发简单邮件、要精细控制命令流用 Commons Net;要 MIME/附件/模板用 Jakarta Mail |
| Telnet | TelnetClient | 裸 Socket(要自己实现 IAC 协商) | 与 Telnet 设备交互直接用 TelnetClient,协商已内置 |
| 子网计算 | SubnetUtils/SubnetUtils6 | 手写位运算 | 直接用工具类,边界行为(/32 等)已被库处理 |
| HTTP 访问 | 无 | JDK HttpClient | Commons Net 不覆盖 HTTP,勿混用 |
八、参考链接
- 官方示例源码(含各协议完整示例):https://github.com/apache/commons-net/tree/master/src/main/java/org/apache/commons/net/examples
- 用户指南:https://commons.apache.org/proper/commons-net/userguide.html
- JavaDoc(3.13.0):https://commons.apache.org/proper/commons-net/javadocs/api-3.13.0/
- RELEASE-NOTES(3.13.0):https://downloads.apache.org/commons/net/RELEASE-NOTES.txt
- 本系列:《Ubuntu 22.04 安装 Apache Commons Net 指南》《CentOS 8 安装 Apache Commons Net 指南》《Apache Commons Net 3.13.0 配置参数详解》《Apache Commons Net 3.13.0 常用 API 与网络客户端用法详解》