柚子快報邀請碼778899分享:【JavaEE初階】網(wǎng)絡編程
柚子快報邀請碼778899分享:【JavaEE初階】網(wǎng)絡編程
??
歡迎關注個人主頁:逸狼
創(chuàng)造不易,可以點點贊嗎~
如有錯誤,歡迎指出~
?絡編程,指?絡上的主機,通過不同的進程,以編程的?式實現(xiàn)?絡通信(或稱為?絡數(shù)據(jù)傳 輸)。
socket api
Socket套接字,是由系統(tǒng)提供?于?絡通信的技術,是基于TCP/IP協(xié)議的?絡通信的基本操作單元。 基于Socket套接字的?絡程序開發(fā)就是?絡編程。
操作系統(tǒng)給應用程序(傳輸層給應用層)提供的api,就叫做socket api(這里學習Java版本的),有兩組不同的api,分別為UDP和TCP兩套版本
UDP 無連接 不可靠傳輸 面向數(shù)據(jù)報 全雙工TCP 有連接 可靠傳輸 面向字節(jié)流 全雙工
有/無連接: 通信雙方若保存了通信對端的信息(IP和端口),就是有連接,不保存就是無連接可靠/不可靠傳輸:盡可能考慮能夠到達對方就是可靠傳輸,完全不考慮就是不可靠傳輸.這個在代碼中沒辦法直接體現(xiàn),它是在內核中實現(xiàn)好的功能
TCP內置了一些機制(感知到對方是否收到; 重傳機制,在對方沒收到時進行重試)可以保證可靠傳輸( 但是可靠傳輸要付出代價 ,TCP協(xié)議設計要比UDP復雜很多,也會損失一些傳輸數(shù)據(jù)的效率)UDP沒有可靠性機制?面向字節(jié)流/數(shù)據(jù)報 :參數(shù)單位是字節(jié)的就是面向字節(jié)流,單位是數(shù)據(jù)包的就是面向數(shù)據(jù)報?
TCP是面向字節(jié)流的,傳輸過程和文件流/水流是一樣的特點UDP是面向數(shù)據(jù)報的,傳輸數(shù)據(jù)的基本單位就是UDP數(shù)據(jù)報,一次發(fā)送/接收必須是完整的數(shù)據(jù)報全/半雙工:一個通信鏈路,可以發(fā)送數(shù)據(jù),也可以接收數(shù)據(jù)就是全雙工; 只能發(fā)送或只能接收就是半雙工? 這里寫的代碼都是全雙工的,不考慮半雙工
UDP版本socket api
通過代碼不好直接操作網(wǎng)卡(網(wǎng)卡有很多不同的型號,之間提供的api都會有差別),操作系統(tǒng)就把網(wǎng)卡概念封裝成socket,應用程序員就不必關注硬件的差異和細節(jié),統(tǒng)一操作socket對象就能間接操作網(wǎng)卡;? socket 可以認為是操作系統(tǒng)中廣義文件下里的一種文件類型,這樣的文件就是網(wǎng)卡這種硬件設備的抽象表現(xiàn)形式
代碼部分需要實現(xiàn)兩個程序
socket相當于網(wǎng)卡的遙控器,網(wǎng)絡編程必須要操作網(wǎng)卡,就需要用到socket對象DatagramSocket
下面通過寫一個"回顯服務器(echo server)"(客戶端發(fā)的請求和服務器返回的響應一致)代碼示例加以理解
UDP服務器
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
public class UdpEchoServer {
private DatagramSocket socket=null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
//通過start 啟動服務器的核心流程
public void start() throws IOException {
System.out.println("服務器啟動!");
while(true){
//通過死循環(huán)不停的處理客戶端的請求
//1.讀取客戶端的請求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//上述收到的數(shù)據(jù),是二進制byte[]的形式體現(xiàn)的,后續(xù)代碼如果要進行打印之類的操作 需要將其轉成字符串
//構造string字符串 獲取字節(jié)報的數(shù)據(jù),從數(shù)組的0位置開始構造string,長度為字節(jié)報的長度
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//2.根據(jù)請求計算響應,由于此處是回顯服務器,響應就是請求
String response = process(request);
//3.把響應寫回到客戶端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length
,requestPacket.getSocketAddress());//UDP是無連接的,所以要手動將客戶端的請求的IP和端口號取出并包裝到responsePacket里
socket.send(responsePacket);
//4.打印日志
System.out.printf("[%s:%d] req=%s ,resp=%s\n",requestPacket.getAddress(),requestPacket.getPort()
,request,response);//獲取IP地址和端口號,請求和響應
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server =new UdpEchoServer(9090);
server.start();
}
}
?
對于一個系統(tǒng)來說,同一時刻,同一個協(xié)議下,一個端口號,只能被一個進程綁定(端口號就是用來區(qū)分進程的,如果有多個進程嘗試綁定一個端口號,后來的進程就會綁定失敗),但是一個進程可以同時綁定多個端口號(通過創(chuàng)建多個socket對象來實現(xiàn))? ?
比如:9090端口在udp下被一個進程綁定了,還可以在TCP下被另一個進程綁定
receive
?
DatagramSocket 這個對象中,不持有對方(客戶端) 的 ip 和端口的. 所以進行 send 的時候,就需要在 send 的數(shù)據(jù)包里,把要發(fā)給誰這樣的信息,寫進去,才能夠正確的把數(shù)據(jù)進行返回
socket在使用完之后需要關閉,此處代碼中,socket生命周期整個進程一樣長,就算沒有close,進程關閉時也會釋放文件描述符表里的所有內容,相當于close了
UDP客戶端
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
public UdpEchoClient(String serverIP ,int serverPort) throws SocketException {
socket =new DatagramSocket();
this.serverIP=serverIP;
this.serverPort =serverPort;
}
public void start() throws IOException {
System.out.println("啟動客戶端!");
Scanner scanner= new Scanner(System.in);
while(true){
//1.從控制臺讀取用戶的輸入
System.out.print("-> ");
String request = scanner.next();
//2.構造出一個UDP請求,發(fā)送給服務器
DatagramPacket requestPacket= new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(this.serverIP),this.serverPort);//這里要將IP字符串轉換成int類型
socket.send(requestPacket);
//3.從服務器中讀取響應
DatagramPacket responsePacket= new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response= new String(responsePacket.getData(),0,requestPacket.getLength());
//4.把響應打印到控制臺上
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
//127.0.0.1稱為環(huán)回IP,代表本機,如果服務器和客戶端在同一個主機上,就使用這個IP
UdpEchoClient udpEchoClient =new UdpEchoClient("127.0.0.1",9090);
udpEchoClient.start();
}
}
服務器這邊創(chuàng)建socket時一定要指定端口號,因為客戶端是通過端口號來找到服務器的,且客戶端是主動發(fā)起的一方.客戶端這邊創(chuàng)建socket時就最好不要指定端口號(不指定不代表沒有,客戶端的端口號是系統(tǒng)自動分配的一個端口,讓系統(tǒng)自動分配一個端口,就能確保分配的是一個無人使用的端口).如果在客戶端指定了端口號,由于客戶端是在用戶的電腦運行的,萬一代碼指定的端口和用戶電腦上運行的其他程序的端口沖突,就會產生bug
服務器和客戶端代碼執(zhí)行流程
TCP版本的socket api
TCP socket api核心類有兩個
ServerSocket? 專門給服務器使用的socket對象Socket? ? ? ? ? ? ?給客戶端使用,也可以給服務器使用
?TCP是有連接的,建立連接的過程類似于"打電話",ServerSocket類里的accept相當于"接電話"("客戶端打電話,服務器接電話")
下面通過編寫TCP回顯服務器來舉例展示
TCP服務器
package net;
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;
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("服務器啟動!");
while(true){
Socket clientSocket = serverSocket.accept();//serverSocket用于幫助clientSocket建立連接
//使用多線程來實現(xiàn)多個客戶端連接服務器
Thread t= new Thread(()->{
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
t.start();
}
}
//針對一個連接,提供處理邏輯
private void processConnection(Socket clientSocket) throws IOException {
//先打印一下客戶端的信息
System.out.printf("[%s:%d] 客戶端上線!",clientSocket.getInetAddress(),clientSocket.getPort());
//獲取到socket中持有的流對象
try(InputStream inputStream = clientSocket.getInputStream();//TCP是全雙工的通信,一個socket對象既可以讀也可以寫
OutputStream outputStream = clientSocket.getOutputStream()){
//使用Scanner包裝一下inputStream ,就可以更方便的讀取這里的請求數(shù)據(jù)了
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true){
//1.讀取請求并解析
if(!scanner.hasNext()){
//如果scanner無法讀取出數(shù)據(jù),說明客戶端關閉了連接,導致服務器讀到了"末尾"
break;
}
String request = scanner.next();
//2.根據(jù)請求計算響應
String response = process(request);
//3.把響應寫回客戶端
//此處可以按照字節(jié)數(shù)組直接寫,也可以有另一種寫法
// outputStream.write(response.getBytes());
printWriter.println(response);
printWriter.flush();//刷新緩沖區(qū)
//4.打印日志
System.out.printf("[%s:%d] req=%s,resp=%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
}
}catch(IOException e){
e.printStackTrace();
}finally{//只要是方法執(zhí)行完畢了,就會執(zhí)行close代碼
//連接失敗打印日志
System.out.printf("[%s:%d] 客戶端下線!\n",clientSocket.getInetAddress(),clientSocket.getPort());
clientSocket.close();//客戶端socket需要手動關閉
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
accept?
約定換行符?
TCP客戶端
package net;
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 {
//這里寫入ip和端口號之后意味著new好對象之后 和服務器的連接就建立完成了
//如果建立連接失敗了,直接就會拋出異常
socket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客戶端啟動!");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
Scanner scannerIn = new Scanner(System.in);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true){
//1.從控制臺讀取數(shù)據(jù)
System.out.print("->");
String request = scannerIn.next();
//2.把請求發(fā)送給服務器
printWriter.println(request);
printWriter.flush();//刷新緩沖區(qū)
//3.從服務器讀取響應
if(!scanner.hasNext()){
break;
}
String response = scanner.next();
//4.打印響應結果
System.out.println(response);
}
}catch(Exception e){
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
容易產生bug的點?
刷新緩沖區(qū)
PrintWrite這樣的類以及很多IO流中的類都是"自帶緩沖區(qū)的",引入緩沖區(qū)后,進行寫數(shù)據(jù)操作不會立即觸發(fā)IO,而是放到內存緩沖區(qū)中,等到緩沖區(qū)贊了一波,在進行統(tǒng)一發(fā)送;
所以當要發(fā)送的數(shù)據(jù)較少時 ,沒辦法攢夠數(shù)據(jù)發(fā)送,停在了緩沖區(qū),所以要引入PrintWrite類里的flush操作 來主動 "刷新緩沖區(qū)"
?clientSocket要自動關閉close
像ServerSocket,DatagramSocket他們的生命周期都是跟隨整個進程的(進程結束,會自動關閉),而服務器代碼中clientSocket是"連接級別"的數(shù)據(jù),隨著客戶端斷開連接,這個socket就不再使用了(即使是同一個客戶端,斷開之后,重新連接,也和舊的socket不是同一個),這樣的socket應該主動關閉以防止 文件資源泄漏.
多個客戶端連接同一個服務器
此處單線程下無法處理多個客戶端本質是服務器代碼里 雙重while循環(huán)導致的,進入了里層的while循環(huán)時,外層while無法執(zhí)行了?,所以這里要比雙層while循環(huán)改成一異while,分別執(zhí)行-->使用多線程解決.主線程用于accept來獲取多個連接 ,每個連接都可以啟動一個新的線程
雖然創(chuàng)建線程比創(chuàng)建進程 更輕量,但是也架不住短時間內 ,創(chuàng)建銷毀大量的線程?
使用線程池 可以解決短時間客戶端涌入,并且每個客戶端請求都很快 的問題
System.out.println("服務器啟動!");
ExecutorService service = Executors.newCachedThreadPool();
while(true){
Socket clientSocket = serverSocket.accept();//serverSocket用于幫助clientSocket建立連接
//使用多線程來實現(xiàn)多個客戶端連接服務器
// Thread t= new Thread(()->{
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// t.start();
//使用線程池
service.submit(()->{
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
長短連接
長連接: 客戶端連上服務器后,一個連接中會多次發(fā)出請求,接收多個響應(當前回顯服務器就屬于這種模式)短連接: 客戶端連上服務器后,一個連接只能發(fā)一個請求,接受一個響應,然后就斷開連接了(可能會頻繁和服務器建立/斷開連接)
柚子快報邀請碼778899分享:【JavaEE初階】網(wǎng)絡編程
文章來源
本文內容根據(jù)網(wǎng)絡資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉載請注明,如有侵權,聯(lián)系刪除。