記錄下之前所做的客戶端向服務端發送文件的小項目,總結下學習到的一些方法與思路。
注:本文參考自《黑馬程序員》視頻。
首先明確需求,在同一局域網下的機器人A想給喜歡了很久的機器人B發送情書,但是機器人B事先並不知道小A的心思,那么作為月老(紅娘)該如何幫助他們呢?
然后建立模型並拆分需求。這里兩台主機使用網線直連,在物理層上確保建立了連接,接下來便是利用相應的協議將信息從電腦A傳給電腦B。在這一步上,可以將此過程抽象為網絡+I/O(Input、Output)的過程。如果能在一台電腦上實現文件之間的傳輸,再加上相互的網絡協議,羞澀的A不就可以將情書發送給B了嗎?因此要先解決在一台電腦上傳輸信息的問題。為了在網絡上傳輸,使用必要的協議是必要的,TCP/IP協議簇就是為了解決計算機間通信而生,而這里主要用到UDP和TCP兩種協議。當小A可以向小B發送情書后,又出現了眾多的追求者,那么小B如何去處理這么多的並發任務呢?這時便要用到多線程的技術。
因此接下來將分別介紹此過程中所用到了I/O流(最基礎)、網絡編程(最重要)、多線程知識(較重要)和其中一些小技巧。
一、I/O流
I/O流用來處理設備之間的數據傳輸,Java對數據的傳輸通過流的方式。
流按操作數據分為兩種:字節流與字符流。如果數據是文本類型,那么需要使用字符流;如果是其他類型,那么使用字節流。簡單來說,字符流=字節流+編碼表。
流按流向分為:輸入流(將硬盤中的數據讀入內存),輸出流(將內存中的數據寫入硬盤)。
簡單來說,想要將某文件傳到目的地,需要將此文件關聯輸入流,然后將輸入流中的信息寫入到輸出流中。將目的關聯輸出流,就可以將信息傳輸到目的地了。
Java提供了大量的流對象可供使用,其中有兩大基類,字節流的兩個頂層父InputStream與OutputStream;字符流的兩個頂層父類Reader與Writer。這些體系的子類都以父類名作為后綴,而子類名的前綴就是該對象的功能。
流對象技巧
下提供4個明確的要點,只要明確以下幾點就能比較清晰的確認使用哪幾個流對象。
1, 明確源和目的(匯)
- 源 :InputStream Reader
- 目的 :OutputStream Writer
2, 明確數據是否是純文本數據
- 源 :是純文本 :Reader
非純文本 :InputStream
- 目的:是純文本 :Writer
非純文本 :OutputStream
到這里就可以明確需求中具體要用哪個體系。
3, 明確具體的設備。
- 源設備:
硬盤:File
鍵盤:System.in
內存:數組
網絡:Socket流
- 目的設備:
硬盤:File
控制台:System.out
內存:數組
網絡:Socket流
4,是否需要其他額外功能。
a) 是否需要高效(緩沖區)?
是,就加上buffer。
b) 是否需要轉換?
是
- 源:InputStreamReader 字節流->字符流
- 目的:OutputStreamWriter 字符流->字節流
在這里源為硬盤,目的也為硬盤,數據類型為情書,可能是文字的情書,也可能是小A唱的歌《情書》,因此使用字節流比較好。因此分析下來源是文件File+字節流InputStream->FileInputStream,目的是文件File+字節流OutputStream->FileOutputStream, 接下來便是數據如何從輸入流到輸出流的問題。
兩個流之間沒有直接關系,需要使用緩沖區來作為中轉,為了將讀入流與緩沖區關聯,首先自定義一個緩沖區數組byte[1024]。為了將讀入流與緩沖區關聯,使用fis.read(buf);為了將寫出流與緩沖區關聯,使用fos.write(buf,0,len)。為了將流中的文件寫出到輸出源中,要使用fos.flush或者fos.close。flush可以多次刷新,而close只能使用一次。
代碼如下,其中讀寫中會遇到的異常為了程序的清晰閱讀,直接拋出,建議實際使用時利用try,catch處理。
1 public class IODemo { 2 /** 3 * 需求:將指定文件從D盤目錄d:\1下移動到d:\2下 4 * @param args 5 * @throws IOException 6 */ 7 public static void main(String[] args) throws IOException { 8 //1,明確源和目的,建立輸入流和輸出流 9 //注意路徑需要使用\\,將\轉義 10 FileInputStream fis = new FileInputStream("d:\\1\\1.png");//源為d盤1目錄下文件1.png 11 FileOutputStream fos = new FileOutputStream("d:\\2\\2.png");//目的為d盤2目錄下文件2.png 12 //2,使用自定義緩沖區將輸入流和輸出流關聯起來 13 byte[] buf = new byte[1024];//定義1024byte的緩沖區 14 int len = 0;//輸入流讀到緩沖區中的長度 15 //3,將數據從輸入流讀入緩沖區 16 //循環讀入,當讀到文件最后,會得到值-1 17 while((len=fis.read(buf))!=-1){ 18 fos.write(buf,0,len);//將讀到長度部分寫入輸出流 19 } 20 //4,關流,需要關閉底層資源 21 fis.close(); 22 fos.close(); 23 } 24 }
這樣小A就可以自己給自己發送情書啦,接下來怎么利用網絡給小A和小B前線搭橋呢?
二、網絡編程
在I/O技術中,網絡的源設備都是Socket流,因此網絡可以簡單理解為將I/O中的設備換成了Socket。
首先要明確的是傳輸協議使用UDP還是TCP。這里直接使用TCP傳輸。
TCP
TCP是傳輸控制協議,具體的特點有以下幾點:
- 建立連接,形成傳輸數據的通道
- 在連接中進行大數據量傳輸
- 通過三次握手完成連接,是可靠協議
- 必須建立連接,效率會稍低
Socket套接字
不管使用UDP還是TCP,都需要使用Socket套接字,Socket就是為網絡服務提供的一種機制。通信的兩端都有Socket,網絡通信其實就是Socket間的通信,數據在兩個Socket間通過I/O傳輸。
TCP傳輸
TCP傳輸的兩端分別為客戶端與服務端,java中對應的對象為Socket與ServerSocket。需要分別建立客戶端與服務端,在建立連接后通過Socket中的IO流進行數據的傳輸,然后關閉Socket。
同樣,客戶端與服務器端是兩個獨立的應用程序。
Socket類實現客戶端套接字,ServerSocket類實現服務器套接字。
客戶端向服務端發送信息建立通道,通道建立后服務器端向客戶端發送信息。
客戶端一般初始化時要指定對方的IP地址和端口,IP地址可以是IP對象,也可以是IP對象字符串表現形式。
建立通道后,信息傳輸通過Socket流,為底層建立好的,又有輸入和輸出,想要獲取輸入或輸出流對象,找Socket來獲取。為字節流。getInputStream()和getOutputStream()方法來獲取輸入流和輸出流。
服務端獲取到客戶端Socket對象,通過其對象與Cilent進行通訊。
客戶端的輸出對應服務端的輸入,服務端的輸出對應客戶端的輸入。
下面將之前的功能復雜化,變成將客戶端硬盤上的文件發送至服務端。
客戶端與服務端的演示
客戶端
1 //客戶端發數據到服務端 2 /* 3 * TCP傳輸,客戶端建立的過程 4 * 1,創建TCP客戶端Socket服務,使用的是Socket對象。 5 * 建議該對象一創建就明確目的地。要連接的主機。 6 * 2,如果連接建立成功,說明數據傳輸通道已建立。 7 * 該通道就是Socket流,是底層建立好的。既然是流,說明這里既有輸入,又有輸出。 8 * 3,使用輸出流,將數據寫出。 9 * 4,關閉資源。 10 */ 11 // 建立客戶端Socket 12 Socket s = new Socket(InetAddress.getLocalHost(), 9003); 13 // 獲得輸出流 14 OutputStream out = s.getOutputStream(); 15 // 獲得輸入流 16 FileInputStream fis = new FileInputStream("d:\\1\\1.png"); 17 // 發送文件信息 18 byte[] buf = new byte[1024]; 19 int len = 0; 20 while ((len = fis.read(buf)) != -1) { 21 // 寫入到Socket輸出流 22 out.write(buf, 0, len); 23 } 24 s.shutdownOutput(); 25 // 關流 26 out.close(); 27 fis.close(); 28 s.close();
注意:在建立客戶端Socket服務的時候,需要指定服務端的IP地址和端口號,此處在實現在一台電腦上演示,因此服務端的地址是本機的IP地址。
服務端
1 // 建立服務端 2 ServerSocket ss = new ServerSocket(9003);// 需要指定端口,客戶端與服務端相同,一般在1000-65535之間 3 //服務端一般一直開啟來接收客戶端的信息。 4 while (true) { 5 // 獲取客戶端Socket 6 Socket s = ss.accept(); 7 // 獲取輸入流與輸出流 8 InputStream in = s.getInputStream();// 輸入流 9 FileOutputStream fos = new FileOutputStream("d:\\3\\3.png"); 10 // 創建緩沖區關聯輸入流與輸出流 11 byte[] buf = new byte[1024]; 12 int len = 0; 13 // 數據的寫入 14 while ((len = in.read(buf)) != -1) { 15 fos.write(buf, 0, len); 16 } 17 // 關流 18 fos.close(); 19 s.close(); 20 }
因為此時還沒有用到File類,因此與流關聯的文件夾必須被提前創建,否則沒辦法成功寫入。所以建議后續使用File對象來完成文件與流的關聯。
三、傳輸任意類型后綴的文件
因為只有一次通信的過程,因此服務端事先不知道客戶端所傳輸文件的類型,因此可以讓服務端與客戶端進行簡單的交互,這里只考慮成功傳輸的情況。
具體實現過程為:一、客戶端向服務端發送文件完整名稱;二、服務端接收到完整名稱,提取文件后綴名發送給客戶端;三、客戶端接收到服務端發送的后綴名進行校驗,不同則關閉客戶端Socket流,結束客戶端進程;四、如果正確,則發送文件信息。五、服務端根據接收到的文件名稱和客戶端ip地址建立相應的文件夾(如果不存在,則創立文件夾),將客戶端Socket輸入流信息寫入文件,關閉客戶端流。這樣因為多了一次傳輸文件后綴名的過程,因此可以傳輸任意類型的文件,便於之后的拓展,如可以加入圖形界面,選擇任意想要傳輸的文件。
這樣基礎功能已經大部分完成,但是此時一次只能連接一個客戶端,這樣如果機器人小B有若干追求者,也只能乖乖等小A將文件傳輸完畢,為了解決可以同時接收多個客戶端的信息,需要用到多線程的技術。
四、多線程
多線程的實現有兩種方法,一種是繼承Thread類,另一種是實現Runnable接口然后作為線程任務傳遞給Thread對象,這里選擇第二種實現Runnable接口。需要覆寫此接口的run()方法,在之前的基礎之上改動,將獲取到的客戶端Socket對象傳入線程任務的run()方法,線程任務類需要持有Socket的引用,利用構造函數對此引用進行初始化。將讀取輸入流至關閉客戶端流的操作封裝至run()方法。需要注意的是,此過程中代碼會拋出異常,而實現接口類不能throw異常,只能進行try,catch處理(接口中無此異常聲明,因此不能拋出)。在服務器類中,新建Thread對象,將線程任務類對象傳入,調用Thread類的start()方法開啟線程。
五、總結
以上便基本實現了此任務的核心功能,即通過TCP協議,實現了多台客戶端與主機間任意類型文件的傳輸,其中最核心的知識點在於I/O流,即需要弄清輸入流與輸出流,利用緩沖區進行二者的關聯;在此基礎上,加入了網絡技術編程,將輸入輸出流更改為Socket套接字;為了增加拓展性,引入文件對象,實現客戶端與服務端的交互;為了實現多台電腦與主機的文件傳輸,引入了多線程。程序中為了盡量簡化與抽象最核心的內容,一些代碼與邏輯難免有紕漏,希望大家多多指正與交流。當然此過程完全可以由UDP協議完成,在某些場景下UDP也更有優勢,此處不再贅述。
完整代碼
客戶端
1 import java.io.File; 2 import java.io.FileInputStream; 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.io.OutputStream; 6 import java.net.InetAddress; 7 import java.net.Socket; 8 import java.net.UnknownHostException; 9 10 public class Client { 11 public static void main(String[] args) throws UnknownHostException, IOException { 12 /* 13 * 客戶端先向服務端發送一個文件名,服務端接收到后給客戶端一個反饋,然后客戶端開始發送文件 14 */ 15 //建立客戶端Socket 16 Socket s = new Socket(InetAddress.getLocalHost(), 9001);//修改為服務器IP地址 17 //獲得輸出流 18 OutputStream out = s.getOutputStream(); 19 //關聯發送文件 20 File file = new File("D:\\1.png"); 21 String name = file.getName();//獲取文件完整名稱 22 String[] fileName = name.split("\\.");//將文件名按照.來分割,因為.是正則表達式中的特殊字符,因此需要轉義 23 String fileLast = fileName[fileName.length-1];//后綴名 24 //寫入信息到輸出流 25 out.write(name.getBytes()); 26 //讀取服務端的反饋信息 27 InputStream in = s.getInputStream(); 28 byte[] names = new byte[50]; 29 int len = in.read(names); 30 String nameIn = new String(names, 0, len); 31 if(!fileLast.equals(nameIn)){ 32 //結束輸出,並結束當前線程 33 s.close(); 34 System.exit(1); 35 } 36 //如果正確,則發送文件信息 37 //讀取文件信息 38 FileInputStream fr = new FileInputStream(file); 39 //發送文件信息 40 byte[] buf = new byte[1024]; 41 while((len=fr.read(buf))!=-1){ 42 //寫入到Socket輸出流 43 out.write(buf,0,len); 44 } 45 //關流 46 out.close(); 47 fr.close(); 48 s.close(); 49 } 50 }
服務端
任務類
1 import java.io.File; 2 import java.io.FileOutputStream; 3 import java.io.InputStream; 4 import java.io.OutputStream; 5 import java.net.Socket; 6 7 public class Task implements Runnable { 8 private Socket s; 9 public Task(Socket s){ 10 this.s = s; 11 } 12 @Override 13 public void run() { 14 String ip = s.getInetAddress().getHostAddress(); 15 try{ 16 //獲取客戶端輸入流 17 InputStream in = s.getInputStream(); 18 //讀取信息 19 byte[] names = new byte[100]; 20 int len = in.read(names); 21 String fileName = new String(names, 0, len); 22 String[] fileNames = fileName.split("\\."); 23 String fileLast = fileNames[fileNames.length-1]; 24 //然后將后綴名發給客戶端 25 OutputStream out = s.getOutputStream(); 26 out.write(fileLast.getBytes()); 27 //新建文件 28 File dir = new File("d:\\server\\"+ip); 29 if(!dir.exists()) 30 dir.mkdirs(); 31 File file = new File(dir,fileNames[0]+"."+fileLast); 32 FileOutputStream fos = new FileOutputStream(file); 33 //將Socket輸入流中的信息讀入到文件 34 byte[] bufIn = new byte[1024]; 35 while((len = in.read(bufIn))!=-1){ 36 //寫入文件 37 fos.write(bufIn, 0, len); 38 } 39 fos.close(); 40 s.close(); 41 }catch(Exception e){ 42 e.printStackTrace(); 43 } 44 } 45 }
服務器類
import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class Server { public static void main(String[] args) throws IOException { /* * 服務端先接收客戶端傳過來的信息,然后向客戶端發送接收成功,新建文件,接收客戶端信息 */ //建立服務端 ServerSocket ss = new ServerSocket(9001);//客戶端端口需要與服務端一致 while(true){ //獲取客戶端Socket Socket s = ss.accept(); new Thread(new Task(s)).start(); } } }
以上內容就到這里,如有錯誤和不清晰的地方,請大家指正!