65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Fiddler 调试 / 检查 WebSocket 流量

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (14投票s)

2014 年 1 月 31 日

CPOL

3分钟阅读

viewsIcon

193493

downloadIcon

1543

使用 Fiddler 调试 / 检查 WebSocket 流量

简介     

最近我使用 SignalR 写了一个项目,它支持 HTML 5 WebSocket。 但是,我找不到好的工具来调试或检查 WebSocket 流量。 我知道 Chrome 和 Fiddler 都支持检查 WebSocket 流量,但它们非常基础。 如果您的流量很大或每个帧都很大,那么使用它们进行调试会变得非常困难。 (请在“背景”部分查看更多详细信息)。 

我将向您展示如何使用 Fiddler (和 FiddlerScript) 以与检查 HTTP 流量相同的方式检查 WebSocket 流量。 此技术适用于所有 WebSocket 实现,包括 SignalR、Socket.IO 和原始 WebSocket 实现等。

背景  

Chrome 流量检查器的局限性 

  • WebSocket 流量显示在“网络”选项卡 -> "connect" 数据包 (101 Switching Protocols) -> “Frame”选项卡中。 除非您再次单击 101 数据包,否则 WebSocket 流量不会自动刷新。
  • 它不支持“Continuation Frame”(延续帧),如下所示: 

 

Fiddler Log 选项卡的局限性 

  • Fiddler Log 选项卡中的 WebSocket 流量帧未分组,因此很难在帧之间导航。 
  • 延续帧未解码,它们显示为二进制。  
  • 此外,如果您的流量很大,Fiddler 将占用 100% 的 CPU 并挂起。  

 

  • 提示:默认情况下,Fiddler 不再将 WebSocket 消息输出到 Log 选项卡。
    您可以使用 FiddlerScript 执行此操作。 只需单击 Rules > Customize
    Rules 并将以下函数添加到您的 Handlers 类中
static function OnWebSocketMessage(oMsg: WebSocketMessage) {
    // Log Message to the LOG tab
    FiddlerApplication.Log.LogString(oMsg.ToString());
}  

解决方案    

使用此解决方案,您可以获得以下所有好处 

 

  • 您可以在 Fiddler 主窗口中看到所有 WebSocket 帧,并且可以轻松地在帧之间导航。 
  • 您可以在 Inspector 选项卡 -> Request info -> JSON 子选项卡中查看帧的详细信息。
  • 延续帧会自动解码并组合在一起。
  • 所有帧都会被捕获并自动显示在 Fiddler 主窗口中,无需手动重新加载。
  • 对于高流量,Fiddler 的 CPU 使用率仍然会很低。

 


它是如何工作的? 

1. 下载 Fiddler Web Debugger (v4.4.5.9) 

2. 打开 Fiddler -> Rules -> Customize Rules... -> 这将打开 FiddlerScript

3. 添加以下代码 FiddlerScript

import System.Threading;

// ...

class Handlers
{
    // ...
    static function Main() 
    {
        // ...
     
        //
        // Print Web Socket frame every 2 seconds
        //
        printSocketTimer =
            new System.Threading.Timer(PrintSocketMessage, null, 0, 2000);
    }
     
    // Create a first-in, first-out queue
    static var socketMessages = new System.Collections.Queue();
    static var printSocketTimer = null;
    static var requestBodyBuilder = new System.Text.StringBuilder();
    static var requestUrlBuilder = new System.Text.StringBuilder();
    static var requestPayloadIsJson = false;
    static var requestPartCount = 0;
       
    //
    // Listen to WebSocketMessage event, and add the socket messages
    // to the static queue.
    //
    static function OnWebSocketMessage(oMsg: WebSocketMessage)
    {       
        Monitor.Enter(socketMessages);      
        socketMessages.Enqueue(oMsg);
        Monitor.Exit(socketMessages);          
    }
       
    //
    // Take socket messages from the static queue, and generate fake
    // HTTP requests that will be caught by Fiddler. 
    //
    static function PrintSocketMessage(stateInfo: Object)
    {
        Monitor.Enter(socketMessages);        
       
        while (socketMessages.Count > 0)
        {
            var oMsg = socketMessages.Dequeue();

            ExtractSocketMessage(oMsg);          
        }
       
        Monitor.Exit(socketMessages);       
    }
       
    //
    // Build web socket message information in JSON format, and send this JSON
    // information in a fake HTTP request that will be caught by Fiddler
    //
    // If a frame is split in multiple messages, following function will combine
    // them into one 
    //
    static function ExtractSocketMessage(oMsg: WebSocketMessage)
    {      
        if (oMsg.FrameType != WebSocketFrameTypes.Continuation)
        {               
            var messageID = String.Format(
                "{0}.{1}", oMsg.IsOutbound ? "Client" : "Server", oMsg.ID);
           
            var wsSession = GetWsSession(oMsg);
            requestUrlBuilder.AppendFormat("{0}.{1}", wsSession, messageID);
               
            requestBodyBuilder.Append("{");
            requestBodyBuilder.AppendFormat("\"doneTime\": \"{0}\",", 
                oMsg.Timers.dtDoneRead.ToString("hh:mm:ss.fff"));
            requestBodyBuilder.AppendFormat("\"messageType\": \"{0}\",", 
                oMsg.FrameType);
            requestBodyBuilder.AppendFormat("\"messageID\": \"{0}\",", messageID);
            requestBodyBuilder.AppendFormat("\"wsSession\": \"{0}\",", wsSession);
            requestBodyBuilder.Append("\"payload\": ");
               
                
            var payloadString = oMsg.PayloadAsString();

            
          if (oMsg.FrameType == WebSocketFrameTypes.Binary)
            {
                payloadString = HexToString(payloadString); 
            }

            if (payloadString.StartsWith("{"))
            {
                requestPayloadIsJson = true;                   
            }
            else
            {
                requestBodyBuilder.Append("\"");
            }
               
            requestBodyBuilder.AppendFormat("{0}", payloadString);
                          
        }
        else
        {
            var payloadString = HexToString(oMsg.PayloadAsString());
            requestBodyBuilder.AppendFormat("{0}", payloadString);
        }
           
        requestPartCount++;
           
        if (oMsg.IsFinalFrame)
        {
            if (!requestPayloadIsJson)
            {
                requestBodyBuilder.Append("\"");
            }
           
            requestBodyBuilder.AppendFormat(", \"requestPartCount\": \"{0}\"", 
                requestPartCount);
               
            requestBodyBuilder.Append("}");      
            
                                
            
            SendRequest(requestUrlBuilder.ToString(), requestBodyBuilder.ToString()); 
                        
            requestBodyBuilder.Clear();
            requestUrlBuilder.Clear();
            requestPayloadIsJson = false;
            requestPartCount = 0;
        }       
    }
       
    //
    // Generate fake HTTP request with JSON data that will be caught by Fiddler
    // We can inspect this request in "Inspectors" tab -> "JSON" sub-tab
    //
    static function SendRequest(urlPath: String, message: String)
    {       
        var request = String.Format(
            "POST http://fakewebsocket/{0} HTTP/1.1\n" +
            "User-Agent: Fiddler\n" +
            "Content-Type: application/json; charset=utf-8\n" +
            "Host: fakewebsocket\n" +
            "Content-Length: {1}\n\n{2}",
            urlPath, message.Length, message);
           
            FiddlerApplication.oProxy.SendRequest(request, null);
    }

    //
    // Unfortunately, WebSocketMessage class does not have a member for 
    // Web Socket session number. Therefore, we are extracting session number
    // from its string output.
    //
    static function GetWsSession(oMsg: WebSocketMessage)
    {
        var message = oMsg.ToString();
        var index = message.IndexOf(".");
        var wsSession = message.Substring(0, index);
       
        return wsSession;
    }
       
    //
    // Extract Hex to String.
    // E.g., 7B-22-48-22-3A-22-54-72-61-6E to {"H":"TransportHub","M":"
    //
    static function HexToString(sourceHex: String)
    {
        sourceHex = sourceHex.Replace("-", "");
       
        var sb = new System.Text.StringBuilder();
       
        for (var i = 0; i < sourceHex.Length; i += 2)
        {
            var hs = sourceHex.Substring(i, 2);
            sb.Append(Convert.ToChar(Convert.ToUInt32(hs, 16)));
        }
       
        var ascii = sb.ToString();
        return ascii;

    }
}

 

4. 设置 Fiddler -> AutoResponder (可选)

 

我已使用 Firefox 25.0、IE 11.0、Chrome 32.0 测试过。 只要网络代理设置正确,此解决方案应该适用于所有支持 WebSocket 的浏览器。 以 IE 为例

  1. 打开 Fiddler,这将自动设置网络代理,但这还不够。
  2. 打开 IE -> 工具 -> Internet 选项 -> 连接选项卡 -> 单击“局域网设置”按钮
  3. 单击“高级”按钮
  4. 选中“对所有协议使用相同的代理服务器”复选框。

值得关注的点  

  1. 您可以通过访问 http://www.websocket.org/echo.htmlhttps://socketio.node.org.cn/demos/chat/https://jabbr.net/ 来测试上述解决方案(需要登录。该站点使用 SignalR 构建,支持 WebSocket)。
  2. 上面的代码每 2 秒打印一次 WebSocket 帧,以避免高 CPU 使用率,这意味着“统计信息”选项卡中的时间戳可能会延迟长达 2 秒。 您可以根据您的流量调整此计时器。 
    • 但是,您可以在 Inspector 选项卡 -> Request info -> JSON 子选项卡 -> doneTime 中看到接收帧的实际时间。
  3. 为简单起见,我只为所有 WebSocket 会话创建了一个队列,无论其会话 ID 如何,延续帧都会组合在一起。 随意扩展我的代码。 现在,一次只调试一个 WebSocket 会话。
  4. 上面的代码假设有效负载以 '{' 开头是 JSON 数据,这对我来说效果很好。 如果此假设在您的情况下是错误的,您可以轻松修改代码。
  5. 注意:Socket.IO 目前在有效负载之前添加了一个特殊的含义数字,这使得帧成为无效的 JSON 数据,即,您无法在 JSON 子选项卡中看到格式良好的帧。 但是,您仍然可以在 Raw 子选项卡中看到帧。 或者,您可以更新我的脚本以处理来自 Socket.IO 的数字前缀。
  6. 如果您觉得我的文章有用,请投票。 感谢您的支持。

 

历史 

2014-01-31:初始版本。


 

© . All rights reserved.