JavaRush /Java Blog /Random-TW /Socket 和 ServerSocket 類,或「你好,伺服器?你聽得到我嗎?”
Sergey Simonov
等級 36
Санкт-Петербург

Socket 和 ServerSocket 類,或「你好,伺服器?你聽得到我嗎?”

在 Random-TW 群組發布
簡介:「桌上有一台電腦,後面有一個編碼器…」 Socket 和 ServerSocket 類,或「你好,伺服器? 你聽得到我嗎?”  - 1曾幾何時,我的一個同學貼出了他學習Java的另一個成果,以一個新程式的截圖的形式。該程式是一個多用戶聊天程式。當時我剛開始自己掌握這種語言程式設計的旅程,但我明確地對自己說:“我想要它!” 時間過去了,作為加深我的程式設計知識的一部分,我完成了下一個專案的工作,我想起了那件事,並決定是時候了。不知何故,我純粹出於好奇已經開始深入研究這個主題,但在我的主要 Java 教科書(這是 Schildt 的完整手冊)中,只為 java.net 包提供了 20 頁。這是可以理解的——這本書已經很大了。有主類別的方法和建構函數表,但僅此而已。當然,下一步是全能的谷歌:無數不同的文章都提出了相同的內容 - 關於套接字的兩三個單詞,以及一個現成的示例。經典的方法(至少在我的學習風格中)是首先了解我需要從工作工具中獲得什麼,它們是什麼,為什麼需要它們,只有這樣,如果問題的解決方案不明顯,那就選擇準備好清單,擰下螺帽和螺栓。但我弄清楚了是什麼,並最終寫了一個多用戶聊天。從表面上看,結果是這樣的: Socket 和 ServerSocket 類,或「你好,伺服器? 你聽得到我嗎?”  - 2在這裡,我將嘗試透過聊天設計的範例讓您了解基於 Java 套接字的客戶端-伺服器應用程式的基礎知識。在 Javarash 課程中,您將進行聊天。它將達到一個完全不同的水平,美麗、大型、多功能。但首先,你總是需要打好基礎,所以這裡我們需要弄清楚這個部分的基礎是什麼。(如果您發現任何不足或錯誤,請在PM或文章下的評論中寫下)。讓我們開始。 第一個標題:「房子......」 為了解釋伺服器和客戶端之間的網路連線如何發生,讓我們以現在經典的公寓大樓為例。假設客戶端需要以某種方式與特定伺服器建立連線。搜尋者需要了解搜尋對象的哪些資訊?是的,地址。伺服器不是雲端上的神奇實體,因此它必須位於特定的機器上。與房子類比,兩個商定的各方應該在那裡舉行會議。為了在一棟公寓大樓中找到對方,僅提供大樓的地址是不夠的;您必須指明會面所在的公寓號碼。同樣,在一台電腦上可以同時有多個伺服器,為了讓客戶端聯繫特定的伺服器,他還需要指定進行連線的連接埠號碼。所以,地址和連接埠號碼。 位址是指網路空間中機器的識別碼。它可以是域名,例如“javarush.ru”,也可以是常規 IP。 港口- 與特定套接字相關聯的唯一號碼(這個術語將在後面討論),換句話說,它被某個服務佔用,以便可以用來聯繫它。因此,為了使至少兩個物件在其中一個(伺服器)的領土上相遇,該區域(伺服器)的所有者必須佔用其(汽車)上的特定公寓(連接埠),而第二個物件必須知道會面地點房屋地址(網域名稱或ip )和公寓號碼(連接埠)。 第二頭:認識 Socket 在與網路工作相關的概念和術語中,非常重要的一個是 Socket。它表示連接發生的點。簡單地說,套接字連接網路上的兩個程式。該類別Socket實現了套接字的思想。客戶端和伺服器將透過其輸入/輸出通道進行通訊: Socket 和 ServerSocket 類,或「你好,伺服器? 你聽得到我嗎?”  - 3 此類在客戶端聲明,伺服器重新建立它,接收連接訊號。這就是在線交流的工作原理。首先,以下是可能的類別建構函數Socket
Socket(String Name_хоста, int порт) throws UnknownHostException, IOException
Socket(InetAddress IP-address, int порт) throws UnknownHostException
“host_name” - 表示特定的網路節點、IP 位址。如果套接字類別無法將其轉換為真實的現有位址,則會拋出異常UnknownHostException。港口就是港口。如果指定連接埠號碼為0,系統本身會分配一個空閒連接埠。如果連線遺失,也可能會發生異常IOException。需要注意的是,第二個建構函式中的位址類型是InetAddress。例如,當您需要指定網域名稱作為地址時,它就可以派上用場。此外,當一個網域意味著多個 IP 位址時,InetAddress您可以使用它們來取得它們的陣列。然而,它也適用於 IP。您也可以獲得主機名稱、組成 IP 位址的位元組陣列等。我們將進一步討論它,但您必須訪問官方文件以獲取完整的詳細資訊。當初始化 類型的物件時Socket,它所屬的用戶端在網路上宣布它想要連接到特定位址和連接埠號碼的伺服器。以下是該類別最常用的方法SocketInetAddress getInetAddress()– 傳回包含有關套接字的資料的物件。如果套接字未連線 - null int getPort()- 傳回與伺服器發生連線的連接埠 int getLocalPort()- 傳回套接字綁定的連接埠。事實上,客戶端和伺服器可以在一個連接埠上“通信”,但它們綁定的連接埠可以完全不同 boolean isConnected()- 如果連接已建立,則返回 true void connect(SocketAddress address)- 表示一個新連接 boolean isClosed()- 如果套接字關閉,則返回 true boolean isBound()-傳回true,如果套接字確實綁定到某個位址,則該類別Socket實作該接口AutoCloseable,因此可以在try-with-resources. 但是,您也可以使用 close() 以經典方式關閉套接字。 前三:這是一個 ServerSocket 假設我們以類別的形式聲明了Socket客戶端的連線請求。伺服器如何猜測我們的願望?為此,伺服器有一個類似 的類 ServerSocket,以及其中的accept() 方法。其建構函式如下所示:
ServerSocket() throws IOException
ServerSocket(int порт) throws IOException
ServerSocket(int порт, int максимум_подключений) throws IOException
ServerSocket(int порт, int максимум_подключений, InetAddress локальный_address) throws IOException
聲明時,ServerSocket 不需要指定連接位址,因為通訊是在伺服器電腦上進行的。僅對於多通道主機,您才需要指定伺服器套接字綁定到哪個 IP。 前三.一:拒絕的伺服器 由於為程式提供超出其需要的資源既昂貴又不合理,因此在構造函數中ServerSocket要求您聲明伺服器在運行期間接受的最大連接數。如果未指定,則預設情況下該數字將被視為等於 50。是的,理論上我們可以假設ServerSocket這是同一個套接字,僅適用於伺服器。但它扮演著與 class 完全不同的角色Socket。僅在連接建立階段需要它。建立類型物件後, ServerSocket您需要找出有人想要連接到伺服器。這裡連接的是accept()方法。目標會一直等待,直到有人想要連接到它,當這種情況發生時,它會返回一個 類型的對象Socket,即重新建立的客戶端套接字。現在客戶端套接字已在伺服器端創建,雙向通訊就可以開始了。在客戶端建立類型物件Socket並使用伺服器端重新建立它ServerSocket是連線所需的最低限度。 前四:給聖誕老人的信 Вопрос:客戶端和伺服器究竟如何通訊? Ответ:透過 I/O 流。我們已經擁有什麼?帶有伺服器位址和客戶端連接埠號碼的套接字,以及相同的東西,這要歸功於伺服器端的accept()。因此可以合理地假設它們將透過套接字進行通訊。為此,有兩種方法可以存取 類型的流InputStreamOutputStream物件Socket。他們來了:
InputStream getInputStream()
OutputStream getOutputStream()
由於讀取和寫入裸字節效率不高,因此可以將流包裝在適配器類別中,無論是否有緩衝。例如:
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
為了實現雙向通信,必須在雙方都執行此類操作。現在,您可以使用 in 發送某些內容,並使用 out 接收某些內容,反之亦然。實際上,這實際上是該類別的唯一功能Socket。是的,不要忘記flush()方法 BufferedWriter——它刷新緩衝區的內容。如果不這樣做,訊息將不會被傳輸,因此也不會被接收。接收線程還要等待行尾指示符——“\n”,否則訊息將不會被接受,因為實際上訊息還沒有完成且不完整。如果這對你來說不方便,別擔心,你可以隨時使用PrintWriter需要包裝的 class ,指定 true 作為第二個參數,然後從緩衝區中彈出將自動發生:
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
另外,無需指示行尾;此類別會為您完成此操作。但是字串 I/O 是套接字功能的限制嗎?不,您想透過套接字流發送物件嗎?看在上帝的份上。將它們序列化就可以了:
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
前五:透過互聯網進行真實通信 因為為了透過具有真實 IP 位址的真實網路進行連接,您需要擁有一個成熟的伺服器,並且因為:
  1. 我們未來的聊天作為一個實用工具,不具備這樣的能力。它只能建立連接並接收/發送訊息。也就是說,它不具備真正的伺服器能力。
  2. 我們的伺服器僅包含套接字資料和 I/O 流,無法作為真正的 WEB 或 FTP 伺服器工作,那麼僅此我們將無法透過 Internet 連線。
此外,我們剛開始開發該程序,這意味著它還不夠穩定,無法立即與真實網路一起工作,因此我們將使用本地主機作為連接位址。也就是說,理論上,除了透過套接字之外,客戶端和伺服器仍然不會以任何方式連接,但為了調試程序,它們將位於同一台機器上,而無需透過網路進行真正的聯繫。為了在建構函式中顯示Socket該位址是本地的,有2種方法:
  1. 將“localhost”寫入位址參數,表示本機存根。「127.0.0.1」也適合於此 - 這只是存根的數位形式。
  2. 使用 InetAddress:
    1. InetAddress.getByName(null)- null 指向本機
    2. InetAddress.getByName("localhost")
    3. InetAddress.getByName("127.0.0.1")
為簡單起見,我們將使用字串類型的「localhost」。但所有其他選擇也是可行的。 第六點:是時候進行對話了 因此,我們已經擁有了與伺服器實現對話會話所需的一切。剩下的就是將它們放在一起:下面的清單顯示了客戶端如何連接到伺服器,向其發送一條訊息,然後伺服器使用它作為其參數中的參數來確認它收到了該訊息:爪哇”
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    private static Socket clientSocket; // socket for communication
    private static ServerSocket server; // server socket
    private static BufferedReader in; // socket read stream
    private static BufferedWriter out; // socket write stream

    public static void main(String[] args) {
        try {
            try  {
                server = new ServerSocket(4004); // server socket listening on port 4004
                System.out.println("Server is running!"); // server would be nice
                // announce your launch
                clientSocket = server.accept(); // accept() will wait until
                //someone won't want to connect
                try { // after establishing a connection and recreating the socket for communication with the client, you can go
                    // to create I/O streams.
                    // now we can receive messages
                    in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                    // and send
                    out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

                    String word = in.readLine(); // wait for the client to write something to us
                    System.out.println(word);
                    // without hesitation responds to the client
                    out.write("Hi, this is the Server! I confirm you wrote: " + word + "\n");
                    out.flush(); // push everything out of the buffer

                } finally { // in any case, the socket will be closed
                    clientSocket.close();
                    // streams would also be nice to close
                    in.close();
                    out.close();
                }
            } finally {
                System.out.println("Server closed!");
                    server.close();
            }
        } catch (IOException e) {
            System.err.println(e);
        }
    }
