第1章:ネットワークの基本要素 - Node、Link、Packet

1.1 プロジェクト構造

go-network-programming/
├── go.mod
├── go.sum
├── main.go
├── packet.go
├── node.go
└── link.go

この章では、ネットワークの基本的な構成要素であるノードリンクパケットをGo言語で実装します。実際のネットワーク機器と同じように、複数のプロセスが並行して動作し、channelを通じてパケットを送受信する仕組みを構築します。

1.2 パケットの実装

パケットは、ネットワークで送信される情報の基本単位です。送信元、宛先、データ本体、タイムスタンプなどの情報を含みます。

ファイル名: ./packet.go

package main

import (
    "fmt"
    "time"
    
    "github.com/google/uuid"
)

// Packet はネットワークで送信される基本単位を表現する
// 実際のTCP/IPパケットのように、ヘッダ情報とペイロードを持つ
type Packet struct {
    ID          string    // パケットの一意識別子
    Source      string    // 送信元ノードの名前
    Destination string    // 宛先ノードの名前
    Data        []byte    // 実際のデータ(ペイロード)
    Size        int       // データサイズ(バイト)
    Timestamp   time.Time // パケット生成時刻
}

// NewPacket は新しいパケットを生成する
// 実際のネットワークスタックでパケットが生成される処理を模倣
func NewPacket(source, destination string, data []byte) *Packet {
    return &Packet{
        ID:          uuid.New().String(),
        Source:      source,
        Destination: destination,
        Data:        data,
        Size:        len(data),
        Timestamp:   time.Now(),
    }
}

// String はパケットの文字列表現を返す(デバッグ用)
func (p *Packet) String() string {
    return fmt.Sprintf("Packet{ID: %s, From: %s, To: %s, Size: %d bytes}", 
        p.ID[:8], p.Source, p.Destination, p.Size)
}

1.3 ノードの実装

ノードは、ネットワーク上のデバイス(PC、スマートフォン、ルーターなど)を表現します。パケットの送受信機能を持ち、複数のリンクに接続できます。

ファイル名: ./node.go

package main

import (
    "fmt"
    "time"
    
    "github.com/google/uuid"
)

// Node はネットワーク上のデバイスを表現する
// 実際のコンピューターやルーターのように、パケットを処理する
type Node struct {
    ID       string           // ノードの一意識別子
    Name     string           // ノードの名前(人間が読める形式)
    inbox    chan *Packet     // 受信用パケットキュー
    outbox   chan *Packet     // 送信用パケットキュー  
    links    map[string]*Link // 接続されているリンクのリスト
    running  bool             // ノードが動作中かどうかのフラグ
}

// NewNode は新しいノードを生成する
// メモリ上にネットワークノードのインスタンスを作成
func NewNode(name string) *Node {
    return &Node{
        ID:      uuid.New().String(),
        Name:    name,
        inbox:   make(chan *Packet, 100), // バッファサイズ100のキュー
        outbox:  make(chan *Packet, 100),
        links:   make(map[string]*Link),
        running: false,
    }
}

// Start はノードの動作を開始する
// バックグラウンドでパケット処理を実行するgoroutineを起動
func (n *Node) Start() {
    if n.running {
        return
    }
    n.running = true
    
    // パケット処理用goroutineを開始
    // 実際のネットワークカードのように、バックグラウンドで動作
    go n.processPackets()
    fmt.Printf("Node %s started\n", n.Name)
}

// Stop はノードの動作を停止する
// 全てのチャネルを閉じてリソースを解放
func (n *Node) Stop() {
    if !n.running {
        return
    }
    n.running = false
    close(n.inbox)
    close(n.outbox)
    fmt.Printf("Node %s stopped\n", n.Name)
}

// Send は指定された宛先にパケットを送信する
// 実際のsocket送信のように、適切なルートを探してパケットを送出
func (n *Node) Send(destination string, data []byte) error {
    if !n.running {
        return fmt.Errorf("node %s is not running", n.Name)
    }
    
    packet := NewPacket(n.Name, destination, data)
    
    // 宛先に到達可能なリンクを探す(シンプルなルーティング)
    for _, link := range n.links {
        if link.CanReach(destination) {
            return link.Send(packet)
        }
    }
    
    return fmt.Errorf("no route to destination %s", destination)
}

// Receive は受信したパケットを返す
// アプリケーションがソケットから読み取る動作を模倣
func (n *Node) Receive() *Packet {
    if !n.running {
        return nil
    }
    
    select {
    case packet := <-n.inbox:
        return packet
    case <-time.After(1 * time.Second):
        return nil // タイムアウト
    }
}

