1. 概要

ソケットプログラミングという用語は、デバイスがすべてネットワークを使用して相互に接続されている複数のコンピューター間で実行されるプログラムを作成することを指します。

ソケットプログラミングに使用できる通信プロトコルには、ユーザーデータグラムプロトコル(UDP)と転送制御プロトコル(TCP)の2つがあります。

2つの主な違いは、UDPはコネクションレスであり、クライアントとサーバーの間にセッションがないことを意味しますが、TCPはコネクション型であり、通信を行うには、最初にクライアントとサーバーの間に排他的接続を確立する必要があります。 。

このチュートリアルでは、 TCP / IP ネットワークを介したソケットプログラミングの概要を示し、Javaでクライアント/サーバーアプリケーションを作成する方法を示します。 UDPは主流のプロトコルではないため、頻繁に使用されることはありません。

2. プロジェクトの設定

Javaは、クライアントとサーバー間の低レベルの通信の詳細を処理するクラスとインターフェースのコレクションを提供します。

これらは主にjava.netパッケージに含まれているため、次のインポートを行う必要があります。

import java.net.*;

また、 java.io パッケージも必要です。これにより、通信中に書き込みおよび読み取りを行うための入力ストリームと出力ストリームが提供されます。

import java.io.*;

簡単にするために、クライアントプログラムとサーバープログラムを同じコンピューターで実行します。 異なるネットワークコンピュータでそれらを実行する場合、変更されるのはIPアドレスだけです。 この場合、127.0.0.1localhostを使用します。

3. 簡単な例

クライアントとサーバーを含む最も基本的な例で手を汚しましょう。 これは、クライアントがサーバーに挨拶し、サーバーが応答する双方向通信アプリケーションになります。

次のコードを使用して、GreetServer。javaというクラスでサーバーアプリケーションを作成します。

main メソッドとグローバル変数を含めて、この記事ですべてのサーバーを実行する方法に注意を向けます。 この記事の残りの例では、この種の反復コードを省略します。

public class GreetServer {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        String greeting = in.readLine();
            if ("hello server".equals(greeting)) {
                out.println("hello client");
            }
            else {
                out.println("unrecognised greeting");
            }
    }

    public void stop() {
        in.close();
        out.close();
        clientSocket.close();
        serverSocket.close();
    }
    public static void main(String[] args) {
        GreetServer server=new GreetServer();
        server.start(6666);
    }
}

また、次のコードを使用してGreetClient。javaというクライアントを作成します。

public class GreetClient {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port) {
        clientSocket = new Socket(ip, port);
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    }

    public String sendMessage(String msg) {
        out.println(msg);
        String resp = in.readLine();
        return resp;
    }

    public void stopConnection() {
        in.close();
        out.close();
        clientSocket.close();
    }
}

サーバーを起動しましょう。IDEでは、Javaアプリケーションとして実行するだけでこれを実行できます。

次に、単体テストを使用してサーバーに挨拶を送信します。これにより、サーバーが応答として挨拶を送信することが確認されます。

@Test
public void givenGreetingClient_whenServerRespondsWhenStarted_thenCorrect() {
    GreetClient client = new GreetClient();
    client.startConnection("127.0.0.1", 6666);
    String response = client.sendMessage("hello server");
    assertEquals("hello client", response);
}

この例は、この記事の後半で何を期待するかについての感触を与えてくれます。 そのため、ここで何が起こっているのかまだ完全には理解していない可能性があります。

次のセクションでは、この簡単な例を使用してソケット通信を分析し、さらに複雑な例についても詳しく説明します。

4. ソケットのしくみ

上記の例を使用して、このセクションのさまざまな部分をステップスルーします。

定義上、 socket は、ネットワーク上の異なるコンピューターで実行されている2つのプログラム間の双方向通信リンクの1つのエンドポイントです。 ソケットはポート番号にバインドされているため、トランスポート層はデータの送信先のアプリケーションを識別できます。

4.1. サーバー

通常、サーバーはネットワーク上の特定のコンピューターで実行され、特定のポート番号にバインドされたソケットを備えています。 この例では、クライアントと同じコンピューターを使用し、ポート6666でサーバーを起動します。

ServerSocket serverSocket = new ServerSocket(6666);

サーバーは、クライアントが接続要求を行うためのソケットをリッスンして待機します。 これは次のステップで発生します。

Socket clientSocket = serverSocket.accept();

サーバーコードがacceptメソッドに遭遇すると、クライアントがサーバーコードに接続要求を行うまでブロックします。

すべてがうまくいけば、サーバーは接続を受け入れします。 受け入れられると、サーバーは新しいソケット clientSocket を取得し、同じローカルポート 6666 にバインドし、リモートエンドポイントをクライアントのアドレスとポートに設定します。

この時点で、新しい Socket オブジェクトにより、サーバーはクライアントと直接接続されます。 次に、出力ストリームと入力ストリームにアクセスして、クライアントとの間でそれぞれメッセージを送受信できます。

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

これで、サーバーは、ソケットがそのストリームで閉じられるまで、クライアントと無限にメッセージを交換することができます。

ただし、この例では、サーバーは接続を閉じる前にのみグリーティング応答を送信できます。 これは、テストを再度実行すると、サーバーが接続を拒否することを意味します。

通信を継続できるようにするには、 while ループ内の入力ストリームから読み取り、クライアントが終了要求を送信したときにのみ終了する必要があります。 次のセクションで、これが実際に動作することを確認します。

新しいクライアントごとに、サーバーはaccept呼び出しによって返される新しいソケットを必要とします。 serverSocket を使用して、接続されたクライアントのニーズに対応しながら、接続要求を引き続きリッスンします。 最初の例では、これをまだ許可していません。

4.2. クライアント

クライアントは、サーバーが実行されているマシンのホスト名またはIP、およびサーバーがリッスンしているポート番号を知っている必要があります。

接続要求を行うために、クライアントはサーバーのマシンとポートでサーバーとのランデブーを試みます。

Socket clientSocket = new Socket("127.0.0.1", 6666);

クライアントはサーバーに対しても自身を識別する必要があるため、この接続中に使用するシステムによって割り当てられたローカルポート番号にバインドします。 私たちはこれを自分たちで扱っていません。

上記のコンストラクターは、サーバーがAccepted接続を持っている場合にのみ新しいソケットを作成します。 そうしないと、接続拒否の例外が発生します。 正常に作成されると、サーバーと通信するためにそこから入力ストリームと出力ストリームを取得できます。

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

サーバーの入力ストリームがクライアントの出力ストリームに接続されているのと同じように、クライアントの入力ストリームはサーバーの出力ストリームに接続されています。

5. 継続的なコミュニケーション

現在のサーバーは、クライアントが接続するまでブロックし、その後、クライアントからのメッセージをリッスンするために再度ブロックします。 単一のメッセージの後、継続性を扱っていないため、接続が閉じられます。

そのため、pingリクエストでのみ役立ちます。 しかし、チャットサーバーを実装したいとします。 サーバーとクライアント間の継続的なやり取りが絶対に必要です。

サーバーの入力ストリームで着信メッセージを継続的に監視するには、whileループを作成する必要があります。

それでは、 EchoServer。java、という新しいサーバーを作成しましょう。このサーバーの唯一の目的は、クライアントから受信したメッセージをエコーバックすることです。

public class EchoServer {
    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        
        String inputLine;
        while ((inputLine = in.readLine()) != null) {
        if (".".equals(inputLine)) {
            out.println("good bye");
            break;
         }
         out.println(inputLine);
    }
}

ピリオド文字を受け取ったときにwhileループが終了する終了条件を追加したことに注意してください。

GreetServer の場合と同様に、mainメソッドを使用してEchoServerを起動します。 今回は、混乱を避けるために、 4444、などの別のポートで開始します。

EchoClientGreetClientに似ているため、コードを複製できます。 わかりやすくするために、それらを分離しています。

別のテストクラスでは、サーバーがソケットを閉じることなく、EchoServerへの複数のリクエストが処理されることを示すテストを作成します。 これは、同じクライアントからリクエストを送信している限り当てはまります。

複数のクライアントを処理することは別のケースであり、これについては次のセクションで説明します。

次に、 setup メソッドを作成して、サーバーとの接続を開始しましょう。

@Before
public void setup() {
    client = new EchoClient();
    client.startConnection("127.0.0.1", 4444);
}

また、 tearDown メソッドを作成して、すべてのリソースを解放します。 これは、ネットワークリソースを使用するすべての場合のベストプラクティスです。

