Array de bytes y la clase java.nio.ByteBuffer

Un buffer es una secuencia finita de elementos -en este caso, tipos primitivos-. La clase abstracta java.nio.Buffer y sus respectivas subclases concretas (java.nio.ByteBuffer, java.nio.IntBuffer, java.nio.FloatBuffer, etc.) sirven como contenedores para datos de tipos primitivos. Estos buffers forman parte de la API NIO junto a las classes Channel, Selector, entre otras.

Estas clases, en particular java.nio.
ByteBuffer
, son de especial utilidad en aplicaciones en las cuales tenemos que generar array de bytes y que no justifica usar frameworks demasiado refinados para dicha tarea.

Un java.nio.Buffer tiene algunas propiedades escenciales:

  • Capacidad: la cantidad de elementos máxima del buffer. Siempre tiene un valor positivo, y no cambia durante la vida del objeto. Debe ser conocido al momento de inicializar el buffer.
  • Límite: según la documentación de java.nio.Buffer, el límite es “el índice del primer elemento que no debería ser leído ni escrito”. Es un valor entre la posición y la capacidad del buffer. La idea es utilizarlo como marcador para que, luego de terminada la escritura hacia el buffer, sepamos hasta donde lo podemos leer para obtener el array generado.
  • Posición: la posición actual dentro del bufffer. Es el índice del próximo elemento a leer o escribir.

Funcionalidades básicas

Todas estas funcionalidades están muy detalladas en los javadocs de las clases, pero es interesante repasar las más útiles:

  • Operaciones de escritura: la API provee las distintas variantes de operaciones put, permitiendo agregar bytes y tipos más grandes como ser int, long, float, arrays de bytes o incluso el contenido de otro ByteBuffer. Para los tipos de datos que ocupen más de un byte, se considerará la codificación (endianness) seteado utilizando el método order(). Tenemos métodos put para agregar bytes en posiciones relativas (en la posición actual) o absolutas (en una posición determinada)
  • Operaciones de lectura: al igual que con las operaciones put, las operaciones get permiten recuperar datos del buffer. Tenemos operaciones para recuperar diferentes tipos
    de datos (siempre teniendo en cuenta la codificación –endianness– cuando los tipos de datos son multi-byte), para recuperarlos desde la posición actual o desde una posición determinada. También podemos cargar un array de bytes con una sección del buffer.
  • clear(): prepara el buffer para una nueva secuencia de operaciones de escritura, seteando el límite con igual valor que la capacidad, y la posición actual en cero.
  • flip(): prepara el buffer para una secuencia de lecturas, seteando el límite igual a la posición actual, y la posición a cero. Más allá de la capacidad máxima del buffer, permite hacer operaciones sobre la porción que utilizamos anteriormente.
  • rewind(): prepara el buffer para una relectura de la información que contiene, seteando la posición en cero sin cambiar el valor del límite.
  • mark() /
    reset()
    : mark() setea la marca del buffer en la posición actual. al invocar reset(), cambiamos de posición en el buffer a la última invocación de mark().
  • slice(): crea un nuevo ByteBuffer a partir de la posición actual y hasta el fin del array. Los buffers comparten el array subyacente, por lo que un cambio en los datos se verá reflejado en ambos.
  • array(): retorna el buffer en forma de array.
  • Chaining (encadenamiento) de invocaciones: los métodos que no deben retornar valores al invocarlos, retornan una referencia a sí mismo, de manera de poder encadenar invocaciones al buffer. El encadenamiento no aporta nuevas funcionalidades, simplemente mejora la usabilidad.

Ejemplo de uso

Imaginemos que necesitamos generar un array de bytes para enviar a un sistema externo, que contiene la siguiente información:

CampoTipo de datoLargoFormato
Largo del mensajeshort2 bytesLittle Endian
Idlong8 bytesBig Endian
Largo del nombreshort2 bytesLittle Endian
NombreStringMáximo 30 caracteres
Edadbyte1 byte
Salarioint4 bytesBig Endian

Debemos codificar el largo total del mensaje y el largo del nombre como little-endian (es decir, el byte menos significativo va en la primer posición, y el byte más significativo va en la última) en lugar de big-endian -a veces también llamado network order, por ser la codificación definida en
el protocolo IP-.

Aprovecharemos las funcionalidades de la clase ByteBuffer para resolver este problema. En particular, ByteBuffer nos permite setear la representación de los enteros de varios bytes en el array, utilizando el método order(ByteOrder bo). Una solución que resuelve la generación del array podría ser la siguiente:

package com.josearrarte.demo;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class ByteBufferDemo {

	private static final int SIZEOF_BYTE = 1;
	private static final int SIZEOF_SHORT = 2;
	private static final int SIZEOF_INT = 4;
	private static final int SIZEOF_LONG = 8;

	public static void main(String[] args) {
		
		byte[] byteArray = buildByteArray(12345678L, "Pedro Picapiedra", (byte)46, 25000);
		
		String hexString = getHexString(byteArray);
		System.out.println(hexString);
	}

	private static byte[] 
buildByteArray(long id, String name, byte age, int salary) {
		
		int capacity = 2 * SIZEOF_SHORT +	// largos 
			SIZEOF_LONG + 					// id
			name.length() + 				// nombre
			SIZEOF_BYTE + 					// edad
			SIZEOF_INT;						// salario
		
		ByteBuffer buffer = ByteBuffer.allocate(capacity);
		
		// largo de datos
		buffer.order(ByteOrder.LITTLE_ENDIAN);
		buffer.putShort((short)capacity);
		
		// id
		buffer.order(ByteOrder.BIG_ENDIAN);
		buffer.putLong(id);

		// largo del campo nombre
		buffer.order(ByteOrder.LITTLE_ENDIAN).putShort((short)name.length());
		
		// nombre
		buffer.order(ByteOrder.BIG_ENDIAN).put(name.getBytes());
		
		// edad
		buffer.put(age).putInt(salary);
		
		return buffer.array();
		
	}
	
	private static String getHexString(byte[] array) {
		final char[] chars = { '0', '1', '2', '3', '4', 
				'5', '6', '7', '8', '9', 
				'A', 'B', 'C', 'D', 'E', 'F' };
		
		StringBuilder strBuilder = new StringBuilder();
		
		for (int 
i = 0; i < array.length; i++) {
			byte b = array[i];
			
			byte lowNibble = (byte) (b & 0x0F);
			byte highNibble = (byte) ((b & 0xF0) >>> 4);
			
			strBuilder.append(chars[highNibble]);
			strBuilder.append(chars[lowNibble]);
			strBuilder.append(' ');
		}
		
		return strBuilder.toString();
	}
}

La salida del programa es:

21 00 00 00 00 00 00 BC 61 4E 10 00 50 65 64 72 6F 20 50 69 63 61 70 69 65 64 72 61 2E 00 00 61 A8

Podemos desglosar la salida para cada dato:

  • Largo de datos: 21 00 (33)
  • Id: 00 00 00 00 00 BC 61 4E (12345678)
  • Largo del campo de nombre: 10 00 (16)
  • Nombre: 50 65 64 72 6F 20 50 69 63 61 70 69 65 64 72 61 (“Pedro Picapiedra”)
  • Edad: 2E (46)
  • Salario: 00 00 61 A8 (25000)

Notemos la facilidad ByteBuffer que nos da a la hora de tener que intercambiar los formatos de
enteros y de mezclar distintos tipos primitivos en un mismo array. No tuvimos necesidad de ir calculando las posiciones dentro del array, ni calcular la representación de enteros en formato little-endian o big-endian.

Referencias

API de java.nio.ByteBuffer
API de java.nio.Buffer
http://en.wikipedia.org/wiki/Endianness
http://en.wikipedia.org/wiki/New_I/O

Etiquetado . Bookmark the permalink.

8 respuestas a Array de bytes y la clase java.nio.ByteBuffer

  1. Pingback: Tweets that mention Array de bytes y la clase java.nio.ByteBuffer | José Arrarte | Blog de notas -- Topsy.com

  2. Claudio says:

    Hola…muy bueno tu ejemplo.
    pero tengo una duda, estoy desarrollando una aplicacion que hace el proceso inverso, es decir estoy haciendo el software que recibe este tipo de mensajes, pero no se como hacer la decodificacion, como poder obtener la informacion que me estan enviado.

    Si pudes poner un ejemplo, te lo agradecederia…
    Gracias.

    Claudio…

    • Muchas gracias por tu comentario Claudio.

      Para hacer lo que necesitas puedes, por ejemplo, partir de un array de bytes con el mensaje que te enviaron, y hacer ByteArray.wrap(anArray). Luego, al ByteBuffer que te retorna ese método puedes invocarle los métodos get*, que hacen el proceso inverso a los put*.

      Saludos y suerte!

  3. Claudio says:

    @José Arrarte
    Jose, me quedo una duda…
    Cuando el mensaje que me llega, segun el manual de la aplicación me envia un array de 21 bytes, me dice que del 1 al 4, es un codigo, del 5 al 8 CRC, etc..
    Como podria obtener estos datos, lo intente con los metod get* del ByteBuffer pero no lo entiendo muy bien….si me pudise orientar un poco más te lo agradeceria mucho…

    Esta es la tabla de uno de los mensajes que debo ser capaz de procesar…gracias…

    Start of Message
    The Start of Message (SOM) packet is 21 bytes long. It contains the SOM
    code, 0x14, a CRC, a message ID, total number of frames and total byte
    length of the message that follows and a character that represents whether
    this is an initial message or a response to previously transmitted message.
    Byte Layout

    1 – 4 : Code = (0x14)
    5 – 8 : 32-Bit CRC
    9 – 12 : Message ID
    13 – 16 :
    Total number of frames
    17 – 20 : Total byte length of the message to be sent. This number represents only
    the message data. This does not include framing numbers, codes, message
    ids, etc…
    21 : ‘C’ for initial transmission or ‘R’ for response to previous transmission

  4. julio says:

    disculpa, esta linea me marca error, “ilegal start expresion” ¿Por que sera? lo estoy ejecutando en bluej.
    ………………………………………………………………………………………………………….
    byte[] byteArray = buildByteArray(12345678L, "Pedro Picapiedra", (byte)46, 25000);

  5. julio says:

    i = 0; i < array.length; i++)

  6. pedro says:

    esta linea igual me causa error
    i = 0; i < array.length; i++)

  7. juan says:

    porque aparecen simbolos que al agregarlos al blog, se corrigen?
    for (int
    i = 0; i < array.length; i++) {
    byte b = array[i];

    byte lowNibble = (byte) (b & 0x0F);
    byte highNibble = (byte) ((b & 0xF0) >>> 4);

    strBuilder.append(chars[highNibble]);
    strBuilder.append(chars[lowNibble]);
    strBuilder.append(‘ ‘);
    }

    return strBuilder.toString();
    }

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *