Problem z przesyłaniem plików

0

Witam,

Muszę napisać przesyłanie plików (między socketami), gdzie plik będzie przesyłany w postaci bloków o dowolnej wielkości.
Generalnie sprawa wygląda prosto: stworzyć tablicę bajtów o wielkości x i kolejno zapisywać do niej fragment pliku i wysyłać przez gniazdko.

np.

File f = new File("plik.jpg");
int rozmiar = (int) f.length();
int blok = 1000;
int iloscBlokow = rozmiar / blok;

byte[] frag = new byte[blok];
FileInputStream fis = new FileInputStream(f);

for (int i = 0; i <= iloscBlokow; i++) {
  fis.read(frag, 0, frag.length);
  output.write(frag, 0, frag.length);
  output.flush();
}

natomiast strona odbierająca podobnie, ale:

Np.

for (i = 0; i <= iloscBlokow; i++) {
  input.read(fragBajty, 0, fragBajty.length);
  fos.write(fragBajty, 0, fragBajty.length);
}

Obiekty input i output to obiekty klas InputStream oraz OutputStream.

I przy tym dzieją się rzeczy dla mnie nie zrozumiałe...
Jeśli blok wynosi (mniej więcej) <= 1000 to nie ma żadnego problemu.
Jeśli blok ustawiam na wartość kilkutysięczną-kilkunastotysięczną, koniecznie muszę dołożyć przynajmniej Thread.sleep(1) po metodzie write obiektu output po stronie wysyłającej. (tak jakby strona odbierająca nie nadążała z odbieraniem danych)
Jeśli blok wynosi kilkadziesiąt tysięcy np 70 000 to opóźnienie muszę zwiększyć, ale nie zauważyłem żadnej zależności o ile.

Wiem, że problem jest przedstawiony bardzo chaotycznie ale zupełnie nie rozumiem co tam się dzieje.
Myślałem, że taki sposób przesyłania tablic bajtów będzie działał w ten sposób: wysyłam tablicę o długości X, odbieram tablicę o długości X i tak w kółko, aż skończy się plik.

Natomiast jeśli przesyłam (bez żadnego bawienia się w opóźnienia) tablicę o wielkości 65000, to odbiorca w pierwszej iteracji odbiera tablicę o wielkości np około 20 000, w drugiej około 30 000, itd

A pliki przesyłają się z błędami.

Czy ktoś ma pomysł w czym tkwi problem i jak spróbować go rozwiązać?

0

Problem polega na tym ze Twoj blok != blok socketow.
To ze Ty sobie ustaliles bufor (tablice) na jakistam rozmiar nie oznacza ze cala tablica za jednym razem zostanie zapisana - to sie moze odbywac blokami ktore zaleza od konfiguracji socketa. Przy zapisywaniu nie ma jeszcze klopotu, poniewaz metoda wroci dopiero jak wszystko zapisze. Problem jest w Twoim wczytywaniu - calkowicie ignorujesz ilosc bajtow ktore sie udalo wczytac, calkowicie to jest zle. InputStream.read() zwraca int, czyli liczbe bajtow ktora udalo sie wczytac, niekoniecznie sa to wszystkie bajty. Musisz zrobic wczytywanie do tablicy w petli, cos w stylu:

byte[] frag = new byte[blok];
int totalRead = 0;
for (int i = 0; i <= iloscBlokow; i++) {
while ((totalRead = input.read(frag, totalRead, fragBajty.length - totalRead) != -1);
file.write(frag, 0, totalRead);
}

Zauwaz ze zapis do pliku jest dopiero po wczytaniu calego bloku. Wzglednie, jak ci sie nie podoba powyzsza petla zmien na jakas inna (for czy cokolwiek), albo mozesz w bloku od razu zapisywac do pliku, nie czekac na caly blok, ale wtedy bedzie to wygladalo jakos tak:

byte[] frag = new byte[blok];
int read;
while ((read = input.read(frag, 0, fragBajty.length) != -1) {
file.write(frag, 0, read);
}

Nie masz problemow przy zapisywaniu / czytaniu do plikow poniewaz one maja wieksze bufory, szybciej zapisac na dysk niz wyslac / odebrac z sieci.
Masz problem przy duzych blokach poniewaz one sa cwiartowane przez TCP, a ty odbierasz tylko pierwszy fragment, omijajac pozostale, po czym nowa iteracja wczytuje stare bajty, ty myslisz ze to juz nowy blok, po drodze zapisujesz cala tablice do pliku, ktora ma same zera w miejscu bajtow ktore powinny byc wczytane.

To jest tylko moje zalozenie i pomysl (tego oczekiwales) poniewaz nie uruchamialem tego. Rowniez zamieszczone przeze mnie kody moga miec (i pewnie maja) jakies bledy skladniowe bo pisalem bez IDE.

0
::. napisał(a)

Nie masz problemow przy zapisywaniu / czytaniu do plikow poniewaz one maja wieksze bufory, szybciej zapisac na dysk niz wyslac / odebrac z sieci.

Aha, przy wczytywaniu pliku blokami rowniez radzilbym podejscie takie jak przy socketach ktore zaprezentowalem, poniewaz nigdy nie mozesz byc pewny przy jakim bloku w jakim OS zaczna sie klopoty. Mozna to wczytywanie ladnie wywalic do innej metody, i przekazywac tylko Input / Output streamy. Wzglednie mozesz uzyyc BufferedInput / OutpuStream, ktore te petle robia za Ciebie, ustawiajac wielkosc bufora na wielkosc bloku.

0

Dzięki za wytłumaczenie problemu, wszystko stało się jasne! :)

Wysyłanie pliku (najpierw wszystkie bloki o równej długości, a na koniec ostatnią tablicę o rozmiarze reszty):

int blok = 512000, bajty = 0;
int iloscProbek = rozmiar / blok;
int reszta = rozmiar - (blok*iloscProbek);
byte[] frag = new byte[blok];
bis = new BufferedInputStream(new FileInputStream(f));

for (int i = 0; i < iloscProbek; i++) {
  bajty = bis.read(frag, 0, frag.length);
  output.write(frag, 0, bajty);
}

byte[] fragOst = new byte[reszta];
bajty = bis.read(fragOst, 0, fragOst.length);
output.write(fragOst, 0, bajty);

Odbieranie:

int blok = 512000, read = 0;
byte[] frag = new byte[blok];
bos = new BufferedOutputStream(new FileOutputStream("tmp/"+nazwa));

while ((read = input.read(frag, 0, frag.length)) != -1)
  bos.write(frag, 0, read);

Niemniej jednak pojawił się problem innego typu. Obliczam checksum za pomocą md5 dla oryginalnego pliku oraz dla przesłanego (na razie w najprostszy sposób - dla całego pliku) i niestety, ale sumy się nie zgadzają.
Długo próbowałem wymyślić dlaczego, ale nic nie przychodzi mi do głowy...

0

Przy uzywaniu roznego rodzaju buforowanych strumieni bardzo wazna jest metoda flush() przy zapisywaniu - po prostu to za zapisujesz cos za pomoca write() nie znaczy to ze to idzie do strumienia ktory opakowuje, tylko ze trafia do bufora; zapisanie do opakowywanego strumienia nastepuje gdy bufor sie zapelni lub wymusisz, wlasnie za pomoca flush(), lub gdy strumien buforowany zostanie zamkniety. Nie widze flush, zatem strzelam ze to jest problem.

0

strzeliłeś idealnie :) dzięki za pomoc!
flush() na obiekcie BufferedOutputStream w pętli while rozwiązał problem

0

Hej, mam kolejny problem. Przesyłanie pliku samo w sobie działa bez zarzutu.
Ale zależy mi, aby klient podawał numer pliku, który chce pobrać (serwer najpierw udostępnia mu listę plików), a serwer go odczytuje, wyszukuje plik wg tego id i zaczyna go nadawać.
Niech to dzieje się w pętli while, tak aby klient mógł pobrać kilka plików w ciągu jednego połączenia z serwerem.
While działa np. do czasu aż klient wpisze "koniec"

Pętla na serwerze:

String wcz = "", nazwaPliku = "";
while (!wcz.equalsIgnoreCase("koniec")) {

//odbieram id nadeslane przez klienta
int wczDlugosc = input.read();
byte[] wczBajty = new byte[wczDlugosc];
input.read(wczBajty, 0, wczBajty.length);
wcz = new String(wczBajty);

//wyszukuje nazwe pliku dla danego id
Set entries = mapa.entrySet();
Iterator it = entries.iterator();
while (it.hasNext()) {
  Map.Entry entry = (Map.Entry) it.next();
  String idTmp = (String) entry.getKey();

  if (idTmp.equals(wcz))
    nazwaPliku = mapa.get(idTmp);
}
  
//wysylam plik
File f = new File("pliki/"+nazwa);
if (f.exists()) {
  int blok2 = 512000, bajty2 = 0;
  int rozmiar2 = (int)f.length();
  int iloscBlokow2 = rozmiar2 / blok2;
  int reszta2 = rozmiar2 % blok2;
  BufferedInputStream bis2 = new BufferedInputStream(new FileInputStream(f));
  byte[] plikBajty = new byte[blok2];

  for (int i = 0; i < iloscBlokow2; i++) {
    bajty2 = bis2.read(plikBajty, 0, plikBajty.length);
    output.write(plikBajty, 0, bajty2);
    output.flush();
  }

  if (reszta2 != 0) {
    byte[] plikBajtyOst = new byte[reszta2];
    bajty2 = bis2.read(plikBajtyOst, 0, plikBajtyOst.length);
    output.write(plikBajtyOst, 0, bajty2);
    output.flush();
  }
  
  bis2.close();
}
else
  System.out.println("Plik nie istnieje!");
}

