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

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

??從上圖中可以清楚地看到,位于網(wǎng)絡(luò)中一臺計(jì)算機(jī)可以通過IP地址去訪問另一臺計(jì)算機(jī),并通過端口號訪問目標(biāo)計(jì)算機(jī)中的某個應(yīng)用程序。
1.2 InetAddress
??了解了IP地址的作用,我們看學(xué)習(xí)下JDK中提供了一個InetAdderss類,該類用于封裝一個IP地址,并提供了一系列與IP地址相關(guān)的方法,下表中列出了InetAddress類的一些常用方法。
| 返回 | 方法描述 |
|---|---|
| static InetAddress | getByName(String host)在給定主機(jī)名的情況下確認(rèn)主機(jī)的IP地址 |
| static InetAddress | getLocalHost(String host)返回本地主機(jī) |
| String | getHostName()獲取次IP地址的主機(jī)名 |
| String | getHostAddress()返回IP地址字符串 |
??上圖中,列舉了InetAddress的四個常用方法。其中,前兩個方法用于獲得該類的實(shí)例對象,第一個方法用于獲得表示指定主機(jī)的InetAddress對象,第二個方法用于獲得表示本地的InetAddress對象。通過InetAddress對象便可獲取指定主機(jī)名,IP地址等,接下來通過一個案例來演示InetAddress的常用方法,如下所示。
TestDemo.java
package com.qtw.api;
import java.net.InetAddress;
public class TestDemo {
public static void main(String[] args) throws Exception{
InetAddress local = InetAddress.getLocalHost();
InetAddress remote = InetAddress.getByName("www.baidu.com");
System.out.println("本機(jī)的IP地址:" + local.getHostAddress());
System.out.println("baidu的IP地址:" + remote.getHostAddress());
System.out.println("baidu的主機(jī)名為:" + remote.getHostName());
}
}
2 UDP與TCP協(xié)議
??在介紹TCP/IP結(jié)構(gòu)時(shí),提到傳輸層的兩個重要的高級協(xié)議,分別是UDP和TCP,其中UDP是User Datagram Protocol的簡稱,稱為用戶數(shù)據(jù)報(bào)協(xié)議,TCP是Transmission Control Protocol的簡稱,稱為傳輸控制協(xié)議。
2.1 UDP協(xié)議
??UDP是無連接通信協(xié)議,即在數(shù)據(jù)傳輸時(shí),數(shù)據(jù)的發(fā)送端和接收端不建立邏輯連接。簡單來說,當(dāng)一臺計(jì)算機(jī)向另外一臺計(jì)算機(jī)發(fā)送數(shù)據(jù)時(shí),發(fā)送端不會確認(rèn)接收端是否存在,就會發(fā)出數(shù)據(jù),同樣接收端在收到數(shù)據(jù)時(shí),也不會向發(fā)送端反饋是否收到數(shù)據(jù)。
??由于使用UDP協(xié)議消耗資源小,通信效率高,所以通常都會用于音頻、視頻和普通數(shù)據(jù)的傳輸例如視頻會議都使用UDP協(xié)議,因?yàn)檫@種情況即使偶爾丟失一兩個數(shù)據(jù)包,也不會對接收結(jié)果產(chǎn)生太大影響。
??但是在使用UDP協(xié)議傳送數(shù)據(jù)時(shí),由于UDP的面向無連接性,不能保證數(shù)據(jù)的完整性,因此在傳輸重要數(shù)據(jù)時(shí)不建議使用UDP協(xié)議。UDP的交換過程如下圖所示。

1.1 TCP協(xié)議
??TCP協(xié)議是面向連接的通信協(xié)議,即在傳輸數(shù)據(jù)前先在發(fā)送端和接收端建立邏輯連接,然后再傳輸數(shù)據(jù),它提供了兩臺計(jì)算機(jī)之間可靠無差錯的數(shù)據(jù)傳輸。在TCP連接中必須要明確客戶端與服務(wù)器端,由客戶端向服務(wù)端發(fā)出連接請求,每次連接的創(chuàng)建都需要經(jīng)過“三次握手”。第一次握手,客戶端向服務(wù)器端發(fā)出連接請求,等待服務(wù)器確認(rèn),第二次握手,服務(wù)器端向客戶端回送一個響應(yīng),通知客戶端收到了連接請求,第三次握手,客戶端再次向服務(wù)器端發(fā)送確認(rèn)信息,確認(rèn)連接。整個交互過程如下圖所示。

