通过FastDFS的Java客户端上传图片连接超时的解决过程
小橘子🍊

问题描述

FastDFS单机环境下的搭建 这篇文章中,我搭建了一个 FastDFS 图片服务器,这里我通过FastDFS提供的命令行工具进行文件上传是成功的,然后就是测试一下通过Java客户端来进行图片的上传操作,很遗憾测试没有通过,控制台打印如下错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
java.net.SocketTimeoutException: connect timed out

at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at org.csource.fastdfs.ClientGlobal.getSocket(ClientGlobal.java:208)
at org.csource.fastdfs.StorageServer.<init>(StorageServer.java:43)
at org.csource.fastdfs.TrackerClient.getStoreStorage(TrackerClient.java:144)
at org.csource.fastdfs.StorageClient.newWritableStorageConnection(StorageClient.java:1627)
at org.csource.fastdfs.StorageClient.do_upload_file(StorageClient.java:639)
at org.csource.fastdfs.StorageClient.upload_file(StorageClient.java:120)
at org.csource.fastdfs.StorageClient.upload_file(StorageClient.java:91)
at org.csource.fastdfs.StorageClient.upload_file(StorageClient.java:73)
at org.csource.fastdfs.StorageClient1.upload_file1(StorageClient1.java:64)
at com.eva.utils.FastDFSClient.uploadFile(FastDFSClient.java:36)
at com.eva.utils.FastDFSClient.uploadFile(FastDFSClient.java:41)
at com.eva.fastdfs.TestFastDFS.testFastDfsClient(TestFastDFS.java:34)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

问题分析

我的 FastDFS 搭建在阿里云服务器上,公网IP地址为 119.23.247.86。

通过打印的 stack dump 信息,可以看到最终报错点是个native方法,native方法对于我来说就是一个黑盒,我只能是跟踪他的上一级调用,通过打一个断点进行调试可以看到此方法为 java.net.AbstractPlainSocketImpl.socketConnect(address, port, timeout),它有三个参数,当前调试的值如下图:

java.net.AbstractPlainSocketImpl

tracker-port

可以看到当前的端口为 22122 ,也就是说这里将要连接 Tracker 服务器,貌似没啥问题啊?那就直接下一步,发现被调用的 native 方法并没有抛出异常,并且顺利的执行到了 java.net.AbstractPlainSocketImpl.socketConnect(address, port, timeout) 的下一行,控制台也没有任何异常信息,现象如下图:

java.net.AbstractPlainSocketImpl

tracker-ok

这就说明并不是Java客户端和tracker建立连接发生错误,于是全速执行,发现代码又重新的停在了 java.net.AbstractPlainSocketImpl.socketConnect(address, port, timeout) 处,现象如下:

java.net.AbstractPlainSocketImpl

storage-port

注意了!! address 变成了 172.17.45.253,这个值怎么跟跟我的阿里云服务器公网地址不一样呢?另外 port 变成了 23000 这就说明当前是在与 storage 进行连接,先不管那么多,直接下一步看看能不能通过吧,现象如下:

java.net.AbstractPlainSocketImpl

storage-fail

可以看到代码是直接跳到了 finally 代码块,这就说明在 native 方法的执行过程中发生了异常,根据传入的参数可以推断,port 是么有问题的,唯一的可能就是 address ,毕竟他竟然不是我的阿里云服务器公网地址,那么为什么会出现这个地址呢?在我的 .properties 等相关配置文件中,都没有出现过这个可疑的 ip 地址,所以下一步就是来排查这个可疑的 ip 地址的来源了。

通过 debug 过程的 stack dump 信息,进行往下跟踪(毕竟是个方法栈嘛~~),可以看到这个可疑的 ip 地址是通过

1
org.csource.fastdfs.TrackerClient#getStoreStorage(org.csource.fastdfs.TrackerServer, java.lang.String) 

这个函数的

1
ip_addr = new String(pkgInfo.body, ProtoCommon.FDFS_GROUP_NAME_MAX_LEN, ProtoCommon.FDFS_IPADDR_SIZE - 1).trim();

这行代码构造出来的,里边的关键参数就是 pkgInfo.body,这个参数是通过当前方法的

1
ProtoCommon.RecvPackageInfo pkgInfo = ProtoCommon.recvPackage(trackerSocket.getInputStream(), ProtoCommon.TRACKER_PROTO_CMD_RESP, ProtoCommon.TRACKER_QUERY_STORAGE_STORE_BODY_LEN);

构建的,这里的 trackerSocket.getInputStream() 是一个流对象,并且是从 trackerSocket 中获取的,于是猜测可疑 ip 就是通过 tracker 服务器传输过来的,现在我们需要深入这行代码,探究具体的接收过程,详细的操作在下边这个方法中执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static RecvPackageInfo recvPackage(InputStream in, byte expect_cmd, long expect_body_len) throws IOException {
// 输入流中数据的
RecvHeaderInfo header = recvHeader(in, expect_cmd, expect_body_len);
if (header.errno != 0) {
return new RecvPackageInfo(header.errno, null);
}

// 存储数据流中的数据
byte[] body = new byte[(int) header.body_len];
int totalBytes = 0;
int remainBytes = (int) header.body_len;
int bytes;

while (totalBytes < header.body_len) {
// 将流中的数据读入到boby中
if ((bytes = in.read(body, totalBytes, remainBytes)) < 0) {
break;
}

totalBytes += bytes;
remainBytes -= bytes;
}

if (totalBytes != header.body_len) {
throw new IOException("recv package size " + totalBytes + " != " + header.body_len);
}

// 将body封装成RecvPackageInfo
return new RecvPackageInfo((byte) 0, body);
}

代码还是很简单的,这个方法返回后就是通过 RecvPackageInfo 来构造这个可疑的 storage 的 ip 地址了。

既然可疑的 ip 地址来自 trackerSockt 的输入流,为什么 tracker 会返回这个 ip 地址呢?立即联想到这个 ip 可能是阿里云服务器的内网地址,于是进行查看,与所想一样,那么现在可以将问题缩小到 tracker 误将服务器的内网地址当公网地址返回给客户端了,于是核验 tracker 和 storage 配置文件,果然是自己在进行配置时,误将内网地址填写为了公网地址。

总结

  • tcp、ip、udp 相关的知识又淡忘了,抽空复习一下 tcp 拥塞控制、慢启动相关的内容。
  • 这里的公网 ip、内网 ip 啥意思呢?类似于虚拟机通过 nat 组网的那种模式吗?宿主机的 ip 地址就是公网ip,和虚拟机相连的那块虚拟网卡分配的 ip 为内网 ip?