13-網路程式設計

EUNEIR發表於2024-03-13

在網路通訊協議下,不同計算機上執行的程式進行資料的傳輸;封裝在java.net包下

網路程式設計三要素:

  • IP:裝置在網路中的地址,是唯一的標識
  • 埠號:應用程式在裝置中的唯一標識
  • 協議:資料在網路中傳輸的規則 TCP、UDP、http、https、ftp

IP:Internet Protocol 網際網路協議地址,常見的IP分為IPV4、IPV6

  1. IPV4 Internet Protocol version 4,網際網路通訊協議第四版

採用32為地址長度,分四組;採用點分十進位制,每八個為一組轉為十進位制(0-255)

IPV4只有不到43億個IP地址

  1. IPV6 Internet Protocol version 6

採取128位地址長度,分成8組,最多有2^128個IP,採用冒分十六進位制,例如2001:0DB8:0000:0023:0008:0800:200C:417A

可以省略前面的0 : 2001:DB8:0:23:8:800:200C:417A

特殊情況:FF01:0:0:0:0:0:0:1101 採用0位壓縮表示法 FF01::1101

地址分類

分為公網IP和私網IP

192.168開頭的就是私有地址,範圍即 192.168.0.0 - 192.168.255.255,專門位組織機構內部使用

特殊IP : 127.0.0.1 ,也即localhost,是本地迴環地址,永遠只會尋找當前本機

假設192.168.1.100是我電腦的IP,這個IP與127.0.0.1是不一樣的:

  • 192.168.1.100傳送資料:資料經過路由器,路由器再傳送到本機
  • 127.0.0.1(localhost)傳送資料:資料傳送到網路卡時就直接發回自己,發不到路由器

三要素

InetAddress

This class represents an Internet Protocol (IP) address,該類表示IP的物件

classDiagram InetAddress <|-- Inet4Address InetAddress <|-- Inet6Address

該類沒有對外提供構造方法,需要透過靜態方法獲取:

image.png

static InetAddress getByName(String host):確定主機名稱的IP地址,主機名稱可以是機器名稱,也可以是IP地址

主機名稱就是給自己電腦起的名字,

透過InetAddress物件就可以獲取電腦名稱或IP地址

InetAddress Iip = InetAddress.getByName("127.0.0.1");  
System.out.println("Iip.getHostAddress() = " + Iip.getHostAddress()); //127.0.0.1  
System.out.println("Iip.getHostName() = " + Iip.getHostName()); //localhost.sangfor.com.cn

getHostName() 可能因為網路原因 或者區域網沒有這臺電腦 是以IP形式體現的

埠號

應用程式在裝置中的唯一標識

埠號:0-65535

其中0-1023之間的埠號用於一些知名的網路服務或者應用,自己可以使用的是1024以上的

協議

計算機網路中,連線和通訊的規則被稱為網路通訊協議

應用層:HTTP、FTP、Telnet、DNS

傳輸層:TCP、UDP

網路層:IP、ICMP、ARP

UDP:

  • 使用者資料包協議,面向無連線的協議
  • 速度快,有大小限制,一次最多傳送64K,資料不安全,易丟失資料

TCP:

  • 傳輸控制協議,面向連線的協議
  • 速度慢,沒有大小限制,資料安全

URL

java.net.URL是統一資源定位符,對可以從網際網路上得到的資源的位置和訪問方法的一種簡潔的表示,是網際網路上標準資源的地址。網際網路上的每個檔案都有一個唯一的URL,它包含的資訊指出檔案的位置以及瀏覽器應該怎麼處理它。 URL由4部分組成:協議、存放資源的主機域名、資原始檔名和埠號。如果未指定該埠號,則使用協議預設的埠。例如HTTP協議的預設埠為80。在瀏覽器中訪問網頁時,位址列顯示的地址就是URL。 URL標準格式為:<協議>://<域名或IP>:<埠>/<路徑> 。其中,<協議>://<域名或IP>是必需的,<埠>/<路徑>有時可省略。如:https://www.baidu.com

為了方便程式設計師程式設計,JDK中提供了URL類,該類的全名是java.net.URL,該類封裝了大量複雜的涉及從遠端站點獲取資訊的細節,可以使用它的各種方法來對URL物件進行分割、合併等處理。

URL url = new URL("http://www.jd.com:8080/java/index.html?name=admin#tip");  
System.out.println("協議:" + url.getProtocol()); //協議:http  
System.out.println("域名:" + url.getHost()); //域名:www.jd.com  
System.out.println("獲取該URL協議預設關聯的埠:" + url.getDefaultPort()); //獲取該URL協議預設關聯的埠:80  
System.out.println("埠:" + url.getPort()); //埠:8080  
System.out.println("獲取資源路徑(不包含請求引數):" + url.getPath()); //獲取資源路徑(不包含請求引數):/java/index.html  
System.out.println("獲取資源路徑(包含請求引數):" + url.getFile()); //獲取資源路徑(包含請求引數):/java/index.html?name=admin  
System.out.println("獲取引數:" + url.getQuery()); //獲取引數:name=admin  
System.out.println("錨點:" + url.getRef()); //錨點:tip

使用java.net.URL類的InputStream openStream() 方法,還可以開啟到此URL的連線並返回一個用於從該連線讀入的InputStream,實現最簡單的網路爬蟲。

