JavaRush /Java Blog /Random-JA /Socket クラスと ServerSocket クラス、または「こんにちは、サーバー?」聞こえますか?"
Sergey Simonov
レベル 36
Санкт-Петербург

Socket クラスと ServerSocket クラス、または「こんにちは、サーバー?」聞こえますか?"

Random-JA グループに公開済み
はじめに: 「テーブルの上にコンピューターがあり、その後ろにエンコーダーがありました...」 Socket クラスと ServerSocket クラス、または「こんにちは、サーバー?」 聞こえますか?"  - 1昔、クラスメートの 1 人が Java の学習結果を、新しいプログラムのスクリーンショットの形で投稿しました。このプログラムはマルチユーザー チャットでした。当時、私はこの言語でプログラミングをマスターするという自分自身の旅を始めたばかりでしたが、「それが欲しい!」とはっきりと自分に言い聞かせました。時が経ち、プログラミングの知識を深めるために次のプロジェクトに取り組み終えた頃、私はあの出来事を思い出し、そろそろ潮時だと判断しました。どういうわけか、私はすでに純粋な好奇心からこのトピックを掘り下げ始めていましたが、私のメインの Java 教科書 (シルトの完全なマニュアルでした) では、java.net パッケージについては 20 ページしか提供されていませんでした。これは当然のことですが、この本はすでに非常に膨大です。メインクラスのメソッドとコンストラクターの表がありましたが、それだけです。次のステップは、もちろん、全能の Google です。同じ内容が紹介されている無数のさまざまな記事 (ソケットに関する 2 ~ 3 語と、既成の例) です。古典的なアプローチ (少なくとも私の学習スタイルでは) は、まず仕事に必要なツール、それらが何であるか、なぜそれらが必要なのかを理解し、その後、問題の解決策が明らかでない場合にのみ、次のツールを選択することです。既製のリストを確認し、ナットとボルトを緩めます。しかし、私はそれが何なのかを理解し、最終的にマルチユーザー チャットを作成しました。表面的には、次のような結果になりました。 Socket クラスと ServerSocket クラス、つまり「こんにちは、サーバー?」 聞こえますか?"  - 2ここでは、チャット設計の例を使用して、Java ソケットに基づくクライアント/サーバー アプリケーションの基本を理解してもらいたいと思います。Javarash コースではチャットを行います。それはまったく異なるレベルにあり、美しく、大きく、多機能になります。しかし、まず第一に、常に基礎を築く必要があるため、ここではそのようなセクションの根底にあるものを理解する必要があります。(不足点やエラーを見つけた場合は、PM または記事の下のコメントに書いてください)。さぁ、始めよう。 先頭 1: 「... という家」 サーバーと 1 つのクライアントの間でネットワーク接続がどのように行われるかを説明するために、今や古典的な集合住宅の例を取り上げましょう。クライアントが何らかの方法で特定のサーバーとの接続を確立する必要があるとします。検索者は検索オブジェクトについて何を知っておく必要がありますか? そう、住所です。サーバーはクラウド上の魔法のような存在ではないため、特定のマシン上に配置する必要があります。家に例えると、合意された 2 つの当事者による会議が開催される場所です。また、アパートの建物内でお互いを見つけるには、建物の 1 つの住所だけでは十分ではなく、会議が行われるアパートの番号を指定する必要があります。同様に、1 台のコンピュータ上に同時に複数のサーバーが存在する可能性があり、クライアントが特定のサーバーに接続するには、接続に使用するポート番号も指定する必要があります。つまり、アドレスとポート番号です。 アドレスとは、インターネット空間におけるマシンの識別子を意味します。これは、 「javarush.ru」などのドメイン名または通常の IP です。ポート- 特定のソケットに関連付けられている固有の番号 (この用語については後で説明します)。言い換えると、特定のサービスに接続できるように、特定のサービスによって占有されます。したがって、少なくとも 2 つのオブジェクトが一方 (サーバー) の領域で出会うためには、そのエリア (サーバー) の所有者はその領域 (車) 上の特定のアパート (ポート) を占有する必要があり、もう一方のオブジェクトは次のことを知って集合場所を見つけなければなりません。家の住所 (ドメインまたは IP )、およびアパート番号 (ポート)。 ヘッド 2: Socket について ネットワークでの作業に関連する概念と用語の中で、非常に重要なものの 1 つは Socket です。これは、接続が発生するポイントを示します。簡単に言えば、ソケットはネットワーク上の 2 つのプログラムを接続します。このクラスは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。2 番目のコンストラクターのアドレス型は であることに注意してくださいInetAddress。たとえば、アドレスとしてドメイン名を指定する必要がある場合に役立ちます。また、ドメインが複数の IP アドレスを意味する場合、InetAddressそれらを使用してそれらの配列を取得できます。ただし、IP でも機能します。ホスト名やIPアドレスを構成するバイト配列なども取得できます。これについてはもう少し詳しく説明しますが、詳細については公式ドキュメントを参照する必要があります。タイプのオブジェクトが初期化されるとSocket、そのオブジェクトが属するクライアントは、特定のアドレスとポート番号でサーバーに接続することをネットワーク上でアナウンスします。以下は、クラスの最も頻繁に使用されるメソッドですSocketInetAddress getInetAddress()– ソケットに関するデータを含むオブジェクトを返します。ソケットが接続されていない場合 (null int getPort())、サーバーへの接続が行われるポートを返します int getLocalPort()。ソケットがバインドされているポートを返します。実際、クライアントとサーバーは 1 つのポートで「通信」できますが、バインドされているポートは完全に異なる場合があります boolean isConnected()- 接続が確立されている場合は true を返します void connect(SocketAddress address)- 新しい接続を示します boolean isClosed()- ソケットが閉じている場合は true を返します boolean isBound()- ソケットが実際にアドレスにバインドされている場合、true を返します。クラスはSocketインターフェイスを実装しているAutoCloseableため、インターフェイスで使用できますtry-with-resources。ただし、close() を使用する従来の方法でソケットを閉じることもできます。 ヘッド 3: これは ServerSocket です。Socketクライアント側で接続リクエストを class の形式で宣言したとします。サーバーは私たちの願いをどのように推測するのでしょうか? このために、サーバーには のようなクラスがあり ServerSocket、その中に accept() メソッドがあります。そのコンストラクターを以下に示します。
ServerSocket() throws IOException
ServerSocket(int порт) throws IOException
ServerSocket(int порт, int максимум_подключений) throws IOException
ServerSocket(int порт, int максимум_подключений, InetAddress локальный_address) throws IOException
宣言時にはServerSocket サーバーマシン上で通信が行われるため、接続アドレスを指定する必要はありません。マルチチャネル ホストの場合のみ、サーバー ソケットがバインドされる IP を指定する必要があります。 ヘッド 3.1: ノーと答えるサーバー 必要以上のリソースをプログラムに提供することはコストがかかり不合理であるため、コンストラクターでは、ServerSocket動作中にサーバーが受け入れる最大接続数を宣言するように求められます。指定しない場合、デフォルトでは、この数値は 50 に等しいとみなされます。はい、理論上、ServerSocketこれはサーバー専用の同じソケットであると想定できます。しかし、それは class とはまったく異なる役割を果たしますSocket。これは接続作成段階でのみ必要です。タイプ オブジェクトを作成したら、 ServerSocket誰かがサーバーへの接続を希望していることを確認する必要があります。accept() メソッドがここに接続されています。ターゲットは、誰かが接続を希望するまで待機し、これが発生すると、タイプ のオブジェクトSocket、つまり再作成されたクライアント ソケットを返します。クライアント ソケットがサーバー側で作成されたので、双方向通信を開始できます。接続には、クライアント側でタイプ オブジェクトを作成しSocket、サーバー側を使用してそれを再作成することServerSocketが最低限必要です。 第 4 章: サンタクロースへの手紙 Вопрос:クライアントとサーバーは正確にどのように通信するのでしょうか? Ответ:I/O ストリーム経由。私たちがすでに持っているものは何でしょうか?サーバー アドレスとクライアントのポート番号を含むソケット。accept() のおかげで、サーバー側でも同じことが行われます。したがって、ソケットを介して通信すると想定するのが合理的です。これを行うには、型のストリームInputStreamとオブジェクトへのアクセスを提供する 2 つのメソッドがあります。どうぞ: OutputStreamSocket
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 をいつでも使用でき、2 番目の引数として 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());
第 5 章: インターネットを介した 実際の通信 実際の IP アドレスを使用して実際のネットワークを介して接続するには、本格的なサーバーが必要です。
  1. 私たちの将来のチャットには、ユーティリティとしてそのような機能はありません。接続の確立とメッセージの送受信のみが可能です。つまり、実際のサーバー機能はありません。
  2. ソケット データと I/O ストリームのみを含むサーバーは、実際の WEB サーバーや FTP サーバーとして機能することができず、これだけではインターネット経由で接続することはできません。