??由于TCP協(xié)議的面向連接特性,它可以保證傳輸數(shù)據(jù)的安全性,所以是一個被廣泛采用的協(xié)議,例如在下載文件時(shí),如果數(shù)據(jù)接收不完整,將會導(dǎo)致文件數(shù)據(jù)丟失而不能被打開,因此,下載文件時(shí)必須采用TCP協(xié)議。
3 UDP通信
3.1 DatagramPacket
??前面介紹了UDP是一種面向無連接的協(xié)議,因此,在通信時(shí)發(fā)送端和接收端不用建立連接。UDP通信的過程就像是貨運(yùn)公司在兩個碼頭間發(fā)送貨物一樣。在碼頭發(fā)送和接收貨物時(shí)都需要使用集裝箱來裝載貨物,UDP通信也是一樣,發(fā)送和接收的數(shù)據(jù)也需要使用“集裝箱”進(jìn)行打包,為此JDK中提供了一個DatagramPacket類,該類的實(shí)例對象就相當(dāng)于一個集裝箱,用于封裝UDP通信中發(fā)送或者接收的數(shù)據(jù)。
??想要創(chuàng)建一個DatagramPacket對象,首先需要了解一下它的構(gòu)造方法。在創(chuàng)建發(fā)送端和接收端的DatagramPacket對象時(shí),使用的構(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)造方法進(jìn)行逐一詳細(xì)地講解。

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

??使用該構(gòu)造方法在創(chuàng)建DatagramPacket對象時(shí),不僅指定了封裝數(shù)據(jù)的字節(jié)數(shù)組和數(shù)據(jù)的大小,還指定了數(shù)據(jù)包的目標(biāo)IP地址(addr)和端口號(port)。該對象通常用于發(fā)送端,因?yàn)樵诎l(fā)送數(shù)據(jù)時(shí)必須指定接收端的IP地址和端口號,就好像發(fā)送貨物的集裝箱上面必須標(biāo)明接收人的地址一樣。
??上面我們講解了DatagramPacket的構(gòu)造方法,接下來對DatagramPacket類中的常用方法進(jìn)行詳細(xì)地講解,如下表所示。

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

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

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

??該構(gòu)造方法既可用于創(chuàng)建接收端的DatagramSocket對象,又可以創(chuàng)建發(fā)送端的DatagramSocket對象,在創(chuàng)建接收端的DatagramSocket對象時(shí),必須要指定一個端口號,這樣就可以監(jiān)聽指定的端口。
??上面我們講解了DatagramSocket的構(gòu)造方法,接下來對DatagramSocket類中的常用方法進(jìn)行詳細(xì)地講解。

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

要實(shí)現(xiàn)UDP通信需要創(chuàng)建一個發(fā)送端程序和一個接收端程序,很明顯,在通信時(shí)只有接收端程序先運(yùn)行,才能避免因發(fā)送端發(fā)送的數(shù)據(jù)無法接收,而造成數(shù)據(jù)丟失。因此,首先需要來完成接收端程序的編寫。
UDP完成數(shù)據(jù)的發(fā)送
package com.qtw.api;
/*
* 發(fā)送端
* 1,創(chuàng)建DatagramSocket對象
* 2,創(chuàng)建DatagramPacket對象,并封裝數(shù)據(jù)
* 3,發(fā)送數(shù)據(jù)
* 4,釋放流資源
*/
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
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ù)報(bào)包,用來將長度為 length 的包發(fā)送到指定主機(jī)上的指定端口號。
byte[] buffer = "hello,UDP".getBytes();
DatagramPacket dp = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("192.168.134.1"), 12306);
//3,發(fā)送數(shù)據(jù)
//public void send(DatagramPacket p) 從此套接字發(fā)送數(shù)據(jù)報(bào)包
sendSocket.send(dp);
//4,釋放流資源
sendSocket.close();
}
}
UDP完成數(shù)據(jù)的接收
package com.qtw.api;
/*
* UDP接收端
*
* 1,創(chuàng)建DatagramSocket對象
* 2,創(chuàng)建DatagramPacket對象
* 3,接收數(shù)據(jù)存儲到DatagramPacket對象中
* 4,獲取DatagramPacket對象的內(nèi)容
* 5,釋放流資源
*/
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
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();
}
}
4 TCP通信
??TCP通信同UDP通信一樣,都能實(shí)現(xiàn)兩臺計(jì)算機(jī)之間的通信,通信的兩端都需要創(chuàng)建socket對象。
??區(qū)別在于,UDP中只有發(fā)送端和接收端,不區(qū)分客戶端與服務(wù)器端,計(jì)算機(jī)之間可以任意地發(fā)送數(shù)據(jù)。
??而TCP通信是嚴(yán)格區(qū)分客戶端與服務(wù)器端的,在通信時(shí),必須先由客戶端去連接服務(wù)器端才能實(shí)現(xiàn)通信,服務(wù)器端不可以主動連接客戶端,并且服務(wù)器端程序需要事先啟動,等待客戶端的連接。
??在JDK中提供了兩個類用于實(shí)現(xiàn)TCP程序,一個是ServerSocket類,用于表示服務(wù)器端,一個是Socket類,用于表示客戶端。
??通信時(shí),首先創(chuàng)建代表服務(wù)器端的ServerSocket對象,該對象相當(dāng)于開啟一個服務(wù),并等待客戶端的連接,然后創(chuàng)建代表客戶端的Socket對象向服務(wù)器端發(fā)出連接請求,服務(wù)器端響應(yīng)請求,兩者建立連接開始通信。
4.1 ServerSocket
??通過前面的學(xué)習(xí)知道,在開發(fā)TCP程序時(shí),首先需要創(chuàng)建服務(wù)器端程序。JDK的java.net包中提供了一個ServerSocket類,該類的實(shí)例對象可以實(shí)現(xiàn)一個服務(wù)器段的程序。通過查閱API文檔可知,ServerSocket類提供了多種構(gòu)造方法,接下來就對ServerSocket的構(gòu)造方法進(jìn)行逐一地講解。
構(gòu)造方法:
ServerSocket(int port) //創(chuàng)建綁定到特定端口的套接字對象
??使用該構(gòu)造方法在創(chuàng)建ServerSocket對象時(shí),就可以將其綁定到一個指定的端口號上(參數(shù)port就是端口號)。
??接下來學(xué)習(xí)一下ServerSocket的常用方法,如表所示。
| 方法摘要 | |
|---|---|
| Socket | accept()偵聽并接受套接字的連接 |
| InetAddress | getInetAddress()返回此服務(wù)器套接字的本地地址 |
??ServerSocket對象負(fù)責(zé)監(jiān)聽某臺計(jì)算機(jī)的某個端口號,在創(chuàng)建ServerSocket對象后,需要繼續(xù)調(diào)用該對象的accept()方法,接收來自客戶端的請求。當(dāng)執(zhí)行了accept()方法之后,服務(wù)器端程序會發(fā)生阻塞,直到客戶端發(fā)出連接請求,accept()方法才會返回一個Scoket對象用于和客戶端實(shí)現(xiàn)通信,程序才能繼續(xù)向下執(zhí)行。
4.2 Socket
??講解了ServerSocket對象可以實(shí)現(xiàn)服務(wù)端程序,但只實(shí)現(xiàn)服務(wù)器端程序還不能完成通信,此時(shí)還需要一個客戶端程序與之交互,為此JDK提供了一個Socket類,用于實(shí)現(xiàn)TCP客戶端程序。
??通過查閱API文檔可知Socket類同樣提供了多種構(gòu)造方法,接下來就對Socket的常用構(gòu)造方法進(jìn)行詳細(xì)講解。
| 構(gòu)造方法 |
|---|
| Socket(String host,int port) 創(chuàng)建一個流套接字并將其連接到指定的主機(jī)上的指定端口 |
| Socket(InetAddress address,int port) 創(chuàng)建一個流套接字并將其連接到指定的主機(jī)上的指定端口 |
在以上Socket的構(gòu)造方法中,最常用的是第一個構(gòu)造方法。
接下來學(xué)習(xí)一下Socket的常用方法,如表所示。
| 方法聲明 | 功能描述 |
|---|---|
| int getPort() | 該方法返回一個int類型對象,該對象是Socket對象與服務(wù)器端連接的端口號 |
| InetAddress getLocalAddress() | 該方法用于獲取Socket對象綁定的本地IP地址,并將IP地址封裝成InetAddress類型的對象返回 |
| void close() | 該方法用于關(guān)閉Socket連接,結(jié)束本次通信。在關(guān)閉socket之前,應(yīng)將與socket相關(guān)的所有的輸入/輸出流全部關(guān)閉,這是因?yàn)橐粋€良好的程序應(yīng)該在執(zhí)行完畢時(shí)釋放所有的資源 |
| InputStream getInputStream() | 該方法返回一個InputStream類型的輸入流對象,如果該對象是由服務(wù)器端的Socket返回,就用于讀取客戶端發(fā)送的數(shù)據(jù),反之,用于讀取服務(wù)器端發(fā)送的數(shù)據(jù) |
| OutputStream getOutputStream() | 該方法返回一個OutputStream類型的輸出流對象,如果該對象是由服務(wù)器端的Socket返回,就用于向客戶端發(fā)送數(shù)據(jù),反之,用于向服務(wù)器端發(fā)送數(shù)據(jù) |
??在Socket類的常用方法中,getInputStream()和getOutStream()方法分別用于獲取輸入流和輸出流。當(dāng)客戶端和服務(wù)端建立連接后,數(shù)據(jù)是以IO流的形式進(jìn)行交互的,從而實(shí)現(xiàn)通信。
??接下來通過一張圖來描述服務(wù)器端和客戶端的數(shù)據(jù)傳輸,如下圖所示。

4.3 簡單的TCP網(wǎng)絡(luò)程序
??要實(shí)現(xiàn)TCP通信需要創(chuàng)建一個服務(wù)器端程序和一個客戶端程序,為了保證數(shù)據(jù)傳輸?shù)陌踩?,首先需要?shí)現(xiàn)服務(wù)器端程序。
服務(wù)端TCPServer.java
package com.qtw.api;
/* TCP 服務(wù)器端
*實(shí)現(xiàn)TCP服務(wù)器程序
* 表示服務(wù)器程序的類 java.net.ServerSocket
* 構(gòu)造方法
* ServerSocket(int port) 傳遞端口號
* 注意:必須要獲得客戶端的套接字對象Socket,方法accept()
*
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8888);
//調(diào)用服務(wù)器套接字對象的方法accept()獲取客戶端的套接字對象
Socket socket = server.accept();
System.out.println(socket);
//通過客戶端套接字對象socket獲取字節(jié)輸入流,讀取客戶端發(fā)送的數(shù)據(jù)
InputStream in = socket.getInputStream();
byte[] data = new byte[1024];
int len = in.read(data);
System.out.println(new String(data,0,len));
//服務(wù)器向客戶端回?cái)?shù)據(jù),字節(jié)輸出流,通過客戶端套接字對象獲取字節(jié)輸出流
OutputStream out = socket.getOutputStream();
out.write("服務(wù)器收到數(shù)據(jù)".getBytes());
socket.close();
server.close();
}
}
完成了服務(wù)器端程序的編寫,接下來編寫客戶端程序。
客戶端TCPClient.java
package com.qtw.api;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/*
* 實(shí)現(xiàn)TCP客戶端,連接到服務(wù)器,和服務(wù)器實(shí)現(xiàn)數(shù)據(jù)交換
* 實(shí)現(xiàn)TCP客戶端程序的類,java.net.Scoket
*
* 構(gòu)造方法
* Socket(String host,int port) 傳遞服務(wù)器IP和端口號
* 注意:構(gòu)造方法只要運(yùn)行就會和服務(wù)器進(jìn)行連接,連接失敗,拋出異常
* OutputStream getOutputStream() 返回套接字的輸出流
* 作用:將數(shù)據(jù)輸出,輸出到服務(wù)器
* InputStream getInputStream() 返回套接字的輸入流
* 作用:從服務(wù)器讀取數(shù)據(jù)
* 客戶端與服務(wù)器數(shù)據(jù)交換,必須使用套接字對象Socket中獲取的IO流,自己new不行
*
*/
public class TCPClient {
public static void main(String[] args) throws IOException {
//創(chuàng)建Scoket對象連接服務(wù)器
Socket socket = new Socket("192.168.3.3",8888);
//通過客戶端的套接字對象Socket方法,獲取字節(jié)輸出流,將數(shù)據(jù)寫向服務(wù)器
OutputStream out = socket.getOutputStream();
out.write("請求連接服務(wù)器".getBytes());
//讀取服務(wù)器發(fā)回的數(shù)據(jù),使用socket套接字對象中的字節(jié)輸入流
InputStream in = socket.getInputStream();
byte[] data = new byte[1024];
int len = in.read(data);
System.out.println(new String(data,0,len));
socket.close();
}
}
4.4 文件上傳案例
??目前大多數(shù)服務(wù)器都會提供文件上傳的功能,由于文件上傳需要數(shù)據(jù)的安全性和完整性,很明顯需要使用TCP協(xié)議來實(shí)現(xiàn)。接下來通過一個案例來實(shí)現(xiàn)圖片上傳的功能
? 首先編寫服務(wù)器端程序,用來接收圖片
package com.qtw.api;
/*
*TCP圖片上傳服務(wù)器
* 1. ServerSocket套接字對象,監(jiān)聽8000端口
* 2. 方法accept()獲取客戶端的連接對象
* 3. 客戶端連接對象獲取字節(jié)輸入流,獲取客戶端發(fā)送圖片
* 4. 創(chuàng)建File對象,綁定上傳文件夾,判斷文件夾是否存在
* 5. 創(chuàng)建字節(jié)輸出流,數(shù)據(jù)目的File對象所在文件夾
* 6. 字節(jié)流讀取圖片,字節(jié)流將圖片寫入到目的文件夾中
* 7. 將上傳成功回寫客戶端
* 8. 關(guān)閉資源
*/
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8000);
//調(diào)用服務(wù)器套接字對象的方法accept()獲取客戶端的套接字對象
Socket socket = server.accept();
//通過客戶端套接字對象socket獲取字節(jié)輸入流,讀取圖片
InputStream in = socket.getInputStream();
//將目的文件夾封裝到File對象
File upload = new File("d:\\upload");
if(!upload.exists()){
upload.mkdirs();
}
//防止文件同名覆蓋,重新定義文件名字
String filename = System.currentTimeMillis()+".jpg";
//創(chuàng)建字節(jié)輸出流,將圖片寫入目的文件夾中
FileOutputStream fos = new FileOutputStream(upload+File.separator+filename);
//讀字節(jié)數(shù)組
byte[] bytes = new byte[1024];
int len = 0;
while ((len = in.read(bytes)) != -1){
fos.write(bytes,0,len);
}
//服務(wù)器向客戶端回?cái)?shù)據(jù),上傳成功
OutputStream out = socket.getOutputStream();
out.write("上傳成功".getBytes());
socket.close();
server.close();
}
}
? 編寫客戶端,完成上傳圖片
package com.qtw.api;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/*
* 實(shí)現(xiàn)TCP圖片上傳客戶端
* 實(shí)現(xiàn)步驟
* 1. Socket 套接字連接服務(wù)器
* 2. 通過Socket獲取字節(jié)輸出流,寫圖片
* 3. 使用自己的流對象,讀取圖片數(shù)據(jù)流
* FileInputStream
* 4. 讀取圖片,使用字節(jié)輸出流,將圖片寫入服務(wù)器,采用字節(jié)數(shù)據(jù)進(jìn)行緩存
* 5. 通過Socket套接字獲取字節(jié)輸入流,讀取服務(wù)器發(fā)回來的上傳成功
* 6. 關(guān)閉資源
*/
public class TCPClient {
public static void main(String[] args) throws IOException {
//創(chuàng)建Scoket對象連接服務(wù)器
Socket socket = new Socket("192.168.3.3",8000);
//通過客戶端的套接字對象Socket方法,獲取字節(jié)輸出流,將數(shù)據(jù)寫向服務(wù)器
OutputStream out = socket.getOutputStream();
//讀取本機(jī)圖片資源
FileInputStream fis = new FileInputStream("e:\\test\\timg.jpg");
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1){
out.write(bytes,0,len);
}
//給服務(wù)器寫終止序列
socket.shutdownOutput();
//讀取服務(wù)器發(fā)回的數(shù)據(jù),上傳成功
InputStream in = socket.getInputStream();
byte[] data = new byte[1024];
len = in.read(data);
System.out.println(new String(data,0,len));
fis.close();
socket.close();
}
}
4.5 文件上傳案例多線程版本
實(shí)現(xiàn)服務(wù)器端可以同時(shí)接收多個客戶端上傳的文件。
我們要修改服務(wù)器端代碼
新建線程類Upload.java
package com.qtw.api;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Upload implements Runnable{
private Socket socket;
public Upload(Socket socket){
this.socket = socket;
}
public void run(){
try{
//通過客戶端套接字對象socket獲取字節(jié)輸入流,讀取圖片
InputStream in = socket.getInputStream();
//將目的文件夾封裝到File對象
File upload = new File("d:\\upload");
if(!upload.exists()){
upload.mkdirs();
}
//防止文件同名覆蓋,重新定義文件名字
String filename = System.currentTimeMillis()+".jpg";
//創(chuàng)建字節(jié)輸出流,將圖片寫入目的文件夾中
FileOutputStream fos = new FileOutputStream(upload+File.separator+filename);
//讀字節(jié)數(shù)組
byte[] bytes = new byte[1024];
int len = 0;
while ((len = in.read(bytes)) != -1){
fos.write(bytes,0,len);
}
//服務(wù)器向客戶端回?cái)?shù)據(jù),上傳成功
OutputStream out = socket.getOutputStream();
out.write("上傳成功".getBytes());
socket.close();
}catch (Exception ex){}
}
}
服務(wù)器端TCPThreadServer.java
package com.qtw.api;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPThreadServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8000);
//獲取到一個客戶端,必須開啟新線程
while(true){
Socket socket = server.accept();
new Thread(new Upload(socket)).start();
}
}
}