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

## 1. ネットワークプログラミング入門

1.1 ネットワークプログラミングの概要【理解】

  • コンピュータネットワーク

地理的に離れた場所にある複数の独立機能を持つコンピュータと外部デバイスを通信回線で接続し、ネットワークオペレーティングシステム、ネットワーク管理ソフトウェア、およびネットワーク通信プロトコルによって管理・調整され、リソース共有と情報伝達を実現するコンピュータシステム

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

ネットワーク通信プロトコルの下で、異なるコンピュータ上で実行されているプログラム間でデータ転送が可能になる

1.2 ネットワークプログラミングの三要素【理解】

  • IPアドレス

ネットワーク内のコンピュータが互いに通信できるようにするため、各コンピュータに一意の識別番号を割り当てる必要がある。この識別番号によってデータを受信するコンピュータを指定し、送信元コンピュータを識別する。IPアドレスがこの識別番号であり、デバイスの識別子

  • ポート

ネットワーク通信は本質的に2つのアプリケーション間の通信である。各コンピュータには多くのアプリケーションが存在するが、ネットワーク通信時にこれらのアプリケーションを区別する方法は?IPアドレスがネットワーク内のデバイスを一意に識別できるなら、ポート番号はデバイス内のアプリケーションを一意に識別できる。アプリケーションの識別子

  • プロトコル

コンピュータネットワークを通じて複数のコンピュータを接続できる。同一ネットワーク内のコンピュータが接続と通信を行う際には、一定のルールを遵守する必要がある。これは道路を走行する車両が交通ルールを遵守するのと同じである。コンピュータネットワークにおいて、これらの接続と通信のルールはネットワーク通信プロトコルと呼ばれ、データの転送形式、転送速度、転送手順などを統一して規定する。通信双方はデータ交換を完了するために同時に遵守しなければならない。一般的なプロトコルにはUDPプロトコルとTCPプロトコルがある

1.3 IPアドレス【理解】

IPアドレス:ネットワーク内のデバイスの一意の識別子

  • IPアドレスの分類

  • IPv4:各ネットワーク接続ホストに32ビットアドレスを割り当てる。TCP/IPの規定によれば、IPアドレスは二進数で表現され、各IPアドレスは32ビット、つまり4バイトの長さを持つ。例えば、二進数形式のIPアドレスが「11000000 10101000 00000001 01000010」の場合、このような長いアドレスを処理するのは非常に面倒である。使用しやすくするため、IPアドレスは通常十進数形式で書かれ、異なるバイト間で「.」記号が使用される。したがって、上記のIPアドレスは「192.168.1.66」と表現できる。IPアドレスのこの表現方法を「ドット付き十進数表現法」と呼び、これは0と1より記憶しやすくなる

  • IPv6:インターネットの急速な発展により、IPアドレスの需要量がますます増加しているが、ネットワークアドレスリソースが限られているため、IPの割り当てがますます緊迫している。アドレス空間を拡大するため、IPv6はアドレス長を128ビットに再定義し、16バイトごとにグループ化し、8つの16進数グループに分けることで、ネットワークアドレスリソースの不足問題を解決した

  • DOSコマンド:

  • ipconfig:ローカルマシンのIPアドレスを確認

  • ping IPアドレス:ネットワークが接続されているか確認

  • 特殊IPアドレス:

  • 127.0.0.1:ループバックアドレスであり、本機アドレスを代表し、通常テストに使用される

1.4 InetAddress【応用】

InetAddress:このクラスはインターネットプロトコル(IP)アドレスを表す

  • 関連メソッド
メソッド名 説明
static InetAddress getByName(String host) ホスト名のIPアドレスを特定する。ホスト名はマシン名でもIPアドレスでもよい
String getHostName() このIPアドレスのホスト名を取得する
String getHostAddress() テキスト表示でのIPアドレス文字列を返す
  • コードデモ
public class InetAddressDemo {
    public static void main(String[] args) throws UnknownHostException {
        // InetAddress address = InetAddress.getByName("itheima");
        InetAddress address = InetAddress.getByName("192.168.1.66");

        // public String getHostName():このIPアドレスのホスト名を取得
        String name = address.getHostName();
        // public String getHostAddress():テキスト表示でのIPアドレス文字列を返す
        String ip = address.getHostAddress();

        System.out.println("ホスト名:" + name);
        System.out.println("IPアドレス:" + ip);
    }
}

