一、網(wǎng)絡通信協(xié)議概述
通過計算機網(wǎng)絡可以使多臺計算機實現(xiàn)連接,位于同一個網(wǎng)絡中的計算機在進行連接和通信時需要遵守一定的規(guī)則,這就好比在道路中行駛的汽車一定要遵守交通規(guī)則一樣。在計算機網(wǎng)絡中,這些連接和通信的規(guī)則被稱為網(wǎng)絡通信協(xié)議,它對數(shù)據(jù)的傳輸格式、傳輸速率、傳輸步驟等做了統(tǒng)一規(guī)定,通信雙方必須同時遵守才能完成數(shù)據(jù)交換。
網(wǎng)絡通信協(xié)議有很多種,目前應用最廣泛的是TCP/IP協(xié)議(Transmission Control Protocal/Internet Protocal傳輸控制協(xié)議/英特網(wǎng)互聯(lián)協(xié)議),它是一個包括TCP協(xié)議和IP協(xié)議,UDP(User Datagram Protocal)協(xié)議和其它一些協(xié)議的協(xié)議組,在學習具體協(xié)議之前首先了解一下TCP/IP協(xié)議組的層次結(jié)構(gòu)。
在進行數(shù)據(jù)傳輸時,要求發(fā)送的數(shù)據(jù)與收到的數(shù)據(jù)完全一樣,這時,就需要在原有的數(shù)據(jù)上添加很多信息,以保證數(shù)據(jù)在傳輸過程中數(shù)據(jù)格式完全一致。TCP/IP協(xié)議的層次結(jié)構(gòu)比較簡單,共分為四層,如圖所示。

上圖中,TCP/IP協(xié)議中的四層分別是應用層、傳輸層、網(wǎng)絡層和鏈路層,每層分別負責不同的通信功能,接下來針對這四層進行詳細地講解。
鏈路層:鏈路層是用于定義物理傳輸通道,通常是對某些網(wǎng)絡連接設備的驅(qū)動協(xié)議,例如針對光纖、網(wǎng)線提供的驅(qū)動。
網(wǎng)絡層:網(wǎng)絡層是整個TCP/IP協(xié)議的核心,它主要用于將傳輸?shù)臄?shù)據(jù)進行分組,將分組數(shù)據(jù)發(fā)送到目標計算機或者網(wǎng)絡。
傳輸層:主要使網(wǎng)絡程序進行通信,在進行網(wǎng)絡通信時,可以采用TCP協(xié)議,也可以采用UDP協(xié)議。
應用層:主要負責應用程序的協(xié)議,例如HTTP協(xié)議、FTP協(xié)議等。
二、IP地址和端口號
要想使網(wǎng)絡中的計算機能夠進行通信,必須為每臺計算機指定一個標識號,通過這個標識號來指定接受數(shù)據(jù)的計算機或者發(fā)送數(shù)據(jù)的計算機。
在TCP/IP協(xié)議中,這個標識號就是IP地址,它可以唯一標識一臺計算機,目前,IP地址廣泛使用的版本是IPv4,它是由4個字節(jié)大小的二進制數(shù)來表示,如:00001010000000000000000000000001。由于二進制形式表示的IP地址非常不便記憶和處理,因此通常會將IP地址寫成十進制的形式,每個字節(jié)用一個十進制數(shù)字(0-255)表示,數(shù)字間用符號“.”分開,如 “192.168.1.100”。
隨著計算機網(wǎng)絡規(guī)模的不斷擴大,對IP地址的需求也越來越多,IPV4這種用4個字節(jié)表示的IP地址面臨枯竭,因此IPv6 便應運而生了,IPv6使用16個字節(jié)表示IP地址,它所擁有的地址容量約是IPv4的8×1028倍,達到2128個(算上全零的),這樣就解決了網(wǎng)絡地址資源數(shù)量不夠的問題。
通過IP地址可以連接到指定計算機,但如果想訪問目標計算機中的某個應用程序,還需要指定端口號。在計算機中,不同的應用程序是通過端口號區(qū)分的。端口號是用兩個字節(jié)(16位的二進制數(shù))表示的,它的取值范圍是065535,其中,01023之間的端口號用于一些知名的網(wǎng)絡服務和應用,用戶的普通應用程序需要使用1024以上的端口號,從而避免端口號被另外一個應用或服務所占用。
接下來通過一個圖例來描述IP地址和端口號的作用,如下圖所示。

