WebSocket连接到TIdHTTPServer,握手问题

3

我正在使用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连接'ws://localhost:55555/'失败:一个或多个保留位处于打开状态:reserved1 = 1,reserved2 = 0,reserved3 = 0。
我期望WebSocket连接成功,然后我就可以在Web浏览器和我的服务器应用程序之间发送数据。
也许有人已经知道错在哪里,并且可以展示如何实现的完整示例?
这是我的应用程序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 切换协议 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= 请求头信息: 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
顺便提一下,在这些事件中不要访问VCL组件。它们在工作线程的上下文中运行。 - Victoria
1个回答

3

您正在错误使用TIdHTTPServer

您犯了两个大错误:

  1. 您的OnConnect事件处理程序正在读取客户端的初始HTTP请求行(即GET行)。它根本不应该从客户端读取任何内容,因为这样会干扰TIdHTTPServer处理HTTP协议。

    在事件处理程序读取请求行并退出后,TIdHTTPServer然后读取下一行(即Host头),并将其解释为请求行,这就是为什么:

    • ARequestInfo->Command属性为"HOST:"而不是"GET"

    • ARequestInfo->HostARequestInfo->DocumentARequestInfo->VersionARequestInfo->VersionMajorARequestInfo->VersionMinor属性都是错误的。

    • 您最终需要使用OnCommandOther事件,而应该使用OnCommandGet事件。

  2. 您在TIdHTTPServer事件中访问TMemo而没有与主UI线程同步。 TIdHTTPServer是一个多线程组件。其事件在工作线程的上下文中触发。VCL / FMX UI控件不是线程安全的,因此您必须正确地与主UI线程同步。

您没有正确实现WebSocket协议

您的服务器没有验证WebSocket协议要求服务器验证的握手中的所有内容(这对于测试来说是可以的,但请确保在生产环境中执行此操作)。

但更重要的是,TIdHTTPServer不适合实现WebSockets(这是一个待办事项)。 WebSocket协议中唯一涉及HTTP的是握手。 握手完成后,其他所有内容都是WebSocket帧,而不是HTTP。 在TIdHTTPServer中处理它需要您在OnCommandGet事件中完全实现WebSocket会话,读取和发送所有WebSocket帧,防止事件处理程序退出,直到连接关闭为止。 对于这种逻辑,我建议直接使用TIdTCPServer,并在其OnExecute事件的开头手动处理HTTP握手,然后循环处理WebSocket帧的其余部分。

您的OnCommandOther事件处理程序在握手完成后目前没有执行任何WebSocket I/O操作。它将控制权返回给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构建。


谢谢你的好回答,但是你能否在回答中添加一些发送/接收WebSocket帧的示例,因为我尝试了这个但不成功:UTF8String sendStr; sendStr = "send: this!";AContext->Connection->IOHandler->WriteLn(sendStr); AContext->Connection->Disconnect();在Chrome网页中,我期望在以下代码中接收消息: connection.onmessage = function (event) { console.log(event.data); } 并用connection.send('Ping')发送消息。我该如何在IdHTTPServer中接收消息并回复给Chrome浏览器的WebSocket客户端? - Jigberto
@Jigberto 我建议你阅读WebSocket协议规范RFC 6455,特别是第5节数据帧。WebSocket数据包有结构,而你根本没有遵循这个结构,甚至不接近。除了交换字符串之外,还涉及到数据包头、数据掩码等等。你必须遵循整个协议。 - Remy Lebeau
嘿,Remy Lebeau,感谢您的帮助回答!我发现使用DataSnap技术来实现我所需的功能会更容易。我还开了一个新问题,请查看,谢谢。 https://stackoverflow.com/questions/44571004/access-remote-database-using-datasnap-technology-in-c-builder-10-1-berlin - Jigberto
@Jigberto 我无法回答那个问题,因为我从未使用过 DataSnap。 - Remy Lebeau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接