public static void main(String[] args) {
    BufferedReader br = null;
    try {
        URL url = new URL("http://www.baidu.com/");
        InputStream ips = url.openStream();
        // 將位元組流轉換為字元流
        br = new BufferedReader(new InputStreamReader(ips));
        String str = null;
        // 這樣就可以將網路內容下載到本地機器。
        // 然後進行資料分析,建立索引,這也是搜尋引擎的第一步。  
        while ((str = br.readLine()) != null) {
            System.out.println(str);
        }
    } catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (br != null) {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

TCP/UDP

圖片4.png

Socket實際是傳輸層供給應用層的程式設計介面。Socket就是應用層與傳輸層之間的橋樑。使用Socket程式設計可以開發客戶機和伺服器應用程式,可以在本地網路上進行通訊,也可透過Internet在全球範圍內通訊。

TCP協議和UDP協議是傳輸層的兩種協議。Socket是傳輸層供給應用層的程式設計介面,所以Socket程式設計就分為TCP程式設計和UDP程式設計兩類。

TCP

使用TCP協議,須先建立TCP連線,形成傳輸資料通道,似於撥打電話 傳輸前,採用“三次握手”方式,屬於點對點通訊,是面向連線的,效率低。 僅支援單播傳輸,每條TCP傳輸連線只能有兩個端點(客戶端、服務端)。 兩個端點的資料傳輸,採用的是“位元組流”來傳輸,屬於可靠的資料傳輸。 傳輸完畢,需釋放已建立的連線,開銷大,速度慢,適用於檔案傳輸、郵件等。

UDP

採用資料包(資料、源、目的)的方式來傳輸,無需建立連線,類似於發簡訊。 每個資料包的大小限制在64K內,超出64k可以分為多個資料包來傳送。 傳送不管對方是否準備好,接收方即使收到也不確認,因此屬於不可靠的。 可以廣播傳送,也就是屬於一對一、一對多和多對一連線的通訊協議。 傳送資料結束時無需釋放資源,開銷小,速度快,適用於視訊會議、直播等。

描述 TCP UDP
是否連線 面向連線 面向非連線
傳輸可靠性 可靠 不可靠
連線物件個數 一對一 一對一、一對多、多對一
傳輸方式 面向位元組流 面向報文
傳輸速度
應用場景 適用於實時應用(視訊會議、直播等) 適用於可靠傳輸(檔案傳輸、郵件等)

UDP通訊

在UDP通訊協議下,兩臺計算機之間進行資料互動,並不需要先建立連線,傳送端直接往指定的IP和埠號上傳送資料即可,但是它並不能保證資料一定能讓對方收到,也不能確定什麼時候可以送達。 java.net.DatagramSocket類和java.net.DatagramPacket類是使用UDP程式設計中需要使用的兩個類,並且傳送端和接收端都需要使用這個倆類,並且傳送端與接收端是兩個獨立的執行程式。

  1. DatagramSocket:負責接收和傳送資料,建立接收端時需要指定埠號。
  2. DatagramPacket:負責把資料打包,建立傳送端時需指定接收端的IP地址和埠。

DatagramSocket

DatagramSocket類作為基於UDP協議的Socket,使用DatagramSocket類可以用於接收和傳送資料,同時建立接收端時還需指定埠號。 DatagramSocket類的構造方法:

DatagramSocket類的構造方法:

方法名 描述
public DatagramSocket() 建立傳送端的資料包套接字,隨機埠號
public DatagramSocket(int port) 建立接收端的資料包套接字,並指定埠號

DatagramSocket類的常用方法:

方法名 描述
public void send(DatagramPacket p) 傳送資料包。
public void receive(DatagramPacket p) 接收資料包。
public void close() 關閉資料包套接字。

DatagramPacket

DatagramPacket類負責把傳送的資料打包(打包的資料為byte型別的陣列),並且建立傳送端時需指定接收端的IP地址和埠。 DatagramPacket類的構造方法:

方法名 描述
public DatagramPacket(byte buf[], int offset, int length) 建立接收端的資料包。
public DatagramPacket(byte buf[], int offset, int length, InetAddress address, int port) 建立傳送端的資料包,並指定接收端的IP地址和埠號。

DatagramPacket類的常用方法:

方法名 描述
public synchronized byte[] getData() 返回資料包中儲存的資料
public synchronized int getLength() 獲得傳送或接收資料包中的長度
  • 傳送資料
  1. 建立傳送端的DatagramSocket物件
  2. 資料打包 -> DatagramPacket
  3. 傳送資料
  4. 釋放資源
        //1. 建立DatagramSocket物件
        // 建立時繫結埠,透過這個埠向外傳送資料
        // 空參 從可用埠中隨機獲取一個
        DatagramSocket socket = new DatagramSocket();

        //2. 打包資料
        byte[] bytes = "你好!".getBytes();
        DatagramPacket packet = new DatagramPacket(bytes, 	
                                                   bytes.length,
                                                   InetAddress.getLocalHost(),
                                                   10086);
        
        //3. 傳送資料
        socket.send(packet);
        
        //4. 關閉連線
        socket.close();
  • 接收資料
  1. 建立接收端的DatagramSocket物件,監聽傳送端指定的本機(接收端)埠
  2. 接收打包好的資料
  3. 解析Packet
  4. 釋放資源
DatagramSocket datagramSocket = new DatagramSocket(12345);  
byte[] buf = new byte[64 * 1024];  
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);  
  
//接收資料,阻塞接收  
datagramSocket.receive(datagramPacket);  
  
byte[] data = datagramPacket.getData();  //data == buf is true
System.out.println(new String(data));

但是這樣做會有一個問題,應該接收多少資料就轉化為多少長度的字串,使用getLength就可以解決:

DatagramSocket datagramSocket = new DatagramSocket(12345);  
byte[] buf = new byte[64 * 1024];  
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);  
  
//接收資料,阻塞接收  
datagramSocket.receive(datagramPacket);  
  
byte[] data = datagramPacket.getData();  
System.out.println(new String(data,0,datagramPacket.getLength()));  
System.out.println(data == buf);  
System.out.println("傳送方的IP是:" + datagramPacket.getAddress() + " , 埠是:" + datagramPacket.getPort()); //傳送方的IP是:/2.0.0.1 , 埠是:63950
  • 在執行時應該先執行接收端,再執行傳送端,接收端的receive方法會阻塞當前執行緒

多發多收

  • 傳送端
