Javaネットワークプログラミング完全ガイド

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

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段階で構成される:

  1. クライアントがサーバーに接続要求(SYN)を送信
  2. サーバーが確認応答(SYN+ACK)を返信
  3. クライアントが最終確認応答(ACK)を送信

この过程が完了すると、接続が確立され数据传输が開始される。

4wayハンドシェイク(接続解除)

TCP接続の解除は以下の4段階で構成される:

  1. クライアントが接続終了要求(FIN)を送信( полуクローズ状態)
  2. サーバーが最終データを送信しACKを返信
  3. サーバーがFINを送信
  4. クライアントが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でサーバーに接続を要求する。

開発ステップ

クライアント側:

  1. Socketを作成(サーバーIPとポート指定)
  2. 入力/出力ストリームを取得
  3. ストリームを通じてデータの読み書き
  4. Socketを关闭(接続解放)

サーバー側:

  1. ServerSocketを作成(ポート指定)
  2. accept()で接続をaccept
  3. 入出力ストリームを取得してデータ通信
  4. 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以下
  • 確認応答なし
  • リアルタイム性が求められるアプリケーションに適する

開発ステップ

送信側:

  1. DatagramSocketを作成
  2. DatagramPacketを作成(データ、サイズ、宛先IP/ポート)
  3. send()で送信
  4. Socketを关闭

受信側:

  1. DatagramSocketを作成(listenポート指定)
  2. DatagramPacketを作成(受信バッファ)
  3. receive()で受信(ブロックする)
  4. 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を選択し、アプリケーションの要件に応じた通信モデルを設計することが、効率的なネットワークプログラミングの鍵となる。

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

6月25日 18:57 投稿