A socketet C-ben nem „kapcsolatobjektumként”, hanem file descriptoron keresztül kezeljük. A fordító nem akadályozza meg a kétszeres lezárást, a már megszűnt buffer használatát vagy az üzenethatárok téves feltételezését. Ezeket explicit életciklus- és protokollszerződéssel kell kizárnunk.
A file descriptor és robusztus I/O ciklus részleges műveletekre vonatkozó szabályai itt is érvényesek. A hosszmező feldolgozása a biztonságos szerializálásra, a megszakítások kezelése pedig a signalokról szóló részre épül. A cél most ezek összekapcsolása egy teljes socket-életciklussá.
POSIX API: a socket() egy kommunikációs
végpont descriptorát hozza létre. Sikeres visszatérése nem jelenti, hogy
a végpont már kapcsolódott is.
A szerver alapvető sorrendje:
getaddrinfo → socket → bind →
listen → accept
A bind() helyi címhez rendeli a végpontot; a
listen() hallgató socketté alakítja. Az
accept() új descriptort ad vissza a
létrejött kapcsolathoz. A hallgató descriptor ettől továbbra is
használható új kapcsolatok fogadására.
A kliens sorrendje:
getaddrinfo → socket →
connect
A getaddrinfo() címjelöltek láncolt listáját állítja
elő. Minden jelöltnél együtt kell használni az ai_family,
ai_socktype, ai_protocol, ai_addr
és ai_addrlen mezőket. Egy sikertelen jelölt socketjét le
kell zárni, majd új sockettel próbálható a következő cím.
A struct sockaddr az API általános címparaméter-típusa;
önmagában nem általános méretű címtároló. IPv4-címhez
struct sockaddr_in, IPv6-címhez
struct sockaddr_in6 tartozik. Ismeretlen családú visszatérő
címhez a megfelelő méretű és igazítású
struct sockaddr_storage használható.
A socklen_t a címhossz API-típusa. Az
accept() hosszparamétere bemenetként a rendelkezésre álló
tároló mérete, kimenetként a visszaadott cím hossza. Ezért minden új
próbálkozás előtt inicializáljuk.
| Erőforrás vagy tároló | Tulajdonos | Kölcsönzött használat | Élettartam vége |
|---|---|---|---|
getaddrinfo() eredménylistája |
végpontnyitó függvény | socket, bind, connect
előkészítése |
freeaddrinfo() |
| Hallgató és kapcsolati descriptor | main() |
munkamenet-kezelő függvény | close() |
| Üzenetbuffer | munkamenet-kezelő függvény | küldő és fogadó segédfüggvények | függvényből kilépés |
| Fogadott partnercím tárolója | main() blokkja |
accept() kimeneti paramétere |
blokk vége |
A descriptor száma lezárás után újra kiosztható. Egy elavult
descriptor használata ezért nem feltétlenül EBADF: akár egy
másik erőforrást is elérhet.
POSIX stream-socket: a send() és
recv() nem őrzi meg az alkalmazás üzenethatárait. Egy
küldés több fogadásra darabolódhat, több küldés pedig egy fogadásban
jelenhet meg.
Pozitív kért fogadási hossz mellett:
recv() > 0: ennyi C bájt érkezett;recv() == 0: a fogadási irány véget ért, a korábban
érkezett adatok elfogytak;recv() == -1: az errno értelmezendő.A pozitív send()-eredmény csak az elfogadott
adatmennyiséget jelzi, nem a partner alkalmazásának feldolgozási
visszaigazolását.
A példában egy keret:
A hossz kizárólag a payload mérete. A megengedett tartomány . A négyezres korlát alkalmazásszintű erőforrás-szabály, nem TCP-korlát.
A feldolgozás sorrendje:
Bizonyításvázlat. Legyen az eddig átvitt, a kívánt adatmennyiség. A ciklus invariánsa . Minden hívás legfeljebb elemet kér, pozitív eredmény után pedig . A pointer csak a még feldolgozatlan tartományra mutat. Hiba esetén nincs állítás teljes üzenetről: a részben módosított buffer tartalma nem publikálható kész payloadként.
A következő program egyetlen kapcsolatot kezel. A kliens elküldi a
ping payloadot, majd shutdown(fd, SHUT_WR)
hívással lezárja a küldési irányát. Ez half-close: a
fogadási irány tovább működik. A szerver egy keretet és az azt követő
EOF-ot várja, visszaküldi a keretet, majd szintén lezárja küldési
irányát.
A protokoll tehát irányonként pontosan egy keretet enged. Az üres payload érvényes; a hiányzó vagy csonka keret nem az.
Profil: ISO C17; Linux hosted user-space, x86-64 System V ABI, LP64, nyolcbites C bájt. Választott fordítási profil: GCC 14.2.0 és glibc 2.40; második fordítóként Clang 19.1.7 ugyanazzal a libc-vel. Az alábbi parancsok helyi ellenőrzésre szolgálnak, nem dokumentált fordítási vagy futtatási eredmények.
/* frame_demo.c */
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <errno.h>
#include <limits.h>
#include <netdb.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
_Static_assert(CHAR_BIT == 8 && sizeof(uint32_t) == 4,
"This example requires 8-bit bytes and uint32_t.");
enum { CAPACITY = 4096 };
/* Required data: any EOF before completion is a protocol error. */
static int receive_exact(int fd, void *buffer, size_t size)
{
unsigned char *p = buffer;
size_t done = 0;
while (done < size) {
ssize_t n = recv(fd, p + done, size - done, 0);
if (n > 0) {
done += (size_t)n;
} else if (n == 0) {
errno = EPROTO;
return -1;
} else if (errno != EINTR) {
return -1;
}
}
return 0;
}
static int send_all(int fd, const void *buffer, size_t size)
{
const unsigned char *p = buffer;
size_t done = 0;
while (done < size) {
ssize_t n = send(fd, p + done, size - done, MSG_NOSIGNAL);
if (n > 0) {
done += (size_t)n;
} else if (n == 0) {
errno = EIO; /* No progress: do not loop forever. */
return -1;
} else if (errno != EINTR) {
return -1;
}
}
return 0;
}
static int receive_frame(int fd, unsigned char *data, uint32_t *length)
{
uint32_t wire;
if (receive_exact(fd, &wire, sizeof wire) < 0)
return -1;
uint32_t n = ntohl(wire);
if (n > CAPACITY) {
errno = EMSGSIZE;
return -1;
}
if (receive_exact(fd, data, (size_t)n) < 0)
return -1;
*length = n;
return 0;
}
static int send_frame(int fd, const unsigned char *data, uint32_t length)
{
if (length > CAPACITY) {
errno = EMSGSIZE;
return -1;
}
uint32_t wire = htonl(length);
if (send_all(fd, &wire, sizeof wire) < 0)
return -1;
return send_all(fd, data, (size_t)length);
}
static int expect_eof(int fd)
{
unsigned char extra;
ssize_t n;
do {
n = recv(fd, &extra, 1, 0);
} while (n < 0 && errno == EINTR);
if (n == 0)
return 0;
if (n > 0)
errno = EPROTO; /* Extra data violates this protocol. */
return -1;
}
static int session(int fd, int server)
{
unsigned char data[CAPACITY] = {'p', 'i', 'n', 'g'};
uint32_t length = 4;
if (!server) {
if (send_frame(fd, data, length) < 0 ||
shutdown(fd, SHUT_WR) < 0)
return -1;
}
if (receive_frame(fd, data, &length) < 0 || expect_eof(fd) < 0)
return -1;
if (server) {
if (send_frame(fd, data, length) < 0)
return -1;
return shutdown(fd, SHUT_WR);
}
if (length != 4 || memcmp(data, "ping", 4) != 0) {
errno = EPROTO;
return -1;
}
return 0;
}
static int open_endpoint(const char *host, const char *service, int server)
{
struct addrinfo hints = {0};
struct addrinfo *addresses = NULL;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = server ? AI_PASSIVE : 0;
const char *node = strcmp(host, "-") == 0 ? NULL : host;
int e = getaddrinfo(node, service, &hints, &addresses);
if (e != 0) {
if (e == EAI_SYSTEM)
perror("getaddrinfo");
else
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(e));
return -1;
}
int fd = -1;
int saved = EADDRNOTAVAIL;
for (const struct addrinfo *a = addresses; a != NULL; a = a->ai_next) {
fd = socket(a->ai_family, a->ai_socktype, a->ai_protocol);
if (fd < 0) {
saved = errno;
continue;
}
int ok;
if (server)
ok = bind(fd, a->ai_addr, a->ai_addrlen) == 0 &&
listen(fd, 8) == 0;
else
ok = connect(fd, a->ai_addr, a->ai_addrlen) == 0;
if (ok)
break;
saved = errno;
(void)close(fd); /* Preserve the primary failure. */
fd = -1;
}
freeaddrinfo(addresses);
if (fd < 0) {
errno = saved;
perror("open_endpoint");
}
return fd;
}
int main(int argc, char **argv)
{
if (argc != 4 ||
(strcmp(argv[1], "s") != 0 && strcmp(argv[1], "c") != 0)) {
fprintf(stderr, "usage: %s s|c host port\n", argv[0]);
return 1;
}
int server = strcmp(argv[1], "s") == 0;
int endpoint = open_endpoint(argv[2], argv[3], server);
if (endpoint < 0)
return 1;
int fd = endpoint;
int result = 1;
if (server) {
struct sockaddr_storage peer;
socklen_t peer_length;
do {
peer_length = (socklen_t)sizeof peer;
fd = accept(endpoint, (struct sockaddr *)&peer, &peer_length);
} while (fd < 0 && errno == EINTR);
if (fd < 0) {
perror("accept");
goto cleanup;
}
}
if (session(fd, server) < 0)
perror("session");
else
result = 0;
cleanup:
if (server && fd >= 0 && close(fd) < 0) {
perror("close connection");
result = 1;
}
if (close(endpoint) < 0) {
perror("close endpoint");
result = 1;
}
return result;
}Fordítás, majd két külön terminálban indítás:
gcc -std=c17 -O2 -Wall -Wextra -Wpedantic -Wconversion \
frame_demo.c -o frame_demo
clang -std=c17 -O2 -Wall -Wextra -Wpedantic -Wconversion \
frame_demo.c -o frame_demo-clang
# Első terminál:
./frame_demo s 127.0.0.1 9090
# Második terminál:
./frame_demo c 127.0.0.1 9090Sikeres esetben a várt eredmény mindkét folyamatnál nulla kilépési
státusz. A szerver az első használható címre köt, nem minden IPv4- és
IPv6-címre egyszerre. A - host szerveroldalon wildcard
címet kér; ezt csak tudatos hálózati kitettséggel használjuk.
A példában minden descriptor a main() tulajdona. A
segédfüggvények csak kölcsönzik. A fogadó buffer legalább
CAPACITY elemű, írható tároló; a hosszparaméter érvényes
kimeneti objektumra mutat. Küldés közben a forrásnak olvashatónak és
változatlannak kell maradnia. Hiba után a munkamenet megszakad, a
descriptort lezárjuk: egy részben elküldött keretet nem kezdünk újra az
elejéről.
Linux-labor: a send(..., MSG_NOSIGNAL)
megakadályozza, hogy az adott küldés SIGPIPE jelzéssel
leállítsa a folyamatot; a hibát, például az EPIPE értéket
továbbra is kezelni kell. A flag nem ISO C-szolgáltatás. Más platformok
hordozhatósági rétegében a támogatást, illetve például a
SO_NOSIGPIPE lehetőségét külön kell ellenőrizni. A
SIGPIPE folyamat egészére történő figyelmen kívül hagyása
szélesebb hatású policy.
A példa blokkoló, egyszálú laborprogram, nincs benne időkorlát vagy
fork/exec. Egy hibás partner ezért korlátlan
ideig feltarthatja, például ha nem küldi el a teljes fejlécet vagy az
elvárt EOF-ot.
Linux/POSIX API: a socket fogadási és küldési
várakozása SO_RCVTIMEO, illetve SO_SNDTIMEO
opcióval korlátozható. Ez azonban nem azonos a teljes üzenetre vonatkozó
határidővel. Ha minden rövid művelet új várakozási keretet kap, egy
lassan csöpögő partner sokáig fenntarthatja a munkamenetet.
A teljes művelethez monoton órán alapuló abszolút határidő kell:
ahol az egyszer meghatározott határidő, az aktuális idő. A névfeloldás idejét külön is számításba kell venni.
Nonblocking módban a fcntl() segítségével, a meglévő
flag-ek megtartásával állítjuk be az O_NONBLOCK értéket. Az
EAGAIN vagy EWOULDBLOCK nem EOF és nem indok
aktív újrapróbálkozási ciklusra: a műveletet később folytatjuk.
Ekkor tartós állapotként meg kell őrizni:
A fenti szinkron segédfüggvények erre nem alkalmasak
változtatás nélkül, mert visszatéréskor elveszítik belső
kurzorukat. A buffer tulajdonosának az utolsó folytatásig élnie kell;
felszabadítás, realloc() vagy felülírás érvénytelenítheti a
függőben lévő hozzáférést.
Nonblocking connect() esetén az EINPROGRESS
folyamatban lévő kapcsolódást jelent. Az írhatósági esemény után
getsockopt(..., SO_ERROR, ...) segítségével kell
ellenőrizni a tényleges eredményt. A folytatások szervezését a poll/epoll és explicit eseményállapotgép rész
tárgyalja.
A legsúlyosabb hiba az egyetlen recv()-re épített
üzenetkezelés. Ugyanilyen veszélyes az ellenőrizetlen hossz alapján
történő allokáció vagy pointerléptetés.
A payload nem feltétlenül sztring: nincs garantált lezáró
nullkarakter, ezért a %s és a strlen() nem
általános feldolgozási eszköz.
Végül az EOF, a protokollhiba és az átmeneti várakozási állapot különböző esemény. A jelen protokollban a teljes keret utáni EOF elvárt; a fejléc vagy payload közbeni EOF csonkolás.
A fejléc és payload három részletben érkezik: 00 00,
majd 00 05 61, végül 62 63 64 65. Mekkora a
payload, és mikor tekinthető késznek?
Egy általánosabb decoder egyetlen bufferben tárolná a fejlécet és
a payloadot. Írj ellenőrzést, amely a dekódolt uint32_t n
alapján biztonságosan számolja ki a teljes méretet. A payload felső
korlátja size_t limit.
Nonblocking küldésnél a program egy lokális buffer címét elmenti,
majd EAGAIN után visszatér. Később a teljes keretet
újraküldi. Milyen két, egymástól független hibát tartalmaz?
A hossz hálózati sorrendben öt. Az első részlet csak két fejlécoktettet tartalmaz. A második befejezi a fejlécet, és egy payloadoktettet ad. A harmadik után áll rendelkezésre az öt payloadoktett.
Ellenőrzési feltétel: ugyanannak a keretnek minden lehetséges darabolása azonos payloadot eredményezzen; a befejezés előtti EOF hibát adjon.
Az előző Linux/C17 profilhoz tartozó részlet,
<stdint.h> és <errno.h>
használatával:
if ((uintmax_t)n > (uintmax_t)limit ||
(uintmax_t)n > (uintmax_t)SIZE_MAX - 4) {
errno = EMSGSIZE;
return -1;
}
size_t total = (size_t)n + 4;Előbb ellenőrzünk, és csak utána adunk össze. Tesztelendő a nulla, a megengedett maximum, az első tiltott érték és — ahol a típusméretek lehetővé teszik — a reprezentálhatatlan teljes méret.
A visszatérés megszüntetheti a lokális buffer élettartamát: a tárolt pointer dangling lesz. Ettől függetlenül az egész keret újraküldése megismétli a korábban már elfogadott prefixet.
A javítás tartós tulajdonoshoz kötött buffer és megőrzött küldési kurzor. Ellenőrzési feltétel: tetszőleges rövid küldések és várakozások mellett a partner pontosan egyszer kapja meg a keret minden oktettjét.
A socket API descriptorokat és adatfolyamot ad, nem kész üzenetkezelést. A helyességet az explicit ownership, az ellenőrzött framing, a részleges haladás megőrzése és a megkülönböztetett hibakimenetek biztosítják. Blokkoló kódban ez ciklusokat jelent; nonblocking kódban ugyanez tartós állapotgéppé és hosszabb buffer-élettartammá alakul.