Վստահելի Udp արձանագրության ներդրում .Net-ի համար

Համացանցը վաղուց փոխվել է։ Համացանցի հիմնական պրոտոկոլներից մեկը՝ UDP-ն օգտագործվում է հավելվածների կողմից ոչ միայն տվյալների գծապատկերներ և հեռարձակումներ առաքելու, այլև ցանցային հանգույցների միջև «հավասարակից» կապեր ապահովելու համար: Իր պարզ դիզայնի շնորհիվ այս արձանագրությունն ունի բազմաթիվ նախկինում չպլանավորված կիրառումներ, սակայն արձանագրության թերությունները, օրինակ՝ երաշխավորված առաքման բացակայությունը, ոչ մի տեղ չեն վերացել։ Այս հոդվածը նկարագրում է երաշխավորված առաքման արձանագրության իրականացումը UDP-ի միջոցով:
Բովանդակությունը:Մուտք
Արձանագրության պահանջներ
Հուսալի UDP վերնագիր
Արձանագրության ընդհանուր սկզբունքները
Ժամկետներ և արձանագրության ժամանակաչափեր
Հուսալի UDP փոխանցման վիճակի դիագրամ
Ավելի խորը կոդի մեջ: փոխանցման կառավարման միավոր
Ավելի խորը կոդի մեջ: պետությունները

Ավելի խորը կոդի մեջ: Կապերի ստեղծում և հաստատում
Ավելի խորը կոդի մեջ: Կապը փակվում է ժամանակի վերջում
Ավելի խորը կոդի մեջ: Տվյալների փոխանցման վերականգնում
Հուսալի UDP API
Ամփոփում
Օգտակար հղումներ և հոդվածներ

Մուտք

Համացանցի սկզբնական ճարտարապետությունը ենթադրում էր միատարր հասցեների տարածություն, որտեղ յուրաքանչյուր հանգույց ուներ գլոբալ և եզակի IP հասցե և կարող էր ուղղակիորեն հաղորդակցվել այլ հանգույցների հետ: Այժմ ինտերնետը, ըստ էության, ունի այլ ճարտարապետություն՝ գլոբալ IP հասցեների մի տարածք և բազմաթիվ տարածքներ՝ մասնավոր հասցեներով, որոնք թաքնված են NAT սարքերի հետևում:Այս ճարտարապետության մեջ միայն գլոբալ հասցեների տարածության սարքերը կարող են հեշտությամբ շփվել ցանցում գտնվող որևէ մեկի հետ, քանի որ նրանք ունեն յուրահատուկ, գլոբալ երթուղային IP հասցե: Մասնավոր ցանցի հանգույցը կարող է միանալ նույն ցանցի այլ հանգույցներին, ինչպես նաև կարող է միանալ այլ հայտնի հանգույցներին գլոբալ հասցեների տարածության մեջ: Այս փոխազդեցությունը ձեռք է բերվում հիմնականում ցանցային հասցեների թարգմանության մեխանիզմի շնորհիվ: NAT սարքերը, ինչպիսիք են Wi-Fi երթուղիչները, ստեղծում են թարգմանչական աղյուսակի հատուկ գրառումներ ելքային կապերի համար և փոփոխում են IP հասցեները և պորտերի համարները փաթեթներում: Սա թույլ է տալիս ելքային կապեր մասնավոր ցանցից դեպի գլոբալ հասցեների տարածության հոսթեր: Բայց միևնույն ժամանակ, NAT սարքերը սովորաբար արգելափակում են բոլոր մուտքային տրաֆիկը, քանի դեռ մուտքային կապերի համար առանձին կանոններ չեն սահմանվել:

Ինտերնետի այս ճարտարապետությունը բավականաչափ ճիշտ է հաճախորդ-սերվեր հաղորդակցության համար, որտեղ հաճախորդները կարող են լինել մասնավոր ցանցերում, իսկ սերվերները ունեն գլոբալ հասցե: Բայց դա դժվարություններ է ստեղծում երկու հանգույցների միջև անմիջական կապի համար բազմազան մասնավոր ցանցեր. Երկու հանգույցների միջև ուղիղ կապը կարևոր է այնպիսի ծրագրերի համար, ինչպիսիք են ձայնի փոխանցումը (Skype), համակարգչից հեռահար հասանելիություն (TeamViewer) կամ առցանց խաղերը:

Տարբեր մասնավոր ցանցերում սարքերի միջև հավասարազոր կապի հաստատման ամենաարդյունավետ մեթոդներից մեկը կոչվում է անցքի դակիչ: Այս տեխնիկան առավել հաճախ օգտագործվում է UDP արձանագրության վրա հիմնված հավելվածների հետ:

Բայց եթե ձեր հավելվածը պահանջում է տվյալների երաշխավորված առաքում, օրինակ՝ դուք ֆայլեր եք փոխանցում համակարգիչների միջև, ապա UDP-ի օգտագործումը շատ դժվարություններ կունենա, քանի որ UDP-ն երաշխավորված առաքման արձանագրություն չէ և չի ապահովում փաթեթների առաքում, ի տարբերություն TCP-ի: արձանագրություն։

Այս դեպքում փաթեթների երաշխավորված առաքումն ապահովելու համար պահանջվում է կիրառել կիրառական շերտի արձանագրություն, որն ապահովում է անհրաժեշտ ֆունկցիոնալությունը և աշխատում է UDP-ով:

Անմիջապես ուզում եմ նշել, որ գոյություն ունի TCP անցք փորելու տեխնիկա տարբեր մասնավոր ցանցերում հանգույցների միջև TCP կապեր հաստատելու համար, բայց շատ NAT սարքերի կողմից դրա աջակցության բացակայության պատճառով այն սովորաբար չի համարվում միացման հիմնական միջոց: նման հանգույցներ.

Այս հոդվածի մնացած մասի համար ես կկենտրոնանամ միայն երաշխավորված առաքման արձանագրության իրականացման վրա: UDP անցքի դակման տեխնիկայի իրականացումը կնկարագրվի հաջորդ հոդվածներում:

Արձանագրության պահանջներ

  1. Փաթեթների հուսալի առաքում, որն իրականացվում է դրական հետադարձ կապի մեխանիզմի միջոցով (այսպես կոչված, դրական ճանաչողություն)
  2. Մեծ տվյալների արդյունավետ փոխանցման անհրաժեշտությունը, այսինքն. արձանագրությունը պետք է խուսափի փաթեթների անհարկի փոխանցումից
  3. Պետք է հնարավոր լինի չեղարկել առաքման հաստատման մեխանիզմը (որպես «մաքուր» UDP արձանագրություն գործելու ունակություն)
  4. Հրամանի ռեժիմի իրականացման հնարավորություն՝ յուրաքանչյուր հաղորդագրության հաստատմամբ
  5. Արձանագրության միջոցով տվյալների փոխանցման հիմնական միավորը պետք է լինի հաղորդագրություն

Այս պահանջները հիմնականում համընկնում են «Վստահելի տվյալների արձանագրության» պահանջների հետ, որոնք նկարագրված են rfc908 и rfc1151, և ես ապավինում էի այդ ստանդարտներին այս արձանագրությունը մշակելիս:

Այս պահանջները հասկանալու համար եկեք դիտարկենք երկու ցանցային հանգույցների միջև տվյալների փոխանցման ժամկետները՝ օգտագործելով TCP և UDP արձանագրությունները: Թող երկու դեպքում էլ մենք կորցնենք մեկ փաթեթ:
Ոչ ինտերակտիվ տվյալների փոխանցում TCP-ով.Վստահելի Udp արձանագրության ներդրում .Net-ի համար

Ինչպես երևում է դիագրամից, փաթեթի կորստի դեպքում TCP-ն կհայտնաբերի կորցրած փաթեթը և կհայտնի այն ուղարկողին՝ խնդրելով կորցրած հատվածի համարը:
Տվյալների փոխանցում UDP արձանագրության միջոցով.Վստահելի Udp արձանագրության ներդրում .Net-ի համար

UDP-ն կորուստների հայտնաբերման որևէ քայլ չի ձեռնարկում: UDP արձանագրության փոխանցման սխալների վերահսկումն ամբողջությամբ դիմումի պատասխանատվությունն է:

TCP արձանագրությունում սխալի հայտնաբերումը ձեռք է բերվում վերջնական հանգույցի հետ կապ հաստատելով, այդ կապի վիճակը պահելով, յուրաքանչյուր փաթեթի վերնագրում ուղարկված բայթերի քանակը նշելով և անդորրագրերը ծանուցելով՝ օգտագործելով ճանաչման համարը:

Բացի այդ, արդյունավետությունը բարելավելու համար (այսինքն՝ ուղարկել մեկից ավելի սեգմենտ՝ առանց հաստատում ստանալու), TCP արձանագրությունն օգտագործում է այսպես կոչված փոխանցման պատուհանը՝ տվյալների բայթերի քանակը, որոնք հատվածի ուղարկողն ակնկալում է ստանալ:

TCP արձանագրության մասին լրացուցիչ տեղեկությունների համար տե՛ս rfc793, UDP-ից մինչև rfc768որտեղ, ըստ էության, դրանք սահմանված են։

Վերոնշյալից պարզ է դառնում, որ UDP-ով հաղորդագրությունների առաքման հուսալի արձանագրություն ստեղծելու համար (այսուհետ՝ UDP). Հուսալի UDP), պահանջվում է TCP-ի նման տվյալների փոխանցման մեխանիզմների ներդրում: Այսինքն:

  • պահպանել կապի վիճակը
  • օգտագործել հատվածի համարակալումը
  • օգտագործել հատուկ հաստատման փաթեթներ
  • օգտագործել պարզեցված պատուհանների մեխանիզմ՝ արձանագրության թողունակությունը մեծացնելու համար

Բացի այդ, ձեզ անհրաժեշտ է.

  • ազդարարել հաղորդագրության սկիզբը, միացման համար ռեսուրսներ հատկացնելու համար
  • ազդարարել հաղորդագրության ավարտը, ստացված հաղորդագրությունը փոխանցել վերին հոսքի հավելվածին և թողարկել արձանագրության ռեսուրսները
  • թույլ տալ կապի հատուկ արձանագրությանը անջատել առաքման հաստատման մեխանիզմը, որպեսզի այն գործի որպես «մաքուր» UDP

Հուսալի UDP վերնագիր

Հիշեցնենք, որ UDP-ի տվյալների գրամը պարփակված է IP տվյալների գրամում: Վստահելի UDP փաթեթը պատշաճ կերպով «փաթաթված» է UDP տվյալների գրամի մեջ:
Հուսալի UDP վերնագրի էկապսուլյացիա.Վստահելի Udp արձանագրության ներդրում .Net-ի համար

Reliable UDP վերնագրի կառուցվածքը բավականին պարզ է.

Վստահելի Udp արձանագրության ներդրում .Net-ի համար

  • Դրոշներ - փաթեթի վերահսկման դրոշներ
  • MessageType - հաղորդագրության տեսակ, որն օգտագործվում է վերին հոսքի հավելվածների կողմից՝ հատուկ հաղորդագրություններին բաժանորդագրվելու համար
  • TransmissionId - փոխանցման համարը, հասցեի և ստացողի պորտի հետ միասին, եզակիորեն նույնականացնում է կապը
  • PacketNumber - փաթեթի համարը
  • Ընտրանքներ - արձանագրության լրացուցիչ ընտրանքներ: Առաջին փաթեթի դեպքում այն ​​օգտագործվում է հաղորդագրության չափը նշելու համար

Դրոշները հետևյալն են.

  • FirstPacket - հաղորդագրության առաջին փաթեթը
  • NoAsk - հաղորդագրությունը չի պահանջում միացնել ճանաչման մեխանիզմը
  • LastPacket - հաղորդագրության վերջին փաթեթը
  • RequestForPacket - հաստատման փաթեթ կամ կորցրած փաթեթի հարցում

Արձանագրության ընդհանուր սկզբունքները

Քանի որ Reliable UDP-ն կենտրոնացած է երկու հանգույցների միջև երաշխավորված հաղորդագրության փոխանցման վրա, այն պետք է կարողանա կապ հաստատել մյուս կողմի հետ: Կապ հաստատելու համար ուղարկողը փաթեթ է ուղարկում FirstPacket դրոշակով, որի պատասխանը կնշանակի կապը հաստատված է: Բոլոր պատասխան փաթեթները կամ, այլ կերպ ասած, ճանաչման փաթեթները, միշտ սահմանում են PacketNumber դաշտի արժեքը մեկով ավելի, քան հաջողությամբ ստացված փաթեթների ամենամեծ PacketNumber արժեքը: Առաջին ուղարկված փաթեթի Ընտրանքներ դաշտը հաղորդագրության չափն է:

Նմանատիպ մեխանիզմ օգտագործվում է կապը դադարեցնելու համար: Հաղորդագրության վերջին փաթեթի վրա դրված է LastPacket դրոշը: Պատասխան փաթեթում նշվում է վերջին փաթեթի համարը + 1, որը ստացող կողմի համար նշանակում է հաղորդագրության հաջող առաքում։
Միացման հաստատման և դադարեցման դիագրամ.Վստահելի Udp արձանագրության ներդրում .Net-ի համար

Երբ կապը հաստատվի, տվյալների փոխանցումը սկսվում է: Տվյալները փոխանցվում են փաթեթների բլոկներով: Յուրաքանչյուր բլոկ, բացի վերջինից, պարունակում է ֆիքսված քանակությամբ փաթեթներ: Այն հավասար է ընդունման/հաղորդման պատուհանի չափին: Տվյալների վերջին բլոկը կարող է ունենալ ավելի քիչ փաթեթներ: Յուրաքանչյուր բլոկ ուղարկելուց հետո ուղարկող կողմը սպասում է առաքման հաստատման կամ կորցրած փաթեթները նորից հանձնելու հարցում՝ թողնելով բաց ստանալու/փոխանցման պատուհանը՝ պատասխաններ ստանալու համար: Բլոկի առաքման հաստատումը ստանալուց հետո ստացման/հաղորդման պատուհանը տեղափոխվում է և ուղարկվում է տվյալների հաջորդ բլոկը:

Ստացող կողմը ստանում է փաթեթները: Յուրաքանչյուր փաթեթ ստուգվում է՝ արդյոք այն ընկնում է փոխանցման պատուհանում: Փաթեթները և կրկնօրինակները, որոնք չեն ընկնում պատուհանը, զտվում են: Որովհետեւ Եթե ​​պատուհանի չափը ֆիքսված է և նույնը ստացողի և ուղարկողի համար, ապա փաթեթների բլոկի առանց կորստի առաքման դեպքում պատուհանը տեղափոխվում է տվյալների հաջորդ բլոկի փաթեթներ ստանալու և առաքման հաստատում: ուղարկվել է. Եթե ​​պատուհանը չլցվի աշխատանքային ժամաչափի կողմից սահմանված ժամկետում, ապա կսկսվի ստուգում, որի վրա փաթեթները չեն առաքվել և կուղարկվեն վերառաքման հարցումներ:
Վերահաղորդման դիագրամ.Վստահելի Udp արձանագրության ներդրում .Net-ի համար

Ժամկետներ և արձանագրության ժամանակաչափեր

Կան մի քանի պատճառ, թե ինչու հնարավոր չէ կապ հաստատել: Օրինակ, եթե ստացող կողմը անցանց է: Այս դեպքում, երբ փորձում եք կապ հաստատել, կապը կփակվի ժամանակի ավարտով: Վստահելի UDP-ի իրականացումն օգտագործում է երկու ժամանակաչափ՝ ժամանակի դադարներ սահմանելու համար: Առաջինը՝ աշխատանքային ժմչփը, օգտագործվում է հեռավոր հոսթից պատասխան սպասելու համար: Եթե ​​այն կրակում է ուղարկողի կողմից, ապա վերջին ուղարկված փաթեթը նորից ուղարկվում է: Եթե ​​ժմչփի ժամկետը սպառվում է ստացողի մոտ, ապա ստուգվում է կորցրած փաթեթները և ուղարկվում են նորից առաքման հարցումներ:

Երկրորդ ժմչփն անհրաժեշտ է հանգույցների միջև կապի բացակայության դեպքում կապը փակելու համար։ Ուղարկողի կողմից այն սկսվում է աշխատանքային ժմչփի ժամկետը լրանալուց անմիջապես հետո և սպասում է հեռավոր հանգույցի պատասխանին: Եթե ​​նշված ժամկետում պատասխան չլինի, կապը դադարեցվում է, և ռեսուրսներն ազատվում են: Ստացող կողմի համար կապի փակման ժամանակաչափը գործարկվում է աշխատանքային ժմչփի երկու անգամ սպառվելուց հետո: Սա անհրաժեշտ է հաստատման փաթեթի կորստից ապահովագրվելու համար: Երբ ժամաչափը սպառվում է, կապը նույնպես դադարեցվում է, և ռեսուրսներն ազատվում են:

Հուսալի UDP փոխանցման վիճակի դիագրամ

Արձանագրության սկզբունքներն իրականացվում են վերջավոր վիճակի մեքենայում, որի յուրաքանչյուր վիճակ պատասխանատու է փաթեթների մշակման որոշակի տրամաբանության համար։
Հուսալի UDP վիճակի դիագրամ.

Վստահելի Udp արձանագրության ներդրում .Net-ի համար

Փակ - իրականում պետություն չէ, դա ավտոմատի մեկնարկային և վերջնակետ է: Պետության համար Փակ ստացվում է փոխանցման կառավարման բլոկ, որը, ներդնելով ասինխրոն UDP սերվեր, փաթեթները փոխանցում է համապատասխան միացումներին և սկսում պետական ​​մշակումը։

FirstPacketSending – սկզբնական վիճակը, որում գտնվում է ելքային կապը, երբ ուղարկվում է հաղորդագրությունը:

Այս վիճակում ուղարկվում է սովորական հաղորդագրությունների առաջին փաթեթը: Առանց ուղարկման հաստատման հաղորդագրությունների համար սա միակ վիճակն է, որտեղ ուղարկվում է ամբողջ հաղորդագրությունը:

Ուղարկման ցիկլ – հաղորդագրությունների փաթեթների փոխանցման հիմնական վիճակ:

Անցում դրան պետությունից FirstPacketSending իրականացվում է հաղորդագրության առաջին փաթեթն ուղարկելուց հետո: Այս վիճակում է, որ գալիս են վերահեռարձակման բոլոր ճանաչողություններն ու խնդրանքները: Դրանից դուրս գալը հնարավոր է երկու դեպքում՝ հաղորդագրության հաջող առաքման դեպքում կամ ժամանակի ավարտի դեպքում։

FirstPacket Received – սկզբնական վիճակը հաղորդագրության ստացողի համար:

Այն ստուգում է փոխանցման սկզբի ճիշտությունը, ստեղծում է անհրաժեշտ կառուցվածքները և ուղարկում է առաջին փաթեթի ստացման հաստատում:

Հաղորդագրության համար, որը բաղկացած է մեկ փաթեթից և ուղարկվել է առանց առաքման ապացույց օգտագործելու, սա միակ վիճակն է: Նման հաղորդագրություն մշակելուց հետո կապը փակվում է։

Մոնտաժում – հաղորդագրությունների փաթեթներ ստանալու հիմնական վիճակ:

Այն գրում է փաթեթներ ժամանակավոր պահեստում, ստուգում է փաթեթների կորստի համար, ուղարկում է ծանուցումներ փաթեթների բլոկի և ամբողջ հաղորդագրության առաքման համար, և ուղարկում է կորցրած փաթեթների վերահանձնման հարցումներ: Ամբողջ հաղորդագրությունը հաջողությամբ ստանալու դեպքում կապը անցնում է վիճակի Ավարտված, հակառակ դեպքում, ժամանակի վերջանում է:

Ավարտված – կապի փակում ամբողջ հաղորդագրության հաջող ստացման դեպքում:

Այս վիճակը անհրաժեշտ է հաղորդագրության հավաքման և այն դեպքի համար, երբ հաղորդագրության առաքման հաստատումը կորել է ուղարկողի ճանապարհին: Այս վիճակից դուրս է գալիս ժամանակի ավարտը, սակայն կապը համարվում է հաջողությամբ փակված:

Ավելի խորը կոդի մեջ: փոխանցման կառավարման միավոր

Reliable UDP-ի հիմնական տարրերից մեկը փոխանցման կառավարման բլոկն է: Այս բլոկի խնդիրն է պահպանել ընթացիկ կապերը և օժանդակ տարրերը, բաշխել մուտքային փաթեթները համապատասխան միացումներին, ապահովել ինտերֆեյս փաթեթներ միացում ուղարկելու համար և իրականացնել արձանագրության API: Փոխանցման կառավարման բլոկը ստանում է UDP շերտից փաթեթներ և դրանք վերամշակման համար ուղարկում պետական ​​մեքենա: Փաթեթներ ստանալու համար այն իրականացնում է ասինխրոն UDP սերվեր:
ReliableUdpConnectionControlBlock դասի որոշ անդամներ.

internal class ReliableUdpConnectionControlBlock : IDisposable
{
  // массив байт для указанного ключа. Используется для сборки входящих сообщений    
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> IncomingStreams { get; private set;}
  // массив байт для указанного ключа. Используется для отправки исходящих сообщений.
  public ConcurrentDictionary<Tuple<EndPoint, Int32>, byte[]> OutcomingStreams { get; private set; }
  // connection record для указанного ключа.
  private readonly ConcurrentDictionary<Tuple<EndPoint, Int32>, ReliableUdpConnectionRecord> m_listOfHandlers;
  // список подписчиков на сообщения.
  private readonly List<ReliableUdpSubscribeObject> m_subscribers;    
  // локальный сокет    
  private Socket m_socketIn;
  // порт для входящих сообщений
  private int m_port;
  // локальный IP адрес
  private IPAddress m_ipAddress;    
  // локальная конечная точка    
  public IPEndPoint LocalEndpoint { get; private set; }    
  // коллекция предварительно инициализированных
  // состояний конечного автомата
  public StatesCollection States { get; private set; }
  // генератор случайных чисел. Используется для создания TransmissionId
  private readonly RNGCryptoServiceProvider m_randomCrypto;    	
  //...
}

Asynchronous UDP սերվերի ներդրում.

private void Receive()
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  // создаем новый буфер, для каждого socket.BeginReceiveFrom 
  byte[] buffer = new byte[DefaultMaxPacketSize + ReliableUdpHeader.Length];
  // передаем буфер в качестве параметра для асинхронного метода
  this.m_socketIn.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref connectedClient, EndReceive, buffer);
}   

private void EndReceive(IAsyncResult ar)
{
  EndPoint connectedClient = new IPEndPoint(IPAddress.Any, 0);
  int bytesRead = this.m_socketIn.EndReceiveFrom(ar, ref connectedClient);
  //пакет получен, готовы принимать следующий        
  Receive();
  // т.к. простейший способ решить вопрос с буфером - получить ссылку на него 
  // из IAsyncResult.AsyncState        
  byte[] bytes = ((byte[]) ar.AsyncState).Slice(0, bytesRead);
  // получаем заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header.ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

Յուրաքանչյուր հաղորդագրության փոխանցման համար ստեղծվում է կառուցվածք, որը պարունակում է տեղեկատվություն կապի մասին: Նման կառուցվածքը կոչվում է կապի գրառում.
ReliableUdpConnectionRecord դասի որոշ անդամներ.

internal class ReliableUdpConnectionRecord : IDisposable
{    
  // массив байт с сообщением    
  public byte[] IncomingStream { get; set; }
  // ссылка на состояние конечного автомата    
  public ReliableUdpState State { get; set; }    
  // пара, однозначно определяющая connection record
  // в блоке управления передачей     
  public Tuple<EndPoint, Int32> Key { get; private set;}
  // нижняя граница приемного окна    
  public int WindowLowerBound;
  // размер окна передачи
  public readonly int WindowSize;     
  // номер пакета для отправки
  public int SndNext;
  // количество пакетов для отправки
  public int NumberOfPackets;
  // номер передачи (именно он и есть вторая часть Tuple)
  // для каждого сообщения свой	
  public readonly Int32 TransmissionId;
  // удаленный IP endpoint – собственно получатель сообщения
  public readonly IPEndPoint RemoteClient;
  // размер пакета, во избежание фрагментации на IP уровне
  // не должен превышать MTU – (IP.Header + UDP.Header + RelaibleUDP.Header)
  public readonly int BufferSize;
  // блок управления передачей
  public readonly ReliableUdpConnectionControlBlock Tcb;
  // инкапсулирует результаты асинхронной операции для BeginSendMessage/EndSendMessage
  public readonly AsyncResultSendMessage AsyncResult;
  // не отправлять пакеты подтверждения
  public bool IsNoAnswerNeeded;
  // последний корректно полученный пакет (всегда устанавливается в наибольший номер)
  public int RcvCurrent;
  // массив с номерами потерянных пакетов
  public int[] LostPackets { get; private set; }
  // пришел ли последний пакет. Используется как bool.
  public int IsLastPacketReceived = 0;
  //...
}

Ավելի խորը կոդի մեջ: պետությունները

Պետությունները իրականացնում են Reliable UDP արձանագրության պետական ​​մեքենան, որտեղ տեղի է ունենում փաթեթների հիմնական մշակումը։ ReliableUdpState վերացական դասը տրամադրում է ինտերֆեյս վիճակի համար.

Վստահելի Udp արձանագրության ներդրում .Net-ի համար

Արձանագրության ողջ տրամաբանությունն իրականացվում է վերևում ներկայացված դասերի կողմից՝ օժանդակ դասի հետ միասին, որն ապահովում է ստատիկ մեթոդներ, ինչպիսիք են, օրինակ, ReliableUdp վերնագրի կառուցումը կապի գրառումից։

Հաջորդը, մենք մանրամասնորեն կքննարկենք ինտերֆեյսի մեթոդների իրականացումը, որոնք որոշում են արձանագրության հիմնական ալգորիթմները:

DisposeByTimeout մեթոդ

DisposeByTimeout մեթոդը պատասխանատու է միացման ռեսուրսների թողարկման համար, երբ դադարից հետո ազատում է կապի ռեսուրսները և ազդարարում հաղորդագրությունների հաջող/անհաջող առաքումը:
ReliableUdpState.DisposeByTimeout:

protected virtual void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;      
  if (record.AsyncResult != null)
  {
    connectionRecord.AsyncResult.SetAsCompleted(false);
  }
  connectionRecord.Dispose();
}

Դա միայն գերագնահատված է նահանգում Ավարտված.
Completed.DisposeByTimeout:

protected override void DisposeByTimeout(object record)
{
  ReliableUdpConnectionRecord connectionRecord = (ReliableUdpConnectionRecord) record;
  // сообщаем об успешном получении сообщения
  SetAsCompleted(connectionRecord);        
}

ProcessPackets մեթոդ

ProcessPackets մեթոդը պատասխանատու է փաթեթի կամ փաթեթների լրացուցիչ մշակման համար: Զանգվել է ուղղակիորեն կամ փաթեթի սպասման ժամանակաչափի միջոցով:

Կարող Մոնտաժում մեթոդը անտեսված է և պատասխանատու է կորցրած փաթեթների ստուգման և վիճակին անցնելու համար Ավարտված, վերջին փաթեթը ստանալու և հաջող ստուգում անցնելու դեպքում
Assembling.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // если после двух попыток срабатываний WaitForPacketTimer 
    // не удалось получить пакеты - запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
  else if (connectionRecord.IsLastPacketReceived != 0)
  // успешная проверка 
  {
    // высылаем подтверждение о получении блока данных
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.State = connectionRecord.Tcb.States.Completed;
    connectionRecord.State.ProcessPackets(connectionRecord);
    // вместо моментальной реализации ресурсов
    // запускаем таймер, на случай, если
    // если последний ack не дойдет до отправителя и он запросит его снова.
    // по срабатыванию таймера - реализуем ресурсы
    // в состоянии Completed метод таймера переопределен
    StartCloseWaitTimer(connectionRecord);
  }
  // это случай, когда ack на блок пакетов был потерян
  else
  {
    if (!connectionRecord.TimerSecondTry)
    {
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

Կարող Ուղարկման ցիկլ այս մեթոդը կանչվում է միայն ժմչփի վրա և պատասխանատու է վերջին հաղորդագրությունը նորից ուղարկելու, ինչպես նաև կապի փակման ժամանակաչափը միացնելու համար:
SendingCycle.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.IsDone != 0)
    return;        
  // отправляем повторно последний пакет 
  // ( в случае восстановления соединения узел-приемник заново отправит запросы, которые до него не дошли)        
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, connectionRecord.SndNext - 1));
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

Կարող Ավարտված մեթոդը դադարեցնում է գործող ժամանակաչափը և հաղորդագրություն է ուղարկում բաժանորդներին:
Completed.ProcessPackets:

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.WaitForPacketsTimer != null)
    connectionRecord.WaitForPacketsTimer.Dispose();
  // собираем сообщение и передаем его подписчикам
  ReliableUdpStateTools.CreateMessageFromMemoryStream(connectionRecord);
}

ReceivePacket մեթոդ

Կարող FirstPacket Received Մեթոդի հիմնական խնդիրն է որոշել, թե արդյոք առաջին հաղորդագրության փաթեթը հասել է ինտերֆեյսի, ինչպես նաև հավաքել հաղորդագրություն, որը բաղկացած է մեկ փաթեթից:
FirstPacketReceived.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // комбинация двух флагов - FirstPacket и LastPacket - говорит что у нас единственное сообщение
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) &
      header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.CreateMessageFromSinglePacket(connectionRecord, header, payload.Slice(ReliableUdpHeader.Length, payload.Length));
    if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
    {
      // отправляем пакет подтверждение          
      ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    }
    SetAsCompleted(connectionRecord);
    return;
  }
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;
  // если не требуется механизм подтверждение
  // запускаем таймер который высвободит все структуры         
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
  else
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Կարող Ուղարկման ցիկլ այս մեթոդը անտեսվում է առաքման հաստատման և վերահաղորդման հարցումներն ընդունելու համար:
SendingCycle.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.RequestForPacket))
    return;
  // расчет конечной границы окна
  // берется граница окна + 1, для получения подтверждений доставки
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize), (connectionRecord.NumberOfPackets));
  // проверка на попадание в окно        
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > windowHighestBound)
    return;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // проверить на последний пакет:
  if (header.PacketNumber == connectionRecord.NumberOfPackets)
  {
    // передача завершена
    Interlocked.Increment(ref connectionRecord.IsDone);
    SetAsCompleted(connectionRecord);
    return;
  }
  // это ответ на первый пакет c подтверждением         
  if ((header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket) && header.PacketNumber == 1))
  {
    // без сдвига окна
    SendPacket(connectionRecord);
  }
  // пришло подтверждение о получении блока данных
  else if (header.PacketNumber == windowHighestBound)
  {
    // сдвигаем окно прием/передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуляем массив контроля передачи
    connectionRecord.WindowControlArray.Nullify();
    // отправляем блок пакетов
    SendPacket(connectionRecord);
  }
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Կարող Մոնտաժում ReceivePacket մեթոդում տեղի է ունենում մուտքային փաթեթներից հաղորդագրություն հավաքելու հիմնական աշխատանքը։
Assembling.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (connectionRecord.IsDone != 0)
    return;
  // обработка пакетов с отключенным механизмом подтверждения доставки
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.NoAsk))
  {
    // сбрасываем таймер
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
    // записываем данные
    ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
    // если получили пакет с последним флагом - делаем завершаем          
    if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
    {
      connectionRecord.State = connectionRecord.Tcb.States.Completed;
      connectionRecord.State.ProcessPackets(connectionRecord);
    }
    return;
  }        
  // расчет конечной границы окна
  int windowHighestBound = Math.Min((connectionRecord.WindowLowerBound + connectionRecord.WindowSize - 1), (connectionRecord.NumberOfPackets - 1));
  // отбрасываем не попадающие в окно пакеты
  if (header.PacketNumber < connectionRecord.WindowLowerBound || header.PacketNumber > (windowHighestBound))
    return;
  // отбрасываем дубликаты
  if (connectionRecord.WindowControlArray.Contains(header.PacketNumber))
    return;
  // записываем данные 
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // если пришел последний пакет
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    Interlocked.Increment(ref connectionRecord.IsLastPacketReceived);
  }
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // если последний пакет уже имеется        
  if (Thread.VolatileRead(ref connectionRecord.IsLastPacketReceived) != 0)
  {
    // проверяем пакеты          
    ProcessPackets(connectionRecord);
  }
}

Կարող Ավարտված Մեթոդի միակ խնդիրը հաղորդագրության հաջող առաքման վերահաստատում ուղարկելն է:
Completed.ReceivePacket:

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // повторная отправка последнего пакета в связи с тем,
  // что последний ack не дошел до отправителя
  if (header.Flags.HasFlag(ReliableUdpHeaderFlags.LastPacket))
  {
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
}

Փաթեթ ուղարկելու եղանակը

Կարող FirstPacketSending այս մեթոդը ուղարկում է տվյալների առաջին փաթեթը, կամ եթե հաղորդագրությունը չի պահանջում առաքման հաստատում, ապա ամբողջ հաղորդագրությունը:
FirstPacketSending.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // если подтверждения не требуется - отправляем все пакеты
  // и высвобождаем ресурсы
  if (connectionRecord.IsNoAnswerNeeded)
  {
    // Здесь происходит отправка As Is
    do
    {
      ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, ReliableUdpStateTools. CreateReliableUdpHeader(connectionRecord)));
      connectionRecord.SndNext++;
    } while (connectionRecord.SndNext < connectionRecord.NumberOfPackets);
    SetAsCompleted(connectionRecord);
    return;
  }
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Կարող Ուղարկման ցիկլ Այս մեթոդով ուղարկվում է փաթեթների բլոկ:
SendingCycle.SendPacket:

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов      
  for (connectionRecord.PacketCounter = 0;
        connectionRecord.PacketCounter < connectionRecord.WindowSize &&
        connectionRecord.SndNext < connectionRecord.NumberOfPackets;
        connectionRecord.PacketCounter++)
  {
    ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
    connectionRecord.SndNext++;
  }
  // на случай большого окна передачи, перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
  {
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
  }
}

Ավելի խորը կոդի մեջ: Կապերի ստեղծում և հաստատում

Այժմ, երբ մենք տեսանք հիմնական վիճակները և վիճակները կարգավորելու համար օգտագործվող մեթոդները, եկեք մի փոքր ավելի մանրամասն ներկայացնենք արձանագրության աշխատանքի մի քանի օրինակ:
Տվյալների փոխանցման դիագրամ նորմալ պայմաններում.Վստահելի Udp արձանագրության ներդրում .Net-ի համար

Մտածեք մանրամասնորեն ստեղծման մասին կապի գրառում առաջին փաթեթը միացնելու և ուղարկելու համար: Փոխանցումը միշտ նախաձեռնվում է այն հավելվածի կողմից, որը կոչում է ուղարկել հաղորդագրություն API: Այնուհետև կանչվում է փոխանցման կառավարման բլոկի StartTransmission մեթոդը, որը սկսում է տվյալների փոխանցումը նոր հաղորդագրության համար:
Ելքային կապի ստեղծում.

private void StartTransmission(ReliableUdpMessage reliableUdpMessage, EndPoint endPoint, AsyncResultSendMessage asyncResult)
{
  if (m_isListenerStarted == 0)
  {
    if (this.LocalEndpoint == null)
    {
      throw new ArgumentNullException( "", "You must use constructor with parameters or start listener before sending message" );
    }
    // запускаем обработку входящих пакетов
    StartListener(LocalEndpoint);
  }
  // создаем ключ для словаря, на основе EndPoint и ReliableUdpHeader.TransmissionId        
  byte[] transmissionId = new byte[4];
  // создаем случайный номер transmissionId        
  m_randomCrypto.GetBytes(transmissionId);
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
  // создаем новую запись для соединения и проверяем, 
  // существует ли уже такой номер в наших словарях
  if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
  {
    // если существует – то повторно генерируем случайный номер 
    m_randomCrypto.GetBytes(transmissionId);
    key = new Tuple<EndPoint, Int32>(endPoint, BitConverter.ToInt32(transmissionId, 0));
    if (!m_listOfHandlers.TryAdd(key, new ReliableUdpConnectionRecord(key, this, reliableUdpMessage, asyncResult)))
      // если снова не удалось – генерируем исключение
      throw new ArgumentException("Pair TransmissionId & EndPoint is already exists in the dictionary");
  }
  // запустили состояние в обработку         
  m_listOfHandlers[key].State.SendPacket(m_listOfHandlers[key]);
}

Առաջին փաթեթի ուղարկում (FirstPacketSending վիճակ).

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{
  connectionRecord.PacketCounter = 0;
  connectionRecord.SndNext = 0;
  connectionRecord.WindowLowerBound = 0;       
  // ... 
  // создаем заголовок пакета и отправляем его 
  ReliableUdpHeader header = ReliableUdpStateTools.CreateReliableUdpHeader(connectionRecord);
  ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.CreateUdpPayload(connectionRecord, header));
  // увеличиваем счетчик
  connectionRecord.SndNext++;
  // сдвигаем окно
  connectionRecord.WindowLowerBound++;
  // переходим в состояние SendingCycle
  connectionRecord.State = connectionRecord.Tcb.States.SendingCycle;
  // Запускаем таймер
  connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
}

Առաջին փաթեթն ուղարկելուց հետո ուղարկողը մտնում է վիճակ Ուղարկման ցիկլ - սպասեք փաթեթի առաքման հաստատմանը:
Ստացող կողմը, օգտագործելով EndReceive մեթոդը, ստանում է ուղարկված փաթեթը, ստեղծում է նորը կապի գրառում և այս փաթեթը նախապես վերլուծված վերնագրով փոխանցում է վիճակի ReceivePacket մեթոդին՝ մշակման համար FirstPacket Received
Ստացողի կողմից կապի ստեղծում.

private void EndReceive(IAsyncResult ar)
{
  // ...
  // пакет получен
  // парсим заголовок пакета        
  ReliableUdpHeader header;
  if (!ReliableUdpStateTools.ReadReliableUdpHeader(bytes, out header))
  {          
    // пришел некорректный пакет - отбрасываем его
    return;
  }
  // конструируем ключ для определения connection record’а для пакета
  Tuple<EndPoint, Int32> key = new Tuple<EndPoint, Int32>(connectedClient, header.TransmissionId);
  // получаем существующую connection record или создаем новую
  ReliableUdpConnectionRecord record = m_listOfHandlers.GetOrAdd(key, new ReliableUdpConnectionRecord(key, this, header. ReliableUdpMessageType));
  // запускаем пакет в обработку в конечный автомат
  record.State.ReceivePacket(record, header, bytes);
}

Ստանալով առաջին փաթեթը և ուղարկեք հաստատում (FirstPacketReceived վիճակ).

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  if (!header.Flags.HasFlag(ReliableUdpHeaderFlags.FirstPacket))
    // отбрасываем пакет
    return;
  // ...
  // by design все packet numbers начинаются с 0;
  if (header.PacketNumber != 0)          
    return;
  // инициализируем массив для хранения частей сообщения
  ReliableUdpStateTools.InitIncomingBytesStorage(connectionRecord, header);
  // записываем данные пакет в массив
  ReliableUdpStateTools.WritePacketData(connectionRecord, header, payload);
  // считаем кол-во пакетов, которые должны прийти
  connectionRecord.NumberOfPackets = (int)Math.Ceiling((double) ((double) connectionRecord.IncomingStream.Length/(double) connectionRecord.BufferSize));
  // записываем номер последнего полученного пакета (0)
  connectionRecord.RcvCurrent = header.PacketNumber;
  // после сдвинули окно приема на 1
  connectionRecord.WindowLowerBound++;
  // переключаем состояние
  connectionRecord.State = connectionRecord.Tcb.States.Assembling;  
  if (/*если не требуется механизм подтверждение*/)
  // ...
  else
  {
    // отправляем подтверждение
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
    connectionRecord.WaitForPacketsTimer = new Timer(CheckByTimer, connectionRecord, connectionRecord.ShortTimerPeriod, -1);
  }
}

Ավելի խորը կոդի մեջ: Կապը փակվում է ժամանակի վերջում

Ժամկետի կառավարումը Reliable UDP-ի կարևոր մասն է: Դիտարկենք մի օրինակ, երբ միջանկյալ հանգույցը ձախողվեց, և տվյալների առաքումը երկու ուղղություններով դարձավ անհնար:
Դիագրամ կապը փակելու ժամանակի դադարով.Վստահելի Udp արձանագրության ներդրում .Net-ի համար

Ինչպես երևում է դիագրամից, ուղարկողի աշխատանքային ժմչփը սկսվում է փաթեթների բլոկն ուղարկելուց անմիջապես հետո։ Դա տեղի է ունենում նահանգի SendPacket մեթոդով Ուղարկման ցիկլ.
Աշխատանքային ժամաչափի միացում (SendingCycle վիճակ).

public override void SendPacket(ReliableUdpConnectionRecord connectionRecord)
{      
  // отправляем блок пакетов   
  // ...   
  // перезапускаем таймер после отправки
  connectionRecord.WaitForPacketsTimer.Change( connectionRecord.ShortTimerPeriod, -1 );
  if ( connectionRecord.CloseWaitTimer != null )
    connectionRecord.CloseWaitTimer.Change( -1, -1 );
}

Ժամաչափի ժամանակահատվածները սահմանվում են կապի ստեղծման ժամանակ: Լռելյայն ShortTimerPeriod-ը 5 վայրկյան է: Օրինակում այն ​​սահմանված է 1,5 վայրկյան:

Մուտքային կապի համար ժմչփը սկսվում է վերջին մուտքային տվյալների փաթեթը ստանալուց հետո, դա տեղի է ունենում վիճակի ReceivePacket մեթոդով: Մոնտաժում
Աշխատանքային ժամաչափի միացում (Հավաքման վիճակ).

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ... 
  // перезапускаем таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
}

Աշխատանքային ժամաչափին սպասելիս մուտքային կապի վրա այլևս փաթեթներ չեն ժամանել: Ժամաչափը անջատվեց և կանչեց ProcessPackets մեթոդը, որտեղ կորցրած փաթեթները գտնվեցին և առաջին անգամ ուղարկվեցին վերառաքման հարցումներ:
Վերաառաքման հարցումների ուղարկում (Հավաքման վիճակ).

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  if (/*проверка на потерянные пакеты */)
  {
    // отправляем запросы на повторную доставку
    // устанавливаем таймер во второй раз, для повторной попытки передачи
    if (!connectionRecord.TimerSecondTry)
    {
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
    connectionRecord.TimerSecondTry = true;
    return;
    }
  // если после двух попыток срабатываний WaitForPacketTimer 
  // не удалось получить пакеты - запускаем таймер завершения соединения
  StartCloseWaitTimer(connectionRecord);
  }
  else if (/*пришел последний пакет и успешная проверка */)
  {
    // ...
    StartCloseWaitTimer(connectionRecord);
  }
  // если ack на блок пакетов был потерян
  else
  { 
    if (!connectionRecord.TimerSecondTry)
    {
      // повторно отсылаем ack
      connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
      connectionRecord.TimerSecondTry = true;
      return;
    }
    // запускаем таймер завершения соединения
    StartCloseWaitTimer(connectionRecord);
  }
}

TimerSecondTry փոփոխականը սահմանված է ճիշտ. Այս փոփոխականը պատասխանատու է աշխատանքային ժմչփի վերագործարկման համար:

Ուղարկողի կողմից աշխատանքային ժմչփը նույնպես գործարկվում է, և վերջին ուղարկված փաթեթը նորից ուղարկվում է:
Կապի փակման ժամանակաչափի միացում (SendingCycle վիճակ).

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  // ...        
  // отправляем повторно последний пакет 
  // ...        
  // включаем таймер CloseWait – для ожидания восстановления соединения или его завершения
  StartCloseWaitTimer(connectionRecord);
}

Դրանից հետո ելքային կապում սկսվում է կապի փակման ժամանակաչափը:
ReliableUdpState.StartCloseWaitTimer:

protected void StartCloseWaitTimer(ReliableUdpConnectionRecord connectionRecord)
{
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(connectionRecord.LongTimerPeriod, -1);
  else
    connectionRecord.CloseWaitTimer = new Timer(DisposeByTimeout, connectionRecord, connectionRecord.LongTimerPeriod, -1);
}

Կապի փակման ժամանակաչափի ժամկետը լռելյայն 30 վայրկյան է:

Կարճ ժամանակ անց ստացողի կողմից աշխատող ժմչփը նորից միանում է, նորից հարցումներ են ուղարկվում, որից հետո միացման փակման ժամանակաչափը սկսում է մուտքային կապի համար։

Երբ փակ ժամանակաչափերը միանում են, երկու կապի գրառումների բոլոր ռեսուրսներն ազատվում են: Ուղարկողը հայտնում է առաքման ձախողման մասին վերին հոսքի հավելված (տես Վստահելի UDP API).
Կապի գրառումների ռեսուրսների թողարկում.

public void Dispose()
{
  try
  {
    System.Threading.Monitor.Enter(this.LockerReceive);
  }
  finally
  {
    Interlocked.Increment(ref this.IsDone);
    if (WaitForPacketsTimer != null)
    {
      WaitForPacketsTimer.Dispose();
    }
    if (CloseWaitTimer != null)
    {
      CloseWaitTimer.Dispose();
    }
    byte[] stream;
    Tcb.IncomingStreams.TryRemove(Key, out stream);
    stream = null;
    Tcb.OutcomingStreams.TryRemove(Key, out stream);
    stream = null;
    System.Threading.Monitor.Exit(this.LockerReceive);
  }
}

Ավելի խորը կոդի մեջ: Տվյալների փոխանցման վերականգնում

Տվյալների փոխանցման վերականգնման դիագրամ փաթեթի կորստի դեպքում.Վստահելի Udp արձանագրության ներդրում .Net-ի համար

Ինչպես արդեն քննարկվել է կապի փակման ժամանակ, երբ աշխատանքային ժամաչափի ժամկետը լրանա, ստացողը կստուգի կորցրած փաթեթների առկայությունը: Փաթեթի կորստի դեպքում կկազմվի հասցեատիրոջը չհասած փաթեթների ցուցակ: Այս թվերը մուտքագրվում են կոնկրետ կապի LostPackets զանգվածում, և ուղարկվում են նորից առաքման հարցումներ:
Փաթեթների վերաբաշխման հարցումների ուղարկում (Հավաքման վիճակ).

public override void ProcessPackets(ReliableUdpConnectionRecord connectionRecord)
{
  //...
  if (!ReliableUdpStateTools.CheckForNoPacketLoss(connectionRecord, connectionRecord.IsLastPacketReceived != 0))
  {
    // есть потерянные пакеты, отсылаем запросы на них
    foreach (int seqNum in connectionRecord.LostPackets)
    {
      if (seqNum != 0)
      {
        ReliableUdpStateTools.SendAskForLostPacket(connectionRecord, seqNum);
      }
    }
    // ...
  }
}

Ուղարկողը կընդունի վերառաքման հարցումը և կուղարկի բացակայող փաթեթները: Հարկ է նշել, որ այս պահին ուղարկողն արդեն սկսել է կապի փակման ժամանակաչափը, և երբ հարցում է ստացվում, այն զրոյացվում է:
Կորցրած փաթեթների վերաուղարկում (SendingCycle վիճակ).

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  // сброс таймера закрытия соединения 
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // это запрос на повторную передачу – отправляем требуемый пакет          
  else
    ReliableUdpStateTools.SendPacket(connectionRecord, ReliableUdpStateTools.RetransmissionCreateUdpPayload(connectionRecord, header.PacketNumber));
}

Նոր ուղարկված փաթեթը (դիագրամում #3 փաթեթը) ստացվում է մուտքային կապով: Ստուգում է արվում՝ տեսնելու, թե արդյոք ստացման պատուհանը լցված է, և արդյոք տվյալների նորմալ փոխանցումը վերականգնված է:
Ստուգում է ստացման պատուհանում հիթերի առկայությունը (Հավաքման վիճակ).

public override void ReceivePacket(ReliableUdpConnectionRecord connectionRecord, ReliableUdpHeader header, byte[] payload)
{
  // ...
  // увеличиваем счетчик пакетов        
  connectionRecord.PacketCounter++;
  // записываем в массив управления окном текущий номер пакета        
  connectionRecord.WindowControlArray[header.PacketNumber - connectionRecord.WindowLowerBound] = header.PacketNumber;
  // устанавливаем наибольший пришедший пакет        
  if (header.PacketNumber > connectionRecord.RcvCurrent)
    connectionRecord.RcvCurrent = header.PacketNumber;
  // перезапускам таймеры        
  connectionRecord.TimerSecondTry = false;
  connectionRecord.WaitForPacketsTimer.Change(connectionRecord.ShortTimerPeriod, -1);
  if (connectionRecord.CloseWaitTimer != null)
    connectionRecord.CloseWaitTimer.Change(-1, -1);
  // ...
  // если нам пришли все пакеты окна, то сбрасываем счетчик
  // и высылаем пакет подтверждение
  else if (connectionRecord.PacketCounter == connectionRecord.WindowSize)
  {
    // сбрасываем счетчик.      
    connectionRecord.PacketCounter = 0;
    // сдвинули окно передачи
    connectionRecord.WindowLowerBound += connectionRecord.WindowSize;
    // обнуление массива управления передачей
    connectionRecord.WindowControlArray.Nullify();
    ReliableUdpStateTools.SendAcknowledgePacket(connectionRecord);
  }
  // ...
}

Հուսալի UDP API

Տվյալների փոխանցման արձանագրության հետ փոխազդելու համար կա բաց Reliable Udp դաս, որը փաթաթում է փոխանցման կառավարման բլոկի վրա: Ահա դասի ամենակարևոր անդամները.

public sealed class ReliableUdp : IDisposable
{
  // получает локальную конечную точку
  public IPEndPoint LocalEndpoint    
  // создает экземпляр ReliableUdp и запускает
  // прослушивание входящих пакетов на указанном IP адресе
  // и порту. Значение 0 для порта означает использование
  // динамически выделенного порта
  public ReliableUdp(IPAddress localAddress, int port = 0) 
  // подписка на получение входящих сообщений
  public ReliableUdpSubscribeObject SubscribeOnMessages(ReliableUdpMessageCallback callback, ReliableUdpMessageTypes messageType = ReliableUdpMessageTypes.Any, IPEndPoint ipEndPoint = null)    
  // отписка от получения сообщений
  public void Unsubscribe(ReliableUdpSubscribeObject subscribeObject)
  // асинхронно отправить сообщение 
  // Примечание: совместимость с XP и Server 2003 не теряется, т.к. используется .NET Framework 4.0
  public Task<bool> SendMessageAsync(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, CancellationToken cToken)
  // начать асинхронную отправку сообщения
  public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)
  // получить результат асинхронной отправки
  public bool EndSendMessage(IAsyncResult asyncResult)  
  // очистить ресурсы
  public void Dispose()    
}

Հաղորդագրությունները ստացվում են բաժանորդագրությամբ: Պատվիրակել ստորագրությունը հետ կանչելու մեթոդի համար.

public delegate void ReliableUdpMessageCallback( ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteClient );

Հաղորդագրություն:

public class ReliableUdpMessage
{
  // тип сообщения, простое перечисление
  public ReliableUdpMessageTypes Type { get; private set; }
  // данные сообщения
  public byte[] Body { get; private set; }
  // если установлено в true – механизм подтверждения доставки будет отключен
  // для передачи конкретного сообщения
  public bool NoAsk { get; private set; }
}

Հաղորդագրության որոշակի տեսակի և/կամ կոնկրետ ուղարկողի բաժանորդագրվելու համար օգտագործվում են երկու կամընտիր պարամետր՝ ReliableUdpMessageTypes messageType և IPEndPoint ipEndPoint:

Հաղորդագրությունների տեսակները.

public enum ReliableUdpMessageTypes : short
{ 
  // Любое
  Any = 0,
  // Запрос к STUN server 
  StunRequest = 1,
  // Ответ от STUN server
  StunResponse = 2,
  // Передача файла
  FileTransfer =3,
  // ...
}

Հաղորդագրությունն ուղարկվում է ասինխրոն, դրա համար արձանագրությունն իրականացնում է ասինխրոն ծրագրավորման մոդել.

public IAsyncResult BeginSendMessage(ReliableUdpMessage reliableUdpMessage, IPEndPoint remoteEndPoint, AsyncCallback asyncCallback, Object state)

Հաղորդագրություն ուղարկելու արդյունքը կլինի ճշմարիտ, եթե հաղորդագրությունը հաջողությամբ հասել է հասցեատիրոջը և կեղծ, եթե կապը փակվել է ժամանակի դադարով.

public bool EndSendMessage(IAsyncResult asyncResult)

Ամփոփում

Այս հոդվածում շատ բան նկարագրված չէ: Թեմաների համադրման մեխանիզմներ, բացառությունների և սխալների կառավարում, ասինխրոն հաղորդագրություն ուղարկելու մեթոդների ներդրում: Բայց արձանագրության առանցքը, փաթեթների մշակման, կապի հաստատման և ժամանակի հետ աշխատելու տրամաբանության նկարագրությունը պետք է պարզ լինի ձեզ համար:

Հուսալի առաքման արձանագրության ցուցադրված տարբերակը բավականաչափ ամուր և ճկուն է, որպեսզի բավարարի նախկինում սահմանված պահանջները: Բայց ես ուզում եմ ավելացնել, որ նկարագրված իրականացումը կարող է բարելավվել: Օրինակ, թողունակությունը մեծացնելու և ժմչփի ժամանակաշրջանները դինամիկ փոխելու համար մեխանիզմներ, ինչպիսիք են սահող պատուհանը և RTT-ը, կարող են ավելացվել արձանագրությանը, օգտակար կլինի նաև միացման հանգույցների միջև MTU-ի որոշման մեխանիզմի ներդրումը (բայց միայն մեծ հաղորդագրություններ ուղարկելու դեպքում): .

Շնորհակալություն ուշադրության համար, անհամբեր սպասում եմ ձեր մեկնաբանություններին և մեկնաբանություններին։

Հ.Գ. Նրանց համար, ովքեր հետաքրքրված են մանրամասներով կամ պարզապես ցանկանում են փորձարկել արձանագրությունը, նախագծի հղումը GitHube-ում.
Հուսալի UDP նախագիծ

Օգտակար հղումներ և հոդվածներ

  1. TCP արձանագրության ճշգրտում. անգլերենով и ռուսերեն լեզվով
  2. UDP արձանագրության ճշգրտում. անգլերենով и ռուսերեն լեզվով
  3. RUDP արձանագրության քննարկում. draft-ietf-sigtran-reliable-udp-00
  4. Հուսալի տվյալների արձանագրություն. rfc908 и rfc1151
  5. UDP-ի միջոցով առաքման հաստատման պարզ իրականացում. Վերցրեք ձեր ցանցի ամբողջական վերահսկողությունը .NET-ի և UDP-ի միջոցով
  6. Հոդված, որը նկարագրում է NAT անցման մեխանիզմները. Peer-to-Peer հաղորդակցություն ցանցի հասցեների թարգմանիչների միջոցով
  7. Ասինխրոն ծրագրավորման մոդելի իրականացում. CLR ասինխրոն ծրագրավորման մոդելի ներդրում и Ինչպես իրականացնել IAsyncResult դիզայնի օրինակը
  8. Ասինխրոն ծրագրավորման մոդելի տեղափոխում առաջադրանքների վրա հիմնված ասինխրոն օրինակին (APM-ը TAP-ում).
    TPL և ավանդական .NET ասինխրոն ծրագրավորում
    Փոխազդել այլ ասինխրոն օրինաչափությունների և տեսակների հետ

Թարմացում: Շնորհակալություն մայորովպ и sidristij ինտերֆեյսին առաջադրանք ավելացնելու գաղափարի համար: Գրադարանի համատեղելիությունը հին օպերացիոն համակարգերի հետ չի խախտվում, քանի որ 4-րդ շրջանակն աջակցում է և՛ XP, և՛ 2003 սերվերին:

Source: www.habr.com

Добавить комментарий