WebSocket-сервер лайт-версия

ac01ae69f1ee4f55870abe20635bd78a.png

Эта статья пригодится тем, кому захочется написать свой WebSocket-сервер на СИ.

Не знал какую придумать КДПВ…

Здравствуйте.

Увлекаясь постройкой «умного дома» мне захотелось попробовать заменить обычный web-сервер на websocket. Идея была реализована и протестирована, однако я пока что так и не понял, что удобнее (для меня), традиционный сервер или websocket.

Впрочем речь не об этом, а о том, что в сети достаточно много примеров реализованных на различных js-фреймворках, PHP и немного на С++, мне же хотелось сделать это на чистом СИ. Найденные примеры были слишком избыточны (для простого обмена небольшим количеством данных) и требовали сторонних библиотек, поэтому я решил написать что-то своё и заодно разобраться в технологии websocket.

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

Если Вы ещё не знакомы с тем, что такое WebSocket, то вот тут, тут и тут очень хорошо всё расписано, ну и конечно же RFC 6455. Я же покажу код простого WebSocket-сервера на СИ с пояснениями того, что и как там происходит.


Алгоритм таков: сервер работает как обычный web-сервер прослушивая какой-нибудь порт, например 80-ый, и обрабатывая стандартные запросы — файлы index.html, *.css, *.png, *.js, *.ttf. Когда сервер отдаст клиенту файл index.html содержащий специальный js-код, этот код, отработав, сделает запрос на соединение по протоколу »ws://…», сервер в свою очередь отреагирует на это «специальным» ответом и не будет закрывать соединение.
Таким образом Websocket соединение будет установлено.

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

index.html


    
        
        
        

        
        
    
        

WebSocket

Hi...

Call PING

Send


Выглядит она вот так:
b45394b9a45b4239a45ce3c1d24cfd75.png
Если надпись зелёная, то соединение установлено, а если красная, то нет.

Итак клиент получил index.html cо «специальной» строчкой…

var websocket = new WebSocket('ws://192.168.5.197:80/ws');


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

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

Так от Chromium:
e840156d8daf490d85409aa2a40a107e.png

Заголовки (браузер генерирует их сам) содержат:
Запрос на websocket-соединение.
Ключ для передачи его серверу.
Подпротоколы — они описаны по ссылкам данным в начале. Для данной статьи они не важны.
Прочая «дребедень».

Соединение


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

Сервер, получив «маячок» (ws) распарсивает весь пакет, вычленяет из него ключ — 9+4iSUx4slLlng0Xcv2zFw== (ключ всегда постоянной длины) и кладёт его в массив resultstr (см. ниже).
Теперь к ключу нужно прибавить специальную строку (GUID) определённую в спецификации RFC 6455:

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


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

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

strcat(resultstr, GUIDKey);

