[PHP]getservbyname関数総合ガイド – ネットワークプログラミングの強力な味方

PHP

こんにちは、PHPエンジニアの皆さん!今回は「getservbyname()」関数について徹底解説します。この関数はネットワークプログラミングにおいて非常に便利ですが、使い方をしっかり理解している方は意外と少ないかもしれません。

getservbyname()とは?

getservbyname()関数は、サービス名とプロトコルからそのサービスの標準ポート番号を取得するためのPHP組み込み関数です。この関数は、OSのサービスデータベース(UNIXシステムでは/etc/servicesファイル)を参照してポート番号を返します。

基本的な構文は次のとおりです:

int getservbyname(string $service, string $protocol)
  • $service: ポート番号を知りたいサービスの名前(例:’http’, ‘ftp’, ‘smtp’など)
  • $protocol: 使用するプロトコル(’tcp’または’udp’)

使用例

基本的な使い方

// HTTPサービスのポート番号を取得
$http_port = getservbyname('http', 'tcp');
echo "HTTPのポート番号: " . $http_port; // 出力: 80

// FTPサービスのポート番号を取得
$ftp_port = getservbyname('ftp', 'tcp');
echo "FTPのポート番号: " . $ftp_port; // 出力: 21

一般的なサービスのポート番号を一覧表示

$services = [
    ['http', 'tcp'],
    ['https', 'tcp'],
    ['ftp', 'tcp'],
    ['ssh', 'tcp'],
    ['smtp', 'tcp'],
    ['pop3', 'tcp'],
    ['imap', 'tcp'],
    ['dns', 'udp'],
    ['ntp', 'udp'],
    ['mysql', 'tcp']
];

echo "<table border='1'>
        <tr>
            <th>サービス名</th>
            <th>プロトコル</th>
            <th>ポート番号</th>
        </tr>";

foreach ($services as $service) {
    $port = @getservbyname($service[0], $service[1]);
    echo "<tr>
            <td>{$service[0]}</td>
            <td>{$service[1]}</td>
            <td>" . ($port !== false ? $port : "未定義") . "</td>
          </tr>";
}

echo "</table>";

サーバー接続時の活用

function connectToService($service, $host, $protocol = 'tcp') {
    $port = getservbyname($service, $protocol);
    
    if ($port === false) {
        throw new Exception("サービス '{$service}' は未定義です。");
    }
    
    echo "{$host}の{$service}サービス(ポート{$port})に接続しています...\n";
    
    // 接続処理
    $socket = @fsockopen($host, $port, $errno, $errstr, 30);
    
    if (!$socket) {
        throw new Exception("接続エラー: $errstr ($errno)");
    }
    
    echo "接続成功!\n";
    return $socket;
}

