A hálózaton nem C-objektumok, hanem egy protokoll szerint értelmezhető oktettek haladnak. A szerializálás (serialization) egy programbeli érték átalakítása meghatározott külső reprezentációvá; a deszerializálás ennek ellenőrzött visszaalakítása. Egy hordozható formátum az értékeket, a mezők szélességét, sorrendjét és érvényességi feltételeit rögzíti, nem pedig egy fordító pillanatnyi memóriaelrendezését.
Az előfeltételek közül a memóriamásolás és byte view adja a reprezentáció elérésének eszközeit, a promóciók és túlcsordulás az aritmetikai biztonságot. Az union, enum és bit-field tárgyalásából pedig azt visszük tovább, hogy egy natív típus kényelmes belső modell lehet anélkül, hogy külső adatformátumnak alkalmas volna.
A host byte order a több C bájtot elfoglaló skaláris értékek adott implementációban használt bájtsorrendje. A wire order a protokoll által előírt sorrend. A kettő egyezhet, de a protokoll helyessége nem függhet ettől.
ISO C17: a C bájt, azaz a sizeof
egysége CHAR_BIT bitet tartalmaz; nem általánosan nyolcat.
A hálózati oktett viszont definíció szerint nyolc bit.
E fejezet codecje ezért explicit feltételként követeli meg a
CHAR_BIT == 8 tulajdonságot és a használt exact-width egész
típusokat.
A 0x12345678 érték négyoktettes reprezentációja:
| Sorrend | Oktettek növekvő offset szerint |
|---|---|
| Big-endian | 12 34 56 78 |
| Little-endian | 78 56 34 12 |
Big-endian esetén a legnagyobb helyi értékű oktett kerül előre. Ha egy oktettes, előjel nélküli mező oktettjei , akkor:
Az explicit codec ezt az összefüggést valósítja meg. Nem szükséges előbb felismernie a host bájtsorrendjét.
Egy struktúra sizeof hosszúságú
objektumreprezentációjának közvetlen kiírása több különböző szerződést
mos össze:
| Natív tulajdonság | Külső formátumbeli probléma |
|---|---|
| Tagok közötti és végi padding | Nem jelent adatmezőt; tartalma nem megbízható, és információt szivárogtathat |
| Több-bájtos egészek reprezentációja | A fogadó más bájtsorrendet használhat |
long, size_t, enum szélessége |
Eltérhet a platformok vagy fordítási profilok között |
| Pointertag | Címet tartalmaz, nem a hivatkozott adatot; más folyamatban nem használható |
| Bit-field elrendezése | Nem helyettesít explicit bitpozíciókat meghatározó protokollt |
A padding kikapcsolása fordítóbővítéssel legfeljebb az egyik problémát módosítja. Nem definiál bájtsorrendet, pointerjelentést vagy verziózást. A helyes megoldás a szemantikai mezők egyenkénti kódolása.
A példában egy verziót, egy sorszámot, egy hőmérsékletet és egy átlátszatlan payloadot továbbítunk. A teljes wire formátum:
| Offset oktettben | Hossz oktettben | Mező | Szabály |
|---|---|---|---|
| 0 | 1 | Verzió | Pontosan 1 |
| 1 | 2 | Payloadhossz | Előjel nélküli, big-endian |
| 3 | 8 | Sorszám | Előjel nélküli, big-endian |
| 11 | 2 | Hőmérséklet | 16 bites kettes komplemens kód, big-endian |
| 13 | Payload | Pontosan oktett |
Ez protokollelrendezés, nem ABI-layout; a táblázat nem függ a host struktúraoffsetjeitől.
A hőmérséklet skálázott egész szám:
Így például jelentése . A protokollnak a mértékegységet és a skálát is rögzítenie kell: a „16 bites signed érték” önmagában kevés. Ha az alkalmazás lebegőpontos mérésből állítja elő -t, a kerekítési és tartományhibapolitikát még a codec meghívása előtt kell alkalmaznia.
Az előjeles érték wire kódja:
Visszafelé:
Ez matematikai átalakítás, nem a host előjeles reprezentációjának
másolása. A belső típus ezért int32_t: a dekódolás közbülső
értékei biztonságosan elférnek benne. Nem végzünk tartományon kívüli
unsigned–signed konverziót.
Egy fogadott puffer adott offsetje nem feltétlenül megfelelően
igazított egy egész típushoz. A uint64_t *-ra alakított cím
dereferálása nem biztonságos dekódolási módszer: sérülhet az igazítási
követelmény, és külön vizsgálatot igényelnének az objektum- és
aliasing-szabályok is. Az, hogy a hardver bizonyos igazítatlan
betöltéseket támogat, nem ad nyelvi felmentést.
Két bevált módszer van:
memcpy egy megfelelő típusú helyi objektumba, majd
bájtsorrend-konverzió.POSIX API: a <arpa/inet.h> fejléc
htons és htonl függvénye a 16, illetve 32
bites hostértéket alakítja hálózati sorrendű reprezentációhoz. Az
ellenkező irány a ntohs, illetve ntohl. Ezek
nem ISO C függvények. A névben szereplő long nem a host
unsigned long tetszőleges szélességét jelenti.
Az alábbi C17 + POSIX kódrészlet négy olvasható
oktettet igényel. A tároló tulajdonosa a hívó; a kölcsönzött
src hozzáférésnek a hívás végéig érvényesnek kell maradnia.
A függvény nem ellenőrzi a hosszt, és nem ad hibakódot: az előfeltételt
a hívónak kell biztosítania.
#include <arpa/inet.h>
#include <stdint.h>
#include <string.h>
uint32_t read_u32_be_posix(const unsigned char *src)
{
uint32_t network_value;
memcpy(&network_value, src, sizeof network_value);
return ntohl(network_value);
}A helyi network_value igazítása megfelelő. A
memcpy viszont önmagában nem cserél bájtsorrendet; ezért
szükséges utána a konverzió.
A POSIX htonl nem nyolcoktettes codec. A következő
példában a 64 bites mezőt is explicit műveletekkel kódoljuk, így nincs
szükség platformspecifikus htonll változatra.
Az encoder a hívó bufferébe ír. A decoder nem másolja a payloadot, hanem kölcsönzött nézetet ad rá.
| Objektum vagy hozzáférés | Tulajdonos és élettartam | Érvénytelenítés |
|---|---|---|
| Encoder bemeneti üzenete és payloadja | A hívó; a hívás végéig érvényes | Megszüntetésük a hívás alatt szerződéssértés |
| Encoder célbuffere | A hívó; legalább cap írható C bájt |
Felszabadítás, sikeres realloc |
| Decoder bemeneti buffere | A hívó; legalább n olvasható C bájt |
Felszabadítás, sikeres realloc |
| Dekódolt payloadnézet | Nem tulajdonol tárolót | A bemenet élettartamának vége; újrafelhasználása megszünteti a nézet üzenetjelentését |
| Dekódolt metaadatobjektum | A hívó | Megszűnése nem szabadítja fel a payloadot |
A bemenet nem módosulhat konkurensen az olvasással. Az encoder célbuffere nem fedheti át a forrásadatokat vagy a metaadatobjektumokat; a decoder kimeneti objektuma nem fedheti át a bemenetet. A pointerek által jelzett tényleges tárolóméret helyessége hívói előfeltétel: ezt egy C-függvény a számszerű hosszparaméterből nem tudja ellenőrizni.
A visszatérési hibák esetén az encoder célbuffere és
written kimenete, illetve a decoder kimeneti objektuma
változatlan marad.
A codec.c ISO C17-et, nyolcbites C bájtot, valamint
uint16_t, uint32_t, uint64_t és
int32_t támogatást igényel. Nem használ POSIX
függvényt.
#include <assert.h>
#include <limits.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#if CHAR_BIT != 8
#error "This codec requires 8-bit C bytes"
#endif
#if !defined(UINT16_MAX) || !defined(UINT32_MAX) || \
!defined(UINT64_MAX) || !defined(INT32_MAX)
#error "Required exact-width integer types are unavailable"
#endif
enum { HEADER_SIZE = 13 };
enum codec_status {
CODEC_OK,
CODEC_INVALID,
CODEC_SHORT,
CODEC_FORMAT
};
struct message {
uint64_t sequence;
int32_t temperature_centi;
const unsigned char *payload;
size_t payload_size;
};
/* Belső előfeltétel: 1 <= n <= 8, p legalább n bájtot jelöl. */
static void put_be(unsigned char *p, size_t n, uint64_t x)
{
while (n != 0) {
--n;
p[n] = (unsigned char)(x & UINT64_C(255));
x >>= 8;
}
}
static uint64_t get_be(const unsigned char *p, size_t n)
{
uint64_t x = 0;
for (size_t i = 0; i < n; ++i) {
x = (x << 8) | (uint64_t)p[i];
}
return x;
}
static enum codec_status encode_message(
unsigned char *dst, size_t cap,
const struct message *m, size_t *written)
{
if (dst == NULL || m == NULL || written == NULL) {
return CODEC_INVALID;
}
if ((m->payload_size != 0 && m->payload == NULL) ||
m->payload_size > UINT16_MAX ||
m->temperature_centi < -INT32_C(32768) ||
m->temperature_centi > INT32_C(32767)) {
return CODEC_INVALID;
}
if (m->payload_size > SIZE_MAX - HEADER_SIZE) {
return CODEC_INVALID;
}
const size_t total = HEADER_SIZE + m->payload_size;
if (cap < total) {
return CODEC_SHORT;
}
const int32_t q = m->temperature_centi;
const uint64_t raw = q < 0
? (uint64_t)(q + INT32_C(65536))
: (uint64_t)q;
dst[0] = 1;
put_be(dst + 1, 2, (uint64_t)m->payload_size);
put_be(dst + 3, 8, m->sequence);
put_be(dst + 11, 2, raw);
if (m->payload_size != 0) {
memcpy(dst + HEADER_SIZE, m->payload, m->payload_size);
}
*written = total;
return CODEC_OK;
}
static enum codec_status decode_message(
const unsigned char *src, size_t n, struct message *out)
{
if (src == NULL || out == NULL) {
return CODEC_INVALID;
}
if (n < HEADER_SIZE) {
return CODEC_SHORT;
}
if (src[0] != 1) {
return CODEC_FORMAT;
}
const size_t length = (size_t)get_be(src + 1, 2);
const size_t available = n - HEADER_SIZE;
if (length > available) {
return CODEC_SHORT;
}
if (length < available) {
return CODEC_FORMAT;
}
const uint32_t raw = (uint32_t)get_be(src + 11, 2);
const int32_t q = raw < UINT32_C(32768)
? (int32_t)raw
: (int32_t)raw - INT32_C(65536);
const struct message result = {
.sequence = get_be(src + 3, 8),
.temperature_centi = q,
.payload = src + HEADER_SIZE,
.payload_size = length
};
*out = result;
return CODEC_OK;
}
int main(void)
{
const unsigned char payload[] = { 0x41, 0x00, 0xff };
const struct message input = {
.sequence = UINT64_C(0x0102030405060708),
.temperature_centi = -125,
.payload = payload,
.payload_size = sizeof payload
};
const unsigned char expected[] = {
0x01, 0x00, 0x03,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0xff, 0x83,
0x41, 0x00, 0xff
};
unsigned char storage[1 + sizeof expected];
unsigned char *wire = storage + 1;
size_t written = 0;
struct message output = {0};
assert(encode_message(wire, sizeof expected,
&input, &written) == CODEC_OK);
assert(written == sizeof expected);
assert(memcmp(wire, expected, written) == 0);
assert(decode_message(wire, written, &output) == CODEC_OK);
assert(output.sequence == input.sequence);
assert(output.temperature_centi == input.temperature_centi);
assert(output.payload_size == sizeof payload);
assert(memcmp(output.payload, payload, sizeof payload) == 0);
assert(decode_message(wire, written - 1, &output) == CODEC_SHORT);
return 0;
}Az eltolt wire cím demonstrálja, hogy a codec nem
igényel egész típushoz megfelelően igazított bemenetet. Az eltolás
önmagában nem bizonyítja, hogy az adott futásban a cím ténylegesen
igazítatlan.
A külön elvégzendő labor választott profilja: Linux x86-64 System V
ABI, LP64, glibc 2.40; GCC 14.2.0 és Clang 19.1.7, alapértelmezett
ABI-opciókkal. A parancsok e konkrét fordítókiadásokra mutató
gcc, illetve clang programot feltételeznek. A
példához itt nem tartozik elvégzett fordítás vagy mérés.
gcc -std=c17 -O2 -Wall -Wextra -Wconversion -Wpedantic \
-pedantic-errors codec.c -o codec-gcc
clang -std=c17 -O2 -Wall -Wextra -Wconversion -Wpedantic \
-pedantic-errors codec.c -o codec-clangAz encoderben nem elegendő előbb kiszámítani a teljes méretet, és utána összevetni a kapacitással. Ha a fejlécméret és a payloadhossz, először ezt ellenőrizzük:
Csak ezután képezzük értékét. Így az összeadás nem fordul körbe. A 16 bites hosszmező külön korlát: az aritmetikai reprezentálhatóság és a protokoll érvényessége két eltérő feltétel.
A decoder előbb megköveteli az
feltételt. Emiatt az
kivonás biztonságos, és a payloadhossz közvetlenül összevethető a
rendelkezésre álló területtel. Ebben az API-ban egy buffer
pontosan egy teljes üzenet: hiányzó payload esetén
CODEC_SHORT, többletbájt esetén CODEC_FORMAT
az eredmény. TCP-streamhez külön framingréteg szükséges; azt a socket API és framing tárgyalja.
A get_be első
iterációja után az akkumulátor az első
oktett big-endian értékét tartalmazza. A következő lépés
,
pontosan a következő oktett hozzáfűzése. Legfeljebb nyolc oktett esetén
az eredmény elfér uint64_t-ban. A put_be
fordítva, az alsó nyolc bit leválasztásával és jobbra léptetéssel
állítja elő ugyanezt a sorozatot.
Minden fejlécolvasást megelőz a teljes fejléc méretellenőrzése. Az encoder minden lehetséges hibavisszatérése az első írás előtt található; a decoder csak a teljes ellenőrzés után rendeli hozzá a helyi eredményt a kimenethez.
A mintában:
A payloadhossz 00 03, a hőmérséklet FF 83,
a teljes üzenet 16 oktett. Ezek levezetett elvárt értékek, nem mérési
eredmények. A round-trip megőrzi a mezőértékeket és a payload tartalmát,
de nem a payload pointerének címét.
Az encoder költsége , allokáció nélkül, folytonos írással és payloadmásolással. A decoder munkát végez, nem allokál, és nem járja be a payloadot; ennek ára a kölcsönzött nézet élettartam-szerződése.
Csak memcpy használata. Az igazítás
problémáját kezeli, a bájtsorrendet nem. Utána konverzió kell, vagy
eleve explicit oktettkiolvasás.
Előjeles nyers mező közvetlen castolása. C17-ben egy nem reprezentálható unsigned érték signed típussá alakítása nem általános dekódolási recept. A példában a kivonás még a tágabb, biztonságos tartományban történik.
A payloadhossz ellenőrzése a másolás után. Ekkor a bounds-sértés már megtörtént. Minden ellenőrzést az első érintett hozzáférés elé kell helyezni.
A dekódolt pointer megtartása a fogadópuffer újrafelhasználása után. A nézet nem saját másolat. Hosszabb megőrzéshez külön tulajdonolt tároló szükséges. Nulla hosszúságnál a visszaadott pointer lehet one-past; nem dereferálható.
Csak encoder–decoder round-trip tesztelése. Két azonosan hibás függvény egymást elfedheti. Ezért kell a protokollból kézzel levezetett oktettsorozat is.
Magyarázó feladat. Mi a sorszám wire
reprezentációja, ha értéke 0x0102030405060708? Miért nem
változhat ez a host architektúrájával?
Kódolási feladat. Bővítsd a tesztet a
-32768, 32767 és 32768
hőmérsékletértékekkel. Ellenőrizd a wire oktetteket és a hibakimenet
változatlanságát.
Diagnosztikai feladat. Egy fejlesztő így
ellenőrzi a méretet:
total = HEADER_SIZE + length; if (total > cap) .... A
decoderből pedig visszaadott payloadpointert eltárolja, majd új
fogadáshoz felhasználja ugyanazt a buffert. Mi a két egymástól független
hiba?
A nyolc oktett 01 02 03 04 05 06 07 08. A wire order a
protokoll tulajdonsága; a host order csak a programbeli egész
objektumreprezentációját befolyásolja. Ellenőrzési feltételként a kódolt
buffer megfelelő nyolc oktettjét hasonlítsd ehhez a rögzített
sorozathoz.
Az alábbi C17 részlet a teljes példa main függvényében,
a meglévő változókkal használható:
struct message edge = input;
edge.temperature_centi = -32768;
assert(encode_message(wire, sizeof expected,
&edge, &written) == CODEC_OK);
assert(wire[11] == 0x80 && wire[12] == 0x00);
assert(decode_message(wire, written, &output) == CODEC_OK);
assert(output.temperature_centi == -32768);
edge.temperature_centi = 32767;
assert(encode_message(wire, sizeof expected,
&edge, &written) == CODEC_OK);
assert(wire[11] == 0x7f && wire[12] == 0xff);
unsigned char saved[sizeof expected];
memcpy(saved, wire, sizeof saved);
written = 999;
edge.temperature_centi = 32768;
assert(encode_message(wire, sizeof expected,
&edge, &written) == CODEC_INVALID);
assert(written == 999);
assert(memcmp(wire, saved, sizeof saved) == 0);A hibás érték nem csonkolható csendben 16 bitre. Az ellenőrzés egyszerre vizsgálja az érvényességi korlátot és a módosítás nélküli hibavisszatérést.
Az összeadás a kapacitásvizsgálat előtt körbefordulhat. A javítás az
előzetes length > SIZE_MAX - HEADER_SIZE ellenőrzés,
majd a kapacitásvizsgálat.
A második hiba ettől független: a pointer címe megmaradhat, miközben már más üzenet adataira mutat. Ez tároló-újrafelhasználáskor logikai érvénytelenítés; felszabadításkor dangling hozzáférés lenne. Ellenőrzési feltétel, hogy minden nézethasználat megelőzze a buffer újrafelhasználását, vagy előtte saját másolat készüljön.
A hordozható protokoll értékeket és oktettsorrendet definiál, nem natív struktúrákat másol. A biztonságos codec külön ellenőrzi a formátumot, a méretaritmetikát és a hozzáférési határokat. Az explicit oktettkezelés megszünteti az igazítás- és host-bájtsorrend-függést; a másolásmentes dekódolás viszont explicit kölcsönzési és élettartam-szerződést igényel.