Kontynuując trwającą serię artykułów poświęconych niestandardowym implementacjom narzędzi konsoli Windows, nie możemy nie wspomnieć o TFTP (Trivial File Transfer Protocol) - prostym protokole przesyłania plików.
Tak jak ostatnim razem, omówmy pokrótce teorię, zobaczmy kod realizujący funkcjonalność podobną do wymaganej i przeanalizujmy ją. Więcej szczegółów - pod rozcięciem
Nie będę kopiował-wklejał informacji referencyjnych, do których linki tradycyjnie znajdują się na końcu artykułu, powiem jedynie, że w swej istocie TFTP jest uproszczoną odmianą protokołu FTP, w którym ustawienie kontroli dostępu ma został usunięty i tak naprawdę nie ma tu nic poza poleceniami odbierania i przesyłania pliku. Aby jednak uczynić naszą implementację nieco bardziej elegancką i dostosowaną do obecnych zasad pisania kodu, składnia została nieco zmieniona - nie zmienia to zasady działania, ale interfejs IMHO staje się trochę bardziej logiczny i łączy w sobie pozytywne aspekty FTP i TFTP.
W szczególności po uruchomieniu klient żąda adresu IP serwera i portu, na którym otwarty jest niestandardowy protokół TFTP (ze względu na niezgodność ze standardowym protokołem uznałem za stosowne pozostawić użytkownikowi możliwość wyboru portu), po czym następuje następuje połączenie, w wyniku którego klient może wysłać jedno z poleceń - get lub put, aby odebrać lub wysłać plik na serwer. Wszystkie pliki są wysyłane w trybie binarnym, aby uprościć logikę.
Do implementacji protokołu tradycyjnie używałem 4 klas:
- Klient TFTPC
- Serwer TFTP
- Tester klienta TFTPC
- Tester serwera TFTP
Z uwagi na to, że klasy testowe istnieją jedynie do debugowania głównych, nie będę ich analizował, ale kod będzie w repozytorium, link do niego znajdziesz na końcu artykułu. Teraz spójrzmy na główne klasy.
Klient TFTPC
Zadaniem tej klasy jest połączenie się ze zdalnym serwerem poprzez jego adres IP i numer portu, odczytanie polecenia ze strumienia wejściowego (w tym przypadku z klawiatury), przeanalizowanie go, przesłanie na serwer i w zależności od tego, czy musisz wysłać lub odebrać plik, przesłać go lub pobrać.
Kod uruchamiający klienta w celu połączenia się z serwerem i oczekiwania na polecenie ze strumienia wejściowego wygląda następująco. Szereg zmiennych globalnych, które tu zastosowano, opisano poza artykułem, w pełnym tekście programu. Ze względu na ich banalność nie przytaczam ich, aby nie przeciążać artykułu.
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());
}
}
Przyjrzyjmy się metodom wywoływanym w tym bloku kodu:
Tutaj plik jest wysyłany - za pomocą skanera przedstawiamy zawartość pliku jako tablicę bajtów, które po kolei wpisujemy do gniazda, po czym zamykamy i ponownie otwieramy (nie jest to rozwiązanie najbardziej oczywiste, ale gwarantuje zwolnienie zasobów), po czym wyświetlamy komunikat o udanym transferze.
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());
}
}
Ten fragment kodu opisuje pobieranie danych z serwera. Wszystko znowu jest banalne, interesuje nas tylko pierwszy blok kodu. Aby dokładnie zrozumieć, ile bajtów należy odczytać z gniazda, trzeba wiedzieć, ile waży przesyłany plik. Rozmiar pliku na serwerze jest reprezentowany jako długa liczba całkowita, dlatego akceptowane są tutaj 4 bajty, które następnie są konwertowane na jedną liczbę. To nie jest podejście bardzo Java, jest raczej podobne do SI, ale rozwiązuje jego problem.
Wtedy wszystko jest banalne – otrzymujemy z gniazda znaną liczbę bajtów i zapisujemy je do pliku, po czym wyświetlamy komunikat o powodzeniu.
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());
}
}
Jeżeli w oknie klienta została wpisana inna komenda niż get lub put, zostanie wywołana funkcja showErrorMessage, wskazująca, że wprowadzone dane były nieprawidłowe. Ze względu na banalność nie będę go cytować. Nieco bardziej interesująca jest funkcja odbierania i dzielenia ciągu wejściowego. Podajemy do niego skaner, z którego spodziewamy się otrzymać linię oddzieloną dwiema spacjami, zawierającą polecenie, adres źródłowy i adres docelowy.
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");
}
}
Wysyłanie polecenia – przesyła wprowadzone polecenie ze skanera do gniazda i wymusza jego wysłanie
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());
}
}
Selektor to funkcja określająca działanie programu w zależności od wprowadzonego ciągu znaków. Wszystko tutaj nie jest zbyt piękne i zastosowany trik nie jest najlepszy z wymuszonym wyjściem poza blok kodu, ale głównym powodem tego jest brak w Javie niektórych rzeczy, takich jak delegacje w C#, wskaźniki funkcji z C++ lub w przynajmniej okropne i okropne goto, które pozwalają ci to pięknie wdrożyć. Jeśli wiesz, jak uczynić kod nieco bardziej eleganckim, z radością przyjmę krytykę w komentarzach. Wydaje mi się, że potrzebny jest tutaj słownik delegatów typu String, ale nie ma delegata ...
private void selector()
{
do{
if (typeOfCommand.equals("get")){
get(sourcePath, destPath);
break;
}
if (typeOfCommand.equals("put")){
put(sourcePath, destPath);
break;
}
showErrorMessage();
}
while (false);
}
}
Serwer TFTP
Funkcjonalność serwera różni się zasadniczo od funkcjonalności klienta tylko tym, że polecenia przychodzą do niego nie z klawiatury, ale z gniazda. Niektóre metody są w zasadzie takie same, więc nie będę ich cytować, dotknę tylko różnic.
Na początek używana jest metoda run, która jako wejście otrzymuje port i przetwarza dane wejściowe z gniazda w wiecznej pętli.
public void run(int port) {
this.port = port;
incialization();
while (true) {
getAndParseInput();
selector();
}
}
Metoda put, która otacza metodę writeToFileFromSocket otwierającą strumień zapisu do pliku i zapisując wszystkie bajty wejściowe z gniazda, po zakończeniu zapisu wyświetla komunikat wskazujący pomyślne zakończenie przesyłania.
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());
}
}
Metoda get pobiera plik serwera. Jak już wspomniano w sekcji po stronie klienta programu, aby pomyślnie przesłać plik, musisz znać jego rozmiar zapisany w długiej liczbie całkowitej, więc dzielę go na tablicę 4 bajtów, przesyłam bajt po bajcie do gniazda, a następnie po otrzymaniu i złożeniu ich na kliencie w liczbę z powrotem przesyłam wszystkie bajty tworzące plik, odczytane ze strumienia wejściowego z pliku.
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());
}
};
Metoda getAndParseInput jest taka sama jak w kliencie, z tą tylko różnicą, że odczytuje dane z gniazda, a nie z klawiatury. Kod znajduje się w repozytorium, podobnie jak selektor.
W tym przypadku inicjacja jest umieszczana w oddzielnym bloku kodu, ponieważ w ramach tej implementacji po zakończeniu transferu zasoby są zwalniane i ponownie zajęte – ponownie w celu zapewnienia ochrony przed wyciekami pamięci.
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());
}
}
Podsumowując:
Właśnie napisaliśmy własną wariację na temat prostego protokołu przesyłania danych i odkryliśmy, jak powinien on działać. W zasadzie nie odkryłem tu Ameryki i nie napisałem zbyt wielu nowych rzeczy, ale podobnych artykułów na temat Habré nie było, a w ramach pisania serii artykułów o narzędziach cmd nie sposób było tego nie poruszyć.
Linki:
Źródło: www.habr.com