java語言進階(十一)_網路程式設計
目錄
第一章 網路程式設計入門
1.1 軟體結構
- C/S結構 :全稱為Client/Server結構,是指客戶端和伺服器結構。常見程式有QQ、迅雷等軟體。
- B/S結構 :全稱為Browser/Server結構,是指瀏覽器和伺服器結構。常見瀏覽器有谷歌、火狐等。
兩種架構各有優勢,但是無論哪種架構,都離不開網路的支援。網路程式設計,就是在一定的協議下,實現兩臺計算機的通訊的程式。
1.2 網路通訊協議
-
網路通訊協議:通過計算機網路可以使多臺計算機實現連線,位於同一個網路中的計算機在進行連線和通訊時需要遵守一定的規則,這就好比在道路中行駛的汽車一定要遵守交通規則一樣。在計算機網路中,這些連線和通訊的規則被稱為網路通訊協議,它對資料的傳輸格式、傳輸速率、傳輸步驟等做了統一規定,通訊雙方必須同時遵守才能完成資料交換。
-
TCP/IP協議: 傳輸控制協議/因特網互聯協議( Transmission Control Protocol/Internet Protocol),是Internet最基本、最廣泛的協議。它定義了計算機如何連入因特網
上圖中,TCP/IP協議中的四層分別是應用層、傳輸層、網路層和鏈路層,每層分別負責不同的通訊功能。
- 鏈路層:鏈路層是用於定義物理傳輸通道,通常是對某些網路連線裝置的驅動協議,例如針對光纖、網線提供的驅動。
- 網路層:網路層是整個TCP/IP協議的核心,它主要用於將傳輸的資料進行分組,將分組資料傳送到目標計算機或者網路。
- 運輸層:主要使網路程式進行通訊,在進行網路通訊時,可以採用TCP協議,也可以採用UDP協議。
- 應用層:主要負責應用程式的協議,例如HTTP協議、FTP協議等。
1.3 協議分類
通訊的協議還是比較複雜的,java.net
包中包含的類和介面,它們提供低層次的通訊細節。可以直接使用這些類和介面,來專注於網路程式開發,而不用考慮通訊的細節。
java.net
包中提供了兩種常見的網路協議的支援:
-
UDP:使用者資料報協議(User Datagram Protocol)。UDP是無連線通訊協議,即在資料傳輸時,資料的傳送端和接收端不建立邏輯連線。簡單來說,當一臺計算機向另外一臺計算機發送資料時,傳送端不會確認接收端是否存在,就會發出資料,同樣接收端在收到資料時,也不會向傳送端反饋是否收到資料。
由於使用UDP協議消耗資源小,通訊效率高,所以通常都會用於音訊、視訊和普通資料的傳輸例如視訊會議都使用UDP協議,因為這種情況即使偶爾丟失一兩個資料包,也不會對接收結果產生太大影響。
但是在使用UDP協議傳送資料時,由於UDP的面向無連線性,不能保證資料的完整性,因此在傳輸重要資料時不建議使用UDP協議。UDP的交換過程如下圖所示。
特點:資料被限制在64kb以內,超出這個範圍就不能傳送了。
資料報(Datagram):網路傳輸的基本單位
-
TCP:傳輸控制協議 (Transmission Control Protocol)。TCP協議是面向連線的通訊協議,即傳輸資料之前,在傳送端和接收端建立邏輯連線,然後再傳輸資料,它提供了兩臺計算機之間可靠無差錯的資料傳輸。
在TCP連線中必須要明確客戶端與伺服器端,由客戶端向服務端發出連線請求,每次連線的建立都需要經過“三次握手”。
- 三次握手:TCP協議中,在傳送資料的準備階段,客戶端與伺服器之間的三次互動,以保證連線的可靠。
- 第一次握手,客戶端向伺服器端發出連線請求,等待伺服器確認。
- 第二次握手,伺服器端向客戶端回送一個響應,通知客戶端收到了連線請求。
- 第三次握手,客戶端再次向伺服器端傳送確認資訊,確認連線。整個互動過程如下圖所示。
- 三次握手:TCP協議中,在傳送資料的準備階段,客戶端與伺服器之間的三次互動,以保證連線的可靠。
完成三次握手,連線建立後,客戶端和伺服器就可以開始進行資料傳輸了。由於這種面向連線的特性,TCP協議可以保證傳輸資料的安全,所以應用十分廣泛,例如下載檔案、瀏覽網頁等。
1.4 網路程式設計三要素
協議
- 協議:計算機網路通訊必須遵守的規則。
IP地址
- IP地址:指網際網路協議地址(Internet Protocol Address),俗稱IP。IP地址用來給一個網路中的計算機裝置做唯一的編號。假如把“個人電腦”比作“一臺電話”的話,那麼“IP地址”就相當於“電話號碼”。
IP地址分類
-
IPv4:是一個32位的二進位制數,通常被分為4個位元組,表示成
a.b.c.d
的形式,例如192.168.65.100
。其中a、b、c、d都是0~255之間的十進位制整數,那麼最多可以表示42億個。 -
IPv6:由於網際網路的蓬勃發展,IP地址的需求量愈來愈大,但是網路地址資源有限,使得IP的分配越發緊張。
為了擴大地址空間,擬通過IPv6重新定義地址空間,採用128位地址長度,每16個位元組一組,分成8組十六進位制數,表示成
ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
,號稱可以為全世界的每一粒沙子編上一個網址,這樣就解決了網路地址資源數量不夠的問題。
常用命令
- 檢視本機IP地址,在控制檯輸入:
ipconfig
- 檢查網路是否連通,在控制檯輸入:
ping 空格 IP地址
ping 220.181.57.216
特殊的IP地址
- 本機IP地址:
127.0.0.1
、localhost
。
埠號
網路的通訊,本質上是[兩個程序](應用程式)的通訊。每臺計算機都有很多的程序,那麼在網路通訊時,如何區分這些程序呢?
如果說IP地址可以唯一標識網路中的裝置,那麼埠號就可以唯一標識裝置中的程序(應用程式)。
- 埠號:用兩個位元組表示的整數,它的取值範圍是0~65535。其中,0~1023之間的埠號用於一些知名的網路服務和應用,普通的應用程式需要使用1024以上的埠號。如果埠號被另外一個服務或應用所佔用,會導致當前程式啟動失敗。
利用協議
+IP地址
+埠號
三元組合,就可以標識網路中的程序,那麼程序間的通訊就可以利用這個標識與其它程序進行互動。
第二章 TCP通訊程式
2.1 概述
TCP通訊能實現兩臺計算機之間的資料互動,通訊的兩端,要嚴格區分為客戶端(Client)與服務端(Server)。
兩端通訊時步驟:
-
服務端程式,需要事先啟動,等待客戶端的連線。
-
客戶端主動連線伺服器端,連線成功才能通訊。服務端不可以主動連線客戶端。
在Java中,提供了兩個類用於實現TCP通訊程式:
-
客戶端:
java.net.Socket
類表示。建立Socket
物件,向服務端發出連線請求,服務端響應請求,兩者建立連線開始通訊。 -
服務端:
java.net.ServerSocket
類表示。建立ServerSocket
物件,相當於開啟一個服務,並等待客戶端的連線。
2.2 Socket類
Socket
類:該類實現客戶端套接字,套接字指的是兩臺裝置之間通訊的端點,包含了IP地址和埠號的網路單位。
構造方法
-
public Socket(String host, int port)
:建立套接字物件並將其連線到指定主機上的指定埠號。如果指定的 host 是 null ,則相當於指定地址為回送地址。小貼士:回送地址(127.x.x.x) 是本機回送地址(Loopback Address),主要用於網路軟體測試以及本地機程序間通訊,無論什麼程式,一旦使用回送地址傳送資料,立即返回,不進行任何網路傳輸。
構造舉例,程式碼如下:
Socket client = new Socket("127.0.0.1", 6666);
成員方法
public InputStream getInputStream()
: 返回此套接字的輸入流。- 如果此 Scoket 具有相關聯的通道,則生成的 InputStream 的所有操作也關聯該通道。
- 關閉生成的 InputStream 也將關閉相關的 Socket。
public OutputStream getOutputStream()
: 返回此套接字的輸出流。- 如果此 Scoket 具有相關聯的通道,則生成的 OutputStream 的所有操作也關聯該通道。
- 關閉生成的 OutputStream 也將關閉相關的 Socket。
public void close()
:關閉此套接字。- 一旦一個 socket 被關閉,它不可再使用。
- 關閉此 socket 也將關閉相關的 InputStream 和 OutputStream 。
public void shutdownOutput()
: 禁用此套接字的輸出流。- 任何先前寫出的資料將被髮送,隨後終止輸出流。
2.3 ServerSocket類
ServerSocket
類:這個類實現了伺服器套接字,該物件等待通過網路的請求。
構造方法
public ServerSocket(int port)
:使用該構造方法在建立 ServerSocket 物件時,就可以將其繫結到一個指定的埠號上,引數 port 就是埠號。
構造舉例,程式碼如下:
ServerSocket server = new ServerSocket(6666);
成員方法
public Socket accept()
:偵聽並接受連線,返回一個新的 Socket 物件,用於和客戶端實現通訊。該方法會一直阻塞直到建立連線。
2.4 簡單的TCP網路程式
TCP通訊分析圖解
- 【服務端】啟動,建立 ServerSocket 物件,等待連線。
- 【客戶端】啟動,建立 Socket 物件,請求連線。
- 【服務端】接收連線,呼叫 accept 方法,並返回一個 Socket 物件。
- 【客戶端】Socket 物件,獲取 OutputStream,向服務端寫出資料。
- 【服務端】Scoket 物件,獲取 InputStream,讀取客戶端傳送的資料。
到此,客戶端向服務端傳送資料成功。
自此,服務端向客戶端回寫資料。
- 【服務端】Socket 物件,獲取 OutputStream,向客戶端回寫資料。
- 【客戶端】Scoket 物件,獲取 InputStream,解析回寫資料。
- 【客戶端】釋放資源,斷開連線。
客戶端向伺服器傳送資料
服務端實現:
public class ServerTCP {
public static void main(String[] args) throws IOException {
System.out.println("服務端啟動 , 等待連線 .... ");
// 1.建立 ServerSocket物件,繫結埠,開始等待連線
ServerSocket ss = new ServerSocket(6666);
// 2.接收連線 accept 方法, 返回 socket 物件.
Socket server = ss.accept();
// 3.通過socket 獲取輸入流
InputStream is = server.getInputStream();
// 4.一次性讀取資料
// 4.1 建立位元組陣列
byte[] b = new byte[1024];
// 4.2 據讀取到位元組陣列中.
int len = is.read(b);
// 4.3 解析陣列,列印字串資訊
String msg = new String(b, 0, len);
System.out.println(msg);
//5.關閉資源.
is.close();
server.close();
}
}
客戶端實現:
public class ClientTCP {
public static void main(String[] args) throws Exception {
System.out.println("客戶端 傳送資料");
// 1.建立 Socket ( ip , port ) , 確定連線到哪裡.
Socket client = new Socket("localhost", 6666);
// 2.獲取流物件 . 輸出流
OutputStream os = client.getOutputStream();
// 3.寫出資料.
os.write("你好麼? tcp ,我來了".getBytes());
// 4. 關閉資源 .
os.close();
client.close();
}
}
伺服器向客戶端回寫資料
服務端實現:
public class ServerTCP {
public static void main(String[] args) throws IOException {
System.out.println("服務端啟動 , 等待連線 .... ");
// 1.建立 ServerSocket物件,繫結埠,開始等待連線
ServerSocket ss = new ServerSocket(6666);
// 2.接收連線 accept 方法, 返回 socket 物件.
Socket server = ss.accept();
// 3.通過socket 獲取輸入流
InputStream is = server.getInputStream();
// 4.一次性讀取資料
// 4.1 建立位元組陣列
byte[] b = new byte[1024];
// 4.2 據讀取到位元組陣列中.
int len = is.read(b);
// 4.3 解析陣列,列印字串資訊
String msg = new String(b, 0, len);
System.out.println(msg);
// =================回寫資料=======================
// 5. 通過 socket 獲取輸出流
OutputStream out = server.getOutputStream();
// 6. 回寫資料
out.write("我很好,謝謝你".getBytes());
// 7.關閉資源.
out.close();
is.close();
server.close();
}
}
客戶端實現:
public class ClientTCP {
public static void main(String[] args) throws Exception {
System.out.println("客戶端 傳送資料");
// 1.建立 Socket ( ip , port ) , 確定連線到哪裡.
Socket client = new Socket("localhost", 6666);
// 2.通過Scoket,獲取輸出流物件
OutputStream os = client.getOutputStream();
// 3.寫出資料.
os.write("你好麼? tcp ,我來了".getBytes());
// ==============解析回寫=========================
// 4. 通過Scoket,獲取 輸入流物件
InputStream in = client.getInputStream();
// 5. 讀取資料資料
byte[] b = new byte[100];
int len = in.read(b);
System.out.println(new String(b, 0, len));
// 6. 關閉資源 .
in.close();
os.close();
client.close();
}
}
第三章 綜合案例
3.1 檔案上傳案例
檔案上傳分析圖解
- 【客戶端】輸入流,從硬碟讀取檔案資料到程式中。
- 【客戶端】輸出流,寫出檔案資料到服務端。
- 【服務端】輸入流,讀取檔案資料到服務端程式。
- 【服務端】輸出流,寫出檔案資料到伺服器硬碟中。
基本實現
服務端實現:
public class FileUpload_Server {
public static void main(String[] args) throws IOException {
System.out.println("伺服器 啟動..... ");
// 1. 建立服務端ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
// 2. 建立連線
Socket accept = serverSocket.accept();
// 3. 建立流物件
// 3.1 獲取輸入流,讀取檔案資料
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
// 3.2 建立輸出流,儲存到本地 .
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.jpg"));
// 4. 讀寫資料
byte[] b = new byte[1024 * 8];
int len;
while ((len = bis.read(b)) != -1) {
bos.write(b, 0, len);
}
//5. 關閉 資源
bos.close();
bis.close();
accept.close();
System.out.println("檔案上傳已儲存");
}
}
客戶端實現:
public class FileUPload_Client {
public static void main(String[] args) throws IOException {
// 1.建立流物件
// 1.1 建立輸入流,讀取本地檔案
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.jpg"));
// 1.2 建立輸出流,寫到服務端
Socket socket = new Socket("localhost", 6666);
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
//2.寫出資料.
byte[] b = new byte[1024 * 8 ];
int len ;
while (( len = bis.read(b))!=-1) {
bos.write(b, 0, len);
bos.flush();
}
System.out.println("檔案傳送完畢");
// 3.釋放資源
bos.close();
socket.close();
bis.close();
System.out.println("檔案上傳完畢 ");
}
}
檔案上傳優化分析
-
檔名稱寫死的問題
服務端,儲存檔案的名稱如果寫死,那麼最終導致伺服器硬碟,只會保留一個檔案,建議使用系統時間優化,保證檔名稱唯一,程式碼如下:
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis()+".jpg") // 檔名稱
BufferedOutputStream bos = new BufferedOutputStream(fis);
-
迴圈接收的問題
服務端,指儲存一個檔案就關閉了,之後的使用者無法再上傳,這是不符合實際的,使用迴圈改進,可以不斷的接收不同使用者的檔案,程式碼如下:
// 每次接收新的連線,建立一個Socket
while(true){
Socket accept = serverSocket.accept();
......
}
-
效率問題
服務端,在接收大檔案時,可能耗費幾秒鐘的時間,此時不能接收其他使用者上傳,所以,使用多執行緒技術優化,程式碼如下:
while(true){
Socket accept = serverSocket.accept();
// accept 交給子執行緒處理.
new Thread(() -> {
......
InputStream bis = accept.getInputStream();
......
}).start();
}
優化實現
public class FileUpload_Server {
public static void main(String[] args) throws IOException {
System.out.println("伺服器 啟動..... ");
// 1. 建立服務端ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
// 2. 迴圈接收,建立連線
while (true) {
Socket accept = serverSocket.accept();
/*
3. socket物件交給子執行緒處理,進行讀寫操作
Runnable介面中,只有一個run方法,使用lambda表示式簡化格式
*/
new Thread(() -> {
try (
//3.1 獲取輸入流物件
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
//3.2 建立輸出流物件, 儲存到本地 .
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fis);) {
// 3.3 讀寫資料
byte[] b = new byte[1024 * 8];
int len;
while ((len = bis.read(b)) != -1) {
bos.write(b, 0, len);
}
//4. 關閉 資源
bos.close();
bis.close();
accept.close();
System.out.println("檔案上傳已儲存");
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
資訊回寫分析圖解
前四步與基本檔案上傳一致.
- 【服務端】獲取輸出流,回寫資料。
- 【客戶端】獲取輸入流,解析回寫資料。
回寫實現
public class FileUpload_Server {
public static void main(String[] args) throws IOException {
System.out.println("伺服器 啟動..... ");
// 1. 建立服務端ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
// 2. 迴圈接收,建立連線
while (true) {
Socket accept = serverSocket.accept();
/*
3. socket物件交給子執行緒處理,進行讀寫操作
Runnable介面中,只有一個run方法,使用lambda表示式簡化格式
*/
new Thread(() -> {
try (
//3.1 獲取輸入流物件
BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
//3.2 建立輸出流物件, 儲存到本地 .
FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() + ".jpg");
BufferedOutputStream bos = new BufferedOutputStream(fis);
) {
// 3.3 讀寫資料
byte[] b = new byte[1024 * 8];
int len;
while ((len = bis.read(b)) != -1) {
bos.write(b, 0, len);
}
// 4.=======資訊回寫===========================
System.out.println("back ........");
OutputStream out = accept.getOutputStream();
out.write("上傳成功".getBytes());
out.close();
//================================
//5. 關閉 資源
bos.close();
bis.close();
accept.close();
System.out.println("檔案上傳已儲存");
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
客戶端實現:
public class FileUpload_Client {
public static void main(String[] args) throws IOException {
// 1.建立流物件
// 1.1 建立輸入流,讀取本地檔案
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.jpg"));
// 1.2 建立輸出流,寫到服務端
Socket socket = new Socket("localhost", 6666);
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
//2.寫出資料.
byte[] b = new byte[1024 * 8 ];
int len ;
while (( len = bis.read(b))!=-1) {
bos.write(b, 0, len);
}
// 關閉輸出流,通知服務端,寫出資料完畢
socket.shutdownOutput();
System.out.println("檔案傳送完畢");
// 3. =====解析回寫============
InputStream in = socket.getInputStream();
byte[] back = new byte[20];
in.read(back);
System.out.println(new String(back));
in.close();
// ============================
// 4.釋放資源
socket.close();
bis.close();
}
}
3.2 模擬B\S伺服器(擴充套件知識點)
模擬網站伺服器,使用瀏覽器訪問自己編寫的服務端程式,檢視網頁效果。
案例分析
-
準備頁面資料,web資料夾。
複製到我們Module中,比如複製到day08中
-
我們模擬伺服器端,ServerSocket類監聽埠,使用瀏覽器訪問
public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(8000); Socket socket = server.accept(); InputStream in = socket.getInputStream(); byte[] bytes = new byte[1024]; int len = in.read(bytes); System.out.println(new String(bytes,0,len)); socket.close(); server.close(); }
-
伺服器程式中位元組輸入流可以讀取到瀏覽器發來的請求資訊
GET/web/index.html HTTP/1.1是瀏覽器的請求訊息。/web/index.html為瀏覽器想要請求的伺服器端的資源,使用字串切割方式獲取到請求的資源。
//轉換流,讀取瀏覽器請求第一行
BufferedReader readWb = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
//取出請求資源的路徑
String[] strArr = requst.split(" ");
//去掉web前面的/
String path = strArr[1].substring(1);
System.out.println(path);
案例實現
服務端實現:
public class SerDemo {
public static void main(String[] args) throws IOException {
System.out.println("服務端 啟動 , 等待連線 .... ");
// 建立ServerSocket 物件
ServerSocket server = new ServerSocket(8888);
Socket socket = server.accept();
// 轉換流讀取瀏覽器的請求訊息
BufferedReader readWb = new
BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
// 取出請求資源的路徑
String[] strArr = requst.split(" ");
// 去掉web前面的/
String path = strArr[1].substring(1);
// 讀取客戶端請求的資原始檔
FileInputStream fis = new FileInputStream(path);
byte[] bytes= new byte[1024];
int len = 0 ;
// 位元組輸出流,將檔案寫會客戶端
OutputStream out = socket.getOutputStream();
// 寫入HTTP協議響應頭,固定寫法
out.write("HTTP/1.1 200 OK\r\n".getBytes());
out.write("Content-Type:text/html\r\n".getBytes());
// 必須要寫入空行,否則瀏覽器不解析
out.write("\r\n".getBytes());
while((len = fis.read(bytes))!=-1){
out.write(bytes,0,len);
}
fis.close();
out.close();
readWb.close();
socket.close();
server.close();
}
}
訪問效果
- 火狐
小貼士:不同的瀏覽器,核心不一樣,解析效果有可能不一樣。
發現瀏覽器中出現很多的叉子,說明瀏覽器沒有讀取到圖片資訊導致。
瀏覽器工作原理是遇到圖片會開啟一個執行緒進行單獨的訪問,因此在伺服器端加入執行緒技術。
public class ServerDemo {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8888);
while(true){
Socket socket = server.accept();
new Thread(new Web(socket)).start();
}
}
static class Web implements Runnable{
private Socket socket;
public Web(Socket socket){
this.socket=socket;
}
public void run() {
try{
//轉換流,讀取瀏覽器請求第一行
BufferedReader readWb = new
BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
//取出請求資源的路徑
String[] strArr = requst.split(" ");
System.out.println(Arrays.toString(strArr));
String path = strArr[1].substring(1);
System.out.println(path);
FileInputStream fis = new FileInputStream(path);
System.out.println(fis);
byte[] bytes= new byte[1024];
int len = 0 ;
//向瀏覽器 回寫資料
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK\r\n".getBytes());
out.write("Content-Type:text/html\r\n".getBytes());
out.write("\r\n".getBytes());
while((len = fis.read(bytes))!=-1){
out.write(bytes,0,len);
}
fis.close();
out.close();
readWb.close();
socket.close();
}catch(Exception ex){
}
}
}
}
訪問效果:
圖解: