1概要


ソケット

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

ソケットプログラミングに使用できる通信プロトコルは2つあります。

ユーザーデータグラムプロトコル(UDP)と転送制御プロトコル(TCP)

です。

この2つの主な違いは、UDPはコネクションレスであるということです。つまり、TCPはコネクション型ですが、クライアントとサーバー間でセッションが確立されないということです。

このチュートリアルでは、TCP/IPネットワーク上でのソケットプログラミングの概要と、クライアント/サーバーアプリケーションをJavaで書く方法を説明します。 UDPは主流のプロトコルではないので、それほど頻繁には遭遇しないかもしれません。


2プロジェクト設定

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

これらはほとんど

java.net

パッケージに含まれているので、以下のインポートを行う必要があります。

import java.net.** ;

通信中に読み書きするための入出力ストリームを提供する

java.io

パッケージも必要です。

import java.io.** ;

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

127.0.0.1



localhost

を使用します。


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);

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

Socket clientSocket = serverSocket.accept();

サーバーコードが

accept

メソッドを見つけると、クライアントが接続要求を出すまでブロックされます。

すべてうまくいけば、サーバーは接続を受け入れます。受け入れられると、サーバーは同じローカルポート

6666

にバインドされた新しいソケット

clientSocket

を取得し、そのリモートエンドポイントもクライアントのアドレスとポートに設定されます。

この時点で、新しい

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);

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

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

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

などの別のポートで起動します。


EchoClient



GreetClient

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

別のテストクラスでは、

EchoServer

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

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

サーバーとの接続を開始する

setup

メソッドを作成しましょう。

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

すべてのリソースを解放するために

tearDown

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

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

それでは、いくつかのリクエストでechoサーバーをテストしましょう。

@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);
}

これは、サーバーが接続を閉じる前に一度だけ通信するという最初の例を改良したものです。

セッションが終了したときにサーバーに通知するための終了信号を送信します

** 6. 複数のクライアントを持つサーバー

**

前の例が最初の例より改善されたように、まだそれほどすばらしい解決策ではありません。サーバーには、多数のクライアントと多数の要求を同時に処理する能力が必要です。

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

ここで見るもう一つの特徴は、同じクライアントが接続を拒否された例外やサーバー上で接続がリセットされることなく、再度切断して再接続できることです。以前はこれができませんでした。

つまり、私たちのサーバーは、複数のクライアントからの複数のリクエストに対して、より堅牢で回復力のあるものになります。

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

メインスレッドは新しい接続を待機するため、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

のmainメソッドを使用してサーバーを起動しましょう。

明確にするために、テストを新しいスイートに入れます。

@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で簡単なクライアント/サーバーアプリケーションを作成しました。

記事の完全なソースコードは、いつもどおりhttps://github.com/eugenp/tutorials/tree/master/core-java[GitHub]プロジェクトにあります。