## 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プロトコルはデータ転送の安全性を保証できるため、非常に広く使用されている。例えばファイルのアップロード、ファイルのダウンロード、ウェブページの閲覧など
- 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マルチキャストの実装【理解】
-
実装ステップ
-
送信側
- 送信側のSocketオブジェクト(DatagramSocket)を作成する
- データを作成し、データをパッケージングする(DatagramPacket)
- DatagramSocketオブジェクトのメソッドを呼び出してデータを送信する(ユニキャストでは指定されたIPのコンピュータに送信するが、マルチキャストではマルチキャストアドレスに送信する)
- リソースを解放する
- 受信側
- 受信側Socketオブジェクト(MulticastSocket)を作成する
- データを受信するためのコンテナを作成する
- 現在のコンピュータをマルチキャストアドレスにバインドし、このグループに参加する
- データをコンテナに受信する
- データパケットを解析し、データを表示する
- リソースを解放する
- コード実装
// 送信側
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ブロードキャストの実装【理解】
-
実装ステップ
-
送信側
- 送信側Socketオブジェクト(DatagramSocket)を作成する
- データを格納するコンテナを作成し、ブロードキャストアドレスをパッケージングする
- データを送信する
- リソースを解放する
- 受信側
- 受信側のSocketオブジェクト(DatagramSocket)を作成する
- データを受信するためのデータパケットを作成する
- DatagramSocketオブジェクトのメソッドを呼び出してデータを受信する
- データパケットを解析し、コンソールに表示する
- 受信側を閉じる
- コード実装
// 送信側
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();
}
}
- 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() | このソケットに接続するソケットを監視し、受け入れる |
- 注意事項
- acceptメソッドはブロッキングであり、クライアント接続を待つ役割を果たす
- クライアントがオブジェクトを作成しサーバーに接続する際、これは3ウェイハンドシェイクプロトコルを通じてサーバーとの接続を保証する
- クライアントにとっては外に出力するため出力ストリームであり、サーバーにとっては内に入力するため入力ストリームである
- readメソッドもブロッキングである
- クライアントがストリームを閉じる際、サーバーに終了マークを書き込む追加の動作がある
- 最後のステップで接続を切断し、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();
}
}
- 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 まとめ【理解】
- 要件:データをバッファーに書き込みたい。
データは外部からバッファーに入ってくるため、バッファーはデータの読み取り操作を行う。 2. 要件:データをバッファーから読み出したい。
データはバッファー内部から外部に出ていく。したがって、バッファーはデータの書き込み操作を行う。 3. capacity:容量(長さ) limit: 界限(最大読み書き可能な位置) position:位置(読み書きするインデックス) 4. バッファー内のデータを取得する前に、flipメソッドを呼び出す必要がある 5. 再度データを書き込む前に、clearメソッドを呼び出す必要がある。
しかし、データはまだ消失しておらず、再度データを書き込むと上書きされるまで消失しない。