2013-04-17 3 views
5

Я построил метод хэширования файла в java, который принимает входное строковое представление filepath+filename, а затем вычисляет хэш этого файла. Хешем может быть любой из поддерживаемых Java-хэшированием хэширования, таких как MD2 - SHA-512.FileChannel ByteBuffer и Hashing Files

Я пытаюсь выполнить каждую последнюю работу, так как этот метод является неотъемлемой частью проекта, над которым я работаю. Мне рекомендовалось использовать FileChannel вместо обычного FileInputStream.

Мой оригинальный метод:

/** 
    * Gets Hash of file. 
    * 
    * @param file String path + filename of file to get hash. 
    * @param hashAlgo Hash algorithm to use. <br/> 
    *  Supported algorithms are: <br/> 
    *  MD2, MD5 <br/> 
    *  SHA-1 <br/> 
    *  SHA-256, SHA-384, SHA-512 
    * @return String value of hash. (Variable length dependent on hash algorithm used) 
    * @throws IOException If file is invalid. 
    * @throws HashTypeException If no supported or valid hash algorithm was found. 
    */ 
    public String getHash(String file, String hashAlgo) throws IOException, HashTypeException { 
     StringBuffer hexString = null; 
     try { 
      MessageDigest md = MessageDigest.getInstance(validateHashType(hashAlgo)); 
      FileInputStream fis = new FileInputStream(file); 

      byte[] dataBytes = new byte[1024]; 

      int nread = 0; 
      while ((nread = fis.read(dataBytes)) != -1) { 
       md.update(dataBytes, 0, nread); 
      } 
      fis.close(); 
      byte[] mdbytes = md.digest(); 

      hexString = new StringBuffer(); 
      for (int i = 0; i < mdbytes.length; i++) { 
       hexString.append(Integer.toHexString((0xFF & mdbytes[i]))); 
      } 

      return hexString.toString(); 

     } catch (NoSuchAlgorithmException | HashTypeException e) { 
      throw new HashTypeException("Unsuppored Hash Algorithm.", e); 
     } 
    } 

Refactored метод:

/** 
    * Gets Hash of file. 
    * 
    * @param file String path + filename of file to get hash. 
    * @param hashAlgo Hash algorithm to use. <br/> 
    *  Supported algorithms are: <br/> 
    *  MD2, MD5 <br/> 
    *  SHA-1 <br/> 
    *  SHA-256, SHA-384, SHA-512 
    * @return String value of hash. (Variable length dependent on hash algorithm used) 
    * @throws IOException If file is invalid. 
    * @throws HashTypeException If no supported or valid hash algorithm was found. 
    */ 
    public String getHash(String fileStr, String hashAlgo) throws IOException, HasherException { 

     File file = new File(fileStr); 

     MessageDigest md = null; 
     FileInputStream fis = null; 
     FileChannel fc = null; 
     ByteBuffer bbf = null; 
     StringBuilder hexString = null; 

     try { 
      md = MessageDigest.getInstance(hashAlgo); 
      fis = new FileInputStream(file); 
      fc = fis.getChannel(); 
      bbf = ByteBuffer.allocate(1024); // allocation in bytes 

      int bytes; 

      while ((bytes = fc.read(bbf)) != -1) { 
       md.update(bbf.array(), 0, bytes); 
      } 

      fc.close(); 
      fis.close(); 

      byte[] mdbytes = md.digest(); 

      hexString = new StringBuilder(); 

      for (int i = 0; i < mdbytes.length; i++) { 
       hexString.append(Integer.toHexString((0xFF & mdbytes[i]))); 
      } 

      return hexString.toString(); 

     } catch (NoSuchAlgorithmException e) { 
      throw new HasherException("Unsupported Hash Algorithm.", e); 
     } 
    } 

Оба возвращают правильный хэш, однако переработан метод представляется только сотрудничать на небольших файлов. Когда я перехожу в большой файл, он полностью задыхается, и я не могу понять, почему. Я новичок в NIO, поэтому, пожалуйста, советую.

EDIT: Вспомните, что я бросаю SHA-512 через него для тестирования.

UPDATE: Обновление с помощью моего текущего метода.

