Open4
「RUSTで始めるTCP自作入門」メモ
TCPセグメントの定義
最初の16バイトが送信元ポートになっている。次の16バイトが送信先ポート。104ビット(13バイト)からTCPフラグ。
ビッグエンディアンで値を入れないといけないので、u16::to_be_bytes
でビッグエンディアンに変換して入れる。
u16::to_be_bytes
逆にビッグエンディアンのデータを受け取るときはu16::from_be_bytes
を使う
TCPステータス
TCP Connection State Diagram Figure 6.
ToyTcpで動くスレッド
- 送信スレッド(メインスレッド)
- 受信スレッド
- 再送管理用のタイマースレッド
の3つのスレッドが動く。condition variableを使って各スレッドでステータスを変更しながら処理が進む
3 way handshakeの流れ
- (送信スレッド) SYNを送り、自身のステータスを
SYNSENT
に変える - (送信スレッド) wait_eventでイベント(ConnectionCompleted)を待つ
- (受信スレッド) SYN/ACKを受け取り、自身のステータスを
ESTABLISHED
に変える - (受信スレッド) イベント(ConnectionCompleted)をpublishする
- (送信スレッド) イベント(ConnectionCompleted)によってスレッドが起こされて処理が完了する
send_paramとrecv_param
send_paramは自分のシーケンス番号を管理するのための構造体で、recv_paramは相手のシーケンス番号をシミュレート(模倣)して返すべきACK番号を知るための構造体というイメージ
基本的に次のような流れで更新される
- Aのsend_param.nextはS
- Aがデータをxバイト送る(packetのシーケンス番号はS)
- Aのsend_param.nextがx増える
- Bがデータをxバイト受信する
- Bのrecv_param.nextがx増える
- AがACKを受け取る
- Aのsend_param.unacked_seqをACKの番号にする
3 way handshake
plantuml
@startuml
participant EchoClient
participant "<<main thread>>\nclient" as client
participant "<<receive thread>>\nclient" as r_client
participant "<<timer thread>>\nclient" as t_client
participant "<<main thread>>\nserver" as server
participant "<<receive thread>>\nserver" as r_server
participant "<<timer thread>>\nserver" as t_server
participant EchoServer
EchoServer -> server: listen
EchoServer -> server :accept
server -> server :wait_event(ConnectionCompleted)
activate server
note over server
LISTEN状態
end note
EchoClient -> client: connect
client -> client: send_tcp_packet(SYN)
note right
1. send_params.init_seqをランダムに決定する
2. packetのseqはsend_params.init_seqにする
3. send_params.nextを send_params.seq + 1に設定する
3. send_params.unacked_seqを send_params.seqに設定する
|param|value|
|--|--|
|send_param.init_seq | 1000(random)|
|send_param.unacked_seq | send_param.init_seq |
|send_param.next | send_param.init_seq + 1|
|send_param.window | 3800(default)|
|recv_param.init_seq | |
|recv_param.next| |
|recv_param.window|3800(default)|
packet
|SEQ|ACK|WINDOW|
|--|--|--|
|send_param.init_seq |0|recv_param.window|
end note
note over client
SYNSENT状態
end note
client --> r_server :listen_handler
note over r_server
1. listenソケットとは別のデータ送受信用のソケットを作成する(SYNRVCD状態)
2. recv_param.next = packet.get_seq() + 1
3.send_param.ini_seqをランダムに決定する
4. send_param.unacked_seqをsend_param.init_seqにする
4. send_param.nextをsend_param.init_seqにする
|param|value|
|--|--|
|send_param.init_seq | 2000(random)|
|send_param.unacked_seq | **send_param.init_seq** |
|send_param.next | **send_param.init_seq + 1**|
|send_param.window | 3800(default)|
|recv_param.init_seq | **packet.SEQ(1000)**|
|recv_param.next| **packet.SEQ(1000) + 1**|
|recv_param.window| **packet.WINDOW(3800)** |
packet
|SEQ|ACK|WINDOW|
|--|--|--|
|send_param.init_seq |recv_param.next |recv_param.window|
end note
r_server -> r_server :send_tcp_packet(SYN/ACK)
note over r_server
SYNRCVD状態
end note
r_server --> r_client :synsent_handler
note over r_client
1. send_param.unacked_seqをpacketのACKにする
2. recv_param.init_seqをpacketのseqにする
3. recv_param.nextをpacketのseq + 1にする
|param|value|
|--|--|
|send_param.init_seq | 1000|
|send_param.unacked_seq | **1000 -> packet.ACK(1001)** |
|send_param.next | send_param.init_seq + 1|
|send_param.window | **packet.WINDOW(3800)** |
|recv_param.init_seq | **packet.SEQ(2000)**|
|recv_param.next| **packet.SEQ(2000) + 1**|
|recv_param.window|3800(default)|
packet
|SEQ|ACK|WINDOW|
|--|--|--|
| **send_param.next(1001)** |**recv_param.next(2001)**|recv_param.window|
end note
r_client -> r_client :send_tcp_packet(ACK)
note over r_client
ESTABLISHED状態
end note
r_client --> r_server :synrcvd_handler
note right
1. send_param.unacked_seqをpacketのACKにする
2. recv_param.nextをpacketのseqにする
|param|value|
|--|--|
|send_param.init_seq | 2000 |
|send_param.unacked_seq | **2000 -> packet.ACK(2001)** |
|send_param.next | send_param.init_seq + 1|
|send_param.window | 3800 |
|recv_param.init_seq | 1000 |
|recv_param.next| 1001 |
|recv_param.window| 3800 |
end note
note over r_server
ESTABLISHED状態
end note
r_server -> r_server: publish_event(ConnectionCompleted)
server -> EchoServer: Ok(sock_id)
deactivate server
ペイロードの受信
送る側はsend_paramを、受け取る側はrecv_paramを更新する。
send_paramはパケットのACK番号で、recv_paramはパケットのシーケンス番号をもとに更新される。
plantuml
@startuml
participant EchoClient
participant "<<main thread>>\nclient" as client
participant "<<receive thread>>\nclient" as r_client
participant "<<timer thread>>\nclient" as t_client
participant "<<main thread>>\nserver" as server
participant "<<receive thread>>\nserver" as r_server
participant "<<timer thread>>\nserver" as t_server
participant EchoServer
=== 3 way handshake 終了 ==
EchoServer -> server :recv(&buffer)
server -> server :wait_event(DataArrived)
activate server
EchoClient -> client :send('aaaaa')
note right
1. packetのseqはsend_param.nextにする
2. send_param.nextを6増やす
3. send_param.windowを6減らす
|param|value|
|--|--|
|send_param.init_seq | 1000|
|send_param.unacked_seq | 1001 |
|send_param.next | **1001 -> 1007** |
|send_param.window | **3800 -> 3874**|
|recv_param.init_seq | 2000 |
|recv_param.next| 2001 |
|recv_param.window| 3800 |
packet
|SEQ|ACK|WINDOW|
|--|--|--|
|send_param.next(1001) |recv_param.next|recv_param.window|
end note
client -> client :send_tcp_packet()
client --> r_server :established_handler
r_server -> r_server :process_payload()
note left
1. payloadの長さを計算する(6 bytes)
2. recv_param.nextをpacketの(seq + 6)にする
3. recv_param.windowを6減らす
|param|value|
|--|--|
|send_param.init_seq | 2000 |
|send_param.unacked_seq | 2001 |
|send_param.next | 2001 |
|send_param.window | 3800 |
|recv_param.init_seq | 1000 |
|recv_param.next| **1001 -> 1007** |
|recv_param.window| **3800 -> 3874 ** |
packet
|SEQ|ACK|WINDOW|
|--|--|--|
|send_param.next(2001) |**recv_param.next(1007)**|recv_param.window|
end note
r_server -> r_server :send_tcp_packet(ACK)
note left
packetのackをrecv_params.nextにする
end note
r_server --> server :publish_event(DataArrived)
server -> EchoServer :受信したデータをbufferにコピーする
deactivate server
note left
1. recv_bufferの先頭から
(recv_buffer.len() - recv_params.window) = 6 bytes
のデータをbufferにcopyする
2. recv_params.windowを6増やす
|param|value|
|--|--|
|send_param.init_seq | 2000 |
|send_param.unacked_seq | 2001 |
|send_param.next | 2001 |
|send_param.window | 3800 |
|recv_param.init_seq | 1000 |
|recv_param.next| 1007 |
|recv_param.window| **3874 -> 3800** |
end note
r_server --> r_client :established_handler
note left
unacked_seqをpacketのackに更新する
|param|value|
|--|--|
|send_param.init_seq | 1000|
|send_param.unacked_seq | **1001 -> packet.ACK(1007)** |
|send_param.next | 1007 |
|send_param.window | 3874|
|recv_param.init_seq | 2000 |
|recv_param.next| 2001 |
|recv_param.window| 3800 |
end note
t_client -> t_client :ack済みのセグメントを削除
note left
send_params.windowを6増やす
end note
@enduml
チェックサム
この記事が詳しい
pnetの実装
ソースアドレスなどを1wordの値をして足していく。TCPのペイロード部分も同じように足していく
/// Calculate the checksum for a packet built on IPv4. Used by UDP and TCP.
pub fn ipv4_checksum(
data: &[u8],
skipword: usize,
extra_data: &[u8],
source: &Ipv4Addr,
destination: &Ipv4Addr,
next_level_protocol: IpNextHeaderProtocol,
) -> u16be {
let mut sum = 0u32;
// Checksum pseudo-header
sum += ipv4_word_sum(source);
sum += ipv4_word_sum(destination);
let IpNextHeaderProtocol(next_level_protocol) = next_level_protocol;
sum += next_level_protocol as u32;
let len = data.len() + extra_data.len();
sum += len as u32;
// Checksum packet header and data
sum += sum_be_words(data, skipword);
sum += sum_be_words(extra_data, extra_data.len() / 2);
finalize_checksum(sum)
}
TCPペイロードの部分はビッグエンディアンで入っているとして扱う。16ビット毎に見てsum
にu16の値を足していく。skipword
は8になっていてここはchecksumが入る場所で、データを送るときはここを0として扱ってチェックサムを計算する。
/// Sum all words (16 bit chunks) in the given data. The word at word offset
/// `skipword` will be skipped. Each word is treated as big endian.
fn sum_be_words(data: &[u8], skipword: usize) -> u32 {
if data.len() == 0 {
return 0;
}
let len = data.len();
let mut cur_data = &data[..];
let mut sum = 0u32;
let mut i = 0;
while cur_data.len() >= 2 {
if i != skipword {
// It's safe to unwrap because we verified there are at least 2 bytes
sum += u16::from_be_bytes(cur_data[0..2].try_into().unwrap()) as u32;
}
cur_data = &cur_data[2..];
i += 1;
}
// If the length is odd, make sure to checksum the final byte
if i != skipword && len & 1 != 0 {
sum += (data[len - 1] as u32) << 8;
}
sum
}
32ビットの値を16bit毎に分けて足し算をして、1の補数をとる
fn finalize_checksum(mut sum: u32) -> u16be {
while sum >> 16 != 0 {
sum = (sum >> 16) + (sum & 0xFFFF);
}
!sum as u16
}