ネットワークプログラミング概要
JavaはInternet而生まれた言語であり、ネットワークアプリケーションの開発を言語レベルから支援している。開発者はJava提供的网络类库を使用することで、ネットワーク接続の詳細を隠蔽し、より効率的にネットワークアプリケーションを構築できる。
ソフトウェアアーキテクチャ
ネットワークアプリケーションは大きく分けて2種類のアーキテクチャに分類される。
C/Sアーキテクチャ(Client/Server構造)は、クライアントとサーバーからなる構造である。QQ、美团、360セキュリティガードなどのアプリケーションがこの形式を採用している。
B/Sアーキテクチャ(Browser/Server構造)は、ブラウザとサーバーからなる構造である。Chrome、Firefox、Safariなどのブラウザがこの形式を採用している。
どちらのアーキテクチャもネットワーク接続を必要とし、ネットワークプログラミングとは、特定のプロトコルに従って2台のコンピュータ間で通信を行うプログラムを指す。
ネットワークの基礎知識
コンピュータネットワークとは、異なる地理的位置に設置されたコンピュータや外部デバイスを通信回線で接続し、大規模で高機能なシステムを構築することで、情報の相互伝達やハードウェア・ソフトウェア・データなどのリソースを共有できるようにするものである。
ネットワークプログラミングの目的は、ネットワークプロトコルを通じて他のコンピュータとデータを交換し、通信を行うことにある。
ネットワークプログラミングでは主に3つの問題を解決する必要がある:
- ネットワーク上の1台または複数のコンピュータを正確に特定する方法
- コンピュータ上で動作する特定のアプリケーションを特定する方法
- 対象を発見した後、データを信頼性が高く効率的に転送する方法
ネットワーク通信の構成要素
ホスト間の通信実現方法
ネットワーク通信を実現するには、以下の要素が必要となる:
- 通信相手の住所:IPアドレスとポート番号
- 通信規則:異なるハードウェアやオペレーティングシステム間の通信に必要なプロトコル
IPアドレスとドメイン名
IPアドレスとは
IPアドレス(Internet Protocol Address)は、ネットワーク上のデバイスに割り当てられる一意の識別子である。電話番号に例えるならば、IPアドレスは「電話番号」に相当する。
IPv4は32ビットの数値であり、4つのバイト(a.b.c.d形式)で表現される。例:192.168.65.100。各値は0から255の整数である。この方式では約42億個のアドレスを表現できるが、2011年初頭にほぼ枯渇した。
IPv4アドレスはネットワークアドレスとホストアドレスで構成される。ネットワークアドレスはデバイスが属するネットワークセグメントを識別し、ホストアドレスは特定のデバイスを識別する。
IPv6は128ビットアドレスを採用しており、16バイトを8つの16進数で表現する。例:ABCD:EF01:2345:6789:ABCD:EF01:2345:6789。IPv6により、地球上の1平方メートルあたり1000個以上のアドレスを割り当て可能となり、アドレス枯渇問題を解決した。2012年6月6日には世界IPv6デープロが行われ、主要ウェブサイトがIPv6対応を宣言した。
IPアドレスはパブリックアドレス(インターネット用)とプライベートアドレス(LAN用)に分類される。192.168.x.xで始まるアドレスはプライベートアドレスであり、組織内部での使用に特化したものである。
特殊なIPアドレス
- ローカルループバックアドレス:127.0.0.1
- ホスト名:localhost
ドメイン名とは
ドメイン名はIPアドレスを人間が覚えやすい名前で表現したものである。www.example.comのような形式で、DNS(Domain Name System)がドメイン名からIPアドレスへの変換を行う。
ドメイン名解決の流れ:ブラウザでドメイン名を入力すると、オペレーティングシステムはまずhostsファイルを調べ、次にローカルDNSキャッシュを確認し、最後にDNSサーバーに問い合わせてIPアドレスを取得する。
ポート番号
IPアドレスがデバイスを一意に識別するのに対し、ポート番号はデバイス上で動作するプロセス(アプリケーション)を一意に識別する。
ポート番号は16ビット整数で、範囲は0から65535である。
- ウェルノウンポート:0〜1023(HTTP:80、FTP:21、Telnet:23など事前定義されたサービス)
- レジスタードポート:1024〜49151(ユーザーアプリケーション向け、Tomcat:8080、MySQL:3306)
- ダイナミックポート:49152〜65535(一時的な接続用)
ネットワーク通信プロトコル
ネットワーク通信プロトコルは、データの転送形式、転送速率、転送ステップ、エラー制御などを統一的に規定する。通信の両端がこの規則を守ることでデータの交換が可能となる。
複雑なプロトコルを効率的に実装するため、プロトコル階層化の考え方が採用されている。これにより、各層が独立して機能し、システムの開発や拡張が容易になる。
主要な参照モデルとして、OSI参照モデルとTCP/IP参照モデルがある。TCP/IPモデルは事実上の国際標準となっている。
トランスポート層プロトコル:TCPとUDP
java.netパッケージには、低レベルの通信詳細を提供するクラスとインターフェースが含まれており、開発者は通信の詳細を意識することなくネットワークアプリケーションに集中できる。
TCPプロトコルの特徴
TCP(Transmission Control Protocol)は接続指向型のプロトコルであり、信頼性の高いバイトストリーム通信を提供する。
- 通信開始前に「3wayハンドshake」で接続を確立
- 確認応答と再送機構により信頼性を保証
- 大量のデータを転送可能
- 転送完了後に接続を解放(オーバーヘッド较大)
TCPは電話の通話に例えられる。
UDPプロトコルの特徴
UDP(User Datagram Protocol)は非接続型のプロトコルであり、シンプルで高速なデータ転送を提供する。
- 接続確立不要
- 送信側と受信側の確認なし(信頼性なし)
- データグラムサイズは64KB以下に制限
- リソース消費小、高速通信
- 映像・音声ストリーミングなどに適する
UDPはSMSや電報に例えられる。
3wayハンドシェイク(接続確立)
TCP接続の確立は以下の3段階で構成される:
- クライアントがサーバーに接続要求(SYN)を送信
- サーバーが確認応答(SYN+ACK)を返信
- クライアントが最終確認応答(ACK)を送信
この过程が完了すると、接続が確立され数据传输が開始される。
4wayハンドシェイク(接続解除)
TCP接続の解除は以下の4段階で構成される:
- クライアントが接続終了要求(FIN)を送信( полуクローズ状態)
- サーバーが最終データを送信しACKを返信
- サーバーがFINを送信
- クライアントがACKを返信し、2MSL待機後に完全クローズ
ネットワークプログラミングAPI
InetAddressクラス
InetAddressクラスはIPアドレスを表す。主要な取得メソッドとして以下がある:
- getLocalHost():ローカルホストのIPアドレス取得
- getByName(String host):ホスト名またはIPアドレスから取得
- getByAddress(byte[] addr):バイト配列からIPアドレス生成
代表的なメソッド:
- getHostAddress():IPアドレスの文字列表現を返す
- getHostName():ホスト名を返す
- isReachable(int timeout):到達可能性テスト
package jp.sample.network;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class AddressSample {
public static void main(String[] args) throws UnknownHostException {
// ローカルホストの取得
InetAddress localHost = InetAddress.getLocalHost();
System.out.println("ローカルホスト: " + localHost);
// ドメイン名からIPアドレス取得
InetAddress remoteHost = InetAddress.getByName("www.example.com");
System.out.println("リモートホスト: " + remoteHost);
// バイト配列からIPアドレス生成
byte[] addressBytes = {(byte)192, (byte)168, 1, 100};
InetAddress fromBytes = InetAddress.getByAddress(addressBytes);
System.out.println("バイト配列から生成: " + fromBytes);
}
}
Socketクラス概要
SocketはIPアドレスとポート番号の組み合わせで識別される通信エンドポイントである。Socketを使用することで、ネットワーク接続をストリームとして扱うことができる。
ストリームSocket(TCP用):
- ServerSocket:サーバーサイドのソケット実装
- Socket:クライアントサイドのソケット実装
データグラムSocket(UDP用):
- DatagramSocket:UDP送信用のソケット
ServerSocketクラス
ServerSocketはTCPサーバーソケットを実装する。
コンストラクタ:
- ServerSocket(int port):指定ポートにバインドされたサーバーソケットを作成
主要メソッド:
- Socket accept():クライアント接続をlistenし、接続受付
Socketクラス
Socketはクライアントソケットを実装する。
コンストラクタ:
- Socket(InetAddress address, int port):指定IPアドレスとポートに接続
- Socket(String host, int port):指定ホスト名とポートに接続
主要メソッド:
- InputStream getInputStream():入力ストリーム取得
- OutputStream getOutputStream():出力ストリーム取得
- InetAddress getInetAddress():接続先IPアドレス取得
- int getPort():接続先ポート番号取得
- void close():ソケット关闭
重要:shutdownInput()やshutdownOutput()はストリームのみを关闭し、close()を呼び出す必要がある。
DatagramSocketクラス
DatagramSocketはUDPデータグラムの送受信に使用する。
コンストラクタ:
- DatagramSocket():ランダムポートにバインド
- DatagramSocket(int port):指定ポートにバインド
主要メソッド:
- void send(DatagramPacket p):データグラム送信
- void receive(DatagramPacket p):データグラム受信(ブロックする)
- void close():ソケット关闭
DatagramPacketクラス
DatagramPacketはUDPデータグラムを表す。
コンストラクタ:
- DatagramPacket(byte[] buf, int length):受信用
- DatagramPacket(byte[] buf, int length, InetAddress address, int port):送信用
主要メソッド:
- InetAddress getAddress():送信先/送信元IPアドレス
- int getPort():送信先/送信元ポート番号
- byte[] getData():データバッファ取得
- int getLength():データ長取得
TCPネットワークプログラミング
通信モデル
JavaにおけるTCPプログラミングは、サーバーサイドとクライアントサイドの両方で構成される。サーバーはServerSocketで接続をlistenし、クライアントはSocketでサーバーに接続を要求する。
開発ステップ
クライアント側:
- Socketを作成(サーバーIPとポート指定)
- 入力/出力ストリームを取得
- ストリームを通じてデータの読み書き
- Socketを关闭(接続解放)
サーバー側:
- ServerSocketを作成(ポート指定)
- accept()で接続をaccept
- 入出力ストリームを取得してデータ通信
- Socketを关闭
実践例:クライアント・サーバー通信
以下に基本的なTCP通信の実装例を示す。
package jp.sample.tcp;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpServer {
private static final int LISTEN_PORT = 8888;
public static void main(String[] args) throws Exception {
try (ServerSocket serverSocket = new ServerSocket(LISTEN_PORT)) {
System.out.println("クライアント接続を待っています...");
Socket clientSocket = serverSocket.accept();
System.out.println("クライアント接続完了: " +
clientSocket.getInetAddress().getHostAddress());
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
PrintWriter writer = new PrintWriter(
clientSocket.getOutputStream(), true)) {
String receivedMessage = reader.readLine();
System.out.println("受信メッセージ: " + receivedMessage);
writer.println("メッセージ受信確認");
}
}
}
}
package jp.sample.tcp;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class TcpClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8888;
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
System.out.println("サーバーに接続しました");
writer.println("こんにちは、サーバー!");
String response = reader.readLine();
System.out.println("サーバーからの応答: " + response);
}
}
}
複数クライアントの同時処理
サーバーが複数のクライアントを同時に処理するには、各クライアント専用のスレッドを割り当てる必要がある。
package jp.sample.tcp;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MultiClientServer {
private static final int PORT = 9999;
private static final int THREAD_POOL_SIZE = 10;
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
try (ServerSocket server = new ServerSocket(PORT)) {
System.out.println("サーバーを起動しました。ポート: " + PORT);
while (true) {
Socket clientSocket = server.accept();
System.out.println("新規接続: " +
clientSocket.getInetAddress().getHostAddress());
executor.execute(() -> handleClient(clientSocket));
}
}
}
private static void handleClient(Socket socket) {
try (socket;
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {
String input;
while ((input = reader.readLine()) != null) {
System.out.println("クライアントからのメッセージ: " + input);
writer.println("エコー: " + input);
}
} catch (Exception e) {
System.out.println("クライアント接続エラー: " + e.getMessage());
}
}
}
チャットシステム実装例
複数クライアント間でメッセージを交換する簡易チャットシステムのサーバー実装を示す。
package jp.sample.chat;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
public class ChatServer {
private static final int SERVER_PORT = 12345;
private final Set<ClientHandler> clients =
Collections.synchronizedSet(new HashSet<>());
private final ExecutorService threadPool =
Executors.newCachedThreadPool();
public static void main(String[] args) throws IOException {
new ChatServer().start();
}
public void start() throws IOException {
try (ServerSocket serverSocket = new ServerSocket(SERVER_PORT)) {
System.out.println("チャットサーバー起動中...");
while (true) {
Socket clientSocket = serverSocket.accept();
ClientHandler handler = new ClientHandler(clientSocket, this);
clients.add(handler);
threadPool.execute(handler);
}
}
}
public void broadcast(String message, ClientHandler sender) {
synchronized (clients) {
for (ClientHandler client : clients) {
if (client != sender) {
client.sendMessage(message);
}
}
}
}
public void removeClient(ClientHandler client) {
clients.remove(client);
}
static class ClientHandler implements Runnable {
private final Socket socket;
private final ChatServer server;
private PrintWriter out;
public ClientHandler(Socket socket, ChatServer server) {
this.socket = socket;
this.server = server;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
out = new PrintWriter(socket.getOutputStream(), true);
String username = in.readLine();
server.broadcast(username + "さんが参加しました", this);
String message;
while ((message = in.readLine()) != null) {
server.broadcast(username + ": " + message, this);
}
} catch (IOException e) {
System.out.println("クライアント切断");
} finally {
server.removeClient(this);
}
}
public void sendMessage(String message) {
if (out != null) {
out.println(message);
}
}
}
}
UDPネットワークプログラミング
UDP通信の特徴
UDPは非接続型のプロトコルであり、接続を確立せずにデータを送信する。信頼性は保証されないが、高速でリソース消費が少ない。
- データグラム単位で送信
- サイズは64KB以下
- 確認応答なし
- リアルタイム性が求められるアプリケーションに適する
開発ステップ
送信側:
- DatagramSocketを作成
- DatagramPacketを作成(データ、サイズ、宛先IP/ポート)
- send()で送信
- Socketを关闭
受信側:
- DatagramSocketを作成(listenポート指定)
- DatagramPacketを作成(受信バッファ)
- receive()で受信(ブロックする)
- Socketを关闭
UDP送受信の実装例
package jp.sample.udp;
import java.net.*;
public class UdpSender {
private static final int DESTINATION_PORT = 5000;
public static void main(String[] args) throws Exception {
try (DatagramSocket socket = new DatagramSocket()) {
String message = "UDPテストメッセージ";
byte[] data = message.getBytes();
InetAddress address = InetAddress.getByName("localhost");
DatagramPacket packet = new DatagramPacket(
data, data.length, address, DESTINATION_PORT);
socket.send(packet);
System.out.println("メッセージを送信しました");
}
}
}
package jp.sample.udp;
import java.net.*;
public class UdpReceiver {
private static final int LISTEN_PORT = 5000;
public static void main(String[] args) throws Exception {
try (DatagramSocket socket = new DatagramSocket(LISTEN_PORT)) {
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
System.out.println("メッセージ受信待...");
socket.receive(packet);
String received = new String(
packet.getData(), 0, packet.getLength());
System.out.println("受信したメッセージ: " + received);
System.out.println("送信元: " + packet.getAddress().getHostAddress());
}
}
}
複数メッセージの送受信
package jp.sample.udp;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
public class UdpMessageSender {
private static final int TARGET_PORT = 8080;
public static void main(String[] args) throws Exception {
try (DatagramSocket socket = new DatagramSocket()) {
List<String> messages = new ArrayList<>();
messages.add("最初のメッセージ");
messages.add("2番目のメッセージ");
messages.add("最後のメッセージ");
InetAddress targetAddress = InetAddress.getByName("127.0.0.1");
for (String msg : messages) {
byte[] data = msg.getBytes();
DatagramPacket packet = new DatagramPacket(
data, data.length, targetAddress, TARGET_PORT);
socket.send(packet);
System.out.println("送信: " + msg);
}
}
}
}
package jp.sample.udp;
import java.net.*;
public class UdpMessageReceiver {
private static final int LISTEN_PORT = 8080;
public static void main(String[] args) throws Exception {
try (DatagramSocket socket = new DatagramSocket(LISTEN_PORT)) {
byte[] buffer = new byte[65535];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
System.out.println("UDPリスナー起動、ポート: " + LISTEN_PORT);
while (true) {
socket.receive(packet);
String data = new String(
packet.getData(), 0, packet.getLength());
System.out.println("[" + packet.getAddress().getHostAddress()
+ "] " + data);
}
}
}
}
URLプログラミング
URLクラスの基本
URL(Uniform Resource Locator)はインターネット上のリソースの住所を表す。http://、https://、ftp://などのプロトコル形式でリソースにアクセスできる。
URLの基本構造:
プロトコル://ホスト名:ポート番号/パス?パラメータ
例:https://www.example.com:8080/api/users?id=100
URLクラスのコンストラクタ
- new URL(String spec):文字列からURL生成
- new URL(URL context, String spec):ベースURLと相対URLから生成
- new URL(String protocol, String host, String file):個別指定
- new URL(String protocol, String host, int port, String file):ポート指定
URLクラスの主要メソッド
- getProtocol():プロトコル名
- getHost():ホスト名
- getPort():ポート番号
- getPath():パス
- getFile():ファイル名(パス+クエリ)
- getQuery():クエリ文字列
package jp.sample.url;
import java.net.URL;
public class UrlSample {
public static void main(String[] args) throws Exception {
URL url = new URL("https://api.example.com:8080/data/info.html?id=123");
System.out.println("プロトコル: " + url.getProtocol());
System.out.println("ホスト: " + url.getHost());
System.out.println("ポート: " + url.getPort());
System.out.println("パス: " + url.getPath());
System.out.println("ファイル: " + url.getFile());
System.out.println("クエリ: " + url.getQuery());
}
}
URLConnectionクラス
URLConnectionはURLへの接続を表し、データの読み書きが可能である。
主要メソッド:
- URLConnection openConnection():接続確立
- InputStream getInputStream():入力ストリーム取得
- OutputStream getOutputStream():出力ストリーム取得
- String getContentType():コンテンツタイプ
package jp.sample.url;
import java.io.*;
import java.net.*;
public class UrlConnectionSample {
public static void main(String[] args) throws Exception {
URL url = new URL("https://www.example.com/api/data");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
try (InputStream is = connection.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(is, "UTF-8"))) {
String line;
StringBuilder content = new StringBuilder();
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
System.out.println("受信データ:\n" + content);
} finally {
connection.disconnect();
}
}
}
URLからのファイルダウンロード
package jp.sample.url;
import java.io.*;
import java.net.*;
public class FileDownloader {
public static void download(String urlString, String savePath)
throws Exception {
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
try (InputStream input = connection.getInputStream();
FileOutputStream output = new FileOutputStream(savePath)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
System.out.println("ダウンロード完了: " + savePath);
}
}
}
まとめ
Javaのネットワークプログラミングにおいては、java.netパッケージに含まれるクラス群を活用することで、TCP・UDP・HTTPなど多様なプロトコルに対応したアプリケーションを開発できる。
IPアドレスとポート番号の組み合わせにより、通信エンドポイントを一意に識別できる。TCPは信頼性の高い接続导向通信を提供し、UDPは高速な非接続通信を提供する。URLクラスを使用することで、Webリソースへのアクセスが容易になる。
適切なプロトコルとAPIを選択し、アプリケーションの要件に応じた通信モデルを設計することが、効率的なネットワークプログラミングの鍵となる。