記錄下以前所作的客戶端向服務端發送文件的小項目,總結下學習到的一些方法與思路。java
注:本文參考自《黑馬程序員》視頻。程序員
首先明確需求,在同一局域網下的機器人A想給喜歡了好久的機器人B發送情書,可是機器人B事先並不知道小A的心思,那麼做爲月老(紅娘)該如何幫助他們呢?正則表達式
而後創建模型並拆分需求。這裏兩臺主機使用網線直連,在物理層上確保創建了鏈接,接下來即是利用相應的協議將信息從電腦A傳給電腦B。在這一步上,能夠將此過程抽象爲網絡+I/O(Input、Output)的過程。若是能在一臺電腦上實現文件之間的傳輸,再加上相互的網絡協議,羞澀的A不就能夠將情書發送給B了嗎?所以要先解決在一臺電腦上傳輸信息的問題。爲了在網絡上傳輸,使用必要的協議是必要的,TCP/IP協議簇就是爲了解決計算機間通訊而生,而這裏主要用到UDP和TCP兩種協議。當小A能夠向小B發送情書後,又出現了衆多的追求者,那麼小B如何去處理這麼多的併發任務呢?這時便要用到多線程的技術。編程
所以接下來將分別介紹此過程當中所用到了I/O流(最基礎)、網絡編程(最重要)、多線程知識(較重要)和其中一些小技巧。數組
I/O流用來處理設備之間的數據傳輸,Java對數據的傳輸經過流的方式。服務器
流按操做數據分爲兩種:字節流與字符流。若是數據是文本類型,那麼須要使用字符流;若是是其餘類型,那麼使用字節流。簡單來講,字符流=字節流+編碼表。網絡
流按流向分爲:輸入流(將硬盤中的數據讀入內存),輸出流(將內存中的數據寫入硬盤)。多線程
簡單來講,想要將某文件傳到目的地,須要將此文件關聯輸入流,而後將輸入流中的信息寫入到輸出流中。將目的關聯輸出流,就能夠將信息傳輸到目的地了。併發
Java提供了大量的流對象可供使用,其中有兩大基類,字節流的兩個頂層父InputStream與OutputStream;字符流的兩個頂層父類Reader與Writer。這些體系的子類都以父類名做爲後綴,而子類名的前綴就是該對象的功能。ide
下提供4個明確的要點,只要明確如下幾點就能比較清晰的確認使用哪幾個流對象。
1, 明確源和目的(匯)
2, 明確數據是不是純文本數據
非純文本 :InputStream
非純文本 :OutputStream
到這裏就能夠明確需求中具體要用哪一個體系。
3, 明確具體的設備。
硬盤:File
鍵盤:System.in
內存:數組
網絡:Socket流
硬盤:File
控制檯:System.out
內存:數組
網絡:Socket流
4,是否須要其餘額外功能。
a) 是否須要高效(緩衝區)?
是,就加上buffer。
b) 是否須要轉換?
是
在這裏源爲硬盤,目的也爲硬盤,數據類型爲情書,多是文字的情書,也多是小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是傳輸控制協議,具體的特色有如下幾點:
無論使用UDP仍是TCP,都須要使用Socket套接字,Socket就是爲網絡服務提供的一種機制。通訊的兩端都有Socket,網絡通訊其實就是Socket間的通訊,數據在兩個Socket間經過I/O傳輸。
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對象來完成文件與流的關聯。
由於只有一次通訊的過程,所以服務端事先不知道客戶端所傳輸文件的類型,所以可讓服務端與客戶端進行簡單的交互,這裏只考慮成功傳輸的狀況。
具體實現過程爲:1、客戶端向服務端發送文件完整名稱;2、服務端接收到完整名稱,提取文件後綴名發送給客戶端;3、客戶端接收到服務端發送的後綴名進行校驗,不一樣則關閉客戶端Socket流,結束客戶端進程;4、若是正確,則發送文件信息。5、服務端根據接收到的文件名稱和客戶端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(); } } }
以上內容就到這裏,若有錯誤和不清晰的地方,請你們指正!