Շարունակելով Windows-ի կոնսոլի կոմունալ ծառայությունների մաքսային իրականացումներին նվիրված շարունակվող հոդվածների շարքը, մենք չենք կարող չանդրադառնալ TFTP-ին (Trivial File Transfer Protocol)՝ պարզ ֆայլերի փոխանցման արձանագրությանը:
Ինչպես նախորդ անգամ, եկեք համառոտ անցնենք տեսությանը, տեսնենք այն կոդը, որն իրականացնում է պահանջվող ֆունկցիոնալությունը և վերլուծենք այն: Ավելի մանրամասն՝ կտրվածքի տակ
Ես չեմ պատճենի-տեղադրի հղումներ, որոնց հղումները ավանդաբար կարելի է գտնել հոդվածի վերջում, միայն կասեմ, որ իր հիմքում TFTP-ն FTP արձանագրության պարզեցված տարբերակն է, որում մուտքի վերահսկման կարգավորումն ունի. հեռացվել է, և իրականում այստեղ ոչինչ չկա, բացի ֆայլ ստանալու և փոխանցելու հրամաններից: Այնուամենայնիվ, մեր իրականացումը մի փոքր ավելի էլեգանտ և կոդ գրելու ներկայիս սկզբունքներին հարմարեցվելու համար, շարահյուսությունը մի փոքր փոխվել է. սա չի փոխում գործողության սկզբունքները, բայց ինտերֆեյսը, IMHO, դառնում է մի փոքր ավելի տրամաբանական և համատեղում է FTP-ի և TFTP-ի դրական կողմերը:
Մասնավորապես, երբ գործարկվում է, հաճախորդը պահանջում է սերվերի IP հասցեն և այն պորտը, որի վրա բաց է մաքսային TFTP (ստանդարտ արձանագրության հետ անհամատեղելիության պատճառով ես նպատակահարմար համարեցի օգտվողին թողնել նավահանգիստ ընտրելու հնարավորությունը), որից հետո տեղի է ունենում միացում, որի արդյունքում հաճախորդը կարող է ուղարկել հրամաններից մեկը՝ ստանալ կամ տեղադրել, ստանալ կամ ուղարկել ֆայլ սերվերին: Բոլոր ֆայլերը ուղարկվում են երկուական ռեժիմով՝ տրամաբանությունը պարզեցնելու համար:
Արձանագրությունն իրականացնելու համար ես ավանդաբար օգտագործում էի 4 դաս.
- TFTPClient
- TFTPS սերվեր
- TFTPClientTester
- TFTPServerTester
Հաշվի առնելով այն հանգամանքը, որ թեստավորման դասերը գոյություն ունեն միայն հիմնականները կարգաբերելու համար, ես դրանք չեմ վերլուծելու, բայց կոդը կլինի պահեստում, դրա հղումը կարելի է գտնել հոդվածի վերջում: Այժմ ես կանդրադառնամ հիմնական դասերին:
TFTPClient
Այս դասի խնդիրն է միացնել հեռավոր սերվերին իր ip-ով և պորտի համարով, կարդալ հրաման մուտքագրման հոսքից (այս դեպքում՝ ստեղնաշարից), վերլուծել այն, փոխանցել այն սերվերին և, կախված նրանից, թե դուք անհրաժեշտ է ուղարկել կամ ստանալ ֆայլ, փոխանցել կամ ստանալ:
Հաճախորդը սերվերին միանալու և մուտքային հոսքից հրաման սպասելու համար գործարկելու ծածկագիրը նման է հետևյալին. Մի շարք գլոբալ փոփոխականներ, որոնք օգտագործվում են այստեղ, նկարագրված են հոդվածից դուրս՝ ծրագրի ամբողջական տեքստում։ Նրանց տրիվիալության պատճառով ես դրանք չեմ մեջբերում հոդվածը չծանրաբեռնելու համար։
public void run(String ip, int port)
{
this.ip = ip;
this.port = port;
try {
inicialization();
Scanner keyboard = new Scanner(System.in);
while (isRunning) {
getAndParseInput(keyboard);
sendCommand();
selector();
}
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
Եկեք անցնենք այս կոդի բլոկում կոչված մեթոդներին.
Այստեղ ֆայլը ուղարկվում է - սկաների միջոցով մենք ներկայացնում ենք ֆայլի բովանդակությունը որպես բայթերի զանգված, որը մենք հերթով գրում ենք վարդակից, որից հետո այն փակում և նորից բացում ենք (ոչ ամենաակնհայտ լուծումը, բայց այն երաշխավորում է ռեսուրսների թողարկումը), որից հետո մենք ցուցադրում ենք հաղորդագրություն հաջող փոխանցման մասին:
private void put(String sourcePath, String destPath)
{
File src = new File(sourcePath);
try {
InputStream scanner = new FileInputStream(src);
byte[] bytes = scanner.readAllBytes();
for (byte b : bytes)
sout.write(b);
sout.close();
inicialization();
System.out.println("nDonen");
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
Կոդի այս հատվածը նկարագրում է սերվերից տվյալների առբերումը: Ամեն ինչ նորից տրիվիալ է, միայն կոդերի առաջին բլոկն է հետաքրքրում։ Որպեսզի հստակ հասկանաք, թե քանի բայթ պետք է կարդալ վարդակից, դուք պետք է իմանաք, թե որքան է կշռում փոխանցված ֆայլը: Սերվերի ֆայլի չափը ներկայացված է որպես երկար ամբողջ թիվ, ուստի այստեղ ընդունվում է 4 բայթ, որոնք հետագայում վերածվում են մեկ թվի: Սա շատ Java մոտեցում չէ, այն բավականին նման է SI-ի համար, բայց լուծում է դրա խնդիրը:
Այնուհետև ամեն ինչ աննշան է. մենք վարդակից ստանում ենք բայթերի հայտնի քանակ և գրում դրանք ֆայլում, որից հետո ցուցադրում ենք հաջողության հաղորդագրություն:
private void get(String sourcePath, String destPath){
long sizeOfFile = 0;
try {
byte[] sizeBytes = new byte[Long.SIZE];
for (int i =0; i< Long.SIZE/Byte.SIZE; i++)
{
sizeBytes[i] = (byte)sin.read();
sizeOfFile*=256;
sizeOfFile+=sizeBytes[i];
}
FileOutputStream writer = new FileOutputStream(new File(destPath));
for (int i =0; i < sizeOfFile; i++)
{
writer.write(sin.read());
}
writer.close();
System.out.println("nDONEn");
}
catch (Exception e){
System.out.println(e.getMessage());
}
}
Եթե հաճախորդի պատուհանում մուտքագրվել է այլ հրաման, բացի get-ից կամ put-ից, ապա կկանչվի showErrorMessage ֆունկցիան՝ նշելով, որ մուտքագրումը սխալ է եղել: Չնչինության պատճառով չեմ մեջբերի։ Որոշ չափով ավելի հետաքրքիր է մուտքագրման տողը ստանալու և բաժանելու գործառույթը: Մենք դրա մեջ ենք փոխանցում սկաները, որից ակնկալում ենք ստանալ երկու բացատով առանձնացված տող, որը պարունակում է հրամանը, աղբյուրի հասցեն և նպատակակետի հասցեն:
private void getAndParseInput(Scanner scanner)
{
try {
input = scanner.nextLine().split(" ");
typeOfCommand = input[0];
sourcePath = input[1];
destPath = input[2];
}
catch (Exception e) {
System.out.println("Bad input");
}
}
Հրաման ուղարկում — սկաներից մուտքագրված հրամանը փոխանցում է վարդակից և ստիպում է այն ուղարկել
private void sendCommand()
{
try {
for (String str : input) {
for (char ch : str.toCharArray()) {
sout.write(ch);
}
sout.write(' ');
}
sout.write('n');
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}
Ընտրիչը գործառույթ է, որը որոշում է ծրագրի գործողությունները՝ կախված մուտքագրված տողից: Այստեղ ամեն ինչ այնքան էլ գեղեցիկ չէ, և օգտագործված հնարքը լավագույնը չէ կոդի բլոկից դուրս հարկադիր ելքի դեպքում, բայց դրա հիմնական պատճառը Java-ում որոշ բաների բացակայությունն է, ինչպիսիք են պատվիրակները C#-ում, ֆունկցիայի ցուցիչները C++-ից կամ առնվազն սարսափելի և սարսափելի goto-ն, որը թույլ է տալիս դա գեղեցիկ կերպով իրականացնել: Եթե գիտեք, թե ինչպես կարելի է ծածկագիրը մի փոքր ավելի էլեգանտ դարձնել, ես ողջունում եմ քննադատությունը մեկնաբանություններում: Ինձ թվում է՝ այստեղ լարային-պատվիրակական բառարան է պետք, բայց պատվիրակ չկա...
private void selector()
{
do{
if (typeOfCommand.equals("get")){
get(sourcePath, destPath);
break;
}
if (typeOfCommand.equals("put")){
put(sourcePath, destPath);
break;
}
showErrorMessage();
}
while (false);
}
}
TFTPS սերվեր
Սերվերի ֆունկցիոնալությունը տարբերվում է հաճախորդի ֆունկցիոնալությունից, մեծ հաշվով, միայն նրանով, որ հրամանները նրան գալիս են ոչ թե ստեղնաշարից, այլ վարդակից: Մեթոդներից մի քանիսը հիմնականում նույնն են, ուստի ես դրանք չեմ մեջբերի, կանդրադառնամ միայն տարբերություններին։
Սկսելու համար օգտագործվում է գործարկման մեթոդը, որը որպես մուտք է ստանում պորտ և մշակում վարդակից մուտքային տվյալները հավերժական օղակով:
public void run(int port) {
this.port = port;
incialization();
while (true) {
getAndParseInput();
selector();
}
}
put մեթոդը, որը փաթաթում է writeToFileFromSocket մեթոդը, որը բացում է գրելու հոսք ֆայլի վրա և գրում բոլոր մուտքային բայթերը վարդակից, ցուցադրում է հաղորդագրություն, որը ցույց է տալիս փոխանցման հաջող ավարտը, երբ գրումն ավարտվի:
private void put(String source, String dest){
writeToFileFromSocket();
System.out.print("nDonen");
};
private void writeToFileFromSocket()
{
try {
FileOutputStream writer = new FileOutputStream(new File(destPath));
byte[] bytes = sin.readAllBytes();
for (byte b : bytes) {
writer.write(b);
}
writer.close();
}
catch (Exception e){
System.out.println(e.getMessage());
}
}
Ստանալ մեթոդը վերբերում է սերվերի ֆայլը: Ինչպես արդեն նշվեց ծրագրի հաճախորդի կողմից բաժնում, ֆայլը հաջողությամբ փոխանցելու համար անհրաժեշտ է իմանալ դրա չափը, որը պահվում է երկար ամբողջ թվով, այնպես որ ես այն բաժանեցի 4 բայթանոց զանգվածի, փոխանցեցի դրանք բայթ առ բայթ: վարդակից, այնուհետև, ստանալով և հավաքելով դրանք հաճախորդի վրա թվի մեջ, ես փոխանցում եմ բոլոր բայթերը, որոնք կազմում են ֆայլը, կարդում եմ ֆայլից մուտքագրված հոսքից:
private void get(String source, String dest){
File sending = new File(source);
try {
FileInputStream readFromFile = new FileInputStream(sending);
byte[] arr = readFromFile.readAllBytes();
byte[] bytes = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(sending.length()).array();
for (int i = 0; i<Long.SIZE / Byte.SIZE; i++)
sout.write(bytes[i]);
sout.flush();
for (byte b : arr)
sout.write(b);
}
catch (Exception e){
System.out.println(e.getMessage());
}
};
GetAndParseInput մեթոդը նույնն է, ինչ հաճախորդում, միակ տարբերությունն այն է, որ այն կարդում է տվյալները ոչ թե ստեղնաշարից, այլ վարդակից: Կոդը պահոցում է, ինչպես ընտրիչը:
Այս դեպքում սկզբնավորումը տեղադրվում է կոդերի առանձին բլոկում, քանի որ Այս իրականացման ընթացքում, փոխանցումն ավարտվելուց հետո, ռեսուրսներն ազատվում են և նորից զբաղված են կրկին՝ կրկին հիշողության արտահոսքից պաշտպանություն ապահովելու համար:
private void incialization()
{
try {
serverSocket = new ServerSocket(port);
socket = serverSocket.accept();
sin = socket.getInputStream();
sout = socket.getOutputStream();
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}
Ամփոփել:
Մենք պարզապես գրել ենք մեր սեփական տարբերակը տվյալների փոխանցման պարզ արձանագրության վրա և պարզել, թե ինչպես այն պետք է աշխատի: Սկզբունքորեն, ես այստեղ Ամերիկա չհայտնաբերեցի և շատ նոր բաներ չգրեցի, բայց Habré-ում նմանատիպ հոդվածներ չկային, և որպես cmd կոմունալ ծառայությունների մասին հոդվածների շարք գրելու մաս, անհնար էր չանդրադառնալ դրան:
Հղումներ.
Source: www.habr.com