從上圖中可以清楚地看到,位于網(wǎng)絡中一臺計算機可以通過IP地址去訪問另一臺計算機,并通過端口號訪問目標計算機中的某個應用程序。
InetAddress
了解了IP地址的作用,我們看學習下JDK中提供了一個InetAdderss類,該類用于封裝一個IP地址,并提供了一系列與IP地址相關的方法。


上圖中,列舉了InetAddress的四個常用方法。其中,前兩個方法用于獲得該類的實例對象,第一個方法用于獲得表示指定主機的InetAddress對象,第二個方法用于獲得表示本地的InetAddress對象。通過InetAddress對象便可獲取指定主機名,IP地址等,接下來通過一個案例來演示InetAddress的常用方法,如下所示:
public class Example01 {
public static void main(String[] args) throws Exception {
InetAddress local = InetAddress.getLocalHost();
InetAddress remote = InetAddress.getByName("www.sxt.cn");
System.out.println("本機的IP地址:" + local.getHostAddress());
System.out.println("sxt的IP地址:" + remote.getHostAddress());
System.out.println("sxt的主機名為:" + remote.getHostName());
}
}
三、 UDP與TCP協(xié)議
在介紹TCP/IP結(jié)構(gòu)時,提到傳輸層的兩個重要的高級協(xié)議,分別是UDP和TCP,其中UDP是User Datagram Protocol的簡稱,稱為用戶數(shù)據(jù)報協(xié)議,TCP是Transmission Control Protocol的簡稱,稱為傳輸控制協(xié)議。
1.UDP協(xié)議
UDP是無連接通信協(xié)議,即在數(shù)據(jù)傳輸時,數(shù)據(jù)的發(fā)送端和接收端不建立邏輯連接。簡單來說,當一臺計算機向另外一臺計算機發(fā)送數(shù)據(jù)時,發(fā)送端不會確認接收端是否存在,就會發(fā)出數(shù)據(jù),同樣接收端在收到數(shù)據(jù)時,也不會向發(fā)送端反饋是否收到數(shù)據(jù)。
由于使用UDP協(xié)議消耗資源小,通信效率高,所以通常都會用于音頻、視頻和普通數(shù)據(jù)的傳輸例如視頻會議都使用UDP協(xié)議,因為這種情況即使偶爾丟失一兩個數(shù)據(jù)包,也不會對接收結(jié)果產(chǎn)生太大影響。
但是在使用UDP協(xié)議傳送數(shù)據(jù)時,由于UDP的面向無連接性,不能保證數(shù)據(jù)的完整性,因此在傳輸重要數(shù)據(jù)時不建議使用UDP協(xié)議。
UDP的交換過程如下圖所示:

1.1 DatagramPacket
前面介紹了UDP是一種面向無連接的協(xié)議,因此,在通信時發(fā)送端和接收端不用建立連接。UDP通信的過程就像是貨運公司在兩個碼頭間發(fā)送貨物一樣。在碼頭發(fā)送和接收貨物時都需要使用集裝箱來裝載貨物,UDP通信也是一樣,發(fā)送和接收的數(shù)據(jù)也需要使用“集裝箱”進行打包,為此JDK中提供了一個DatagramPacket類,該類的實例對象就相當于一個集裝箱,用于封裝UDP通信中發(fā)送或者接收的數(shù)據(jù)。
想要創(chuàng)建一個DatagramPacket對象,首先需要了解一下它的構(gòu)造方法。在創(chuàng)建發(fā)送端和接收端的DatagramPacket對象時,使用的構(gòu)造方法有所不同,接收端的構(gòu)造方法只需要接收一個字節(jié)數(shù)組來存放接收到的數(shù)據(jù),而發(fā)送端的構(gòu)造方法不但要接收存放了發(fā)送數(shù)據(jù)的字節(jié)數(shù)組,還需要指定發(fā)送端IP地址和端口號。
接下來根據(jù)API文檔的內(nèi)容,對DatagramPacket的構(gòu)造方法進行逐一詳細地講解:

