问题
I'm using C++Builder 10.1 Berlin to write a simple WebSocket server application, which listens on a port for some commands sent from a web browser, like Google Chrome.
On my Form, I have a TMemo, TButton and TIdHTTPServer, and I have this code:
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() );
}
From Chrome, I execute this Javascript code:
var connection = new WebSocket('ws://localhost:55555');
connection.onopen = function () {
connection.send('Ping');
};
But I get this error from Chrome:
VM77:1 WebSocket connection to 'ws://localhost:55555/' failed: One or more reserved bits are on: reserved1 = 1, reserved2 = 0, reserved3 = 0
I expect the WebSocket connection to be successful, and then I can send data between the web browser and my server application.
Maybe somebody already knows what is wrong and can show a full example of how to achieve this?
Here is what my application's Memo1 shows:
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
Here is what Chrome shows:
Response Request:
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=
Request 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
回答1:
You are misusing TIdHTTPServer
You are making two big mistakes:
Your
OnConnect
event handler is reading the client's initial HTTP request line (theGET
line). It should not be reading anything from the client at all, as doing so interfers withTIdHTTPServer
's handling of the HTTP protocol.After the event handler reads the request line and exits,
TIdHTTPServer
then reads the next line (theHost
header) and interprets that as the request line instead, which is why:the
ARequestInfo->Command
property is"HOST:"
instead of"GET"
.the
ARequestInfo->Host
,ARequestInfo->Document
,ARequestInfo->Version
,ARequestInfo->VersionMajor
,ARequestInfo->VersionMinor
properties are all wrong.you end up having to use the
OnCommandOther
event when you should be using theOnCommandGet
event instead.
You are accessing the
TMemo
in yourTIdHTTPServer
events without synchronizing with the main UI thread.TIdHTTPServer
is a multi-threaded component. Its events are fired in the context of worker threads. VCL/FMX UI controls are not thread-safe, so you must synchronize properly with the main UI thread.
You are not implementing the WebSocket protocol correctly
Your server is not validating everything in the handshake that the WebSocket protocol requires a server to validate (which is fine for testing, but make sure you do it for production).
But more importantly, TIdHTTPServer
is not well-suited for implementing WebSockets (that is a TODO item). The only thing about the WebSocket protocol that involves HTTP is the handshake. After the handshake is finished, everything else is WebSocket framing, not HTTP. To handle that in TIdHTTPServer
requires you to implement the entire WebSocket session inside of the OnCommandGet
event, reading and sending all WebSocket frames, preventing the event handler from exiting, until the connection is closed. For that kind of logic, I would suggest using TIdTCPServer
directly instead, and just handle the HTTP handshake manually at the beginning of its OnExecute
event, and then loop the rest of the event handling the WebSocket frames.
Your OnCommandOther
event handler is not currently performing any WebSocket I/O after the handshake is finished. It is returning control to TIdHTTPServer
, which will then attempt to read a new HTTP request. As soon as the client sends a WebSocket frame to the server, TIdHTTPServer
will fail to process it since it is not HTTP, and will likely send an HTTP response back to the client, which will get misinterpreted, causing the client to fail the WebSocket session and close the socket connection.
With that said, try something more like this instead:
#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...
}
That being said, there are plenty of 3rd party WebSocket libraries available. You should use one of them instead of implementing WebSockets manually. Some libraries even build on top of Indy.
来源:https://stackoverflow.com/questions/44438988/websocket-connect-to-tidhttpserver-handshake-issue