また、プログラムは開発を始めたばかりなので、実際のネットワークですぐに動作できるほど安定していないため、接続アドレスとしてローカル ホストを使用します。つまり、理論的には、クライアントとサーバーはソケットを介する以外は接続されませんが、プログラムのデバッグの場合は、ネットワーク経由で実際に接続することなく、同じマシン上に存在します。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」を使用します。ただし、他のすべてのオプションも実行可能です。 ヘッド 6: 会話の時間です。 つまり、サーバーとの会話セッションを実装するために必要なものはすべてすでに揃っています。残っているのは、それをまとめるだけです。次のリストは、クライアントがどのようにサーバーに接続し、1 つのメッセージを送信し、サーバーがそのメッセージを引数として使用してメッセージを受信したかを確認する方法を示しています。ジャワ」
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);
        }

    }
}
もちろん、最初にサーバーを起動する必要があります。接続するものが存在しない場合、クライアントは起動時に何に接続するのでしょうか。:) 出力は次のようになります: /* 何か言いたいことがありましたか? ここに入力してください: こんにちは、サーバーですか? 聞こえますか?こんにちは、サーバーです!確認しました、あなたは次のように書きました: こんにちは、サーバーですか? 聞こえますか?クライアントはクローズされました... */ 万歳!サーバーにクライアントと通信する方法を教えました。通信が 2 つのレプリカで発生するのではなく、必要な数だけ発生するように、スレッドの読み取りと書き込みを while (true) ループでラップし、特定のメッセージに従って終了を指示します (たとえば、「exit」)。 、サイクルが中断され、プログラムは終了します。 ヘッド 7: マルチユーザーの方が良いです。 サーバーが私たちの声を聞くことができるという事実は良いことですが、私たちと同じ種類の誰かと通信できればもっと良いでしょう。すべてのソースは記事の最後に添付しますので、ここでは、必ずしも大きくはありませんが、正しく使用すればマルチユーザー チャットをでっち上げることを可能にする重要なコードを示します。したがって、サーバーを介して他のクライアントと通信できるようにしたいと考えています。どうやってするの?明らかに、クライアント プログラムには独自のメソッドがあるため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 スレッド、およびコンストラクターから直接スレッドを開始するために必要なものすべても含まれます。さて、しかし、サーバー スレッドがクライアントからのメッセージを読み取るとどうなるでしょうか? クライアントにのみ返送しますか? あまり効果的ではありません。マルチユーザー チャットを作成しているため、接続されている各クライアントが 1 人が書いた内容を受信できるようにする必要があります。クライアントに関連付けられたすべてのサーバー スレッドのリストを使用し、特定のスレッドに送信される各メッセージをクライアントに送信できるようにする必要があります。
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) {}
}
これで、クライアント全員がそのうちの 1 人が言ったことを知ることになります。メッセージを送信者に送信したくない場合 (送信者は自分が何を書いたかをすでに知っています!)、単にスレッドを反復処理するときに、オブジェクトを処理するときにループが実行せず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 ストリームを閉じる必要があります。特定のクライアントに対する同じセッション終了信号をサーバー スレッドに送信する必要があります。サーバー スレッドはそのソケットに対して同じことを行い、メイン サーバー クラスのスレッドのリストから自身を削除する必要があります。 ヘッド 8: 完璧さには制限がありません。 プロジェクトを改善するための新しい機能を際限なく発明できます。しかし、新しく接続されたクライアントに正確に何を転送する必要があるのでしょうか? 最後の 10 件の出来事は、彼が到着する前に起こったと思います。これを行うには、任意のサーバー スレッドでの最後のアクションが宣言されたリストに入力されるクラスを作成する必要があります。リストがすでにいっぱいである (つまり、すでに 10 個ある) 場合は、最初のアクションを削除して追加します。最後に来たもの。このリストの内容を新しい接続で受信するには、サーバー スレッドの作成時に出力ストリームでそれらをクライアントに送信する必要があります。どうやってするの?たとえば、次のようになります。
public void printStory(BufferedWriter writer) {
// ...
}
サーバー スレッドはすでにストリームを作成しているため、出力ストリームを引数として渡すことができます。次に、検索サイクルで新しいクライアントに転送する必要があるものをすべて渡すだけです。 結論: これは単なる基本であり、実際のアプリケーションを作成する場合、このチャット アーキテクチャは機能しない可能性が高くなります。このプログラムは教育目的で作成されており、これに基づいて、クライアントとサーバー間の通信 (またはその逆) を行う方法、複数の接続でこれを行う方法、そしてもちろん、これがソケット上でどのように構成されるかを示しました。以下にソースを整理し、解析中のプログラムのソースコードも添付します。記事を書くのは初めての経験です) ご清聴ありがとうございます:)
  1. 「Java Enterprise で考える」、Bruce Eckel 他著 アル。2003年
  2. Java 8、完全ガイド、Herbert Schildt、第 9 版、2017 (第 22 章)
  3. Java でのソケットプログラミングのソケットに関する記事
  4. 公式ドキュメントのソケット
  5. 公式ドキュメントのServerSocket
  6. GitHub のソース
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION