
场景客户端输入音乐文件名服务端检索并返回对应的音乐文件若文件不存在则返回默认音乐。客户端接收后保存至本地指定目录。一、需求分析与技术方案需求点技术方案网络通信Java SocketServerSocket数据传输DataInputStream/DataOutputStream支持 UTF 字符串 二进制字节流文件读写自定义StreamUtils工具类或 Apache Commons IO文件不存在兜底服务端维护一个default.mp3作为默认返回本地存储客户端指定路径E:\核心设计先传文件名字符串再传文件长度long最后传文件内容字节流。这样客户端能精确知道何时读取结束避免粘包/拆包问题。二、项目结构music-transfer/ ├── src/ │ ├── StreamUtils.java // 流处理工具类 │ ├── MusicServer.java // 服务端 │ └── MusicClient.java // 客户端 └── server_music/ // 服务端音乐库目录 ├── 高山流水.mp3 └── default.mp3 // 默认兜底音乐实现思路三、核心代码实现3.1 StreamUtils.java —— 流处理工具类importjava.io.ByteArrayOutputStream;importjava.io.IOException;importjava.io.InputStream;/** * 流工具类负责将输入流转换为字节数组 * 提示中提到的 StreamUtils此处为教学版自定义实现 */publicclassStreamUtils{/** * 将输入流全部读取为字节数组 * param is 输入流 * return 字节数组 * throws IOException IO异常 */publicstaticbyte[]streamToByteArray(InputStreamis)throwsIOException{ByteArrayOutputStreambosnewByteArrayOutputStream();byte[]buffernewbyte[1024];intlen;while((lenis.read(buffer))!-1){bos.write(buffer,0,len);}byte[]arraybos.toByteArray();bos.close();returnarray;}}3.2 MusicServer.java —— 服务端importjava.io.*;importjava.net.ServerSocket;importjava.net.Socket;/** * 音乐文件服务端 * 1. 监听 9999 端口 * 2. 接收客户端发送的音乐文件名 * 3. 查找本地文件不存在则返回 default.mp3 * 4. 发送文件长度 文件内容 */publicclassMusicServer{// 服务端音乐库根目录privatestaticfinalStringMUSIC_DIRserver_music/;// 默认兜底音乐privatestaticfinalStringDEFAULT_MUSICdefault.mp3;// 监听端口privatestaticfinalintPORT9999;publicstaticvoidmain(String[]args){System.out.println(音乐文件服务端已启动监听端口: PORT);try(ServerSocketserverSocketnewServerSocket(PORT)){while(true){// 等待客户端连接单线程版教学演示用SocketsocketserverSocket.accept();System.out.println(客户端已连接: socket.getInetAddress());handleClient(socket);}}catch(IOExceptione){e.printStackTrace();}}privatestaticvoidhandleClient(Socketsocket){try(DataInputStreamdisnewDataInputStream(socket.getInputStream());DataOutputStreamdosnewDataOutputStream(socket.getOutputStream())){// 1. 读取客户端请求的音乐文件名UTF格式StringmusicNamedis.readUTF();System.out.println(客户端请求音乐: musicName);// 2. 构建文件路径并判断是否存在FilemusicFilenewFile(MUSIC_DIRmusicName);if(!musicFile.exists()){System.out.println(文件不存在使用默认音乐: DEFAULT_MUSIC);musicFilenewFile(MUSIC_DIRDEFAULT_MUSIC);}// 3. 读取文件为字节数组使用 StreamUtilsbyte[]fileBytesStreamUtils.streamToByteArray(newFileInputStream(musicFile));// 4. 先发送文件长度让客户端知道要读多少字节dos.writeLong(fileBytes.length);// 5. 发送文件内容dos.write(fileBytes);dos.flush();System.out.println(文件发送完成: musicFile.getName() (fileBytes.length bytes));}catch(IOExceptione){System.out.println(客户端处理异常: e.getMessage());}finally{try{socket.close();}catch(IOExceptione){e.printStackTrace();}}}}3.3 MusicClient.java —— 客户端importjava.io.*;importjava.net.Socket;importjava.util.Scanner;/** * 音乐文件客户端 * 1. 连接服务端 * 2. 输入音乐文件名并发送 * 3. 接收文件长度 文件内容 * 4. 保存到 E:\ 目录 */publicclassMusicClient{privatestaticfinalStringSERVER_HOST127.0.0.1;privatestaticfinalintSERVER_PORT9999;privatestaticfinalStringSAVE_DIRe:\\\\;publicstaticvoidmain(String[]args){ScannerscannernewScanner(System.in);System.out.print(请输入要下载的音乐文件名如高山流水.mp3: );StringmusicNamescanner.nextLine().trim();// 确保文件名非空if(musicName.isEmpty()){System.out.println(文件名不能为空);return;}try(SocketsocketnewSocket(SERVER_HOST,SERVER_PORT);DataOutputStreamdosnewDataOutputStream(socket.getOutputStream());DataInputStreamdisnewDataInputStream(socket.getInputStream())){System.out.println(已连接服务端正在请求文件...);// 1. 发送音乐文件名dos.writeUTF(musicName);dos.flush();// 2. 读取文件长度longfileLengthdis.readLong();System.out.println(准备接收文件大小: fileLength bytes);// 3. 读取文件内容使用 StreamUtils 一次性读取byte[]fileDataStreamUtils.streamToByteArray(dis);// 安全校验实际读取长度应与声明长度一致if(fileData.length!fileLength){System.out.println(警告接收数据长度与预期不符);}// 4. 保存到本地 E:\StringsavePathSAVE_DIRmusicName;// 如果请求的文件不存在服务端返回的是 default.mp3// 但客户端仍按原文件名保存也可改为保存为 default.mp3try(FileOutputStreamfosnewFileOutputStream(savePath)){fos.write(fileData);}System.out.println(文件下载成功保存路径: savePath);System.out.println(文件大小: fileData.length bytes);}catch(IOExceptione){System.out.println(下载失败: e.getMessage());e.printStackTrace();}finally{scanner.close();}}}四、运行4.1 准备工作在项目根目录创建server_music/文件夹放入一首名为高山流水.mp3的音乐放入一首名为default.mp3的默认音乐兜底用4.2 编译与运行# 编译javac-dout src/*.java# 启动服务端先运行java-cpout MusicServer# 启动客户端另开终端java-cpout MusicClient4.3 测试输入服务端行为客户端结果高山流水.mp3找到文件直接发送E:\高山流水.mp3不存在的音乐.mp3文件不存在发送default.mp3E:\不存在的音乐.mp3内容为默认音乐五、技术要点深度解析5.1 为什么先传长度再传内容TCP 是流式协议没有消息边界。如果不先告知文件长度客户端在while(read ! -1)时会一直阻塞等待因为服务端不会关闭连接关闭连接就无法复用了。先传long类型的长度客户端就可以精确读取指定字节数这是网络文件传输的标准做法。5.2 StreamUtils 的作用StreamUtils.streamToByteArray(InputStream)将任意输入流转换为字节数组。在本案例中服务端将FileInputStream→byte[]→ 网络发送客户端将SocketInputStream→byte[]→ 本地文件它屏蔽了底层缓冲区操作的细节让代码更简洁。5.3 异常与资源管理代码中全部使用try-with-resources语法确保Socket、Stream在使用后自动关闭避免资源泄漏。生产环境建议加上线程池处理多客户端并发。六、扩展思考如何支持多客户端并发将handleClient逻辑放入ThreadPoolExecutor中执行避免阻塞主线程。大文件传输内存溢出怎么办当前版本用StreamUtils一次性读取到内存大文件应改用边读边发的方式每次读写 8KB 缓冲区。如何增加断点续传客户端发送已接收的文件长度Range请求服务端用FileInputStream.skip()跳过已传字节。