问题
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
OnConnectevent handler is reading the client's initial HTTP request line (theGETline). 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,
TIdHTTPServerthen reads the next line (theHostheader) and interprets that as the request line instead, which is why:the
ARequestInfo->Commandproperty is"HOST:"instead of"GET".the
ARequestInfo->Host,ARequestInfo->Document,ARequestInfo->Version,ARequestInfo->VersionMajor,ARequestInfo->VersionMinorproperties are all wrong.you end up having to use the
OnCommandOtherevent when you should be using theOnCommandGetevent instead.
You are accessing the
TMemoin yourTIdHTTPServerevents without synchronizing with the main UI thread.TIdHTTPServeris 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