1.5 ポートとプロトコル【理解】

  • ポート

  • デバイス上のアプリケーションの一意の識別子

  • ポート番号

  • 2バイトで表される整数で、その値の範囲は0から65535。そのうち、0から1023までのポート番号は一部の著名なネットワークサービスとアプリケーションに使用され、通常のアプリケーションは1024以上のポート番号を使用する必要がある。ポート番号が別のサービスまたはアプリケーションによって占有されている場合、現在のプログラムの起動に失敗する

  • プロトコル

  • コンピュータネットワークにおいて、接続と通信のルールはネットワーク通信プロトコルと呼ばれる

  • UDPプロトコル

  • ユーザーデータグラムプロトコル(User Datagram Protocol)

  • UDPは接続なし通信プロトコルであり、データ転送時にはデータの送信側と受信側の間に論理接続が確立されない。簡単に言えば、コンピュータが別のコンピュータにデータを送信する場合、送信側は受信側が存在するか確認せずにデータを送信し、同様に受信側はデータを受信した際にも送信側に受信確認を返さない。

  • UDPプロトコルを使用するとシステムリソースの消費が少なく、通信効率が高いので、通常オーディオ、ビデオ、および一般的なデータ転送に使用される

  • 例えばビデオ会議は通常UDPプロトコルを使用するが、この場合偶発的に1〜2個のデータパケットが失われても受信結果に大きな影響を与えない。しかし、UDPプロトコルを使用してデータを転送する場合、UDPの接続なしの特性によりデータの完全性を保証できないため、重要なデータを転送する際にはUDPプロトコルの使用は推奨されない

  • TCPプロトコル

  • 伝送制御プロトコル (Transmission Control Protocol)

  • TCPプロトコルは接続指向の通信プロトコルであり、データ転送前に送信側と受信側の間に論理接続を確立し、その後データを転送する。TCP接続ではクライアントとサーバーが明確に区別され、クライアントがサーバーに接続要求を送信し、各接続の作成には「3ウェイハンドシェイク」が必要である

  • 3ウェイハンドシェイク:TCPプロトコルにおいて、データ送信の準備段階でクライアントとサーバー間の3回のインタラクションにより接続の信頼性を保証する

1回目のハンドシェイク:クライアントがサーバーに接続要求を送信し、サーバーの確認を待つ

2回目のハンドシェイク:サーバーがクライアントに応答を返し、接続要求を受信したことを通知する

3回目のハンドシェイク:クライアントがサーバーに再び確認情報を送信し、接続を確認する

  • 3ウェイハンドシェイクが完了し接続が確立されると、クライアントとサーバーはデータ転送を開始できる。この接続指向の特性により、TCPプロトコルはデータ転送の安全性を保証できるため、非常に広く使用されている。例えばファイルのアップロード、ファイルのダウンロード、ウェブページの閲覧など
  1. UDP通信プログラム

2.1 UDPデータ送信【応用】

  • JavaにおけるUDP通信

  • UDPプロトコルは信頼性の低いネットワークプロトコルであり、通信の両端でそれぞれSocketオブジェクトを構築するが、これら2つのSocketはデータの送受信のためのオブジェクトにすぎないため、UDPプロトコルに基づく通信双方にとって、クライアントとサーバーの概念はない

  • JavaはDatagramSocketクラスをUDPプロトコルに基づくSocketとして提供する

  • コンストラクタ

メソッド名 説明
DatagramSocket() データグラムソケットを作成し、ローカルアドレスの任意の利用可能なポートにバインドする
DatagramPacket(byte[] buf, int len, InetAddress add, int port) データパケットを作成し、指定されたホストの指定されたポートに長さlenのデータパケットを送信する
  • 関連メソッド
メソッド名 説明
void send(DatagramPacket p) データグラムパケットを送信する
void close() データグラムソケットを閉じる
void receive(DatagramPacket p) このソケットからデータグラムパケットを受信する
  • データ送信のステップ

  • 送信側のSocketオブジェクト(DatagramSocket)を作成する

  • データを作成し、データをパッケージングする

  • DatagramSocketオブジェクトのメソッドを呼び出してデータを送信する

  • 送信側を閉じる

  • コードデモ

public class SendDemo {
    public static void main(String[] args) throws IOException {
        // 送信側のSocketオブジェクト(DatagramSocket)を作成する
        // DatagramSocket() データグラムソケットを作成し、ローカルホストの任意の利用可能なポートにバインドする
        DatagramSocket ds = new DatagramSocket();

        // データを作成し、データをパッケージングする
        // DatagramPacket(byte[] buf, int length, InetAddress address, int port)
        // 指定されたホストの指定されたポートに長さlengthのデータパケットを送信するデータパケットを作成する。
        byte[] bys = "こんにちは、UDP、到着しました".getBytes();

        DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("127.0.0.1"), 10086);

