🏴 send(), recv() гранични случајеви

Осмишљавање, прављење и описивање граничних случајева је тешко јер није “забавно”. Углавном подразумевамо да је неки општи опис довољно добар за граничне случајеве. Па, ако баш и није, то је, бре, гранични случај, ретко се дешава и ако се ништа не “спуцава”, у реду је. Али, овде причамо о програмској подршци, која нема душу и нема миљенике међу вредностима и (граничним случајевима). Дакле, сваки гранични случај би требало да буде “просто још један случај”.

Утичнице-сокети који шаљу и примају… ништа

Узмимо за пример добро познате утичнe (енг. socket) функције send() и recv(). Обе примају складиште (за пријем или предају) и дужину тог складишта (у октетима). У Ц-у, то изгледа отприлике овако:

int send(socket_t skt, uint8_t* buffer, size_t length);
int recv(socket_t skt, uint8_t* buffer, size_t length);

Детаљи зависе од одређеног извођења, типови могу да се разликују и може бити још параметара (“опције”). Неки језици дају своју “спрегу вишег нивоа”, која прима складиште као један податак (објекат) који у себи садржи и (показивач на) октете и дужину, али то не мења на ствари. Такође, дужина може бити означени или неозначени цели број - за наше потребе, ако је означени, сматрамо да је прослеђивање броја мањег од нула грешка и то нас не занима.

Занима нас да видимо шта се збива ако проследимо дужину нула: 0.

Ако ћемо право, то баш и нема смисла. Шта би ми то хтели, да пошаљемо нишпта? Просто немојмо да шаљемо ишта. Хоћемо да примимо ништа? Па, баш лепо, али, задржимо то за себе.

Ако нема смисла, а што то уопште радимо?

Један пример би био ако у неком општем коду примате дужину и онда је просто прослеђујете. Нема баш много смисла да “посејете” if (length > 0) по свим таквим местима.

int send_b64_encoded(socket_t skt, 
    uint8_t* buffer, size_t length)
{
    uint8_t* encoded = ab64_encode(buffer, &length);
    int rslt = send(skt, encoded, length);
    free(encoded);
    return rslt;
}

Овако издвојено, јасно је да додавање једног if није проблем. Али, ако ово почне да се шири, рецимо, додавањем send_ascii85_encoded(), send_base41_encoded()…, онда може да постане напорно.

Посебно, до овога може да се дође услед грешке. Рецимо, често се шаље (и прима) нека велика количина података “на парче” (постоје многи разлози за то). Имамо укупну количину података да пошаљемо као length = total_length, шаљемо парче по парче, смањујући length -= length_of_sent_data. На крају, доћи ћемо до length == 0, када треба да престанемо да шаљемо. Али, услед неке бубе у коду, може да се деси да ипак пробамо да пошаљемо са length == 0.

Добро, овај свет није савршен, шта сад?

Дакле, шта ће send() and recv() да раде у овом случају?

У свим извођењима утичница или ичег сличног, ово није описано. Опис је обично овакав:

Ова функција шаље length бајтова из складишта buffer.

Наравно, није то цео опис, али, у целом опису нема ништа више о томе “шта ако је length==0”. Да ли се то сматра грешком? Ако се сматра, која грешка се пријављује (путем errno у већини случајева). Ако се то не сматра грашком, какво је очекивано понашање? Да ли је понашање различито зависно од неких опција задатих на утичници, пре свега, да ли се користи блокирајући или неблокирајућу У/И.

Што је сад ово уопште толико важно, питамо се? Па, скоро сва извођења утичница такође описује и следеће:

Функција враћа -1 у случају грешке (са кодом грешке у errno) или број октета послатих (примљених за recv()). Резултат је 0 ако је веза по утичници раскинута.

Случај “веза раскинута” очигледно има смисла само за утичнице које користе протоколе са успоставом везе - управо, за протоколе без успоставе везе, треба да користимо sendto()). Употреба errno као “медијума за пренос кода грешке” није свуда примењена, мада јесте најчешћа - рецимо, Прозори користе WSAGetLastError. Ипак, то нама није битно, и у остатку просто сматрајмо да када кажемо errno, у ствари мислимо и “или одговарајући медијум у библиотеци коју користимо”.

Дакле, ако је резултат функције број уписаних октета, а ми смо “рекли” функцији да упише нула октета, има смисла сматрати да је функција успела (колико је тешко уписати нула октета?) и вратити 0. Али, то се не слаже са идејом да 0 означава да је дошло до раскида везе.

Другим речима, “човек овде не зна шта да мисли”!

Ово је “диван” пример граничног случаја који није довољно разматран. У време осмишљавања, могло је да се дође до другачије спреге. У току или након прављења, ово је могло да се боље опише.

Како ово у ствари ради?

Вероватно Вас занима како ово у ствари ради “у стварном свету”? Нисам се бавио детаљним истраживањем, али, моје пробе указују да:

  • recv(length=0) враћа -1 са “WOULDBLOCK” у errno ако је утичница неблокирајућа
  • recv(length=0) враћа 0 ако је утичница блокирајућа, јер никад не успе да прочита 0 октета, просто чека “довека”, односно, док друга страна не раскине везу, када врати 0
  • слично важи за send(length=0)
  • нисам пробао sento() ни recvfrom()

Из извесног угла гледања, ово је смислено. Али, у ствари није. У ствари је прилично лоше, јер је основни проблем, а то је погрешна употреба ових функција, увек исти, независно од тога да ли је утичница блокирајућа или не, а функција се другачије понаша.

Како би требало да ради?

Да не буде да само кукамо како нешто не ваља, да видимо како би ово требало да ради.

Решење у “затеченом стању” би било да прогласимо length=0 за грешку, дакле, вратимо -1 и у errno упишемо BUFFER_CANT_BE_EMPTY (тако некакав код можда већ постоји - ако не постоји, додамо га).

Боља спрега би била да немамо “посебне вредности” за резултат. Нека резултат увек буде код грешке (0 ако нема грешке), а “стварни број октета” дати ако излазни (или улазно-излазни) параметар функције. Раскид везе би био посебан код грешке, попут CONNECTION_CLOSED. Дакле, ако би неко задао length=0, просто би смо вратили 0 (нема грешке).

Наравоученије

Гранични случајеви морају да се обраде, баш као и било који други случај. Чак иако “немају смисла”, “не значе ми ништа”, “довољно је да се ништа не спуца”… Време потрошено да се разјашњава шта је ту шта, шта је очекивано понашање а шта не и шта да се с тим ради је превелико у поређењу са временом које је потребно да се ово правилно направи и опише (и провери).

Written on March 3, 2014