2.1 プロジェクト構造
go-network-programming/
├── go.mod
├── go.sum
├── main.go
├── packet.go
├── node.go
├── link.go
├── network_stats.go # 新規追加
└── bandwidth_limiter.go # 新規追加
この章では、ネットワークに時間の概念を本格的に導入します。実際のネットワークのように、帯域幅制限、パケット処理時間、スループット測定を実装し、大きなファイルの送信をシミュレートします。
2.2 ネットワーク統計の追加
ネットワークの性能を測定するための統計機能を追加します。
ファイル名: ./network_stats.go
package main
import (
"fmt"
"sync"
"time"
)
// NetworkStats はネットワークの統計情報を管理する
// 実際のネットワークモニタリングツールのような機能を提供
type NetworkStats struct {
mu sync.RWMutex
startTime time.Time
totalPacketsSent int64
totalPacketsRecv int64
totalBytesSent int64
totalBytesRecv int64
packetLossCount int64
lastUpdateTime time.Time
}
// NewNetworkStats は新しい統計オブジェクトを作成
func NewNetworkStats() *NetworkStats {
return &NetworkStats{
startTime: time.Now(),
lastUpdateTime: time.Now(),
}
}
// RecordSentPacket は送信パケットを記録
func (ns *NetworkStats) RecordSentPacket(packet *Packet) {
ns.mu.Lock()
defer ns.mu.Unlock()
ns.totalPacketsSent++
ns.totalBytesSent += int64(packet.Size)
ns.lastUpdateTime = time.Now()
}
// RecordReceivedPacket は受信パケットを記録
func (ns *NetworkStats) RecordReceivedPacket(packet *Packet) {
ns.mu.Lock()
defer ns.mu.Unlock()
ns.totalPacketsRecv++
ns.totalBytesRecv += int64(packet.Size)
ns.lastUpdateTime = time.Now()
}
// RecordPacketLoss はパケット損失を記録
func (ns *NetworkStats) RecordPacketLoss() {
ns.mu.Lock()
defer ns.mu.Unlock()
ns.packetLossCount++
ns.lastUpdateTime = time.Now()
}
// GetThroughput は現在のスループットを計算(bps: bits per second)
func (ns *NetworkStats) GetThroughput() float64 {
ns.mu.RLock()
defer ns.mu.RUnlock()
duration := time.Since(ns.startTime).Seconds()
if duration == 0 {
return 0
}
// バイト数をビット数に変換(1バイト = 8ビット)
totalBits := float64(ns.totalBytesSent) * 8
return totalBits / duration
}
// GetPacketLossRate はパケット損失率を計算(0.0-1.0)
func (ns *NetworkStats) GetPacketLossRate() float64 {
ns.mu.RLock()
defer ns.mu.RUnlock()
if ns.totalPacketsSent == 0 {
return 0.0
}
return float64(ns.packetLossCount) / float64(ns.totalPacketsSent)
}
// Print は統計情報を表示
func (ns *NetworkStats) Print() {
ns.mu.RLock()
defer ns.mu.RUnlock()
duration := time.Since(ns.startTime)
throughputBps := ns.GetThroughput()
throughputKbps := throughputBps / 1000
lossRate := ns.GetPacketLossRate() * 100
fmt.Printf("=== Network Statistics ===\n")
fmt.Printf("Duration: %v\n", duration.Round(time.Millisecond))
fmt.Printf("Packets Sent: %d\n", ns.totalPacketsSent)
fmt.Printf("Packets Received: %d\n", ns.totalPacketsRecv)
fmt.Printf("Bytes Sent: %d\n", ns.totalBytesSent)
fmt.Printf("Bytes Received: %d\n", ns.totalBytesRecv)
fmt.Printf("Throughput: %.2f Kbps\n", throughputKbps)
fmt.Printf("Packet Loss Rate: %.2f%%\n", lossRate)
fmt.Printf("=========================\n")
}
2.3 帯域幅制限機能の実装
実際のネットワークのように、帯域幅制限を実装します。
ファイル名: ./bandwidth_limiter.go
package main
import (
"sync"
"time"
)
// BandwidthLimiter は帯域幅制限を実装する
// トークンバケットアルゴリズムを使用してトラフィック制御を行う
type BandwidthLimiter struct {
mu sync.Mutex
maxBandwidth int64 // 最大帯域幅(bytes per second)
bucketSize int64 // バケットサイズ(bytes)
tokens int64 // 現在のトークン数
lastRefill time.Time // 最後にトークンを補充した時刻
refillRate int64 // 1秒あたりのトークン補充量
}
// NewBandwidthLimiter は新しい帯域幅制限器を作成
// maxBandwidthはbytes per secondで指定
func NewBandwidthLimiter(maxBandwidth int64) *BandwidthLimiter {
return &BandwidthLimiter{
maxBandwidth: maxBandwidth,
bucketSize: maxBandwidth * 2, // 2秒分のバケットサイズ
tokens: maxBandwidth * 2, // 初期状態では満タン
lastRefill: time.Now(),
refillRate: maxBandwidth,
}
}
// TryConsume は指定されたバイト数を消費できるかチェック
// 消費できる場合はtrueを返し、トークンを減らす
func (bl *BandwidthLimiter) TryConsume(bytes int64) bool {
bl.mu.Lock()
defer bl.mu.Unlock()
bl.refillTokens()
if bl.tokens >= bytes {
bl.tokens -= bytes
return true
}
return false
}
// WaitAndConsume は指定されたバイト数を消費するまで待機
// 必要に応じてブロックして、確実に消費する
func (bl *BandwidthLimiter) WaitAndConsume(bytes int64) {
for {
if bl.TryConsume(bytes) {
return
}
// トークンが不足している場合は少し待つ
time.Sleep(10 * time.Millisecond)
}
}
// refillTokens は時間経過に応じてトークンを補充
func (bl *BandwidthLimiter) refillTokens() {
now := time.Now()
elapsed := now.Sub(bl.lastRefill).Seconds()
if elapsed > 0 {
// 経過時間に応じてトークンを補充
tokensToAdd := int64(elapsed * float64(bl.refillRate))
bl.tokens += tokensToAdd
// バケットサイズを超えないように制限
if bl.tokens > bl.bucketSize {
bl.tokens = bl.bucketSize
}
bl.lastRefill = now
}
}
// GetCurrentTokens は現在のトークン数を返す(デバッグ用)
func (bl *BandwidthLimiter) GetCurrentTokens() int64 {
bl.mu.Lock()
defer bl.mu.Unlock()
bl.refillTokens()
return bl.tokens
}
2.4 改良されたリンクの実装
帯域幅制限と統計機能を持つリンクに改良します。
ファイル名: ./link.go (更新)
package main
import (
"fmt"
"math/rand"
"time"
"github.com/google/uuid"
)
// Link はノード間の接続を表現する(改良版)
type Link struct {
ID string
NodeA *Node
NodeB *Node
Bandwidth int64 // 帯域幅(bytes per second)
Latency time.Duration // 基本遅延時間
PacketLoss float64 // パケット損失率
channel chan *Packet
running bool
stats *NetworkStats // 統計情報
limiter *BandwidthLimiter // 帯域幅制限器
}
// NewLink は新しいリンクを生成する(改良版)
func NewLink(nodeA, nodeB *Node, bandwidthMbps int, latency time.Duration) *Link {
// MbpsをBytes per secondに変換(1Mbps = 125,000 bytes/sec)
bandwidthBps := int64(bandwidthMbps * 125000)
link := &Link{
ID: uuid.New().String(),
NodeA: nodeA,
NodeB: nodeB,
Bandwidth: bandwidthBps,
Latency: latency,
PacketLoss: 0.0,
channel: make(chan *Packet, 100), // より大きなバッファ
running: false,
stats: NewNetworkStats(),
limiter: NewBandwidthLimiter(bandwidthBps),
}
nodeA.AddLink(link)
nodeB.AddLink(link)
return link
}
// SetPacketLoss はパケット損失率を設定
func (l *Link) SetPacketLoss(lossRate float64) {
l.PacketLoss = lossRate
}
// Start はリンクの動作を開始する
func (l *Link) Start() {
if l.running {
return
}
l.running = true
go l.forwardPackets()
fmt.Printf("Link between %s and %s started (Bandwidth: %d Mbps, Latency: %v)\n",
l.NodeA.Name, l.NodeB.Name, l.Bandwidth/125000, l.Latency)
}
// 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)
l.stats.Print()
}
// Send はリンクを通じてパケットを送信する
func (l *Link) Send(packet *Packet) error {
if !l.running {
return fmt.Errorf("link is not running")
}
// 統計情報に記録
l.stats.RecordSentPacket(packet)
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 {
// パケット損失をシミュレート
if l.PacketLoss > 0 && rand.Float64() < l.PacketLoss {
l.stats.RecordPacketLoss()
fmt.Printf("Packet lost due to network error: %s\n", packet.ID[:8])
continue
}
// 帯域幅制限を適用
// パケットサイズ分のトークンを消費するまで待機
l.limiter.WaitAndConsume(int64(packet.Size))
// 実際の送信時間を計算(帯域幅による遅延)
transmissionTime := time.Duration(float64(packet.Size) / float64(l.Bandwidth) * float64(time.Second))
// 基本遅延 + 送信時間
totalDelay := l.Latency + transmissionTime
time.Sleep(totalDelay)
// 宛先ノードを決定
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:
l.stats.RecordReceivedPacket(packet)
fmt.Printf("Packet delivered to %s: %s (delay: %v)\n",
targetNode.Name, packet.ID[:8], totalDelay)
case <-time.After(10 * time.Millisecond):
fmt.Printf("Failed to deliver packet to %s (queue full)\n",
targetNode.Name)
l.stats.RecordPacketLoss()
}
}
}
default:
time.Sleep(1 * time.Millisecond)
}
}
}
// PrintStats は統計情報を表示
func (l *Link) PrintStats() {
fmt.Printf("=== Link Stats: %s <-> %s ===\n", l.NodeA.Name, l.NodeB.Name)
l.stats.Print()
}
func (l *Link) String() string {
return fmt.Sprintf("Link{%s <-> %s, %dMbps, %v latency}",
l.NodeA.Name, l.NodeB.Name, l.Bandwidth/125000, l.Latency)
}
2.5 大きなファイル送信のテスト
複数のパケットで構成される大きなファイルの送信をテストします。
ファイル名: ./main.go (更新)
package main
import (
"fmt"
"time"
)
// sendLargeFile は大きなファイルを複数のパケットに分割して送信
func sendLargeFile(sender *Node, receiver string, fileSize int, chunkSize int) {
fmt.Printf("%s sends %d bytes file to %s (chunk size: %d)\n",
sender.Name, fileSize, receiver, chunkSize)
totalChunks := (fileSize + chunkSize - 1) / chunkSize // 切り上げ計算
for i := 0; i < totalChunks; i++ {
remainingSize := fileSize - (i * chunkSize)
currentChunkSize := chunkSize
if remainingSize < chunkSize {
currentChunkSize = remainingSize
}
// ダミーデータを作成
data := make([]byte, currentChunkSize)
for j := range data {
data[j] = byte(i % 256) // チャンク番号に基づく値
}
err := sender.Send(receiver, data)
if err != nil {
fmt.Printf("Error sending chunk %d: %v\n", i+1, err)
continue
}
fmt.Printf("Sent chunk %d/%d (%d bytes)\n", i+1, totalChunks, currentChunkSize)
// 少し間隔を空けて送信(実際のアプリケーションのように)
time.Sleep(5 * time.Millisecond)
}
}
// receiveFile は複数のパケットを受信してファイルを再構築
func receiveFile(receiver *Node, expectedChunks int) int {
fmt.Printf("%s starts receiving file (%d chunks expected)\n",
receiver.Name, expectedChunks)
receivedChunks := 0
totalBytes := 0
for receivedChunks < expectedChunks {
packet := receiver.Receive()
if packet != nil {
receivedChunks++
totalBytes += packet.Size
fmt.Printf("Received chunk %d (%d bytes) from %s\n",
receivedChunks, packet.Size, packet.Source)
} else {
fmt.Println("Timeout waiting for packet")
break
}
}
fmt.Printf("File reception complete: %d chunks, %d total bytes\n",
receivedChunks, totalBytes)
return totalBytes
}
func main() {
fmt.Println("=== ネットワーク時間シミュレーション ===")
// ノードを作成
alice := NewNode("Alice")
bob := NewNode("Bob")
// 10Mbps、50ms遅延のリンクを作成(実際のインターネット接続のような設定)
link := NewLink(alice, bob, 10, 50*time.Millisecond)
// パケット損失を1%に設定
link.SetPacketLoss(0.01)
// システム開始
alice.Start()
bob.Start()
link.Start()
// 大きなファイル(1MB)を1KB単位で送信
fileSize := 1024 * 1024 // 1MB
chunkSize := 1024 // 1KB
expectedChunks := fileSize / chunkSize
// 送信開始時刻を記録
startTime := time.Now()
// 別goroutineでファイルを受信
receiveDone := make(chan int)
go func() {
receivedBytes := receiveFile(bob, expectedChunks)
receiveDone <- receivedBytes
}()
// ファイルを送信
sendLargeFile(alice, "Bob", fileSize, chunkSize)
// 受信完了を待つ
receivedBytes := <-receiveDone
// 転送時間と統計を表示
transferTime := time.Since(startTime)
actualThroughput := float64(receivedBytes*8) / transferTime.Seconds() / 1000 // Kbps
fmt.Printf("\n=== Transfer Results ===\n")
fmt.Printf("Transfer Time: %v\n", transferTime.Round(time.Millisecond))
fmt.Printf("Expected Bytes: %d\n", fileSize)
fmt.Printf("Received Bytes: %d\n", receivedBytes)
fmt.Printf("Actual Throughput: %.2f Kbps\n", actualThroughput)
fmt.Printf("Expected Throughput: %d Kbps (10 Mbps = 10,000 Kbps)\n", 10*1000)
// しばらく待ってからシステムを停止
time.Sleep(100 * time.Millisecond)
alice.Stop()
bob.Stop()
link.Stop()
}
2.6 期待される出力例
=== ネットワーク時間シミュレーション ===
Link added to node Alice
Link added to node Bob
Node Alice started
Node Bob started
Link between Alice and Bob started (Bandwidth: 10 Mbps, Latency: 50ms)
Alice sends 1048576 bytes file to Bob (chunk size: 1024)
Bob starts receiving file (1024 chunks expected)
Sent chunk 1/1024 (1024 bytes)
Packet delivered to Bob: a1b2c3d4 (delay: 50.8192ms)
Received chunk 1 (1024 bytes) from Alice
Sent chunk 2/1024 (1024 bytes)
...
Packet lost due to network error: x9y8z7w6
...
File reception complete: 1019 chunks, 1043456 total bytes
=== Transfer Results ===
Transfer Time: 891ms
Expected Bytes: 1048576
Received Bytes: 1043456
Actual Throughput: 9372.45 Kbps
Expected Throughput: 10000 Kbps (10 Mbps = 10,000 Kbps)
=== Network Statistics ===
Duration: 891ms
Packets Sent: 1024
Packets Received: 1019
Bytes Sent: 1048576
Bytes Received: 1043456
Throughput: 9372.45 Kbps
Packet Loss Rate: 0.49%
=========================
2.7 重要な概念の解説
2.7.1 帯域幅制限
- トークンバケットアルゴリズム: 一定速度でトークンを補充し、パケット送信時にトークンを消費
- 実際のネットワーク機器で使用されているのと同じ原理
2.7.2 伝送遅延
- 基本遅延: 物理的な信号伝播時間(光ファイバー、銅線など)
- 送信遅延: パケットサイズと帯域幅による遅延
2.7.3 スループット測定
- 理論値vs実測値: パケット損失や処理遅延により実測値は理論値を下回る
- Mbps vs MBps: 1 Mbps = 1,000,000 bps = 125,000 Bytes/sec
2.8 練習問題
-
異なる帯域幅でのテスト: 1Mbps、100Mbpsのリンクを作成し、転送時間の違いを確認してください。
-
パケット損失の影響: 損失率を0%, 1%, 5%に変更して、スループットへの影響を測定してください。
-
複数同時転送: 同じリンクで複数のファイルを同時に転送し、帯域幅の共有を確認してください。
2.9 次章への準備
第3章では、複数のノードを接続するスイッチを実装し、MACアドレスによる転送を学習します。ローカルエリアネットワーク(LAN)の基本的な動作を再現していきます。