java實現兩台電腦間TCP協議文件傳輸


  記錄下之前所做的客戶端向服務端發送文件的小項目,總結下學習到的一些方法與思路。

注:本文參考自《黑馬程序員》視頻。

  首先明確需求,在同一局域網下的機器人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();
        }
    }
}

  以上內容就到這里,如有錯誤和不清晰的地方,請大家指正!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM