WebSocket 连接到 TIdHTTPServer,握手问题
WebSocket connect to TIdHTTPServer, handshake issue
我正在使用 C++Builder 10.1 Berlin 编写一个简单的 WebSocket 服务器应用程序,它在端口上侦听从 Web 浏览器发送的一些命令,例如 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 connection to 'ws://localhost:55555/' failed: One or more reserved bits are on: reserved1 = 1, reserved2 = 0, reserved3 = 0
我希望 WebSocket 连接成功,然后我可以在网络浏览器和我的服务器应用程序之间发送数据。
也许有人已经知道出了什么问题并且可以展示如何实现这一点的完整示例?
这是我的应用程序的 Memo1 显示的内容:
192.168.0.25
GET / HTTP/1.1
Get:TnBN9qjOJiwka2eJe7mR0A==
http://
HOST:
--------
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://bcbjournal.org
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like 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 Switching Protocols
Connection: Upgrade
Content-Type: text/html; charset=ISO-8859-1
Content-Length: 13
Date: Thu, 08 Jun 2017 15:04:00 GMT
Upgrade: websocket
Sec-WebSocket-Accept: 2coLmtu++HmyY8PRTNuaR320KPE=
请求Headers
GET ws://192.168.0.25:55555/ HTTP/1.1
Host: 192.168.0.25:55555
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://bcbjournal.org
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like 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
您在滥用 TIdHTTPServer
你犯了两个大错误:
您的 OnConnect
事件处理程序正在读取客户端的初始 HTTP 请求行(GET
行)。它根本不应从客户端读取任何内容,因为这样做会干扰 TIdHTTPServer
对 HTTP 协议的处理。
事件处理程序读取请求行并退出后,TIdHTTPServer
然后读取下一行(Host
header)并将其解释为请求行,即为什么:
ARequestInfo->Command
属性 是 "HOST:"
而不是 "GET"
。
ARequestInfo->Host
、ARequestInfo->Document
、ARequestInfo->Version
、ARequestInfo->VersionMajor
、ARequestInfo->VersionMinor
属性都是错误的。
当您应该使用 OnCommandGet
事件时,您最终不得不使用 OnCommandOther
事件。
您正在访问 TIdHTTPServer
事件中的 TMemo
,但未与主 UI 线程同步。 TIdHTTPServer
是一个 multi-threaded 组件。它的事件在工作线程的上下文中被触发。 VCL/FMX UI 控件不是 thread-safe,因此您必须与主 UI 线程正确同步。
您没有正确实现 WebSocket 协议
您的服务器没有验证 WebSocket 协议要求服务器验证的握手中的所有内容(这对于测试来说很好,但请确保您在生产环境中这样做)。
但更重要的是,TIdHTTPServer
不是 well-suited 用于实现 WebSockets(即 TODO item)。 WebSocket 协议唯一涉及 HTTP 的就是握手。握手完成后,其他一切都是 WebSocket 框架,而不是 HTTP。要在 TIdHTTPServer
中处理该问题,需要您在 OnCommandGet
事件中实现 entire WebSocket session,读取和发送所有 WebSocket 帧,防止事件处理程序退出,直到连接关闭。对于这种逻辑,我建议直接使用 TIdTCPServer
,并在其 OnExecute
事件开始时手动处理 HTTP 握手,然后循环处理 WebSocket 帧的其余事件。
握手完成后,您的 OnCommandOther
事件处理程序当前未执行任何 WebSocket I/O。它正在将控制权返回给 TIdHTTPServer
,后者将尝试读取新的 HTTP 请求。一旦客户端向服务器发送 WebSocket 帧,TIdHTTPServer
将无法处理它,因为它不是 HTTP,并且可能会将 HTTP 响应发送回客户端,这将被误解,导致客户端使 WebSocket 失败 session 并关闭套接字连接。
话虽如此,请尝试更类似的方法:
#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...
}
也就是说,有很多第 3 方 WebSocket 库可用。您应该使用其中之一,而不是手动实现 WebSockets。有些库甚至建立在 Indy 之上。
我正在使用 C++Builder 10.1 Berlin 编写一个简单的 WebSocket 服务器应用程序,它在端口上侦听从 Web 浏览器发送的一些命令,例如 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 connection to 'ws://localhost:55555/' failed: One or more reserved bits are on: reserved1 = 1, reserved2 = 0, reserved3 = 0
我希望 WebSocket 连接成功,然后我可以在网络浏览器和我的服务器应用程序之间发送数据。
也许有人已经知道出了什么问题并且可以展示如何实现这一点的完整示例?
这是我的应用程序的 Memo1 显示的内容:
192.168.0.25 GET / HTTP/1.1 Get:TnBN9qjOJiwka2eJe7mR0A== http:// HOST: -------- Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: http://bcbjournal.org Sec-WebSocket-Version: 13 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like 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 Switching Protocols Connection: Upgrade Content-Type: text/html; charset=ISO-8859-1 Content-Length: 13 Date: Thu, 08 Jun 2017 15:04:00 GMT Upgrade: websocket Sec-WebSocket-Accept: 2coLmtu++HmyY8PRTNuaR320KPE=
请求Headers
GET ws://192.168.0.25:55555/ HTTP/1.1 Host: 192.168.0.25:55555 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: http://bcbjournal.org Sec-WebSocket-Version: 13 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like 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
您在滥用 TIdHTTPServer
你犯了两个大错误:
您的
OnConnect
事件处理程序正在读取客户端的初始 HTTP 请求行(GET
行)。它根本不应从客户端读取任何内容,因为这样做会干扰TIdHTTPServer
对 HTTP 协议的处理。事件处理程序读取请求行并退出后,
TIdHTTPServer
然后读取下一行(Host
header)并将其解释为请求行,即为什么:ARequestInfo->Command
属性 是"HOST:"
而不是"GET"
。ARequestInfo->Host
、ARequestInfo->Document
、ARequestInfo->Version
、ARequestInfo->VersionMajor
、ARequestInfo->VersionMinor
属性都是错误的。当您应该使用
OnCommandGet
事件时,您最终不得不使用OnCommandOther
事件。
您正在访问
TIdHTTPServer
事件中的TMemo
,但未与主 UI 线程同步。TIdHTTPServer
是一个 multi-threaded 组件。它的事件在工作线程的上下文中被触发。 VCL/FMX UI 控件不是 thread-safe,因此您必须与主 UI 线程正确同步。
您没有正确实现 WebSocket 协议
您的服务器没有验证 WebSocket 协议要求服务器验证的握手中的所有内容(这对于测试来说很好,但请确保您在生产环境中这样做)。
但更重要的是,TIdHTTPServer
不是 well-suited 用于实现 WebSockets(即 TODO item)。 WebSocket 协议唯一涉及 HTTP 的就是握手。握手完成后,其他一切都是 WebSocket 框架,而不是 HTTP。要在 TIdHTTPServer
中处理该问题,需要您在 OnCommandGet
事件中实现 entire WebSocket session,读取和发送所有 WebSocket 帧,防止事件处理程序退出,直到连接关闭。对于这种逻辑,我建议直接使用 TIdTCPServer
,并在其 OnExecute
事件开始时手动处理 HTTP 握手,然后循环处理 WebSocket 帧的其余事件。
握手完成后,您的 OnCommandOther
事件处理程序当前未执行任何 WebSocket I/O。它正在将控制权返回给 TIdHTTPServer
,后者将尝试读取新的 HTTP 请求。一旦客户端向服务器发送 WebSocket 帧,TIdHTTPServer
将无法处理它,因为它不是 HTTP,并且可能会将 HTTP 响应发送回客户端,这将被误解,导致客户端使 WebSocket 失败 session 并关闭套接字连接。
话虽如此,请尝试更类似的方法:
#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...
}
也就是说,有很多第 3 方 WebSocket 库可用。您应该使用其中之一,而不是手动实现 WebSockets。有些库甚至建立在 Indy 之上。