最終更新日:2021/01/30 原本2017-

ネットワークプログラミング

コンピュータの創世記からそのリソースを有効利用するために,Client/Server(C/S)モデルが広く利用されてきました. 計算などを行うメインフレームと呼ばれる大型計算機をサーバと,それを操作するための表示などを行う端末をクライアントするC/Sシステムから始まり,現在の様々なコンピュータシステムにおいてこの概念は利用されています. たとえ単一のコンピュータであったとしても内部的にC/Sモデルを採用しているアプリケーションはたくさんあります. 他のモデルとしてはPeer to Peer (P2P)モデルがあります. P2Pではサーバ,クライアントといった明確な役割を持たずに相互の情報交換を行います.
ここでは,C/Sモデルに基づくネットワークアプリケーションをJavaで実現していきましょう. コンピュータとコンピュータが通信するためには何らかの約束事(プロトコル)が必要です. ここでは,既に世の中で広く利用されているTCP/IPを利用してアプリケーションを作成します. TCP/IPはUNIXを始め,様々なOSで標準的に利用できます. また,Javaはネットワークに対応しており,様々な有用なフレームワークなどが利用でき,更に様々なOS上でも同様に動作出来ます.
Javaでネットワークプログラムを行うための重要な要素としてソケットがあります. ソケットはもともとUNIXのために考案され,アプリケーションとTCP/IPを接続するインタフェースの役割をはたします. このソケットを利用することで,JavaプログラマはTCP/IPにおけるパケットの低レベルな処理を意識せずにネットワークアプリケーションの作成に専念できます. このソケットは最初はC言語で実装されましたが,Javaだけでなく様々な言語でのネットワークインタフェースの標準仕様になっています.

TCP/IPで通信する際に,その通信を識別するためには以下の5つが必要です.
  1. 宛先IPアドレス(例:192.168.11.10)
  2. 送信元IPアドレス
  3. 宛先ポート番号(例:80)
  4. 送信元ポート番号
  5. プロトコル番号(例:TCPなら6,UDPなら17)
ポート番号は0~65535まで利用できますが,0~1023まではよく使うポート番号(well-known port numberと呼ばれてています)なでの,自作のアプリケーションでは使わないようにしましょう. 1024~49151もIANAによって割り当てられていますが,他の用途で用いても多くの場合問題ありません. C/Sモデルにおいて,ポート番号をアプリケーションで確定しなければならないのはサーバ側だけです. サーバはクライアントからの接続を常に待ち続けなければならないので,ポート番号を変える事はできず,固定しておかなければなりません. これに対して,クライアント側のポート番号は必ずしも決まっている必要はありません. クライアントのポート番号を指定する事も可能ですが,多くの場合,OSに割り当てを任せて,自動的に設定します. この時,OSは49151~65535のいずれかの空いているポート番号を選択します.

エコーシステムの作成

ここでは,C/Sモデルの例として下図のようなエコーシステムを作成してみましょう.

ここでのエコーシステムとはクライアントで入力した文字列がサーバに送信され,サーバは受信された文字列をそのままクライアントに送信するシステムとします. 上図の左がサーバ(EchoServer),右がクライアント(EchoClient)です. このエコーシステムではTCPを用いて実現することにします. JavaでTCPアプリケーションを作成するためにはServerSocketクラスSocketクラスを用います. Socketクラスは,サーバとクライアントそれぞれのアプリケーションとTCP/IPを接続する端子として機能します. なので,Socketクラスはサーバでも,クライアントでも利用されます. それに対して,ServerSocketクラスはその名のとおり,サーバだけで利用されます. ServerSocketクラスの主な仕事はクライアントからの接続を待ち続け,接続要求があった時にSocketクラスのインスタンスを返して,双方のソケットの接続を完了することです. サーバとクライアント双方でのソケットの接続完了までの手続きは下図の通りです.

これはTCP接続における3ウェイシェイクハンドに相当します. その後,TCPコネクション上でデータのやり取りを行います. JavaのSocketクラスではInputStreamクラスOutputStreamクラスのバイト入出力ストリームが利用できます. 最後に双方でソケットをcloseして終了しなければなりません. SocketクラスはAutoClosableインタフェースを実装しているので,try-with-resources文が利用できます.
下記のEchoServer.javaとEchoClienet.javaをコピーてみましょう.

EchoServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package jp.ac.utsunomiya_u.is;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
 
public class EchoServer {
 
    public static void main(String[] args) {
        // Scannerクラスのインスタンス(標準入力System.inからの入力)
        try (Scanner scanner = new Scanner(System.in)) {
            System.out.print("EchoServer (" + getMyIpAddress() + ") > Input server port > ");
            // ポート番号入力
            int port = scanner.nextInt();
            // ServerSocketクラスのインスタンスをポート番号を指定して生成
            try (ServerSocket serverSocket = new ServerSocket(port)) {
                System.out.println("EchoServer (" + getMyIpAddress() + ") > Started and Listening for connections on port " + serverSocket.getLocalPort());
                // EchoTaskクラスのインスタンスをServerSocketクラスのacceptメソッドで返されるSocketを元に生成
                EchoTask echoTask = new EchoTask(serverSocket.accept());
                // データ送受信
                echoTask.call();
            } catch (IOException ex) {
                Logger.getLogger(EchoServer.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
 
    /**
     * 自ホストのIPアドレス取得
     *
     * @return 自ホストのIPアドレス
     */
    private static String getMyIpAddress() {
        try {
            // 自ホストのIPアドレス取得
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException ex) {
            Logger.getLogger(EchoServer.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }
 
    /**
     * EchoServerでStringの送受信を担うクラス
     */
    private static class EchoTask {
 
        /**
         * Socketクラスのインスタンス
         */
        private final Socket socket;
 
        /**
         * コンストラクタ
         *
         * @param socket Socketクラスのインスタンス
         */
        EchoTask(Socket socket) {
            this.socket = socket;
        }
 
        public Void call() {
            System.out.println("EchoServer (" + getMyIpAddress() + ") > Accepted connection from " + socket.getRemoteSocketAddress());
            // クライアントからの読み込み用のBufferedReaderクラスのインスタンスをSocketのInputStreamから生成
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    // クライアントへの書き込み用のPrintWriterクラスのインスタンスをSocketのOutputStreamから生成(自動フラッシュ)
                    PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {
                String inputLine;
                // クライアントからの入力待機し,それを受け取ったらStringに収納
                while ((inputLine = reader.readLine()) != null) {
                    System.out.println("EchoClient (" + socket.getRemoteSocketAddress() + ") > " + inputLine);
                    // 受け取ったStringをそのままクライアントへ書き込み
                    writer.println(inputLine);
                }
            } catch (IOException ex) {
                Logger.getLogger(EchoServer.class.getName()).log(Level.SEVERE, null, ex);
            } finally {
                System.out.println("EchoServer (" + getMyIpAddress() + ") > Terminated connection from " + socket.getRemoteSocketAddress());
                try {
                    // ソケットのクローズ処理
                    socket.close();
                } catch (IOException ex) {
                    Logger.getLogger(EchoServer.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            return null;
        }
    }
}

EchoClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package jp.ac.utsunomiya_u.is;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
 
public class EchoClient {
 
    public static void main(String[] args) {
        String myIpAddress = null;
        try {
            // 自ホストのIPアドレス取得
            myIpAddress = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException ex) {
            Logger.getLogger(EchoClient.class.getName()).log(Level.SEVERE, null, ex);
        }
 
        // Scannerクラスのインスタンス(標準入力System.inからの入力)
        try (Scanner scanner = new Scanner(System.in)) {
            System.out.print("EchoClient (" + myIpAddress + ") > Input IP address of EchoServer > ");
            // サーバのIPアドレスを標準入力から設定
            InetAddress serverAddress = InetAddress.getByName(scanner.next());
            System.out.print("EchoClient (" + myIpAddress + ") > Input port number of EchoServer > ");
            // サーバのポート番号を標準入力から設定
            int serverPort = scanner.nextInt();
 
            // SocketクラスのインスタンスをサーバのIPアドレスとポート番号から生成
            try (Socket socket = new Socket(serverAddress, serverPort);
                    // サーバからの読み込み用のBufferedReaderクラスのインスタンスをSocketのInputStreamから生成
                    BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    // サーバへの書き込み用のPrintWriterクラスのインスタンスをSocketのOutputStreamから生成(自動フラッシュ)                     
                    PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {
                System.out.print("EchoClient (" + myIpAddress + ") > ");
                // 入力が存在する限りループ
                while (scanner.hasNext()) {
                    // Scannerからトークンを読み込み
                    String str = scanner.next();
                    if (str.equals("quit")) {
                        break;
                    }
                    // 読み込んだStringをSocketに送信
                    writer.println(str);
                    System.out.println("EchoServer (" + serverAddress.getHostAddress() + ") > " + reader.readLine());
                    System.out.print("EchoClient (" + myIpAddress + ") > ");
                }
            } catch (IOException ex) {
                Logger.getLogger(EchoClient.class.getName()).log(Level.SEVERE, null, ex);
            }
        } catch (UnknownHostException ex) {
            Logger.getLogger(EchoClient.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}
ここでは,EchoServerとEchoClientの両方を同じホストで実行する場合について説明します. この時,サーバのIPアドレスは, のいずれかで良いです. これはループバックアドレスと呼ばれ,ホスト内でTCP/IPを利用するための特別なアドレスです. ホストに割り当てられているIPアドレスはコマンドプロンプトで"ipconfig"というコマンドで簡単に調べる事ができます. "ipconfig /all"で詳細な情報を見ることができます.
C:\Users\fujii>ipconfig

Windows IP 構成


イーサネット アダプター イーサネット:

   接続固有の DNS サフィックス . . . . .:
   リンクローカル IPv6 アドレス. . . . .: 
   IPv4 アドレス . . . . . . . . . . . .: 192.168.11.100
   サブネット マスク . . . . . . . . . .: 255.255.255.0
   デフォルト ゲートウェイ . . . . . . .: 192.168.11.1            
        
今,自分が使用しているPCに割り当てられているIPアドレスを調べてみましょう.
Eclipseでウィンドウ設定の初期化を行うには,"Window"->"Perspective"->"Reset Perspective..."を選択してください.
下の例でのIPアドレスは,一例なので実際の環境によってアドレスは変わります.
番号Screen説明EchoServerEchoClient
1 "Package Explorer"で"EchoServer"プロジェクトにフォーカスが当たっている事を確認した上で,"Run"->"Run"でEchoServerを起動します
2 "Console"ウィンドウでポート番号(ここでは"10000")を入力します
EchoServer (192.168.11.100) > Input server port > 10000
EchoServer (192.168.11.100) > Started and Listening for connections on port 10000
3 "Package Explorer"で"EchoClient"プロジェクトにフォーカスが当たっている事を確認した上で,"Run"->"Run"でEchoClientを起動します
4 "Console"ウィンドウでサーバのIPアドレス(ここでは"localhost")を入力します
EchoClient (192.168.11.100) > Input IP address of EchoServer > localhost
5 "Console"ウィンドウでサーバのポート番号(ここでは"10000")を入力します.この例ではクライアント側のポート番号はOSによって55208が選ばれました
EchoServer (192.168.11.100) > Input server port > 10000
EchoServer (192.168.11.100) > Started and Listening for connections on port 10000
EchoServer (192.168.11.100) > Accepted connection from /127.0.0.1:55208
EchoClient (192.168.11.100) > Input IP address of EchoServer > localhost
EchoClient (192.168.11.100) > Input port number of EchoServer > 10000
EchoClient (192.168.11.100) > 
6 2つの"Console"ウィンドウの切り替えは”Display Selected Console”で行えます
7 EchoClientからメッセージ(ここでは"Hello")を送信すると,EchoServerからすぐさま同じメッセージが返答されます
EchoClient (192.168.11.100) > Input IP address of EchoServer > localhost
EchoClient (192.168.11.100) > Input port number of EchoServer > 10000
EchoClient (192.168.11.100) > Hello
EchoServer (127.0.0.1) > Hello
EchoClient (192.168.11.100) > 
8 Consoleウィンドウを切り替えて,EchoServerでもメッセージを確認してください
EchoServer (192.168.11.100) > Input server port > 10000
EchoServer (192.168.11.100) > Started and Listening for connections on port 10000
EchoServer (192.168.11.100) > Accepted connection from /127.0.0.1:55208
EchoClient (/127.0.0.1:55208) > Hello
9 EchoClientのConsoleウィンドウで"quit"と入力して終了します."Terminate"フラグ(赤い四角)が消灯することを確認します
EchoClient (192.168.11.100) > Input IP address of EchoServer > localhost
EchoClient (192.168.11.100) > Input port number of EchoServer > 10000
EchoClient (192.168.11.100) > Hello
EchoServer (127.0.0.1) > Hello
EchoClient (192.168.11.100) > quit
10 EchoServerのConsoleウィンドウに切り替えて,"Terminate"フラグ(赤い四角)が消灯することを確認します
EchoServer (192.168.11.100) > Input server port > 10000
EchoServer (192.168.11.100) > Started and Listening for connections on port 10000
EchoServer (192.168.11.100) > Accepted connection from /127.0.0.1:55823
EchoClient (/127.0.0.1:55823) > Hello
EchoServer (192.168.11.100) > Terminated connection from /127.0.0.1:55823
それではいよいよ異なるホストで通信してみましょう.
  1. 近くの席の人と2人以上のグループを作って下さい
  2. 各グループの中でサーバ役の人を一人決定して下さい
  3. サーバ役でない人の中からクライアント役の人を一人決定してください
  4. サーバ役の人がポート番号10000でEchoServerを立ち上げて下さい(上記手順1,2).その時,Consoleに出るサーバのIPアドレスをメモして下さい
  5. クライアント役の人がサーバのIPアドレスとポート番号10000でEchoClientを立ち上げて下さい(上記手順3,4,5)
  6. サーバ役の人はコンソールに"Accepted connection from xxx.xxx.xxx.xxx:xxxxx"と出力されることを確認してください
  7. クライアント役の人が適当なメッセージをコンソールから入力してください.その時,サーバからの返答が直ちに表示される事を確認してください
  8. サーバ役の人はクライアント役の人が入力した文字列と同じ文字列がサーバのコンソールに出力されている事を確認してください
  9. エコーシステムの動作を確認したら,クライアント役の人が"quit"と入力してEchoClientを終了してください
  10. サーバ役の人はEchoServerが終了した事を確認してください
  11. 2から11の手順をグループ内で役割を交代しながら動作を確認してみてください
毎回サーバ,クライアントの両方のアプリケーションが終了している事を確認してください.
3人以上でグループを組んでいる場合,サーバ役を一人,クライアント役を複数で動作を確認してみてください.
EchoServerとEchoClientの動作は以下のように示せます.

パケット観測

ここで,サーバとクライアントの間のパケットのやり取りを観察してみましょう. パケットを観察するためのツールとしてWiresharkという便利なツールがあります. ここでは,EchoServerとEchoClientの間の通信の例を観察してみましょう.
段階サーバ (192.168.11.100)クライアント(192.168.11.2)
Wiresharkキャプチャ説明コンソールコンソール
接続確立
  1. クライアントからの接続要求によってクライアントはサーバに向けてSYNパケットを送ります.Seqはシーケンス番号を意味します.シーケンス番号は乱数値で決定されますが,相対値として0として表示されています
  2. クライアントからのSYNパケットを受け取ったサーバはクライアントに向けてSYN-ACKパケットを送ります.Seqはやはり相対値として0と表示されます.Ackは応答番号で対象となるパケットのシーケンス番号+1となります.ここでは上のパケットのシーケンス番号が0なので1となります
  3. サーバからのSYN-ACKパケットを受け取ったクライアントはサーバに向けてACKパケットを送ります.シーケンス番号は直前の受信パケットの応答番号と同じなので,1になります.応答番号は直前の受信パケットのシーケンス番号+1なので1となります.
この時点でTCPの接続が完了します. この手続をスリーウェイシェイクハンドと言います. この例では,クライアント側のポート番号はOSによって50183に選ばれました.
EchoServer (192.168.11.100) > Input server port > 10000
EchoServer (192.168.11.100) > Started and Listening for connections on port 10000
EchoServer (192.168.11.100) > Accepted connection from /192.168.11.2:50183
EchoClient (192.168.11.2) > Input IP address of EchoServer > 192.168.11.100
EchoClient (192.168.11.2) > Input port number of EchoServer > 10000
EchoClient (192.168.11.2) >
データ転送
  1. クライアントから"Hello"という文字列がサーバに送信されます.このパケットはPSHとACKフラグが立っています.シーケンス番号と応答番号は直前の自身が送信したACKと同じになります.Lenは送信データのバイト数です.
  2. クライアントから送信された文字列"Hello"をそのまま返送しています.このパケットはPSHとACKフラグが立っています.先のクライアントからのPSHパケットに対してACKパケットを返す時に,送信すべきデータ(ここでは受信した文字列と同じ文字列)がある場合,PSHとACKフラグを立てて送信出来ます.このパケットのシーケンス番号は直前の応答番号1と同じです.しかし,応答番号はTCP接続確立の場合と異なり,応答対象のパケットのシーケンス番号(ここでは1)+受信データ長(ここでは7)の8となります
  3. サーバからのメッセージに対してクライアントがACKパケットを返します.シーケンス番号は直前の受信パケットの確認応答番号と同じで8となります.確認応答番号は,同じく,応答対象のパケットのシーケンス番号(ここでは1)+受信データ長(ここでは7)の8となります
PSHフラグはTCPでデータを受信したらすぐに上位層に受け渡すことを意味します.
EchoClient (/192.168.11.2:50183) > Hello
EchoClient (192.168.11.2) > Hello
EchoServer (192.168.11.100) > Hello
接続切断
  1. クライアントのコンソールで"quit"が入力されることで,切断処理が開始されます.このパケットはFINとACKフラグが立っています.シーケンス番号と確認応答番号は直前の自身の送信パケットと同じで両方とも8です
  2. クライアントからFINパケットを受信したサーバは直ちにACKパケットを返します.このパケットのシーケンス番号は直前のFINパケットの確認応答番号と同じで,確立のときと同様に応答対象パケットのシーケンス番号+1で9となります
  3. その後,サーバから改めてFINパケットがクライアントに送信されます.このパケットのシーケンス番号と確認応答番号はその直前の自身の送信パケットと同じになります
  4. 最後にサーバからのFINパケットに対して,クライアントがACKパケットを返します.このパケットのシーケンス番号は応答対象パケットの確認応答番号と同じで9となり,確認応答番号は応答対象パケットのシーケンス番号+1で9となります
EchoServer (192.168.11.100) > Terminated connection from /192.168.11.2:50183
EchoClient (192.168.11.2) > quit

複数クライアントに対応できるEchoServer

前出のEchoServerでは最初にEchoTaskのインスタンスが生成されcallメソッドが呼び出され,whileループでEchoClientとのデータ転送を行っている間,このサーバアプリケーションに他のEchoClientが接続する事は出来ません. 多くのC/Sモデルでのサービスでは,サーバは複数のクライアントからの接続に同時に対応できる必要があります.

このためには上図に示すように,サーバでクライアントごとに別々のソケットを用意し,サーバとクライアントを接続する事を考えます. ここでの問題は,サーバでのソケット生成のタイミングです. クライアントからの接続要求はいつ来るかわかりません. そこで,接続要求が来た順にソケットを生成し,そのソケットを使ったデータ転送をサーバアプリケーションの別のスレッド上で実行するようにして,複数のクライアントの相手を別々の複数のスレッドで処理することが可能です. この要求の実現方法としてマルチスレッドプログラミングを利用する方法があります.
下記のMultiEchoServer.javaをコピーし,動作を確認してみましょう.

MultiEchoServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package jp.ac.utsunomiya_u.is;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
 
public class MultiEchoServer {
 
    public static void main(String[] args) {
        // Scannerクラスのインスタンス(標準入力System.inからの入力)
        try (Scanner scanner = new Scanner(System.in)) {
            System.out.print("EchoServer (" + getMyIpAddress() + ") > Input server port > ");
            // ポート番号入力
            int port = scanner.nextInt();
            // スレッドプールの生成
            ExecutorService executorService = Executors.newCachedThreadPool();
            // ServerSocketクラスのインスタンスをポート番号を指定して生成
            try (ServerSocket serverSocket = new ServerSocket(port)) {
                System.out.println("EchoServer (" + getMyIpAddress() + ") > Started and Listening for connections on port " + serverSocket.getLocalPort());
                while (true) {
                    // ServerSocketに対する要求を待機し,それを受け取ったらSocketクラスのインスタンスを生成しEchoTaskを実行
                    executorService.submit(new EchoTask(serverSocket.accept()));
                }
            } catch (IOException ex) {
                Logger.getLogger(MultiEchoServer.class.getName()).log(Level.SEVERE, null, ex);
            } finally {
                // スレッドプールの停止
                executorService.shutdown();
            }
        }
    }
 
    /**
     * 自ホストのIPアドレス取得
     *
     * @return 自ホストのIPアドレス
     */
    private static String getMyIpAddress() {
        try {
            // 自ホストのIPアドレス取得
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException ex) {
            Logger.getLogger(MultiEchoServer.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }
 
    private static class EchoTask implements Callable<Void> {
 
        /**
         * Socketクラスのインスタンス
         */
        private final Socket socket;
 
        /**
         * コンストラクタ
         *
         * @param socket Socketクラスのインスタンス
         */
        EchoTask(Socket socket) {
            this.socket = socket;
        }
 
        @Override
        public Void call() {
            System.out.println("EchoServer (" + getMyIpAddress() + ") > Accepted connection from " + socket.getRemoteSocketAddress());
            // クライアントからの読み込み用のBufferedReaderクラスのインスタンスをSocketのInputStreamから生成
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    // クライアントへの書き込み用のPrintWriterクラスのインスタンスをSocketのOutputStreamから生成(自動フラッシュ)
                    PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {
                String inputLine;
                // クライアントからの入力待機し,それを受け取ったらStringに収納
                while ((inputLine = reader.readLine()) != null) {
                    System.out.println("EchoClient (" + socket.getRemoteSocketAddress() + ") > " + inputLine);
                    // 受け取ったStringをそのままクライアントへ書き込み
                    writer.println(inputLine);
                }
            } catch (IOException ex) {
                Logger.getLogger(MultiEchoServer.class.getName()).log(Level.SEVERE, null, ex);
            } finally {
                System.out.println("EchoServer (" + getMyIpAddress() + ") > Terminated connection from " + socket.getRemoteSocketAddress());
                try {
                    // ソケットのクローズ処理
                    socket.close();
                } catch (IOException ex) {
                    Logger.getLogger(MultiEchoServer.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            return null;
        }
    }
}
  1. MultiEchoServerをポート番号10000で立ち上げます
  2. EchoClientを立ち上げ,サーバIPアドレス:localhost,ポート番号:10000でMultiEchoServerに接続します
  3. サーバ側で接続を確認し,クライアント側で文字列の送信を行い動作を確認します
  4. さらに,EchoClientを立ち上げ,サーバIPアドレス:localhost,ポート番号:10000でMultiEchoServerに接続します
  5. サーバ側で接続を確認し,クライアント側で文字列の送信を行い動作を確認します
27行目
スレッドプールを生成
31-34行目
クライアントからの接続確立が完了し,acceptメソッドが新しいソケットを生成したら,そのソケットを使うデータ転送のためのタスクを生成し,スレッドプールにそのタスクを送信します.
39行目
スレッドプールの停止
59行目
CallableインタフェースをEchoTaskクラスで実装することで,スレッドプールに対応します.
75行目
Callableインタフェースを実装したのでcallメソッドをオーバーライドしなければなりません.
MultiEchoServerの動作は以下のように示せます.