Hadoop分散式档案系统(HDFS)被设计成适合运行在通用硬体(commodity hardware)上的分散式档案系统。它和现有的分散式档案系统有很多共同点。但同时,它和其他的分散式档案系统的区别也是很明显的。HDFS是一个高度容错性的系统,适合部署在廉价的机器上。HDFS能提供高吞吐量的数据访问,非常适合大规模数据集上的套用。HDFS放宽了一部分POSIX约束,来实现流式读取档案系统数据的目的。HDFS在最开始是作为Apache Nutch搜寻引擎项目的基础架构而开发的。HDFS是Apache Hadoop Core项目的一部分。
HDFS有着高容错性(fault-tolerant)的特点,并且设计用来部署在低廉的(low-cost)硬体上。而且它提供高吞吐量(high throughput)来访问应用程式的数据,适合那些有着超大数据集(large data set)的应用程式。HDFS放宽了(relax)POSIX的要求(requirements)这样可以实现流的形式访问(streaming access)档案系统中的数据。
基本介绍
- 全称:Hadoop Distributed File System
- 简称:hdfs
- 实质:分散式档案系统
- 作用:作为Apache Nutch的基础架构
- 特点:高容错性
- 适用:大规模数据集
体系结构
HDFS採用了主从(Master/Slave)结构模型,一个HDFS集群是由一个NameNode和若干个DataNode组成的。其中NameNode作为主伺服器,管理档案系统的命名空间和客户端对档案的访问操作;集群中的DataNode管理存储的数据。
特点和目标
硬体故障
硬体故障是常态,而不是异常。整个HDFS系统将由数百或数千个存储着档案数据片段的伺服器组成。实际上它里面有非常巨大的组成部分,每一个组成部分都很可能出现故障,这就意味着HDFS里的总是有一些部件是失效的,因此,故障的检测和自动快速恢复是HDFS一个很核心的设计目标。
数据访问
运行在HDFS之上的应用程式必须流式地访问它们的数据集,它不是运行在普通档案系统之上的普通程式。HDFS被设计成适合批量处理的,而不是用户互动式的。重点是在数据吞吐量,而不是数据访问的反应时间,POSIX的很多硬性需求对于HDFS套用都是非必须的,去掉POSIX一小部分关键语义可以获得更好的数据吞吐率。
大数据集
运行在HDFS之上的程式有很大量的数据集。典型的HDFS档案大小是GB到TB的级别。所以,HDFS被调整成支持大档案。它应该提供很高的聚合数据频宽,一个集群中支持数百个节点,一个集群中还应该支持千万级别的档案。
简单一致性模型
大部分的HDFS程式对档案操作需要的是一次写多次读取的操作模式。一个档案一旦创建、写入、关闭之后就不需要修改了。这个假定简单化了数据一致的问题,并使高吞吐量的数据访问变得可能。一个Map-Reduce程式或者网路爬虫程式都可以完美地适合这个模型。
移动计算比移动数据更经济
在靠近计算数据所存储的位置来进行计算是最理想的状态,尤其是在数据集特别巨大的时候。这样消除了网路的拥堵,提高了系统的整体吞吐量。一个假定就是迁移计算到离数据更近的位置比将数据移动到程式运行更近的位置要更好。HDFS提供了接口,来让程式将自己移动到离数据存储更近的位置。
异构软硬体平台间的可移植性
HDFS被设计成可以简便地实现平台间的迁移,这将推动需要大数据集的套用更广泛地採用HDFS作为平台。
名位元组点和数据节点
HDFS是一个主从结构,一个HDFS集群是由一个名位元组点,它是一个管理档案命名空间和调节客户端访问档案的主伺服器,当然还有一些数据节点,通常是一个节点一个机器,它来管理对应节点的存储。HDFS对外开放档案命名空间并允许用户数据以档案形式存储。
内部机制是将一个档案分割成一个或多个块,这些块被存储在一组数据节点中。名位元组点用来操作档案命名空间的档案或目录操作,如打开,关闭,重命名等等。它同时确定块与数据节点的映射。数据节点负责来自档案系统客户的读写请求。数据节点同时还要执行块的创建,删除,和来自名位元组点的块複製指令。