@After
public void tearDown() {
    client.stopConnection();
}

次に、いくつかのリクエストでエコーサーバーをテストします。

@Test
public void givenClient_whenServerEchosMessage_thenCorrect() {
    String resp1 = client.sendMessage("hello");
    String resp2 = client.sendMessage("world");
    String resp3 = client.sendMessage("!");
    String resp4 = client.sendMessage(".");
    
    assertEquals("hello", resp1);
    assertEquals("world", resp2);
    assertEquals("!", resp3);
    assertEquals("good bye", resp4);
}

これは、サーバーが接続を閉じる前に1回だけ通信する最初の例からの改善です。 ここで、セッションが終了したときにサーバーに通知する終了信号を送信します。

6. 複数のクライアントを備えたサーバー

前の例は最初の例よりも改善されていましたが、それでも優れたソリューションではありません。 サーバーには、多くのクライアントと多くの要求を同時に処理する能力が必要です。

このセクションでは、複数のクライアントの処理について説明します。

ここで確認できるもう1つの機能は、サーバーで接続拒否の例外や接続のリセットを発生させることなく、同じクライアントが切断して再接続できることです。 以前はこれを行うことができませんでした。

これは、複数のクライアントからの複数のリクエストに対して、サーバーがより堅牢で復元力があることを意味します。

これを行うには、新しいクライアントごとに新しいソケットを作成し、そのクライアントが別のスレッドで要求するサービスを提供します。 同時に処理されるクライアントの数は、実行中のスレッドの数と同じになります。

メインスレッドは、新しい接続をリッスンするときにwhileループを実行します。

では、これを実際に見てみましょう。 EchoMultiServer。java。という別のサーバーを作成します。その中に、各クライアントのソケットでの通信を管理するためのハンドラースレッドクラスを作成します。

public class EchoMultiServer {
    private ServerSocket serverSocket;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        while (true)
            new EchoClientHandler(serverSocket.accept()).start();
    }

    public void stop() {
        serverSocket.close();
    }

    private static class EchoClientHandler extends Thread {
        private Socket clientSocket;
        private PrintWriter out;
        private BufferedReader in;

        public EchoClientHandler(Socket socket) {
            this.clientSocket = socket;
        }

        public void run() {
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(
              new InputStreamReader(clientSocket.getInputStream()));
            
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                if (".".equals(inputLine)) {
                    out.println("bye");
                    break;
                }
                out.println(inputLine);
            }

            in.close();
            out.close();
            clientSocket.close();
    }
}

whileループ内でacceptを呼び出すことに注意してください。 while ループが実行されるたびに、新しいクライアントが接続するまでaccept呼び出しをブロックします。 次に、このクライアント用のハンドラースレッドEchoClientHandlerが作成されます。

スレッド内で発生することは、単一のクライアントのみを処理した EchoServer、と同じです。 EchoMultiServer は、この作業を EchoClientHandler に委任して、whileループでより多くのクライアントをリッスンし続けることができるようにします。

サーバーのテストには引き続きEchoClientを使用します。 今回は、サーバーとの間で複数のメッセージを送受信する複数のクライアントを作成します。

ポート5555でメインメソッドを使用してサーバーを起動しましょう。

わかりやすくするために、新しいスイートにテストを追加します。

@Test
public void givenClient1_whenServerResponds_thenCorrect() {
    EchoClient client1 = new EchoClient();
    client1.startConnection("127.0.0.1", 5555);
    String msg1 = client1.sendMessage("hello");
    String msg2 = client1.sendMessage("world");
    String terminate = client1.sendMessage(".");
    
    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

@Test
public void givenClient2_whenServerResponds_thenCorrect() {
    EchoClient client2 = new EchoClient();
    client2.startConnection("127.0.0.1", 5555);
    String msg1 = client2.sendMessage("hello");
    String msg2 = client2.sendMessage("world");
    String terminate = client2.sendMessage(".");
    
    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

これらのテストケースをいくつでも作成でき、それぞれが新しいクライアントを生成し、サーバーがそれらすべてにサービスを提供します。

7. 結論

この記事では、 TCP / IPを介したソケットプログラミングの概要に焦点を当て、Javaで簡単なクライアント/サーバーアプリケーションを作成しました。

この記事の完全なソースコードは、GitHubプロジェクトにあります。