【Go言語】ChatGPTに聞きながらローカル環境で利用可能なポート番号を取得して表示してみる

「[【Go言語】ChatGPTに聞きながらローカル環境で利用可能なIPアドレスとMacアドレスを取得して一覧表示してみる](https://qiita.com/akase244/items/4ecf06d2a308f880fd54)」の続きということで、今度は開発で使っているUbuntu環境で利用可能な空きのポート番号を取得したいと思い、ChatGPTに聞きながら実装してみた記録です。

---

利用可能なポート番号を取得するにあたり、システムポート（1023番までのウェルノウンポート）は除外することにしたいと思い、ChatGPTにこんな質問をしてみました。

> Go言語を使ってローカル環境で利用可能なポート番号を取得したい。
> なお、システムポートは除外する。

---

以下の回答が得られました。

> Go言語でローカル環境の利用可能なポート番号を取得するには、次の手順が考えられます。
> 
> 1. システムポート(0～1023)を除外。
> 1. ローカルホスト(localhost)でポートが利用可能かを確認。
> 1. 利用可能なポートをリストアップして返す。
> 
> 以下のコードは、システムポートを除外した上で利用可能なポートを取得する例です。

```
package main

import (
	"fmt"
	"net"
)

// CheckPortAvailability checks if a port is available on localhost.
func CheckPortAvailability(port int) bool {
	address := fmt.Sprintf("127.0.0.1:%d", port)
	listener, err := net.Listen("tcp", address)
	if err != nil {
		return false
	}
	defer listener.Close()
	return true
}

func main() {
	var availablePorts []int
	startPort := 1024
	endPort := 65535 // Maximum valid port number

	for port := startPort; port <= endPort; port++ {
		if CheckPortAvailability(port) {
			availablePorts = append(availablePorts, port)
			// Stop early if you only need one port
			if len(availablePorts) == 10 { // For example, get the first 10 available ports
				break
			}
		}
	}

	if len(availablePorts) == 0 {
		fmt.Println("No available ports found.")
	} else {
		fmt.Println("Available ports:", availablePorts)
	}
}
```

### 実装ポイント

> 1. システムポートの除外: ポート番号1024以上からスキャンを開始。
> 1. ポートの利用可能性確認: `net.Listen` を使って特定のポートにバインド可能か確認。
> 1. スキャン範囲調整: 必要なポート数に応じて範囲やスキャン条件を調整可能。
> 
> このコードを実行することで、ローカル環境で利用可能なポート番号を取得できます。ただし、ポートスキャンは時間がかかる場合があるため、実用例に応じて最適化してください。

---

提示されたコードを実行してみると、利用可能なポート番号が10個表示されました。

```
$ go run main.go 
Available ports: [1024 1025 1026 1027 1028 1029 1030 1031 1032 1033]
```

---

取得するポート番号が複数されましたが、今回は1件のみ抽出したいと考えていたのと、システムポート以外でもMySQL(3306番)、開発用（3000番）などのよく利用される番号は避けたいので、次のように返信しました。

> 取得する際は1件のみを抽出したい。
> なお、3000や8080のように別のサービスで利用されやすい番号は除外したい。

---

以下の回答が得られました。

> 以下のコードでは、次の要件を満たしています：
> 
> - システムポートを除外：ポート番号1024以上を対象。
> - 人気のポートを除外：3000や8080など、一般的に利用されるポート番号をスキップ。
> - 1件のみ取得：最初に利用可能なポートを見つけたら終了。

### コード例

```
package main

import (
	"fmt"
	"net"
)

// ExcludedPorts contains commonly used ports that should be skipped.
var ExcludedPorts = map[int]bool{
	3000: true,
	8080: true,
	8000: true,
	8081: true,
	3306: true, // MySQL
	5432: true, // PostgreSQL
	6379: true, // Redis
}

// CheckPortAvailability checks if a port is available on localhost.
func CheckPortAvailability(port int) bool {
	address := fmt.Sprintf("127.0.0.1:%d", port)
	listener, err := net.Listen("tcp", address)
	if err != nil {
		return false
	}
	defer listener.Close()
	return true
}

// FindAvailablePort finds the first available port excluding certain ranges.
func FindAvailablePort(startPort, endPort int) (int, error) {
	for port := startPort; port <= endPort; port++ {
		// Skip excluded ports
		if ExcludedPorts[port] {
			continue
		}
		if CheckPortAvailability(port) {
			return port, nil
		}
	}
	return 0, fmt.Errorf("no available ports found in range %d-%d", startPort, endPort)
}

func main() {
	startPort := 1024
	endPort := 65535 // Maximum valid port number

	port, err := FindAvailablePort(startPort, endPort)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Printf("Available port found: %d\n", port)
}
```

### 実装ポイント

> 1. 一般的なポートの除外: `ExcludedPorts` マップに利用されたくないポート番号を事前に定義し、スキャン中にスキップ。
> 1. 1件だけ取得: 最初に利用可能なポートを見つけたら即座に返却。
> 1. 可読性の向上: ポート範囲や除外リストを簡単にカスタマイズ可能。

### 実行結果例

> 例えば、 `3000` や `8080` が使用中の場合、代わりに他の利用可能なポートを出力します。

```
Available port found: 1025
```

---

サンプルで提示されたコード例では、1024番以降でよく利用されているものが少ないと感じたので、次のように指示をしました。

> 1024番以降で、他のサービスなどで利用されている代表的なポート番号を教えてください。

---

以下の回答が得られました。

> 1024番以降で、代表的なサービスやプロトコルで利用されるポート番号の一部を以下に挙げます。このリストは完全ではありませんが、一般的に利用されるポートを含んでいます。

### 代表的なポート番号と用途

| ポート番号 | サービス / 用途 |
|:-----------|:------------|
| 1024 | ダイナミックポートの開始 |
| 1433 | Microsoft SQL Server |
| 1521 | Oracle Database |
| 2375 | Docker API (非SSL) |
| 2376 | Docker API (SSL) |
| 3000 | 開発用（例: Node.js アプリケーション） |
| 3306 | MySQL |
| 3389 | Windowsリモートデスクトップ（RDP） |
| 5432 | PostgreSQL |
| 5672 | RabbitMQ (AMQP) |
| 5984 | CouchDB |
| 6379 | Redis |
| 8000 | 開発用（例: Python HTTP サーバー） |
| 8080 | HTTP プロキシ、開発用 |
| 8081 | 開発用（例: JBoss、デバッグツール） |
| 8443 | HTTPS (開発用) |
| 9000 | 開発用（例: PHP-FPM、SonarQube） |
| 9092 | Apache Kafka |
| 9200 | Elasticsearch |
| 27017 | MongoDB |

### 補足

> - 動的/プライベートポート範囲: 通常、1024～49151は「登録済みポート」、49152～65535は「ダイナミックポート」として使用されます。
> - 開発用ポート: 3000や8080、8000は開発環境で多く利用されます。
> - DB関連: 3306 (MySQL)、5432 (PostgreSQL)、6379 (Redis) などは有名です。
> - クラウドやコンテナ関連: Docker API (2375, 2376) や Kubernetes (多くのダイナミックポート) も使用します。

### 使用を避けるべきポート

> これらのポートは特定のアプリケーションやサービスで頻繁に使用されるため、避けるのが無難です。不要な競合や接続エラーを回避するため、これらを除外するリスト ( `ExcludedPorts` ) に含めると良いでしょう。

---

ということで、「使用を避けるべきポート」を踏まえたコードを提示してもらうため、以下のように返信しました。

> では、使用を避けるべきポートを除外する形でコードを提示してください。

---

以下の回答が得られました。

> 以下は、使用を避けるべきポートを除外し、利用可能なポートを1件だけ取得するコードです。避けるべきポート番号をリスト化し、それをスキップするようにしています。

### 改良版コード

```
package main

import (
	"fmt"
	"net"
)

// ExcludedPorts contains ports commonly used by other services.
var ExcludedPorts = map[int]bool{
	1024: true, // Dynamic ports start here
	1433: true, // Microsoft SQL Server
	1521: true, // Oracle Database
	2375: true, // Docker API (non-SSL)
	2376: true, // Docker API (SSL)
	3000: true, // Development (e.g., Node.js)
	3306: true, // MySQL
	3389: true, // Remote Desktop Protocol (RDP)
	5432: true, // PostgreSQL
	5672: true, // RabbitMQ
	5984: true, // CouchDB
	6379: true, // Redis
	8000: true, // Development (e.g., Python HTTP server)
	8080: true, // HTTP proxy, development
	8081: true, // Development (e.g., JBoss, tools)
	8443: true, // HTTPS (development)
	9000: true, // Development (e.g., PHP-FPM, SonarQube)
	9092: true, // Apache Kafka
	9200: true, // Elasticsearch
	27017: true, // MongoDB
}

// CheckPortAvailability checks if a port is available on localhost.
func CheckPortAvailability(port int) bool {
	address := fmt.Sprintf("127.0.0.1:%d", port)
	listener, err := net.Listen("tcp", address)
	if err != nil {
		return false
	}
	defer listener.Close()
	return true
}

// FindAvailablePort finds the first available port excluding the defined ports.
func FindAvailablePort(startPort, endPort int) (int, error) {
	for port := startPort; port <= endPort; port++ {
		// Skip excluded ports
		if ExcludedPorts[port] {
			continue
		}
		if CheckPortAvailability(port) {
			return port, nil
		}
	}
	return 0, fmt.Errorf("no available ports found in range %d-%d", startPort, endPort)
}

func main() {
	startPort := 1024
	endPort := 65535 // Maximum valid port number

	port, err := FindAvailablePort(startPort, endPort)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Printf("Available port found: %d\n", port)
}
```

### 特徴

> 1. 除外ポートの定義:
>    - ExcludedPortsマップで、避けたいポート番号を明示的に指定。
>    - 必要に応じてこのリストにポート番号を追加可能。
>2. ポート確認ロジック:
>    - CheckPortAvailability関数で、指定ポートが利用可能か確認。
>    - 利用可能でない場合は次のポートをチェック。
>3. 効率性:
>    - 最初に見つかった利用可能ポートを即座に返すため、全ポートをスキャンする必要なし。

---

実行してみると以下のように、現在利用可能な空きのポート番号が得られました。

```
$ go run main.go 
Available port found: 1025
```

---

期待した結果を得ることができましたが、現状のコードだと1024〜65535番の中で利用可能な若い番号を取得するため、以下のように複数回実行するといつも同じような結果が得られる状態になります。

```
$ go run main.go 
Available port found: 1025

$ go run main.go 
Available port found: 1025

$ go run main.go 
Available port found: 1025
```

これを避けるため、以下のように追加で指示を与えました。

> 開始を1024番、終了を65535番としてループ処理を行っているため、結果が空いている番号の昇順に得られることになります。
> これではいつも同じような番号が取得されることになるため、ランダムに値を取得できないでしょうか。

---

以下の回答が得られました。

> ランダムにポート番号を取得するためには、以下の方法を取ることができます：
> 
> 1. ポート番号の範囲をランダムにシャッフルする:
>     - 1024から65535までのポート番号をリストに格納し、ランダムに並べ替えます。
>     - 並べ替えたリストから順にポートの空き状態を確認します。
>
> 以下にランダムにポート番号を取得するコードを示します。

### 改良版コード（ランダム取得）

```
package main

import (
	"fmt"
	"math/rand"
	"net"
	"time"
)

// ExcludedPorts contains ports commonly used by other services.
var ExcludedPorts = map[int]bool{
	1024: true, // Dynamic ports start here
	1433: true, // Microsoft SQL Server
	1521: true, // Oracle Database
	2375: true, // Docker API (non-SSL)
	2376: true, // Docker API (SSL)
	3000: true, // Development (e.g., Node.js)
	3306: true, // MySQL
	3389: true, // Remote Desktop Protocol (RDP)
	5432: true, // PostgreSQL
	5672: true, // RabbitMQ
	5984: true, // CouchDB
	6379: true, // Redis
	8000: true, // Development (e.g., Python HTTP server)
	8080: true, // HTTP proxy, development
	8081: true, // Development (e.g., JBoss, tools)
	8443: true, // HTTPS (development)
	9000: true, // Development (e.g., PHP-FPM, SonarQube)
	9092: true, // Apache Kafka
	9200: true, // Elasticsearch
	27017: true, // MongoDB
}

// CheckPortAvailability checks if a port is available on localhost.
func CheckPortAvailability(port int) bool {
	address := fmt.Sprintf("127.0.0.1:%d", port)
	listener, err := net.Listen("tcp", address)
	if err != nil {
		return false
	}
	defer listener.Close()
	return true
}

// GenerateRandomPorts generates a shuffled slice of port numbers in a given range.
func GenerateRandomPorts(startPort, endPort int) []int {
	ports := make([]int, 0, endPort-startPort+1)
	for port := startPort; port <= endPort; port++ {
		ports = append(ports, port)
	}
	rand.Seed(time.Now().UnixNano())
	rand.Shuffle(len(ports), func(i, j int) {
		ports[i], ports[j] = ports[j], ports[i]
	})
	return ports
}

// FindRandomAvailablePort finds an available port from a randomized list.
func FindRandomAvailablePort(startPort, endPort int) (int, error) {
	ports := GenerateRandomPorts(startPort, endPort)
	for _, port := range ports {
		// Skip excluded ports
		if ExcludedPorts[port] {
			continue
		}
		if CheckPortAvailability(port) {
			return port, nil
		}
	}
	return 0, fmt.Errorf("no available ports found in range %d-%d", startPort, endPort)
}

func main() {
	startPort := 1024
	endPort := 65535 // Maximum valid port number

	port, err := FindRandomAvailablePort(startPort, endPort)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Printf("Available port found: %d\n", port)
}
```

### 特徴

> 1. ランダム性の導入:
>     - GenerateRandomPorts関数でポート番号をランダムにシャッフル。
>     - math/randパッケージを利用してランダム性を確保。
> 2. 既存機能の再利用:
>     - CheckPortAvailability関数で利用可能なポートをチェック。
> 3. 公平な分布:
>     - ポート番号が昇順に偏ることなく、ランダムに取得される。

---

複数回実行してみると、今度は毎回違うポート番号を得ることができました。

```
$ go run main.go 
Available port found: 19727

$ go run main.go 
Available port found: 50652

$ go run main.go 
Available port found: 1103
```

---

よく考えてみると、「除外対象のポート」に本来追加すべきポート番号が指定できていない場合は意図した結果が得られない可能性があるため、「システムポートとよく利用されるポート番号を除外する」という指定ではなく、「ダイナミックポートの範囲(49152～65535番)」のみを対象とするように、以下の指示を行いました。

> ExcludedPortsでよく利用される可能性のあるポート番号を除外していますが、除外対象に漏れがあると意図した結果にならないため、「登録済みポート」は全て除外するようにして、ダイナミックポートの範囲でランダムに値を取得するように変更したいです。

---

以下の回答が得られました。

> 「登録済みポート」をすべて除外し、ダイナミックポートの範囲（49152～65535）内でランダムに値を取得するコードを以下に示します。

### ダイナミックポートのみを対象とするコード

```
package main

import (
	"fmt"
	"math/rand"
	"net"
	"time"
)

// CheckPortAvailability checks if a port is available on localhost.
func CheckPortAvailability(port int) bool {
	address := fmt.Sprintf("127.0.0.1:%d", port)
	listener, err := net.Listen("tcp", address)
	if err != nil {
		return false
	}
	defer listener.Close()
	return true
}

// GenerateRandomDynamicPort generates a random port number within the dynamic port range (49152–65535).
func GenerateRandomDynamicPort() int {
	rand.Seed(time.Now().UnixNano())
	return rand.Intn(65535-49152+1) + 49152
}

// FindRandomAvailablePort finds an available port within the dynamic port range.
func FindRandomAvailablePort(maxAttempts int) (int, error) {
	for i := 0; i < maxAttempts; i++ {
		port := GenerateRandomDynamicPort()
		if CheckPortAvailability(port) {
			return port, nil
		}
	}
	return 0, fmt.Errorf("no available ports found after %d attempts", maxAttempts)
}

func main() {
	maxAttempts := 100 // Number of attempts to find an available port

	port, err := FindRandomAvailablePort(maxAttempts)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Printf("Available dynamic port found: %d\n", port)
}
```

### 特徴

> 1. ダイナミックポート範囲の利用:
>     - ダイナミックポートの範囲は 49152～65535 とされています。
>     - GenerateRandomDynamicPort 関数でこの範囲内のランダムなポート番号を生成します。
> 2. 登録済みポートの完全排除:
>     - 登録済みポート（0～49151）はそもそも対象外とし、漏れを防ぎます。
> 3. ランダム性の確保:
>     - 実行ごとに異なるランダムなポート番号を選びます。
> 4. 試行回数の設定:
>     - 利用可能なポートが見つかるまで最大試行回数を設定可能（例: 100回）。

---

複数回実行してみると、49152〜65535の範囲内で毎回違うポート番号を得ることができました。

```
$ go run main.go 
Available dynamic port found: 65354

$ go run main.go 
Available dynamic port found: 50861

$ go run main.go 
Available dynamic port found: 62143
```