【问题标题】:WebSocket connect to TIdHTTPServer, handshake issueWebSocket 连接到 TIdHTTPServer,握手问题
【发布时间】:2017-06-08 14:50:21
【问题描述】:

我正在使用 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 获取/HTTP/1.1 获取:TnBN9qjOJiwka2eJe7mR0A== http:// 主持人: -------- 连接:升级 Pragma:无缓存 缓存控制:无缓存 升级:websocket 来源:http://bcbjournal.org Sec-WebSocket-版本:13 用户代理:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 接受编码:gzip、deflate、sdch 接受语言:en-US,en;q=0.8 Sec-WebSocket-Key:TnBN9qjOJiwka2eJe7mR0A== Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits

这是 Chrome 显示的内容:

响应请求:

HTTP/1.1 101 交换协议 连接:升级 内容类型:文本/html;字符集=ISO-8859-1 内容长度:13 日期:格林威治标准时间 2017 年 6 月 8 日星期四 15:04:00 升级:websocket Sec-WebSocket-Accept: 2coLmtu++HmyY8PRTNuaR320KPE=

请求标头

获取 ws://192.168.0.25:55555/HTTP/1.1 主机:192.168.0.25:55555 连接:升级 Pragma:无缓存 缓存控制:无缓存 升级:websocket 来源:http://bcbjournal.org Sec-WebSocket-版本:13 用户代理:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36 接受编码:gzip、deflate、sdch 接受语言:en-US,en;q=0.8 Sec-WebSocket-Key:TnBN9qjOJiwka2eJe7mR0A== Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits

【问题讨论】:

  • 旁注,不要从这些事件中访问 VCL 组件。它们在工作线程的上下文中运行。

标签: javascript html websocket c++builder c++builder-10.1-berlin


【解决方案1】:

你在滥用TIdHTTPServer

你犯了两个大错误:

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

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

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

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

    • 当您应该使用 OnCommandGet 事件时,您最终不得不使用 OnCommandOther 事件。

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

你没有正确实现 WebSocket 协议

您的服务器没有在握手中验证 WebSocket 协议需要服务器验证的所有内容(这对于测试来说很好,但请确保您在生产环境中这样做)。

但更重要的是,TIdHTTPServer 并不适合实现 WebSockets(即TODO item)。 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...
}

话虽如此,有很多 3rd 方 WebSocket 库可用。您应该使用其中之一,而不是手动实现 WebSocket。一些库甚至建立在 Indy 之上。

【讨论】:

  • 谢谢你的好答案,但你能否在你的答案中添加一些如何发送/接收 WebSocket 帧的示例,因为我尝试过没有成功:` UTF8String sendStr; sendStr = "发送:这个!"; AContext->Connection->IOHandler->WriteLn(sendStr); AContext->Connection->Disconnect();在 chrome 网页中,我希望在 connection.onmessage = function (event) { console.log(event.data); 中收到消息并使用 connection.send('Ping'); 发送如何在 IdHTTPServer 中获取消息并回复 chrome 浏览器 websocket 客户端?`
  • @Jigberto 我建议你阅读 WebSocket 协议规范,RFC 6455,尤其是Section 5 Data Framing。 WebSocket 数据包具有它们的结构,而您根本没有遵循该结构,甚至没有关闭。除了交换字符串之外,还涉及更多内容。有数据包头、数据屏蔽等。你必须遵循的整个协议。
  • 嗨 Remy Lebeau,感谢您的帮助!我发现使用 DataSnap 技术来完成我需要的东西会更容易。我打开了新问题,请检查,谢谢stackoverflow.com/questions/44571004/…
  • @Jigberto 我无法回答这个问题,因为我从未使用过 DataSnap。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-08
  • 2017-05-09
  • 2019-06-11
  • 2017-02-12
  • 1970-01-01
相关资源
最近更新 更多