“客戶端.java”
import java.io.*;
import java.net.Socket;

public class Client {

    private static Socket clientSocket; // socket for communication
    private static BufferedReader reader; // we need a reader that reads from the console, otherwise
    // do we know what the client wants to say?
    private static BufferedReader in; // socket read stream
    private static BufferedWriter out; // socket write stream

    public static void main(String[] args) {
        try {
            try {
                // address - local host, port - 4004, same as the server
                clientSocket = new Socket("localhost", 4004); // with this line we request
                // the server has access to the connection
                reader = new BufferedReader(new InputStreamReader(System.in));
                // read messages from the server
                in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                // write there
                out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

                System.out.println("Did you have something to say? Enter it here:");
                // if the connection happened and the threads were successfully created - we can
                // work further and offer the client something to enter
                // if not, an exception will be thrown
                String word = reader.readLine(); // wait for the client to do something
                // will not write to the console
                out.write(word + "\n"); // send a message to the server
                out.flush();
                String serverWord = in.readLine(); // wait for the server to say
                System.out.println(serverWord); // received - display
            } finally { // in any case, you need to close the socket and streams
                System.out.println("The client has been closed...");
                clientSocket.close();
                in.close();
                out.close();
            }
        } catch (IOException e) {
            System.err.println(e);
        }

    }
}
當然,您應該先啟動伺服器,因為如果沒有任何東西可以連接客戶端,那麼客戶端在啟動時將連接到什麼?:) 輸出會是這樣的: /* 你想說些什麼嗎?在此輸入:您好,伺服器嗎?你聽得到我嗎?你好,這裡是伺服器!我確認,您寫道: 你好,伺服器?你聽得到我嗎?客戶端已關閉... */ 萬歲!我們教伺服器如何與客戶端通訊!因此,通訊不會發生在兩個副本中,而是發生在任意數量的副本中,只需將執行緒的讀取和寫入包裝在while (true) 循環中,並根據某個訊息指示退出,例如「退出」 ,循環中斷,程式結束。 前七:多用戶更好。 伺服器能聽到我們的聲音這一點很好,但如果我們能和我們同類的人交流那就更好了。我將在文章末尾附上所有原始程式碼,因此在這裡我將展示並不總是很大但很重要的程式碼片段,如果使用正確,這些程式碼片段將使得可以建立多用戶聊天。因此,我們希望能夠透過伺服器與其他客戶端進行通訊。怎麼做?顯然,由於客戶端程式有自己的方法main,這意味著它可以與伺服器分開啟動,並與其他客戶並行啟動。這為我們帶來了什麼?不知何故,對於每個新連接,伺服器不會立即進行通信,而是將該連接寫入某種列表並繼續等待新連接,並且某種輔助服務與特定的通信進行通信。客戶。客戶端必須彼此獨立地寫入伺服器並等待回應。線程來救援。假設我們有一個負責記住新連線的類別:它應該指定以下內容:
  1. 連接埠號。
  2. 在其中寫入新連線的清單。
  3. 並且ServerSocket,在一個(!)副本中。
public class Server {

    public static final int PORT = 8080;
    public static LinkedList<ServerSomthing> serverList = new LinkedList<>(); // list of all threads

    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(PORT);
            try {
            while (true) {
                // Blocks until a new connection is made:
                Socket socket = server.accept();
                try {
                    serverList.add(new ServerSomthing(socket)); // add a new connection to the list
                } catch (IOException e) {
                    // If it fails, the socket is closed,
                    // otherwise, the thread will close it when it exits:
                    socket.close();
                }
            }
        } finally {
            server.close();
        }
    }
}
好的,現在每個重新建立的套接字都不會遺失,而是會儲存在伺服器上。更遠。每個客戶都需要有人傾聽他們的聲音。讓我們使用上一章中的伺服器函數來建立一個執行緒。
class ServerSomthing extends Thread {

    private Socket socket; // socket through which the server communicates with the client,
    // except for it - the client and server are not connected in any way
    private BufferedReader in; // socket read stream
    private BufferedWriter out; // socket write stream

    public ServerSomthing(Socket socket) throws IOException {
        this.socket = socket;
        // если потоку ввода/вывода приведут к генерированию исключения, оно пробросится дальше
        in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        start(); // call run()
    }
    @Override
    public void run() {
        String word;
        try {

            while (true) {
                word = in.readLine();
                if(word.equals("stop")) {
                    break;                }
                for (ServerSomthing vr : Server.serverList) {
                    vr.send(word); // send the received message with
                   // bound client to everyone else including him
                }
            }

        } catch (IOException e) {
        }
    }

