Перейти к содержанию

Бинарный протокол

Сеть SoulSeek использует собственный бинарный протокол поверх TCP. Все целые числа имеют порядок байт little-endian. Кодек находится в src-tauri/src/soulseek/proto.rs.

Источник: src-tauri/src/soulseek/proto.rs


Формат передачи данных

Каждое сообщение передаётся в следующей обёртке:

[u32 payload_len][payload]

Полезная нагрузка начинается с 4-байтового кода сообщения (также little-endian u32), за которым следуют поля, специфичные для сообщения:

[u32 len][u32 code][...fields...]

Строки предваряются длиной:

[u32 len][bytes]

Нет ни нулевого терминатора, ни маркера кодировки — строки считаются 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 получает и молча отбрасывает.