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