WebSocket подключиться к TIdHTTPServer, проблема рукопожатия
Я использую C++Builder 10.1 Berlin для написания простого серверного приложения WebSocket, которое прослушивает порт для некоторых команд, отправляемых из веб-браузера, например, Google Chrome.
В моей форме у меня есть TMemo, TButton и TIdHTTPServer, и у меня есть этот код:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
IdHTTPServer1->Bindings->DefaultPort = 55555;
IdHTTPServer1->Active = true;
}
void __fastcall TForm1::IdHTTPServer1Connect(TIdContext *AContext)
{
Memo1->Lines->Add(AContext->Binding->PeerIP);
Memo1->Lines->Add( AContext->Connection->IOHandler->ReadLn(enUTF8));
Memo1->Lines->Add( AContext->Data->ToString());
}
void __fastcall TForm5::IdHTTPServer1CommandOther(TIdContext *AContext, TIdHTTPRequestInfo *ARequestInfo,
TIdHTTPResponseInfo *AResponseInfo)
{
UnicodeString svk,sValue;
TIdHashSHA1 *FHash;
TMemoryStream *strmRequest;
FHash = new TIdHashSHA1;
strmRequest = new TMemoryStream;
strmRequest->Position = 0;
svk = ARequestInfo->RawHeaders->Values["Sec-WebSocket-Key"];
Memo1->Lines->Add("Get:"+svk);
AResponseInfo->ResponseNo = 101;
AResponseInfo->ResponseText = "Switching Protocols";
AResponseInfo->CloseConnection = False;
//Connection: Upgrade
AResponseInfo->Connection = "Upgrade";
//Upgrade: websocket
AResponseInfo->CustomHeaders->Values["Upgrade"] = "websocket";
sValue = svk + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
sValue = TIdEncoderMIME::EncodeBytes( FHash->HashString(sValue) );
AResponseInfo->CustomHeaders->Values["Sec-WebSocket-Accept"] = sValue;
AResponseInfo->ContentText = "Welcome here!";
AResponseInfo->WriteHeader();
UnicodeString URLstr = "http://"+ARequestInfo->Host+ARequestInfo->Document;
if (ARequestInfo->UnparsedParams != "") URLstr = URLstr+"?"+ARequestInfo->UnparsedParams;
Memo1->Lines->Add(URLstr);
Memo1->Lines->Add(ARequestInfo->Command );
Memo1->Lines->Add("--------");
Memo1->Lines->Add(ARequestInfo->RawHeaders->Text );
Memo1->Lines->Add(AContext->Data->ToString() );
}
Из Chrome я выполняю этот код Javascript:
var connection = new WebSocket('ws://localhost:55555');
connection.onopen = function () {
connection.send('Ping');
};
Но я получаю эту ошибку от Chrome:
VM77: 1 Соединение WebSocket с 'ws://localhost:55555/' прервано: один или несколько зарезервированных битов включены: зарезервировано1 = 1, зарезервировано2 = 0, зарезервировано3 = 0
Я ожидаю, что соединение WebSocket будет успешным, и тогда я смогу отправлять данные между веб-браузером и моим серверным приложением.
Может быть, кто-то уже знает, что не так, и может показать полный пример того, как этого добиться?
Вот что показывает Memo1 моего приложения:
192.168.0.25 GET / HTTP / 1.1 Получить:TnBN9qjOJiwka2eJe7mR0A== http:// HOST: -------- Соединение: Обновить Pragma: без кэширования Cache-Control: без кэширования Обновить: websocket Происхождение: http://bcbjournal.org Sec-WebSocket-Version: 13 Пользователь-агент: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, как Gecko) Chrome/57.0.2987.133 Safari/537.36 Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0,8 Sec-WebSocket-Key: TnBN9qjOJiwka2eJe7mR0A== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Вот что показывает Chrome:
Запрос ответа:
HTTP / 1.1 101 протоколы коммутации Подключение: Обновление Content-Type: text/html; кодировка =ISO-8859-1 Длина контента: 13 Дата: четверг, 08 июня 2017 г. 15:04:00 по Гринвичу Обновление: websocket Sec-WebSocket-Accept: 2coLmtu++HmyY8PRTNuaR320KPE=
Заголовки запроса
ПОЛУЧИТЕ ws://192.168.0.25:55555/ HTTP/1.1 Host: 192.168.0.25:55555 Подключение: Обновление Прагма: без кеша Cache-Control: без кеша Обновление: websocket Происхождение: http://bcbjournal.org Sec-WebSocket-версия: 13 Пользователь-агент: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, как Gecko) Chrome / 57.0.2987.133 Safari / 537.36 Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US, en; q = 0,8 Sec-WebSocket-Key: TnBN9qjOJiwka2eJe7mR0A == Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
1 ответ
Вы злоупотребляете TIdHTTPServer
Вы делаете две большие ошибки:
Ваш
OnConnect
Обработчик события читает начальную строку HTTP-запроса клиента (GET
линия). Он вообще не должен ничего читать от клиента, так как это мешаетTIdHTTPServer
Обработка протокола HTTP.После того, как обработчик событий читает строку запроса и завершает работу,
TIdHTTPServer
затем читает следующую строку (Host
заголовок) и интерпретирует это как строку запроса, поэтому:ARequestInfo->Command
свойство"HOST:"
вместо"GET"
,ARequestInfo->Host
,ARequestInfo->Document
,ARequestInfo->Version
,ARequestInfo->VersionMajor
,ARequestInfo->VersionMinor
свойства все не так.в конечном итоге вам придется использовать
OnCommandOther
событие, когда вы должны использоватьOnCommandGet
событие вместо
Вы получаете доступ к
TMemo
в вашемTIdHTTPServer
события без синхронизации с основным потоком пользовательского интерфейса.TIdHTTPServer
многопоточный компонент Его события запускаются в контексте рабочих потоков. Элементы управления пользовательского интерфейса VCL/FMX не являются поточно-ориентированными, поэтому необходимо правильно синхронизировать их с основным потоком пользовательского интерфейса.
Вы не правильно реализуете протокол WebSocket
Ваш сервер не проверяет все в рукопожатии, что протокол WebSocket требует проверки сервера (это хорошо для тестирования, но убедитесь, что вы делаете это для производства).
Но что более важно, TIdHTTPServer
не подходит для реализации WebSockets (это элемент TODO). Единственное, что касается протокола WebSocket, который включает HTTP, - это рукопожатие. После того, как рукопожатие закончено, все остальное - кадрирование WebSocket, а не HTTP. Чтобы справиться с этим в TIdHTTPServer
требует, чтобы вы реализовали весь сеанс WebSocket внутри OnCommandGet
событие, чтение и отправка всех фреймов WebSocket, предотвращая выход обработчика события, пока соединение не будет закрыто. Для такой логики я бы предложил использовать TIdTCPServer
вместо этого, и просто обработайте HTTP-рукопожатие вручную в начале его OnExecute
событие, а затем зациклите остаток события, обрабатывающего фреймы WebSocket.
Ваш OnCommandOther
Обработчик событий не выполняет никаких операций ввода-вывода WebSocket после завершения рукопожатия. Возвращает контроль TIdHTTPServer
, который затем попытается прочитать новый HTTP-запрос. Как только клиент отправляет фрейм WebSocket на сервер, TIdHTTPServer
не сможет обработать его, так как это не HTTP, и, скорее всего, отправит ответ HTTP обратно клиенту, что приведет к неверной интерпретации, что приведет к сбою сеанса WebSocket и закрытию соединения с сокетом.
С учетом сказанного, попробуйте что-то более похожее на это:
#include ...
#include <IdSync.hpp>
class TLogNotify : public TIdNotify
{
protected:
String FMsg;
void __fastcall DoNotify()
{
Form1->Memo1->Lines->Add(FMsg);
}
public:
__fastcall TLogNotify(const String &S) : TIdNotify(), FMsg(S) {}
};
__fastcall TForm1::TForm1(TComponent *Owner)
: TForm(Owner)
{
IdHTTPServer1->DefaultPort = 55555;
}
void __fastcall TForm1::Log(const String &S)
{
(new TLogNotify(S))->Notify();
}
void __fastcall TForm1::Button1Click(TObject *Sender)
{
IdHTTPServer1->Active = true;
}
void __fastcall TForm1::IdHTTPServer1Connect(TIdContext *AContext)
{
Log(_D("Connected: ") + AContext->Binding->PeerIP);
}
void __fastcall TForm1::IdHTTPServer1Disconnect(TIdContext *AContext)
{
Log(_D("Disconnected: ") + AContext->Binding->PeerIP);
}
void __fastcall TForm5::IdHTTPServer1CommandGet(TIdContext *AContext, TIdHTTPRequestInfo *ARequestInfo, TIdHTTPResponseInfo *AResponseInfo)
{
Log(ARequestInfo->RawHTTPCommand);
if (ARequestInfo->Document != _D("/"))
{
AResponseInfo->ResponseNo = 404;
return;
}
if ( !(ARequestInfo->IsVersionAtLeast(1, 1) &&
TextIsSame(ARequestInfo->RawHeaders->Values[_D("Upgrade")], _D("websocket")) &&
TextIsSame(ARequestInfo->Connection, _D("Upgrade")) ) )
{
AResponseInfo->ResponseNo = 426;
AResponseInfo->ResponseText = _D("upgrade required");
return;
}
String svk = ARequestInfo->RawHeaders->Values[_D("Sec-WebSocket-Key")];
if ( (ARequestInfo->RawHeaders->Values[_D("Sec-WebSocket-Version")] != _D("13")) ||
svk.IsEmpty() )
{
AResponseInfo->ResponseNo = 400;
return;
}
// validate Origin, Sec-WebSocket-Protocol, and Sec-WebSocket-Extensions as needed...
Log(_D("Get:") + svk);
AResponseInfo->ResponseNo = 101;
AResponseInfo->ResponseText = _D("Switching Protocols");
AResponseInfo->CloseConnection = false;
AResponseInfo->Connection = _D("Upgrade");
AResponseInfo->CustomHeaders->Values[_D("Upgrade")] = _D("websocket");
TIdHashSHA1 *FHash = new TIdHashSHA1;
try {
String sValue = svk + _D("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
sValue = TIdEncoderMIME::EncodeBytes( FHash->HashString(sValue) );
AResponseInfo->CustomHeaders->Values[_D("Sec-WebSocket-Accept")] = sValue;
}
__finally {
delete FHash;
}
AResponseInfo->WriteHeader();
String URLstr = _D("http://") + ARequestInfo->Host + ARequestInfo->Document;
if (!ARequestInfo->UnparsedParams.IsEmpty()) URLstr = URLstr + _D("?") + ARequestInfo->UnparsedParams;
Log(URLstr);
Log(_D("--------"));
Log(ARequestInfo->RawHeaders->Text);
// now send/receive WebSocket frames here as needed,
// using AContext->Connection->IOHandler directly...
}
При этом существует множество сторонних библиотек WebSocket. Вы должны использовать один из них вместо реализации WebSockets вручную. Некоторые библиотеки даже основаны на Indy.