使用該構(gòu)造方法在創(chuàng)建DatagramPacket對象時,指定了封裝數(shù)據(jù)的字節(jié)數(shù)組和數(shù)據(jù)的大小,沒有指定IP地址和端口號。很明顯,這樣的對象只能用于接收端,不能用于發(fā)送端。因為發(fā)送端一定要明確指出數(shù)據(jù)的目的地(ip地址和端口號),而接收端不需要明確知道數(shù)據(jù)的來源,只需要接收到數(shù)據(jù)即可。

使用該構(gòu)造方法在創(chuàng)建DatagramPacket對象時,不僅指定了封裝數(shù)據(jù)的字節(jié)數(shù)組和數(shù)據(jù)的大小,還指定了數(shù)據(jù)包的目標IP地址(addr)和端口號(port)。該對象通常用于發(fā)送端,因為在發(fā)送數(shù)據(jù)時必須指定接收端的IP地址和端口號,就好像發(fā)送貨物的集裝箱上面必須標明接收人的地址一樣。
DatagramPacket類中的常用方法:

1.2 DatagramSocket
DatagramPacket數(shù)據(jù)包的作用就如同是“集裝箱”,可以將發(fā)送端或者接收端的數(shù)據(jù)封裝起來。然而運輸貨物只有“集裝箱”是不夠的,還需要有碼頭。在程序中需要實現(xiàn)通信只有DatagramPacket數(shù)據(jù)包也同樣不行,為此JDK中提供的一個DatagramSocket類。DatagramSocket類的作用就類似于碼頭,使用這個類的實例對象就可以發(fā)送和接收DatagramPacket數(shù)據(jù)包。
發(fā)送數(shù)據(jù)的過程如下圖所示:

在創(chuàng)建發(fā)送端和接收端的DatagramSocket對象時,使用的構(gòu)造方法也有所不同,下面對DatagramSocket類中常用的構(gòu)造方法進行講解:

該構(gòu)造方法用于創(chuàng)建發(fā)送端的DatagramSocket對象,在創(chuàng)建DatagramSocket對象時,并沒有指定端口號,此時,系統(tǒng)會分配一個沒有被其它網(wǎng)絡程序所使用的端口號。

該構(gòu)造方法既可用于創(chuàng)建接收端的DatagramSocket對象,又可以創(chuàng)建發(fā)送端的DatagramSocket對象,在創(chuàng)建接收端的DatagramSocket對象時,必須要指定一個端口號,這樣就可以監(jiān)聽指定的端口。
DatagramSocket類中的常用方法:

1.3 UDP網(wǎng)絡程序
了解了DatagramPacket和DatagramSocket的作用,接下來通過一個案例來學習一下它們在程序中的具體用法。
下圖為UDP發(fā)送端與接收端交互圖解:

要實現(xiàn)UDP通信需要創(chuàng)建一個發(fā)送端程序和一個接收端程序,很明顯,在通信時只有接收端程序先運行,才能避免因發(fā)送端發(fā)送的數(shù)據(jù)無法接收,而造成數(shù)據(jù)丟失。因此,首先需要來完成接收端程序的編寫。
-
接收端程序編寫步驟:
- 創(chuàng)建DatagramSocket對象
- 創(chuàng)建DatagramPacket對象
- 接收數(shù)據(jù)存儲到DatagramPacket對象中
- 獲取DatagramPacket對象的內(nèi)容
- 釋放流資源
-
發(fā)送端程序編寫步驟:
- 創(chuàng)建DatagramSocket對象
- 創(chuàng)建DatagramPacket對象,并封裝數(shù)據(jù)
- 發(fā)送數(shù)據(jù)
- 釋放流資源
UDP完成數(shù)據(jù)的接收
/*
* UDP接收端
*
* 1,創(chuàng)建DatagramSocket對象
* 2,創(chuàng)建DatagramPacket對象
* 3,接收數(shù)據(jù)存儲到DatagramPacket對象中
* 4,獲取DatagramPacket對象的內(nèi)容
* 5,釋放流資源
*/
public class UDPReceive {
public static void main(String[] args) throws IOException {
//1,創(chuàng)建DatagramSocket對象,并指定端口號
DatagramSocket receiveSocket = new DatagramSocket(12306);
//2,創(chuàng)建DatagramPacket對象, 創(chuàng)建一個空的倉庫
byte[] buffer = new byte[1024];
DatagramPacket dp = new DatagramPacket(buffer, 1024);
//3,接收數(shù)據(jù)存儲到DatagramPacket對象中
receiveSocket.receive(dp);
//4,獲取DatagramPacket對象的內(nèi)容
//誰發(fā)來的數(shù)據(jù) getAddress()
InetAddress ipAddress = dp.getAddress();
String ip = ipAddress.getHostAddress();//獲取到了IP地址
//發(fā)來了什么數(shù)據(jù) getData()
byte[] data = dp.getData();
//發(fā)來了多少數(shù)據(jù) getLenth()
int length = dp.getLength();
//顯示收到的數(shù)據(jù)
String dataStr = new String(data,0,length);
System.out.println("IP地址:"+ip+ "數(shù)據(jù)是"+ dataStr);
//5,釋放流資源
receiveSocket.close();
}
}
- UDP完成數(shù)據(jù)的發(fā)送
/*
* 發(fā)送端
* 1,創(chuàng)建DatagramSocket對象
* 2,創(chuàng)建DatagramPacket對象,并封裝數(shù)據(jù)
* 3,發(fā)送數(shù)據(jù)
* 4,釋放流資源
*/
public class UDPSend {
public static void main(String[] args) throws IOException {
//1,創(chuàng)建DatagramSocket對象
DatagramSocket sendSocket = new DatagramSocket();
//2,創(chuàng)建DatagramPacket對象,并封裝數(shù)據(jù)
//public DatagramPacket(byte[] buf, int length, InetAddress address, int port)
//構(gòu)造數(shù)據(jù)報包,用來將長度為 length 的包發(fā)送到指定主機上的指定端口號。
byte[] buffer = "hello,UDP".getBytes();
DatagramPacket dp = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("192.168.75.58"), 12306);
//3,發(fā)送數(shù)據(jù)
//public void send(DatagramPacket p) 從此套接字發(fā)送數(shù)據(jù)報包
sendSocket.send(dp);
//4,釋放流資源
sendSocket.close();
}
}
2.TCP協(xié)議
TCP協(xié)議是面向連接的通信協(xié)議,即在傳輸數(shù)據(jù)前先在發(fā)送端和接收端建立邏輯連接,然后再傳輸數(shù)據(jù),它提供了兩臺計算機之間可靠無差錯的數(shù)據(jù)傳輸。
在TCP連接中必須要明確客戶端與服務器端,由客戶端向服務端發(fā)出連接請求,每次連接的創(chuàng)建都需要經(jīng)過“三次握手”。
第一次握手,客戶端向服務器端發(fā)出連接請求,等待服務器確認;第二次握手,服務器端向客戶端回送一個響應,通知客戶端收到了連接請求;第三次握手,客戶端再次向服務器端發(fā)送確認信息,確認連接。
整個交互過程如下圖所示:

由于TCP協(xié)議的面向連接特性,它可以保證傳輸數(shù)據(jù)的安全性,所以是一個被廣泛采用的協(xié)議,例如在下載文件時,如果數(shù)據(jù)接收不完整,將會導致文件數(shù)據(jù)丟失而不能被打開,因此,下載文件時必須采用TCP協(xié)議。
TCP通信同UDP通信一樣,都能實現(xiàn)兩臺計算機之間的通信,通信的兩端都需要創(chuàng)建socket對象。
區(qū)別在于,UDP中只有發(fā)送端和接收端,不區(qū)分客戶端與服務器端,計算機之間可以任意地發(fā)送數(shù)據(jù)。
而TCP通信是嚴格區(qū)分客戶端與服務器端的,在通信時,必須先由客戶端去連接服務器端才能實現(xiàn)通信,服務器端不可以主動連接客戶端,并且服務器端程序需要事先啟動,等待客戶端的連接。
在JDK中提供了兩個類用于實現(xiàn)TCP程序,一個是ServerSocket類,用于表示服務器端,一個是Socket類,用于表示客戶端。
通信時,首先創(chuàng)建代表服務器端的ServerSocket對象,該對象相當于開啟一個服務,并等待客戶端的連接,然后創(chuàng)建代表客戶端的Socket對象向服務器端發(fā)出連接請求,服務器端響應請求,兩者建立連接開始通信。
2.1 ServerSocket
通過前面的學習知道,在開發(fā)TCP程序時,首先需要創(chuàng)建服務器端程序。JDK的java.net包中提供了一個ServerSocket類,該類的實例對象可以實現(xiàn)一個服務器段的程序。通過查閱API文檔可知,ServerSocket類提供了多種構(gòu)造方法。

ServerSocket的常用方法:

ServerSocket對象負責監(jiān)聽某臺計算機的某個端口號,在創(chuàng)建ServerSocket對象后,需要繼續(xù)調(diào)用該對象的accept()方法,接收來自客戶端的請求。當執(zhí)行了accept()方法之后,服務器端程序會發(fā)生阻塞,直到客戶端發(fā)出連接請求,accept()方法才會返回一個Scoket對象用于和客戶端實現(xiàn)通信,程序才能繼續(xù)向下執(zhí)行。
2.2 Socket
講解了ServerSocket對象可以實現(xiàn)服務端程序,但只實現(xiàn)服務器端程序還不能完成通信,此時還需要一個客戶端程序與之交互,為此JDK提供了一個Socket類,用于實現(xiàn)TCP客戶端程序。
通過查閱API文檔可知Socket類同樣提供了多種構(gòu)造方法:

使用該構(gòu)造方法在創(chuàng)建Socket對象時,會根據(jù)參數(shù)去連接在指定地址和端口上運行的服務器程序,其中參數(shù)host接收的是一個字符串類型的IP地址。

該方法在使用上與第二個構(gòu)造方法類似,參數(shù)address用于接收一個InetAddress類型的對象,該對象用于封裝一個IP地址。
在以上Socket的構(gòu)造方法中,最常用的是第一個構(gòu)造方法。
接下來學習一下Socket的常用方法:

在Socket類的常用方法中,getInputStream()和getOutStream()方法分別用于獲取輸入流和輸出流。當客戶端和服務端建立連接后,數(shù)據(jù)是以IO流的形式進行交互的,從而實現(xiàn)通信。
接下來通過一張圖來描述服務器端和客戶端的數(shù)據(jù)傳輸:

2.3 簡單的TCP網(wǎng)絡程序
了解了ServerSocket、Socket類的基本用法,為了讓大家更好地掌握這兩個類的使用,接下來通過一個TCP通信的案例來進一步學習。
如下圖所示:

要實現(xiàn)TCP通信需要創(chuàng)建一個服務器端程序和一個客戶端程序,為了保證數(shù)據(jù)傳輸?shù)陌踩?,首先需要實現(xiàn)服務器端程序。
-
服務器端實現(xiàn)步驟:
- 創(chuàng)建服務器端ServerSocket對象,指定服務器端端口號
- 開啟服務器,等待著客戶端Socket對象的連接,如有客戶端連接,返回客戶端的Socket對象
- 通過客戶端的Socket對象,獲取客戶端的輸入流,為了實現(xiàn)獲取客戶端發(fā)來的數(shù)據(jù)
- 通過客戶端的輸入流,獲取流中的數(shù)據(jù)
- 通過客戶端的Socket對象,獲取客戶端的輸出流,為了實現(xiàn)給客戶端反饋信息
- 通過客戶端的輸出流,寫數(shù)據(jù)到流中
- 關閉流資源
-
客戶端實現(xiàn)步驟:
- 創(chuàng)建客戶端的Socket對象
- 獲取Socket的輸出流對象
- 寫數(shù)據(jù)給服務器
- 獲取Socket的輸入流對象
- 使用輸入流,讀反饋信息
- 關閉流資源
服務器端程序:
/*
* TCP 服務器端
*
* 1,創(chuàng)建服務器ServerSocket對象(指定服務器端口號)
* 2,開啟服務器了,等待客戶端的連接,當客戶端連接后,可以獲取到連接服務器的客戶端Socket對象
* 3,給客戶端反饋信息
* 4,關閉流資源
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
//1,創(chuàng)建服務器ServerSocket對象(指定服務器端口號)
ServerSocket ss = new ServerSocket(8888);
//2,開啟服務器了,等待客戶端的連接,當客戶端連接后,可以獲取到連接服務器的客戶端Socket對象
Socket s = ss.accept();
//3,給客戶端反饋信息
/*
* a,獲取客戶端的輸出流
* b,在服務端端,通過客戶端的輸出流寫數(shù)據(jù)給客戶端
*/
//a,獲取客戶端的輸出流
OutputStream out = s.getOutputStream();
//b,在服務端端,通過客戶端的輸出流寫數(shù)據(jù)給客戶端
out.write("你已經(jīng)連接上了服務器".getBytes());
//4,關閉流資源
out.close();
s.close();
//ss.close(); 服務器流 通常都是不關閉的
}
}
客戶端程序:
/*
* TCP 客戶端
*
* 1,創(chuàng)建客戶端Socket對象,(指定要連接的服務器地址與端口號)
* 2,獲取服務器端的反饋回來的信息
* 3,關閉流資源
*/
public class TCPClient {
public static void main(String[] args) throws IOException {
//1,創(chuàng)建客戶端Socket對象,(指定要連接的服務器地址與端口號)
Socket s = new Socket("192.168.74.58", 8888);
//2,獲取服務器端的反饋回來的信息
InputStream in = s.getInputStream();
//獲取獲取流中的數(shù)據(jù)
byte[] buffer = new byte[1024];
//把流中的數(shù)據(jù)存儲到數(shù)組中,并記錄讀取字節(jié)的個數(shù)
int length = in.read(buffer);
//顯示數(shù)據(jù)
System.out.println( new String(buffer, 0 , length) );
//3,關閉流資源
in.close();
s.close();
}
}
四、 實例:文件上傳
目前大多數(shù)服務器都會提供文件上傳的功能,由于文件上傳需要數(shù)據(jù)的安全性和完整性,很明顯需要使用TCP協(xié)議來實現(xiàn)。接下來通過一個案例來實現(xiàn)圖片上傳的功能。
1.單線程實現(xiàn)文件上傳

