在使用Hadoop的过程中,很容易通过FileSystem类的API来读取HDFS中的文件内容,读取内容的过程是怎样的呢?今天来分析客户端读取HDFS文件的过程,下面的一个小程序完成的功能是读取HDFS中某个目录下的文件内容,然后输出到控制台,代码如下:
public class LoadDataFromHDFS {
public static void main(String[] args) throws IOException {
new LoadDataFromHDFS().loadFromHdfs("hdfs://localhost:9000/user/wordcount/");
}
public void loadFromHdfs(String hdfsPath) throws IOException {
Configuration conf = new Configuration();
Path hdfs = new Path(hdfsPath);
FileSystem in = FileSystem.get(conf);
//in = FileSystem.get(URI.create(hdfsPath), conf);//这两行都会创建一个DistributedFileSystem对象
FileStatus[] status = in.listStatus(hdfs);
for(int i = 0; i < status.length; i++) {
byte[] buff = new byte[1024];
FSDataInputStream inputStream = in.open(status[i].getPath());
while(inputStream.read(buff) > 0) {
System.out.print(new String(buff));
}
inputStream.close();
}
}
}
FileSystem in = FileSystem.get(conf)这行代码创建一个DistributedFileSystem,如果直接传入一个Configuration类型的参数,那么默认会读取属性fs.default.name的值,根据这个属性的值创建对应的FileSystem子类对象,如果没有配置fs.default.name属性的值,那么默认创建一个org.apache.hadoop.fs.LocalFileSystem类型的对象。但是这里是要读取HDFS中的文件,所以在core-site.xml文件中配置fs.default.name属性的值为hdfs://localhost:9000,这样FileSystem.get(conf)返回的才是一个DistributedFileSystem类的对象。 还有一种创建DistributedFileSystem这种指定文件系统类型对像的方法是使用FileSystem.get(Configuration conf)的一个重载方法FileSystem.get(URI uri, Configuration),其实调用第一个方法时在FileSystem类中先读取conf中的属性fs.default.name的值,再调用的FileSystem.get(URI uri, Configuration)方法。
创建完了读取HDFS的DistributedFileSystem对象就可以按照HDFS的API对HDFS中的文件和目录进行操作了,如列出某个目录中文件和目录,读取文件,写入文件等。现在就来看看从HDFS中读取文件的过程。在上面的代码中首先列出了目录中的所有文件,然后逐个文件进行读取。与使用Java IO读取本地文件类似,首先根据文件的路径创建一个输入流,在Hadoop中使用FileSystem.open()方法来创建输入流,这个方法返回的是一个FSDataInputStream对象,关于Hadoop输入流类结构的设计,参考博文Hadoop源码分析之HDFS客户端的输入流类结构。
调用in.ipen(status[i].getPath());这行代码会返回一个FSDataInputStream对象,在上面的代码中实际是一个DFSClient.DFSDataInputStream类的对象,上面调用FileSystem.open()方法后会进入到DistributedFileSystem.open()方法中,代码如下:
public FSDataInputStream open(Path f, int bufferSize) throws IOException {
statistics.incrementReadOps(1);
return new DFSClient.DFSDataInputStream(
dfs.open(getPathName(f), bufferSize, verifyChecksum, statistics));
}
方法中,statistics是一个org.apache.hadoop.fs.FileSystem.Statistics类型,它实现了文件系统读写过程中的一些统计。这个方法虽然只有两行,但是却调用了多个方法,首先是getPathName(),获取path在NameNode中的路径,方法的代码如下:
private String getPathName(Path file) {
checkPath(file);
String result = makeAbsolute(file).toUri().getPath();
if (!DFSUtil.isValidName(result)) {
throw new IllegalArgumentException("Pathname " + result + " from " +
file+" is not a valid DFS filename.");
}
return result;
}
在这个方法中先调用了checkPath()方法,用于检查路径的合法性,例如保证用户不会在RawLocalFileSystem中创建“hdfs://xxx”这样的路径,其实就是检查了当前DistributedFileSystem对象的URI和根据传入的Path变量的URI。然后将Path对象file转换为文件具体的用/分隔的String路径,返回的结果是result。makeAbsolute()方法就是将相对路径转换为绝对路径,如果file本身就是一个绝对路径,那么就不用转换,在判断路径file是否是一个绝对路径的isAbsolute()方法中,还调用了一个名为hasWindowsDrive()的方法,其代码如下:
/**
* 判断是否是Windows上的磁盘路径
* @param path
* @param slashed 路径的第一个字符是否是‘/’
*/
private boolean hasWindowsDrive(String path, boolean slashed) {
if (!WINDOWS) return false;
int start = slashed ? 1 : 0;
return
path.length() >= start+2 && //路径长度大于2,例如长度大于C:的长度
(slashed ? path.charAt(0) == '/' : true) &&
path.charAt(start+1) == ':' &&
((path.charAt(start) >= 'A' && path.charAt(start) <= 'Z') ||
(path.charAt(start) >= 'a' && path.charAt(start) <= 'z'));
}
这个函数判断是否是在Windows操作系统上运行HDFS,Windows操作系统对磁盘的划分使用C、D、E等这些字符开头,会对这些情况做一些特殊的判断。getPathName()方法返回的字符串表示NameNode目录文件树中的文件路径,对于上面读取HDFS内容的代码中给出的路径hdfs://localhost:9000/user/wordcount/,则会返回/user/wordcount/这个字符串,user和wordcount为从根目录开始的两个目录。
getPathName()方法返回后,调用进入到DFSClient.open(String src, int buffersize, boolean verifyChecksum,FileSystem.Statistics s