Доброго времени суток.

Разбор полетов провожу на Reactjs (сторона клиента) и Nodejs (сторона сервера).

Недавно в моем маленьком проекте встал вопрос, как легко и просто можно обмениваться изображениями по типу клиент — сервер.

Сегодня мы научимся отправлять бинарные данные (конкретно изображения ) со стороны клиента и обрабатывать их на сервере. Добро пожаловать в под кат.

Если ваше web-приложение — это соц.сеть или мессенджер или что-то подобное, то вам непременно придется работать с бинарными данными, в большинстве случаев — это обработка изображений. Об этом и пойдет речь.

Чтобы иметь общее представление, я буду толковать последовательно.

Ознакомимся со следующим


  1. Двоичная система счисления и десятичная система счисления
  2. Кодовая страница: ASCII, Unicode
  3. Бит и байт
  4. Формат hex
  5. Двоичный файл
  6. Буффер и потоки nodejs
  7. ArrayBuffer и Uint8Array
  8. FileReader

Двоичная система счисления и десятичная система счисления.


Это число, которое записывается с помощью двух символов: единиц и нулей.
Простым языком — это 2 (основание) в степени n (количество разрядов), например число 10001 имеет 5 разрядов:

4 разряд = 1; 2^4 = 16;
3 разряд = 0;
2 разряд = 0;
1 разряд = 0;
0 разряд = 1; 2^0 = 1;

1-true — производим расчет;
0-false — не производим расчет;

В итоге получаем 16 + 1 = 17;
Обратной операцией компьютер представляет данные в двоичном виде.

const num = 17; // десятичное число
const binNum = num.toString(2); // преобразование в двоичный вид
const initNum = parseInt(binNum, 2); // обратная операция

Десятичная система счисления — это число, которое записывается с помощью 10 символов, от 0 — 9. Мы используем их повседневно.

Дело в том, что компьютер живет своим представлением информации. Если мы пишем на родном языке или считаем в десятичной системе счисления, то для компьютера все это двоичное представление. Вы спросите: “Что значит двоичное представление?”. Как говорила моя учительница английского, “как слышится, так и пишется”. Компьютер представляет файлы, содержащие текст или иное, или какие-либо вычисления в двоичную систему счисления — это и есть двоичное представление. Языком машины число «17» является — 10001.

Кодовая страница: ASCII, Unicode


Возникает вопрос. Понятно с числами, но как же текст? Как машина будет преобразовывать текст в двоичный вид? Все просто. Для этого существуют кодировки типа ASCII или Unicode. Это простая таблица ( кодовая страница ), где имеется сопоставление нашему символу число, например символ “S” — это число 53 по таблице ASCII, далее компьютер представит число 53 в двоичном виде — 110101.
Что касаемо Unicode — это мощный стандарт, включающий в себя кодировку UTF-8, которая имеет большой спрос в web-пространстве и unix подобных системах. Принцип работы общий.

Бит и байт


Бит — это единица измерения информации компьютером. А мы уже знаем как компьютер представляет информацию. Да! Вы правы. Один бит равен 1 или 0. Для компьютера 1 или 0 — это что-то вроде транзистора, true или false. Теперь понять что такое байт совсем не трудно — это 8 битов, его еще называют октет ( типа состоит из 8 ).

Формат hex


Переходим к “hex” формату. Мы говорили о двоичной системе счисления, десятичной системе, помимо прочего существует и шестнадцатиричная система счисления, верстальщики поймут о чем я. Это число, которое записывается с помощью 16 символов, от 0 — 9 и от a-f. Навивается вопрос: “Зачем так все усложнять?”. Так как единицей памяти является 8-битный байт, все же значение его удобнее записывать двумя шестнадцатиричными цифрами. Также в стандарте Юникода номер символа принято записывать в шестнадцатиричном виде, используя не менее 4 цифр.
Данный формат работает следующим образом: в случае с 8 битным байтом, он делит 8 битов по полам, то есть 10101011 — 1010 | 1011, после преобразует каждую часть соответственно. 1010 — a, 1011 — b;

const num = 196; // десятичное число
const hexNum = num.toString(16); // преобразование в hex вид
const initNum = parseInt(hexNum, 16); // преобразование в десятичный вид - обратная операция

Двоичный файл