/** 
    * Gets Hash of file. 
    * 
    * @param file String path + filename of file to get hash. 
    * @param hashAlgo Hash algorithm to use. <br/> 
    *  Supported algorithms are: <br/> 
    *  MD2, MD5 <br/> 
    *  SHA-1 <br/> 
    *  SHA-256, SHA-384, SHA-512 
    * @return String value of hash. (Variable length dependent on hash algorithm used) 
    * @throws IOException If file is invalid. 
    * @throws HashTypeException If no supported or valid hash algorithm was found. 
    */ 
    public String getHash(String fileStr, String hashAlgo) throws IOException, HasherException { 

     File file = new File(fileStr); 

     MessageDigest md = null; 
     FileInputStream fis = null; 
     FileChannel fc = null; 
     ByteBuffer bbf = null; 
     StringBuilder hexString = null; 

     try { 
      md = MessageDigest.getInstance(hashAlgo); 
      fis = new FileInputStream(file); 
      fc = fis.getChannel(); 
      bbf = ByteBuffer.allocateDirect(8192); // allocation in bytes - 1024, 2048, 4096, 8192 

      int b; 

      b = fc.read(bbf); 

      while ((b != -1) && (b != 0)) { 
       bbf.flip(); 

       byte[] bytes = new byte[b]; 
       bbf.get(bytes); 

       md.update(bytes, 0, b); 

       bbf.clear(); 
       b = fc.read(bbf); 
      } 

      fis.close(); 

      byte[] mdbytes = md.digest(); 

      hexString = new StringBuilder(); 

      for (int i = 0; i < mdbytes.length; i++) { 
       hexString.append(Integer.toHexString((0xFF & mdbytes[i]))); 
      } 

      return hexString.toString(); 

     } catch (NoSuchAlgorithmException e) { 
      throw new HasherException("Unsupported Hash Algorithm.", e); 
     } 
    } 

Так что я попытался бенчмарком хэширования из MD5-файл 2.92GB, используя свой оригинальный пример и пример моего последнего изменения в. Конечно, какой-то бенчмарк является относительным, поскольку происходит кеширование ОС и дисков и другое «волшебство», которое будет искажать повторяющиеся чтения одних и тех же файлов ... но вот кадр в некоторых тестах. Я загрузил каждый метод и уволил его 5 раз после его компиляции. Тест был проведен с последнего (5-го) запуска, так как это был бы «самый горячий» запуск для этого алгоритма и любая «магия» (в моей теории в любом случае).

Here's the benchmarks so far: 

    Original Method - 14.987909 (s) 
    Latest Method - 11.236802 (s) 

То есть в время, затраченное на хэш тот же файл 2.92GB 25.03% decrease. Довольно хорошо.

+2

Почему бы не использовать [MessageDigest.update (ByteBuffer)] (http://docs.oracle.com/javase/6/docs/api/java/security/MessageDigest.html#update%28java.nio. ByteBuffer% 29), который берет 'ByteBuffer' непосредственно вместо использования массива поддержки? – prunge

+0

Просто хотел добавить для будущих посетителей - если вы переключитесь на использование 'ByteBuffer.allocateDirect()', то не будет резервного массива, а 'ByteBuffer.array()' будет 'fail'. Вместо этого переключитесь на использование 'MessageDigest.update (ByteBuffer)' за совет @punge. Это не только более эффективно, но и чище, чем пытаться прочитать буфер для некоторого массива, а затем передать этот массив в 'MessageDigest.update()'. – SnakeDoc

+1

@SnakeDoc Я нашел ошибку в вашем строковом коде для хэш-номеров. Также представлена ​​реализация с отображением памяти, которая может быть адаптирована к более крупным файлам, умножая несколько раз на файл с шагом менее 2 ГБ. – Bill

ответ

3

3 предложения:

1) очистить буфер после каждого чтения

while (fc.read(bbf) != -1) { 
    md.update(bbf.array(), 0, bytes); 
    bbf.clear(); 
} 

2) не закрывать как к и ФиС, это излишним, закрытие ФИС достаточно. FileInputStream.close API говорит:

If this stream has an associated channel then the channel is closed as well. 

3) если вы хотите улучшение производительности с FileChannel использовать

ByteBuffer.allocateDirect(1024); 
+1

Если используется 'ByteBuffer.allocateDirect (1024);' тогда 'ByteBuffer.array()' вызовы будут терпеть неудачу с 'UnsupportedOperationException'. – prunge

+0

@prunge Я немного столкнулся с этой проблемой ... Думаю, я решил, что это бит в моем последнем обновлении ... как это выглядит сейчас? – SnakeDoc

+1

Да, но вы можете использовать ByteBuffer.get (byte []) –

-1

Вот пример для файла хэширования с использованием NIO

  • Путь
  • FileChanngel
  • MappedByteBuffer

И избегать использования байта []. Таким образом, я думаю, должна улучшенная версия выше. И второй пример nio, где хешированное значение хранится в пользовательских атрибутах.То, что может использоваться для генерации HTML etag других образцов, файл не изменяется.

public static final byte[] getFileHash(final File src, final String hashAlgo) throws IOException, NoSuchAlgorithmException { 
    final int   BUFFER = 32 * 1024; 
    final Path  file = src.toPath(); 
    try(final FileChannel fc = FileChannel.open(file)) { 
     final long  size = fc.size(); 
     final MessageDigest hash = MessageDigest.getInstance(hashAlgo); 
     long position = 0; 
     while(position < size) { 
      final MappedByteBuffer data = fc.map(FileChannel.MapMode.READ_ONLY, 0, Math.min(size, BUFFER)); 
      if(!data.isLoaded()) data.load(); 
      System.out.println("POS:"+position); 
      hash.update(data); 
      position += data.limit(); 
      if(position >= size) break; 
     } 
     return hash.digest(); 
    } 
} 

