使用 Fiddler 调试 / 检查 WebSocket 流量






4.97/5 (14投票s)
使用 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 为例
- 打开 Fiddler,这将自动设置网络代理,但这还不够。
- 打开 IE -> 工具 -> Internet 选项 -> 连接选项卡 -> 单击“局域网设置”按钮
- 单击“高级”按钮
- 选中“对所有协议使用相同的代理服务器”复选框。
值得关注的点
- 您可以通过访问 http://www.websocket.org/echo.html, https://socketio.node.org.cn/demos/chat/ 和 https://jabbr.net/ 来测试上述解决方案(需要登录。该站点使用 SignalR 构建,支持 WebSocket)。
- 上面的代码每 2 秒打印一次 WebSocket 帧,以避免高 CPU 使用率,这意味着“统计信息”选项卡中的时间戳可能会延迟长达 2 秒。 您可以根据您的流量调整此计时器。
- 但是,您可以在 Inspector 选项卡 -> Request info -> JSON 子选项卡 -> doneTime 中看到接收帧的实际时间。
- 为简单起见,我只为所有 WebSocket 会话创建了一个队列,无论其会话 ID 如何,延续帧都会组合在一起。 随意扩展我的代码。 现在,一次只调试一个 WebSocket 会话。
- 上面的代码假设有效负载以 '{' 开头是 JSON 数据,这对我来说效果很好。 如果此假设在您的情况下是错误的,您可以轻松修改代码。
- 注意:Socket.IO 目前在有效负载之前添加了一个特殊的含义数字,这使得帧成为无效的 JSON 数据,即,您无法在 JSON 子选项卡中看到格式良好的帧。 但是,您仍然可以在 Raw 子选项卡中看到帧。 或者,您可以更新我的脚本以处理来自 Socket.IO 的数字前缀。
- 如果您觉得我的文章有用,请投票。 感谢您的支持。
历史
2014-01-31:初始版本。