Переходим к следующему пункту, его еще называют бинарным файлом — это массив байтов. Почему двоичный? Байты состоят из битов, то есть символов двоичной системы счисления, отсюда и название двоичный. Под категорию двоичный файл подходят абсолютно все файлы, то есть файлы содержащие текст, изображения — все это двоичные файлы, отсюда двоичные данные, отсюда бинарные данные.

Нередко бинарные данные выводятся символами кодовой страницы ASCII, а также в формате hex, в частности изображения. Приведу пример. У нас есть следующее изображение:

Смотреть вложение


Его частичная визуализация представляется в виде hex-agent-smith

Смотреть вложение

Правая часть символов является символами таблицы ASCII, левая же часть как раз и есть hex формат, преобразованный от двоичного кода.

ArrayBuffer и Uint8Array


ArrayBuffer — объект для работы с бинарными данными на javascript. Фиксирует длину памяти для работы с бинарником. Например: new ArrayBuffer(256) — создаст массивоподобный объект размером в 256 байт, не больше — не меньше. На каждый байт приходится конкретная информация. ArrayBuffer не является массивом и методы массива к нему не применимы. Вы зададите вопрос, как же его использовать? Объект Uint8Array дает представление ArrayBuffer в беззнаковое 8-битное число, то есть от 0 — 255.

FileReader


FileReader? Это конструктор по созданию следующего:

<input type="file">

Что дальше?


Теперь рассмотрим прекрасный пример, демонстрирующий обработку изображения:

  1. Модуль ReactJS
    import React, {useState, useRef, useCallback} from 'react';
    import axios from 'axios';
    
    export const UPLOAD_AVATAR = 'http://localhost:8080/api/upload_avatar';
    
    function App() {
      // определим изменяемый ref для объекта FileReader
      const fileRef = useRef(null);
      const [ loading, setLoading ] = useState(false);
    
      const handleSubmit = useCallback( event => {
        event.preventDefault();
    
        const fetchData = async (uint8Array) => {
          try {
            const response = await axios({
              method: 'post',
              url: UPLOAD_AVATAR,
              data: [...uint8Array] // не отправляем в JSON, размер изображения увеличится
            });
    
            setLoading(false);
            console.log(response);
          } catch (e) {
            console.error(e.message, 'function handleSubmit')
          }
        };
    
        if(!fileRef.current) return void null;
    
        const reader = new FileReader();
        reader.onloadend = () => {
          const uint8Array = new Uint8Array(reader.result);
          setLoading(true);
          fetchData(uint8Array);
        };
    
    
        // рекомендованный метод
        reader.readAsArrayBuffer(fileRef.current[0]);
    
        // метод reader.readAsBinaryString(fileRef.current[0]) 
        // согласно MDN,
        // уже был однажды удален из File API specification, 
        // но после его вернули
        // в использование, но все же рекомендуют 
        // использовать readAsArrayBuffer
        // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsBinaryString
      }, []);
    
      const nodeDom = (
        <form onSubmit={handleSubmit}>
          <div>
            <input
              onChange={e => fileRef.current = e.target.files}
              accept="image/*"
              type="file"
              id="button-file"
            />
          </div>
          <button type="submit">{loading ? 'Сохраняю...' : 'Сохранить'}</button>
        </form>
      );
    
      return nodeDom
    }
    
    export default App;
    
                   

  2. Модуль ExpressJS
    const express = require("express");
    const cors = require("cors");
    
    const crypto = require('crypto');
    const fs = require('fs');
    
    const PORT = 8080;
    
    const app = express();
    
    app.use(express.static("public"));
    app.use(express.json({ limit: "50mb" }));
    app.use(cors());
    
    app.post("/api/upload_avatar", (req, res) => {
      console.log(req.body);
      const randomString = crypto.randomBytes(5).toString('hex');
      const stream = fs.createWriteStream(`./public/images/${randomString}.png`);
    
      stream.on('finish', function () {
        console.log('file has been written');
        res.end('file has been written');
      });
    
      stream.write(Buffer.from(req.body), 'utf-8');
      stream.end();
    });
    
    app.listen(PORT, () => console.log(`Server running at ${PORT}`));
    
                  


Итог:


  • Для чего нужны кодировки
  • Общее представление бинарных данных
  • Рекомендуемый способ отправки байтов
  • Объект буфера
  • Объект FileReader
  • Простая реализация на хуках