Java: conversión de cadenas hacia y desde ByteBuffer y problemas asociados

81

Estoy usando Java NIO para mis conexiones de socket, y mi protocolo está basado en texto, por lo que necesito poder convertir Strings en ByteBuffers antes de escribirlos en SocketChannel, y convertir los ByteBuffers entrantes de nuevo en Strings. Actualmente, estoy usando este código:

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

Esto funciona la mayor parte del tiempo, pero me pregunto si es la forma preferida (o la más sencilla) de realizar cada dirección de esta conversión, o si hay otra forma de intentarlo. De vez en cuando, y aparentemente de forma aleatoria, las llamadas encode()y decode()arrojarán una java.lang.IllegalStateException: Current state = FLUSHED, new state = CODING_ENDexcepción, o similar, incluso si estoy usando un nuevo objeto ByteBuffer cada vez que se realiza una conversión. ¿Necesito sincronizar estos métodos? ¿Alguna mejor manera de convertir entre cadenas y ByteBuffers? ¡Gracias!

DivideByHero
fuente
Sería útil ver el seguimiento completo de la pila de la excepción.
Michael Borgwardt

Respuestas:

53

Consulte las descripciones de la API CharsetEncodery CharsetDecoder: debe seguir una secuencia específica de llamadas a métodos para evitar este problema. Por ejemplo, para CharsetEncoder:

  1. Restablezca el codificador mediante el resetmétodo, a menos que no se haya utilizado antes;
  2. Invocar el encodemétodo cero o más veces, siempre que haya una entrada adicional disponible, pasando falseel argumento endOfInput y llenando el búfer de entrada y vaciando el búfer de salida entre invocaciones;
  3. Invoque el encodemétodo una última vez, pasando truepor el argumento endOfInput; y entonces
  4. Invoque el flushmétodo para que el codificador pueda vaciar cualquier estado interno al búfer de salida.

Por cierto, este es el mismo enfoque que estoy usando para NIO, aunque algunos de mis colegas están convirtiendo cada carácter directamente en un byte sabiendo que solo están usando ASCII, que puedo imaginar que probablemente sea más rápido.

Adamski
fuente
2
¡Muchas gracias, eso fue muy útil! Descubrí que tenía varios subprocesos llamando a mis funciones de conversión al mismo tiempo, aunque no lo había diseñado para permitir eso. Lo arreglé llamando a charset.newEncoder (). Encode () y charset.newDecoder (). Decode () para asegurarme de que estaba usando un nuevo codificador / decodificador cada vez para evitar problemas de concurrencia, o tener que sincronizar innecesariamente en esos objetos, que no comparten datos significativos en mi caso. ¡También ejecuté algunas pruebas y no encontré ninguna diferencia de rendimiento medible al usar newEncoder () / newDecoder () cada vez!
DivideByHero
3
No hay problema. Podría evitar tener que crear nuevos codificadores / decodificadores cada vez, pero seguir siendo seguro para subprocesos utilizando ThreadLocal y creando perezosamente un codificador / decodificador dedicado por subproceso según sea necesario (esto es lo que he hecho).
Adamski
1
¿Podría funcionar esto? new String (bb.array (), 0, bb.array (). length, "UTF-8")
bentech
36

A menos que las cosas hayan cambiado, estás mejor con

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

Por lo general, buffer.hasArray () será siempre verdadero o siempre falso dependiendo de su caso de uso. En la práctica, a menos que realmente desee que funcione bajo cualquier circunstancia, es seguro optimizar la rama que no necesita.

Fuwjax
fuente
14

La respuesta de Adamski es buena y describe los pasos en una operación de codificación cuando se usa el método de codificación general (que toma un búfer de bytes como una de las entradas)

Sin embargo, el método en cuestión (en esta discusión) es una variante de encode - encode (CharBuffer en) . Este es un método conveniente que implementa toda la operación de codificación . (Consulte la referencia de documentos de Java en PS)

Según los documentos, este método no debe invocarse si una operación de codificación ya está en progreso (que es lo que está sucediendo en el código de ZenBlender, usando un codificador / decodificador estático en un entorno de subprocesos múltiples).

Personalmente, me gusta usar métodos de conveniencia (en lugar de los métodos de codificación / decodificación más generales) ya que eliminan la carga al realizar todos los pasos debajo de las cubiertas.

ZenBlender y Adamski ya han sugerido varias opciones para hacer esto de forma segura en sus comentarios. Listarlos todos aquí:

  • Cree un nuevo objeto codificador / decodificador cuando sea necesario para cada operación (no es eficiente ya que podría conducir a una gran cantidad de objetos). O,
  • Utilice un ThreadLocal para evitar crear un nuevo codificador / decodificador para cada operación. O,
  • Sincronice toda la operación de codificación / decodificación (esto podría no ser lo mejor a menos que sacrificar algo de simultaneidad esté bien para su programa)

PD

referencias de java docs:

  1. Método de codificación (conveniencia): http://docs.oracle.com/javase/6/docs/api/java/nio/charset/CharsetEncoder.html#encode%28java.nio.CharBuffer%29
  2. Método de codificación general: http://docs.oracle.com/javase/6/docs/api/java/nio/charset/CharsetEncoder.html#encode%28java.nio.CharBuffer,%20java.nio.ByteBuffer,%20boolean% 29
gurpsina
fuente