try {
    $socket = connectToService('http', 'example.com');
    fwrite($socket, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
    $response = fread($socket, 1024);
    echo "サーバーからの応答: " . substr($response, 0, 50) . "...\n";
    fclose($socket);
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}

実用的なアプリケーション例

ポートスキャナー

function scanPorts($host, $services) {
    echo "ホスト {$host} のポートスキャン結果:\n\n";
    echo "サービス\tプロトコル\tポート\t状態\n";
    echo "---------------------------------------------\n";
    
    foreach ($services as $service) {
        list($name, $protocol) = $service;
        $port = @getservbyname($name, $protocol);
        
        if ($port === false) {
            continue; // 未定義のサービスはスキップ
        }
        
        $socket = @fsockopen($host, $port, $errno, $errstr, 1);
        
        if ($socket) {
            echo "{$name}\t{$protocol}\t{$port}\t開いています\n";
            fclose($socket);
        } else {
            echo "{$name}\t{$protocol}\t{$port}\t閉じています\n";
        }
    }
}

// 一般的なサービスのリスト
$common_services = [
    ['http', 'tcp'],
    ['https', 'tcp'],
    ['ftp', 'tcp'],
    ['ssh', 'tcp'],
    ['smtp', 'tcp'],
    ['pop3', 'tcp']
];

// 使用例
scanPorts('localhost', $common_services);

サービス検出ツール

class ServiceDiscovery {
    private $target;
    private $timeout;
    private $serviceList;
    
    public function __construct($target, $timeout = 1) {
        $this->target = $target;
        $this->timeout = $timeout;
        
        // 一般的なサービスとプロトコルのマッピング
        $this->serviceList = [
            'web' => [
                ['http', 'tcp'],
                ['https', 'tcp']
            ],
            'mail' => [
                ['smtp', 'tcp'],
                ['pop3', 'tcp'],
                ['imap', 'tcp']
            ],
            'file' => [
                ['ftp', 'tcp'],
                ['sftp', 'tcp'],
                ['tftp', 'udp']
            ],
            'remote' => [
                ['ssh', 'tcp'],
                ['telnet', 'tcp'],
                ['rdp', 'tcp']
            ],
            'database' => [
                ['mysql', 'tcp'],
                ['postgresql', 'tcp'],
                ['mongodb', 'tcp'],
                ['redis', 'tcp']
            ]
        ];
    }
    
    public function discoverServices() {
        $result = [];
        
        foreach ($this->serviceList as $category => $services) {
            $result[$category] = [];
            
            foreach ($services as $service) {
                list($name, $protocol) = $service;
                $port = @getservbyname($name, $protocol);
                
                if ($port === false) {
                    continue;
                }
                
                $status = $this->checkPort($port, $protocol);
                
                if ($status === true) {
                    $result[$category][] = [
                        'name' => $name,
                        'protocol' => $protocol,
                        'port' => $port,
                        'status' => 'OPEN'
                    ];
                    
                    // サービスバージョンを検出(簡易版)
                    $version = $this->detectVersion($name, $port);
                    if ($version) {
                        $result[$category][count($result[$category]) - 1]['version'] = $version;
                    }
                }
            }
        }
        
        return $result;
    }
    
    private function checkPort($port, $protocol) {
        $errno = 0;
        $errstr = '';
        
        if ($protocol === 'tcp') {
            $socket = @fsockopen($this->target, $port, $errno, $errstr, $this->timeout);
            
            if ($socket) {
                fclose($socket);
                return true;
            }
        } else if ($protocol === 'udp') {
            // UDPでのポートチェックは複雑なので、実環境では別の方法が必要
            // ここでは簡易的な実装
            return false;
        }
        
        return false;
    }
    
    private function detectVersion($service, $port) {
        // 実際のバージョン検出は複雑でサービスごとに異なる
        // ここでは簡易的な実装のみ(HTTPの場合のみ)
        
        if ($service === 'http') {
            $socket = @fsockopen($this->target, $port, $errno, $errstr, $this->timeout);
            
            if ($socket) {
                fwrite($socket, "HEAD / HTTP/1.0\r\nHost: {$this->target}\r\n\r\n");
                $response = '';
                
                while (!feof($socket)) {
                    $response .= fgets($socket, 128);
                    // ヘッダーだけを取得
                    if (strpos($response, "\r\n\r\n") !== false) {
                        break;
                    }
                }
                
                fclose($socket);
                
                // サーバータイプを抽出
                if (preg_match('/Server: ([^\r\n]+)/i', $response, $matches)) {
                    return $matches[1];
                }
            }
        }
        
        return null;
    }
    
    public function generateReport() {
        $services = $this->discoverServices();
        $report = "=== サービス検出レポート: {$this->target} ===\n\n";
        
        foreach ($services as $category => $items) {
            if (empty($items)) {
                continue;
            }
            
            $report .= "## {$category}サービス\n";
            
            foreach ($items as $item) {
                $report .= "- {$item['name']} ({$item['protocol']}/{$item['port']}): {$item['status']}";
                
                if (isset($item['version'])) {
                    $report .= " - バージョン: {$item['version']}";
                }
                
                $report .= "\n";
            }
            
            $report .= "\n";
        }
        
        return $report;
    }
}

// 使用例
$discovery = new ServiceDiscovery('localhost');
echo $discovery->generateReport();

逆引き:getservbyport()

getservbyname()の逆の操作を行いたい場合、getservbyport()関数を使用します。この関数はポート番号とプロトコルからサービス名を取得します。

function displayServiceInfo($ports) {
    echo "<table border='1'>
            <tr>
                <th>ポート番号</th>
                <th>プロトコル</th>
                <th>サービス名</th>
            </tr>";
    
    foreach ($ports as $port_info) {
        list($port, $protocol) = $port_info;
        $service = getservbyport($port, $protocol);
        
        echo "<tr>
                <td>{$port}</td>
                <td>{$protocol}</td>
                <td>" . ($service !== false ? $service : "未登録") . "</td>
              </tr>";
    }
    
    echo "</table>";
}

// 一般的なポート番号のリスト
$common_ports = [
    [80, 'tcp'],
    [443, 'tcp'],
    [21, 'tcp'],
    [22, 'tcp'],
    [25, 'tcp'],
    [110, 'tcp'],
    [3306, 'tcp'],
    [1433, 'tcp'],
    [5432, 'tcp'],
    [27017, 'tcp']
];

displayServiceInfo($common_ports);

エラー処理とよくある問題

getservbyname()関数は、サービスが見つからない場合にfalseを返します。エラー処理を適切に行うことが重要です。

function safeGetPort($service, $protocol) {
    $port = @getservbyname($service, $protocol);
    
    if ($port === false) {
        // サービスが見つからない場合のフォールバック値
        $defaults = [
            'http' => 80,
            'https' => 443,
            'ftp' => 21,
            'ssh' => 22,
            'smtp' => 25,
            'pop3' => 110,
            'imap' => 143,
            'mysql' => 3306,
            'postgresql' => 5432,
            'mongodb' => 27017
        ];
        
        $key = $service . ($protocol !== 'tcp' ? '_' . $protocol : '');
        
        if (isset($defaults[$key])) {
            return $defaults[$key];
        }
        
        throw new Exception("サービス '{$service}' のポート番号が見つかりませんでした。");
    }
    
    return $port;
}

try {
    echo "HTTP: " . safeGetPort('http', 'tcp') . "\n";
    echo "未知のサービス: " . safeGetPort('unknown_service', 'tcp') . "\n";
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}

プラットフォーム依存性

getservbyname()関数は、OSのサービスデータベースに依存しています。そのため、異なるOSで異なる結果が返される可能性があります。クロスプラットフォームのアプリケーションを開発する場合は、この点に注意してください。

function isServiceAvailable($service, $protocol) {
    return @getservbyname($service, $protocol) !== false;
}

echo "HTTPサービスは利用可能: " . (isServiceAvailable('http', 'tcp') ? 'はい' : 'いいえ') . "\n";
echo "HTTPSサービスは利用可能: " . (isServiceAvailable('https', 'tcp') ? 'はい' : 'いいえ') . "\n";
echo "MySQLサービスは利用可能: " . (isServiceAvailable('mysql', 'tcp') ? 'はい' : 'いいえ') . "\n";

セキュリティの考慮事項

getservbyname()関数自体は安全ですが、取得したポート番号を使用してネットワーク接続を行う場合、適切なセキュリティ対策を講じることが重要です。

class SecureConnection {
    private $allowedServices = ['http', 'https', 'ssh'];
    private $blockedPorts = [23, 25, 69]; // telnet, smtp, tftp
    
    public function connect($service, $host, $protocol = 'tcp') {
        // サービス名のバリデーション
        if (!in_array($service, $this->allowedServices)) {
            throw new Exception("サービス '{$service}' への接続は許可されていません。");
        }
        
        // ポート番号の取得
        $port = @getservbyname($service, $protocol);
        
        if ($port === false) {
            throw new Exception("サービス '{$service}' は未定義です。");
        }
        
        // ブロックされたポートのチェック
        if (in_array($port, $this->blockedPorts)) {
            throw new Exception("ポート {$port} への接続はセキュリティ上の理由でブロックされています。");
        }
        
        // ホスト名のバリデーション
        if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
            throw new Exception("無効なホスト名です。");
        }
        
        // 接続処理
        $socket = @fsockopen($host, $port, $errno, $errstr, 30);
        
        if (!$socket) {
            throw new Exception("接続エラー: $errstr ($errno)");
        }
        
        return $socket;
    }
}

// 使用例
$secureConn = new SecureConnection();

try {
    $socket = $secureConn->connect('https', 'example.com');
    fclose($socket);
    echo "接続成功!\n";
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}

応用:サービス検出ライブラリ

より高度なサービス検出機能を提供するライブラリを作成することもできます。

class ServiceRegistry {
    private static $customServices = [];
    
    public static function registerService($name, $protocol, $port) {
        $key = self::getServiceKey($name, $protocol);
        self::$customServices[$key] = $port;
    }
    
    public static function unregisterService($name, $protocol) {
        $key = self::getServiceKey($name, $protocol);
        
        if (isset(self::$customServices[$key])) {
            unset(self::$customServices[$key]);
            return true;
        }
        
        return false;
    }
    
    public static function getServicePort($name, $protocol) {
        // まずカスタムサービスをチェック
        $key = self::getServiceKey($name, $protocol);
        
        if (isset(self::$customServices[$key])) {
            return self::$customServices[$key];
        }
        
        // 次にシステムのサービスをチェック
        $port = @getservbyname($name, $protocol);
        
        return $port !== false ? $port : null;
    }
    
    public static function getServiceName($port, $protocol) {
        // まずカスタムサービスをチェック
        foreach (self::$customServices as $key => $value) {
            if ($value == $port && strpos($key, $protocol) !== false) {
                return str_replace("_{$protocol}", "", $key);
            }
        }
        
        // 次にシステムのサービスをチェック
        $name = @getservbyport($port, $protocol);
        
        return $name !== false ? $name : null;
    }
    