DatagramSocket socket = new DatagramSocket();  
Scanner sc = new Scanner(System.in);  
String line;  
byte[] data = new byte[10];  
DatagramPacket packet = new DatagramPacket(data, 10, InetAddress.getLocalHost(), 12345);  
while (!(line = sc.nextLine()).equals("end conn")){  
    packet.setData(line.getBytes());  
    packet.setLength(line.getBytes().length);  
    socket.send(packet);  
}
  • 接收端
DatagramSocket socket = new DatagramSocket(12345);  
byte[] data = new byte[1024 * 64];  
DatagramPacket packet = new DatagramPacket(data, data.length);  
String line;  
  
do {  
    socket.receive(packet);  
    line = new String(packet.getData(),0, packet.getLength());  
    System.out.print("from " + packet.getAddress() + ":" + packet.getPort() + " ");  
    System.out.println(line);  
}while (!line.equals("end conn"));

通訊方式

  1. 單播:以前的程式碼就是單播
  2. 組播:組播地址:224.0.0.0 - 239.255.255.255 其中224.0.0.0 - 224.0.0.255為預留的組播地址
  3. 廣播:255.255.255.255 區域網中所有電腦

組播:MulticastSocket

組播傳送資料:

        MulticastSocket mcSocket = new MulticastSocket();
        
        byte[] bytes = "你好".getBytes();
        InetAddress mcIP = InetAddress.getByName("224.0.0.1");//指定組播地址
        DatagramPacket packet = new DatagramPacket(bytes, 0, bytes.length, mcIP, 10086);
        
        mcSocket.send(packet);
        
        mcSocket.close();

組播接收資料:

        MulticastSocket mcSocket = new MulticastSocket(10086); //指定接收哪個埠

        InetAddress mcIP = InetAddress.getByName("224.0.0.1");
        byte[] bytes = new byte[1024];
        DatagramPacket packet = new DatagramPacket(bytes, 0, bytes.length);

		/*將本機加入組播地址*/
        mcSocket.joinGroup(mcIP);
        mcSocket.receive(packet);

        System.out.println(packet.getData());

廣播傳送資料:

image-20230413205613553

TCP通訊

TCP是一種可靠的網路協議,它在通訊的兩端各建立一個Socket物件,通訊之前要保證連線已經建立,透過Socket產生IO流來進行通訊

套接字是一種程序間的資料交換機制,利用套接字(Socket)開發網路應用程式早已被廣泛的採用,以至於成為事實上的標準。 在網路通訊中,第一次主動發起通訊的程式被稱作客戶端(Client),而在第一次通訊中等待連線的程式被稱作服務端(Server)。一旦通訊建立,則客戶端和伺服器端完全一樣,沒有本質的區別。 套接字與主機地址和埠號相關聯,主機地址就是客戶端或伺服器程式所在的主機的IP地址,埠地址是指客戶端或伺服器程式使用的主機的通訊埠。在客戶端和伺服器中,分別建立獨立的Socket,並透過Socket的屬性,將兩個Socket進行連線,這樣客戶端和伺服器透過套接字所建立連線並使用IO流進行通訊。

三次握手和四次揮手

  • 三次握手:確保連線建立

image-20230414093158937

  • 四次揮手:確保連線斷開,且資料處理完畢

image-20230414093339320

半關閉

`void close()`

image-20230414132408535


 Socket socket = new Socket("127.0.0.1",10000);

連線本機的10000埠,如果連線不上程式碼會報錯,但是此時直接執行會報錯:

image-20230414085753891

因為伺服器的程式碼還沒寫,會直接報錯。

  • 客戶端:
        //1. 建立Socket物件 連線伺服器的埠
        //   如果此時連線不上 程式碼報錯
        Socket socket = new Socket("127.0.0.1",10000);

        //2. 從連線通道中獲取輸出流
        OutputStream ops = socket.getOutputStream();

        //寫出資料
        byte[] bytes = "hello world".getBytes();
        ops.write(bytes,0, bytes.length);

        //3.釋放資源
        ops.close(); //關閉流
		socket.close(); //斷開連線

注意:image-20230414091001861

image-20230414091024388

  • 伺服器:
        //1. 建立ServerSocket物件
        ServerSocket serverSocket = new ServerSocket(10000);

        //2.獲取客戶端的Socket 也就是客戶端與伺服器的連線
        // 阻塞方法
        Socket socket = serverSocket.accept();
        //從連線通道中獲取資料
        InputStream ips = socket.getInputStream();

        byte[] bytes = new byte[1024];
        int readCount = ips.read(bytes);
        System.out.println(new String(bytes,0,readCount));

        //4. 釋放資源
        socket.close(); //斷開客戶端連線
        serverSocket.close(); //關閉伺服器

注意:image-20230414091158693

注意:read()方法會阻塞執行緒

半關閉

中文亂碼問題

如果在伺服器使用位元組流讀取,中文就會產生亂碼問題。

需要在伺服器端使用字元流讀取資料:

image-20230414092502299

如果想進一步提高效率,可以再包裝一層緩衝流:

image-20230414092721733

此時輸出端必須每次輸出結束都寫入一個換行符

三次握手和四次揮手

  • 三次握手:確保連線建立

image-20230414093158937

  • 四次揮手:確保連線斷開,且資料處理完畢

image-20230414093339320

基於TCP協議的程式設計

TCP協議程式設計的概述

套接字是一種程序間的資料交換機制,利用套接字(Socket)開發網路應用程式早已被廣泛的採用,以至於成為事實上的標準。
在網路通訊中,第一次主動發起通訊的程式被稱作客戶端(Client),而在第一次通訊中等待連線的程式被稱作服務端(Server)。一旦通訊建立,則客戶端和伺服器端完全一樣,沒有本質的區別。
套接字與主機地址和埠號相關聯,主機地址就是客戶端或伺服器程式所在的主機的IP地址,埠地址是指客戶端或伺服器程式使用的主機的通訊埠。在客戶端和伺服器中,分別建立獨立的Socket,並透過Socket的屬性,將兩個Socket進行連線,這樣客戶端和伺服器透過套接字所建立連線並使用IO流進行通訊。
圖片7.png