// AddLink はノードにリンクを追加する
// ネットワークインターフェースを追加する動作に相当
func (n *Node) AddLink(link *Link) {
    n.links[link.ID] = link
    fmt.Printf("Link added to node %s\n", n.Name)
}

// processPackets はパケット処理のメインループ
// 実際のNIC(Network Interface Card)のパケット処理を模倣
func (n *Node) processPackets() {
    for n.running {
        select {
        case packet := <-n.outbox:
            if packet != nil {
                fmt.Printf("Node %s processing outgoing: %s\n", n.Name, packet)
            }
        default:
            time.Sleep(10 * time.Millisecond)
        }
    }
}

// String はノードの文字列表現を返す(デバッグ用)
func (n *Node) String() string {
    return fmt.Sprintf("Node{Name: %s, ID: %s, Links: %d}", 
        n.Name, n.ID[:8], len(n.links))
}

1.4 リンクの実装

リンクは、ノード間の物理的または論理的な接続を表現します。帯域幅、遅延、パケット損失などのネットワーク特性を持ちます。

ファイル名: ./link.go

package main

import (
    "fmt"
    "time"
    
    "github.com/google/uuid"
)

// Link はノード間の接続を表現する
// イーサネットケーブルや無線接続のような物理メディアを模倣
type Link struct {
    ID          string        // リンクの一意識別子
    NodeA       *Node         // 接続されているノードA
    NodeB       *Node         // 接続されているノードB
    Bandwidth   int           // 帯域幅(Mbps)
    Latency     time.Duration // 遅延時間
    PacketLoss  float64       // パケット損失率(0.0-1.0)
    channel     chan *Packet  // パケット転送用チャネル
    running     bool          // リンクが稼働中かどうか
}

// NewLink は新しいリンクを生成する
// 2つのノード間にネットワーク接続を確立
func NewLink(nodeA, nodeB *Node, bandwidth int, latency time.Duration) *Link {
    link := &Link{
        ID:         uuid.New().String(),
        NodeA:      nodeA,
        NodeB:      nodeB,
        Bandwidth:  bandwidth,
        Latency:    latency,
        PacketLoss: 0.0, // 初期状態では損失なし
        channel:    make(chan *Packet, 50), // バッファ付きチャネル
        running:    false,
    }
    
    // 両方のノードにこのリンクを登録
    nodeA.AddLink(link)
    nodeB.AddLink(link)
    
    return link
}

// Start はリンクの動作を開始する
// パケット転送処理を行うgoroutineを起動
func (l *Link) Start() {
    if l.running {
        return
    }
    l.running = true
    
    // パケット転送用goroutineを開始
    // 実際のスイッチやハブのパケット転送機能を模倣
    go l.forwardPackets()
    fmt.Printf("Link between %s and %s started\n", l.NodeA.Name, l.NodeB.Name)
}

// Stop はリンクの動作を停止する
func (l *Link) Stop() {
    if !l.running {
        return
    }
    l.running = false
    close(l.channel)
    fmt.Printf("Link between %s and %s stopped\n", l.NodeA.Name, l.NodeB.Name)
}

// Send はリンクを通じてパケットを送信する
// 実際のネットワークカードからの送信を模倣
func (l *Link) Send(packet *Packet) error {
    if !l.running {
        return fmt.Errorf("link is not running")
    }
    
    // チャネルにパケットを送信(非ブロッキング)
    select {
    case l.channel <- packet:
        return nil
    case <-time.After(100 * time.Millisecond):
        return fmt.Errorf("link congested") // 輻輳状態
    }
}

// CanReach は指定された宛先に到達可能かチェックする
// シンプルなルーティング判定(直接接続のみ)
func (l *Link) CanReach(destination string) bool {
    return l.NodeA.Name == destination || l.NodeB.Name == destination
}

// forwardPackets はパケット転送のメインループ
// スイッチやルーターのパケット転送処理を模倣
func (l *Link) forwardPackets() {
    for l.running {
        select {
        case packet := <-l.channel:
            if packet != nil {
                // ネットワーク遅延をシミュレート
                // 実際の光ファイバーや銅線の伝送遅延を模倣
                time.Sleep(l.Latency)
                
                // 宛先ノードを決定
                var targetNode *Node
                if packet.Destination == l.NodeA.Name {
                    targetNode = l.NodeA
                } else if packet.Destination == l.NodeB.Name {
                    targetNode = l.NodeB
                } else {
                    // ブロードキャスト的な動作:送信元でないノードに転送
                    if packet.Source != l.NodeA.Name {
                        targetNode = l.NodeA
                    } else {
                        targetNode = l.NodeB
                    }
                }
                
                // パケットを宛先ノードの受信キューに配送
                if targetNode != nil && targetNode.running {
                    select {
                    case targetNode.inbox <- packet:
                        fmt.Printf("Packet forwarded to %s: %s\n", 
                            targetNode.Name, packet)
                    case <-time.After(10 * time.Millisecond):
                        fmt.Printf("Failed to deliver packet to %s (queue full)\n", 
                            targetNode.Name)
                    }
                }
            }
        default:
            time.Sleep(1 * time.Millisecond)
        }
    }
}

// String はリンクの文字列表現を返す(デバッグ用)
func (l *Link) String() string {
    return fmt.Sprintf("Link{%s <-> %s, %dMbps, %v latency}", 
        l.NodeA.Name, l.NodeB.Name, l.Bandwidth, l.Latency)
}

1.5 メイン関数とテスト実行

実際にノード間でパケットを送受信するサンプルコードです。

ファイル名: ./main.go

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("=== ネットワークシミュレーション開始 ===")
    
    // 2つのノードを作成(AliceとBobという名前)
    alice := NewNode("Alice")
    bob := NewNode("Bob")
    
    // ノード間にリンクを作成(100Mbps、10ms遅延)
    // これは実際のイーサネット接続に相当
    link := NewLink(alice, bob, 100, 10*time.Millisecond)
    
    // 全システムを起動
    alice.Start()
    bob.Start()
    link.Start()
    
    // AliceからBobにメッセージを送信
    message := []byte("Hello, Bob! This is Alice.")
    fmt.Printf("Alice sends message: %s\n", string(message))
    
    err := alice.Send("Bob", message)
    if err != nil {
        fmt.Printf("Error sending message: %v\n", err)
        return
    }
    
    // ネットワーク遅延を考慮して待機
    time.Sleep(50 * time.Millisecond)
    
    // Bobがメッセージを受信
    received := bob.Receive()
    if received != nil {
        fmt.Printf("Bob received: %s\n", string(received.Data))
        fmt.Printf("Packet details: %s\n", received)
    } else {
        fmt.Println("No message received")
    }
    
    // 逆方向の通信もテスト
    response := []byte("Hi Alice! Nice to hear from you.")
    fmt.Printf("Bob sends response: %s\n", string(response))
    
    err = bob.Send("Alice", response)
    if err != nil {
        fmt.Printf("Error sending response: %v\n", err)
    } else {
        time.Sleep(50 * time.Millisecond)
        received = alice.Receive()
        if received != nil {
            fmt.Printf("Alice received: %s\n", string(received.Data))
        }
    }
    
    // システムを正常に終了
    fmt.Println("=== システム終了 ===")
    alice.Stop()
    bob.Stop()
    link.Stop()
}

1.6 実行方法

# プロジェクトディレクトリで実行
go run .

1.7 期待される出力例

=== ネットワークシミュレーション開始 ===
Link added to node Alice
Link added to node Bob
Node Alice started
Node Bob started
Link between Alice and Bob started
Alice sends message: Hello, Bob! This is Alice.
Packet forwarded to Bob: Packet{ID: a1b2c3d4, From: Alice, To: Bob, Size: 27 bytes}
Bob received: Hello, Bob! This is Alice.
Packet details: Packet{ID: a1b2c3d4, From: Alice, To: Bob, Size: 27 bytes}
Bob sends response: Hi Alice! Nice to hear from you.
Packet forwarded to Alice: Packet{ID: e5f6g7h8, From: Bob, To: Alice, Size: 32 bytes}
Alice received: Hi Alice! Nice to hear from you.
=== システム終了 ===
Node Alice stopped
Node Bob stopped
Link between Alice and Bob stopped

1.8 重要な概念の解説

1.8.1 並行処理

  • 各ノードとリンクは独立したgoroutineで動作
  • 実際のネットワーク機器のように、同時に複数の処理を実行

1.8.2 チャネル通信

  • パケットの送受信はGoのチャネルを使用
  • バッファ付きチャネルで輻輳制御を模倣

1.8.3 非ブロッキング送信

  • select文とタイムアウトを使用してデッドロックを回避
  • 実際のネットワークスタックのような動作

1.9 練習問題

  1. 3ノード接続: Charlie というノードを追加し、Alice-Bob-Charlie の線形ネットワークを構築してください。

  2. パケット統計: ノードクラスに送受信パケット数をカウントする機能を追加してください。

  3. パケット損失: リンクにパケット損失機能を実装し、ランダムにパケットを破棄してください。

1.10 次章への準備

第2章では、時間をより詳細に扱い、帯域幅制限やスループット測定を実装します。また、複数のパケットを同時に処理する機能を追加していきます。