Java: Перетворення рядка в і з ByteBuffer та пов'язані з ним проблеми


82

Я використовую Java NIO для своїх сокетних з'єднань, і мій протокол базується на тексті, тому мені потрібно мати можливість перетворити рядки в ByteBuffers перед тим, як записати їх у SocketChannel, і перетворити вхідні ByteBuffers назад у рядки. В даний час я використовую цей код:

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;
}

Це працює більшу частину часу, але я сумніваюся, чи це найкращий (чи найпростіший) спосіб зробити кожен напрямок цього перетворення, чи є інший спосіб спробувати. Іноді, здавалося б , і у випадковому порядку, заклики до encode()і decode()викине java.lang.IllegalStateException: Current state = FLUSHED, new state = CODING_ENDвиняток, або подібне, навіть якщо я використовую новий об'єкт ByteBuffer кожен раз , коли перетворення зроблено. Чи потрібно синхронізувати ці методи? Будь-який кращий спосіб конвертувати між рядками та ByteBuffers? Дякую!


Це допомогло б побачити повний стек винятку.
Майкл Борґвардт,

Відповіді:


53

Перегляньте описи CharsetEncoderта CharsetDecoderAPI - Ви повинні дотримуватися певної послідовності викликів методів, щоб уникнути цієї проблеми. Наприклад, для CharsetEncoder:

  1. Скиньте кодер через reset методу, якщо він раніше не використовувався;
  2. Викликайте encodeметод нуль або більше разів, доки може бути доступним додаткове введення, передаючиfalse аргумент endOfInput і заповнюючи вхідний буфер і змиваючи вихідний буфер між викликами;
  3. Закликайте encodeметод останнього разу, проходячиtrue аргумент endOfInput; і потім
  4. Викличте flushметод, щоб кодер міг змити будь-який внутрішній стан до вихідного буфера.

До речі, це той самий підхід, який я використовую для NIO, хоча деякі мої колеги перетворюють кожен символ безпосередньо в байт у знаннях, якими вони користуються лише за допомогою ASCII, що, я думаю, можливо швидше.


2
Велике спасибі, це було дуже корисно! Я виявив, що в мене було кілька потоків, які одночасно викликали мої функції перетворення, хоча я і не розробив це, щоб дозволити це. Я виправив це, зателефонувавши charset.newEncoder (). Encode () та charset.newDecoder (). Decode (), щоб переконатись, що я щоразу використовував новий кодер / декодер, щоб уникнути проблем з паралельністю, або без потреби синхронізувати ці об’єкти, які в моєму випадку не діляться значущими даними. Я також провів кілька тестів і не виявив жодної помітної різниці в продуктивності при використанні newEncoder () / newDecoder () кожного разу!
DivideByHero

3
Нема проблем. Ви можете уникнути необхідності створювати нові кодери / декодери кожного разу, але все одно залишатись безпечними для потоків, використовуючи ThreadLocal, і ліниво створюючи спеціальний кодер / декодер для кожного потоку (це те, що я зробив).
Адамський

1
Чи може це спрацювати? new String (bb.array (), 0, bb.array (). length, "UTF-8")
bentech

38

Якщо щось не змінилося, то вам краще

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);
}

Зазвичай buffer.hasArray () завжди буде істинним або завжди хибним, залежно від вашого випадку використання. На практиці, якщо ви дійсно не хочете, щоб це працювало за будь-яких обставин, можна безпечно оптимізувати непотрібну вам гілку.


14

Відповідь Адамського є доброю і описує кроки в операції кодування при використанні загального методу кодування (який приймає байт-буфер як один із входів)

Однак розглянутий метод (у цьому обговоренні) є варіантом кодування - кодування (CharBuffer в) . Це зручний метод, який реалізує всю операцію кодування . (Будь ласка, див. Посилання на документи Java у PS)

Відповідно до документів, цей метод не слід викликати, якщо операція кодування вже виконується (саме це відбувається в коді ZenBlender - використання статичного кодера / декодера в багатопотоковому середовищі).

Особисто я люблю користуватися зручністю методи (у порівнянні із більш загальними методами кодування / декодування), оскільки вони знімають тягар, виконуючи всі дії під ковдрами.

ZenBlender і Адамскі вже запропонували у своїх коментарях різні способи безпечного здійснення цього. Перелічивши їх усіх тут:

  • Створіть новий об'єкт кодера / декодера, коли це потрібно для кожної операції (неефективно, оскільки це може призвести до великої кількості об'єктів). АБО,
  • Використовуйте ThreadLocal, щоб уникнути створення нового кодера / декодера для кожної операції. АБО,
  • Синхронізуйте всю операцію кодування / декодування (це, можливо, не буде кращим, якщо не пожертвувати деякою паралельністю для вашої програми)

PS

посилання на java-документи:

  1. Метод кодування (зручність): http://docs.oracle.com/javase/6/docs/api/java/nio/charset/CharsetEncoder.html#encode%28java.nio.CharBuffer%29
  2. Загальний метод кодування: http://docs.oracle.com/javase/6/docs/api/java/nio/charset/CharsetEncoder.html#encode%28java.nio.CharBuffer,%20java.nio.ByteBuffer,%20boolean% 29
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.