Assembla il pacchetto di rete sullo stack per iovec
Il problema ricorrente di assemblare un pacchetto di rete da payload, numero di sequenza, intestazione e altri vari. le informazioni sono per lo più risolte o sull'heap (ad esempio aggiungendo a std::vector) o allocando prima un buffer (si spera abbastanza grande) e poi scrivendo in quel buffer. Alcuni elementi rimangono sempre gli stessi o cambiano solo in minima parte (come l'header) e quindi l'approccio scatter / gather offerto da writev con iovec, Asio con sequenze di buffer o altre interfacce di rete consentono di evitare quelle copie non necessarie.
Ci sono ancora i problemi che parti differenti del messaggio vengono prodotte in parti differenti del codice, specialmente quando si deve usare più di un sottoprotocollo. In tal caso, siamo nuovamente tentati di utilizzare l'allocazione dinamica della memoria per costruire iovec. Vorrei evitare quelle allocazioni dinamiche della memoria e buffer potenzialmente sovradimensionati per così ho creato la seguente implementazione dello stack in-stack (l'ho chiamata stack_stack):
template<class T, size_t length=1>
struct stack_stack {
using next_type = stack_stack<T, length-1>;
using prev_type = stack_stack<T, length+1>;
const T value;
const next_type * next = nullptr;
static constexpr size_t ssize = length;
struct iterator {
using value_type = T;
using pointer = const value_type*;
using reference = const value_type&;
using iterator_category = std::input_iterator_tag;
iterator& operator++() {
ptr = static_cast<const stack_stack*>(ptr)->next;
return *this;
}
bool operator==(iterator other) const {
return ptr == *other;
}
bool operator!=(iterator other) const {
return ptr != *other;
}
pointer operator*() {return static_cast<pointer>(ptr);}
const void* ptr;
};
iterator begin() const {return iterator{this};}
iterator end() const {return iterator{nullptr};}
prev_type push_front(T val) const {
return {val, this};
}
};
Tiene traccia della sua lunghezza utilizzando i parametri del modello e potrebbe essere utilizzato come nel seguente scenario di esempio:
struct ioitem {
char* data;
size_t size;
};
template<class stack>
void Send(const stack& b) {
for (auto a : b) {
std::cout << a->data << std::endl;
}
}
template<class stack>
void SendWithHeader(const stack& b) {
auto header = std::string("HDX1"); // This would normally some kind of constexpr
Send(b.push_front({header.data(), header.size()}));
}
template<class stack>
void SendWithSeqno(const stack& b) {
auto seq_no = std::string("5");
auto b1 = b.push_front({seq_no.data(), seq_no.size()}); // it's ok if one module addds more than one part
auto b2 = b1.push_front({seq_no.data(), seq_no.size()});
SendWithHeader(b2);
}
template<class stack>
void SendWithTag(const stack& b) {
auto tag_name = std::string("my tag"); // I am just making up a protocol here
SendWithSeqno(b.push_front({tag_name.data(), tag_name.size()}));
}
int main() {
auto my_data = std::string("Hello World");
auto my_Buffer = stack_stack<ioitem>{my_data.data(), my_data.size()};
SendWithTag(my_Buffer);
}
Cosa vorrei migliorare:
- Nella
Sendfunzione potrei copiare lo stack in un array di dimensioni statiche in base alla dimensione distack::ssize. Tuttavia non sono riuscitostd::copya lavorare. - Non mi piacciono gli hack con
void*l'iteratore.
Inoltre: è un buon modo per affrontare questo problema o c'è una soluzione molto migliore (senza il mucchio) che è rimasta sotto il mio radar? Ho cercato implementazioni simili alla mia ma non sono riuscito a trovare nulla.
Risposte
L'iteratore non si comporta correttamente
Ci sono vari motivi per cui std::copy()non funziona stack_stacke tutto ha a che fare con l'iteratore. Primo, ti manca difference_type. Poiché i tuoi iteratori non supportano il prendere la differenza, impostalo su void:
using difference_type = void;
Secondo, i tuoi operatori di confronto sono sbagliati. Dovrebbero fare constriferimento a othere puoi accedere otherdirettamente alle variabili membro di , quindi:
bool operator==(const iterator &other) const {
return ptr == other.ptr;
}
Inoltre, sebbene questo sia un banale operatore di confronto, è bene definire operator!=in termini di operator==, per evitare potenziali errori:
bool operator!=(const iterator &other) const {
return !(*this == other); // Just invert the result of operator==
}
Infine, il risultato di operator*dovrebbe essere un riferimento ai dati effettivi, non un puntatore, quindi:
reference operator*() {
return *static_cast<pointer>(ptr);
}
Adesso funziona ed std::copy()è felice. Nel tuo codice, devi cambiare qualche uso di ->in .per far sì che stampi il contenuto di uno stack, in questo modo:
for (auto item: stack) {
std::cout << item.data << "\n";
}
Evitare gli void*hack
Bene, hai creato un problema per te stesso. Ogni elemento della pila punta all'elemento successivo, ma ha un tipo diverso. La soluzione più pulita che vedo senza modificare il sistema di tipi utilizzato stack_stackè fare questo:
struct iterator {
...
using pointer = const stack_stack*;
...
iterator& operator++() {
ptr = reinterpret_cast<pointer>(ptr->next);
return *this;
}
...
reference operator*() {
return ptr->value;
}
pointer ptr;
};
Quindi abbiamo rimosso tutte le bugie, tranne quella sul tipo quando le seguiamo ptr->next.
L'approccio più pulito
Se vuoi farlo ancora più pulito, non dovresti avere un parametro template length. Forse anche non chiamarlo stack, assomiglia più accuratamente a un elemento di un elenco collegato singolarmente. Per tenere traccia della lunghezza di questo elenco, creerei un tipo separato che assomigli all'elenco nel suo insieme e che memorizza la lunghezza e un puntatore alla testa, entrambi i quali aggiorneremo quando aggiungeremo elementi:
template<class T>
struct stack_list {
struct item {
const T value;
const item *const next;
// Constructor which will update the head of stack_list
item(const T &value, const item *&head): value(value), next(head) {
head = this;
}
// Delete copy constructor, move and assignment operators
item(const item &other) = delete;
item &operator=(const item &other) = delete;
item &operator=(const item &&other) = delete;
};
struct iterator {
... // left as an excercise to the reader
};
size_t size{};
const item *head{};
[[nodiscard]] item push_front(T value) {
size++;
return {value, head}
}
}
Quindi puoi usarlo in questo modo:
auto my_data = ...;
stack_list<ioitem> sl;
auto my_buffer = sl.push_front({my_data.begin(), my_data.size()});
std::cout << "List size: " << sl.size << "\n"
<< "First element: " << sl.head->value << "\n";
Usare la tua classe per costruire iovecs
Come hai notato, devi ancora convertire il tuo stack (o elenco) di messaggi di posta ioitemelettronica in un array di file struct iovec. Quindi potrebbe essere meglio costruire direttamente questo array. Se vuoi farlo in pila, l'opzione più sicura è semplicemente andare con a std::array<iovec, N>, dove Nè abbastanza grande da gestire la maggior parte o tutti i casi. Se la dimensione richiesta può variare molto, allora puoi forse creare una classe che contenga un'unione di a std::arraye a std::vectore passi al vettore se l'array è pieno. Potresti essere in grado di utilizzare una libreria esistente che implementa l'ottimizzazione di piccoli vettori , ma poiché in pratica sempre push_front(), la tua implementazione che inizia sul retro dell'array potrebbe essere la più efficiente. Potrebbe sembrare:
template<size_t N = 8>
class iovec_builder {
std::array<struct iovec, N> iov;
size_t iovlen{};
public:
void push_front(struct iovec item) {
if (iovlen == N) {
// handle array being full
} else {
// add starting from the back
iovlen++;
iov[N - iovlen] = item;
}
}
struct iovec *get_iov() {
return &iov[N - iovlen];
}
size_t get_iovlen() const {
return iovlen;
}
};
E usalo come:
iovec_builder iovb;
std::string my_data("Hello World");
iovb.push_front({my_data.data(), my_data.len()});
iovb.push_front({..., ...});
struct msghdr msg{};
msg.iov = iovb.get_iov();
msg.iovlen = iovb.get_iovlen();
...
sendmsg(fd, &msg, ...);
Potrebbe sprecare un po 'di spazio nello stack, ma ne sprecherai di più avendo un elenco collegato e dovendo copiarlo in un array.