HDFS读文件的简要代码如下:
public static void main(String[] args) throws Exception {
InputStream input;
OutputStream output;
Configuration conf = new Configuration();
conf.set("fs.defaultFS", "hdfs://hadoop66:8020");
FileSystem fs = FileSystem.get(conf);
output = new FileOutputStream("D:/1.txt");
input = fs.open(new Path("/user/1.txt"));// new DFSInputStream
byte[] buffer = new byte[1024];
int len = 0;
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
input.close();
output.flush();
output.close();
}
整个过程也比较复杂,我画了一个简要的源码分析流程图,来展示整个操作的过程,如下:
流程和源码简要分析如下:
1.FileSystem通过实例化DistributedFileSystem,DistriburedFileSystem内部有一个执行远程RPC调用的对象DFSClient dfs。通过调用dfs.open(getPathName§, bufferSize, verifyChecksum)来实例化一个DFSInputStream
2.DFSInputStream通过调用openInfo(false),然后调用dfsClient.getLocatedBlocks(src, 0),来连接NameNode获取文件的块信息。
3.NameNode的NameNodeRpcServer接收到请求后会检查是否具有读权限,文件是否存在等一系列操作,然后返回LocatedBlocks,包括了Block信息和DataNode的位置。
4.返回DFSInputStream后,然后调用read(buf)方法读取数据,第一次会调用BlockSeekTo(pos)来实例化一个BlockReader,BlockReader是一个接口,有几个实现类,这里用到的是BlockReaderRemote2,暂时还没有弄明白各个BlockReader之间的区别。
5.通过调用BlockReaderRemote2.newBlockReader来实例化BlockReader,这个过程会通过new Sender(out).readBlock来连接DataNode
6.DataNode接收到Socket的请求后,会创建一个DataXceuver来响应请求,然后通过判断op的类型来进行相应的操作。
7.通过new Sender(out).readBlock发送出去的op类型为READ_BLOCK,然后调用opReadBlock来进行相应的操作,然后实例化一个BlockSender来处理相应的请求。
8.实例化BlockSender对象后,给客户端返回成功信息
9.通过调用BlockReaderRemote2.read读取数据
10.BlockReaderRemote2.read方法会从curDataSlice读取数据,curDataSlice是一个ByteBuff缓冲对象,如果有数据,则直接冲里面取走buff缓冲区大小的数据。
11.如果没有,则会调用readNextPacket()从DataNode读取一个Packet来处理。
12.读取到buff字节大小的数据时,会返回读取数据的大小
这里还有一个疑问,就是上面只是一个块的读写过程,但一个文件会有多个块,关于怎么切换块的问题,我详细查找了一下代码原来是在这里有详细的讲解:
protected synchronized int readWithStrategy(ReaderStrategy strategy, int off, int len) throws IOException {
dfsClient.checkOpen();
if (closed.get()) {
throw new IOException("Stream closed");
}
Map<ExtendedBlock,Set<DatanodeInfo>> corruptedBlockMap = new HashMap<>();
failures = 0;
if (pos < getFileLength()) {//获取文件的长度
int retries = 2;
while (retries > 0) {
try {
//每次读取都会判断是否要切换块
if (pos > blockEnd || currentNode == null) {
currentNode = blockSeekTo(pos); //新建BlockReader
}
int realLen = (int) Math.min(len, (blockEnd - pos + 1L));
synchronized(infoLock) {
if (locatedBlocks.isLastBlockComplete()) {
realLen = (int) Math.min(realLen,
locatedBlocks.getFileLength() - pos);
}
}
int result = readBuffer(strategy, off, realLen, corruptedBlockMap);
if (result >= 0) {
pos += result;
} else {
throw new IOException("Unexpected EOS from the reader");
}
if (dfsClient.stats != null) {
dfsClient.stats.incrementBytesRead(result);
}
return result;
} catch (ChecksumException ce) {
throw ce;
} catch (IOException e) {
if (retries == 1) {
DFSClient.LOG.warn("DFS Read", e);
}
blockEnd = -1;
if (currentNode != null) { addToDeadNodes(currentNode); }
if (--retries == 0) {
throw e;
}
} finally {
reportCheckSumFailure(corruptedBlockMap,
currentLocatedBlock.getLocations().length);
}
}
}
return -1;
}
核心代码在这里:
if (pos > blockEnd || currentNode == null) {
currentNode = blockSeekTo(pos); //新建BlockReader
}
每次读取的时候,都会判断读取的位置是否超过块的位置,如果超过,则根据pos位置获取下一个块的位置开始读取,直到最后一个块结束