名位元组点和数据节点都是运行在普通的机器之上的软体,机器典型的都是GNU/Linux,HDFS是用java编写的,任何支持java的机器都可以运行名位元组点或数据节点,利用java语言的超轻便性,很容易将HDFS部署到大範围的机器上。典型的部署是由一个专门的机器来运行名位元组点软体,集群中的其他每台机器运行一个数据节点实例。体系结构不排斥在一个机器上运行多个数据节点的实例,但是实际的部署不会有这种情况。
集群中只有一个名位元组点极大地简单化了系统的体系结构。名位元组点是仲裁者和所有HDFS元数据的仓库,用户的实际数据不经过名位元组点。
档案命名空间
HDFS支持传统的继承式的档案组织结构。一个用户或一个程式可以创建目录,存储档案到很多目录之中。档案系统的名字空间层次和其他的档案系统相似。可以创建、移动档案,将档案从一个目录移动到另外一个,或重命名。HDFS还没有实现用户的配额和访问控制。HDFS还不支持硬连结和软连结。然而,HDFS结构不排斥在将来实现这些功能。
名位元组点维护档案系统的命名空间,任何档案命名空间的改变和或属性都被名位元组点记录。应用程式可以指定档案的副本数,档案的副本数被称作档案的複製因子,这些信息由命名空间来负责存储。
数据複製
HDFS设计成能可靠地在集群中大量机器之间存储大量的档案,它以块序列的形式存储档案。档案中除了最后一个块,其他块都有相同的大小。属于档案的块为了故障容错而被複製。块的大小和複製数是以档案为单位进行配置的,套用可以在档案创建时或者之后修改複製因子。HDFS中的档案是一次写的,并且任何时候都只有一个写操作。
名位元组点负责处理所有的块複製相关的决策。它周期性地接受集群中数据节点的心跳和块报告。一个心跳的到达表示这个数据节点是正常的。一个块报告包括该数据节点上所有块的列表。

