引言
本项目的目的是实现两个应用,通过网络连接在不同的主机之间传输一个文件的功能。两个应用应该分别利用 UDP 和 TCP 协议,以具有传输至少 1 MB 文件的能力。
实现和说明
源代码
两个应用都由单个程序实现,源代码下载地址。
说明
程序使用以下命令行进行编译:
javac *.java
然后使用以下两个命令行运行:
Receiver: # java FileReceiver [protocol] [port] Sender: # java FileSender [protocol] [host] [port] [filename] |
其中 [protocol] 参数可以是 "udp" 或者 "tcp",但 sender 和 receiver 必须一致。
文件将会在 receiver 启动的目录下生成,默认指定名为 "Received-[filename]"。
TCP 实现
实现概述
在 TCP 实现中,Receiver 打开了一个 ServerSocket,并对定义好的端口进行监听。Sender 启动后将会为监听者 Receiver 打开一个新的 Socket,这导致了 socket 两端 InputStream 和 OutputStream 对象的创建。
一个包含了文件名和文件大小的初始信息将由 Sender 发送给 Receiver。这样 Receiver 可以使用一个有意义的名字来存储接收到的文件,并可以判断什么时候文件完全传输完毕。此信息并不是必须的,当 Receiver 无法接收文件时停止 Sender 占用不必要的带宽。
文件通过一个 FileInputstream 对象对它的读取进行传输,然后将数据写到一个 Socket 返回的 OutputStream 对象。为提高应用效率,每次读取和中继的数据是 8 kb,使用一个字节数组作为缓存。
TCP 使用经验
实践证明,TCP 文件传输是简单可靠的。程序的效率取决于使用的缓存大小,但传输的文件在所有执行的
测试中都准确地被接收和保存。
UDP 实现
实现概述
UDP 文件传输的实现使用的是标准 Java datagram 类:DatagramPacket 和 DatagramSocket。
当 receiver 被执行时,它打开一个指定端口号的 socket 并等待,监听传入的数据包。sender 启动后,它打开一个连接到指定主机和端口的 socket,并传输包含有文件名以及将要传输文件大小等信息的单个 packet。当这个 package 发送以后,这个 socket 将等待并监听 package。
基于接收到的初始 package,receiver 为文件创建一 outputStream 对象,并给监听着的 sender 发送一个含有 "OK" 单词的 package。收到这个 "OK" 包以后,sender 开始读取文件内容,并将其通过 UDP 数据包发送,每次含有 512 字节的块。receiver 将这些块按照接收到的次序写入文件,并重复接收,直到接收到的字节达到它所期望数字。之后程序终止。
UDP 使用经验
UDP 是一种不可靠的传输连续数据的协议。这意味着传输过程中会有丢包,而且接收到包的次序也是随机的。上面的例子并没有解决文件传输中的这些问题。这意味着以上应用在其每次运行时(所得到的文件)并不是正确的和完整的。以下是关于两个经常发生的问题的原因以及可行解决方案的描述。
如果在文件传输过程中两个包接收顺序错误,而写入文件的顺序是按接收顺序来的。这将造成接收文件损坏。对于这种问题的解决方案是每次传输时定义一个序列号。这可以让 Receiver 按照正确的顺序来存储这些包,不管它们到达的先后次序。
如果传输文件时出现丢包,Receiver 将不能收到它所期望数量的数据。在上面的示例中,这会导致 Receiver 继续运行,等待剩余的数据。对于这个问题一个可行的解决方案是,receiver 在给定时间跨度之后进行每次传输,调用超时。但为了使此次请求具有目的性,我们要像上面说的那样为包扩展序列号。否则我们无法接收到给定数量的数据,并局限于请求文件的完整传输。
另一个关于这两个问题的解决方案,在每次正确接收包之后再向 sender 发起接收请求。这个方法消除了丢包的可能性,但却会使传输异常缓慢。
结语
上面的实现让文件在主机之间传输变得可行。但如果使用的是 UDP 协议的话,我们就无法保证文件的完整性和接收(顺序)的正确性。我们对解决这些问题进行了大体说明,但具体在实际的文件传输中,对这些问题的最简单的解决方案就是,用 TCP 取代 UDP。