WebSocket-сервер на СИ

WebSocket-сервер



Эта статья поможет тем, кто захочет разобраться в этой технологии и написать свой WebSocket-сервер на языке СИ. Исходники прилагаются.


Заранее скажу. В приведённом примере не реализована работа по протоколу "wss://..." (https), не поддерживается длина тела сообщения больше 65535-ти байт и не предусмотрена передача фрагментированных сообщений. Всё это будет делаться позже.


В сети можно найти определённое количество примеров ws-серверов написанных на разных языках (преимущественно node.js и php) однако все они слишком громоздки и/или требуют сторонних библиотек. Мне же, во-первых, хотелось во всём разобраться, а во вторых, был нужен компактный сервер, использующий минимальное количество ресурсов и с возможностью работы на роутере (без переноса системы на флешку) под управлением OpenWrt.


Если Вы совсем не знакомы с технологией WebSocket, то рекомендую прочесть вот это или если знаете английский, то лучше сразу это RFC 6455.

Говоря простым языком, протокол websoket — это когда сервер не закрывает соединение после обмена данными и может через любой промежуток времени передать клиенту (либо клиент серверу) какую-либо информацию без предварительных ласк «рукопожатий».

В статье показан пример слегка урезанного WebSocket-сервера, и подробно рассказано, что и как там происходит внутри.




Алгоритм WebSocket таков: сервер работает как обычный web-сервер прослушивая какой-нибудь порт, например 86-ый (или 80-ый, не важно, у меня дальше везде будет прописан 86-ой так как 80-ый занят для других дел), и обрабатывает стандартные запросы отдавая файлы — index.html, *.css и т.д.
Когда клиент получает файл index.html содержащий специальный js-код, этот код, отработав, делает запрос на соединение по протоколу "ws://..." и отправляет секретный ключ. Сервер в свою очередь обрабатывает этот ключ (добавляет к нему идентификатор GUID ⇨ хеширует алгоритмом SHA1 ⇨ переводит в кодировку Base64) и отправляет клиенту новый ключ вместе со специальным заголовком, разрешающем ws-соединение. После этого соединение остаётся открытым и Websocket-соединение считается установленным.


Опять же, Websocket-соединение можно установить с помощью разных серверов. Один сервер, например, Lighttpd будет работать на стандартном порту и отдавать весь контент, включая index.html с js-кодом, который будет создавать ws-соединение на другой порт другого сервера, передающего какие-либо данные, например, погоду в Мехико. То есть web-сервер для контента может находится в Алжире, а ws-сервер (или несколькими) в Мексике. Ну это так, отступление от основной темы.


Теперь нужно отвлечься от сервера и разобраться с клиентом, то есть с браузерной стороной (о написании своего клиента в следующей части).
К счастью, здесь уже всё сделано разработчиками браузеров и нам нужно только загрузить страничку (index.html) со «специальным» js-кодом…

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <!--<link rel="stylesheet" href="style.css" type="text/css" />-->
        <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
        <!--<script src="jquery.js"></script>-->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
  
        <style type="text/css">
           .knopka {
           height: 40px;
           width: 120px;
           text-align: center;
           line-height: 2.1;
           cursor: pointer;
           border: 1px solid #999;
           border-radius: 10px;}
           v{}
        </style>
        <script type="text/javascript">
            $(function() {
                window.WebSocket = window.WebSocket || window.MozWebSocket;
                var websocket = new WebSocket('ws://192.168.5.196:86/ws');

                websocket.onopen = function () {
                    $('h1').css('color', '#65c178'); /* green */
                };

                websocket.onclose = function (e) {
                    console.log("WebSocket: ", e)
                    $('h1').css('color', '#fe457e'); /* red */
                };

                websocket.onerror = function () {
                    $('h1').css('color', '#fe457e'); /* red */
     
                };

                websocket.onmessage = function (message) {
                    console.log(message.data);
                    $('v').append($('<p>', { text: message.data }));
                };
                
                $('.knp0').click(function(e) {
                    e.preventDefault();
                    websocket.send($('input').val());
                    $('input').val('');
                });

                $("input[id='text-count']").keydown(function count() {
                    number = $("input[id='text-count']").val().length + 1;
                    $("#count").html(" The number of bytes: " + number);
                });

                $('.knp1').click(function(e) {
                    e.preventDefault();
                    websocket.send("hi");
                });

                $('.knp2').click(function(e) {
                    e.preventDefault();
                    websocket.send("ping");
                });

                $('.knp3').click(function(e) {
                    e.preventDefault();
                    websocket.send("close");
                });

            });
        </script>
        </head>
    <body>
        <h1>WebSocket</h1>
     
          <div class='knopka knp1'>Hi...</div>
          <div class='knopka knp3'>Close</div>
          <div class='knopka knp2'>Call PING</div>
          <div class='knopka knp0'>Send</div> <input type="text" id="text-count"/><span id="count"></span>
          <v></v>
    </body>
</html>


Честно содран где-то в сети.

Выглядит она вот так:


Если надпись зелёная, то соединение установлено, а если красная, то нет.

Что касается браузера, то порекомендую использовать FireFox, так как, например, Chromium (и его производные) постоянно запрашивает favicon.ico, что порой мешает установлению ws-соединения.



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

Итак вы открыли браузер по адресу 192.168.5.196:86 и получили index.html cо «специальной» строчкой…
Порт может быть стандартный (80), адрес укажите свой.

var websocket = new WebSocket('ws://192.168.5.196:86/ws');