Socket類的概述

Socket類實現客戶端套接字(Client),套接字是兩臺機器間通訊的端點。
Socket類的構造方法:

方法名 描述
public Socket(InetAddress a, int p) 建立套接字並連線到指定IP地址的指定埠號

Socket類的成員方法:

方法名 描述
public InetAddress getInetAddress() 返回此套接字連線到的遠端 IP 地址。
public InputStream getInputStream() 返回此套接字的輸入流(接收網路訊息)。
public OutputStream getOutputStream() 返回此套接字的輸出流(傳送網路訊息)。
public void shutdownInput() 禁用此套接字的輸入流
public void shutdownOutput() 禁用此套接字的輸出流。
public synchronized void close() 關閉此套接字(預設會關閉IO流)。

ServerSocket類的概述

ServerSocket類用於實現伺服器套接字(Server服務端)。伺服器套接字等待請求透過網路傳入。它基於該請求執行某些操作,然後可能向請求者返回結果。
ServerSocket類的構造方法:

方法名 描述
public ServerSocket(int port) 建立伺服器套接字並繫結埠號

ServerSocket類的常用方法:

方法名 描述
public Socket accept() 偵聽要連線到此套接字並接受它。
public InetAddress getInetAddress() 返回此伺服器套接字的本地地址。
public void close() 關閉此套接字。

TCP單向通訊的實現

Java語言的基於套接字程式設計分為服務端程式設計和客戶端程式設計,其通訊模型如圖所示:
圖片8.png

伺服器端實現步驟

  1. 建立ServerSocket物件,繫結並監聽埠;
  2. 透過accept監聽客戶端的請求;
  3. 建立連線後,透過輸出輸入流進行讀寫操作;
  4. 呼叫close()方法關閉資源。

【示例】TCP:單向通訊之服務端

