Network Packets
- Краен срок:
- 03.12.2019 17:00
- Точки:
- 20
Срокът за предаване на решения е отминал
Network Packets
Когато искаме да изпратим съобщение по мрежата, не става просто да метнем един низ и го прочетем от другата страна. Протоколи като TCP и UDP се грижат да го нацепят на парчета с фиксиран размер, и да добавят неща като пореден номер на пакета (защото може да пристигнат в грешния ред) и checksum -- някаква стойност генерирана от съобщението, с която може да се валидира, че е пристигнало в оригиналния си вид.
Ние ще имплементираме някаква проста форма на подобно пакетиране. Очакваме да вземем един низ, да го разбием на пакети, които да се сериализират до байтове, и после да изпарсим оригиналния низ от тези байтове.
Всеки един от индивидуалните пакети ще се сериализира в байтове (u8
) по следния начин:
- Байт 0: Версията на протокола. Винаги ще бъде 1, но трябва да има forward compatibility!
- Байт 1: Размера на съобщението, което се съхранява в следващите байтове: N.
- Следващите точно N байта: Самото съобщение, в байтове. Термина, който ще използваме по-надолу за тази част от пакета е "payload".
- Следващите 4 байта: Едно u32 число, което е сумата от всички байтове на съобщението (checksum), записана в big endian формат (вижте документацията на
u32
).
Откъм интерфейс, очакваме кода да изглежда по този начин:
use std::fmt;
/// Грешките, които ще очакваме да върнете. По-долу ще е описано кои от тези грешки очакваме да се
/// върнат в каква ситуация.
///
#[derive(Debug)]
pub enum PacketError {
InvalidPacket,
InvalidChecksum,
UnknownProtocolVersion,
CorruptedMessage,
}
/// Нужна е имплементация на Display за грешките, за да може да имплементират `std::error::Error`.
/// Свободни сте да напишете каквито искате съобщения, ще тестваме само типовете, не низовия им
/// вид.
///
/// Ако са във формат на хайку, няма да получите бонус точки, но може да получите чувство на
/// вътрешно удовлетворение.
///
impl fmt::Display for PacketError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
unimplemented!()
}
}
/// Тази имплементация би трябвало да сработи директно благодарение на горните. При желание, можете
/// да си имплементирате ръчно някои от методите, само внимавайте.
///
impl std::error::Error for PacketError {}
/// Един пакет, съдържащ част от съобщението. Изберете сами какви полета да използвате за
/// съхранение.
///
/// Може да е нужно да добавите lifetimes на дефиницията тук и/или на методите в impl блока.
///
#[derive(PartialEq, Debug)]
pub struct Packet {
// ...
}
impl Packet {
/// Конструира пакет от дадения slice от байтове. Приема параметър `size`, който е размера на
/// payload-а на новия пакет. Връща двойка от пакет + оставащите байтове. Тоест, ако имате низа
/// "abcd" и викнете метода върху байтовата му репрезентация с параметър `size` равен на 3, ще
/// върнете двойката `(<пакет с payload "abc">, <байтовия низ "d">)`.
///
/// Байтове от низ можете да извадите чрез `.as_bytes()`, можете и да си конструирате байтов
/// литерал като b"abcd".
///
/// Ако подадения `size` е по-голям от дължината на `source`, приемаме, че размера ще е точно
/// дължината на `source` (и остатъка ще е празен slice).
///
/// Ако параметъра `size` е 0, очакваме тази функция да panic-не (приемаме, че това извикване
/// просто е невалидно, програмистка грешка).
///
pub fn from_source(source: &[u8], size: u8) -> (Self, &[u8]) {
unimplemented!()
}
/// Връща само slice-а който пакета опакова. Тоест, ако сме конструирали пакета със
/// `Packet::from_source(b"abc", 3)`, очакваме `.payload()` да ни върне `b"abc"`.
///
/// Защо това просто не е публично property? За да не позволяваме мутация, а само конструиране
/// и четене.
///
pub fn payload(&self) -> &[u8] {
unimplemented!()
}
/// Сериализира пакета, тоест превръща го в байтове, готови за трансфер. Версия, дължина,
/// съобщение (payload), checksum. Вижте по-горе за детайлно обяснение.
///
pub fn serialize(&self) -> Vec<u8> {
unimplemented!()
}
/// Имайки slice от байтове, искаме да извадим един пакет от началото и да върнем остатъка,
/// пакетиран в `Result`.
///
/// Байтовете са репрезентация на пакет -- версия, размер, и т.н. както е описано по-горе.
///
/// Ако липсват версия, размер, чексума, или размера е твърде малък, за да може да се изпарси
/// валиден пакет от байтовете, връщаме грешка `PacketError::InvalidPacket`.
///
/// Ако версията е различна от 1, връщаме `PacketError::UnknownProtocolVersion`.
///
/// Ако checksum-а, който прочитаме от последните 4 байта на пакета е различен от изчисления
/// checksum на payload-а (сумата от байтовете му), връщаме `PacketError::InvalidChecksum`.
///
/// Забележете, че ако размера е по-голям от истинския размер на payload-а, се очаква
/// `PacketError::InvalidPacket`. Ако размера е по-малък от истинския размер на payload-а,
/// въпросния ще се изпарси, но чексумата ще е грешна, така че ще очакваме
/// `PacketError::InvalidChecksum`. Малко тъпо! Но уви, протоколите имат подобни тъпи ръбове,
/// особено като са написани за един уикенд. Авторите обещават по-добър протокол за версия 2.
///
pub fn deserialize(bytes: &[u8]) -> Result<(Packet, &[u8]), PacketError> {
unimplemented!()
}
}
/// Структура, която ще служи за итериране по пакети. Ще я конструираме от някакво съобщение, и
/// итерацията ще връща всеки следващ пакет, докато съобщението не бъде напълно "изпратено".
/// Изберете каквито полета ви трябват.
///
/// Може да е нужно да добавите lifetimes на дефиницията тук и/или на методите в impl блока.
///
pub struct PacketSerializer {
// ...
}
impl Iterator for PacketSerializer {
type Item = Packet;
fn next(&mut self) -> Option<Self::Item> {
unimplemented!()
}
}
/// Този trait ще ни позволи да конвертираме един `String` (а ако искаме, и други неща) от и до
/// комплект от байтове за прехвърляне по мрежата.
///
/// Детайли за методите вижте по-долу в имплементацията на този trait за `String`.
///
pub trait Packetable: Sized {
fn to_packets(&self, packet_size: u8) -> PacketSerializer;
fn to_packet_data(&self, packet_size: u8) -> Vec<u8>;
fn from_packet_data(packet_data: &[u8]) -> Result<Self, PacketError>;
}
impl Packetable for String {
/// Този метод приема размер, който да използваме за размера на payload-а на всеки пакет. Връща
/// итератор върху въпросните пакети. Низа трябва да се използва под формата на байтове.
///
/// Както при `.from_source`, ако подадения `packet_size` е по-голям от дължината на оставащите
/// байтове, приемаме, че размера на съответния пакет ще е колкото остава.
///
fn to_packets(&self, packet_size: u8) -> PacketSerializer {
unimplemented!()
}
/// Имайки итератор по пакети, лесно можем да сериализираме всеки индивидуален пакет в поредица
/// от байтове със `.serialize()` и да го натъпчем във вектора.
///
/// Както при `.from_source`, ако подадения `packet_size` е по-голям от дължината на оставащите
/// байтове, приемаме, че размера на съответния пакет ще е колкото остава.
///
fn to_packet_data(&self, packet_size: u8) -> Vec<u8> {
unimplemented!()
}
/// Обратното на горния метод е тази асоциирана функция -- имайки slice от байтове които са
/// сериализирана репрезентация на пакети, искаме да десериализираме пакети от този slice, да
/// им извадим payload-ите, и да ги сглобим в оригиналното съобщение.
///
/// Грешките, които могат да се върнат, са същите, които идват от `.deserialize()`.
///
/// Една допълнителна грешка, която може да се случи е при сглобяване на съобщението -- ако е
/// имало липсващ пакет, може съчетанието на байтовете да не генерира правилно UTF8 съобщение.
/// Тогава връщаме `PacketError::CorruptedMessage`.
///
fn from_packet_data(packet_data: &[u8]) -> Result<Self, PacketError> {
unimplemented!()
}
}
Тоест, един пълен round-trip на един низ би могъл да изглежда така:
let source = String::from("hello");
let packet_data = source.to_packet_data(100);
let destination = String::from_packet_data(&packet_data).unwrap();
assert_eq!(source, destination);
Бележки за имплементацията
Внимавайте при жонглирането на типове. Вероятно ще имате ситуации, в които трябва да конвертирате между
usize
иu8
иu32
. Мислете внимателно кога можете да си позволите просто да използватеas
и къде е най-удобно да го използвате. Понякога, може да е по-удачно да ползвате.try_into()
отstd::convert::TryInto
-- компилатора вероятно ще ви го предложи. Сами решете.Ако имате разнообразни типове грешки от разни вградени функции, можете да си имплементирате
From
за конвертиране от тях до вашия тип. Не е необходимо обаче -- може да има и по-лесни ad-hoc начини от една грешка да получите друга грешка в тази ситуация, разгледайте документацията наResult
.Работим с байтове, не с char-ове. Това ще ви улесни някои неща, ще ви усложни други. Тествайте си кода внимателно с разнообразни типове низове.
Документацията на примитивния тип
slice
може да ви даде идеи как най-лесно да манипулирате slice-ове.
Както винаги, не забравяйте, че имате базов тест, с който кода задължително трябва да може да се компилира: test_basic.rs
Задължително прочетете (или си припомнете): Указания за предаване на домашни