    private void send(String msg) {
        try {
            out.write(msg + "\n");
            out.flush();
        } catch (IOException ignored) {}
    }
}
因此,在伺服器執行緒的建構函式中,必須初始化一個套接字,執行緒將透過該套接字與特定客戶端進行通訊。還有 I/O 線程,以及直接從建構函數啟動線程所需的所有其他內容。好的,但是當伺服器執行緒從客戶端讀取訊息時會發生什麼?只發回給您的客戶?不太有效。我們正在進行多用戶聊天,因此我們需要每個連線的客戶接收一個人寫的內容。您需要使用與其客戶端關聯的所有伺服器執行緒的列表,並將每個訊息傳送到特定線程,以便將其發送到其客戶端:
for (ServerSomthing vr : Server.serverList) {
    vr.send(word); // send the received message
    // from the linked client to everyone else, including him
}
private void send(String msg) {
    try {
        out.write(msg + "\n");
        out.flush();
    } catch (IOException ignored) {}
}
現在所有客戶都會知道其中一位所說的話!如果您不想將訊息發送給發送它的人(他已經知道他寫了什麼!),只需在迭代線程時,指定在處理物件時,循環將移動到下一個元素this而不執行對其採取任何行動。或者,如果您願意,可以向客戶端發送訊息,說明訊息已成功接收和發送。現在伺服器一切都清楚了。讓我們轉向客戶,或者更確切地說,轉向客戶!一切都是一樣的,與上一章中的客戶端類比,只有在建立實例時,您才需要建立建構函式中所需的所有內容,如本章中的伺服器所示。但是,如果在創建客戶端時,他還沒有時間輸入任何內容,但有些內容已經發送給他怎麼辦?(例如,那些已經連接到他之前的聊天記錄的人的通信歷史)。因此,處理發送訊息的周期必須與從控制台讀取訊息並發送到伺服器轉發給其他人的週期分開。線再次拯救了我們。將客戶端創建為線程是沒有意義的。更方便的做法是在run方法中創建一個帶有循環的線程來讀取訊息,同樣類推,這樣寫:
// thread reading messages from the server
private class ReadMsg extends Thread {
    @Override
    public void run() {

        String str;
        try {
            while (true) {
                str = in.readLine(); // waiting for a message from the server
                if (str.equals("stop")) {

                    break; // exit the loop if it's "stop"
                }
                            }
        } catch (IOException e) {

        }
    }
}
// thread sending messages coming from the console to the server
public class WriteMsg extends Thread {

    @Override
    public void run() {
        while (true) {
            String userWord;
            try {
               userWord = inputUser.readLine(); // messages from the console
                if (userWord.equals("stop")) {
                    out.write("stop" + "\n");
                    break; // exit the loop if it's "stop"
                } else {
                    out.write(userWord + "\n"); // send to the server
                }
                out.flush(); // clean up
            } catch (IOException e) {

            }

        }
    }
}
在客戶端建構函數中,您只需要啟動這些執行緒。如果客戶要離開,如何正確關閉資源?我需要關閉伺服器執行緒資源嗎?為此,您很可能需要建立一個單獨的方法,在退出訊息循環時呼叫該方法。在那裡您需要關閉套接字和 I/O 流。特定客戶端的相同會話結束訊號必須傳送到其伺服器線程,該線程必須對其套接字執行相同的操作,並將其自身從主伺服器類別的線程列表中刪除。 第八點:完美無極限 您可以不斷發明新功能來改進您的專案。但是到底該向新連線的客戶端傳輸什麼內容呢?我認為最後十件事發生在他到來之前。為此,您需要建立一個類,其中任何伺服器線程的最後一個操作都將輸入到聲明的列表中,如果列表已經滿(即已經有 10 個),則刪除第一個並添加最後一個來的。為了讓新連線能夠接收到該清單的內容,在建立伺服器執行緒時,需要在輸出流中將它們傳送到客戶端。怎麼做?例如,像這樣:
public void printStory(BufferedWriter writer) {
// ...
}
伺服器執行緒已經建立了流,並且可以將輸出流作為參數傳遞。接下來,您只需在一個搜尋週期中將需要傳輸的所有內容傳遞給新客戶端即可。 結論: 這只是基礎知識,在創建真正的應用程式時,這種聊天架構很可能不起作用。該程式是出於教育目的而創建的,在其基礎上,我展示瞭如何使客戶端與伺服器通訊(反之亦然),如何對多個連接執行此操作,當然,還有如何在套接字上組織它。下面重新整理了原始碼,並附上了正在分析的程式的原始程式碼。這是我第一次寫文章)感謝您的關注:)
  1. 《Java Enterprise 中的思考》,作者:Bruce Eckel 等人。阿爾。2003年
  2. Java 8,完整指南,Herbert Schildt,第 9 版,2017 年(第 22 章)
  3. Java 中的套接字程式設計有關套接字的文章
  4. 官方文件中的socket
  5. 官方文件中的ServerSocket
  6. GitHub 上的資源
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION