1. 網絡通信協議
通過計算機網絡可以使多台計算機實現連接,位於同一個網絡中的計算機在進行連接和通信時需要遵守一定的規則,這就好比在道路中行駛的汽車一定要遵守交通規則一樣。在計算機網絡中,這些連接和通信的規則被稱為網絡通信協議,它對數據的傳輸格式、傳輸速率、傳輸步驟等做了統一規定,通信雙方必須同時遵守才能完成數據交換。
網絡通信協議有很多種,目前應用最廣泛的是TCP/IP協議(Transmission Control Protocal/Internet Protoal傳輸控制協議/英特網互聯協議),它是一個包括TCP協議和IP協議,UDP(User Datagram Protocol)協議和其它一些協議的協議組,在學習具體協議之前首先了解一下TCP/IP協議組的層次結構。
在進行數據傳輸時,要求發送的數據與收到的數據完全一樣,這時,就需要在原有的數據上添加很多信息,以保證數據在傳輸過程中數據格式完全一致。TCP/IP協議的層次結構比較簡單,共分為四層,如圖所示。
上圖中,TCP/IP協議中的四層分別是應用層、傳輸層、網絡層和鏈路層,每層分別負責不同的通信功能,接下來針對這四層進行詳細地講解。
鏈路層:鏈路層是用於定義物理傳輸通道,通常是對某些網絡連接設備的驅動協議,例如針對光纖、網線提供的驅動。
網絡層:網絡層是整個TCP/IP協議的核心,它主要用於將傳輸的數據進行分組,將分組數據發送到目標計算機或者網絡。
傳輸層:主要使網絡程序進行通信,在進行網絡通信時,可以采用TCP協議,也可以采用UDP協議。
應用層:主要負責應用程序的協議,例如HTTP協議、FTP協議等。
1.1 IP地址和端口號
要想使網絡中的計算機能夠進行通信,必須為每台計算機指定一個標識號,通過這個標識號來指定接受數據的計算機或者發送數據的計算機。
在TCP/IP協議中,這個標識號就是IP地址,它可以唯一標識一台計算機,目前,IP地址廣泛使用的版本是IPv4,它是由4個字節大小的二進制數來表示,如:00001010000000000000000000000001。由於二進制形式表示的IP地址非常不便記憶和處理,因此通常會將IP地址寫成十進制的形式,每個字節用一個十進制數字(0-255)表示,數字間用符號“.”分開,如 “192.168.1.100”。
隨着計算機網絡規模的不斷擴大,對IP地址的需求也越來越多,IPV4這種用4個字節表示的IP地址面臨枯竭,因此IPv6 便應運而生了,IPv6使用16個字節表示IP地址,它所擁有的地址容量約是IPv4的8×1028倍,達到2128個(算上全零的),這樣就解決了網絡地址資源數量不夠的問題。
通過IP地址可以連接到指定計算機,但如果想訪問目標計算機中的某個應用程序,還需要指定端口號。在計算機中,不同的應用程序是通過端口號區分的。端口號是用兩個字節(16位的二進制數)表示的,它的取值范圍是0~65535,其中,0~1023之間的端口號用於一些知名的網絡服務和應用,用戶的普通應用程序需要使用1024以上的端口號,從而避免端口號被另外一個應用或服務所占用。
接下來通過一個圖例來描述IP地址和端口號的作用,如下圖所示。
從上圖中可以清楚地看到,位於網絡中一台計算機可以通過IP地址去訪問另一台計算機,並通過端口號訪問目標計算機中的某個應用程序。
1.2 InetAddress
了解了IP地址的作用,我們看學習下JDK中提供了一個InetAdderss類,該類用於封裝一個IP地址,並提供了一系列與IP地址相關的方法,下表中列出了InetAddress類的一些常用方法。
上圖中,列舉了InetAddress的四個常用方法。其中,前兩個方法用於獲得該類的實例對象,第一個方法用於獲得表示指定主機的InetAddress對象,第二個方法用於獲得表示本地的InetAddress對象。通過InetAddress對象便可獲取指定主機名,IP地址等,接下來通過一個案例來演示InetAddress的常用方法,如下所示。
package cn.jxufe.java.chapter13.demo01; import java.net.InetAddress; import java.net.UnknownHostException; /* * 表示互聯網中的IP地址 * java.net.InetAddress * 靜態方法 * static InetAddress getLocalHost() LocalHost本地主機 * 返回本地主機,返回值InetAddress對象 * * static InetAddress getByName(String hostName)傳遞主機名,獲取IP地址對象 * * 非靜態方法 * String getHoustAddress()獲取主機IP地址 * String getHoustName()獲取主機名 * */ public class Test01InetAddress { public static void main(String[] args) throws UnknownHostException { // TODO Auto-generated method stub InetAddress inet = InetAddress.getLocalHost(); // 輸出結果就是主機名,和 IP地址 System.out.println(inet.toString()); String ip = inet.getHostAddress(); String name = inet.getHostName(); System.out.println(name); System.out.println(ip); /*String host = inet.toString(); String[] str = host.split("/"); for(String s : str){ System.out.println(s); }*/ InetAddress inet2 = InetAddress.getByName("www.baidu.com"); System.out.println(inet2); } }
2. UDP與TCP協議
在介紹TCP/IP結構時,提到傳輸層的兩個重要的高級協議,分別是UDP和TCP,其中UDP是User Datagram Protocol的簡稱,稱為用戶數據報協議,TCP是Transmission Control Protocol的簡稱,稱為傳輸控制協議。
2.1 UDP協議
UDP是無連接通信協議,即在數據傳輸時,數據的發送端和接收端不建立邏輯連接。簡單來說,當一台計算機向另外一台計算機發送數據時,發送端不會確認接收端是否存在,就會發出數據,同樣接收端在收到數據時,也不會向發送端反饋是否收到數據。
由於使用UDP協議消耗資源小,通信效率高,所以通常都會用於音頻、視頻和普通數據的傳輸例如視頻會議都使用UDP協議,因為這種情況即使偶爾丟失一兩個數據包,也不會對接收結果產生太大影響。
但是在使用UDP協議傳送數據時,由於UDP的面向無連接性,不能保證數據的完整性,因此在傳輸重要數據時不建議使用UDP協議。UDP的交換過程如下圖所示。
2.2 TCP協議
TCP協議是面向連接的通信協議,即在傳輸數據前先在發送端和接收端建立邏輯連接,然后再傳輸數據,它提供了兩台計算機之間可靠無差錯的數據傳輸。在TCP連接中必須要明確客戶端與服務器端,由客戶端向服務端發出連接請求,每次連接的創建都需要經過“三次握手”。第一次握手,客戶端向服務器端發出連接請求,等待服務器確認,第二次握手,服務器端向客戶端回送一個響應,通知客戶端收到了連接請求,第三次握手,客戶端再次向服務器端發送確認信息,確認連接。整個交互過程如下圖所示。
由於TCP協議的面向連接特性,它可以保證傳輸數據的安全性,所以是一個被廣泛采用的協議,例如在下載文件時,如果數據接收不完整,將會導致文件數據丟失而不能被打開,因此,下載文件時必須采用TCP協議。
3. UDP通信
3.1 DatagramPacket
前面介紹了UDP是一種面向無連接的協議,因此,在通信時發送端和接收端不用建立連接。UDP通信的過程就像是貨運公司在兩個碼頭間發送貨物一樣。在碼頭發送和接收貨物時都需要使用集裝箱來裝載貨物,UDP通信也是一樣,發送和接收的數據也需要使用“集裝箱”進行打包,為此JDK中提供了一個DatagramPacket類,該類的實例對象就相當於一個集裝箱,用於封裝UDP通信中發送或者接收的數據。
想要創建一個DatagramPacket對象,首先需要了解一下它的構造方法。在創建發送端和接收端的DatagramPacket對象時,使用的構造方法有所不同,接收端的構造方法只需要接收一個字節數組來存放接收到的數據,而發送端的構造方法不但要接收存放了發送數據的字節數組,還需要指定發送端IP地址和端口號。
接下來根據API文檔的內容,對DatagramPacket的構造方法進行逐一詳細地講解。
使用該構造方法在創建DatagramPacket對象時,指定了封裝數據的字節數組和數據的大小,沒有指定IP地址和端口號。很明顯,這樣的對象只能用於接收端,不能用於發送端。因為發送端一定要明確指出數據的目的地(ip地址和端口號),而接收端不需要明確知道數據的來源,只需要接收到數據即可。
使用該構造方法在創建DatagramPacket對象時,不僅指定了封裝數據的字節數組和數據的大小,還指定了數據包的目標IP地址(addr)和端口號(port)。該對象通常用於發送端,因為在發送數據時必須指定接收端的IP地址和端口號,就好像發送貨物的集裝箱上面必須標明接收人的地址一樣。
上面我們講解了DatagramPacket的構造方法,接下來對DatagramPacket類中的常用方法進行詳細地講解,如下表所示。
3.2 DatagramSocket
DatagramPacket數據包的作用就如同是“集裝箱”,可以將發送端或者接收端的數據封裝起來。然而運輸貨物只有“集裝箱”是不夠的,還需要有碼頭。在程序中需要實現通信只有DatagramPacket數據包也同樣不行,為此JDK中提供的一個DatagramSocket類。DatagramSocket類的作用就類似於碼頭,使用這個類的實例對象就可以發送和接收DatagramPacket數據包,發送數據的過程如下圖所示。
在創建發送端和接收端的DatagramSocket對象時,使用的構造方法也有所不同,下面對DatagramSocket類中常用的構造方法進行講解。
該構造方法用於創建發送端的DatagramSocket對象,在創建DatagramSocket對象時,並沒有指定端口號,此時,系統會分配一個沒有被其它網絡程序所使用的端口號。
該構造方法既可用於創建接收端的DatagramSocket對象,又可以創建發送端的DatagramSocket對象,在創建接收端的DatagramSocket對象時,必須要指定一個端口號,這樣就可以監聽指定的端口。
上面我們講解了DatagramSocket的構造方法,接下來對DatagramSocket類中的常用方法進行詳細地講解。
package cn.jxufe.java.chapter13.demo01; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.UnknownHostException; /* * 實現UDP協議的發送端: * 實現封裝數據的類 java.net.DatagramPacket 將你的數據包裝 * 實現數據傳輸的類 java.net.DatagramSocket 將數據包發出去 * * 實現步驟: * 1. 創建DatagramPacket對象,封裝數據, 接收的地址和端口 * 2. 創建DatagramSocket * 3. 調用DatagramSocket類方法send,發送數據包 * 4. 關閉資源 * * DatagramPacket構造方法: * DatagramPacket(byte[] buf, int length, InetAddress address, int port) * * DatagramSocket構造方法: * DatagramSocket()空參數 * 方法: send(DatagramPacket d) * */ public class Test02UDPSend { public static void main(String[] args) throws IOException { // TODO Auto-generated method stub // 創建數據包對象,封裝要發送的數據,接收端IP,端口 byte[] data = "你好UDP".getBytes(); // 創建InetAddress對象,封裝自己的IP地址 InetAddress inet = InetAddress.getByName("127.0.0.1"); DatagramPacket dp = new DatagramPacket(data, data.length, inet, 6000); // 創建DatagramSocket對象,數據包的發送和接收對象 DatagramSocket ds = new DatagramSocket(); // 調用ds對象的方法send,發送數據包 ds.send(dp); // 關閉資源 ds.close(); } }
package cn.jxufe.java.chapter13.demo01; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; /* * 實現UDP接收端 * 實現封裝數據包 java.net.DatagramPacket 將數據接收 * 實現輸出傳輸 java.net.DatagramSocket 接收數據包 * * 實現步驟: * 1. 創建DatagramSocket對象,綁定端口號 * 要和發送端端口號一致 * 2. 創建字節數組,接收發來的數據 * 3. 創建數據包對象DatagramPacket * 4. 調用DatagramSocket對象方法 * receive(DatagramPacket dp)接收數據,數據放在數據包中 * 5. 拆包 * 發送的IP地址 * 數據包對象DatagramPacket方法getAddress()獲取的是發送端的IP地址對象 * 返回值是InetAddress對象 * 接收到的字節個數 * 數據包對象DatagramPacket方法 getLength() * 發送方的端口號 * 數據包對象DatagramPacket方法 getPort()發送端口 * 6. 關閉資源 */ public class Test03UDPReceive { public static void main(String[] args) throws IOException { // 創建數據包傳輸對象DatagramSocket 綁定端口號 DatagramSocket ds = new DatagramSocket(6000); // 創建字節數組 byte[] data = new byte[1024]; // 創建數據包對象,傳遞字節數組 DatagramPacket dp = new DatagramPacket(data, data.length); // 調用ds對象的方法receive傳遞數據包 ds.receive(dp); // 獲取發送端的IP地址對象 String ip = dp.getAddress().getHostAddress(); // 獲取發送的端口號 int port = dp.getPort(); // 獲取接收到的字節個數 int length = dp.getLength(); System.out.println(new String(data, 0, length) + "..." + ip + ":" + port); ds.close(); } }
4.TCP通信
TCP通信同UDP通信一樣,都能實現兩台計算機之間的通信,通信的兩端都需要創建socket對象。
區別在於,UDP中只有發送端和接收端,不區分客戶端與服務器端,計算機之間可以任意地發送數據。
而TCP通信是嚴格區分客戶端與服務器端的,在通信時,必須先由客戶端去連接服務器端才能實現通信,服務器端不可以主動連接客戶端,並且服務器端程序需要事先啟動,等待客戶端的連接。
在JDK中提供了兩個類用於實現TCP程序,一個是ServerSocket類,用於表示服務器端,一個是Socket類,用於表示客戶端。
通信時,首先創建代表服務器端的ServerSocket對象,該對象相當於開啟一個服務,並等待客戶端的連接,然后創建代表客戶端的Socket對象向服務器端發出連接請求,服務器端響應請求,兩者建立連接開始通信。
4.1ServerSocket
通過前面的學習知道,在開發TCP程序時,首先需要創建服務器端程序。JDK的java.net包中提供了一個ServerSocket類,該類的實例對象可以實現一個服務器段的程序。通過查閱API文檔可知,ServerSocket類提供了多種構造方法,接下來就對ServerSocket的構造方法進行逐一地講解。
使用該構造方法在創建ServerSocket對象時,就可以將其綁定到一個指定的端口號上(參數port就是端口號)。
接下來學習一下ServerSocket的常用方法,如表所示。
ServerSocket對象負責監聽某台計算機的某個端口號,在創建ServerSocket對象后,需要繼續調用該對象的accept()方法,接收來自客戶端的請求。當執行了accept()方法之后,服務器端程序會發生阻塞,直到客戶端發出連接請求,accept()方法才會返回一個Scoket對象用於和客戶端實現通信,程序才能繼續向下執行。
4.2Socket
講解了ServerSocket對象可以實現服務端程序,但只實現服務器端程序還不能完成通信,此時還需要一個客戶端程序與之交互,為此JDK提供了一個Socket類,用於實現TCP客戶端程序。
通過查閱API文檔可知Socket類同樣提供了多種構造方法,接下來就對Socket的常用構造方法進行詳細講解。
使用該構造方法在創建Socket對象時,會根據參數去連接在指定地址和端口上運行的服務器程序,其中參數host接收的是一個字符串類型的IP地址。
該方法在使用上與第二個構造方法類似,參數address用於接收一個InetAddress類型的對象,該對象用於封裝一個IP地址。
在以上Socket的構造方法中,最常用的是第一個構造方法。
接下來學習一下Socket的常用方法,如表所示。
方法聲明 |
功能描述 |
int getPort() |
該方法返回一個int類型對象,該對象是Socket對象與服務器端連接的端口號 |
InetAddress getLocalAddress() |
該方法用於獲取Socket對象綁定的本地IP地址,並將IP地址封裝成InetAddress類型的對象返回 |
void close() |
該方法用於關閉Socket連接,結束本次通信。在關閉socket之前,應將與socket相關的所有的輸入/輸出流全部關閉,這是因為一個良好的程序應該在執行完畢時釋放所有的資源 |
InputStream getInputStream() |
該方法返回一個InputStream類型的輸入流對象,如果該對象是由服務器端的Socket返回,就用於讀取客戶端發送的數據,反之,用於讀取服務器端發送的數據 |
OutputStream getOutputStream() |
該方法返回一個OutputStream類型的輸出流對象,如果該對象是由服務器端的Socket返回,就用於向客戶端發送數據,反之,用於向服務器端發送數據 |
在Socket類的常用方法中,getInputStream()和getOutStream()方法分別用於獲取輸入流和輸出流。當客戶端和服務端建立連接后,數據是以IO流的形式進行交互的,從而實現通信。
接下來通過一張圖來描述服務器端和客戶端的數據傳輸,如下圖所示。
package cn.jxufe.java.chapter13.demo01; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; /* * 實現TCP客戶端,連接到服務器 * 和服務器實現數據交換 * 實現TCP客戶端程序的類 java.net.Socket * * 構造方法: * Socket(String host, int port) 傳遞服務器IP和端口號 * 注意:構造方法只要運行,就會和服務器進行連接,連接失敗,拋出異常 * * OutputStream getOutputStream() 返回套接字的輸出流 * 作用: 將數據輸出,輸出到服務器 * * InputStream getInputStream() 返回套接字的輸入流 * 作用: 從服務器端讀取數據 * * 客戶端服務器數據交換,必須使用套接字對象Socket中的獲取的IO流,自己new流,不行 */ public class Test04TCPClient { public static void main(String[] args) throws IOException { // TODO Auto-generated method stub // 創建Socket對象,連接服務器 Socket socket = new Socket("127.0.0.1", 8888); // 通過客戶端的套接字對象Socket方法,獲取字節輸出流,將數據寫向服務器 OutputStream out = socket.getOutputStream(); out.write("服務器OK".getBytes()); // 讀取服務器發回的數據,使用socket套接字對象中的字節輸入流 InputStream in = socket.getInputStream(); byte[] data = new byte[1024]; int len = in.read(data); System.out.println(new String(data, 0, len)); socket.close(); } }
package cn.jxufe.java.chapter13.demo01; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; /* * 實現TCP服務器程序 * 表示服務器程序的類 java.net.ServerSocket * 構造方法: * ServerSocket(int port) 傳遞端口號 * * 很重要的事情: 必須要獲得客戶端的套接字對象Socket * Socket accept() */ public class Test05TCPServer { public static void main(String[] args) throws IOException { // TODO Auto-generated method stub ServerSocket server = new ServerSocket(8888); // 調用服務器套接字對象中的方法accept() 獲取客戶端套接字對象 Socket socket = server.accept(); // 通過客戶端套接字對象,socket獲取字節輸入流,讀取的是客戶端發送來的數據 InputStream in = socket.getInputStream(); byte[] data = new byte[1024]; int len = in.read(data); System.out.println(new String(data, 0, len)); // 服務器向客戶端回數據,字節輸出流,通過客戶端套接字對象獲取字節輸出流 OutputStream out = socket.getOutputStream(); out.write("收到,謝謝".getBytes()); socket.close(); server.close(); } }
4.3文件上傳案例
目前大多數服務器都會提供文件上傳的功能,由於文件上傳需要數據的安全性和完整性,很明顯需要使用TCP協議來實現。接下來通過一個案例來實現圖片上傳的功能。如下圖所示。原圖:文件上傳.bmp
package cn.jxufe.java.chapter13.demo01; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; /* * 實現TCP圖片上傳客戶端 * 實現步驟: * 1. Socket套接字連接服務器 * 2. 通過Socket獲取字節輸出流,寫圖片 * 3. 使用自己的流對象,讀取圖片數據源 * FileInputStream * 4. 讀取圖片,使用字節輸出流,將圖片寫到服務器 * 采用字節數組進行緩沖 * 5. 通過Socket套接字獲取字節輸入流 * 讀取服務器發回來的上傳成功 * 6. 關閉資源 */ public class Test06TCPClient { public static void main(String[] args) throws IOException { // TODO Auto-generated method stub Socket socket = new Socket("127.0.0.1", 8000); // 獲取字節輸出流,圖片寫到服務器 OutputStream out = socket.getOutputStream(); // 創建字節輸入流,讀取本機上的數據源圖片 FileInputStream fis = new FileInputStream("D:\\1.jpg"); // 開始讀寫字節數組 int len = 0; byte[] bytes = new byte[1024]; while ((len = fis.read(bytes)) != -1) { out.write(bytes, 0, len); } // 給服務器寫終止序列 socket.shutdownOutput(); // 獲取字節輸入流,讀取服務器的上傳成功 InputStream in = socket.getInputStream(); len = in.read(bytes); System.out.println(new String(bytes, 0, len)); fis.close(); socket.close(); } }
package cn.jxufe.java.chapter13.demo01; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.Random; /* * TCP圖片上傳服務器 * 1. ServerSocket套接字對象,監聽端口8000 * 2. 方法accept()獲取客戶端的連接對象 * 3. 客戶端連接對象獲取字節輸入流,讀取客戶端發送圖片 * 4. 創建File對象,綁定上傳文件夾 * 判斷文件夾存在, 不存,在創建文件夾 * 5. 創建字節輸出流,數據目的File對象所在文件夾 * 6. 字節流讀取圖片,字節流將圖片寫入到目的文件夾中 * 7. 將上傳成功會寫客戶端 * 8. 關閉資源 * */ public class Test07TCPServer { public static void main(String[] args) throws IOException { // TODO Auto-generated method stub ServerSocket server = new ServerSocket(8000); Socket socket = server.accept(); // 通過客戶端連接對象,獲取字節輸入流,讀取客戶端圖片 InputStream in = socket.getInputStream(); // 將目的文件夾封裝到File對象 File upload = new File("d:\\upload"); if (!upload.exists()) upload.mkdirs(); // 防止文件同名被覆蓋,從新定義文件名字 // 規則: 域名+毫秒值+6位隨機數 String filename = "jxufe" + System.currentTimeMillis() + new Random().nextInt(999999) + ".jpg"; // 創建字節輸出流,將圖片寫入到目的文件夾中 FileOutputStream fos = new FileOutputStream(upload + File.separator + filename); // 讀寫字節數組 byte[] bytes = new byte[1024]; int len = 0; while ((len = in.read(bytes)) != -1) { fos.write(bytes, 0, len); } // 通過客戶端連接對象獲取字節輸出流 // 上傳成功寫回客戶端 socket.getOutputStream().write("上傳成功".getBytes()); fos.close(); socket.close(); server.close(); } }
多線程實現
package cn.jxufe.java.chapter13.demo01; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.net.Socket; import java.util.Random; public class Upload implements Runnable { private Socket socket; public Upload(Socket socket) { // TODO Auto-generated constructor stub this.socket = socket; } public void run() { // TODO Auto-generated method stub try { // 通過客戶端連接對象,獲取字節輸入流,讀取客戶端圖片 InputStream in = socket.getInputStream(); // 將目的文件夾封裝到File對象 File upload = new File("d:\\upload"); if (!upload.exists()) upload.mkdirs(); // 防止文件同名被覆蓋,從新定義文件名字 // 規則: 域名+毫秒值+6位隨機數 String filename = "itcast" + System.currentTimeMillis() + new Random().nextInt(999999) + ".jpg"; // 創建字節輸出流,將圖片寫入到目的文件夾中 FileOutputStream fos = new FileOutputStream(upload + File.separator + filename); // 讀寫字節數組 byte[] bytes = new byte[1024]; int len = 0; while ((len = in.read(bytes)) != -1) { fos.write(bytes, 0, len); } // 通過客戶端連接對象獲取字節輸出流 // 上傳成功寫回客戶端 socket.getOutputStream().write("上傳成功".getBytes()); fos.close(); socket.close(); } catch (Exception ex) { } } }
package cn.jxufe.java.chapter13.demo01; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class Test09TCPServer { public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(8000); // 獲取到一個客戶端,必須開啟新線程 Socket socket = server.accept(); // new Thread(new Test08Upload(socket)).start(); new Thread(new Upload(socket)).start(); } }