        // DatagramSocketオブジェクトのメソッドを呼び出してデータを送信する
        // void send(DatagramPacket p) このソケットからデータグラムパケットを送信する
        ds.send(dp);

        // 送信側を閉じる
        // void close() このデータグラムソケットを閉じる
        ds.close();
    }
}

2.2 UDPデータ受信【応用】

  • データ受信のステップ

  • 受信側のSocketオブジェクト(DatagramSocket)を作成する

  • データを受信するためのデータパケットを作成する

  • DatagramSocketオブジェクトのメソッドを呼び出してデータを受信する

  • データパケットを解析し、コンソールに表示する

  • 受信側を閉じる

  • コンストラクタ

メソッド名 説明
DatagramPacket(byte[] buf, int len) 長さlenのデータパケットを受信するためのDatagramPacketを作成する
  • 関連メソッド
メソッド名 説明
byte[] getData() データバッファを返す
int getLength() 送信するデータの長さまたは受信するデータの長さを返す
  • サンプルコード
public class ReceiveDemo {
    public static void main(String[] args) throws IOException {
        // 受信側のSocketオブジェクト(DatagramSocket)を作成する
        DatagramSocket ds = new DatagramSocket(12345);

        // データを受信するためのデータパケットを作成する
        byte[] bys = new byte[1024];
        DatagramPacket dp = new DatagramPacket(bys, bys.length);

        // DatagramSocketオブジェクトのメソッドを呼び出してデータを受信する
        ds.receive(dp);

        // データパケットを解析し、コンソールに表示する
        System.out.println("データは:" + new String(dp.getData(), 0, dp.getLength()));
    }
}

2.3 UDP通信プログラム練習【応用】

  • ケース要件

UDPデータ送信:データはキーボード入力から取得し、入力データが886の場合、データ送信を終了する

UDPデータ受信:受信側は送信側がいつ送信を停止するか分からないため、無限ループで受信する

  • コード実装
/*
    UDPデータ送信:
        データはキーボード入力から取得し、入力データが886の場合、データ送信を終了する
 */
public class SendDemo {
    public static void main(String[] args) throws IOException {
        // 送信側のSocketオブジェクト(DatagramSocket)を作成する
        DatagramSocket ds = new DatagramSocket();
        // キーボード入力データ
        Scanner sc = new Scanner(System.in);
        while (true) {
            String s = sc.nextLine();
            // 入力データが886の場合、データ送信を終了する
            if ("886".equals(s)) {
                break;
            }
            // データを作成し、データをパッケージングする
            byte[] bys = s.getBytes();
            DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("192.168.1.66"), 12345);

            // DatagramSocketオブジェクトのメソッドを呼び出してデータを送信する
            ds.send(dp);
        }
        // 送信側を閉じる
        ds.close();
    }
}

/*
    UDPデータ受信:
        受信側は送信側がいつ送信を停止するか分からないため、無限ループで受信する
 */
public class ReceiveDemo {
    public static void main(String[] args) throws IOException {
        // 受信側のSocketオブジェクト(DatagramSocket)を作成する
        DatagramSocket ds = new DatagramSocket(12345);
        while (true) {
            // データを受信するためのデータパケットを作成する
            byte[] bys = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bys, bys.length);
            // DatagramSocketオブジェクトのメソッドを呼び出してデータを受信する
            ds.receive(dp);
            // データパケットを解析し、コンソールに表示する
            System.out.println("データは:" + new String(dp.getData(), 0, dp.getLength()));
        }
        // 受信側を閉じる
        // ds.close();
    }
}

2.4 UDPの3種類の通信方式【理解】

  • ユニキャスト

ユニキャストは2つのホスト間のピアツーピア通信に使用される

  • マルチキャスト

マルチキャストは特定のホストグループへの通信に使用される

  • ブロードキャスト

ブロードキャストは1つのホストがローカルネットワーク上のすべてのホストへのデータ通信に使用される

2.5 UDPマルチキャストの実装【理解】

  • 実装ステップ

  • 送信側

  1. 送信側のSocketオブジェクト(DatagramSocket)を作成する
  2. データを作成し、データをパッケージングする(DatagramPacket)
  3. DatagramSocketオブジェクトのメソッドを呼び出してデータを送信する(ユニキャストでは指定されたIPのコンピュータに送信するが、マルチキャストではマルチキャストアドレスに送信する)
  4. リソースを解放する
  • 受信側
  1. 受信側Socketオブジェクト(MulticastSocket)を作成する
  2. データを受信するためのコンテナを作成する
  3. 現在のコンピュータをマルチキャストアドレスにバインドし、このグループに参加する
  4. データをコンテナに受信する
  5. データパケットを解析し、データを表示する
  6. リソースを解放する
  • コード実装
// 送信側
public class ClinetDemo {
    public static void main(String[] args) throws IOException {
        // 1. 送信側のSocketオブジェクト(DatagramSocket)を作成する
        DatagramSocket ds = new DatagramSocket();
        String s = "こんにちは、マルチキャスト";
        byte[] bytes = s.getBytes();
        InetAddress address = InetAddress.getByName("224.0.1.0");
        int port = 10000;
        // 2. データを作成し、データをパッケージングする(DatagramPacket)
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
        // 3. DatagramSocketオブジェクトのメソッドを呼び出してデータを送信する(ユニキャストでは指定されたIPのコンピュータに送信するが、マルチキャストではマルチキャストアドレスに送信する)
        ds.send(dp);
        // 4. リソースを解放する
        ds.close();
    }
}
// 受信側
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        // 1. 受信側Socketオブジェクト(MulticastSocket)を作成する
        MulticastSocket ms = new MulticastSocket(10000);
        // 2. データを受信するためのコンテナを作成する
        DatagramPacket dp = new DatagramPacket(new byte[1024], 1024);
        // 3. 現在のコンピュータをマルチキャストアドレスにバインドし、このグループに参加する
        ms.joinGroup(InetAddress.getByName("224.0.1.0"));
        // 4. データをコンテナに受信する
        ms.receive(dp);
        // 5. データパケットを解析し、データを表示する
        byte[] data = dp.getData();
        int length = dp.getLength();
        System.out.println(new String(data, 0, length));
        // 6. リソースを解放する
        ms.close();
    }
}

2.6 UDPブロードキャストの実装【理解】

  • 実装ステップ

  • 送信側

  1. 送信側Socketオブジェクト(DatagramSocket)を作成する
  2. データを格納するコンテナを作成し、ブロードキャストアドレスをパッケージングする
  3. データを送信する
  4. リソースを解放する
  • 受信側
  1. 受信側のSocketオブジェクト(DatagramSocket)を作成する
  2. データを受信するためのデータパケットを作成する
  3. DatagramSocketオブジェクトのメソッドを呼び出してデータを受信する
  4. データパケットを解析し、コンソールに表示する
  5. 受信側を閉じる
  • コード実装
// 送信側
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        // 1. 送信側Socketオブジェクト(DatagramSocket)を作成する
        DatagramSocket ds = new DatagramSocket();
        // 2. データを格納するコンテナを作成し、ブロードキャストアドレスをパッケージングする
        String s = "ブロードキャスト、こんにちは";
        byte[] bytes = s.getBytes();
        InetAddress address = InetAddress.getByName("255.255.255.255");
        int port = 10000;
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
        // 3. データを送信する
        ds.send(dp);
        // 4. リソースを解放する
        ds.close();
    }
}
// 受信側
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        // 1. 受信側のSocketオブジェクト(DatagramSocket)を作成する
        DatagramSocket ds = new DatagramSocket(10000);
        // 2. データを受信するためのデータパケットを作成する
        DatagramPacket dp = new DatagramPacket(new byte[1024], 1024);
        // 3. DatagramSocketオブジェクトのメソッドを呼び出してデータを受信する
        ds.receive(dp);
        // 4. データパケットを解析し、コンソールに表示する
        byte[] data = dp.getData();
        int length = dp.getLength();
        System.out.println(new String(data, 0, length));
        // 5. 受信側を閉じる
        ds.close();
    }
}

  1. TCP通信プログラム

3.1 TCPデータ送信【応用】

  • JavaにおけるTCP通信

  • JavaはTCPプロトコルに基づくネットワークに対して良好なカプセル化を提供し、Socketオブジェクトを使用して両端の通信ポートを表現し、Socketを介してIOストリームを生成してネットワーク通信を行う。

  • JavaはクライアントにSocketクラスを、サーバーにServerSocketクラスを提供する

  • コンストラクタ

メソッド名 説明
Socket(InetAddress address, int port) ストリームソケットを作成し、指定されたIPの指定されたポート番号に接続する
Socket(String host, int port) ストリームソケットを作成し、指定されたホストの指定されたポート番号に接続する
  • 関連メソッド
メソッド名 説明
InputStream getInputStream() このソケットの入力ストリームを返す
OutputStream getOutputStream() このソケットの出力ストリームを返す
  • サンプルコード
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        // クライアントのSocketオブジェクト(Socket)を作成する
        // Socket(String host, int port) ストリームソケットを作成し、指定されたホストの指定されたポート番号に接続する
        Socket s = new Socket("127.0.0.1", 10000);

        // 出力ストリームを取得し、データを書き込む
        // OutputStream getOutputStream() このソケットの出力ストリームを返す
        OutputStream os = s.getOutputStream();
        os.write("こんにちは、TCP、到着しました".getBytes());

        // リソースを解放する
        s.close();
    }
}

3.2 TCPデータ受信【応用】3ウェイハンドシェイクと4ウェイハンドオフ

  • コンストラクタ
メソッド名 説明
ServerSocket(int port) 指定されたポートにバインドされたサーバーソケットを作成する
  • 関連メソッド
メソッド名 説明
Socket accept() このソケットに接続するソケットを監視し、受け入れる
  • 注意事項
  1. acceptメソッドはブロッキングであり、クライアント接続を待つ役割を果たす
  2. クライアントがオブジェクトを作成しサーバーに接続する際、これは3ウェイハンドシェイクプロトコルを通じてサーバーとの接続を保証する
  3. クライアントにとっては外に出力するため出力ストリームであり、サーバーにとっては内に入力するため入力ストリームである
  4. readメソッドもブロッキングである
  5. クライアントがストリームを閉じる際、サーバーに終了マークを書き込む追加の動作がある
  6. 最後のステップで接続を切断し、4ウェイハンドオフプロトコルを通じて接続の終了を保証する
  • 3ウェイハンドシェイクと4ウェイハンドオフ

  • 3ウェイハンドシェイク

  • 4ウェイハンドオフ

  • サンプルコード

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        // サーバー側のSocketオブジェクト(ServerSocket)を作成する
        // ServerSocket(int port) 指定されたポートにバインドされたサーバーソケットを作成する
        ServerSocket ss = new ServerSocket(10000);

        // Socket accept() このソケットに接続するソケットを監視し、受け入れる
        Socket s = ss.accept();

        // 入力ストリームを取得し、データを読み込み、コンソールに表示する
        InputStream is = s.getInputStream();
        byte[] bys = new byte[1024];
        int len = is.read(bys);
        String data = new String(bys, 0, len);
        System.out.println("データは:" + data);

        // リソースを解放する
        s.close();
        ss.close();
    }
}

3.3 TCPプログラム練習【応用】

  • ケース要件

クライアント:データを送信し、サーバーのフィードバックを受信する

サーバー:メッセージを受信した後、フィードバックを提供する

  • ケース分析

  • クライアントはオブジェクトを作成し、出力ストリームを使用してデータを出力する

  • サーバーはオブジェクトを作成し、入力ストリームを使用してデータを受信する

  • サーバーは出力ストリームを使用してフィードバックデータを提供する

  • クライアントは入力ストリームを使用してフィードバックデータを受信する

  • コード実装

// クライアント
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 10000);

        OutputStream os = socket.getOutputStream();
        os.write("こんにちは".getBytes());
        // os.close(); ここでストリームを閉じると、ソケット全体が使用できなくなる
        socket.shutdownOutput(); // 出力ストリームのみを閉じる。終了マークを書き込み、ソケットに影響を与えない

        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line;
        while((line = br.readLine()) != null) {
            System.out.println(line);
        }
        br.close();
        os.close();
        socket.close();
    }
}
// サーバー
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10000);

        Socket accept = ss.accept();

        InputStream is = accept.getInputStream();
        int b;
        while((b = is.read()) != -1) {
            System.out.println((char) b);
        }

        System.out.println("私の実行は見ましたか?");

        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
        bw.write("あなたは誰ですか?");
        bw.newLine();
        bw.flush();

        bw.close();
        is.close();
        accept.close();
        ss.close();
    }
}

3.4 TCPプログラムファイルアップロード練習【応用】

  • ケース要件

クライアント:データはローカルファイルから取得し、サーバーのフィードバックを受信する

サーバー:受信したデータをローカルファイルに書き込み、フィードバックを提供する

  • ケース分析

  • クライアントオブジェクトを作成し、ファイルを指す入力ストリームオブジェクトを作成し、データを読み込むたびにサーバーにデータを出力し、出力終了後にshutdownOutput()メソッドを使用してサーバーに転送終了を通知する

  • サーバーオブジェクトを作成し、ファイルを指す出力ストリームオブジェクトを作成し、データを受信するたびに出力ストリームを使用してファイルに出力し、転送終了後に、出力ストリームを使用してクライアントにフィードバック情報を提供する

  • クライアントはサーバーのフィードバック情報を受信する

  • 関連メソッド

メソッド名 説明
void shutdownInput() このソケットの入力ストリームを「ストリームの末尾」に配置する
void shutdownOutput() このソケットの出力ストリームの使用を禁止する
  • コード実装
// クライアント
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 10000);

        // ローカルのストリーム、ローカルファイルを読み取るために使用する
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("socketmodule\\ClientDir\\1.jpg"));

        // サーバーに書き込む---ネットワーク内のストリーム
        OutputStream os = socket.getOutputStream();
        BufferedOutputStream bos = new BufferedOutputStream(os);

        int b;
        while((b = bis.read()) != -1) {
            bos.write(b); // ネットワークを通じてサーバーに書き込む
        }
        bos.flush();
        // サーバーに終了マークを送信し、ファイル転送が完了したことを通知する
        socket.shutdownOutput();

        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line;
        while((line = br.readLine()) != null) {
            System.out.println(line);
        }
        bis.close();
        socket.close();
    }
}
// サーバー
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10000);

        Socket accept = ss.accept();

        // ネットワーク内のストリーム、クライアントからデータを読み取る
        BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
        // ローカルのIOストリーム、データをローカルに書き込み、永続的なストレージを実現する
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("socketmodule\\ServerDir\\copy.jpg"));

        int b;
        while((b = bis.read()) != -1) {
            bos.write(b);
        }

        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
        bw.write("アップロード成功");
        bw.newLine();
        bw.flush();

        bos.close();
        accept.close();
        ss.close();
    }
}

3.5 TCPプログラムサーバーの最適化【応用】

上記の「3.4 TCPプログラムファイルアップロード練習」セクションのコードには問題があることがわかりました:1回しか実行できず(サーバープログラムを1回実行すると終了する)、以下の最適化が必要です:

  • 最適化方案一

  • 要件

サーバーは1つのクライアント要求しか処理できず、1つの画像を受信した後、サーバーが閉じる。

  • 解決方案

ループを使用する

  • コード実装
// サーバーコードは以下の通り、クライアントコードは前のケースと同じため省略
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10000);

        while (true) {
            Socket accept = ss.accept();

            // ネットワーク内のストリーム、クライアントからデータを読み取る
            BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
            // ローカルのIOストリーム、データをローカルに書き込み、永続的なストレージを実現する
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("optimizeserver\\ServerDir\\copy.jpg"));

            int b;
            while((b = bis.read()) != -1) {
                bos.write(b);
            }

            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
            bw.write("アップロード成功");
            bw.newLine();
            bw.flush();

            bos.close();
            accept.close();
        }
        // ss.close();
        
    }
}

  • 最適化方案二

  • 要件

2回目のファイルアップロード時、1回目のファイルが上書きされる。

  • 解決方案

UUID.randomUUID()メソッドを使用してランダムなファイル名を生成する

  • コード実装
// サーバーコードは以下の通り、クライアントコードは前のケースと同じため省略
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10000);

        while (true) {
            Socket accept = ss.accept();

            // ネットワーク内のストリーム、クライアントからデータを読み取る
            BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
            // ローカルのIOストリーム、データをローカルに書き込み、永続的なストレージを実現する
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("optimizeserver\\ServerDir\\" + UUID.randomUUID().toString() + ".jpg"));

            int b;
            while((b = bis.read()) != -1) {
                bos.write(b);
            }

            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
            bw.write("アップロード成功");
            bw.newLine();
            bw.flush();

            bos.close();
            accept.close();
        }
        // ss.close();

    }
}

  • 最適化方案三

  • 要件

ループを使用してもサーバーは複数のクライアント要求を処理できるが、複数のクライアントと同時に通信できない。

  • 解決方案

マルチスレッド処理を開始する

  • コード実装
// スレッドタスククラス
public class ThreadSocket implements Runnable {
    private Socket acceptSocket;

