rust-network - 解説
実装の詳細
ソケットの基本操作
use std::net::{TcpStream, TcpListener, UdpSocket};
// TCP サーバー
let listener = TcpListener::bind("0.0.0.0:8080")?;
for stream in listener.incoming() {
let stream = stream?;
// handle stream
}
// TCP クライアント
let mut stream = TcpStream::connect("127.0.0.1:8080")?;
stream.write_all(b"Hello")?;
// UDP
let socket = UdpSocket::bind("0.0.0.0:9000")?;
socket.send_to(b"Hello", "127.0.0.1:9001")?;
let (n, from) = socket.recv_from(&mut buffer)?;
DNSパケット構造
DNS Query:
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Transaction ID (16bit) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT (16bit) |
| ANCOUNT (16bit) |
| NSCOUNT (16bit) |
| ARCOUNT (16bit) |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| Question Section |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
ホスト名のエンコード
// "example.com" を DNS形式にエンコード
// [7]example[3]com[0]
fn encode_hostname(hostname: &str) -> Vec<u8> {
let mut encoded = Vec::new();
for part in hostname.split('.') {
encoded.push(part.len() as u8);
encoded.extend_from_slice(part.as_bytes());
}
encoded.push(0); // 終端
encoded
}
よくある間違い
1. パケット分割の未考慮
// 間違い: 一度の read で全データ取得を仮定
let n = stream.read(&mut buffer)?;
// TCPはストリーム → データが分割される可能性
// 正しい: メッセージ境界を自分で管理
// 例: 長さプレフィックス
let mut len_buf = [0u8; 4];
stream.read_exact(&mut len_buf)?;
let len = u32::from_be_bytes(len_buf) as usize;
let mut data = vec![0u8; len];
stream.read_exact(&mut data)?;
2. UDPのパケット順序
// UDPは順序保証なし
// パケット1 → パケット2 → パケット3 と送信
// 受信順: パケット2 → パケット1 → パケット3 かも
// 必要なら自分でシーケンス番号を管理
struct Packet {
sequence: u32,
data: Vec<u8>,
}
3. タイムアウト設定忘れ
// 間違い: 永遠にブロック
let (n, from) = socket.recv_from(&mut buffer)?;
// 正しい: タイムアウト設定
socket.set_read_timeout(Some(Duration::from_secs(5)))?;
match socket.recv_from(&mut buffer) {
Ok((n, from)) => { /* データ受信 */ }
Err(e) if e.kind() == ErrorKind::WouldBlock => { /* タイムアウト */ }
Err(e) => { /* その他エラー */ }
}
パフォーマンス最適化
バッファサイズ
// MTU を考慮
// Ethernet MTU: 1500 bytes
// IP header: 20 bytes
// UDP header: 8 bytes
// → UDP payload: 1472 bytes
const UDP_PAYLOAD_MAX: usize = 1472;
// TCP では大きめのバッファ
const TCP_BUFFER_SIZE: usize = 64 * 1024; // 64KB
ソケットオプション
use std::net::TcpStream;
let stream = TcpStream::connect(addr)?;
// Nagle アルゴリズム無効化(低遅延)
stream.set_nodelay(true)?;
// 読み書きタイムアウト
stream.set_read_timeout(Some(Duration::from_secs(30)))?;
stream.set_write_timeout(Some(Duration::from_secs(30)))?;
// 非ブロッキングモード
stream.set_nonblocking(true)?;
発展トピック
非同期I/O
// mio を使用した低レベル非同期I/O
use mio::{Events, Interest, Poll, Token};
use mio::net::TcpListener;
let mut poll = Poll::new()?;
let mut events = Events::with_capacity(1024);
let mut listener = TcpListener::bind(addr)?;
poll.registry().register(&mut listener, Token(0), Interest::READABLE)?;
loop {
poll.poll(&mut events, None)?;
for event in &events {
match event.token() {
Token(0) => {
// 新しい接続を受け入れ
}
Token(n) => {
// クライアントからのデータ
}
}
}
}
UDPホールパンチング
// NAT越えの基本手順
// 1. 両方のクライアントがSTUNサーバーに接続
// 2. 外部IP:ポートを取得
// 3. 相手の外部アドレスを交換
// 4. 互いにUDPパケットを送信
fn punch_hole(local: &UdpSocket, peer_external: SocketAddr) {
// パケットを送信(NAT穴を開ける)
local.send_to(b"PUNCH", peer_external).ok();
// 相手からのパケットを待つ
let mut buf = [0u8; 1024];
local.recv_from(&mut buf).ok();
}