October 10, 2022
By: Kevin

从wireshark抓取数据帧结合代码调试的完整流程

  1. 1. Wireshark 抓包并复制十六进制
  2. 2. 字符串 → byte[]
    1. 2.1 推荐方案: JDK 17+ java.util.HexFormat
    2. 2.2 旧版 JDK 替代
  3. 3. 统一到 byte-streams 生态
  4. 4. 用 Gloss 描述协议
  5. 5. 解码并转成业务 Map
  6. 6. 快速 REPL 验证
  7. 7. 单元测试示例
  8. 8. 踩坑与优化
  9. 9. 小结

本文记录一次从 Wireshark 抓包到 Clojure Gloss 解码的完整实践, 帮助你快速把十六进制流解析成结构化数据. 全流程仅依赖 clj-commons/byte-streamsgloss, 其余全部使用 JDK 自带功能.

1. Wireshark 抓包并复制十六进制

  1. 在 Wireshark 里选中一条 TCP 报文.
  2. 在TCP的数据(TCP Segment data)上右键 → Copy → as Hex Stream.
  3. 得到一段纯十六进制字符串, 例如:
ff000000000000000000000000000000000000000000387cd6ea37fcd6ea387cd6ea41c73d3041b602d041ab54003f62da13000000000000000037fcd6ea41afcdee000000003f49638f3d81a12f

粘到 Clojure REPL 或编辑器备用.

2. 字符串 → byte[]

2.1 推荐方案: JDK 17+ java.util.HexFormat

(import '[java.util HexFormat]
        '[clojure.string :as str])

(def hex-str "...上面的字符串...")

(def bytes (.parseHex (HexFormat/of)
                      (str/replace hex-str #"\s+" "")))
;; bytes 现在是真正的 Java byte[]
  • 零依赖, 速度快.
  • 要求 JDK ≥ 17.

2.2 旧版 JDK 替代

(defn hex->byte-array
  "Parse a pure hex string (ignore any whitespace) into a Java byte[]."
  ^bytes
  [^String s]
  (let [clean (str/replace s #"[^0-9A-Fa-f]" "")     ; 去掉空格或其它非16进制字符
        n     (count clean)]
    (when (odd? n)
      (throw (ex-info "Hex string length must be even" {:len n})))
    (byte-array                             ; 最终构造 byte[]
     (for [i (range 0 n 2)]
       (unchecked-byte
        (Integer/parseInt (.substring clean i (+ i 2)) 16))))))

3. 统一到 byte-streams 生态

(require '[byte-streams :as bs])
(def ba (bs/to-byte-array bytes)) ; ba 就是你后面要喂给 Gloss 的东西

bs/to-byte-array 会探测类型, 已是 byte[] 时不会复制.

4. 用 Gloss 描述协议

(require '[gloss.core :as g])

(g/defcodec frame-synthesis
  [:ubyte :ubyte :ubyte :ubyte :ubyte :ubyte :ubyte :ubyte :ubyte :ubyte
   :float32-be
   :float32-be
   :float32-be

   :float32-be
   :float32-be
   :float32-be

   :float32-be
   :float32-be
   :float32-be

   :float32-be
   :float32-be
   :float32-be

   :float32-be
   :float32-be
   :float32-be

   :float32-be
   :float32-be])
  • :ubyte 描述单字节无符号整数.
  • :float32-be 描述大端 32-bit 浮点数.
  • 其余型号(裂解、天马合成)只要再 g/defcodec 一次即可.

5. 解码并转成业务 Map

(require '[gloss.io :as gio])

(defn parse-synthesis
  [bs]
  (let [[_FF m20 m21 m22 m23 m24 m25 m26 m27

         inverter1-actual-frequency
         inverter2-actual-frequency
         inverter3-actual-frequency

         flowmeter1-instantaneous-flow
         flowmeter2-instantaneous-flow
         flowmeter3-instantaneous-flow

         temperature1
         temperature2
         temperature3

         pressure1
         inverter4-actual-frequency
         flowmeter4-instantaneous-flow

         flowmeter5-instantaneous-flow
         temperature4
         pressure2

         pressure3
         pressure4] (gio/decode frame-synthesis bs)]
    ;;变量名直接在这个位置进行更改

    (merge {:inverter1-actual-frequency            inverter1-actual-frequency
            :inverter2-actual-frequency            inverter2-actual-frequency
            :inverter3-actual-frequency            inverter3-actual-frequency

            :flowmeter1-instantaneous-flow         flowmeter1-instantaneous-flow
            :flowmeter2-instantaneous-flow         flowmeter2-instantaneous-flow
            :flowmeter3-instantaneous-flow         flowmeter3-instantaneous-flow

            :temperature1                          temperature1
            :temperature2                          temperature2
            :temperature3                          temperature3

            :pressure1                             pressure1
            :inverter4-actual-frequency            inverter4-actual-frequency
            :flowmeter4-instantaneous-flow         flowmeter4-instantaneous-flow

            :flowmeter5-instantaneous-flow         flowmeter5-instantaneous-flow
            :temperature4                          temperature4
            :pressure2                             pressure2

            :pressure3                             pressure3
            :pressure4                             pressure4}

           (parse-m20 m20)
           (parse-m21 m21)
           (parse-m22 m22)
           (parse-m23 m23)
           (parse-m24 m24)
           (parse-m25 m25)
           (parse-m26 m26)
           (parse-m27 m27))))

如果需要解析位域, 建议把 m20m21 等字节交给一个辅助函数:

(defn byte->bits [b]
  (reverse
   (map #(pos? (bit-and b (bit-shift-left 1 %)))
        (range 8))))

然后用 zipmap 把布尔位映射到含义即可.

6. 快速 REPL 验证

(comment
  (def ba (-> hex-str
              (str/replace #"\s+" "")
              (.parseHex (HexFormat/of))))
  (gio/decode frame-synthesis ba))

确认字段数值与现场仪表一致后, 再集成到生产代码.

7. 单元测试示例

(ns socket-test
  (:require [clojure.test :refer :all]
            [socket :as sut]))

(deftest frame-synthesis-decode-test
  (let [bs (sut/hex->bytes "ff00...")]
    (is (= {:temperature1 25.3
            :pressure1    1.02}
           (select-keys (sut/parse-synthesis bs)
                        [:temperature1 :pressure1])))))

8. 踩坑与优化

  • 长度校验: 用 (gp/sizeof codec) 动态获取字节数, 读流时不会读多或读少.
  • 字节序: 西门子 S7 默认大端, 但也可能小端混用, 一定跟电气工程师确认.
  • 浮点精度: float32 仅 7 位有效数字, 若需要高精度, 改用 :float64-be.
  • 性能: Gloss 内部零拷贝, 但高频场景仍建议把 byte[] 缓存到对象池.

9. 小结

  1. Wireshark 复制 Hex Stream.
  2. HexFormat.parseHex 把字符串一次性转成 byte[].
  3. byte-streams 统一数据流类型.
  4. gloss.core/defcodec 抽象协议, gloss.io/decode 得到结构化向量.
  5. 再按业务需求映射成易用的 Clojure map.

这样你就完成了从抓包到解码的闭环, 可以放心把解析后的数据喂给报警逻辑、监控面板或数据库. 如果还有更复杂的帧, 只需要再写一段 defcodec + parse-* 即可复用同一套流程.

Tags: wireshark network