    public ThreadSocket(Socket accept) {
        this.acceptSocket = accept;
    }
  
    @Override
    public void run() {
        BufferedOutputStream bos = null;
        try {
            // ネットワーク内のストリーム、クライアントからデータを読み取る
            BufferedInputStream bis = new BufferedInputStream(acceptSocket.getInputStream());
            // ローカルのIOストリーム、データをローカルに書き込み、永続的なストレージを実現する
            bos = new BufferedOutputStream(new FileOutputStream("optimizeserver\\ServerDir\\" + UUID.randomUUID().toString() + ".jpg"));

            int b;
            while((b = bis.read()) != -1) {
                bos.write(b);
            }
          
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(acceptSocket.getOutputStream()));
            bw.write("アップロード成功");
            bw.newLine();
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(bos != null){
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (acceptSocket != null){
                try {
                    acceptSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
// サーバーコード
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10000);

        while (true) {
            Socket accept = ss.accept();
            ThreadSocket ts = new ThreadSocket(accept);
            new Thread(ts).start();
        }
        // ss.close();
    }
}

  • 最適化方案四

  • 要件

マルチスレッドを使用してもサーバーは複数のクライアント要求を同時に処理できるが、リソースの消費が大きすぎる。

  • 解決方案

スレッドプールを追加する

  • コード実装
// サーバーコードは以下の通り、スレッドタスククラスコードは上記と同じため省略
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10000);
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3, // コアスレッド数
                10, // スレッドプールの総数
                60, // 一時スレッドのアイドル時間
                TimeUnit.SECONDS, // 一時スレッドのアイドル時間の単位
                new ArrayBlockingQueue<>(5), // ブロッキングキュー
                Executors.defaultThreadFactory(), // スレッドの作成方法
                new ThreadPoolExecutor.AbortPolicy() // タスク拒否戦略
        );

        while (true) {
            Socket accept = ss.accept();
            ThreadSocket ts = new ThreadSocket(accept);
            // new Thread(ts).start();
            pool.submit(ts);
        }
        // ss.close();
    }
}

  1. NIO

4.1 概要【理解】

  • BIO

Blocking IO、ブロッキングIO

  • NIO

No Blocking IO、ノンブロッキングIO

  • ブロッキングIOの欠点

待機中は何もできない

  • ノンブロッキングIOの利点

常に待機する必要がなく、準備が整ったら実行する

4.2 NIOとBIOの違い【理解】

  • 違い1

BIOはブロッキングであり、NIOはノンブロッキングである

  • 違い2

BIOはストリーム指向であり、NIOはバッファー指向である

BIOではデータ転送は一方向であり、NIOのバッファーは双方向である

4.3 NIOの3大モジュール【理解】

  • バッファー

データを格納するために使用される

  • チャネル

接続を確立し、データを転送するために使用される

  • セレクター

チャネルの状態を監視する

4.4 NIOバッファーオブジェクトの作成【応用】

  • メソッド紹介
メソッド名 説明
static ByteBuffer allocate(長さ) byteタイプのバッファーを作成する
static ByteBuffer wrap(byte[] array) 内容のあるbyteタイプのバッファーを作成する
  • コード例
public class CreateByteBufferDemo1 {
    public static void main(String[] args) {
        // method1();

        // method2();

        ByteBuffer wrap = ByteBuffer.wrap("aaa".getBytes());
        for (int i = 0; i < 3; i++) {
            System.out.println(wrap.get());
        }
    }

    private static void method2() {
        byte[] bytes = {97, 98, 99};
        ByteBuffer byteBuffer2 = ByteBuffer.wrap(bytes);
        // バッファーの長さ3
        // バッファー内の内容はバイト配列の内容である
        for (int i = 0; i < 3; i++) {
            System.out.println(byteBuffer2.get());
        }
        System.out.println(byteBuffer2.get());
    }

    private static void method1() {
        ByteBuffer byteBuffer1 = ByteBuffer.allocate(5);
        // get
        for (int i = 0; i < 5; i++) {
            System.out.println(byteBuffer1.get());
        }
        System.out.println(byteBuffer1.get());
    }
}

4.5 NIOバッファーへのデータ追加【応用】

  • メソッド紹介

  • コード例