副本位置:第一小步
块副本存放位置的选择严重影响HDFS的可靠性和性能。副本存放位置的最佳化是HDFS区分于其他分散式档案系统的的特徵,这需要精心的调节和大量的经验。机架敏感的副本存放策略是为了提高数据的可靠性,可用性和网路频宽的利用率。副本存放策略的实现是这个方向上比较原始的方式。短期的实现目标是要把这个策略放在生产环境下验证,了解更多它的行为,为以后测试研究更精緻的策略打好基础。
HDFS运行在跨越大量机架的集群之上。两个不同机架上的节点是通过交换机实现通信的,在大多数情况下,相同机架上机器间的网路频宽优于在不同机架上的机器。
在开始的时候,每一个数据节点自检它所属的机架id,然后在向名位元组点注册的时候告知它的机架id。HDFS提供接口以便很容易地挂载检测机架标示的模组。一个简单但不是最优的方式就是将副本放置在不同的机架上,这就防止了机架故障时数据的丢失,并且在读数据的时候可以充分利用不同机架的频宽。这个方式均匀地将複製分散在集群中,这就简单地实现了组建故障时的负载均衡。然而这种方式增加了写的成本,因为写的时候需要跨越多个机架传输档案块。
默认的HDFS block放置策略在最小化写开销和最大化数据可靠性、可用性以及总体读取频宽之间进行了一些折中。一般情况下複製因子为3,HDFS的副本放置策略是将第一个副本放在本地节点,将第二个副本放到本地机架上的另外一个节点而将第三个副本放到不同机架上的节点。这种方式减少了机架间的写流量,从而提高了写的性能。机架故障的几率远小于节点故障。这种方式并不影响数据可靠性和可用性的限制,并且它确实减少了读操作的网路聚合频宽,因为档案块仅存在两个不同的机架, 而不是三个。档案的副本不是均匀地分布在机架当中,1/3在同一个节点上,1/3副本在同一个机架上,另外1/3均匀地分布在其他机架上。这种方式提高了写的性能,并且不影响数据的可靠性和读性能。
副本的选择
为了儘量减小全局的频宽消耗读延迟,HDFS尝试返回给一个读操作离它最近的副本。假如在读节点的同一个机架上就有这个副本,就直接读这个,如果HDFS集群是跨越多个数据中心,那幺本地数据中心的副本优先于远程的副本。
安全模式
在启动的时候,名位元组点进入一个叫做安全模式的特殊状态。安全模式中不允许发生档案块的複製。名位元组点接受来自数据节点的心跳和块报告。一个块报告包含数据节点所拥有的数据块的列表。
每一个块有一个特定的最小複製数。当名位元组点检查这个块已经大于最小的複製数就被认为是安全地複製了,当达到配置的块安全複製比例时(加上额外的30秒),名位元组点就退出安全模式。它将检测数据块的列表,将小于特定複製数的块複製到其他的数据节点。
档案系统的元数据的持久化
HDFS的命名空间是由名位元组点来存储的。名位元组点使用叫做EditLog的事务日誌来持久记录每一个对档案系统元数据的改变,如在HDFS中创建一个新的档案,名位元组点将会在EditLog中插入一条记录来记录这个改变。类似地,改变档案的複製因子也会向EditLog中插入一条记录。名位元组点在本地档案系统中用一个档案来存储这个EditLog。整个档案系统命名空间,包括档案块的映射表和档案系统的配置都存在一个叫FsImage的档案中,FsImage也存放在名位元组点的本地档案系统中。
名位元组点在记忆体中保留一个完整的档案系统命名空间和档案块的映射表的镜像。这个元数据被设计成紧凑的,这样4GB记忆体的名位元组点就足以处理非常大的档案数和目录。名位元组点启动时,它将从磁碟中读取FsImage和EditLog,将EditLog中的所有事务套用到FsImage的仿记忆体空间,然后将新的FsImage刷新到本地磁碟中,因为事务已经被处理并已经持久化的FsImage中,然后就可以截去旧的EditLog。这个过程叫做检查点。当前实现中,检查点仅在名位元组点启动的时候发生,正在支持周期性的检查点。
数据节点将HDFS数据存储到本地的档案系统中。数据节点并不知道HDFS档案的存在,它在本地档案系统中以单独的档案存储每一个HDFS档案的数据块。数据节点不会将所有的数据块档案存放到同一个目录中,而是启发式的检测每一个目录的最优档案数,并在适当的时候创建子目录。在本地同一个目录下创建所有的数据块档案不是最优的,因为本地档案系统可能不支持单个目录下巨额档案的高效操作。当数据节点启动的时候,它将扫描它的本地档案系统,根据本地的档案产生一个所有HDFS数据块的列表并报告给名位元组点,这个报告称作块报告。
通信协定
所有的通信协定都是在TCP/IP协定之上构建的。一个客户端和指定TCP配置连线埠的名位元组点建立连线之后,它和名位元组点之间通信的协定是Client Protocol。数据节点和名位元组点之间通过Datanode Protocol通信。
RPC(Remote Procedure Call)抽象地封装了Client Protocol和DataNode Protocol协定。按照设计,名位元组点不会主动发起一个RPC,它只是被动地对数据节点和客户端发起的RPC作出反馈。
异常处理
可靠性
HDFS的主要目标就是在存在故障的情况下也能可靠地存储数据。三个最常见的故障是名位元组点故障,数据节点故障和网路断开。
重新複製
一个数据节点周期性传送一个心跳包到名位元组点。网路断开会造成一组数据节点子集和名位元组点失去联繫。名位元组点根据缺失的心跳信息判断故障情况。名位元组点将这些数据节点标记为死亡状态,不再将新的IO请求转发到这些数据节点上,这些数据节点上的数据将对HDFS不再可用,可能会导致一些块的複製因子降低到指定的值。
名位元组点检查所有的需要複製的块,并开始複製他们到其他的数据节点上。重新複製在有些情况下是不可或缺的,例如:数据节点失效,副本损坏,数据节点磁碟损坏或者档案的複製因子增大。
数据正确性
从数据节点上取一个档案块有可能是坏块,坏块的出现可能是存储设备错误,网路错误或者软体的漏洞。HDFS客户端实现了HDFS档案内容的校验。当一个客户端创建一个HDFS档案时,它会为每一个档案块计算一个校验码并将校验码存储在同一个HDFS命名空间下一个单独的隐藏档案中。当客户端访问这个档案时,它根据对应的校验档案来验证从数据节点接收到的数据。如果校验失败,客户端可以选择从其他拥有该块副本的数据节点获取这个块。
元数据失效
FsImage和Editlog是HDFS的核心数据结构。这些档案的损坏会导致整个集群的失效。因此,名位元组点可以配置成支持多个FsImage和EditLog的副本。任何FsImage和EditLog的更新都会同步到每一份副本中。
同步更新多个EditLog副本会降低名位元组点的命名空间事务交易速率。但是这种降低是可以接受的,因为HDFS程式中产生大量的数据请求,而不是元数据请求。名位元组点重新启动时,选择最新一致的FsImage和EditLog。
名位元组点对于一个HDFS集群是单点失效的。假如名位元组点失效,就需要人工的干预。还不支持自动重启和到其它名位元组点的切换。
特点
快照
快照支持在一个特定时间存储一个数据拷贝,快照可以将失效的集群回滚到之前一个正常的时间点上。HDFS已经支持元数据快照。
数据组织
数据块
HDFS的设计是用于支持大档案的。运行在HDFS上的程式也是用于处理大数据集的。这些程式仅写一次数据,一次或多次读数据请求,并且这些读操作要求满足流式传输速度。HDFS支持档案的一次写多次读操作。HDFS中典型的块大小是64MB,一个HDFS档案可以被被切分成多个64MB大小的块,如果需要,每一个块可以分布在不同的数据节点上。
阶段状态
一个客户端创建一个档案的请求并不会立即转发到名位元组点。实际上,一开始HDFS客户端将档案数据快取在本地的临时档案中。应用程式的写操作被透明地重定向到这个临时本地档案。当本地档案堆积到一个HDFS块大小的时候,客户端才会通知名位元组点。名位元组点将档案名称插入到档案系统层次中,然后为它分配一个数据块。名位元组点构造包括数据节点ID(可能是多个,副本数据块存放的节点也有)和目标数据块标识的报文,用它回复客户端的请求。客户端收到后将本地的临时档案刷新到指定的数据节点数据块中。
当档案关闭时,本地临时档案中未上传的残留数据就会被转送到数据节点。然后客户端就可以通知名位元组点档案已经关闭。此时,名位元组点将档案的创建操作添加到到持久化存储中。假如名位元组点在档案关闭之前死掉,档案就丢掉了。
上述流程是在认真考虑了运行在HDFS上的目标程式之后被採用。这些应用程式需要流式地写档案。如果客户端对远程档案系统进行直接写入而没有任何本地的快取,这就会对网速和网路吞吐量产生很大的影响。这方面早有前车之鉴,早期的分散式档案系统如AFS,也用客户端缓冲来提高性能,POSIX接口的限制也被放宽以达到更高的数据上传速率。
流水式複製
当客户端写数据到HDFS档案中时,如上所述,数据首先被写入本地档案中,假设HDFS档案的複製因子是3,当本地档案堆积到一块大小的数据,客户端从名位元组点获得一个数据节点的列表。这个列表也包含存放数据块副本的数据节点。当客户端刷新数据块到第一个数据节点。第一个数据节点开始以4kb为单元接收数据,将每一小块都写到本地库中,同时将每一小块都传送到列表中的第二个数据节点。同理,第二个数据节点将小块数据写入本地库中同时传给第三个数据节点,第三个数据节点直接写到本地库中。一个数据节点在接前一个节点数据的同时,还可以将数据流水式传递给下一个节点,所以,数据是流水式地从一个数据节点传递到下一个。
可访问性
HDFS提供多种方式由应用程式访问,自然地,HDFS提供为程式提供java api,为c语言包装的java api也是可用的,还有一个HTTP浏览器可以浏览HDFS中的档案,通过WebDAV协定访问HDFS库的方式也正在构建中。
DFSShell
HDFS允许用户数据组织成档案和资料夹的方式,它提供一个叫DFSShell的接口,使用户可以和HDFS中的数据互动。命令集的语法跟其他用户熟悉的shells(bash,csh)相似。以下是一些例子:
Action | Command |
创建目录 /foodir | hadoop dfs -mkdir /foodir |
查看档案 /foodir/myfile.txt | hadoop dfs -cat /foodir/myfile.txt |
删除档案/foodir/myfile.txt | hadoop dfs -rm /foodir myfile.txt |
DFSAdmin
DFSAdmin命令集是用于管理dfs集群的,这些命令只由HDFS管理员使用。示例:
Action | Command |
将集群设定成安全模式 | bin/hadoop dfsadmin -safemode enter |
产生一个数据节点的列表 | bin/hadoop dfsadmin -report |
去掉一个数据节点 | bin/hadoop dfsadmin -decommission datanodename |
浏览器接口
典型的HDFS初始化配置了一个web 服务,通过一个可配的TCP连线埠可以访问HDFS的命名空间。这就使得用户可以通过web浏览器去查看HDFS命名空间的内容。
存储空间回收
档案删除和恢复删除
当一个档案被用户或程式删除时,它并没有立即从HDFS中删除。HDFS将它重新命名后转存到/trash目录下,这个档案只要还在/trash目录下保留就可以重新快速恢复。档案在/trash中存放的时间是可配置的。存储时间逾时后,名位元组点就将目标档案从名字空间中删除,同时此档案关联的所有档案块都将被释放。注意,用户删除档案的时间和HDF系统回收空闲存储之间的时间间隔是可以估计的。
删除一个档案之后,只要它还在/trash目录下,用户就可以恢复删除一个档案。如果一个用户希望恢复删除他已经删除的档案,可以查找/trash目录获得这个档案。/trash目录仅保存最新版本的删除档案。/trash目录也像其他目录一样,只有一个特殊的功能,HDFS採用一个特定的策略去自动地删除这个目录里的档案,当前默认的策略是删除在此目录存放超过6小时的档案。以后这个策略将由一个定义好的接口来配置。
减少複製因子
当档案的複製因子减少了,名位元组点选择删除多余的副本,下一次的心跳包的回覆就会将此信息传递给数据节点。然后,数据节点移除相应的块,对应的空闲空间将回归到集群中,需要注意的就是,在setReplication函式调用后和集群空闲空间更新之间会有一段时间延迟。
HDFS的档案读取解析
档案内容读取的代码可以分为三个大步骤。
1、获取档案系统
2、通过档案系统打开档案
3、将档案内容输出
public static void read(Path path) throws IOException{ FileSystem hdfs = HdfsUtils.getFilesystem(); //步骤 1 FSDataInputStream fsDataInputStream = hdfs.open(path); //步骤 2 IOUtils.copyBytes(fsDataInputStream, System.out, 4096,false); //步骤 3}
接下来,我们来看一下每个步骤的详细过程
获取档案系统对象
要从HDFS上读取档案,必须先得到一个FileSystem。HDFS本身就是一个档案系统,所以,我们得到一个档案系统后就可以对HDFS进行相关操作。获取档案系统的步骤可以分为以下2步。
1、读取配置档案。
2、获取档案系统。
读取配置档案:Configuration类有三个构造器,无参数的构造器表示直接载入默认资源,也可以指定一个boolean参数来关闭载入默认值,或直接使用另外一个Configuration对象来初始化。
package com.yq.common; import java.net.URI; import org.apache.hadoop.conf.Configuration;import org.apache.hadoop.fs.FileSystem; public class HdfsUtils { public static FileSystem getFilesystem(){ FileSystem hdfs=null; Configuration conf=new Configuration(); try{ URI uri = new URI("hdfs://localhost:9000"); hdfs = FileSystem.get(uri,conf); } catch(Exception ex){ // } return hdfs; }}
打开档案
FSDataInputStream fsDataInputStream = hdfs.open(path);
打开档案其实就是创建一个档案输入流,跟蹤档案系统的open方法,可以找到源码
public FSDataInputStream open(Path f) throws IOException { return open(f, getConf().getInt("io.file.buffer.size", 4096)); }
再跟蹤open方法,找到以下抽象方法。
public abstract FSDataInputStream open(Path f, int bufferSize) throws IOException; //这个方法在DistributedFileSystem类有实现,如下 @Override public FSDataInputStream open(Path f, final int bufferSize) throws IOException { statistics.incrementReadOps(1); Path absF = fixRelativePart(f); return new FileSystemLinkResolver<FSDataInputStream>() { @Override public FSDataInputStream doCall(final Path p) throws IOException, UnresolvedLinkException { return new HdfsDataInputStream( dfs.open(getPathName(p), bufferSize, verifyChecksum)); } @Override public FSDataInputStream next(final FileSystem fs, final Path p) throws IOException { return fs.open(p, bufferSize); } }.resolve(this, absF); }
在返回结果的时候,创建了一个FileSystemLinkResolver对象,并实现了此类的两个抽象方法。doCall方法和next方法都在resolve方法里用到了,而next方法只是在resolve方法异常捕获时才调用。
跟蹤doCall方法,doCall方法里的open()方法有3个参数,src表示要打开的档案路径,buffersize表示缓冲大小,verifyChecksum表示是否校验和,的原始码如下。
public DFSInputStream open(String src, int buffersize, boolean verifyChecksum) throws IOException, UnresolvedLinkException { checkOpen(); // Get block info from namenode return new DFSInputStream(this, src, buffersize, verifyChecksum); }
checkOpen方法表示检查档案系统是否已经打开,如果没有打开,则抛出异常(FileSystemclosed)。
然后返回一个分散式档案系统输入流(DFSInputStream),此处调用的构造方法原始码如下。
DFSInputStream(DFSClient dfsClient, String src, int buffersize, boolean verifyChecksum ) throws IOException, UnresolvedLinkException { this.dfsClient = dfsClient; this.verifyChecksum = verifyChecksum; this.buffersize = buffersize; this.src = src; this.cachingStrategy = dfsClient.getDefaultReadCachingStrategy(); openInfo(); }
这个方法先是做了一些準备工作,然后调用openInfo()方法,openInfo()方法是一个执行绪安全的方法,作用是从namenode获取已打开的档案信息。其原始码如下。
/** * Grab the open-file info from namenode */ synchronized void openInfo() throws IOException, UnresolvedLinkException { lastBlockBeingWrittenLength = fetchLocatedBlocksAndGetLastBlockLength(); int retriesForLastBlockLength = dfsClient.getConf().retryTimesForGetLastBlockLength; while (retriesForLastBlockLength > 0) { // Getting last block length as -1 is a special case. When cluster // restarts, DNs may not report immediately. At this time partial block // locations will not be available with NN for getting the length. Lets // retry for 3 times to get the length. if (lastBlockBeingWrittenLength == -1) { DFSClient.LOG.warn("Last block locations not available. " + "Datanodes might not have reported blocks completely." + " Will retry for " + retriesForLastBlockLength + " times"); waitFor(dfsClient.getConf().retryIntervalForGetLastBlockLength); lastBlockBeingWrittenLength = fetchLocatedBlocksAndGetLastBlockLength(); } else { break; } retriesForLastBlockLength--; } if (retriesForLastBlockLength == 0) { throw new IOException("Could not obtain the last block locations."); } }
此方法有调用fetchLocatedBlocksAndGetLastBlockLength()方法获取块的位置信息。
private long fetchLocatedBlocksAndGetLastBlockLength() throws IOException { final LocatedBlocks newInfo = dfsClient.getLocatedBlocks(src, 0); if (DFSClient.LOG.isDebugEnabled()) { DFSClient.LOG.debug("newInfo = " + newInfo); } if (newInfo == null) { throw new IOException("Cannot open filename " + src); } if (locatedBlocks != null) { Iterator<LocatedBlock> oldIter = locatedBlocks.getLocatedBlocks().iterator(); Iterator<LocatedBlock> newIter = newInfo.getLocatedBlocks().iterator(); while (oldIter.hasNext() && newIter.hasNext()) { if (! oldIter.next().getBlock().equals(newIter.next().getBlock())) { throw new IOException("Blocklist for " + src + " has changed!"); } } } locatedBlocks = newInfo; long lastBlockBeingWrittenLength = 0; if (!locatedBlocks.isLastBlockComplete()) { final LocatedBlock last = locatedBlocks.getLastLocatedBlock(); if (last != null) { if (last.getLocations().length == 0) { if (last.getBlockSize() == 0) { // if the length is zero, then no data has been written to // datanode. So no need to wait for the locations. return 0; } return -1; } final long len = readBlockLength(last); last.getBlock().setNumBytes(len); lastBlockBeingWrittenLength = len; } } currentNode = null; return lastBlockBeingWrittenLength; }
getLocatedBlocks方法可以获取块的位置信息。LocatedBlocks类是许多块的位置信息的集合。因为从此类的源码可以发现有这个一个私有属性:
private final List<LocatedBlock> blocks; // array of blocks with prioritized locations
通过档案名称,FSDataInputStream类可以获取相档案内容,也可以充当namenode与datanode桥樑。
将档案内容在标準输出显示
因为之前已经获得了一个FSDataInputStream,所以,我们可以调用方法copyBytes将FSDataInputStream拷贝到标準输出流System.out显示。
public static void copyBytes(InputStream in, OutputStream out, int buffSize, boolean close) throws IOException { try { copyBytes(in, out, buffSize); if(close) { out.close(); out = null; in.close(); in = null; } } finally { if(close) { closeStream(out); closeStream(in); } } }
此方法里又调用了另外一个copyBytes方法,作用同样是从一个流拷贝到另外一个流。
public static void copyBytes(InputStream in, OutputStream out, int buffSize) throws IOException { PrintStream ps = out instanceof PrintStream ? (PrintStream)out : null; byte buf[] = new byte[buffSize]; int bytesRead = in.read(buf); while (bytesRead >= 0) { out.write(buf, 0, bytesRead); if ((ps != null) && ps.checkError()) { throw new IOException("Unable to write to output stream."); } bytesRead = in.read(buf); } }
先从输入流中读取buffSize大小的数据到缓冲里面,然后将缓冲里的数据写入到输出流out里。一直循环,直到从输入流中读到缓冲里的位元组长度为0,表示输入流里的数据已经读取完毕。