柚子快報激活碼778899分享:10.JAVAEE之網(wǎng)絡(luò)編程
1.網(wǎng)絡(luò)編程
通過網(wǎng)絡(luò),讓兩個主機之間能夠進行通信 =>基于這樣的通信來完成一定的功能進行網(wǎng)絡(luò)編程的時候,需要操作系統(tǒng)給咱們提供一組 AP1, 通過這些 API才能完成編程(API 可以認為是 應(yīng)用層 和 傳輸層 之間交互的路徑)(API:Socket API相當于一個插座:通過這一套 Socket AP| 可以完成不同主機之間,不同系統(tǒng)之間的網(wǎng)絡(luò)通信)傳輸層,提供的網(wǎng)絡(luò)協(xié)議,主要是兩個:TCP UDP這倆協(xié)議的特性(工作原理) 差異很大.導(dǎo)致,使用這兩種協(xié)議進行網(wǎng)絡(luò)編程,也存在一定差別系統(tǒng)就分別提供了兩套 APITCP 和 UDP 的區(qū)別.(后面網(wǎng)絡(luò)原理章節(jié), 學(xué)習的重點)
? ? ? ?1.TCP 是有連接的, UDP 是無連接的
? ? ? ?2.TCP 是可靠傳輸?shù)?UDP 是不可靠傳輸?shù)?/p>
? ? ? ?3.TCP 是面向字節(jié)流的,UDP 是面向數(shù)據(jù)報
? ? ? ?4.TCP 和 UDP 都是全雙工的
?1.TCP 是有連接的, UDP 是無連接的 (連接 是 抽象 的概念) 計算機中,這種 抽象 的連接是很常見的,此處的連接本質(zhì)上就是建立連接的雙方,各自保存對方的信息兩臺計算機建立連接,就是雙方彼此保存了對方的關(guān)鍵信息~~ TCP 要想通信, 就需要先建立連接 (剛才說的, 保存對方信息),做完之后,才能后續(xù)通信(如果 A 想和 B 建立連接, 但是 B 拒絕了! 通信就無法完成!!!)
UDP 想要通信,就直接發(fā)送數(shù)據(jù)即可~~不需要征得對方的同意,UDP 自身也不會保存對方的信息(UDP 不知道,但是寫程序的人得知道.UDP 自己不保存,但是你調(diào)用 UDP 的 socket api的時候要把對方的位置啥的給傳過去)
2.TCP 是可靠傳輸?shù)?UDP 是不可靠傳輸?shù)?網(wǎng)絡(luò)上進行通信, A ->B 發(fā)送一個消息,這個消息是不可能做到 100% 送達的!!?
可靠傳輸,退而求其次. A ->B 發(fā)消息,消息是不是到達 B 這一方,A 自己能感知到.(A 心里有數(shù))進一步的,就可以在發(fā)送失敗的時候采取一定的措施(嘗試重傳之類的)
TCP 就內(nèi)置了可靠傳輸機制;UDP 就沒有內(nèi)置可靠傳輸
【tips】可靠傳輸,聽起來挺美好的呀, 為啥不讓 UDP 也搞個可靠傳輸呢??
想要可靠傳輸,你就是要付出代價的(需要去交換) 可靠傳輸要付出什么代價? 1)機制更復(fù)雜 2)傳輸效率會降低
3.TCP 是面向字節(jié)流的,UDP 是面向數(shù)據(jù)報 此處說的 字節(jié)流 和 文件 操作這里的 字節(jié)流 是一個意思!!! TCP 也是和文件操作一樣,以字節(jié)為單位來進行傳輸. UDP 則是按照數(shù)據(jù)報為單位,來進行傳輸?shù)?UDP 數(shù)據(jù)報是有嚴格的格式的?
網(wǎng)絡(luò)通信數(shù)據(jù)的基本單位,涉及到多種說法~~ 1.數(shù)據(jù)報(Datagram) 2.數(shù)據(jù)包(Packet)
3.數(shù)據(jù)幀(Frame) 4.數(shù)據(jù)段 (Segment)?
4. TCP 和 UDP 都是全雙工的 一個信道,允許雙向通信, 就是全雙工 一個信道,只能單向通信,就是半雙工 代碼中使用一個 Socket 對象, 就可以發(fā)送數(shù)據(jù)也能接受數(shù)據(jù)~~?
2.UDP 的 socket api 如何使用
Datagramsocket?
Datagrampacket?
【回顯服務(wù)器:(echo server)】
寫一個簡單的 UDP 的客戶端/服務(wù)器 通信的程序. 這個程序沒有啥業(yè)務(wù)邏輯,只是單純的調(diào)用 socket api. 讓客戶端給服務(wù)器發(fā)送一個請求,請求就是一個從控制臺輸入的字符串. 服務(wù)器收到字符串之后,也就會把這個字符串原封不動的返回給客戶端,客戶端再顯示出來.?
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
// 創(chuàng)建一個 DatagramSocket 對象. 后續(xù)操作網(wǎng)卡的基礎(chǔ).
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
// 這么寫就是手動指定端口
socket = new DatagramSocket(port);
// 這么寫就是讓系統(tǒng)自動分配端口
// socket = new DatagramSocket();
}
public void start() throws IOException {
// 通過這個方法來啟動服務(wù)器.
System.out.println("服務(wù)器啟動!");
// 一個服務(wù)器程序中, 經(jīng)常能看到 while true 這樣的代碼.
while (true) {
// 1. 讀取請求并解析.
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
// 當前完成 receive 之后, 數(shù)據(jù)是以 二進制 的形式存儲到 DatagramPacket 中了.
// 要想能夠把這里的數(shù)據(jù)給顯示出來, 還需要把這個二進制數(shù)據(jù)給轉(zhuǎn)成字符串.
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根據(jù)請求計算響應(yīng)(一般的服務(wù)器都會經(jīng)歷的過程)
// 由于此處是回顯服務(wù)器, 請求是啥樣, 響應(yīng)就是啥樣.
String response = process(request);
// 3. 把響應(yīng)寫回到客戶端.
// 搞一個響應(yīng)對象, DatagramPacket
// 往 DatagramPacket 里構(gòu)造剛才的數(shù)據(jù), 再通過 send 返回.
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
// 4. 打印一個日志, 把這次數(shù)據(jù)交互的詳情打印出來.
System.out.printf("[%s:%d] req=%s, resp=%s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp = "";
private int serverPort = 0;
public UdpEchoClient(String ip, int port) throws SocketException {
// 創(chuàng)建這個對象, 不能手動指定端口.
socket = new DatagramSocket();
// 由于 UDP 自身不會持有對端的信息. 就需要在應(yīng)用程序里, 把對端的情況給記錄下來.
// 這里咱們主要記錄對端的 ip 和 端口 .
serverIp = ip;
serverPort = port;
}
public void start() throws IOException {
System.out.println("客戶端啟動!");
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 從控制臺讀取數(shù)據(jù), 作為請求
System.out.print("-> ");
String request = scanner.next();
// 2. 把請求內(nèi)容構(gòu)造成 DatagramPacket 對象, 發(fā)給服務(wù)器.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
// 3. 嘗試讀取服務(wù)器返回的響應(yīng)了.
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 4. 把響應(yīng), 轉(zhuǎn)換成字符串, 并顯示出來.
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
// UdpEchoClient client = new UdpEchoClient("42.192.83.143", 9090);
client.start();
}
}
【客戶端】
對于服務(wù)器來說,也就需要把端口號給明確下來了~~客戶端的端口號是不需要確定的.交給系統(tǒng)進行分配即可,如果你手動指定確定的端口,就可能和別人的程序的端口號沖突【tips】服務(wù)器這邊手動指定端口,就不會出現(xiàn)沖突嘛?? 為啥客戶端在意這個沖突,而服務(wù)器不在意呢?? 服務(wù)器是在程序猿手里的,一個服務(wù)器上都有哪些程序, 都使用哪些端口,程序猿都是可控的!!程序猿寫代碼的時候,就可以指定一個空閑的端口,給當前的服務(wù)器使用即可 但是客戶端就不可控,客戶端是在用戶的電腦上;一方面,用戶千千萬~~ 每個用戶電腦上裝的程序都不一樣,占用的端口也不一樣;交給系統(tǒng)分配比較穩(wěn)妥.系統(tǒng)能保證肯定分配一個空閑的端口
服務(wù)器一旦啟動,就會立即執(zhí)行到這里的 receive 方法此時,客戶端的請求可能還沒來呢~~ 這種情況也沒關(guān)系.receive 就會直接阻塞, 就會一直阻塞到真正客戶端把請求發(fā)過來為止,(類似于阻塞隊列)
【question】
//根據(jù)請求計算響應(yīng)(核心步驟)
這個步驟是一個服務(wù)器程序,最核心的步驟!!! 咱們當前是 echo server 不涉及到這些流程,也不必考慮響應(yīng)怎么計算,只要請求過來,就把請求當做響應(yīng)
【question】
【question】上述寫的代碼中,為啥沒寫 close?? socket 也是文件,不關(guān)閉不就出問題了,不就文件資源泄露了么,為啥這里咱們可以不寫 close?為啥不寫 close 也不會出現(xiàn)文件資源泄露??
private DatagramSocket socket = null; 這個 socket 在整個程序運行過程中都是需要使用的(不能提前關(guān)閉)當 socket 不需要使用的時候, 意味著程序就要結(jié)束了
進程結(jié)束,此時隨之文件描述符表就會銷毀了(PCB 都銷毀了).談何泄露?? 隨著銷毀的過程,被系統(tǒng)自動回收了~~
啥時候才會出現(xiàn)泄露?代碼中頻繁的打開文件,但是不關(guān)閉在一個進程的運行過程中,不斷積累打開的文件,逐漸消耗掉文件描述符表里的內(nèi)容最終就消耗殆盡了 但是如果進程的生命周期很短,打開一下沒多久就關(guān)閉了.談不上泄露文件資源泄露這樣的問題,在服務(wù)器這邊是比較 嚴重的, 在客戶端這邊一般來說影響不大.
?【服務(wù)器】
【交互】
1.服務(wù)器先啟動.服務(wù)器啟動之后,就會進入循環(huán),執(zhí)行到 receive 這里并阻塞 (此時還沒有客戶端過來呢) 2.客戶端開始啟動,也會先進入 while 循環(huán),執(zhí)行 scanner.next.并且也在這里阻塞當用戶在控制臺輸入字符串之后,next 就會返回,從而構(gòu)造請求數(shù)據(jù)并發(fā)送出來~~
3.客戶端發(fā)送出數(shù)據(jù)之后, 服務(wù)器: 就會從 receive 中返回,進一步的執(zhí)行解析請求為字符串,執(zhí)行 process 操作,執(zhí)行 send 操作 客戶端: 繼續(xù)往下執(zhí)行,執(zhí)行到 receive,等待服務(wù)器的響應(yīng)
4.客戶端收到從服務(wù)器返回的數(shù)據(jù)之后,就會從 receive 中返回執(zhí)行這里的打印操作,也就把響應(yīng)給顯示出來了 5.服務(wù)器這邊完成一次循環(huán)之后, 又執(zhí)行到 receive 這里,客戶端這邊完成一次循環(huán)之后,又執(zhí)行到 scanner.next 這里雙雙進入阻塞
?【翻譯服務(wù)器】
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer {
private Map
public UdpDictServer(int port) throws SocketException {
super(port);
// 此處可以往這個表里插入幾千幾萬個這樣的英文單詞.
dict.put("dog", "小狗");
dict.put("cat", "小貓");
dict.put("pig", "小豬");
}
// 重寫 process 方法, 在重寫的方法中完成翻譯的過程.
// 翻譯本質(zhì)上就是 "查表"
@Override
public String process(String request) {
return dict.getOrDefault(request, "該詞在詞典中不存在!");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
上述重寫 process 方法,就可以在子類中組織你想要的"業(yè)務(wù)邏輯",(你要寫代碼解決一些實際的問題)
3.TCP 的 socket api 如何使用?
TCP 的 socket api 和 UDP 的 socket api 差異又很大~,
但是和前面講的 文件操作,有密切聯(lián)系的
兩個關(guān)鍵的類
1.ServerSocket(給服務(wù)器使用的類,使用這個類來綁定端口號)
2.Socket(既會給服務(wù)器用,又會給客戶端用)
這倆類都是用來表示 socket 文件的,(抽象了網(wǎng)卡這樣的硬件設(shè)備)
TCP 是字節(jié)流的.傳輸?shù)幕締挝?是 byte
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// 需要在創(chuàng)建 Socket 的同時, 和服務(wù)器 "建立連接", 此時就得告訴 Socket 服務(wù)器在哪里~~
// 具體建立連接的細節(jié), 不需要咱們代碼手動干預(yù). 是內(nèi)核自動負責的.
// 當我們 new 這個對象的時候, 操作系統(tǒng)內(nèi)核, 就開始進行 三次握手 具體細節(jié), 完成建立連接的過程了.
socket = new Socket(serverIp, serverPort);
}
public void start() {
// tcp 的客戶端行為和 udp 的客戶端差不多.
// 都是:
// 3. 從服務(wù)器讀取響應(yīng).
// 4. 把響應(yīng)顯示到界面上.
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
PrintWriter writer = new PrintWriter(outputStream);
Scanner scannerNetwork = new Scanner(inputStream);
while (true) {
// 1. 從控制臺讀取用戶輸入的內(nèi)容
System.out.print("-> ");
String request = scanner.next();
// 2. 把字符串作為請求, 發(fā)送給服務(wù)器
// 這里使用 println, 是為了讓請求后面帶上換行.
// 也就是和服務(wù)器讀取請求, scanner.next 呼應(yīng)
writer.println(request);
writer.flush();
// 3. 讀取服務(wù)器返回的響應(yīng).
String response = scannerNetwork.next();
// 4. 在界面上顯示內(nèi)容了.
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服務(wù)器啟動!");
ExecutorService service = Executors.newCachedThreadPool();
while (true) {
// 通過 accept 方法, 把內(nèi)核中已經(jīng)建立好的連接拿到應(yīng)用程序中.
// 建立連接的細節(jié)流程都是內(nèi)核自動完成的. 應(yīng)用程序只需要 "撿現(xiàn)成" 的.
Socket clientSocket = serverSocket.accept();
// 此處不應(yīng)該直接調(diào)用 processConnection, 會導(dǎo)致服務(wù)器不能處理多個客戶端.
// 創(chuàng)建新的線程來調(diào)用更合理的做法.
// 這種做法可行, 不夠好
// Thread t = new Thread(() -> {
// processConnection(clientSocket);
// });
// t.start();
// 更好一點的辦法, 是使用線程池.
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
// 通過這個方法, 來處理當前的連接.
public void processConnection(Socket clientSocket) {
// 進入方法, 先打印一個日志, 表示當前有客戶端連上了.
System.out.printf("[%s:%d] 客戶端上線!\n", clientSocket.getInetAddress(), clientSocket.getPort());
// 接下來進行數(shù)據(jù)的交互.
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 使用 try ( ) 方式, 避免后續(xù)用完了流對象, 忘記關(guān)閉.
// 由于客戶端發(fā)來的數(shù)據(jù), 可能是 "多條數(shù)據(jù)", 針對多條數(shù)據(jù), 就循環(huán)的處理.
while (true) {
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
// 連接斷開了. 此時循環(huán)就應(yīng)該結(jié)束
System.out.printf("[%s:%d] 客戶端下線!\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
// 1. 讀取請求并解析. 此處就以 next 來作為讀取請求的方式. next 的規(guī)則是, 讀到 "空白符" 就返回.
String request = scanner.next();
// 2. 根據(jù)請求, 計算響應(yīng).
String response = process(request);
// 3. 把響應(yīng)寫回到客戶端.
// 可以把 String 轉(zhuǎn)成字節(jié)數(shù)組, 寫入到 OutputStream
// 也可以使用 PrintWriter 把 OutputStream 包裹一下, 來寫入字符串.
PrintWriter printWriter = new PrintWriter(outputStream);
// 此處的 println 不是打印到控制臺了, 而是寫入到 outputStream 對應(yīng)的流對象中, 也就是寫入到 clientSocket 里面.
// 自然這個數(shù)據(jù)也就通過網(wǎng)絡(luò)發(fā)送出去了. (發(fā)給當前這個連接的另外一端)
// 此處使用 println 帶有 \n 也是為了后續(xù) 客戶端這邊 可以使用 scanner.next 來讀取數(shù)據(jù).
printWriter.println(response);
// 此處還要記得有個操作, 刷新緩沖區(qū). 如果沒有刷新操作, 可能數(shù)據(jù)仍然是在內(nèi)存中, 沒有被寫入網(wǎng)卡.
printWriter.flush();
// 4. 打印一下這次請求交互過程的內(nèi)容
System.out.printf("[%s:%d] req=%s, resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 在這個地方, 進行 clientSocket 的關(guān)閉.
// processConnection 就是在處理一個連接. 這個方法執(zhí)行完畢, 這個連接也就處理完了.
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
// 此處也是寫的回顯服務(wù)器. 響應(yīng)和請求是一樣的.
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
【服務(wù)器】
內(nèi)核中有一個“隊列”(可以視為阻塞隊列)
如果有客戶端,和服務(wù)器建立連接,這個時候服務(wù)器的應(yīng)用程序是不需要做出任何操作(也沒有任何感知的),內(nèi)核直接就完成了連接建立的流程(三次握手).
完成流程之后,就會在內(nèi)核的隊列中(這個隊列是每個 serverSocket 都有一個這樣的隊列)。
排隊應(yīng)用程序要想和這個客戶端進行通信,就需要通過一個 accept 方法把內(nèi)核隊列里已經(jīng)建立好的連接對象,拿到應(yīng)用程序中。
【question】
前面寫過的 DatagramSocket, ServerSocket 都沒寫 close, 但是我們說這個東西都沒關(guān)系但是 clientSocket 如果不關(guān)閉,就會真的泄露了!!! DatagramSocket 和 ServerSocket,都是在程序中,只有這么一個對象.申明周期, 都是貫穿整個程序的.
而ClientSocket 則是在循環(huán)中,每次有一個新的客戶端來建立連接,都會創(chuàng)建出新的clientSocket
每次執(zhí)行這個,都會創(chuàng)建新的 clientSocket,并且這個 socket 最多使用到 該客戶端退出(斷開連接) 此時,如果有很多客戶端都來建立連接~~此時,就意味著每個連接都會創(chuàng)建 clientSocket.當連接斷開clientSocket 就失去作用了,但是如果沒有手動 close此時這個 socket 對象就會占據(jù)著文件描述符表的位置
【客戶端】
【question】出現(xiàn)一個bug
當前啟動兩個客戶端,同時連接服務(wù)器. 其中一個客戶端(先啟動的客戶端) 一切正常. 另一個客戶端 (后啟動的客戶端)則沒法和服務(wù)器進行任何交互,(服務(wù)器不會提示"建立連接”,也不會針對 請求 做出任何響應(yīng))
上述bug和代碼結(jié)構(gòu)密切相關(guān)
確實如剛才推理的現(xiàn)象一樣,第一個客戶端結(jié)束的時候,就從 processConnection 返回了就可以執(zhí)行到第二次 accept 了,也就可以處理第二個客戶端了~~ 很明顯,如果啟動第三個客戶端,第三個客戶端也會僵硬住,又會需要第二個客戶端結(jié)束才能活過來...
如何解決上述問題?讓一個服務(wù)器可以同時接待多個客戶端呢??
關(guān)鍵就是,在處理第一個客戶端的請求的過程中,要讓代碼能夠快速的第二次執(zhí)行到 accept ~~~【多線程】
上述這里的關(guān)鍵,就是讓這兩個循環(huán)能夠"并發(fā)"執(zhí)行. 各自執(zhí)行各自的,不會因為進入一個循環(huán)影響到另一個~~
【剛才出現(xiàn)這個問題的關(guān)鍵在于兩重循環(huán)在一個線程里進入第二重循環(huán)的時候,無法繼續(xù)執(zhí)行第一個循環(huán). Udp 版本的服務(wù)器,當時是只有一個循環(huán),不存在類似的問題~~(前面部署到云服務(wù)器的時候)】
柚子快報激活碼778899分享:10.JAVAEE之網(wǎng)絡(luò)編程
相關(guān)閱讀
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。