public static final byte[] getCachedFileHash(final File src, final String hashAlgo) throws NoSuchAlgorithmException, FileNotFoundException, IOException{ 
    final Path path = src.toPath(); 
    if(!Files.isReadable(path)) return null; 
    final UserDefinedFileAttributeView view = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class); 
    final String name = "user.hash."+hashAlgo; 
    final ByteBuffer bb = ByteBuffer.allocate(64); 
    try { view.read(name, bb); return ((ByteBuffer)bb.flip()).array(); 
    } catch(final NoSuchFileException t) { // Not yet calculated 
    } catch(final Throwable t) { t.printStackTrace(); } 
    System.out.println("Hash not found calculation"); 
    final byte[] hash = getFileHash(src, hashAlgo); 
    view.write(name, ByteBuffer.wrap(hash)); 
    return hash; 
} 
+0

Вы должны отобразить весь файл один раз, а не все эти биты. Кажется, вы не знакомы с проблемами памяти/коллекции, связанными с использованием большого количества подключенных буферов. – EJP

+0

@EJP для полноты, можете ли вы рассказать о том, почему это может быть плохо в этом сценарии? – SnakeDoc

+0

1) Нет ли проблем при одновременном отображении большого файла в память? 2) Есть ли способ «переместить» отображаемое окно? – SkateScout

1

Еще одно возможное улучшение может произойти, если код только назначил временный буфер один раз.

например.

 int bufsize = 8192; 
     ByteBuffer buffer = ByteBuffer.allocateDirect(bufsize); 
     byte[] temp = new byte[bufsize]; 
     int b = channel.read(buffer); 

     while (b > 0) { 
      buffer.flip(); 

      buffer.get(temp, 0, b); 
      md.update(temp, 0, b); 
      buffer.clear(); 

      b = channel.read(buffer); 
     } 

Добавление

Примечание: Существует ошибка в коде строки здания. Он печатает ноль как одноразрядное число. Это можно легко устранить. например

hexString.append(mdbytes[i] == 0 ? "00" : Integer.toHexString((0xFF & mdbytes[i]))); 

Кроме того, в качестве эксперимента я переписал код для использования отображенных байт-буферов. Он работает примерно на 30% быстрее (6-7 миллисов против 9-11 миллинов FWIW). Я ожидаю, что вы сможете получить больше от него, если вы написали код хеширования кода, который работал непосредственно в буфере байтов.

Я попытался объяснить инициализацию JVM и кеширование файловой системы путем хеширования файла с каждым алгоритмом перед запуском таймера. Первый пробег кода примерно в 25 раз медленнее обычного. По-видимому, это связано с инициализацией JVM, поскольку все прогоны в цикле синхронизации примерно одинаковой длины. Похоже, что они не используют кеширование. Я тестировал алгоритм MD5. Кроме того, во время временной части выполняется только один алгоритм на протяжении всей тестовой программы.

Код в цикле короче, поэтому потенциально более понятно. Я не на 100% уверен, что какое-то давление памяти, отображающее многие файлы под большим объемом, будет воздействовать на JVM, так что это может быть что-то, что вам нужно будет исследовать и подумать, хотите ли вы рассмотреть такое решение, если хотите запустить это под нагрузкой.

public static byte[] hash(File file, String hashAlgo) throws IOException { 

    FileInputStream inputStream = null; 

    try { 
     MessageDigest md = MessageDigest.getInstance(hashAlgo); 
     inputStream = new FileInputStream(file); 
     FileChannel channel = inputStream.getChannel(); 

     long length = file.length(); 
     if(length > Integer.MAX_VALUE) { 
      // you could make this work with some care, 
      // but this code does not bother. 
      throw new IOException("File "+file.getAbsolutePath()+" is too large."); 
     } 

     ByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, length); 

     int bufsize = 1024 * 8;   
     byte[] temp = new byte[bufsize]; 
     int bytesRead = 0; 

     while (bytesRead < length) { 
      int numBytes = (int)length - bytesRead >= bufsize ? 
             bufsize : 
             (int)length - bytesRead; 
      buffer.get(temp, 0, numBytes); 
      md.update(temp, 0, numBytes); 
      bytesRead += numBytes; 
     } 

     byte[] mdbytes = md.digest(); 
     return mdbytes; 

    } catch (NoSuchAlgorithmException e) { 
     throw new IllegalArgumentException("Unsupported Hash Algorithm.", e); 
    } 
    finally { 
     if(inputStream != null) { 
      inputStream.close(); 
     } 
    } 
} 
Смежные вопросы