Эта статья пригодится тем, кому захочется написать свой 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-кодом…
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<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.197:80/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('');
});
$('.knp1').click(function(e) {
e.preventDefault();
websocket.send("hi");
});
$('.knp2').click(function(e) {
e.preventDefault();
websocket.send("ping");
});
});
</script>
</head>
<body>
<h1>WebSocket</h1>
<div class='knopka knp1'>Hi...</div><br>
<div class='knopka knp2'>Call PING</div><br>
<div class='knopka knp0'>Send</div> <input type="text" />
<v></v>
</body>
</html>
Выглядит она вот так:
Если надпись зелёная, то соединение установлено, а если красная, то нет.
Итак клиент получил index.html cо «специальной» строчкой…
var websocket = new WebSocket('ws://192.168.5.197:80/ws');
Это и есть запрос на соединение по протоколу websocket. В скобках указывается протокол, адрес сервера (если порт 80-ый, то можно не писать), буквы после слеша могут быть любыми, а можно и вовсе без них, это просто «маячок» для сервера (см.ниже), говорящий ему, что нужно распарсить весь пакет и поискать там заголовок — Sec-WebSocket-Key: с рандомным ключём — 9+4iSUx4slLlng0Xcv2zFw== в кодировке Base64.
От разных браузеров запросы выглядят по разному, но в целом суть одна и та же, вот так от FireFox:
Так от Chromium:
Заголовки (браузер генерирует их сам) содержат:
— Запрос на 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 <openssl/sha.h>) и получаем обратно уже хеш-сумму этой строки…
////////////////////////////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 если ему отправить маскированное сообщение:
Однако если отправить маскированное сообщение браузеру FireFox, то ошибок не наблюдается.
Допустим браузер прислал сообщение «Hello geektimes», тогда на стороне сервера это будет выглядеть так:
Принято — 21 байт.
Opcode: 0x01 — говорит о том, что это текстовое сообщение.
Maska: 0x01 — маска есть.
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);
...
Если закрыть страничку, то браузер пошлёт опкод закрытия (0x08):
...
#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ом.
Не обязательно посылать вместе с 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 может только сервер.
Чтобы закрыть соединение с клиентом, нужно отправить ему опкод — 0x88
...
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 <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>
#include <openssl/sha.h>
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+/"};
// 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/
И бинарник , который запускается с двумя параметрами — порт (80) и путь к папке с программой и файлом index.html (в начале статьи). Туда же можно положить favicon`ky (чтоб браузеры типа Chromium не надрывались почём зря).
sudo chmod +x ./websocket
sudo ./websocket 80 /home/dima/c-websocket/
При испытаниях желательно пользоваться браузером FireFox.
Желающие могут скомпилить прогу для роутера или ещё чего-нибудь.
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 +libopenssl
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 -lcrypto
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
Дополнение к статье:
Ниже представлен код не требующий подключения библиотеки openssl.
Это будет полезно тем, кто собирается использовать программу на роутере без флешки, а установка openssl требует много места.
Установить нужно только libpthread.
opkg update
opkg install libpthread
#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);
// 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
#define SHA_DIGEST_LENGTH 20
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+/"};
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) 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); // пишем ссобытие в лог
close(client_fd); // закрываем соединение с клиентом
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); // пишем ссобытие в лог
close(client_fd); // закрываем соединение с клиентом
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.");
close(client_fd);
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.");
close(client_fd);
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.");
close(client_fd);
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;
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 main
// gcc -Wall -Wextra -Werror websocket.c -o websocket -pthread
// sudo ./websocket 85 /home/dima/
#############################################################
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
// /websocketstd/websocketstd 86 /websocketstd/
П.С. Код не «причёсан», содержит много ненужной отладочной информации и всяких логов. Так же есть некоторые неправильности (например потоки обращаются к одной и той же функции).
Комментарии (17)
savostin
04.12.2016 21:32+1Чем libwebsockets не подошел?
Radmin
05.12.2016 00:12+1Увлекаясь постройкой «умного дома»...
Я не нашёл в статье, а каков реальный сценарий использования такого сервера в IoT в вашем конкретном случае?
nochkin
05.12.2016 03:43В статье автор упомянул про передачу небольшого количества данных. Видимо, это и есть один из сценариев.
stDistarik
05.12.2016 12:22В примере нет кода отвечающего за обмен данными с МК, который управляет «умным домом». Описан только WS-сервер.
Сценарий таков: МК переодически (раз в 300мс) отправляет статус (инфа от датчиков и т.д.) серверу, сервер смотрит есть ли какие-то изменения, если изменения есть, то отправляет клиентам новые данные, если нет, то нет.
Если клиент присылает запрос на какие-то действия, то сервер пересылает их в МК, МК что-то делает и возвращает статус серверу, сервер рассылает его клиентам.Radmin
05.12.2016 13:15Я правильно понимаю, что постоянное соединение ваш ws-сервер держит только с клиентами (назовём их frontend)? А как обрабатываются поступающие данные с МК? С ним тоже соединение не закрывается, или просто прослушивается порт и реагирует на входящие соединения?
stDistarik
05.12.2016 16:07С МК соединение висит в отдельном потоке и принимает/отправляет данные постоянно, то есть постоянно открыт /dev/ttyUSB*.
Просто я убрал этот функционал из данного примера ибо хотел описать только ws-сервер.
tmin10
Извините, но заглавная картинка прямо выжигает глаза, красно-белое в таких размерах это чересчур…
stDistarik
Сейчас исправим.
tmin10
Спасибо, так намного лучше.