В итоге получаем вот такую строку:
9+4iSUx4slLlng0Xcv2zFw==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 *)resultstr, strlen (resultstr), temp); (она определена в заголовке #include ) и получаем обратно уже хеш-сумму этой строки…

////////////////////////////sha1///////////////////////////////////////
          unsigned char temp[SHA_DIGEST_LENGTH] = {0,};
          char buf[SHA_DIGEST_LENGTH*2] = {0,};

          SHA1((unsigned char *)resultstr, strlen(resultstr), temp);

          for(i=0; i < SHA_DIGEST_LENGTH; i++) 
           {
             sprintf((char*)&(buf[i*2]), "%02x", temp[i]);
           }

Далее с помощью функции base64_encode (temp, key_out, sizeof (temp)); переводим хеш-сумму в кодировку Base64 и получаем вот такой ключ — s3pPLMBiTxaQ9kYGzzhZRbK+xOo= (он тоже всегда постоянной длины)

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

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

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.");

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

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

Подведём итог первой части (соединения):
Браузер получает index.html со «специальным» js-кодом, отправляет серверу запрос на ws-соединение вместе с ключём-идентификатором.
Сервер «склеивает» этот ключ со специальной строкой, с помощью алгоритма SHA1 создаёт хеш-сумму склееной строки, переводит эту хеш-сумму в кодировку Base64 и отправляет клиенту вместе с заголовком.
Websocket соединение установлено.


Работа с Websocket


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


    0                   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
   +-+-+-+-+-------+-+-------------+-------------------------------+
   |F|R|R|R| опкод |М| Длина тела  |    Расширенная длина тела     |
   |I|S|S|S|(4бита)|А|   (7бит)    |            (1 байт)           |
   |N|V|V|V|       |С|             |(если длина тела==126 или 127) |
   | |1|2|3|       |К|             |                               |
   | | | | |       |А|             |                               |
   +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +

   +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
   |  Продолжение расширенной длины тела, если длина тела = 127    |
   + - - - - - - - - - - - - - - - +-------------------------------+

   + - - - - - - - - - - - - - - - +-------------------------------+
   |                               |  Ключ маски, если МАСКА = 1   |
   +-------------------------------+-------------------------------+

   +-------------------------------+-------------------------------+
   | Ключ маски (продолжение)      |       Данные фрейма ("тело")  |
   +-------------------------------- - - - - - - - - - - - - - - - +

   +-------------------------------- - - - - - - - - - - - - - - - +
   :                     Данные продолжаются ...                   :
   + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

   + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
   |                     Данные продолжаются ...                   |
   +---------------------------------------------------------------+

Каждый горизонтальный блок — это 32 бита.

Представленный сервер умеет обмениваться PING — PONGами (нужны для проверки соединения) и работать с текстовыми сообщениями длина которых не превышает 125-ти байт (большего мне не требовалось).

Фрейм с текстовым сообщением полученный от клиента выглядит так…

Первый байт:

      0 1 2 3 4 5 6 7 
     +-+-+-+-+-------+
     |F|R|R|R| opcode|
     |I|S|S|S|  (4)  |
     |N|V|V|V|       |
     | |1|2|3|       |
     +-+-+-+-+-------+

Второй байт:

      0 1 2 3 4 5 6 7 
     +---------------+
     |M| Payload len |
     |A|     (7)     |           
     |S|             |  
     |K|             |             
     +---------------+

Третий, четвёртый, пятый и шестой байты — это маска.
Следом идёт сообщение (ограниченное 125-ю байтами).

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

Вот что говорит браузер Chromium если ему отправить маскированное сообщение:
e25f1dc44c974cc2acda9625fbc7bae9.png

Однако если отправить маскированное сообщение браузеру FireFox, то ошибок не наблюдается.

Допустим браузер прислал сообщение «Hello geektimes», тогда на стороне сервера это будет выглядеть так:

cc21ae9bb3fd4961a290cbe7aa6cc8d5.png

Принято — 21 байт.
Opcode: 0×01 — говорит о том, что это текстовое сообщение.
Maska: 0×01 — маска есть.
Payload_len: 15 — длина сообщения 15 байт.

В программе при этом происходят следующие действия…

...
#define WS_TEXT_FRAME 0x01
...

void * ws_func(void *client_arg) 
 { 
   ...

   while(1)
    {
      ...

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

         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] & 0x03);
            printf("RSV3: 0x%02x\n", inbuf[0] & 0x04);
            printf("Opcode: 0x%02x\n", inbuf[0] & 0x0F);
                      
         payload_len = inbuf[1] & 0x7F; 
            printf("Maska: 0x%02x\n", inbuf[1] & 0x80 ? 1:0);
            printf("Payload_len: %d\n", inbuf[1] & 0x7F);

         masking_key[0] = inbuf[2]; 
         masking_key[1] = inbuf[3];
         masking_key[2] = inbuf[4];
         masking_key[3] = inbuf[5];

              
         char payload[128] = {0,}; // сюда положим сообщение

         ...

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

Если закрыть страничку, то браузер пошлёт опкод закрытия (0×08):

cee825eecc7941d298c122366c00378a.png

...
#define WS_CLOSING_FRAME 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) error_log("open file close client_fd ws 2."); // закрываем соединение с клиентом
            pthread_exit(NULL); // убиваем поtok
          }
...

Если сервер отправит браузеру PING

...
               printf("\nPING client - %d\n", client_fd); 
               char ping[] = {0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f}; // Ping - не маскированный, тело содержит слово Hello, эта же слово вернётся с Понгом
               
               if(send(client_fd, ping, 7, 0) == -1)
                {
                  warning_access_log("Error PING."); 
                  if(close(client_fd) == -1) error_log("open file close client_fd ws 3."); // закрываем соединение с клиентом
                  pthread_exit(NULL); 
                }
...


… тогда браузер в ответ отправит PONG, тем самым сообщив серверу о своём присутствии. Вместе с PINGом можно отправить любую фразу, браузер вернёт её вместе с PONGом.

da0000eecc2646bb90437377fd816ee1.png

Не обязательно посылать вместе с PINGом какую-то фразу, можно послать «пустой» PING…

...
               char ping[] = {0x89, 0x00}; 
               if(send(client_fd, ping, 2, 0) == -1)
                {
                  warning_access_log("Error PING."); 
                  if(close(client_fd) == -1) error_log("open file close client_fd ws 3."); // закрываем соединение с клиентом
                  pthread_exit(NULL); 
                }
...

Отправить PING может только сервер.

Чтобы закрыть соединение с клиентом, нужно отправить ему опкод — 0×88

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

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

               char hello[] = {0x81, 0x05, 'H', 'e', 'l', 'l', 'o'};  
           
               if(send(client_fd, hello, 7, 0) == -1)
                {
                  if(close(client_fd) == -1) error_log("send hello."); 
                  pthread_exit(NULL); 
                }