- 首先編寫服務器端程序,用來接收圖片。
/*
* 文件上傳 服務器端
*
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
//1,創(chuàng)建服務器,等待客戶端連接
ServerSocket serverSocket = new ServerSocket(8888);
Socket clientSocket = serverSocket.accept();
//顯示哪個客戶端Socket連接上了服務器
InetAddress ipObject = clientSocket.getInetAddress();//得到IP地址對象
String ip = ipObject.getHostAddress(); //得到IP地址字符串
System.out.println("小樣,抓到你了,連接我??!" + "IP:" + ip);
//7,獲取Socket的輸入流
InputStream in = clientSocket.getInputStream();
//8,創(chuàng)建目的地的字節(jié)輸出流 D:\\upload\\192.168.74.58(1).jpg
BufferedOutputStream fileOut = new BufferedOutputStream(new FileOutputStream("D:\\upload\\192.168.74.58(1).jpg"));
//9,把Socket輸入流中的數(shù)據(jù),寫入目的地的字節(jié)輸出流中
byte[] buffer = new byte[1024];
int len = -1;
while((len = in.read(buffer)) != -1){
//寫入目的地的字節(jié)輸出流中
fileOut.write(buffer, 0, len);
}
//-----------------反饋信息---------------------
//10,獲取Socket的輸出流, 作用:寫反饋信息給客戶端
OutputStream out = clientSocket.getOutputStream();
//11,寫反饋信息給客戶端
out.write("圖片上傳成功".getBytes());
out.close();
fileOut.close();
in.close();
clientSocket.close();
//serverSocket.close();
}
}
- 編寫客戶端,完成上傳圖片
/*
* 文件上傳 客戶端
*
* public void shutdownOutput() 禁用此Socket的輸出流,間接的相當于告知了服務器數(shù)據(jù)寫入完畢
*/
public class TCPClient {
public static void main(String[] args) throws IOException {
//2,創(chuàng)建客戶端Socket,連接服務器
Socket socket = new Socket("192.168.74.58", 8888);
//3,獲取Socket流中的輸出流,功能:用來把數(shù)據(jù)寫到服務器
OutputStream out = socket.getOutputStream();
//4,創(chuàng)建字節(jié)輸入流,功能:用來讀取數(shù)據(jù)源(圖片)的字節(jié)
BufferedInputStream fileIn = new BufferedInputStream(new FileInputStream("D:\\NoDir\\test.jpg"));
//5,把圖片數(shù)據(jù)寫到Socket的輸出流中(把數(shù)據(jù)傳給服務器)
byte[] buffer = new byte[1024];
int len = -1;
while ((len = fileIn.read(buffer)) != -1){
//把數(shù)據(jù)寫到Socket的輸出流中
out.write(buffer, 0, len);
}
//6,客戶端發(fā)送數(shù)據(jù)完畢,結(jié)束Socket輸出流的寫入操作,告知服務器端
socket.shutdownOutput();
//-----------------反饋信息---------------------
//12,獲取Socket的輸入流 作用: 讀反饋信息
InputStream in = socket.getInputStream();
//13,讀反饋信息
byte[] info = new byte[1024];
//把反饋信息存儲到info數(shù)組中,并記錄字節(jié)個數(shù)
int length = in.read(info);
//顯示反饋結(jié)果
System.out.println( new String(info, 0, length) );
//關閉流
in.close();
fileIn.close();
out.close();
socket.close();
}
}
2.多線程實現(xiàn)文件上傳
實現(xiàn)服務器端可以同時接收多個客戶端上傳的文件,主要修改服務器端。

- 修改服務器端代碼
/*
* 文件上傳多線程版本, 服務器端
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
//1,創(chuàng)建服務器,等待客戶端連接
ServerSocket serverSocket = new ServerSocket(6666);
//實現(xiàn)多個客戶端連接服務器的操作
while(true){
final Socket clientSocket = serverSocket.accept();
//啟動線程,完成與當前客戶端的數(shù)據(jù)交互過程
new Thread(){
public void run() {
try{
//顯示哪個客戶端Socket連接上了服務器
InetAddress ipObject = clientSocket.getInetAddress();//得到IP地址對象
String ip = ipObject.getHostAddress(); //得到IP地址字符串
System.out.println("小樣,抓到你了,連接我??!" + "IP:" + ip);
//7,獲取Socket的輸入流
InputStream in = clientSocket.getInputStream();
//8,創(chuàng)建目的地的字節(jié)輸出流 D:\\upload\\192.168.74.58(1).jpg
BufferedOutputStream fileOut = new BufferedOutputStream(new FileOutputStream("D:\\upload\\"+ip+"("+System.currentTimeMillis()+").jpg"));
//9,把Socket輸入流中的數(shù)據(jù),寫入目的地的字節(jié)輸出流中
byte[] buffer = new byte[1024];
int len = -1;
while((len = in.read(buffer)) != -1){
//寫入目的地的字節(jié)輸出流中
fileOut.write(buffer, 0, len);
}
//-----------------反饋信息---------------------
//10,獲取Socket的輸出流, 作用:寫反饋信息給客戶端
OutputStream out = clientSocket.getOutputStream();
//11,寫反饋信息給客戶端
out.write("圖片上傳成功".getBytes());
out.close();
fileOut.close();
in.close();
clientSocket.close();
} catch(IOException e){
e.printStackTrace();
}
};
}.start();
}
//serverSocket.close();
}
}