現(xiàn)音樂文件傳輸系統(tǒng))
場(chǎng)景客戶端輸入音樂文件名服務(wù)端檢索并返回對(duì)應(yīng)的音樂文件若文件不存在則返回默認(rèn)音樂。客戶端接收后保存至本地指定目錄。一、需求分析與技術(shù)方案需求點(diǎn)技術(shù)方案網(wǎng)絡(luò)通信Java SocketServerSocket數(shù)據(jù)傳輸DataInputStream/DataOutputStream支持 UTF 字符串 二進(jìn)制字節(jié)流文件讀寫自定義StreamUtils工具類或 Apache Commons IO文件不存在兜底服務(wù)端維護(hù)一個(gè)default.mp3作為默認(rèn)返回本地存儲(chǔ)客戶端指定路徑E:\核心設(shè)計(jì)先傳文件名字符串再傳文件長(zhǎng)度long最后傳文件內(nèi)容字節(jié)流。這樣客戶端能精確知道何時(shí)讀取結(jié)束避免粘包/拆包問題。二、項(xiàng)目結(jié)構(gòu)music-transfer/ ├── src/ │ ├── StreamUtils.java // 流處理工具類 │ ├── MusicServer.java // 服務(wù)端 │ └── MusicClient.java // 客戶端 └── server_music/ // 服務(wù)端音樂庫(kù)目錄 ├── 高山流水.mp3 └── default.mp3 // 默認(rèn)兜底音樂實(shí)現(xiàn)思路三、核心代碼實(shí)現(xiàn)3.1 StreamUtils.java —— 流處理工具類importjava.io.ByteArrayOutputStream;importjava.io.IOException;importjava.io.InputStream;/** * 流工具類負(fù)責(zé)將輸入流轉(zhuǎn)換為字節(jié)數(shù)組 * 提示中提到的 StreamUtils此處為教學(xué)版自定義實(shí)現(xiàn) */publicclassStreamUtils{/** * 將輸入流全部讀取為字節(jié)數(shù)組 * param is 輸入流 * return 字節(jié)數(shù)組 * 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 —— 服務(wù)端importjava.io.*;importjava.net.ServerSocket;importjava.net.Socket;/** * 音樂文件服務(wù)端 * 1. 監(jiān)聽 9999 端口 * 2. 接收客戶端發(fā)送的音樂文件名 * 3. 查找本地文件不存在則返回 default.mp3 * 4. 發(fā)送文件長(zhǎng)度 文件內(nèi)容 */publicclassMusicServer{// 服務(wù)端音樂庫(kù)根目錄privatestaticfinalStringMUSIC_DIRserver_music/;// 默認(rèn)兜底音樂privatestaticfinalStringDEFAULT_MUSICdefault.mp3;// 監(jiān)聽端口privatestaticfinalintPORT9999;publicstaticvoidmain(String[]args){System.out.println(音樂文件服務(wù)端已啟動(dòng)監(jiān)聽端口: PORT);try(ServerSocketserverSocketnewServerSocket(PORT)){while(true){// 等待客戶端連接單線程版教學(xué)演示用SocketsocketserverSocket.accept();System.out.println(客戶端已連接: socket.getInetAddress());handleClient(socket);}}catch(IOExceptione){e.printStackTrace();}}privatestaticvoidhandleClient(Socketsocket){try(DataInputStreamdisnewDataInputStream(socket.getInputStream());DataOutputStreamdosnewDataOutputStream(socket.getOutputStream())){// 1. 讀取客戶端請(qǐng)求的音樂文件名UTF格式StringmusicNamedis.readUTF();System.out.println(客戶端請(qǐng)求音樂: musicName);// 2. 構(gòu)建文件路徑并判斷是否存在FilemusicFilenewFile(MUSIC_DIRmusicName);if(!musicFile.exists()){System.out.println(文件不存在使用默認(rèn)音樂: DEFAULT_MUSIC);musicFilenewFile(MUSIC_DIRDEFAULT_MUSIC);}// 3. 讀取文件為字節(jié)數(shù)組使用 StreamUtilsbyte[]fileBytesStreamUtils.streamToByteArray(newFileInputStream(musicFile));// 4. 先發(fā)送文件長(zhǎng)度讓客戶端知道要讀多少字節(jié)dos.writeLong(fileBytes.length);// 5. 發(fā)送文件內(nèi)容dos.write(fileBytes);dos.flush();System.out.println(文件發(fā)送完成: 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. 連接服務(wù)端 * 2. 輸入音樂文件名并發(fā)送 * 3. 接收文件長(zhǎng)度 文件內(nèi)容 * 4. 保存到 E:\ 目錄 */publicclassMusicClient{privatestaticfinalStringSERVER_HOST127.0.0.1;privatestaticfinalintSERVER_PORT9999;privatestaticfinalStringSAVE_DIRe:\\\\;publicstaticvoidmain(String[]args){ScannerscannernewScanner(System.in);System.out.print(請(qǐng)輸入要下載的音樂文件名如高山流水.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(已連接服務(wù)端正在請(qǐng)求文件...);// 1. 發(fā)送音樂文件名dos.writeUTF(musicName);dos.flush();// 2. 讀取文件長(zhǎng)度longfileLengthdis.readLong();System.out.println(準(zhǔn)備接收文件大小: fileLength bytes);// 3. 讀取文件內(nèi)容使用 StreamUtils 一次性讀取byte[]fileDataStreamUtils.streamToByteArray(dis);// 安全校驗(yàn)實(shí)際讀取長(zhǎng)度應(yīng)與聲明長(zhǎng)度一致if(fileData.length!fileLength){System.out.println(警告接收數(shù)據(jù)長(zhǎng)度與預(yù)期不符);}// 4. 保存到本地 E:\StringsavePathSAVE_DIRmusicName;// 如果請(qǐng)求的文件不存在服務(wù)端返回的是 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();}}}四、運(yùn)行4.1 準(zhǔn)備工作在項(xiàng)目根目錄創(chuàng)建server_music/文件夾放入一首名為高山流水.mp3的音樂放入一首名為default.mp3的默認(rèn)音樂兜底用4.2 編譯與運(yùn)行# 編譯javac-dout src/*.java# 啟動(dòng)服務(wù)端先運(yùn)行java-cpout MusicServer# 啟動(dòng)客戶端另開終端java-cpout MusicClient4.3 測(cè)試輸入服務(wù)端行為客戶端結(jié)果高山流水.mp3找到文件直接發(fā)送E:\高山流水.mp3不存在的音樂.mp3文件不存在發(fā)送default.mp3E:\不存在的音樂.mp3內(nèi)容為默認(rèn)音樂五、技術(shù)要點(diǎn)深度解析5.1 為什么先傳長(zhǎng)度再傳內(nèi)容TCP 是流式協(xié)議沒有消息邊界。如果不先告知文件長(zhǎng)度客戶端在while(read ! -1)時(shí)會(huì)一直阻塞等待因?yàn)榉?wù)端不會(huì)關(guān)閉連接關(guān)閉連接就無(wú)法復(fù)用了。先傳long類型的長(zhǎng)度客戶端就可以精確讀取指定字節(jié)數(shù)這是網(wǎng)絡(luò)文件傳輸?shù)臉?biāo)準(zhǔn)做法。5.2 StreamUtils 的作用StreamUtils.streamToByteArray(InputStream)將任意輸入流轉(zhuǎn)換為字節(jié)數(shù)組。在本案例中服務(wù)端將FileInputStream→byte[]→ 網(wǎng)絡(luò)發(fā)送客戶端將SocketInputStream→byte[]→ 本地文件它屏蔽了底層緩沖區(qū)操作的細(xì)節(jié)讓代碼更簡(jiǎn)潔。5.3 異常與資源管理代碼中全部使用try-with-resources語(yǔ)法確保Socket、Stream在使用后自動(dòng)關(guān)閉避免資源泄漏。生產(chǎn)環(huán)境建議加上線程池處理多客戶端并發(fā)。六、擴(kuò)展思考如何支持多客戶端并發(fā)將handleClient邏輯放入ThreadPoolExecutor中執(zhí)行避免阻塞主線程。大文件傳輸內(nèi)存溢出怎么辦當(dāng)前版本用StreamUtils一次性讀取到內(nèi)存大文件應(yīng)改用邊讀邊發(fā)的方式每次讀寫 8KB 緩沖區(qū)。如何增加斷點(diǎn)續(xù)傳客戶端發(fā)送已接收的文件長(zhǎng)度Range請(qǐng)求服務(wù)端用FileInputStream.skip()跳過已傳字節(jié)。