前回の記事でWebSocketについての解説をしたが、先にSocket通信についての説明をしておくべきだと思ったので、今回はSocket通信について書く。
そもそもソケットとは?
- ネットワーク通信におけるエンドポイントを定義する抽象的な概念
- ソケットを使用することでアプリケーションがネットワークを介してデータを送受信できる
- 特定のプロトコル(TCPやUDP)に基づいて通信を行うためのインタフェースを提供
ソケット
- IPアドレスとポート番号が関連づけられており、この組み合わせによって、特定のマシンと特定のアプリケーションが通信相手を特定できる
- ストリームソケット(Stream Socket): TCPを使った信頼性のある通信に使用
- データグラムソケット(Datagram Socket):UDPを使った非信頼性のある通信に使用
- IPアドレス:通信相手を一意に識別するためのアドレス。IPv4では32ビット、IPv6では128ビット
- ポート番号:同じマシン上で動作する複数のアプリケーション間で通信を区別するために使用される番号。ポート番号は0~65535の範囲で、HTTPサーバーなら80、HTTPSサーバーなら443など、特定のサービスに対して一般に使用されるポート番号がある。0~1023のポートはウェルノウンポートと呼ばれ、一般ユーザーはbindすることができない
TCP
特徴
- 信頼性の高い通信: TCPはデータの送受信において信頼性を重視する。データの順序や完全性が保証され、パケットが失われた場合や順序が入れ替わった場合でも自動で再送や再構成が行われる。
- 接続指向 (Connection-Oriented): TCPは通信を行う前に、送信元と受信先の間で「3-wayハンドシェイク」と呼ばれるプロセスを経て接続を確立する。この接続によって、データの送信・受信が安定的に行われる。
- フロー制御 (Flow Control): TCPは送信速度を制御し、受信側がパケットを処理できる速度に合わせてデータを送信する。これにより、受信側のバッファがオーバーフローするのを防ぐ。
- エラーチェックと再送: TCPは、受信側でデータに誤りがあった場合や、パケットが欠落した場合に自動的に再送を要求する仕組みを備えている。
- 順序の保証: TCPでは、送信したパケットが送信順序通りに相手に届くことが保証される。到着順序が異なった場合でも、TCPはデータを適切に並べ替える。
典型的な用途
- HTTP/HTTPS: WebブラウザとWebサーバー間の通信はTCPを使用する。信頼性の高い通信が必要なため。
- FTP: ファイル転送プロトコルもTCPを使用し、ファイルが完全かつ正確に転送されるようにしている。
- SMTP: メールの送受信もTCPを使用する。
UDP
特徴
- 信頼性のない通信: UDPは、TCPとは異なり、データの信頼性や順序は保証しない。パケットが失われたり、順番が入れ替わっても、再送や順序の再構成は行われない。
- コネクションレス (Connectionless): UDPでは、データを送信する前に接続を確立する手順は不要。パケットを送信したら即座に通信が完了する。したがって、オーバーヘッドが少なく、遅延が少ない通信を実現できる。
- エラーチェックは限定的: UDPはパケットのエラーチェックを行うが、エラーが検出された場合にそのパケットを破棄するのみで、再送要求は行わない。
- 軽量・高速: 信頼性を犠牲にする代わりに、UDPは非常に高速で、低遅延の通信が可能。
典型的な用途
- DNSクエリ: DNSでは、短い要求と応答を行うためにUDPが使用される。応答の再送はアプリケーション側で実装されることが多い。
- VoIP: 音声やビデオ通話のように、リアルタイム性が求められるが、多少のデータ損失が許容されるアプリケーションでUDPが使用される。
- オンラインゲーム: ゲームのリアルタイムな操作やデータ転送では、多少のデータ損失や順序の違いよりも、低遅延が優先されるためUDPが使われる。
特徴 | TCP | UDP |
---|---|---|
プロトコルの種類 | 接続指向(コネクション型) | コネクションレス(非接続型) |
信頼性 | 高い(データの順序保証、再送) | 低い(信頼性なし、再送なし) |
接続確立 | 必要(3-wayハンドシェイク) | 不要 |
データの順序保証 | あり | なし |
フロー制御 | あり | なし |
エラーチェックと再送 | あり | エラーチェックのみ(再送なし) |
オーバーヘッド | 高い | 低い |
遅延 | 比較的高い | 低い |
主な用途 | ファイル転送、Web通信、Eメール | DNSクエリ、ストリーミング、オンラインゲーム |
通信手順
TCPソケットの通信手順
コネクション指向のため、3-way ハンドシェイク(SYN, SYN-ACK, ACK)で接続を確立し、データの送受信が終わると4-way ハンドシェイクで接続を終了する。
sequenceDiagram participant Client participant Server Server->>Server: socket() ソケット作成 Server->>Server: bind() IPアドレスとポート番号をバインド Server->>Server: listen() 接続要求を待機 Client->>Server: 接続要求 (SYN) Server->>Client: 接続受け入れ (SYN-ACK) Client->>Server: ACK (接続確立) Client->>Server: send() データ送信 Server->>Client: recv() データ受信 Server->>Client: send() データ送信 Client->>Server: recv() データ受信 Client->>Server: close() 接続終了要求 Server->>Client: close() 接続終了
3-way ハンドシェイク
- クライアントはサーバーにSYN(同期)パケットを送信する。
- サーバーはSYN-ACK(同期・応答)パケットで応答する。
- クライアントがACK(応答)パケットを送信して、接続が確立される。
データ送受信
接続が確立されると、クライアントとサーバー間でデータが送受信される。 データの送信はパケットに分割され、TCPは受信側が正しい順序でデータを再構築することを保証する。
4-way ハンドシェイク(接続終了)
通信が終わると、クライアントまたはサーバーが接続を終了するために4-way ハンドシェイクが行われる。
- クライアントはFINパケットを送信して接続終了を要求する。
- サーバーがACKパケットで応答し、接続終了を承認する。
- サーバーは自分の終了準備ができた後、FINパケットを送信する。
- クライアントがACKパケットを返し、接続が終了する。
UDPソケットの通信手順
コネクションレスのため、接続の確立や終了の手順はなく、データを送信して終わり。
sequenceDiagram participant Client participant Server Client->>Server: データ送信 (sendto) Server->>Client: データ受信 (recvfrom) Note over Client,Server: コネクションレスのためハンドシェイクなし Client->>Server: データ送信 (sendto) Server->>Client: データ受信 (recvfrom) Note over Client,Server: 通信終了
コード例
TCPサーバー
package main import ( "fmt" "net" "os" ) func main() { // TCPサーバーのIPアドレスとポートを指定してリッスン listener, err := net.Listen("tcp", "127.0.0.1:8080") if err != nil { fmt.Println("Error starting TCP server:", err) os.Exit(1) } defer listener.Close() fmt.Println("TCP server is listening on 127.0.0.1:8080") // クライアント接続を待機 for { conn, err := listener.Accept() if err != nil { fmt.Println("Error accepting connection:", err) continue } go handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { fmt.Println("Error reading from connection:", err) return } fmt.Printf("Received message: %s\n", string(buf[:n])) }
TCPクライアント
package main import ( "fmt" "net" "os" ) func main() { // サーバーのIPアドレスとポート番号を指定して接続 conn, err := net.Dial("tcp", "127.0.0.1:8080") if err != nil { fmt.Println("Error connecting to server:", err) os.Exit(1) } defer conn.Close() // メッセージを送信 message := "Hello, Server!" _, err = conn.Write([]byte(message)) if err != nil { fmt.Println("Error sending message:", err) return } fmt.Println("Message sent to server") }
UDPサーバー
package main import ( "fmt" "net" "os" ) func main() { addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080") if err != nil { fmt.Println("Error resolving address:", err) os.Exit(1) } conn, err := net.ListenUDP("udp", addr) if err != nil { fmt.Println("Error starting UDP server:", err) os.Exit(1) } defer conn.Close() fmt.Println("UDP server is listening on 127.0.0.1:8080") // データ受信 buf := make([]byte, 1024) for { n, addr, err := conn.ReadFromUDP(buf) if err != nil { fmt.Println("Error reading from connection:", err) continue } fmt.Printf("Received message from %s: %s\n", addr.String(), string(buf[:n])) } }
UDPクライアント
package main import ( "fmt" "net" "os" ) func main() { addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080") if err != nil { fmt.Println("Error resolving address:", err) os.Exit(1) } conn, err := net.DialUDP("udp", nil, addr) if err != nil { fmt.Println("Error connecting to server:", err) os.Exit(1) } defer conn.Close() // メッセージを送信 message := "Hello, UDP Server!" _, err = conn.Write([]byte(message)) if err != nil { fmt.Println("Error sending message:", err) return } fmt.Println("Message sent to UDP server") }