Java: Konwersja ciągu znaków do iz ByteBuffera i powiązane problemy

82

Używam Java NIO do połączeń przez gniazdo, a mój protokół jest oparty na tekście, więc przed zapisaniem ich w SocketChannel muszę być w stanie przekonwertować ciągi znaków na ByteBuffers i przekonwertować przychodzące ByteBuffers z powrotem na ciągi. Obecnie używam tego kodu:

public static Charset charset = Charset.forName("UTF-8");
public static CharsetEncoder encoder = charset.newEncoder();
public static CharsetDecoder decoder = charset.newDecoder();

public static ByteBuffer str_to_bb(String msg){
  try{
    return encoder.encode(CharBuffer.wrap(msg));
  }catch(Exception e){e.printStackTrace();}
  return null;
}

public static String bb_to_str(ByteBuffer buffer){
  String data = "";
  try{
    int old_position = buffer.position();
    data = decoder.decode(buffer).toString();
    // reset buffer's position to its original so it is not altered:
    buffer.position(old_position);  
  }catch (Exception e){
    e.printStackTrace();
    return "";
  }
  return data;
}

Działa to przez większość czasu, ale pytam, czy jest to preferowany (lub najprostszy) sposób wykonania każdego kierunku tej konwersji, czy też istnieje inny sposób, aby spróbować. Sporadycznie i pozornie losowo wywołuje encode()i zgłasza wyjątek lub decode()coś java.lang.IllegalStateException: Current state = FLUSHED, new state = CODING_ENDpodobnego, nawet jeśli używam nowego obiektu ByteBuffer za każdym razem, gdy wykonywana jest konwersja. Czy muszę zsynchronizować te metody? Czy jest jakiś lepszy sposób na konwersję między ciągami znaków i ByteBuffers? Dzięki!

DivideByHero
źródło
Pomogłoby zobaczyć pełny ślad stosu wyjątku.
Michael Borgwardt

Odpowiedzi:

53

Zapoznaj się z opisami interfejsu API CharsetEncoderi CharsetDecoder- aby uniknąć tego problemu, należy wykonać określoną sekwencję wywołań metod . Na przykład dla CharsetEncoder:

  1. Zresetuj enkoder za pomocą reset metody, chyba że nie był wcześniej używany;
  2. Wywołaj encodemetodę zero lub więcej razy, o ile dostępne są dodatkowe dane wejściowe, przechodzącfalse argument endOfInput i wypełniając bufor wejściowy i opróżniając bufor wyjściowy między wywołaniami;
  3. Wywołaj encodemetodę po raz ostatni, przechodząctrue argument endOfInput; i wtedy
  4. Wywołaj flushmetodę, aby koder mógł opróżnić dowolny stan wewnętrzny do bufora wyjściowego.

Nawiasem mówiąc, to jest to samo podejście, którego używam dla NIO, chociaż niektórzy z moich kolegów konwertują każdy znak bezpośrednio na bajt, wiedząc, że używają tylko ASCII, co, jak mogę sobie wyobrazić, jest prawdopodobnie szybsze.

Adamskiego
źródło
2
Bardzo dziękuję, to było bardzo pomocne! Odkryłem, że mam wiele wątków wywołujących moje funkcje konwersji jednocześnie, mimo że nie zaprojektowałem tego tak, aby to umożliwiało. Naprawiłem to, wywołując charset.newEncoder (). Encode () i charset.newDecoder (). Decode (), aby upewnić się, że za każdym razem używam nowego kodera / dekodera, aby uniknąć problemów z współbieżnością lub niepotrzebnej synchronizacji na tych obiektach, które nie udostępniają znaczących danych w moim przypadku. Przeprowadziłem również kilka testów i nie znalazłem mierzalnej różnicy w wydajności przy korzystaniu z newEncoder () / newDecoder () za każdym razem!
DivideByHero
3
Nie ma problemu. Możesz uniknąć konieczności tworzenia nowych koderów / dekoderów za każdym razem, ale nadal zachowujesz bezpieczeństwo wątków, używając ThreadLocal i leniwie tworząc dedykowany koder / dekoder dla każdego wątku w razie potrzeby (to właśnie zrobiłem).
Adamski
1
Czy to zadziała? new String (bb.array (), 0, bb.array (). length, "UTF-8")
bentech
38

O ile nic się nie zmieniło, lepiej będzie z

public static ByteBuffer str_to_bb(String msg, Charset charset){
    return ByteBuffer.wrap(msg.getBytes(charset));
}

public static String bb_to_str(ByteBuffer buffer, Charset charset){
    byte[] bytes;
    if(buffer.hasArray()) {
        bytes = buffer.array();
    } else {
        bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
    }
    return new String(bytes, charset);
}

Zazwyczaj wartość buffer.hasArray () zawsze będzie miała wartość true lub zawsze false, w zależności od przypadku użycia. W praktyce, chyba że naprawdę chcesz, aby działało w żadnych okolicznościach, możesz bezpiecznie zoptymalizować gałąź, której nie potrzebujesz.

Fuwjax
źródło
14

Odpowiedź Adamskiego jest dobra i opisuje kroki w operacji kodowania przy użyciu ogólnej metody kodowania (która przyjmuje bufor bajtów jako jedno z wejść)

Jednak omawiana metoda (w tej dyskusji) jest wariantem encode - encode (CharBuffer in) . Jest to wygodna metoda, która implementuje całą operację kodowania . (Zobacz dokumentację java w PS)

Zgodnie z dokumentacją, ta metoda nie powinna zatem być wywoływana, jeśli operacja kodowania jest już w toku (co dzieje się w kodzie ZenBlender - przy użyciu statycznego kodera / dekodera w środowisku wielowątkowym).

Osobiście lubię korzystać z wygody metod (zamiast bardziej ogólnych metod kodowania / dekodowania), ponieważ odciążają one wykonywanie wszystkich czynności pod okładkami.

ZenBlender i Adamski zasugerowali już wiele sposobów bezpiecznego zrobienia tego w swoich komentarzach. Wymieniając je wszystkie tutaj:

  • Utwórz nowy obiekt kodera / dekodera, gdy jest to potrzebne dla każdej operacji (nie jest wydajne, ponieważ może prowadzić do dużej liczby obiektów). LUB,
  • Użyj ThreadLocal, aby uniknąć tworzenia nowego kodera / dekodera dla każdej operacji. LUB,
  • Zsynchronizuj całą operację kodowania / dekodowania (może to nie być preferowane, chyba że poświęcenie pewnej współbieżności jest w porządku dla twojego programu)

PS

odniesienia do dokumentacji java:

  1. Metoda kodowania (wygodna): http://docs.oracle.com/javase/6/docs/api/java/nio/charset/CharsetEncoder.html#encode%28java.nio.CharBuffer%29
  2. Ogólna metoda kodowania: http://docs.oracle.com/javase/6/docs/api/java/nio/charset/CharsetEncoder.html#encode%28java.nio.CharBuffer,%20java.nio.ByteBuffer,%20boolean% 29
gurpsin
źródło