简介:这份资源面向大数据与分布式系统方向的开发者及高校学生,提供一套基于Hadoop平台、借助SSH框架实现HDFS网盘的完整项目源码,帮助读者理解分布式存储与远程安全访问的落地方式。压缩包共629个文件,约37.4MB,涵盖60个java源文件、61个class编译文件、51个jsp页面、97个jar依赖包,以及png、gif等界面素材和xml、js、css等前端与配置资源,结构上包含文件服务、用户管理、HDFS操作与监控等模块,便于对照学习SSH与HDFS的整合思路。目前已有132人学习下载。读者可从中获取Hadoop集群配置、SSH免密通信、HDFS客户端工具开发及网盘功能实现的完整参考,适合作为课程设计、毕业设计或分布式存储实践的对照案例,也可用于排查节点通信与文件读写中的常见问题。
1. 从一份课程设计说起:Hadoop + SSH 框架做 HDFS 网盘到底在做什么
很多人第一次看到「基于 Hadoop,利用 SSH 框架实现 HDFS 网盘」这个题目,第一反应是懵的:SSH 不是远程登录工具吗,怎么就成了「框架」?其实这里的 SSH 指的是 Struts2 + Spring + Hibernate 这套经典的 Java Web 三层组合,和 Linux 里那个ssh命令完全是两码事,这也是这个标题最容易让人翻车的地方。它要解决的核心问题是:把 Hadoop 分布式文件系统 HDFS 当成后端存储,用一套 Java Web 应用把它包装成普通用户能用的网盘——上传、下载、浏览目录、删除、重命名,前端点一下,后端通过 HDFS Java API 落到集群上。
这套东西适合谁?适合正在做大数据课程设计的学生、想理解 HDFS 读写流程的初学者,以及需要给内部团队搭一个轻量文件共享入口的工程师。它不追求高并发和商业级体验,但能把「HDFS 编程实践」这条链路完整走通:从伪分布式搭建、SSH 免密、HDFS 常用命令,到用 Java 代码调FileSystem对象做增删改查。下面我按自己踩过的顺序,把这条路拆开讲清楚。
2. 环境先立住:Hadoop 伪分布式搭建与 SSH 免密登录
2.1 为什么先搭伪分布式而不是直接上集群
做课程设计或者本地验证,没必要一上来就搞多节点。伪分布式(Pseudo-Distributed)在单台机器上把 NameNode、DataNode、ResourceManager、NodeManager 全部跑起来,用的是真实 HDFS 的读写流程,只是副本数默认 1。它的价值在于:你能用hdfs dfs命令真实操作文件,也能用 Java API 连上去,代码和真集群几乎不用改。常见做法是 Ubuntu 或 CentOS 8 上装 JDK8 + Hadoop 3.x,配好core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml四个文件。
这里有个血泪经验:Hadoop 对 JDK 版本敏感,JDK8 最稳,JDK11 以上容易在启动时抛IllegalAccessError。另外主机名不要用下划线,hostname里带_会导致 DataNode 注册失败,这是很多人排查半天的玄学问题。
2.2 SSH 免密登录:Hadoop 启动脚本的硬依赖
Hadoop 的start-dfs.sh、stop-dfs.sh这些脚本内部会通过 SSH 去连本机或其他节点,所以必须配免密。注意,这里的 SSH 是 Linux 的远程登录工具,和后面 Web 层的 SSH 框架同名但无关,别搞混。
# 生成密钥对,一路回车即可 ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa # 把公钥追加到授权文件,实现本机免密 cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys # 验证:第一次会问 yes/no,之后应直接登录不输密码 ssh localhost逻辑说明:ssh-keygen生成私钥id_rsa和公钥id_rsa.pub,-P ''表示空密码短语,方便脚本自动调用。authorized_keys权限必须是 600,否则 SSH 会以「权限过于开放」为由拒绝,报Authentication refused: bad ownership or modes。验证时如果还要输密码,检查~/.ssh目录权限是否为 700。
参数上,-t rsa指定密钥类型,现在也可以换ed25519,但 Hadoop 生态里 rsa 兼容性最好。如果你在 CentOS 8 上遇到ssh localhost连不上,先确认sshd服务在跑:sudo systemctl restart sshd,再看防火墙有没有拦 22 端口。
2.3 四个配置文件的最小改动
伪分布式不需要改太多,关键是core-site.xml里指定fs.defaultFS为hdfs://localhost:9000,hdfs-site.xml里把dfs.replication设为 1。改完执行hdfs namenode -format格式化,再start-dfs.sh。用jps应该能看到 NameNode、DataNode、SecondaryNameNode 三个进程。少一个就去看logs目录下对应的.log,八成是端口占用或者主机名解析问题。
提示:格式化只能做一次,重复格式化会导致 NameNode 和 DataNode 的 clusterID 不一致,DataNode 起不来。真格式化了就删掉
data和name目录重来。
3. 后端打通:用 HDFS Java API 封装网盘的文件操作
3.1 选型理由:为什么用 FileSystem 而不是 WebHDFS
HDFS 对外提供两种编程入口:一是 REST 风格的 WebHDFS,二是原生 Java API。网盘这种场景,后端本来就是 Java Web,直接引hadoop-client依赖用FileSystem对象最顺手,不用额外处理 HTTP 认证和 JSON 解析。WebHDFS 更适合非 Java 语言或者跨网络调用。所以这里选 Java API,配合 Spring 管理FileSystem这个重量级对象的生命周期。
核心依赖就一个,版本和你集群保持一致:
<dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-client</artifactId> <version>3.3.4</version> </dependency>版本号必须和集群里hadoop version输出一致,否则会出现Protocol mismatch或者序列化异常。这是新手最容易忽略的点,本地能跑、一上服务器就崩,多半是版本对不上。
3.2 上传与下载:把 InputStream 交给 HDFS
网盘最核心的两个动作就是上传和下载。上传是把本地文件流写进 HDFS,下载是反过来。下面这段代码是 Service 层的骨架,用 Spring 注入FileSystem。
// 上传:localPath 本地文件,hdfsPath HDFS 目标路径 public void upload(String localPath, String hdfsPath) throws IOException { // 目标已存在则覆盖 Path dst = new Path(hdfsPath); try (InputStream in = new FileInputStream(localPath)) { // 第三个参数 true 表示覆盖已存在文件 fs.copyFromLocalFile(false, true, new Path(localPath), dst); } } // 下载:把 HDFS 文件写到本地输出流 public void download(String hdfsPath, OutputStream out) throws IOException { Path src = new Path(hdfsPath); try (FSDataInputStream in = fs.open(src)) { IOUtils.copyBytes(in, out, 4096, false); } }逻辑说明:copyFromLocalFile的第一个布尔参数是「是否删除本地源文件」,网盘场景必须传false,否则用户上传完本地文件就没了,这是灾难级 bug。第二个参数是「是否覆盖」,传true。IOUtils.copyBytes的第三个参数是缓冲区大小,4096 是默认值,大文件可以调到 8192 或 16384 提升吞吐,但别太大,否则内存吃紧。第四个参数false表示不自动关闭流,交给 try-with-resources 处理。
参数上,fs.open返回的FSDataInputStream支持seek,做断点续传时用得上。如果下载大文件出现Premature EOF,通常是 DataNode 磁盘满或者网络抖动,先查hdfs dfsadmin -report看剩余空间。
3.3 目录浏览与删除:listStatus 和 delete 的边界
网盘要展示文件列表,用listStatus拿到FileStatus数组,里面包含文件名、大小、修改时间、副本数等信息。
// 列出目录下所有文件,过滤掉目录本身 public List<Map<String, Object>> list(String hdfsDir) throws IOException { List<Map<String, Object>> result = new ArrayList<>(); for (FileStatus status : fs.listStatus(new Path(hdfsDir))) { Map<String, Object> item = new HashMap<>(); item.put("name", status.getPath().getName()); item.put("size", status.getLen()); item.put("isDir", status.isDirectory()); item.put("mtime", status.getModificationTime()); result.add(item); } return result; } // 删除,recursive 决定是否递归删目录 public boolean delete(String hdfsPath, boolean recursive) throws IOException { return fs.delete(new Path(hdfsPath), recursive); }逻辑说明:listStatus返回的是该目录的直接子项,不递归。如果目录不存在会抛FileNotFoundException,前端调用前最好先fs.exists判断一下。delete的第二个参数很关键:删空目录传false可以,删非空目录必须传true,否则返回false且不报错,用户以为删了其实没删,这是典型的静默失败。删除操作不可逆,HDFS 没有回收站(除非开了 Trash 机制),所以生产环境建议先做二次确认。
注意:
FileSystem对象是线程安全的,但不要每次请求都FileSystem.get()新建,开销很大。用 Spring 单例管理,配置里指定fs.defaultFS,让它连到你的 NameNode。
4. Web 层整合:Struts2 + Spring 把 HDFS 操作暴露成网盘接口
4.1 SSH 框架里各层怎么分工
这套「SSH 框架」里,Struts2 负责接收前端请求和结果跳转,Spring 负责依赖注入和事务,Hibernate 理论上管关系型数据库。但网盘的文件元数据其实都在 HDFS 里,Hibernate 在这里更多是存用户表、权限表、操作日志。很多人纠结 Hibernate 要不要用,我的建议是:如果只是课程设计,用户登录和文件记录用 Hibernate 存 MySQL 就够了,别硬套复杂映射。
分层上,Action 层只做参数校验和调用 Service,Service 层封装 HDFS 操作,DAO 层走 Hibernate。这样职责清晰,也符合课程设计对「框架整合」的考察点。
4.2 Struts2 Action 接收上传文件
Struts2 的文件上传靠拦截器,Action 里定义三个属性:File类型的上传文件、文件名、内容类型。
public class FileAction extends ActionSupport { private File upload; // 临时文件 private String uploadFileName; // 原始文件名 private String uploadContentType; // MIME 类型 private String currentDir = "/"; // 当前目录 // setter 省略,Struts2 靠 setter 注入 public String doUpload() { // 目标路径拼上原始文件名 String dst = currentDir + uploadFileName; try { hdfsService.upload(upload.getAbsolutePath(), dst); return SUCCESS; } catch (IOException e) { addActionError("上传失败:" + e.getMessage()); return ERROR; } } }逻辑说明:Struts2 会把上传的文件先落到临时目录,upload指向这个临时文件,uploadFileName才是用户看到的原始名。所以上传时要用upload.getAbsolutePath()作为本地源路径,目标路径用currentDir + uploadFileName拼接。注意文件名里如果有中文或特殊字符,HDFS 路径可能出问题,建议做一次 URL 编码或者用 UUID 重命名。
参数上,struts.xml里要配fileUpload拦截器,并设置maximumSize限制单文件大小,默认是 2MB,网盘场景肯定不够,调到 100MB 以上。同时struts.multipart.maxSize也要改,两个地方都改才生效,只改一个会报File too large。
4.3 Spring 管理 FileSystem 单例
FileSystem的创建成本高,必须交给 Spring 做单例。配置类里这样写:
@Configuration public class HadoopConfig { @Value("${hdfs.defaultFS}") private String defaultFS; @Bean public FileSystem fileSystem() throws IOException { Configuration conf = new Configuration(); conf.set("fs.defaultFS", defaultFS); // 避免本地库缺失警告 conf.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem"); return FileSystem.get(conf); } }逻辑说明:FileSystem.get(conf)会根据fs.defaultFS的 scheme 返回对应实现,hdfs://开头就返回DistributedFileSystem。显式设置fs.hdfs.impl是为了避免某些环境下加载到错误的实现类。这个 Bean 是单例,整个应用共用一个连接,性能最好。
参数上,defaultFS建议写在application.properties里,格式hdfs://你的主机名:9000。如果连不上,先确认 NameNode 的 9000 端口通不通,telnet一下。另外客户端机器要能解析 NameNode 的主机名,/etc/hosts里加一条映射,否则会报UnknownHostException。
5. 避坑排查:HDFS 网盘落地时最容易翻车的 5 个点
5.1 上传成功但文件大小是 0
现象:前端提示上传成功,去 HDFS 一看文件存在但getLen()返回 0。原因通常是 Struts2 的临时文件在 Action 执行完就被删了,而你的上传逻辑是异步的或者延迟执行的,流已经关了。解决:在 Action 方法内同步完成上传,不要丢给线程池异步处理;或者先把临时文件复制到自己的目录再操作。
5.2 下载中文文件名乱码
现象:下载下来的文件名变成%E6%96%87%E4%BB%B6这种。原因是 HTTP 响应头里的Content-Disposition没有正确编码。解决:用URLEncoder.encode(fileName, "UTF-8")编码后再塞进响应头,同时把filename*用 RFC 5987 格式写,浏览器才认。
5.3 DataNode 启动后立刻挂掉
现象:jps里 DataNode 一闪而过,日志报Incompatible clusterID。原因是重复执行了hdfs namenode -format,NameNode 的 clusterID 变了,DataNode 还是旧的。解决:删掉dfs.datanode.data.dir指向的目录,重新启动 DataNode,它会重新注册。记住格式化只做一次。
5.4 SSH 免密配了但 Hadoop 启动还要密码
现象:ssh localhost能免密,但start-dfs.sh还是提示输密码。原因是脚本用的是ssh到当前主机名而不是localhost,而你的公钥只对localhost生效。解决:把公钥也追加到authorized_keys对应的主机名解析上,或者直接ssh-copy-id 你的主机名。检查hostname和/etc/hosts是否一致。
5.5 大文件上传内存溢出
现象:上传几百 MB 文件时抛OutOfMemoryError。原因是代码里用了FileUtils.readFileToByteArray把整个文件读进内存。解决:全程用流式处理,copyFromLocalFile本身就是流式的,别自己再包一层字节数组。Struts2 的maximumSize也要同步调大,否则拦截器层就拦掉了。
6. 进阶技巧:用副本数和块大小调优你的网盘体验
把基本功能跑通之后,真正拉开差距的是对 HDFS 参数的把控。网盘场景下,用户上传的文件大小差异极大,从几 KB 的文档到几个 GB 的视频都有,统一用默认的 128MB 块大小和 3 副本并不划算。我的习惯是按目录做差异化配置:普通文档目录副本数设 2 省空间,重要资料目录设 3 保可靠,临时目录设 1。
设置副本数用fs.setReplication(path, (short) 2),注意这个操作是异步的,NameNode 会调度 DataNode 去复制,不会立刻生效。块大小在写入时通过Configuration指定,但已经写入的文件改不了块大小,只能重写。所以上传前就要想清楚,别等文件堆了几百 GB 再后悔。
验证副本是否生效,用hdfs fsck /path -files -blocks看每个块的副本分布,输出里Live_repl就是当前存活副本数。如果小于你设的值,说明有 DataNode 掉线或者磁盘满了,去hdfs dfsadmin -report里看Under replicated blocks的数量。
还有一个容易被忽略的点:小文件问题。网盘里如果全是几 KB 的文件,每个文件都会在 NameNode 里占一条元数据,几十万个小文件会把 NameNode 内存吃光。常见做法是定期用 MapReduce 或 Spark 做小文件合并,把同目录下的小文件打包成一个大文件(比如 SequenceFile 或 HAR 归档)。课程设计里不一定要做,但面试被问到「HDFS 为什么不适合存大量小文件」时,这就是标准答案。
最后说个我自己的习惯:每次改完 HDFS 配置,先在一份测试目录上验证读写,确认没问题再动生产目录。HDFS 的删除没有后悔药,skipTrash一旦生效就是真删。我一般会在 Service 层加一层软删除逻辑,删除时先移到/trash目录,定时任务再清理,给自己留条后路。这套方案值不值得做?如果你要吃透 HDFS 读写流程和 Java API,它是最短的路径;如果只是想要个网盘,现成方案更省事。希望帮到你。
本文还有配套的精品资源,点击获取