Бинарный протокол¶
Сеть SoulSeek использует собственный бинарный протокол поверх TCP. Все целые числа имеют порядок байт little-endian. Кодек находится в src-tauri/src/soulseek/proto.rs.
Источник: src-tauri/src/soulseek/proto.rs
Формат передачи данных¶
Каждое сообщение передаётся в следующей обёртке:
Полезная нагрузка начинается с 4-байтового кода сообщения (также little-endian u32), за которым следуют поля, специфичные для сообщения:
Строки предваряются длиной:
Нет ни нулевого терминатора, ни маркера кодировки — строки считаются UTF-8 (или декодируются с потерями через String::from_utf8_lossy).
Обёртка peer handshake¶
Инициирующие сообщения peer-соединения (PierceFirewall и PeerInit) используют 1-байтовый код, а не 4-байтовый. Это первые сообщения, которыми обмениваются стороны при новом TCP-соединении до начала обычного потока сообщений:
PierceFirewall: [u32 length=5][u8 code=0x00][u32 token]
PeerInit: [u32 length=...][u8 code=0x01][str username][str type][u32 token]
Сырой FileTransferInit ещё более необычен: он не обёртывается вовсе. Первые 4 байта — это сырой u32 transfer token. Если эти 4 байта интерпретировать как длину фрейма, значение окажется огромным (мегабайты) — именно это служит признаком для декодера.
Построитель сообщений: Msg¶
Msg — это fluent builder для исходящих сообщений:
pub struct Msg(Vec<u8>);
impl Msg {
pub fn new(code: u32) -> Self {
let mut v = vec![0u8; 4]; // length placeholder
v.extend_from_slice(&code.to_le_bytes());
Msg(v)
}
pub fn u8(mut self, v: u8) -> Self { ... }
pub fn bool(self, v: bool) -> Self { self.u8(v as u8) }
pub fn u32(mut self, v: u32) -> Self { ... }
pub fn u64(mut self, v: u64) -> Self { ... }
pub fn str(mut self, s: &str) -> Self { ... } // writes [u32 len][bytes]
pub fn build(mut self) -> Vec<u8> {
// patches the 4-byte length prefix, returns the full frame
let len = (self.0.len() - 4) as u32;
self.0[..4].copy_from_slice(&len.to_le_bytes());
self.0
}
}
Использование:
// FileSearch (code 26): token + query
let msg = Msg::new(26).u32(token).str(&query).build();
// Login (code 1): username + password + version + md5_hash + minor_version
let msg = Msg::new(1)
.str(&username)
.str(&password)
.u32(CLIENT_VERSION)
.str(&hash)
.u32(1)
.build();
Читатель сообщений: Buf¶
Buf<'a> — это курсор над байтовым срезом для входящих сообщений:
pub struct Buf<'a> {
d: &'a [u8],
pub pos: usize,
}
impl<'a> Buf<'a> {
pub fn u8(&mut self) -> Option<u8> { ... }
pub fn bool(&mut self) -> Option<bool> { self.u8().map(|v| v != 0) }
pub fn u32(&mut self) -> Option<u32> { ... }
pub fn u64(&mut self) -> Option<u64> { ... }
pub fn str(&mut self) -> Option<String> { ... } // reads [u32 len] then [bytes]
pub fn rest(&self) -> &[u8] { &self.d[self.pos..] }
pub fn remaining(&self) -> usize { ... }
}
Все методы чтения возвращают Option и продвигают pos. Если оставшихся байт недостаточно, возвращается None. Вызывающий код использует ? для передачи ошибок парсинга.
Строки декодируются через String::from_utf8_lossy — некорректные байты заменяются символом U+FFFD вместо ошибки парсинга.
Приём сообщений¶
Обычные сообщения¶
pub async fn recv_msg(r: &mut OwnedReadHalf) -> std::io::Result<Vec<u8>> {
let mut len_buf = [0u8; 4];
r.read_exact(&mut len_buf).await?;
let len = u32::from_le_bytes(len_buf) as usize;
if len > 8 * 1024 * 1024 {
return Err(/* "slsk msg too large" */);
}
let mut buf = vec![0u8; len];
r.read_exact(&mut buf).await?;
Ok(buf)
}
Ограничение 8 МБ защищает от поедания памяти при повреждённых фреймах или рассинхронизации протокола.
Неоднозначность peer init / FileTransferInit¶
На F-type (передача файлов) соединениях первые байты неоднозначны:
- Если peer отправляет обёрнутый
PierceFirewallилиPeerInit, 4-байтовая длина будет ≤ 8192. - Если peer отправляет сырой
FileTransferInit(4-байтовый token), эти байты при чтении как длина фрейма дают значение в миллионах.
recv_peer_init_or_fti_lead обрабатывает этот случай:
const MAX_PEER_INIT_FRAME_PAYLOAD: usize = 8192;
pub async fn recv_peer_init_or_fti_lead(r: &mut OwnedReadHalf)
-> std::io::Result<PeerFramedOrRawFti>
{
let len = u32::from_le_bytes(len_buf) as usize;
if len <= MAX_PEER_INIT_FRAME_PAYLOAD {
// Normal framed message — read payload
Ok(PeerFramedOrRawFti::FramedPayload(buf))
} else {
// Value too large to be a frame length — must be raw FTI token
Ok(PeerFramedOrRawFti::RawFileTransferInit(u32::from_le_bytes(len_buf)))
}
}
Если сырой FTI был считан здесь, token сохраняется в FConnReady.file_transfer_init_consumed, чтобы обработчик передачи не пытался прочитать его из socket повторно.
Известные коды сообщений¶
| Код | Направление | Название |
|---|---|---|
| 1 | C→S | Login |
| 2 | C→S | SetWaitPort |
| 3 | C→S / S→C | GetPeerAddress (запрос / ответ) |
| 18 | S→C | ConnectToPeer |
| 26 | C→S | FileSearch |
| 35 | C→S | SharedFolderFiles |
| 71 | C→S | HaveNoParent |
| 92 | C→S / S→C | CheckPrivileges |
| 9 | P→P | FileSearchResponse |
| 40 | P→P | TransferRequest |
| 41 | P→P | TransferResponse |
| 43 | P→P | QueueUpload |
| 46 | P→P | UploadFailed |
| 50 | P→P | UploadDenied |
Коды 64, 69, 83, 84, 89–93, 100, 102, 104 — это сервисные keepalive-сообщения сервера, которые neegde получает и молча отбрасывает.