    private static function getServiceKey($name, $protocol) {
        return "{$name}_{$protocol}";
    }
    
    public static function listAllServices() {
        $result = [];
        
        // システムのwell-knownポートをスキャン(効率のため一部のみ)
        for ($port = 1; $port <= 1024; $port++) {
            $tcp_name = @getservbyport($port, 'tcp');
            $udp_name = @getservbyport($port, 'udp');
            
            if ($tcp_name !== false) {
                $result[] = [
                    'name' => $tcp_name,
                    'protocol' => 'tcp',
                    'port' => $port,
                    'source' => 'system'
                ];
            }
            
            if ($udp_name !== false && $udp_name !== $tcp_name) {
                $result[] = [
                    'name' => $udp_name,
                    'protocol' => 'udp',
                    'port' => $port,
                    'source' => 'system'
                ];
            }
        }
        
        // カスタムサービスを追加
        foreach (self::$customServices as $key => $port) {
            list($name, $protocol) = explode('_', $key);
            $result[] = [
                'name' => $name,
                'protocol' => $protocol,
                'port' => $port,
                'source' => 'custom'
            ];
        }
        
        return $result;
    }
}

// 使用例
// カスタムサービスの登録
ServiceRegistry::registerService('myapp', 'tcp', 8080);
ServiceRegistry::registerService('mydb', 'tcp', 27018);

// サービスポートの取得
echo "HTTP: " . ServiceRegistry::getServicePort('http', 'tcp') . "\n";
echo "MyApp: " . ServiceRegistry::getServicePort('myapp', 'tcp') . "\n";

// ポートからサービス名を取得
echo "ポート80のサービス: " . ServiceRegistry::getServiceName(80, 'tcp') . "\n";
echo "ポート8080のサービス: " . ServiceRegistry::getServiceName(8080, 'tcp') . "\n";

// すべてのサービスのリスト(省略)
// $allServices = ServiceRegistry::listAllServices();

まとめ

getservbyname()関数は、PHPでネットワークプログラミングを行う際に非常に役立つツールです。この関数を使用することで、サービス名からポート番号を効率的に取得し、ネットワーク接続やサービス検出などの機能を簡単に実装できます。

この関数の主な利点は:

  1. 標準サービスのポート番号を自動的に取得できる
  2. プラットフォーム間の互換性(OSのサービスデータベースに基づく)
  3. ネットワークアプリケーションの柔軟性向上

ただし、プラットフォーム依存性やエラー処理の必要性に注意しながら使用することが重要です。

効果的なネットワークプログラミングのために、getservbyname()getservbyport()関数をぜひ活用してみてください!何か質問があれば、お気軽にどうぞ。

タイトルとURLをコピーしました