JAVA Socket通信 打造屬於自己的網盤
近一個月沒敲JAVA代碼了,最近老師布置了一個寫JAVA網盤的作業,總共花了十幾個小時,總算寫完了,debug真的累,感覺自己還是菜了,沒有那種有一個想法就能馬上用代碼實現的能力。。。。不扯了,下面開始正題。
功能介紹
- 支持1個客戶端,1個服務器端。服務器提供網盤空間。
- 首先運行服務器。服務器運行之後,客戶端運行網盤客戶端。
- 運行客戶端。用戶能夠輸入昵稱。確定,則連接到服務器。連接成功,即可出現客戶端面。
- 可以在網盤中新建文件夾,刪除空文件夾,重命名文件夾;可以將自己電腦上某個文件上傳到網盤中的某個文件夾下(支持單文件),可以刪除單個文件、重命名文件、下載單個文件。
- 可實現大文件傳輸
整體思路
大概分了這麽幾個類
服務器端
MainServer:
原來是想做個服務器界面的,但還是有點懶,就算了,所以這個類現在就用來創建Panserver對象
public class MainServer { private PanServer panServer;//服務器對象 public static void main(String[] args){ MainServer ms =new MainServer(); ms.actionServer(); } // 開啟服務器 publicvoid actionServer() { // 1.要得到服務器狀態 if (null == panServer) { panServer = new PanServer(8888); panServer.start(); } else if (panServer.isRunning()) {// 己經在運行 panServer.stopPanServer(); panServer = null; } } }
Panserver:
用於建立服務器SocketServer的類
package PanServer; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; /** * 〈服務器socketserver創建〉 * * @author ITryagain * @create 2018/12/5 * @since 1.0.0 */ public class PanServer extends Thread { private ServerSocket ss;//服務器對象 private int port;//端口 private boolean running=false;//服務器是否在運行中 PanServer(int port){ this.port=port; } public void run(){ setupServer(); } //在指定端口上啟動服務器 private void setupServer(){ try{ ss=new ServerSocket(this.port); running=true; System.out.println("服務器創建成功:"+this.port); while(running){ Socket client = ss.accept(); System.out.println("進入了一個客戶機對象:"+client.getRemoteSocketAddress()); ServerThread ct = new ServerThread(client); ct.start(); } }catch(IOException e){ // TODO Auto-generated catch block e.printStackTrace(); } } /* * 查詢服務器是否在運行 * * @return: true為運行中 */ public boolean isRunning() { return this.running; } // 關閉服務器 public void stopPanServer() { this.running = false; try { ss.close(); } catch (Exception e) {} } }
else
剩下的三個類就是服務器實現的關鍵了
其中最重要的是ServerThread類,該類用於實現與客戶端的通信,通過接收客戶端的指令進行不同的操作,其中函數如下圖所示
首先,我們在建立連接時,需要輸入用戶名,並創建一個文件夾,文件名為用戶名,因此,我們需要一個接收一開始發送的用戶名信息,寫在processSocket內
Socket sc=this.client; ins=sc.getInputStream(); ous=sc.getOutputStream(); //將輸入流ins封裝為可以讀取一行字符串也就是以\r\n結尾的字符串 BufferedReader brd=new BufferedReader(new InputStreamReader(ins)); sendMsg2Me("歡迎您使用!請輸入你的用戶名:"); User_name=brd.readLine(); System.out.println(User_name);
這樣我們就讀取了用戶名,讀取用戶名後,馬上就能創建文件夾
File directory = new File("D:\\"+User_name); if(!directory.exists()){ directory.mkdir(); }
然後就進入while循環,不斷從客戶端讀取用戶操作信息
String input=brd.readLine();//一行一行的讀取客戶機發來的消息 while(true) { System.out.println("服務器收到的是"+input); if((!this.upLoad)&&(!this.downLoad)){ check(input); } if(this.upLoad){//上傳中 UpLoad(input); } if(this.downLoad){//下載中 DownLoad(input); } input=brd.readLine();//讀取下一條 System.out.println(input);
}
這裏我用了三個函數來分別處理三種狀態,其中check函數用來解碼,這裏我給出其中新建文件夾的寫法示例,刪除和重命名與之類似
private void check(String input){ if(input.charAt(0)==‘~‘){ String tmp=input.substring(input.indexOf("~")+1,input.indexOf("#")); System.out.println(tmp); if(tmp.equals("downLoad")){ this.downLoad=true; }else if(tmp.equals("upLoad")){ this.upLoad=true; }else if(tmp.equals("new")){ //新建文件夾 System.out.println(input.substring(input.indexOf("#")+1)); File directory = new File(input.substring(input.indexOf("#")+1)); if(!directory.exists()){ directory.mkdir(); } }else if(tmp.equals("delete")){ //刪除文件夾 }else if(tmp.equals("change")){ //重命名文件夾 } } }
然後剩下的就是UpLoad和DownLoad函數了,這兩個函數分別對應了上傳和下載功能,我一開始把這兩個功能都放在一開始建立的SockerServer裏面了,結果發現文件上傳了之後關閉流時把我線程也關了orz。。。還是太菜了,這種錯都能寫出來,百度了一番,看到好多人都是再開幾個端口解決的。。。一開始就想到這方法了,但不想這麽幹,總覺的應該還有更好的辦法,可最終還是決定用這種方法了(真香)。這裏就給出其中一個函數的寫法吧
private void UpLoad(String input){ System.out.println("上傳文件"); UpLoadThread upLoadThread = new UpLoadThread(8889,input); upLoadThread.start(); this.upLoad=false; }
既然給了UoLoad的寫法,就順便講講upLoadThread吧
/** * 〈服務器接受文件線程〉 * * @author ITryagain * @create 2018/12/8 * @since 1.0.0 */ public class UpLoadThread extends Thread{ private ServerSocket UpLoadServer; private int port; private String input; private FileOutputStream fos; UpLoadThread(int port,String input){ this.port=port; this.input=input; } public void run(){ } private static DecimalFormat df = null; static { // 設置數字格式,保留一位有效小數 df = new DecimalFormat("#0.0"); df.setRoundingMode(RoundingMode.HALF_UP); df.setMinimumFractionDigits(1); df.setMaximumFractionDigits(1); } /** * 格式化文件大小 * @param length * @return */ private String getFormatFileSize(long length) { } }
大致函數就是這樣的,其中run方法裏面就是文件接收了,(如果發現缺了什麽自己補一補,就一個變量的申明沒加上去)
try{ UpLoadServer = new ServerSocket(port); socket = UpLoadServer.accept(); dis = new DataInputStream(socket.getInputStream()); //文件名和長度 String fileName = input.substring(input.indexOf("#")+1); long fileLength = dis.readLong(); File file = new File(fileName); fos = new FileOutputStream(file); //開始接收文件 byte[] bytes = new byte[1024]; int length=0; while((length = dis.read(bytes, 0, bytes.length)) != -1) { fos.write(bytes, 0, length); fos.flush(); } System.out.println("======== 文件接收成功 [File Name:" + fileName + "] [Size:" + getFormatFileSize(fileLength) + "] ========"); try { if(fos!=null) fos.close(); if(dis != null) dis.close(); if(socket !=null) socket.close(); if(UpLoadServer!=null) UpLoadServer.close(); } catch (Exception e) {} }catch(IOException e){ }
然後就是 getFormatFileSize() 函數了,這個函數是用來幹嘛的呢?就是用來轉化一下文件大小單位的,不然到時候一個幾 GB 的文件顯示的就是 *****
******B了,那麽長一串,看著也不舒服。
private String getFormatFileSize(long length) { double size = ((double) length) / (1 << 30); if(size >= 1) { return df.format(size) + "GB"; } size = ((double) length) / (1 << 20); if(size >= 1) { return df.format(size) + "MB"; } size = ((double) length) / (1 << 10); if(size >= 1) { return df.format(size) + "KB"; } return length + "B"; }
服務器端剩下沒講的代碼其實都差不多,就自己去實現吧
客戶端
客戶端就比較麻煩了,尤其是這個界面,花了我老半天時間
MainClient:
這個類就是客戶端用於創建界面以及管理線程的類了,界面我是用JTree來實現的,看下函數先吧
public class MainClient extends JFrame implements ActionListener, TreeModelListener { private JTree tree; private int ServerIP = 8888; private JLabel statusLabel; private DefaultTreeModel treeModel; private String oldNodeName; private OutputStream ous; private Socket client; private String name; private String stress = "D:\\"; private String downLoadStress="D:\\下載\\"; public static void main(String[] args){ MainClient mc=new MainClient(); mc.showLoginUI(); } public void showLoginUI(){ } // 登陸事件處理 private void loginAction() { } //顯示操作窗口 private void showMainUI() { } @Override public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("新建文件夾")) { } if (e.getActionCommand().equals("刪除文件夾")) { } if (e.getActionCommand().equals("上傳文件")) { } if(e.getActionCommand().equals("下載文件")){ } } @Override /* * 修改文件名字 */ public void treeNodesChanged(TreeModelEvent e) { } //選擇上傳文件 public void chooseSendFile(){ } public void sendFile(File file,String path) throws IOException{ } private String getSendPath(TreePath parentPath){ } //選擇下載文件 public void chooseDownLoadFile() { } //下載文件 public void downloadFile(String path) throws IOException{ } private String getDownLoadPath(TreePath parentPath){ } private void newFile(String path){ } private void deleteFile(String path){ } private void ChangeFileName(String NewName,String path){ } @Override public void treeNodesInserted(TreeModelEvent e) { } @Override public void treeNodesRemoved(TreeModelEvent e) { } @Override public void treeStructureChanged(TreeModelEvent e) { } }
先來看看界面吧,打開後你可能只能看到三個按鈕,拉動一下框框就能看到第四個了,大小設置有點問題。
private void showMainUI() { JFrame frame=new JFrame("網盤"); Container contentPane = frame.getContentPane(); DefaultMutableTreeNode root = new DefaultMutableTreeNode(name); tree = new JTree(root); tree.addMouseListener(new MyTreeMouseListener(oldNodeName)); treeModel = (DefaultTreeModel)tree.getModel(); treeModel.addTreeModelListener(this); tree.setEditable(true); tree.getCellEditor().addCellEditorListener(new MyTreeCellEditorListener(tree)); JScrollPane scrollPane = new JScrollPane(); scrollPane.setViewportView(tree); JPanel toolBarPanel = new JPanel(); JButton b = new JButton("新建文件夾"); b.addActionListener(this); toolBarPanel.add(b); b = new JButton("刪除文件夾"); b.addActionListener(this); toolBarPanel.add(b); b = new JButton("上傳文件"); b.addActionListener(this); toolBarPanel.add(b); b = new JButton("下載文件"); b.addActionListener(this); toolBarPanel.add(b); statusLabel = new JLabel("Action"); contentPane.add(toolBarPanel, BorderLayout.NORTH); contentPane.add(scrollPane, BorderLayout.CENTER); contentPane.add(statusLabel, BorderLayout.SOUTH); frame.pack(); frame.setVisible(true); frame.requestFocus(); frame.setSize(400, 400); frame.setLocationRelativeTo(null); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); tree.setRootVisible(false); }
裏面兩監聽類的實現(提醒一下,可能到後面你做重命名文件夾會遇到坑,跟其中一個監聽類有關,提醒一下oldNodeName是用來記錄修改前的文件夾名稱的,想辦法獲取這個變量的值就能很快實現文件夾重命名的功能),還有文件路徑的實現也自己好好想想
至於界面的其它一些功能實現可以去看這篇博客(包括兩個監聽類的實現以及 actionPerformed() 和 treeNodesChanged() 等函數的實現)https://www.cnblogs.com/freshier/p/4614811.html
然後這裏給出NewFile()函數寫法
private void newFile(String path){ String fileName = "~new#"+path+"\r\n"; System.out.println(fileName); try { this.ous.write(fileName.getBytes()); this.ous.flush(); } catch (IOException e) { e.printStackTrace(); } }
至於 delete 和 change 的寫法與此類似
前面我給出了服務器上傳文件的類,這裏就給出客戶端上傳文件的類的寫法
先看下 sendFile() 函數
public void sendFile(File file,String path) throws IOException{ String fileName = "~upLoad#"+path+"\\"+file.getName()+"\r\n"; System.out.println(fileName); this.ous.write(fileName.getBytes()); this.ous.flush(); FileSendThread send_socket = new FileSendThread(8889,file,path); send_socket.start(); }
FileSendThread:
客戶端上傳文件的類
public class FileSendThread extends Thread { private Socket fileSendSocket; private int port; private String path; private File file; private OutputStream ous; private FileInputStream fis; private DataOutputStream dos; FileSendThread(int port, File file,String path){ this.port=port; this.file=file; this.path=path; } public void run(){ try{ fileSendSocket = new Socket("localhost",this.port); // 發送: 文件名稱 文件長度 this.ous = fileSendSocket.getOutputStream(); dos = new DataOutputStream(this.ous); dos.writeLong(file.length()); //開始傳輸文件 System.out.println("======開始傳輸文件======="); byte[] bytes = new byte[1024]; int length; long progress = 0; fis = new FileInputStream(file); while((length=fis.read(bytes,0,bytes.length))!=-1){ dos.write(bytes,0,length); dos.flush(); progress = progress + length; System.out.print("| " + (100*progress/file.length()) + "% |"); } System.out.println(); System.out.println("======== 文件傳輸成功 ========"); }catch(IOException e1){ }finally { try { if (fis != null) fis.close(); if (dos != null) dos.close(); }catch(IOException e){ // TODO Auto-generated catch block e.printStackTrace(); } } } }
至於下載功能,與上傳類似,把服務器和客戶端上傳類中的部分代碼換一換就好了。
到了這裏你們是否還記得我們在通信連接建立時需要傳輸用戶名?實現方式如下
登陸框的實現,我這裏算是偷懶了,直接利用了 JOptionPane
public void showLoginUI(){ name = JOptionPane.showInputDialog("請輸入用戶名"); System.out.println(name); loginAction(); }
// 登陸事件處理 private void loginAction() { try { this.client = new Socket("localhost",ServerIP); if(loginServer()){ showMainUI(); }else{ JOptionPane.showMessageDialog(null,"登陸失敗","確定",JOptionPane.WARNING_MESSAGE); } } catch (IOException e) { JOptionPane.showMessageDialog(null,"連接失敗","確定",JOptionPane.WARNING_MESSAGE); } }
private boolean loginServer(){ try{ this.ous = this.client.getOutputStream(); String _name=name+"\r\n"; this.ous.write(_name.getBytes()); this.ous.flush(); return true; }catch(IOException e){ return false; } }
順便介紹幾個方法
文件夾重命名
File directory = new File("D:\a.txt"); directory.renameTo(new File("D:\b.txt"));
這裏實現的是將a.txt重命名為b.txt,對文件夾也有效
文件夾刪除
boolean success = (new File("D:\a.txt").delete(); if(success){ System.out.println("刪除成功"); }else{ ystem.out.println("刪除失敗"); }
還有String的幾個操作
1、我的編碼
- 上傳文件 ~upLoad#文件路徑+文件
- 下載文件 ~downLoad#文件路徑+文件
- 文件重命名 ~change#文件路徑+原文件名@文件路徑+新文件名
- 新建文件夾 ~new#文件路徑+文件名
- 刪除文件夾 ~delete#文件路徑+文件名
2.String中的 subString() 和 indexOf() 函數
總結
到這裏,我的網盤介紹算是完了,因為這是作業並且還沒檢查的緣故,就沒有把所有代碼放出來了,就介紹了下思路,順便給將要寫網盤或者正在寫網盤的你們一點思路,相信有了這些思路,你們能很快地寫完網盤,或者有了寫的思路。太久沒寫代碼果然不行,搞了一個多月的強化學習、機器學習和數據挖掘,差點就不會寫代碼了(面向搜索引擎編程感覺還行。。。百度有時很強大,可能只是你沒學會正確的搜索的姿勢)
JAVA Socket通信 打造屬於自己的網盤