public class ByteBufferDemo2 {
    public static void main(String[] args) {
//        int position()		  現在操作するインデックス
//        int limit() 		  最大操作可能なインデックス
//        int capacity()		  バッファーの総長
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        System.out.println(byteBuffer.position());//0
        System.out.println(byteBuffer.limit());//10
        System.out.println(byteBuffer.capacity());//10

//        put(byte b)		  1バイトずつ追加する
//        byteBuffer.put((byte) 97);
//        System.out.println(byteBuffer.position());
//        System.out.println(byteBuffer.limit());
//        System.out.println(byteBuffer.capacity());

//        put(byte[] src)		 1バイト配列ずつ追加する
//        byteBuffer.put("aaa".getBytes());
//        System.out.println(byteBuffer.position());//3
//        System.out.println(byteBuffer.limit());//10
//        System.out.println(byteBuffer.capacity());//10

//        position(int newPosition) positionを変更する
//        byteBuffer.position(1);

//        limit(int newLimit)	  limitを変更する
//        byteBuffer.limit(5);
//        System.out.println(byteBuffer.position());
//        System.out.println(byteBuffer.limit());
//        System.out.println(byteBuffer.capacity());

//        int remaining()		  まだ操作できる残り
//        boolean hasRemaining()	  まだ操作できるか

        byteBuffer.put("0123456789".getBytes());
        System.out.println(byteBuffer.remaining()); //0  まだ操作できる残り0個
        System.out.println(byteBuffer.hasRemaining()); // false
    }
}

4.6 NIOバッファーからのデータ取得【応用】

  • メソッド紹介
メソッド名 紹介
flip() 読み書きモードを切り替える(書き込み→読み込み)
get() 1バイトを読み取る
get(byte[] dst) 複数バイトを読み取る
get(int index) 指定インデックスのバイトを読み取る
rewind() positionを0に設定し、再読み込み可能にする
clear() データの読み書きが完了(読み込み→書き込み)
array() バッファーをバイト配列に変換して返す

上記は以前バッファーに書き込んだときの3つのパラメータの位置ですが、データを読み取る際の問題は、バッファー配列の長さが分からないため、ループ読み取りできないことです。したがって、まずバッファーに「伝える」必要があり、その後バッファーの3つのパラメータが以下のようになります:(バッファーを書き込みモードから読み込みモードに切り替えるのと同等)

その後、読み取り時は1バイトずつ読み取り、positionが以下の位置になるまで読み取り、後ろにデータがないことを示し、ループを終了します

  • コード例
public class ByteBufferDemo3 {
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        byteBuffer.put("abc".getBytes());

//        flip()  読み書きモードを切り替える(書き込み→読み込み)
        byteBuffer.flip();
//        get()   1バイトを読み取る
//        while(byteBuffer.limit() != byteBuffer.position()){
//            System.out.println((char) byteBuffer.get());
//        }

        for (int i = 0; i < byteBuffer.limit(); i++) {
            System.out.println((char) byteBuffer.get());
        }

//        get(byte[] dst) 複数バイトを読み取る
//        byte[] bytes = new byte[byteBuffer.limit()];
//        byteBuffer.get(bytes);
//        System.out.println(new String(bytes));

//        get(int index)  指定インデックスのバイトを読み取る
//        System.out.println((char) byteBuffer.get(0));

//        rewind()    positionを0に設定し、再読み込み可能にする
//        byteBuffer.rewind();
//        for (int i = 0; i < byteBuffer.limit(); i++) {
//            System.out.println((char) byteBuffer.get());
//        }

//        clear()     データの読み書きが完了(読み込み→書き込み)
        byteBuffer.clear();
        byteBuffer.put("qqq".getBytes());
//        array()     バッファーをバイト配列に変換して返す

        byte[] bytes = byteBuffer.array();
        System.out.println(new String(bytes));
    }
}

4.7 まとめ【理解】

  1. 要件:データをバッファーに書き込みたい。

データは外部からバッファーに入ってくるため、バッファーはデータの読み取り操作を行う。 2. 要件:データをバッファーから読み出したい。

データはバッファー内部から外部に出ていく。したがって、バッファーはデータの書き込み操作を行う。 3. capacity:容量(長さ) limit: 界限(最大読み書き可能な位置) position:位置(読み書きするインデックス) 4. バッファー内のデータを取得する前に、flipメソッドを呼び出す必要がある 5. 再度データを書き込む前に、clearメソッドを呼び出す必要がある。

しかし、データはまだ消失しておらず、再度データを書き込むと上書きされるまで消失しない。

タグ: ネットワークプログラミング udp TCP NIO Java

7月6日 17:01 投稿