Pętla u klienta:

String wcz = "";
while (!wcz.equals("koniec")) {
  System.out.println("Podaj ID: ");
  wcz = klawiatura.readLine();

  byte[] wczBajty = wcz.getBytes();
  output.write(wczBajty.length);
  output.write(wczBajty);

  int blok2 = 512000, read2 = 0, j = 0;
  byte[] plikBajty = new byte[blok2];
  //tutaj nazwa pliku jest wpisana na sztywno, powinno miec miejsce jeszcze odebranie nazwy pliku
  bos2 = new BufferedOutputStream(new FileOutputStream("plikiKlienta/plik.jpg"));

  while ((read2 = input.read(plikBajty, 0, plikBajty.length)) != -1) {
    bos2.write(plikBajty, 0, read2);
    bos2.flush();
  }

  bos2.close();
}

Niestety powyższy kod powoduje, że plik odbiera się prawidłowo, jednak komunikat "Podaj ID: " nie wyświetla się ponownie, ponieważ program nie wychodzi z pętli "while ((read2 = input.read(plikBajty, 0, plikBajty.length)) != -1)".
Dodam, że nie wychodzi z niej gdy pojawia się while na serwerze, bez niej pętla u klienta działa prawidłowo, ale wiadomo - nie można pobrać plików wielokrotnie :)

Wiem, że znowu chaotycznie, ale chyba nadal nie do końca rozumiem, co tam w środku się dzieje...

0

Nie zamykasz po stronie serwera strumienia wyjsciowego, czyli po stronie klienta nigdy nie mozesz dostac -1, po prostu czeka na bajty i dostaje ciagle zero albo nawet blokuje bo nie ma czego wczytac. Po stronie serwera nie widze czegos w stylu:
if (wcz.equals("koniec") out.close()

Bez sensu sprawdzasz nazwe pliku po id z ta petla, przeciez masz mape czyl zrob tak:
String plik = mapa.get(wcz);
if (plik == null) nieznany plik

Zle masz zrobiona komunikacje na serwerze, wszystko w jednym watku, czytanie i pisanie. Powinno byc raczej osobno, np w formie kolejki, ze watek czytajacy i piszacy maja jedna wspolna instancje jakiejs blokujacej kolejki np ArrayBlockingQueue, i watek czytajacy odklada na nia nowe id, a watek piszacy z niej pobiera.

0

Faktycznie z tą mapą to niezły babol, a co do blokującej kolejki i ArrayBlockingQueue to muszę o tym poczytać, bo nigdy nie korzystałem.

Z zamknięciem strumienia wychodzącego miałeś oczywiście rację, ale gdy go zamykam na serwerze po przesłaniu pliku, a klient poda kolejne ID to na serwerze wypada SocketException: Socket closed.
Nawiązywanie osobnego połączenia dla każdego żądanego pliku to chyba nie najlepszy pomysł?

0

Nienajlepszy.
Ty masz tutaj troche specyficzne dzialanie, musialbys wiec wymyslic jakis swoj mini-protokol. W koncu laczysz wiele plikow w 1 strumien. To co chcesz zrobic to taki troche mini-ftp. Jak ja to widze - musialbys zdefiniowac jakies cos co bedzie wysylane do klienta po zakonczeniu przesylania pliku, np jakis zestaw konkretnych kilku bajtow, takie magic-bytes. Klient jak wczytuje, to zawsze bedzie sprawdzal czy ostatnie ilestam bajtow to sa te ktore oznaczaja koniec odczytu. Na start mysle ze wystarczy np jak zdefiniujesz np "end", i po stronie klienta w petli czytajacej bys sprawdzal czy ostatnie 3 wczytane bajty sa "end". Jesli sa, to wychodzisz z petli czytajacej, i wpadasz do petli glownej w ktorej pytasz usera o id. Petle bys tez mogl zmienic z -1 na while (true) { in.read(...)} bo ten -1 i tak nie wystapi a jest mniej czytelne. Jak napiszesz to z "end" to sie pobawisz w jakies bardziej wyrafinowany znacznik.
Problem: jak klient chce pliki tekstowe, to moze sie zdarzyc ze jakis plik bedzie mial slowo "end" i akurat bedzie ono ostatnie wczytane, w przypatku tym strumien nie zostanie do konca wczytany. Aby temu zapobiec mozesz wymyslic jakis wlasny ciag ktory sie w pliku tekstowym nie znajdzie. Jakis ciag ujemnych bajtow np jest dosc malo prawdopodobny, jednak wtedy masz szanse na to ze plik binarny bedzie mial takie bajty. Musisz sprytnie dobrac ten znacznik, moze jakies slowo plus jakies bajty? - slowo malo prawdopodobne w binarnym, ciag ujemnych bajtow w tekstowym? Sam nie wiem ;d
Mozesz tez uzyc ObjectOutput/InputStream do przsylania, wtedy przesylasz cala tablice i sie nie martiwsz o znaczniki. Ale i to bedzie wymagac troche gimnastyki i przepisanie czesci.

0

Wzorując się na tym co napisałeś, wymyśliłem coś takiego:

w skrócie, u klienta ("pseudokod" :) ):

warunek = true
while (warunek) {
  inputKlawiatura = input.readUTF();

  if (inputKlawiatura == "koniec")
    warunek = false;
  else {
    if (inputKlawiatura == "1") { //ID pliku
      //żądanie pliku o id 1
    }
  }
}

Po pierwsze korzystam z klas ObjectOut/InputStream.
Odbieranie pliku polega dokładnie na tym samym, z tym, że jeśli strona wysyłająca trafi na -1 wczytując plik, to wysyła tablicę bajtów o długości 8 i wypełnioną samymi zerami.

Zadaniem odbiorcy jest cały czas odbieranie tablic (int odebrane = input.read(plikBajty, 0, plikBajty.length)), jednak jeśli trafi na tablicę o wielkości 8 i sprawdzi, że suma jej elementów to zero - to wychodzi z pętli odbierającej plik i zamyka co trzeba.

0

ehhhh, szybko chyba nie skończę tego programu...

Mam następujący problem nad którym siedzę już drugi dzień:
Dla każdego przesłanego bloku danych muszę obliczyć skrót (np. md5) i porównać md5 bloku oryginalnego oraz odebranego.
Jeśli nie zgadzają się, to dokonuję retransmisji danego bloku...

W jaki sposób retransmitować odpowiedni blok (jak wyciągnąć właściwy) nawet jeszcze się nie zastanawiałem, bo jest inny problem- jeśli przesyłany blok jest duży to nie dociera do odbiorcy w całości, przez co liczenie md5 nie sprawdza się.

Próbowałem wymyślić jakiś warunek, który oblicza skrót dopiero po osiągnięciu pewnego limitu (np wielkości bloku) ale niestety... nic co by dobrze działało

0

Poczytaj sobie jak dziala protokol TCP - tam masz retransmisje pakietow (u Ciebie blokow), kazdy pakiet ma swoj numerek itp. Musisz zrobic chyba cos podobnego.

0

OK,

Ale zanim poczytam, to co myślisz o czymś takim:
Tworzę klasę Pakiet, która implementuje Serializable i zawiera pola składowe int ID oraz byte[] blokDanych (+ akcesory).

I teraz: zapisuję fragment pliku do tablicy bajtów. Natępnie dodaję ją wraz z odp. numerem ID do obiektu klasy Pakiet.
Za pomocą writeObject wysyłam pakiet, a klient go odbiera, pobiera z niego wartości ID oraz tablicę bajtów i zapisuje ją do pliku...
(w poniższym kodzie póki co nie wykorzystuje do niczego numer ID)

Wysyłanie:

for (k = 0; k < iloscBlokow; k++) {
  bis.read(plikBajty, 0, plikBajty.length);
  
  Pakiet p = new Pakiet();
  p.setId(k);
  p.setBlok(plikBajty);

  this.output.writeObject(p);
  this.output.flush();
}

if (reszta != 0) {
  bis.read(plikReszta, 0, plikReszta.length);

  Pakiet p = new Pakiet();
  p.setId(k);
  p.setBlok(plikReszta);

  this.output.writeObject(p);
  this.output.flush();
}

Odbieranie:

for (int i = 0; i < iloscBlokow; i++) {
  Pakiet p = (Pakiet) input.readObject();
  int idBloku = p.getId();
  
  bos.write(p.getBlok(), 0, p.getBlok().length);
  bos.flush();
}

if (reszta != 0) {
  Pakiet p = (Pakiet) input.readObject();
  int idBloku = p.getId();

  bos.write(p.getBlok(), 0, p.getBlok().length);
  bos.flush();
}

Niestety odebrany plik (ma poprawny rozmiar) nie chce się otworzyć...

EDIT: A czy nie ma tutaj czasem tego samego problemu, że obiekt "pakiet" dochodzi w częściach (ze względu na swój rozmiar), przez co tablica bajtów jest źle zapisywana do pliku?

1 użytkowników online, w tym zalogowanych: 0, gości: 1