… Это и есть запрос на соединение по протоколу websocket.
В скобках указывается протокол (ws://), адрес сервера (если порт 80-ый, то можно не писать), буквы (ws) после слеша могут быть любыми, а можно и вовсе без них, это просто «маячок» (см.ниже) для сервера, говорящий ему, что нужно распарсить весь пакет и поискать там заголовок — Sec-WebSocket-Key: с рандомным ключём — ViFipDSLZ5fyHuVf3PFDBQ==  в кодировке Base64.


От разных браузеров запросы выглядят по разному, но в целом суть одна и та же, вот так от FireFox:




Так от Chromium:



Заголовки (браузер генерирует их сам), интересные нам содержат:

Upgrade: websocket — запрос на websocket-соединение.
Sec-WebSocket-Key: ViFipDSLZ5fyHuVf3PFDBQ== — ключ для сервера.
Sec-WebSocket-Extensions: — расширения и подпротоколы будут описаны в следующих частях, для данной статьи (да и в целом для работы сервера) они не важны.




Соединение

Далее я буду сопровождать текст вырезками из кода, а после будет код целиком...

Сервер, получив «маячок» (ws) распарсивает весь пакет, вычленяет из него ключ — ViFipDSLZ5fyHuVf3PFDBQ== (ключ всегда постоянной длины) и кладёт его в массив…

...
char resultstr[64] = {0,};
...
if((p = strstr(buffer, "Sec-WebSocket-Key:")) != NULL)
        {                                                  
          char resultstr[64] = {0,};
          int i = 0, it = 0;
          for(i = 19; it < 24; i++, it++)
           {
             resultstr[it] = p[i];
           }

...


Теперь к ключу нужно прибавить специальную строку (GUID) определённую в спецификации RFC 6455:

...
char GUIDKey[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; //36
...

Это строка никогда не меняется.


Склеиваем ключ полученый от клиена с GUIDKey:

...
strcat(resultstr, GUIDKey);
...


В итоге получаем вот такую строку — ViFipDSLZ5fyHuVf3PFDBQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

Блок отвечающий за эти действия:

...
    /////////////////////////////////// WS /////////////////////////////////////////////////
    else if((strstr(str_from_buf, "GET /ws ")) != NULL) 
     {
       warning_access_log(buffer);

       if((p = strstr(buffer, "Sec-WebSocket-Key:")) != NULL)
        {                                                  
          char resultstr[64] = {0,};
          int i = 0, it = 0;
          for(i = 19; it < 24; i++, it++)
           {
             resultstr[it] = p[i];
           }

          strcat(resultstr, GUIDKey);
...


Далее склеенную строку передаём в функцию…

...
////////////////////////////sha1///////////////////////////////////////
          unsigned char temp[SHA_DIGEST_LENGTH] = {0,};
          SHA1(temp, resultstr, strlen(resultstr));
...

… и получаем обратно хеш-сумму этой строки — 32b97b0ab1fd13594ac7817f16bd6f95fee075e3 (всегда постоянной длины).


Далее с помощью функции…

...
////////////////////////////Base64//////////////////////////////////// 
          unsigned char key_out[64] = {0,};
          base64_encode(temp, key_out, sizeof(temp));
...

… переводим хеш-сумму в кодировку Base64 и получаем ключ, который нужно отдать клиенту — s3pPLMBiTxaQ9kYGzzhZRbK+xOo= (он тоже всегда постоянной длины).


Ну и наконец собираем и отправляем ответ клиенту…

...
char resp[131] = {0,};
snprintf(resp, 130, "%s%s%s", response_ws, key_out, "\r\n\r\n");
if(send(client_fd, resp, sizeof(char) * strlen(resp), MSG_NOSIGNAL) == -1) warning_access_log("send response_ws.");
...


… который выглядит вот так:

__________________________________________________
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
__________________________________________________

В заголовках мы сообщаем клиенту что переключились на протокол WS и отправляем ему наш ключ.

Соединение не закрывается и передаётся в отдельный поток:

...
//////////////////////////// START WS /////////////////////////////////
          if(pthread_create(&ws_thread, NULL, &ws_func, &client_fd) != 0) error_log("creating WS.");
          pthread_detach(ws_thread);
          sem_wait(&sem);
...




Если клиента удовлетворяет наш ключ, тогда ws-соединение установится, а надпись в браузере станет зелёной.




Фреймы


Тут начинается самое интересное. Обмен данными производится с помощью хитроумных фреймов, которые выглядят вот так:


    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-------+-+-------------+-------------------------------+
   |F|R|R|R| опкод |М| Длина тела  |    Расширенная длина тела     |
   |I|S|S|S|(4бита)|А|   (7бит)    |       2 байта (16 бит)        |
   |N|V|V|V|       |С|   0 - 125   |        если длина тела        |
   | |1|2|3|       |К|             |   больше 125 и меньше 65536   |
   | | | | |       |А|             |                               |
   +-+-+-+-+-------+-+-------------+-------------------------------+
   |  Продолжение расширенной длины тела, если длина тела > 65535  |
   +-------------------------------+-------------------------------+
   |                               |  Ключ маски, если МАСКА == 1  |
   +-------------------------------+-------------------------------+
   | Ключ маски (продолжение)      |       Данные фрейма ("тело")  |
   +---------------------------------------------------------------+
   |                     Данные продолжаются ...                   |
   +---------------------------------------------------------------+
   |                     Данные продолжаются ...                   |
   +---------------------------------------------------------------+
                                и т.д.

Так фрейм показан в RFC 6455.

Я перерисовал по-другому:


    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-------+-+-------------+-------------------------------+-----------------------------------------------------------------------------------------------+---------------------------------------------------------------+-------------------------------+
   |F|R|R|R| опкод |М| Длина тела  |    Расширенная длина тела     | 5-ый байт     | 6-ой байт     | 7-ой байт    | 8-ой байт      | 9-ый байт     | 10-ый байт    | 11-ый байт    | 12-ый байт    | 13-ый байт    | 14-ый байт    | 15-ый байт    | 16-ый байт    |
   |I|S|S|S|(4бита)|А|   (7бит)    |       2 байта (16 бит)        |               |               |              |                |               |               |               |               |               |               |               |               |
   |N|V|V|V|       |С|   0 - 125   |       если длина тела         |             Продолжение расширенной длины тела, если длина тела > 65535  (6 байт)             |             Ключ маски, если МАСКА == 1 (4 байта)             |  Отсюда и далее идёт тело     |  и т.д.
   | |1|2|3|       |К|             |  больше 125 и меньше 65535    |               |               |              |                |               |               |               |               |               |               |           сообщения           |
   | | | | |       |А|             |               |               |               |               |              |                |               |               |               |               |               |               |               |               |
   +-+-+-+-+-------+-+-------------+-------------------------------+-----------------------------------------------------------------------------------------------+---------------------------------------------------------------+-------------------------------+



Фреймы могут быть разной длины и передаваться либо целиком, либо фрагментами (несколько фреймов).

Зачем передавать сообщение фрагментами? Например часть данных ещё не готова (вытаскивается из базы дынных и длина этих данных ещё не известна), тогда можно отправить какую-то часть.


Фреймы бывают разных типов:

• Фрейм для передачи текста.
• Фрейм для передачи бинарных данных.
• Фрейм сообщающий о закрытии соединения.
• Фрейм PING.
• Фрейм PONG.
• Фрейм сообщающий о том, что это фрагмент сообщения.

Подробнее о типах фреймов см. ниже.



Первый байт



    0 1 2 3 4 5 6 7 
   +-+-+-+-+-------+
   |F|R|R|R| опкод |
   |I|S|S|S|(4бита)|
   |N|V|V|V|       |
   | |1|2|3|       |
   | | | | |       |
   +-+-+-+-+-------+



Первые четыре бита первого байта содержат…


      0 1 2 3 
     +-+-+-+-+
     |F|R|R|R| 
     |I|S|S|S| 
     |N|V|V|V|    
     | |1|2|3|  
     +-+-+-+-+


0-ой бит (FIN) — если сообщение передаётся одним фреймом (сообщение не фрагментированно), то FIN == 1. Если сообщение передаётся нескольким фреймами (сообщение фрагментированно), тогда FIN == 1 только у последнего фрейма, у всех предыдущих фреймов FIN == 0.
То есть FIN == 1 — это конец передачи (final — true), а FIN == 0 — это не конец передачи (final — false).

1-ый бит (RSV1), 2-ой бит (RSV2) и 3-й бит (RSV3) — нужны при использовании подпротоколов (Sec-WebSocket-Extensions), если подпротоколы не используются, то эти три бита равны нулю (подпротоколы в этой статье рассматриваться не будут, поэтому эти биты для нас не важны).



Следующие четыре бита первого байта — это опкод, который определяет тип передаваемого фрейма:


      4 5 6 7 
     +-------+
     |opcode |
     | (4)   |
     |       |
     |       |
     +-------+


0x1 — текстовый фрейм (информация передаётся в виде текста).
0x2 — двоичный фрейм (информация передаётся в виде бинарных данных).
0x3-7 — зарезервированы на будущее.
0x8 — закрытие соединения (при получении клиентом или сервером такого опкода соединение закрывается).
0x9 — обозначает PING (сервер отправляет этот опкод клиенту для проверки связи. Послать PING может только сервер).
0xA — обозначает PONG (браузер отвечает этим опкодом на PING от сервера).
0xB-F — зарезервированы на будущее.
0x0 — если фрейм фрагментирован, то все фреймы кроме первого имеют такой опкод.



Второй байт



    8 9 0 1 2 3 4 5 
   +---------------+
   |М| Длина тела  |
   |А|   (7бит)    | 
   |С|  0 - 125    | 
   |К|             |
   |А|             | 
   +-+-------------+


Здесь нужно сосредоточить внимание, так как начинается карусель с длиной сообщений.

Первый бит второго байта несёт в себе признак наличия или отсутствия битовой маски (1 или 0).
Сама битовая маска, идущая после длины «тела сообщения», состоит из четырёх байт и выступает в роли контрольной суммы (см. ниже).

Внимание! Браузер ⇨ серверу отправляет сообщение только с маской, а сервер ⇨ браузеру только без маски.

Следующие 7 бит второго байта содержат в себе длину «тела сообщения» если это сообщение меньше 126 байт (0 — 125).


от 0 до 125 байт

Если длина «тела сообщения» лежит в пределах 0 — 125 байт, то следующие 4 байта содержат битовую маску, а после идёт само сообщение.

То есть, сообщение от браузера ⇨ серверу с текстом "stD" будет выглядеть вот так:


    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
   +-+-+-+-+-------+-+-------------+---------------------------------------------------------------+-----------------------------------------------+
   |F|R|R|R| опкод |М| Длина тела  | 3-й байт      | 4-ый байт     | 5-ый байт     | 6-ой байт     | 7-ой байт     | 8-ой байт     |  9-ый байт    |
   |I|S|S|S|(4бита)|А|             |               |               |               |               |               |               |               |
   |N|V|V|V|       |С|      3      |                       Ключ маски 4 байта                      |       s               t               D       |  
   | |1|2|3|       |К|             |               |               |               |               |                   (3 байта)                   | 
   | | | | |       |А|             |               |               |               |               |               |               |               |
   +-+-+-+-+-------+-+-------------+-------------------------------+-------------------------------------------------------------------------------+

• FIN == 1.
• RSV1,2,3 == 0.
• МАСКА == 1.
• Длина тела — 3 байта.
• 4 байта — маска.
• Тело сообщения (stD) — 3 байта.

Браузер сам заполняет первые байты и маску, нам нужно только написать сообщение.


Вот так сервер получит данные:


Общая длина посылки 9 байт, 6 «служебных» байт и 3 байта полезной нагрузки (тело сообщения).


Блок отвечающий за приём текста < 126 байт и обработку его битовой маской:

...
         else if(opcode == WS_TEXT_FRAME && payload_len < 126) // от клиента получен текст
          {
            masking_key[0] = inbuf[2];
            masking_key[1] = inbuf[3];
            masking_key[2] = inbuf[4];
            masking_key[3] = inbuf[5]; 
            
            unsigned int i = 6, pl = 0;
            for(; pl < payload_len; i++, pl++)
             {
               payload[pl] = inbuf[i]^masking_key[pl % 4]; 
             }
                
            printf("Payload_len: %d\n", inbuf[1] & 0x7F);     
            printf("\nReciv TEXT_FRAME from %d client, payload: %s\n", client_fd, payload);
...



Нужно обратить внимание на то, что если используются русские буквы, тогда каждый символ займёт два байта:





больше 125 и меньше 65535 байт


Если длина «тела сообщения» больше 125 и меньше 65535 байт, тогда в последние 7 бит второго байта записывается число 126, оно не имеет отношения к длине сообщения:


    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 
   +-+-+-+-+-------+-+-------------+
   |F|R|R|R| опкод |М|             |  
   |I|S|S|S|(4бита)|А|             |       
   |N|V|V|V|       |С|     126     |      
   | |1|2|3|       |К|             |  
   | | | | |       |А|             |  
   +-+-+-+-+-------+-+-------------+

Цифра 126 — это просто «код», сообщающий серверу, что размер отправленных данных лежит в пределах от 126 до 65535 байт.

В следующие два байта пишется длина сообщения (16-ти битное число типа unsigned int):


    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
   +-+-+-+-+-------+-+-------------+-------------------------------+
   |F|R|R|R| опкод |М|             |               |               | 
   |I|S|S|S|(4бита)|А|             |          Длина тела           | 
   |N|V|V|V|       |С|     126     |      например 24458 байт      |
   | |1|2|3|       |К|             |               |               | 
   | | | | |       |А|             |               |               | 
   +-+-+-+-+-------+-+-------------+-------------------------------+


В следующие четыре байта записывается маска:


    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
   +-+-+-+-+-------+-+-------------+-------------------------------+---------------------------------------------------------------+
   |F|R|R|R| опкод |М|             |               |               | 5-ый байт     | 6-ой байт     | 7-ой байт    | 8-ой байт      | 
   |I|S|S|S|(4бита)|А|             |          Длина тела           |               |               |              |                | 
   |N|V|V|V|       |С|     126     |      например 24458 байт      |             Ключ маски, если МАСКА == 1 (4 байта)             | 
   | |1|2|3|       |К|             |               |               |               |               |              |                | 
   | | | | |       |А|             |               |               |               |               |              |                |
   +-+-+-+-+-------+-+-------------+-------------------------------+---------------------------------------------------------------+


После маски идёт «тело сообщения»:


    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 
   +-+-+-+-+-------+-+-------------+-------------------------------+---------------------------------------------------------------+-------------------------------+---------------+-------
   |F|R|R|R| опкод |М|             |               |               | 5-ый байт     | 6-ой байт     | 7-ой байт    | 8-ой байт      | 9-ый байт     | 10-ый байт    | 11-ый байт и т.д.
   |I|S|S|S|(4бита)|А|             |          Длина тела,          |               |               |              |                |               |               | 
   |N|V|V|V|       |С|     126     |      например 24458 байт      |             Ключ маски, если МАСКА == 1 (4 байта)             | отсюда и далее идёт тело сообщения размером 24458 байт
   | |1|2|3|       |К|             |               |               |               |               |              |                |               |               |               |
   | | | | |       |А|             |               |               |               |               |              |                |               |               |               |
   +-+-+-+-+-------+-+-------------+-------------------------------+---------------------------------------------------------------+-------------------------------+---------------+-------



Вот так сервер получит эти данные:


Получен «код» — 126. Общее кол-во полученных данных 24466 байт == 8 служебных байт и 24458 байт полезной нагрузки.


А это блок отвечающий за приём текста размером от 126 до 65535 байт и обработку его битовой маской:

...
         else if(opcode == WS_TEXT_FRAME && payload_len == 126) // от клиента получен текст
          {
            unsigned char len16[2] = {0,};
            unsigned int payload_len16 = 0;
            len16[0] = inbuf[2];
            len16[1] = inbuf[3]; 
            payload_len16 = (len16[0] << 8) | len16[1];
            
            masking_key[0] = inbuf[4];
            masking_key[1] = inbuf[5];
            masking_key[2] = inbuf[6];
            masking_key[3] = inbuf[7]; 
            
            unsigned int i = 8, pl = 0;
            for(; pl < payload_len16; i++, pl++)
             {
               payload[pl] = inbuf[i]^masking_key[pl % 4]; 
             }
            
            printf("Payload_code: %d\n", inbuf[1] & 0x7F);  
            printf("Payload_len16: %u\n", payload_len16);       
            printf("\nReciv TEXT_FRAME from %d client, payload: %s\n", client_fd, payload);
          }
...




больше 65535 байт


Если длина «тела сообщения» больше 65535 байт, тогда в последние 7 бит второго байта записывается число 127, оно так же не имеет отношения к длине сообщения, а только сообщает серверу, что размер отправленных данных больше 65535 байт:


    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 
   +-+-+-+-+-------+-+-------------+
   |F|R|R|R| опкод |М|             |  
   |I|S|S|S|(4бита)|А|             |       
   |N|V|V|V|       |С|     127     |      
   | |1|2|3|       |К|             |  
   | | | | |       |А|             |  
   +-+-+-+-+-------+-+-------------+


В следующие восемь байт пишется длина сообщения (64-ёх битное число):


    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 
   +-+-+-+-+-------+-+-------------+-------------------------------+---------------------------------------------------------------+-------------------------------+
   |F|R|R|R| опкод |М|             | 3-ий байт     | 4-ый байт     | 5-ый байт     | 6-ой байт     | 7-ой байт    | 8-ой байт      | 9-ый байт     | 10-ый байт    | 
   |I|S|S|S|(4бита)|А|             |               |               |               |               |              |                |               |               | 
   |N|V|V|V|       |С|     127     |                              Длина тела 8 байт (64 бита), например 724458 байт                                                | 
   | |1|2|3|       |К|             |               |               |               |               |              |                |               |               |              
   | | | | |       |А|             |               |               |               |               |              |                |               |               |            
   +-+-+-+-+-------+-+-------------+-------------------------------+---------------------------------------------------------------+-------------------------------+



Следом так же идут 4 байта маски, после которой идёт «тело сообщения». Рисовать схему не буду, так как думаю, что всё понятно.

В моём примере сервера, не реализована работа с сообщениями > 65535 байт по причине отсутствия необходимости.


Все выше описанные механизмы передачи текстовых данных относились к посылке от браузера к серверу, посылки же от сервера к браузеру, выглядят точно так же, только у них отсутствует маска. То есть в первый бит второго байта записывается 0, а за байтами указывающими длину сообщения, сразу идёт само сообщение.

Эти условия оговорены в спецификации (RFC 6455) и если, к примеру, отправить браузеру Chromium маскированное сообщение то он выругается вот так:




Теперь поговорим о том, как формировать сообщения от сервера к клиенту и о управляющих фреймах (PING-PONG и фрейм закрытия)




Сервер


закрытие соединения

Управляющие фреймы может посылать только сервер. Исключением является закрытие странички в браузере, тогда браузер посылает опкод закрытия — 0x08:




Блок отвечающий за это действо:

...
         if(opcode == WS_CLOSING_FRAME) // от клиента получен код закрытия соединения
          {
            memset(reciv_r, 0, 48);
            snprintf(reciv_r, 47, "%s%d\n", "Ws_func recive opcod - 0x08, DIE clien - ",  client_fd);
            warning_access_log(reciv_r); // пишем событие в лог
            if(close(client_fd) == -1) warning_access_log("Error close client in WS_2."); // закрываем соединение с клиентом
            pthread_exit(NULL); // убиваем поtok
          }
...



Сервер формирует и посылает опкод закрытия вот так:

...
         char close_client[] = {0x88, 0};
               
         if(send(client_fd, close_client, 2, 0) == -1)
          {
            warning_access_log("Error CLOSE."); 
            if(close(client_fd) == -1) warning_access_log("Error close client in WS_4."); // закрываем соединение с клиентом
            pthread_exit(NULL); 
          }
...


Для восстановления соединения нужно обновить страничку.


PING-PONG

Служит для проверки связи между сервером и браузером. Сервер посылает PING — это команда на которую браузер должен отправить команду PONG (опкод 0x0A). PING может содержать произвольный текст, который вернётся вместе с PONGом от браузера, а может быть и пустым, без текста.

char ping[] = {0x89, 0x00};


Только сервер может послать PING браузеру, браузер может только отвечать.


Вот так сервер посылает PING со словом «Hello»:

...
       char ping[] = {0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f}; // Ping - не маскированный, тело содержит слово Hello, это же слово вернётся с Понгом
        // char ping[] = {0x89, 0x05, 'H', 'e', 'l', 'l', 'o'}; // можно так
       
       if(send(client_fd, ping, 7, 0) == -1)
        {
          warning_access_log("Error PING."); 
          if(close(client_fd) == -1) warning_access_log("Error close client in WS_3."); // закрываем соединение с клиентом
          pthread_exit(NULL); 
        }
...



А вот так получает ответ — PONG:

...
         else if(opcode == WS_PONG_FRAME) // от клиента получен PONG (маскированный)
          {
            masking_key[0] = inbuf[2];
            masking_key[1] = inbuf[3];
            masking_key[2] = inbuf[4];
            masking_key[3] = inbuf[5]; 

            unsigned int i = 6, pl = 0;
            for(; pl < payload_len; i++, pl++)
             {
               payload[pl] = inbuf[i]^masking_key[pl % 4]; 
             }
                     
            printf("Payload_len: %d\n", inbuf[1] & 0x7F);
            printf("\nRecive PONG and text \"%s\"\n", payload);
          }
...




На страничке сделаны (для удобства отладки) кнопки, которые «заставляют» посылать сервер команду закрытия (Close) и команду PING (Call PING).


Если нажать на страничке кнопку "Hi...", тогда сервер ответит фразой с номером клиента "Hi client — 4".

Вот так сервер формирует и отправляет сообщение < 126 байт с номером клиента:

...
            if(payload[0] == 'h' && payload[1] == 'i') // от клиента получен текст "hi"
             {
               char messag[] = "Hi client - ";
               int message_size = (int) strlen(messag);
               char out_data[128] = {0,};
               memcpy(out_data + 2, messag, message_size); // копируем сообщение в массив "out_data" начиная со второго байта (первые два байта для опкода и длины тела)
               char nom_client[5] = {0,};
               sprintf(nom_client, "%d", client_fd); // номер клиента
               int nom_client_size = (int) strlen(nom_client);
               memcpy(out_data + 2 + message_size, nom_client, nom_client_size); // копируем номер клиента в массив "out_data" следом за сообщением

               message_size += nom_client_size; // получаем длину тела сообщения

               out_data[0] = 0x81;
               out_data[1] = (char)message_size;

               printf("\nSize out Msg: %d\n", message_size);
               
               if(send(client_fd, out_data, message_size + 2, 0) == -1) // отправка
                {
                  warning_access_log("Error Hi."); 
                  if(close(client_fd) == -1) warning_access_log("Error close client in WS_5."); // закрываем соединение с клиентом
                  pthread_exit(NULL); 
                }
              }
...



Вот так будет попроще, только фраза «Hi client!»:

char messag[] = "Hi client!";
               int message_size = (int) strlen(messag); // получаем длину тела сообщения
               char out_data[32] = {0,};
	       out_data[0] = 0x81;
               out_data[1] = (char)message_size;
               memcpy(out_data + 2, messag, message_size); // копируем сообщение в массив "out_data" начиная со второго байта (первые два байта для опкода и длины тела)
    
               printf("\nSize out Msg1: %d\n", message_size);
               
               if(send(client_fd, out_data, message_size + 2, 0) == -1) 
                {
                  warning_access_log("Error Hi."); 
                  if(close(client_fd) == -1) warning_access_log("Error close client in WS_5."); // закрываем соединение с клиентом
                  pthread_exit(NULL); 
                }




А вот так формируются и отправляются сообщения 126 — 65535 байт:

...
            else if(payload[0] == 'H' && payload[1] == 'I') // от клиента получен текст "HI"
             {
               char messag[] = "istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru";

               unsigned short message_size = strlen(messag);
               char out_data[SW_BUF] = {0,};
               memcpy(out_data + 4, messag, message_size); // копируем сообщение в массив "out_data" начиная со второго байта (первые два байта для опкода и длины тела)
           
               out_data[0] = 0x81; // == 10000001 == FIN1......opcod 1 (текст)
               out_data[1] = 126; // пишем цифру 126, которая в двоичном виде == 01111110, соответственно маска 0, остальные 7 бит == 126
               out_data[3] = message_size & 0xFF; // собираем длину сообщения
               out_data[2] = (message_size >> 8) & 0xFF; // собираем длину сообщения

               printf("\nSize out Msg2: %d\n", message_size);
               
               if(send(client_fd, out_data, message_size + 4, 0) == -1) // отправка
                {
                  warning_access_log("Error Hi."); 
                  if(close(client_fd) == -1) warning_access_log("Error close client in WS_5."); // закрываем соединение с клиентом
                  pthread_exit(NULL); 
                }
              }
...

На веб-страничке впишите в поле ввода слово HI (большими буквами) и нажмите кнопку Send, в ответ сервер отправит сообщение больше 125-ти байт.





Описание конкретных задач, будь то сервер для чата или отдачи графиков не входит в рамки статьи, а того что написано вполне хватает, при частичном изменении и дополнении, для многих задач.




Ну и наконец исходник целиком. Код не «причёсан», содержит много ненужной отладочной информации и всяких логов (чтоб видеть происходящее).

Функция хеширования включена в код, чтоб не подключать библиотеку openssl (на роутере она занимает много места).

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

websocketstd.c

#include "myws.h"

#define BUFSIZE 1024
#define FILESTR 32
#define ALLARRAY 64
#define SHA_DIGEST_LENGTH 20
#define SW_BUF 65552

char patch_to_dir[ALLARRAY] = {0,};
char fpfile[ALLARRAY] = {0,};
char buffer[BUFSIZE] = {0,};
int client_fd, count_warning_log =0;
struct stat stat_buf;
sem_t sem;

typedef struct
 {
   uint32_t state[5];
   uint32_t count[2];
   unsigned char buffer[64];
 } SHA1_CTX;


/////////////////////////////////////////////// SHA1 /////////////////////////////////////////////////////////////
void SHA1Transform( uint32_t state[5], const unsigned char buffer[64])
 {
    uint32_t a, b, c, d, e;
    typedef union
     {
       unsigned char c[64];
       uint32_t l[16];
     } CHAR64LONG16;

    CHAR64LONG16 block[1];    
    memcpy(block, buffer, 64);

    a = state[0]; b = state[1]; c = state[2]; d = state[3]; e = state[4];

    R0(a, b, c, d, e, 0); R0(e, a, b, c, d, 1); R0(d, e, a, b, c, 2); R0(c, d, e, a, b, 3);
    R0(b, c, d, e, a, 4); R0(a, b, c, d, e, 5); R0(e, a, b, c, d, 6); R0(d, e, a, b, c, 7);
    R0(c, d, e, a, b, 8); R0(b, c, d, e, a, 9); R0(a, b, c, d, e, 10); R0(e, a, b, c, d, 11);
    R0(d, e, a, b, c, 12); R0(c, d, e, a, b, 13); R0(b, c, d, e, a, 14); R0(a, b, c, d, e, 15);
    R1(e, a, b, c, d, 16); R1(d, e, a, b, c, 17); R1(c, d, e, a, b, 18); R1(b, c, d, e, a, 19);
    R2(a, b, c, d, e, 20); R2(e, a, b, c, d, 21); R2(d, e, a, b, c, 22); R2(c, d, e, a, b, 23);
    R2(b, c, d, e, a, 24); R2(a, b, c, d, e, 25); R2(e, a, b, c, d, 26); R2(d, e, a, b, c, 27);
    R2(c, d, e, a, b, 28); R2(b, c, d, e, a, 29); R2(a, b, c, d, e, 30); R2(e, a, b, c, d, 31);
    R2(d, e, a, b, c, 32); R2(c, d, e, a, b, 33); R2(b, c, d, e, a, 34); R2(a, b, c, d, e, 35);
    R2(e, a, b, c, d, 36); R2(d, e, a, b, c, 37); R2(c, d, e, a, b, 38); R2(b, c, d, e, a, 39);
    R3(a, b, c, d, e, 40); R3(e, a, b, c, d, 41); R3(d, e, a, b, c, 42); R3(c, d, e, a, b, 43);
    R3(b, c, d, e, a, 44); R3(a, b, c, d, e, 45); R3(e, a, b, c, d, 46); R3(d, e, a, b, c, 47);
    R3(c, d, e, a, b, 48); R3(b, c, d, e, a, 49); R3(a, b, c, d, e, 50); R3(e, a, b, c, d, 51);
    R3(d, e, a, b, c, 52); R3(c, d, e, a, b, 53); R3(b, c, d, e, a, 54); R3(a, b, c, d, e, 55);
    R3(e, a, b, c, d, 56); R3(d, e, a, b, c, 57); R3(c, d, e, a, b, 58); R3(b, c, d, e, a, 59);
    R4(a, b, c, d, e, 60); R4(e, a, b, c, d, 61); R4(d, e, a, b, c, 62); R4(c, d, e, a, b, 63);
    R4(b, c, d, e, a, 64); R4(a, b, c, d, e, 65); R4(e, a, b, c, d, 66); R4(d, e, a, b, c, 67);
    R4(c, d, e, a, b, 68); R4(b, c, d, e, a, 69); R4(a, b, c, d, e, 70); R4(e, a, b, c, d, 71);
    R4(d, e, a, b, c, 72); R4(c, d, e, a, b, 73); R4(b, c, d, e, a, 74); R4(a, b, c, d, e, 75);
    R4(e, a, b, c, d, 76); R4(d, e, a, b, c, 77); R4(c, d, e, a, b, 78); R4(b, c, d, e, a, 79);

    state[0] += a; state[1] += b; state[2] += c; state[3] += d; state[4] += e;
    a = b = c = d = e = 0;
    memset(block, 0, sizeof(block));
 }


void SHA1Init( SHA1_CTX * context)
 {
    context->state[0] = 0x67452301;
    context->state[1] = 0xEFCDAB89;
    context->state[2] = 0x98BADCFE;
    context->state[3] = 0x10325476;
    context->state[4] = 0xC3D2E1F0;
    context->count[0] = context->count[1] = 0;
 }


void SHA1Update( SHA1_CTX * context, const unsigned char *data, uint32_t len)
 {
    uint32_t i;
    uint32_t j;

    j = context->count[0];
    if ((context->count[0] += len << 3) < j) context->count[1]++;

    context->count[1] += (len >> 29);
    j = (j >> 3) & 63;
    if((j + len) > 63)
     {
       memcpy(&context->buffer[j], data, (i = 64 - j));

       SHA1Transform(context->state, context->buffer);
       for(; i + 63 < len; i += 64)
        {
          SHA1Transform(context->state, &data[i]);
        }

       j = 0;
     }

    else i = 0;

    memcpy(&context->buffer[j], &data[i], len - i);
 }


void SHA1Final( unsigned char digest[20], SHA1_CTX * context)
 {
    unsigned i;
    unsigned char c, finalcount[8];

    for(i = 0; i < 8; i++)
     {
       finalcount[i] = (unsigned char) ((context->count[(i >= 4 ? 0 : 1)] >> ((3 - (i & 3)) * 8)) & 255);     
     }

    c = 0200;
    SHA1Update(context, &c, 1);

    while((context->count[0] & 504) != 448)
     {
       c = 0000;
       SHA1Update(context, &c, 1);
     }

    SHA1Update(context, finalcount, 8); 

    for(i = 0; i < 20; i++)
     {
       digest[i] = (unsigned char) ((context->state[i >> 2] >> ((3 - (i & 3)) * 8)) & 255);
     }

    memset(context, 0, sizeof(*context));
    memset(&finalcount, 0, sizeof(finalcount));
 }


void SHA1(unsigned char *hash_out, const char *str, unsigned int len)
 {
    SHA1_CTX ctx;
    unsigned int ii;
    SHA1Init(&ctx);
    for (ii=0; ii<len; ii+=1) SHA1Update(&ctx, (const unsigned char*)str + ii, 1);
    SHA1Final((unsigned char *)hash_out, &ctx);
    hash_out[20] = 0;
 }


////////////////////////////////////// base64_encode ////////////////////////////////////////////
int base64_encode(unsigned char sha_key_in[], unsigned char base64_key_out[], int len)
 {
   int idx, idx2, blks, left_over;

   blks = (len / 3) * 3;
   for(idx=0, idx2=0; idx < blks; idx += 3, idx2 += 4) 
    {
      base64_key_out[idx2] = charset[sha_key_in[idx] >> 2];
      base64_key_out[idx2+1] = charset[((sha_key_in[idx] & 0x03) << 4) + (sha_key_in[idx+1] >> 4)];
      base64_key_out[idx2+2] = charset[((sha_key_in[idx+1] & 0x0f) << 2) + (sha_key_in[idx+2] >> 6)];
      base64_key_out[idx2+3] = charset[sha_key_in[idx+2] & 0x3F];
    }

   left_over = len % 3;

   if(left_over == 1) 
    {
      base64_key_out[idx2] = charset[sha_key_in[idx] >> 2];
      base64_key_out[idx2+1] = charset[(sha_key_in[idx] & 0x03) << 4];
      base64_key_out[idx2+2] = '=';
      base64_key_out[idx2+3] = '=';
      idx2 += 4;
    }

   else if(left_over == 2) 
    {
      base64_key_out[idx2] = charset[sha_key_in[idx] >> 2];
      base64_key_out[idx2+1] = charset[((sha_key_in[idx] & 0x03) << 4) + (sha_key_in[idx+1] >> 4)];
      base64_key_out[idx2+2] = charset[(sha_key_in[idx+1] & 0x0F) << 2];
      base64_key_out[idx2+3] = '=';
      idx2 += 4;
    }

   base64_key_out[idx2] = 0;
   return(idx2);
 }


////////////////////////////////////// error_log ////////////////////////////////////////////
void error_log(char *my_error) 
 { 
   time_t t;
   time(&t);
   FILE *f;
   f = fopen("/var/log/ErrorWsstd.log", "a"); 
   if(f == NULL) printf("Error open /var/log/ErrorWsstd.log.\n");
   fprintf(f, "%s", ctime( &t));
   fprintf(f, "Error %s\n\n", my_error);
   printf("Error %s Write to /var/log/ErrorWsstd.log.\n", my_error);
   fclose(f);
   exit(0);
 }


//////////////////////////////// warning_access_log ////////////////////////////////////////
void warning_access_log(char *war_ac) 
 {  
   count_warning_log++;
   if(count_warning_log > 100)
     {
       system("gzip -f /var/log/Access_warning.log");
       count_warning_log = 0;
       time_t t;
       time(&t);
       FILE *f;
       f = fopen("/var/log/Access_warning.log", "w"); 
       fprintf(f, "%s", ctime( &t));
       fprintf(f, "%s\n\n", war_ac);
       printf("_______________________________________\nWrite to /var/log/Access_warning.log...\n%s\n", war_ac);
       fclose(f);
     }

    else
     {
       time_t t;
       time(&t);
       FILE *f;
       f = fopen("/var/log/Access_warning.log", "a"); 
       fprintf(f, "%s", ctime( &t));
       fprintf(f, "%s\n\n", war_ac);
       printf("_______________________________________\nWrite to /var/log/Access_warning.log...\n%s\n", war_ac);
       fclose(f);
     }
 }


//////////////////////////////// read_in_file ////////////////////////////////////////
void read_in_file(char *name_file) 
 { 
   off_t offset = 0;
   memset(&stat_buf, 0, sizeof(stat_buf));    
   memset(fpfile, 0, ALLARRAY);
   snprintf(fpfile, (int)strlen(patch_to_dir) + (int)strlen(name_file) + 1, "%s%s", patch_to_dir, name_file);
   int file = open(fpfile, O_RDONLY);

   if(file < 0) 
    {
      if(close(client_fd) == -1) warning_access_log("open file close client_fd.");
      warning_access_log("Not File."); 
    }
 
   else
    {
      if(fstat(file, &stat_buf) != 0) error_log("fstat.");
      if(sendfile(client_fd, file, &offset, stat_buf.st_size) == -1) warning_access_log("sendfile."); 
      if(close(file) == -1) error_log("close file.");
      if(close(client_fd) == -1) warning_access_log("in function read_in_file() - close client_fd.");
      warning_access_log(buffer);
      printf("Trans %s\n\n", name_file);
    }
 }


///////////////////////////////////////// ws_func ///////////////////////////////////////////////////
void * ws_func(void *client_arg) 
 { 
   int client_fd = * (int *) client_arg;
   sem_post(&sem);
   warning_access_log("START_WS");
   printf("\nClient ID - %d\n", client_fd);
   char inbuf[SW_BUF] = {0,};
   char reciv_r[48] = {0,};

   while(1)
    {
      memset(inbuf, 0, SW_BUF); 
      long int rec_b = read(client_fd, inbuf, SW_BUF - 1); // ожидаем данные от клиента и читаем их по приходу

      memset(reciv_r, 0, 48);
      snprintf(reciv_r, 47, "%s%ld%s%d\n", "Ws_func recive ", rec_b, " bytes from clien ",  client_fd);
      warning_access_log(reciv_r); // пишем событие в лог

      if(rec_b == 0 || rec_b == -1) // если клиент отвалился или что-то нехорошо, тогда...
       {
         memset(reciv_r, 0, 48);
         snprintf(reciv_r, 47, "%s%ld%s%d\n", "Ws_func read return - ", rec_b, ", DIE clien - ",  client_fd);
         warning_access_log(reciv_r); // пишем ссобытие в лог
         if(close(client_fd) == -1) warning_access_log("Error close client in WS_1."); // закрываем соединение с клиентом
         pthread_exit(NULL);
       } 

      if(rec_b > 0)  // если чё то получили, то ...                    
       { 
         char masking_key[4] = {0,}; // сюда положим маску
         char opcode; // сюда тип фрейма
         unsigned char payload_len; // сюда длину сообщения (тела), то есть без служебных байтов либо цифры 126 или 127

         opcode = inbuf[0] & 0x0F;  
            printf("FIN: 0x%02X\n", inbuf[0] & 0x01);
            printf("RSV1: 0x%02X\n", inbuf[0] & 0x02);
            printf("RSV2: 0x%02X\n", inbuf[0] & 0x04);
            printf("RSV3: 0x%02X\n", inbuf[0] & 0x08);
            printf("Opcode: 0x%02X\n", inbuf[0] & 0x0F);
                      
         payload_len = inbuf[1] & 0x7F; 
            printf("Maska: 0x%02x\n", inbuf[1] & 0x80 ? 1:0);
            
         unsigned char payload[SW_BUF] = {0,};


         if(opcode == WS_CLOSING_FRAME) // от клиента получен код закрытия соединения
          {
            memset(reciv_r, 0, 48);
            snprintf(reciv_r, 47, "%s%d\n", "Ws_func recive opcod - 0x08, DIE clien - ",  client_fd);
            warning_access_log(reciv_r); // пишем ссобытие в лог
            if(close(client_fd) == -1) warning_access_log("Error close client in WS_2."); // закрываем соединение с клиентом
            pthread_exit(NULL); // убиваем поtok
          }

         else if(opcode == WS_PONG_FRAME) // от клиента получен PONG (маскированный)
          {
            masking_key[0] = inbuf[2];
            masking_key[1] = inbuf[3];
            masking_key[2] = inbuf[4];
            masking_key[3] = inbuf[5]; 

            unsigned int i = 6, pl = 0;
            for(; pl < payload_len; i++, pl++)
             {
               payload[pl] = inbuf[i]^masking_key[pl % 4]; 
             }
                     
            printf("Payload_len: %d\n", inbuf[1] & 0x7F);
            printf("\nRecive PONG and text \"%s\"\n", payload);
          }

         else if(opcode == WS_TEXT_FRAME && payload_len < 126) // от клиента получен текст
          {
            masking_key[0] = inbuf[2];
            masking_key[1] = inbuf[3];
            masking_key[2] = inbuf[4];
            masking_key[3] = inbuf[5]; 
            
            unsigned int i = 6, pl = 0;
            for(; pl < payload_len; i++, pl++)
             {
               payload[pl] = inbuf[i]^masking_key[pl % 4]; 
             }
                
            printf("Payload_len: %d\n", inbuf[1] & 0x7F);     
            printf("\nReciv TEXT_FRAME from %d client, payload: %s\n", client_fd, payload);


            if(payload[0] == 'p' && payload[1] == 'i' && payload[2] == 'n' && payload[3] == 'g') // от клиента получен текст "ping"  
             {
               printf("\nPING client - %d\n", client_fd); 

               char ping[] = {0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f}; // Ping - не маскированный, тело содержит слово Hello, это же слово вернётся с Понгом
               // char ping[] = {0x89, 0x05, 'H', 'e', 'l', 'l', 'o'}; // можно так
               
               if(send(client_fd, ping, 7, 0) == -1)
                {
                  warning_access_log("Error PING."); 
                  if(close(client_fd) == -1) warning_access_log("Error close client in WS_3."); // закрываем соединение с клиентом
                  pthread_exit(NULL); 
                }

             }

            else if(payload[0] == 'c' && payload[1] == 'l' && payload[2] == 'o' && payload[3] == 's' && payload[4] == 'e') // от клиента получен текст "close"
             {
               printf("\nClose client - %d\n", client_fd); 

               char close_client[] = {0x88, 0};
               
               if(send(client_fd, close_client, 2, 0) == -1)
                {
                  warning_access_log("Error CLOSE."); 
                  if(close(client_fd) == -1) warning_access_log("Error close client in WS_4."); // закрываем соединение с клиентом
                  pthread_exit(NULL); 
                }

             }

            else if(payload[0] == 'h' && payload[1] == 'i') // от клиента получен текст "hi"
             {
               char messag[] = "Hi client - ";
               int message_size = (int) strlen(messag);
               char out_data[128] = {0,};
               memcpy(out_data + 2, messag, message_size); // копируем сообщение в массив "out_data" начиная со второго байта (первые два байта для опкода и длины тела)
               char nom_client[5] = {0,};
               sprintf(nom_client, "%d", client_fd); // номер клиента
               int nom_client_size = (int) strlen(nom_client);
               memcpy(out_data + 2 + message_size, nom_client, nom_client_size); // копируем номер клиента в массив "out_data" следом за сообщением

               message_size += nom_client_size; // получаем длину тела сообщения

               out_data[0] = 0x81;
               out_data[1] = (char)message_size;

               printf("\nSize out Msg1: %d\n", message_size);
               
               if(send(client_fd, out_data, message_size + 2, 0) == -1) 
                {
                  warning_access_log("Error Hi."); 
                  if(close(client_fd) == -1) warning_access_log("Error close client in WS_5."); // закрываем соединение с клиентом
                  pthread_exit(NULL); 
                }
              }

            else if(payload[0] == 'H' && payload[1] == 'I') // от клиента получен текст "HI"
             {
               char messag[] = "istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru_istarik.ru";

               unsigned short message_size = strlen(messag);
               char out_data[SW_BUF] = {0,};
               memcpy(out_data + 4, messag, message_size); // копируем сообщение в массив "out_data" начиная со второго байта (первые два байта для опкода и длины тела)
           
               out_data[0] = 0x81; // == 10000001 == FIN1......opcod 1 (текст)
               out_data[1] = 126; // пишем цифру 126, котороя в двоичном виде == 01111110, соответственно маска 0, остальные 7 бит == 126
               out_data[3] = message_size & 0xFF; // собираем длину сообщения
               out_data[2] = (message_size >> 8) & 0xFF; // собираем длину сообщения

               printf("\nSize out Msg2: %d\n", message_size);
               
               if(send(client_fd, out_data, message_size + 4, 0) == -1) // отправка
                {
                  warning_access_log("Error Hi."); 
                  if(close(client_fd) == -1) warning_access_log("Error close client in WS_5."); // закрываем соединение с клиентом
                  pthread_exit(NULL); 
                }
              }

            else
             {
               char messag[] = "I do not know what to tell you bro... Link OK. But what do you want I do not understand, explain how a human being...";
               int message_size = (int) strlen(messag);
               char out_data[128] = {0,};
               memcpy(out_data + 2, messag, message_size); // копируем сообщение в массив "out_data" начиная со второго байта (первые два байта для опкода и длины тела)
       
               out_data[0] = 0x81;
               out_data[1] = (char)message_size;

               printf("\nSize out Msg3: %d\n", message_size);
               
               if(send(client_fd, out_data, message_size + 2, 0) == -1) 
                {
                  warning_access_log("Error Hi."); 
                  if(close(client_fd) == -1) warning_access_log("Error close client in WS_5."); // закрываем соединение с клиентом
                  pthread_exit(NULL); 
                }
              }

          } // END if < 126
   
         else if(opcode == WS_TEXT_FRAME && payload_len == 126) // от клиента получен текст
          {
            unsigned char len16[2] = {0,};
            unsigned int payload_len16 = 0;
            len16[0] = inbuf[2]; 
            len16[1] = inbuf[3]; 
            payload_len16 = (len16[0] << 8) | len16[1]; // собираем длину сообщения
            
            masking_key[0] = inbuf[4];
            masking_key[1] = inbuf[5];
            masking_key[2] = inbuf[6];
            masking_key[3] = inbuf[7]; 
            
            unsigned int i = 8, pl = 0;
            for(; pl < payload_len16; i++, pl++)
             {
               payload[pl] = inbuf[i]^masking_key[pl % 4]; 
             }
            
            printf("Payload_code: %d\n", inbuf[1] & 0x7F);  
            printf("Payload_len16: %u\n", payload_len16);       
            printf("\nReciv TEXT_FRAME from %d client, payload: %s\n", client_fd, payload);
          }

         /*else if(opcode == WS_TEXT_FRAME && payload_len == 127) // от клиента получен текст
          {
            // text > 65535
          }*/

         else
          {
            //
          } 

       } // END if(n > 0)  
   
    } // END while(1)

 } // END ws_func



int main(int argc, char *argv[])  
{  
  if(argc != 3) error_log("not argumets.");
     
  unsigned int PORTW = strtoul(argv[1], NULL, 0); // порт для web-сервера 80
  strncpy(patch_to_dir, argv[2], 63); // путь к файлу index.html
  warning_access_log("START");
  pthread_t ws_thread;
  

  /////////////////////////////////////////////////////////    WEB    ///////////////////////////////////////////////////////////////
  int one = 1;
  struct sockaddr_in svr_addr, cli_addr;
  socklen_t sin_len = sizeof(cli_addr);
 
  int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (sock < 0) error_log("not socket.");
 
  setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int));
 
  svr_addr.sin_family = AF_INET;
  svr_addr.sin_addr.s_addr = INADDR_ANY;
  svr_addr.sin_port = htons(PORTW);

  if(bind(sock, (struct sockaddr *) &svr_addr, sizeof(svr_addr)) == -1) 
   {
     close(sock);
     error_log("bind.");
   }
 
  if(listen(sock, 5) == -1) 
   {
     close(sock);
     error_log("listen.");
   }


  signal(SIGPIPE, SIG_IGN);

  char str_from_buf[FILESTR] = {0,};
  char result_file[FILESTR] = {0,};

      
  for(;;) 
   {
    client_fd = accept(sock, (struct sockaddr *) &cli_addr, &sin_len);
 
    if(client_fd == -1) continue;

    printf("Сonnected %s:%d client - %d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), client_fd);

    memset(buffer, 0, BUFSIZE);
    memset(str_from_buf, 0, FILESTR);
    memset(result_file, 0, FILESTR);
    char *p = NULL;

    if(read(client_fd, buffer, BUFSIZE - 1) == -1) warning_access_log("Error in main - read_client_fd.");

    int i = 0;
    for(; i < FILESTR; i++)
     {
       str_from_buf[i] = buffer[i];
       if(str_from_buf[i] == '\n') break;
       if(i > 31)
        {
          str_from_buf[i] = '\0';
          break;  
        }
     }


    if((strstr(str_from_buf, "GET / ")) != NULL) 
     {
       if(send(client_fd, response, (int)strlen(response), MSG_NOSIGNAL) == -1) warning_access_log("send response.");
       read_in_file("index.html");
     }


    /////////////////////////////////// WS /////////////////////////////////////////////////
    else if((strstr(str_from_buf, "GET /ws ")) != NULL) 
     {
       warning_access_log(buffer);
       if((p = strstr(buffer, "Sec-WebSocket-Key:")) != NULL)
        {                                                  
          char resultstr[64] = {0,};
          int i = 0, it = 0;
          for(i = 19; it < 24; i++, it++)
           {
             resultstr[it] = p[i];
           }

          strcat(resultstr, GUIDKey);

          printf("\n_____________|Key ot clienta__________|GUIDKey____________________________\n");
          printf("Result_stroka:%s\n", resultstr);


          ////////////////////////////sha1///////////////////////////////////////
          unsigned char temp[SHA_DIGEST_LENGTH] = {0,};
          SHA1(temp, resultstr, strlen(resultstr));


        ///////////////////// нужна только для того чтоб увидеть SHA1-хеш //////////////////////
          char buf[SHA_DIGEST_LENGTH*2] = {0,};                                               //
          for(i=0; i < SHA_DIGEST_LENGTH; i++)                                                // 
           {                                                                                  //
             sprintf((char*)&(buf[i*2]), "%02x", temp[i]);                                    //  
           }                                                                                  //
          printf("\nSHA1_hash:%s\n", buf);                                                    // 
        ////////////////////////////////////////////////////////////////////////////////////////


          ////////////////////////////Base64//////////////////////////////////// 
          unsigned char key_out[64] = {0,};
          base64_encode(temp, key_out, sizeof(temp));

          printf("\nKey_for_client:%s\n", key_out);

          sem_init(&sem, 0, 0);

          char resp[131] = {0,};
          snprintf(resp, 130, "%s%s%s", response_ws, key_out, "\r\n\r\n");
          if(send(client_fd, resp, sizeof(char) * strlen(resp), MSG_NOSIGNAL) == -1) warning_access_log("send response_ws.");
    

          //////////////////////////// START WS /////////////////////////////////
          if(pthread_create(&ws_thread, NULL, &ws_func, &client_fd) != 0) error_log("creating WS.");
          pthread_detach(ws_thread);
          sem_wait(&sem);
        }
     }

    else if((p = strstr(str_from_buf, ".png")) != NULL) 
     {
       int index = p - str_from_buf;
       int i = 0;
       int otbor = 0;
       for(; i < index + 3; i++)
        {
          result_file[i] = str_from_buf[i];
    
          if(result_file[i] == '/') 
           {
             otbor = i;
           }
        }

       memset(result_file, 0, FILESTR);
       strncpy(result_file, str_from_buf + otbor - 3, index -1); //  otbor + 1
       if(send(client_fd, response_img, (int)strlen(response_img), MSG_NOSIGNAL) == -1) warning_access_log("Error send response_img.");
       read_in_file(result_file);
     }

    else if((p = strstr(str_from_buf, ".css")) != NULL) 
     {
       int index = p - str_from_buf;
       int i = 0;
       int otbor = 0;
       for(; i < index + 3; i++)
        {
          result_file[i] = str_from_buf[i];
    
          if(result_file[i] == '/') 
           {
             otbor = i;
           }
        }

       memset(result_file, 0, FILESTR);
       strncpy(result_file, str_from_buf + otbor + 1, index -1);
       if(send(client_fd, response_css, (int)strlen(response_css), MSG_NOSIGNAL) == -1) warning_access_log("Error send response_css.");
       read_in_file(result_file);
     }


    else if((strstr(str_from_buf, "jquery.js")) != NULL) 
     {
       if(send(client_fd, response_js, (int)strlen(response_js), MSG_NOSIGNAL) == -1) warning_access_log("Error send response_js.");
       read_in_file("jquery.js");
     }

    else if((strstr(str_from_buf, "favicon.ico")) != NULL) 
     {
       if(send(client_fd, response_xicon, (int)strlen(response_xicon), MSG_NOSIGNAL) == -1) warning_access_log("Error send favicon.ico.");
       read_in_file("favicon.ico");
     }

    else 
     {
       if(send(client_fd, response_403, sizeof(response_403), MSG_NOSIGNAL) == -1) warning_access_log("Error send response_403.");
       if(close(client_fd) == -1) warning_access_log("Error close client_fd 403.");
       warning_access_log(buffer);
     }

   }// end for(;;)

} //END main

// gcc -Wall -Wextra -Werror websocketstd.c -o websocketstd -pthread 



myws.h

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/stat.h> 
#include <fcntl.h>    
#include <time.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/sendfile.h>

#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))

#if BYTE_ORDER == LITTLE_ENDIAN
#define blk0(i) (block->l[i] = (rol(block->l[i],24)&0xFF00FF00) \
    |(rol(block->l[i],8)&0x00FF00FF))

#elif BYTE_ORDER == BIG_ENDIAN
#define blk0(i) block->l[i]

#else
#error "Endianness not defined!"
#endif

#define blk(i) (block->l[i&15] = rol(block->l[(i+13)&15]^block->l[(i+8)&15] \
    ^block->l[(i+2)&15]^block->l[i&15],1))

#define R0(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30);
#define R1(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30);
#define R2(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30);
#define R3(v,w,x,y,z,i) z+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30);
#define R4(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30);

#define WS_TEXT_FRAME 0x01
#define WS_PING_FRAME 0x09
#define WS_PONG_FRAME 0x0A
#define WS_CLOSING_FRAME 0x08

char response[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=UTF-8\r\n\r\n";

char response_404[] = "HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/html; charset=UTF-8\r\n\r\n";

char response_img[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: image/png; charset=UTF-8\r\n\r\n";  

char response_xicon[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: image/x-icon; charset=UTF-8\r\n\r\n";

char response_css[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/css; charset=UTF-8\r\n\r\n";

char response_js[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/js; charset=UTF-8\r\n\r\n";

char response_ttf[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: font/ttf ; charset=UTF-8\r\n\r\n";

char response_text[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/text; charset=UTF-8\r\n\r\n";

char response_403[] = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=UTF-8\r\n\r\n"
"<!DOCTYPE html><html><head><title>403</title>"
"<style>body { background-color: #312f2f }"
"h1 { font-size:4cm; text-align: center; color: #666;}</style></head>"
"<body><h1>403</h1></body></html>\r\n";

char GUIDKey[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; //36

char response_ws[] = "HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: "; //97

unsigned char charset[]={"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"};



Makefile для Openwrt

include $(TOPDIR)/rules.mk

PKG_NAME:=websocketstd
PKG_VERSION:=1
PKG_RELEASE:=1

PKG_BUILD_DIR:= $(BUILD_DIR)/$(PKG_NAME)

include $(INCLUDE_DIR)/package.mk


define Package/websocketstd
	SECTION:=utils
	CATEGORY:=Utilities
	TITLE:=websocketstd - Websocketstd utility
	DEPENDS:=+libpthread 
 
endef

define Package/websocketstd/description
    websocketstd - Websocketstd utility
endef

define Build/Prepare
	mkdir -p $(PKG_BUILD_DIR)
	$(CP) ./src/* $(PKG_BUILD_DIR)/
endef

define Build/Compile
	$(TARGET_CC)  $(TARGET_CFLAGS) -c  -o $(PKG_BUILD_DIR)/websocketstd.o $(PKG_BUILD_DIR)/websocketstd.c 
	$(TARGET_CC) $(TARGET_LDFLAGS) -o  $(PKG_BUILD_DIR)/websocketstd $(PKG_BUILD_DIR)/websocketstd.o -pthread
endef

define Package/websocketstd/install
	$(INSTALL_DIR) $(1)/
	$(INSTALL_BIN) $(PKG_BUILD_DIR)/websocketstd $(1)/
endef

$(eval $(call BuildPackage,websocketstd))


make package/websocketstd/compile V=s


Кросс-компиляция описана здесь.


На роутере нужно установить пакет libpthread:

opkg update
opkg install libpthread




Готовый бинарник для , который запускается с двумя параметрами — порт (86) и путь к папке (в моём случае это /home/dima/c-websocket/) с программой и файлом index.html (в начале статьи). Туда же можно положить favicon`ky (чтоб браузеры типа Chromium не надрывались почём зря).

sudo chmod +x /home/dima/c-websocket/websocketstd
sudo /home/dima/c-websocket/websocketstd 86 /home/dima/c-websocket/




Всем спасибо. В следующих частях будет описание «своего» клиента, написание сервера/клиента для Андройд, а так же использование websocket`а в составе «умного дома».

П.С. Эта статья (в урезаном виде) публиковалась мной на geektimes.







  • 0
  • 2500

Комментарии (0)

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.