public class Test01 {
    public static void main(String[] args)  {
        ServerSocket serverSocket = null;
        Socket accept = null;
        try {
            // 例項化ServerSocket物件(服務端),並明確伺服器的埠號
            serverSocket = new ServerSocket(8888);
            System.out.println("服務端已啟動,等待客戶端連線..");
            // 使用ServerSocket監聽客戶端的請求
            accept = serverSocket.accept();
            // 透過輸入流來接收客戶端傳送的資料
            InputStreamReader reader = new InputStreamReader(accept.getInputStream());
            char[] chars = new char[1024];
            int len = -1;
            while ((len = reader.read(chars)) != -1) {
                System.out.println("接收到客戶端資訊:" + new String(chars, 0, len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 關閉資源
            if (accept != null) {
                try {
                    accept.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

注意:socket物件呼叫了close()方法之後,那麼該socket物件就不能再使用了!

客戶端實現步驟

  1. 建立Socket物件,指定服務端的地址和埠號;
  2. 建立連線後,透過輸入輸出流進行讀寫操作;
  3. 透過輸出輸入流獲取伺服器返回資訊;
  4. 呼叫close()方法關閉資源。

【示例】TCP:單向通訊之客戶端

public class Test02 {
    public static void main(String[] args) {
        Socket socket = null;
        try {
            // 例項化Socket物件(客戶端),並明確連線伺服器的IP和埠號
            InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
            socket = new Socket(inetAddress, 8888);
            // 獲得該Socket的輸出流,用於傳送資料
            Writer writer = new OutputStreamWriter(socket.getOutputStream());
            writer.write("為中華之崛起而讀書!");
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 關閉資源
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

注意:一定是先啟動伺服器程式,然後再啟動客戶端程式,先後循序千萬別弄混了!

TCP雙向通訊的實現

上一個章節我們掌握了Socket的單項通訊,那麼如何實現Socket的雙向通訊呢?在本章節我們將講解的講解雙向通訊的實現。
在雙向通訊的案例中,客戶端需要向服務端傳送一張圖片,服務端收到客戶端傳送的圖片後,則需要向客戶端回覆收到圖片的反饋。在客戶端給服務端傳送圖片的時候,圖片傳送完畢必須呼叫shutdownOutput()方法來關閉socket輸出流,否則服務端讀取資料就會一直阻塞。

伺服器端實現步驟

  1. 建立ServerSocket物件,繫結監聽埠;
  2. 透過accept()方法監聽客戶端請求;
  3. 使用輸入流接收客戶端傳送的圖片,然後透過輸出流儲存圖片
  4. 透過輸出流返回客戶端圖片收到。
  5. 呼叫close()方法關閉資源

【示例】TCP:雙向通訊之服務端

public class Test01 {
    public static void main(String[] args) {
        Socket socket = null;
        ServerSocket serverSocket = null;
        try {
            // 1.建立ServerSocket物件(客戶端),並明確埠號
            serverSocket = new ServerSocket(8889);
            System.out.println("服務端已啟動,等待客戶端連線..");
            // 2.使用ServerSocket監聽客戶端的請求
            socket = serverSocket.accept();
            // 3.使用輸入流接收客戶端傳送的圖片,然後透過輸出流儲存圖片
            InputStream inputStream = socket.getInputStream();
            byte[] bytes = new byte[1024];
            int len = -1;
            FileOutputStream fos = new FileOutputStream("./socket/images/yaya.jpeg");
            while ((len = inputStream.read(bytes)) != -1) {
                fos.write(bytes, 0, len);
            }
            // 4.給客戶端反饋資訊
            Writer osw = new OutputStreamWriter(socket.getOutputStream());
            BufferedWriter bw = new BufferedWriter(osw);
            bw.write("圖片已經收到,謝謝");
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5.關閉資源
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客戶端實現步驟

  1. 建立socket物件,指明需要連線的伺服器地址和埠號;
  2. 建立連線後,透過輸出流向伺服器端傳送圖片;
  3. 透過輸入流獲取伺服器的響應資訊;
  4. 呼叫close()方法關閉資源

【示例】TCP:雙向通訊之客戶端

public class Test02 {
    public static void main(String[] args) {
        Socket socket = null;
        try {
            // 1.例項化Socket物件(客戶端),並設定連線伺服器的IP和埠號
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 8889);
            // 2.透過輸入流讀取圖片,然後再透過輸出流來傳送圖片
            OutputStream outputStream = socket.getOutputStream();
            FileInputStream fis = new FileInputStream("./socket/images/tly.jpeg");
            byte[] bytes = new byte[1024];
            int len = -1;
            while ((len = fis.read(bytes)) != -1) {
                outputStream.write(bytes, 0, len);
            }
            // 注意:此處必須關閉Socket的輸出流,來告訴伺服器圖片傳送完畢
            socket.shutdownOutput();
            // 3.接收伺服器的反饋
            InputStreamReader isr = new InputStreamReader(socket.getInputStream());
            BufferedReader reader = new BufferedReader(isr);
            String lineStr = null;
            while ((lineStr = reader.readLine()) != null) {
                System.out.println("伺服器端反饋:" + lineStr);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.關閉資源
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

練習

多發多收

image-20230414093633815

  • 客戶端:
class Client{
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 10001);
        String line;
        Scanner sc = new Scanner(System.in);
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

        while (true){
            line = sc.nextLine();
            bw.write(line);
            bw.flush(); /*必須呼叫flush重新整理*/
            if ("#end conn".equals(line)) break;
        }
        socket.close();
    }
}

image-20230414101043071

如果在此處沒有用flush()重新整理,使用者的輸入都存入了緩衝區當中,最終輸入#end conn時,bw關閉會將所有資料一次性輸出,伺服器一次將資料全部接收(本例設定為1024個字元),而客戶端已經停止執行了,伺服器端無法停止:

image-20230414101511739

  • 伺服器
class Server{
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10001);
        Socket socket = serverSocket.accept();
        String hostIP = socket.getInetAddress().getHostAddress();
        int port = socket.getPort();
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());

        char[] chars = new char[1024];
        while (true){
            int readCount = isr.read(chars);/*阻塞*/
            if (readCount != -1){
                String msg = new String(chars, 0, readCount);
                System.out.println(hostIP + "/" + port + " : " + msg);
                if ("#end conn".equals(msg)) break;
            }
        }

        isr.close();
        socket.close();
        serverSocket.close();
    }
}

採用InputStreamReader接收資料是因為:在輸出時雖然採用BufferedWriter進行輸出,但是輸出完畢後並沒有newLine(),也就是說在伺服器如果採用BufferedReader接收資料的話,只有在客戶端斷開連線時(#end conn)才會讀到一行資料(實際上是客戶端發出所有資料的拼接)

image-20230414104148218

而在這裡採用InputStreamReader,轉換流底層有緩衝區,read每次從緩衝區中讀取資料,如果緩衝區讀取完畢就從管道中再次獲取儘可能多的資料存入位元組緩衝區;每次從緩衝區中讀出1024個字元,緩衝區每次從管道中獲取8192個位元組。

可以在客戶端每次輸出後都呼叫newLine()方法:

image-20230414104437527

這樣在伺服器就可以使用BufferedReader接收資料了

注意:image-20230414114254258

在判斷是否結束時使用 -1 來判斷,但是如果客戶端還未傳送資料的話,此時是讀不到任何資料的,也就是read方法也會阻塞當前執行緒。

在客戶端斷開連線後,SocketInputStream的成員eof會被置為true,read()方法裡會去判斷eof為true是就返回-1

image-20230414133149401

在傳送端中,如果輸入結束指令#end conn ,跳出迴圈後直接關閉socket,eof被置為true;在接收端就需要判斷read返回值 = -1結束

常見異常

  • java.net.SocketException: Connection reset一端退出,但退出時並未關閉該連線,另一端如果在從連線中讀資料則丟擲該異常

客戶端:image-20230414143418533

伺服器:image-20230414143326833

客戶端退出後未關閉連線,伺服器仍連續讀取資料,就會報該異常

  1. 如果客戶端退出後關閉了連線,伺服器此時在連續讀取資料,由於每次呼叫read方法都會判斷eof變數,所以每次都會返回-1,在伺服器端應該使用readCount來判斷是否結束,也就是說伺服器端不需要對結束指令#end conn進行判斷,只需要判斷read方法的返回值

  2. 對於伺服器使用BufferedReader讀取資料的情況,如果客戶端退出並關閉連線,伺服器會一直讀取 null,這就是伺服器端的結束條件,如果客戶端退出時沒有關閉連線,還是會報該異常

  • java.net.SocketException: Connect reset by peer:如果一端的Socket被關閉(或主動關閉,或因為異常退出而引起的關閉),另一端仍傳送資料,傳送的第一個資料包引發該異常

接收和反饋

image-20230414093704580

image-20230414145833966

如果對客戶端返回訊息,需要指定客戶端的接收埠號,本例中:

image-20230414112547402

也可以:

image-20230414153046441

  • 伺服器:

image-20230414112839680

  • 客戶端

image-20230414113259599

最終客戶端和伺服器都需要對自身的兩個埠進行關閉

注意:在伺服器向客戶端傳送通知時連線只能開闢一次

其他的實現方式:(客戶端只傳送一次,傳送完畢等待伺服器通知;伺服器端接收到訊息後對客戶端進行通知)

image-20230414152420203

只有客戶端在斷開連線時read才能讀到-1,只有返回-1才能結束伺服器阻塞狀態,但是如果在此時斷開客戶端連線

可以在此處呼叫socket.shutdownOutput()

半關閉問題

問題背景:客戶端連線伺服器,傳送一個請求,捕獲響應資訊。

客戶端透過輸出流向伺服器傳送資料, 如果不關閉輸出流,伺服器無法判斷出客戶端是否已經輸出完畢,因此伺服器的讀操作將會處於阻塞狀態;當客戶端關閉輸出流,伺服器得到客戶端的輸出已經結束的資訊,伺服器開始執行讀操作。

然而,這會導致另外一個問題,客戶端輸出流關閉的時候,socket 也會自動斷開連線。當伺服器需要透過輸出流向客戶端傳輸資料時,便會出現java.net.SocketException: Socket closed

解決方案:使用 半關閉:

// dos2.close(); // 會導致 socket 連線斷開
 
socket.shutdownOutput();
// 現在 socket 是半關閉狀態,輸出流關閉,但輸入流開啟,socket 連線不會斷開。

以下內容摘自《Java 核心技術卷2》第三章

2.2 半關閉

套接字連線的一端可以終止其輸出,同時仍舊可以接受來自另一端的資料。

這是一種很典型的情況,例如我們在向伺服器傳輸資料,但並不知道要傳輸多少個資料。如果關閉一個套接字,那麼伺服器的連線將立刻斷開,因而也就無法讀取伺服器的響應了。

使用半關閉的方法就可以解決上述的問題,可以透過關閉一個套接字的輸出流來表示傳送給伺服器的請求資料已經結束,但是必須保持輸入流處於開啟狀態。

socket.shutdownOutput();
socket.shutdownInput();

當然,該協議只適用於一站式的服務,例如 HTTP 服務,在這種服務中,客戶端連線伺服器,傳送一個請求,捕獲響應資訊,然後斷開連線。

上傳檔案

  • 伺服器
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10001);
        Socket socket = serverSocket.accept();
        InputStream ips = socket.getInputStream();
        FileOutputStream fos = new FileOutputStream("copy_names.txt");
        int readCount;
        byte[] bytes = new byte[1024 * 2];
        while ((readCount = ips.read(bytes)) != -1){
            fos.write(bytes,0,readCount);
        }
        
        OutputStream ops = socket.getOutputStream();
        ops.write("done".getBytes());
        ops.flush();

        fos.close();
        ips.close();
        socket.close();
        serverSocket.close();
    }
  • 客戶端
	 public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 10001);
        FileInputStream fis = new FileInputStream("names.txt");
        OutputStream ops = socket.getOutputStream();
        byte[] bytes = new byte[1024 * 2];
        int readCount;
        while ((readCount = fis.read(bytes)) != -1){
            ops.write(bytes,0,readCount);
        }
        socket.shutdownOutput();

        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        System.out.println(reader.readLine());

        ops.close();
        fis.close();
        socket.close();
    }

注意:image-20230414155801230

否則伺服器的迴圈無法終止:

image-20230414155909384

如果客戶端向伺服器單向傳送一次資料,傳送完成之後記得傳送結束標記

複製檔案時使用的流最後一定要關閉

上傳的檔名重複

java.util.UUID,表示通用唯一識別符號的類,UUID標識一個128位的值

透過UUID.randomUUID()可以獲取一個隨機的UUID字串

System.out.println(UUID.randomUUID());//97e1f3f4-1257-4309-befa-e5dff0879692
System.out.println(UUID.randomUUID().toString().replaceAll("-",""));//97e1f3f412574309befae5dff0879692

多執行緒上傳

image-20230414093945436

思路:在伺服器端將請求連線的每一個使用者看作一個執行緒,分別對這些執行緒進行操作

  • 客戶端:
class Client {
    public static void doSome(File file) throws IOException {
        Socket socket = new Socket("127.0.0.1", 10008);
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
        byte[] bytes = new byte[1024 * 2];
        int readCount;
        while ((readCount = bis.read(bytes)) != -1){
            bos.write(bytes,0,readCount);
        }
        bos.flush();
        socket.shutdownOutput();
        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        System.out.println(reader.readLine());
        socket.close();
    }
}
  • 伺服器:
class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10008);
        while (true){
            Socket socket = serverSocket.accept();
            new Thread(new MyRunnable(socket)).start();
        }
    }
}

注意:

image-20230414172614803

  • 執行緒類:
public class MyRunnable implements Runnable{
    Socket socket;
    public MyRunnable(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            String name = UUID.randomUUID().toString().replaceAll("-", "");

            String hostAddress = socket.getInetAddress().getHostAddress();
            String hostName = socket.getInetAddress().getHostName();

            BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("serverdir/" + name + ".txt"));
            byte[] bytes = new byte[1024 * 2];
            int readCount;
            while ((readCount = bis.read(bytes)) != -1){
                bos.write(bytes,0,readCount);
            }
            bos.close();
            socket.getOutputStream().write((Thread.currentThread().getName() + "@" + hostAddress + "@" + hostName + "接收完畢").getBytes());
            socket.close();
        } catch (IOException e){
            e.printStackTrace();
        }
    }
}

注意:Server不停止執行,每個執行緒的執行緒號都是依次向上累加

執行緒池上傳

image-20230414094014716

只需要將Server端的提交方式改為:

image-20230414173211681

BS 接收瀏覽器的資料並列印

image-20230414094108876

客戶端就是瀏覽器,我們只需要在伺服器接收資料就可以了

瀏覽器中訪問指定的埠號,伺服器就能列印出資料:

GET / HTTP/1.1
Host: localhost:10000
Connection: keep-alive
sec-ch-ua: "Chromium";v="112", "Microsoft Edge";v="112", "Not:A-Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.39
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7,en-GB;q=0.6,en-GB-oxendict;q=0.5
Cookie: Webstorm-3a48b5d7=0f49d683-9f8a-41cb-b29b-af0cf5bba784
x-forwarded-for: 8.8.8.8

聊天室

多收多發

public class TcpClientDemo1 {  
    public static void main(String[] args) throws Exception {  
  
        // 目標:客戶端開發: 多發多收  
        // 1、建立一個Socket通訊管道與服務端建立可靠連結  
        Socket socket = new Socket("127.0.0.1", 9999);  
        // 2、從socket通訊管道中得到一個位元組輸出流。  
        OutputStream os = socket.getOutputStream();  
        // 3、把位元組輸出流包裝成一個資料輸出流  
        DataOutputStream dos = new DataOutputStream(os);  
  
        Scanner sc = new Scanner(System.in);  
        while (true) {  
            System.out.println("請說:");  
            String msg = sc.nextLine();  
  
            if("exit".equals(msg)) {  
                System.out.println("退出成功!~");  
                socket.close();  
                break;  
            }  
  
            // 4、寫資料出去  
            dos.writeUTF(msg);  
            dos.flush();  
        }  
  
    }  
}
public class TcpServerDemo2 {  
    public static void main(String[] args) throws Exception {  
        try {  
            System.out.println("=========服務端程式============");  
            // 目標:服務端的開發。  
            // 1、註冊埠  
            ServerSocket serverSocket = new ServerSocket(9999);  
            // 2、監聽客戶端的連結請求,得到服務端socket  
            Socket socket = serverSocket.accept();  
            System.out.println("有人上線了~~~");  
            // 3、從服務端中獲取一個位元組輸入流。  
            InputStream is = socket.getInputStream();  
            // 4、把位元組輸入流包裝成資料輸入流  
            DataInputStream dis = new DataInputStream(is);  
  
            while (true) {  
                // 5、讀資料  
                String msg = dis.readUTF();  
  
                System.out.println("服務端收到了:");  
                System.out.println(msg);  
  
                System.out.println("對方IP:"  
                  + socket.getInetAddress().getHostAddress());  
                System.out.println("對方埠:"  
                        + socket.getPort());  
            }  
        } catch (Exception e) {  
            System.out.println("有人下線了");  
        }  
    }  
}
  • 多收多發
public class ServerReaderThread extends Thread{  
    private Socket socket;  
    public ServerReaderThread(Socket socket) {  
        this.socket = socket;  
    }  
  
    @Override  
    public void run() {  
        try {  
            // 3、從服務端中獲取一個位元組輸入流。  
            InputStream is = socket.getInputStream();  
            // 4、把位元組輸入流包裝成資料輸入流  
            DataInputStream dis = new DataInputStream(is);  
  
            while (true) {  
                // 5、讀資料  
                String msg = dis.readUTF();  
                System.out.println(socket.getInetAddress().getHostAddress() + " 說:" + msg);  
                System.out.println("--------------------------------------");  
            }  
        } catch (Exception e) {  
            System.out.println(socket.getInetAddress().getHostAddress()  
               + "下線了!");  
        }  
    }  
}
public class TcpClientDemo1 {  
    public static void main(String[] args) throws Exception {  
  
        // 目標:客戶端開發: 多發多收  
        // 1、建立一個Socket通訊管道與服務端建立可靠連結  
        Socket socket = new Socket("127.0.0.1", 9999);  
        // 2、從socket通訊管道中得到一個位元組輸出流。  
        OutputStream os = socket.getOutputStream();  
        // 3、把位元組輸出流包裝成一個資料輸出流  
        DataOutputStream dos = new DataOutputStream(os);  
  
        Scanner sc = new Scanner(System.in);  
        while (true) {  
            System.out.println("請說:");  
            String msg = sc.nextLine();  
  
            if("exit".equals(msg)) {  
                System.out.println("退出成功!~");  
                socket.close();  
                break;  
            }  
            // 4、寫資料出去  
            dos.writeUTF(msg);  
            dos.flush();  
        }  
    }  
}
public class TcpServerDemo2 {  
    public static void main(String[] args) throws Exception {  
        try {  
            System.out.println("=========服務端程式============");  
            // 目標:服務端的開發。  
            // 1、註冊埠  
            ServerSocket serverSocket = new ServerSocket(9999);  
            while (true) {  
                // 2、監聽客戶端的連結請求,得到服務端socket  
                Socket socket = serverSocket.accept();  
                System.out.println(socket.getInetAddress().getHostAddress() + "上線了~!");  
                // 3、把這個客戶端管道交給一個獨立的子執行緒來處理。  
                new ServerReaderThread(socket).start();  
            }  
        } catch (Exception e) {  
           e.printStackTrace();  
        }  
    }  
}
  • 群聊
public class TcpClientDemo1 {  
    public static void main(String[] args) throws Exception {  
  
        // 目標:客戶端開發: 多發多收  
        // 1、建立一個Socket通訊管道與服務端建立可靠連結  
        Socket socket = new Socket("127.0.0.1", 9999);  
  
        // 立即為這個客戶端管道分配一個獨立的執行緒專門負責這個管道的收訊息。  
        new ClientReaderThread(socket).start();  
  
        // 2、從socket通訊管道中得到一個位元組輸出流。  
        OutputStream os = socket.getOutputStream();  
        // 3、把位元組輸出流包裝成一個資料輸出流  
        DataOutputStream dos = new DataOutputStream(os);  
  
        Scanner sc = new Scanner(System.in);  
  
        while (true) {  
            System.out.println("請說:");  
            String msg = sc.nextLine();  
  
            if("exit".equals(msg)) {  
                System.out.println("退出成功!~");  
                socket.close();  
                break;  
            }  
            // 4、寫資料出去  
            dos.writeUTF(msg);  
            dos.flush();  
        }  
  
    }  
}
public class TcpServerDemo2 {  
  
    // 定義一個線上集合儲存全部的線上socket管道。  
    public static List<Socket> onLineSockets = new ArrayList<>();  
  
    public static void main(String[] args) throws Exception {  
        try {  
            System.out.println("=========服務端程式============");  
            // 目標:服務端的開發。  
            // 1、註冊埠  
            ServerSocket serverSocket = new ServerSocket(9999);  
            while (true) {  
                // 2、監聽客戶端的連結請求,得到服務端socket  
                Socket socket = serverSocket.accept();  
                System.out.println(socket.getInetAddress().getHostAddress() + "上線了~!");  
                onLineSockets.add(socket);  
                // 3、把這個客戶端管道交給一個獨立的子執行緒來處理。  
                new ServerReaderThread(socket).start();  
            }  
        } catch (Exception e) {  
           e.printStackTrace();  
        }  
    }  
}
public class ClientReaderThread extends Thread{  
    private Socket socket;  
    public ClientReaderThread(Socket socket) {  
        this.socket = socket;  
    }  
  
    @Override  
    public void run() {  
        try {  
            // 3、從服務端中獲取一個位元組輸入流。  
            InputStream is = socket.getInputStream();  
            // 4、把位元組輸入流包裝成資料輸入流  
            DataInputStream dis = new DataInputStream(is);  
  
            while (true) {  
                // 5、讀資料  
                String msg = dis.readUTF();  
                // 把這個訊息轉發給當前線上的全部socket管道接收。  
                System.out.println("收到:" + msg);  
                System.out.println("--------------------------------------");  
            }  
        } catch (Exception e) {  
            System.out.println("客戶端完成正常退出!");  
        }  
    }  
  
  
}
public class ServerReaderThread extends Thread{  
    private Socket socket;  
    public ServerReaderThread(Socket socket) {  
        this.socket = socket;  
    }  
  
    @Override  
    public void run() {  
        try {  
            // 3、從服務端中獲取一個位元組輸入流。  
            InputStream is = socket.getInputStream();  
            // 4、把位元組輸入流包裝成資料輸入流  
            DataInputStream dis = new DataInputStream(is);  
  
            while (true) {  
                // 5、讀資料  
                String msg = dis.readUTF();  
                System.out.println(socket.getInetAddress().getHostAddress() + " 說:" + msg);  
                // 把這個訊息轉發給當前線上的全部socket管道接收。  
                sendMsgToAll(msg);  
                System.out.println("--------------------------------------");  
            }  
        } catch (Exception e) {  
            System.out.println(socket.getInetAddress().getHostAddress()  
               + "下線了!");  
        }  
    }  
  
    private void sendMsgToAll(String msg) throws Exception {  
        // 遍歷線上集合的每個socket,把訊息推給人家  
        for (Socket onLineSocket : TcpServerDemo2.onLineSockets) {  
            // 這個管道不能是自己,就應該發訊息給他。  
            if(onLineSocket != socket) {  
                DataOutputStream dos = new DataOutputStream( onLineSocket.getOutputStream() );  
                dos.writeUTF(msg);  
                dos.flush(); // 刷出去訊息。  
            }  
        }  
    }  
}

簡易BS架構

BS架構是基於瀏覽器/伺服器的,請求協議基於TCP,HTTP協議也是長連線,只是通訊的時長較短

a3b108b15a6510fee174b8d40ae4bac.png

瀏覽器的每個請求,伺服器都開啟一個新的執行緒進行處理。

BS架構的基本原理:

  • 客戶端使用瀏覽器發起請求

  • 伺服器必須返回HTTP協議規定好的資料格式,否則瀏覽器無法識別,參照[[HTTP|HTTP的格式]]

//1. 伺服器開啟服務
ServerSocket serverSocket = new ServerSocket(8080);  
while (true) {  
    // 2、監聽瀏覽器請求的管道連結。  
    Socket socket = serverSocket.accept();  
    // 3、交給一個獨立的執行緒負責為這個管道響應一個網頁回去。  
    new ServerReaderThread(socket).start();  
}
public class ServerReaderThread extends Thread{  //繼承Thread子類,重寫run方法
    private Socket socket;  
    public ServerReaderThread(Socket socket) {  
        this.socket = socket;  
    }  
  
    @Override  
    public void run() {  
        try {  
            // 響應一個網頁給 socket 管道。  
            PrintStream ps = new PrintStream(socket.getOutputStream());  
            ps.println("HTTP/1.1 200 OK");  
            ps.println("Content-Type:text/html;charset=UTF-8");  
            ps.println(); // 必須換行  
            ps.println("<div style='color:red;font-size:80px'>我愛磊哥</div>");  
  
            ps.close();  
            socket.close();  
        } catch (Exception e) {  
            System.out.println(socket.getInetAddress().getHostAddress()  
               + "下線了!");  
        }  
    }  
}

注意:繼承Thread的子類,未啟動是任務,啟動是執行緒

但是程式也有改進的地方,HTTP連線通訊時間極短,非常使用執行緒池最佳化

ExecutorService pool = new ThreadPoolExecutor(3, 5, 5, 
											  TimeUnit.SECONDS  , 
											  new ArrayBlockingQueue<>(5),
											  Executors.defaultThreadFactory(),  
											  new ThreadPoolExecutor.AbortPolicy());  
// 1、註冊埠  
ServerSocket serverSocket = new ServerSocket(8080);  
while (true) {  
    // 2、監聽瀏覽器請求的管道連結。  
    Socket socket = serverSocket.accept();  
    // 把這個管道包裝成一個任務物件,交給執行緒池排隊  
    pool.execute(new ServerRunnable(socket));  //Thread就是Runnable
}
public class ServerRunnable implements Runnable{  
    private Socket socket;  
    public ServerRunnable(Socket socket) {  
        this.socket = socket;  
    }  
  
    @Override  
    public void run() {  
  
        try {  
            // 響應一個網頁給 socket 管道。  
            PrintStream ps = new PrintStream(socket.getOutputStream());  
            ps.println("HTTP/1.1 200 OK");  
            ps.println("Content-Type:text/html;charset=UTF-8");  
            ps.println(); // 必須換行  
            ps.println("<div style='color:red;font-size:80px'>我愛磊哥</div>");  
  
            ps.close();  
            socket.close();  
        } catch (Exception e) {  
            System.out.println(socket.getInetAddress().getHostAddress()  
               + "下線了!");  
        }  
    }  
}

相關文章