Ну и наконец код целиком…

Исходник
#include 
#include 
#include 
#include  
#include 
#include 
#include 
#include 
#include 
#include  
#include     
#include 
#include 
#include 
#include 
#include 
#include 

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"
"403"
""
"

403

\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+/"}; // opcode - тип фрейма #define WS_TEXT_FRAME 0x01 #define WS_PING_FRAME 0x09 #define WS_PONG_FRAME 0x0A #define WS_CLOSING_FRAME 0x08 #define BUFSIZE 1024 #define FILESTR 32 #define ALLARRAY 64 char patch_to_dir[ALLARRAY] = {0,}; char fpfile[ALLARRAY] = {0,}; char buffer[BUFSIZE] = {0,}; int client_fd; int count_warning_log =0; struct stat stat_buf; sem_t sem; 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); } 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); } 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); } } 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) error_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[132] = {0,}; char reciv_r[48] = {0,}; while(1) { memset(inbuf, 0, 132); int rec_b = read(client_fd, inbuf, 131); // ожидаем данные от клиента и читаем их по приходу memset(reciv_r, 0, 48); snprintf(reciv_r, 39, "%s%d%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%d%s%d\n", "Ws_func read return - ", rec_b, ", DIE clien - ", client_fd); warning_access_log(reciv_r); // пишем ссобытие в лог if(close(client_fd) == -1) error_log("open file close client_fd ws 1."); // закрываем соединение с клиентом pthread_exit(NULL); } if(rec_b > 0) // если что-то получили, то ... { char masking_key[4] = {0,}; // сюда положим маску char opcode; // сюда тип фрейма int payload_len; // сюда длину сообщения (тела), то есть без служебных байтов 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] & 0x03); printf("RSV3: 0x%02x\n", inbuf[0] & 0x04); printf("Opcode: 0x%02x\n", inbuf[0] & 0x0F); payload_len = inbuf[1] & 0x7F; printf("Maska: 0x%02x\n", inbuf[1] & 0x80 ? 1:0); printf("Payload_len: %d\n", inbuf[1] & 0x7F); masking_key[0] = inbuf[2]; masking_key[1] = inbuf[3]; masking_key[2] = inbuf[4]; masking_key[3] = inbuf[5]; char payload[128] = {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) error_log("open file close client_fd ws 2."); // закрываем соединение с клиентом pthread_exit(NULL); // убиваем поtok } if(opcode == WS_PONG_FRAME) // от клиента получен PONG (маскированный) { int i = 6, pl = 0; for(; pl < payload_len; i++, pl++) { payload[pl] = inbuf[i]^masking_key[pl % 4]; } printf("\nRecive PONG and text \"%s\"\n", payload); } if(opcode == WS_TEXT_FRAME) // от клиента получен текст { int i = 6, pl = 0; for(; pl < payload_len; i++, pl++) { payload[pl] = inbuf[i]^masking_key[pl % 4]; } 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, эта же слово вернётся с Понгом if(send(client_fd, ping, 7, 0) == -1) { warning_access_log("Error PING."); if(close(client_fd) == -1) error_log("open file close client_fd ws 3."); // закрываем соединение с клиентом pthread_exit(NULL); } } 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) error_log("open file close client_fd ws 4."); // закрываем соединение с клиентом pthread_exit(NULL); } } 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) error_log("open file close client_fd ws 5."); // закрываем соединение с клиентом pthread_exit(NULL); } } } } } } 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; 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); ////////////////////////////sha1/////////////////////////////////////// unsigned char temp[SHA_DIGEST_LENGTH] = {0,}; char buf[SHA_DIGEST_LENGTH*2] = {0,}; SHA1((unsigned char *)resultstr, strlen(resultstr), temp); for(i=0; i < SHA_DIGEST_LENGTH; i++) { sprintf((char*)&(buf[i*2]), "%02x", temp[i]); } ////////////////////////////Base64//////////////////////////////////// unsigned char key_out[64] = {0,}; base64_encode(temp, key_out, sizeof(temp)); 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 main // gcc -Wall -Wextra -Werror websocket.c -o websocket -pthread -lcrypto // sudo ./websocket 80 /home/dima/c-websocket/

И бинарник 00946ffa1e4249f6a2e782585eac1d64.png, который запускается с двумя параметрами — порт (80) и путь к папке с программой и файлом index.html (в начале статьи). Туда же можно положить favicon`ky.

sudo chmod +x ./websocket
sudo ./websocket 80 /home/dima/c-websocket/


Желающие могут скомпилить прогу для роутера или ещё чего-нибудь.

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

© Geektimes