[創建時間:2016-05-12 00:19:00]
在NetAnalyzer2016中加入了一個HTTP分析功能,很過用戶對此都很感興趣,那么今天寫一下具體的實現方式,對我自己也算是一個總結吧,好了,廢話就不多少了,直接開始吧。
本文是專注於HTTP數據的分析,所以前期數據包采集相關的內容並不會本文不會涉及,相關內容可以見 NetAnalyzer 筆記 四
在這邊默認你已經獲取到了http數據報文。
一,提取TCP會話數據
通過TCP/IP協議知道,我們可以通過網絡層的IP地址可以唯一的確定一台主機,通過傳輸層的端口確定一個主機應用,而主機應用與另外一個主機應用的一次數據通信(報文交換)我們稱之為一次會話,而建立的傳輸層協議為TCP上面的一次交互數據我們就稱之為TCP會話數據。
一次常規的TCP會話
在這里我們可以看到本地主機應用標志 IP地址: 192.168.1.102 端口 55298 和遠端主機應用標志 IP地址:124.238.254.191 端口 80 (80端口所對應的應用協議就是HTTP),通過這組標志,而下面每行記錄都代表一TCP數據包,
每個TCP包包含了TCP狀態標志,載荷數據(TCP數據包封裝的數據)量,序列號等信息,大家可以留意一下每兩條記錄之間的序列號和載荷數據量之間的關系。
我們這里要提取的數據正是這組TCP報文中的載荷數據
這段代碼用於定義並生成一個TCP標志

1 /// <summary> 2 /// 根據IP與端口構建ID 3 /// </summary> 4 public class ConnIDbyIPPort : IConnectionID 5 { 6 /// <summary> 7 /// Client IP 8 /// </summary> 9 IPAddress _srcIPaddress;//Client IP 10 /// <summary> 11 /// Server IP 12 /// </summary> 13 IPAddress _dstIPaddress;//Server IP 14 /// <summary> 15 /// Client port 16 /// </summary> 17 int _srcPort;//Client port 18 /// <summary> 19 /// Server port 20 /// </summary> 21 int _dstPort;//Server port 22 /// <summary> 23 /// 源IP地址屬性 24 /// </summary> 25 public IPAddress SrcIPAddress 26 { 27 get { return _srcIPaddress; } 28 } 29 /// <summary> 30 /// 目標IP地址屬性 31 /// </summary> 32 public IPAddress DstIPAddress 33 { 34 get { return _dstIPaddress; } 35 } 36 /// <summary> 37 /// 源端口屬性 38 /// </summary> 39 public int SrcPort 40 { 41 get { return _srcPort; } 42 } 43 /// <summary> 44 /// 目的端口屬性 45 /// </summary> 46 public int DstPort 47 { 48 get { return _dstPort; } 49 } 50 51 /// <summary> 52 /// 判斷當前的數據包的方向 53 /// 注意: 54 /// 此處所說的方向具有相對性 55 /// </summary> 56 /// <param name="srcPort"></param> 57 /// <returns></returns> 58 public bool isSameDirection(int srcPort) 59 { 60 if (srcPort == _srcPort) 61 return true; 62 else if (srcPort == _dstPort) 63 return false; 64 else 65 throw new ArgumentException("無效的參數,不是該鏈接之中的數據!"); 66 } 67 /// <summary> 68 /// 構造方法 69 /// </summary> 70 /// <param name="ClientIPaddress">源IP地址</param> 71 /// <param name="ClientPort">源端口</param> 72 /// <param name="ServerIPaddress">目標IP地址</param> 73 /// <param name="ServerPort">目標端口</param> 74 public ConnIDbyIPPort(IPAddress ClientIPaddress, int ClientPort, IPAddress ServerIPaddress, int ServerPort) 75 { 76 _srcIPaddress = ClientIPaddress; 77 _srcPort = ClientPort; 78 _dstIPaddress = ServerIPaddress; 79 _dstPort = ServerPort; 80 } 81 /// <summary> 82 /// 構造函數 83 /// </summary> 84 /// <param name="rawpacket"></param> 85 public ConnIDbyIPPort(RawCapture rawpacket) 86 { 87 SetInfo(rawpacket); 88 } 89 /// <summary> 90 /// 創建ID 91 /// </summary> 92 /// <param name="rawpacket"></param> 93 private void SetInfo(RawCapture rawpacket) 94 { 95 try 96 { 97 Packet packet = Packet.ParsePacket(rawpacket.LinkLayerType, rawpacket.Data); 98 99 TcpPacket tcp = TcpPacket.GetEncapsulated(packet); 100 if (tcp != null) 101 { 102 IpPacket ip = (IpPacket)tcp.ParentPacket; 103 _srcIPaddress = ip.SourceAddress; 104 _srcPort = tcp.SourcePort; 105 _dstIPaddress = ip.DestinationAddress; 106 _dstPort = tcp.DestinationPort; 107 return; 108 } 109 UdpPacket udp = UdpPacket.GetEncapsulated(packet); 110 if (udp != null) 111 { 112 IpPacket ip = (IpPacket)udp.ParentPacket; 113 _srcIPaddress = ip.SourceAddress; 114 _srcPort = udp.SourcePort; 115 _dstIPaddress = ip.DestinationAddress; 116 _dstPort = udp.DestinationPort; 117 return; 118 } 119 } 120 catch (Exception ex) 121 { 122 throw ex; 123 } 124 } 125 /// <summary> 126 /// 創建ConnectionID; 127 /// </summary> 128 /// <param name="rawpacket">原始數據報文</param> 129 /// <returns></returns> 130 public IConnectionID CreatID(RawCapture rawpacket) 131 { 132 try 133 { 134 ConnIDbyIPPort id = new ConnIDbyIPPort(rawpacket); 135 return id; 136 137 } 138 catch (Exception ex) 139 { 140 return null; 141 } 142 } 143 /// <summary> 144 /// 重寫Equals()方法 145 /// </summary> 146 /// <param name="obj">所要比較的ConnectionID</param> 147 /// <returns>比較結果</returns> 148 public override bool Equals(object obj)//重寫的Equals,用於比較兩個連接是否相同 149 { 150 if (obj == null) 151 return false; 152 ConnIDbyIPPort tempID = (ConnIDbyIPPort)obj; 153 if (this._srcIPaddress.Equals(tempID._srcIPaddress) && this._srcPort == tempID._srcPort && this._dstIPaddress.Equals(tempID._dstIPaddress) && this._dstPort.Equals(tempID._dstPort))//正向比較 154 { 155 return true; 156 } 157 else if (this._srcIPaddress.Equals(tempID._dstIPaddress) && this._srcPort == tempID._dstPort && this._dstIPaddress.Equals(tempID._srcIPaddress) &&this._dstPort==tempID._srcPort)//逆向比較 158 { 159 return true; 160 } 161 else 162 { 163 return false; 164 } 165 } 166 /// <summary> 167 /// 使標識用Equals進行比較 168 /// </summary> 169 /// <returns></returns> 170 public override int GetHashCode()//系統默認對象通過HashCode比較,故在此屏蔽此方法 171 { 172 return 1; 173 } 174 /// <summary> 175 /// 重寫ToString()方法 176 /// </summary> 177 /// <returns>ConnectionID</returns> 178 public override string ToString()//獲取ID字符串 179 { 180 return _srcIPaddress.ToString() + ":" + _srcPort.ToString() + "--" + 181 _dstIPaddress.ToString() + ":" + _dstPort.ToString(); 182 } 183 #region 操作符重載 184 //public static bool operator==(ConnectionID id1,ConnectionID id2) 185 //{ 186 // return id1.Equals(id2); 187 //} 188 //public static bool operator !=(ConnectionID id1, ConnectionID id2) 189 //{ 190 // return !id1.Equals(id2); 191 //} 192 #endregion 193 194 }
對於這段代碼的使用方法如下:
1 // 選擇一個RawCapture 2 RawCapture rawPacket = PacketList[i]; 3 // 通過RawCapture 獲取到一個標志 4 var tmpId = new ConnIDbyIPPort(rawPacket);
通過標志位挑選會話數據
1 /// <summary> 2 /// 通過ID和連接類型獲取原始數據列表 3 /// </summary> 4 /// <param name="id"></param> 5 /// <param name="connType">之定義了三種,以后會擴展</param> 6 /// <returns></returns> 7 private RawCapture[] getConnectionList(IConnectionID id, ConnectionType connType) 8 { 9 if (id == null) 10 return null; 11 try 12 { 13 //var values = from v in PacketList 14 // where id.Equals(id.CreatID(v)) 15 // select v; 16 //if (values == null) 17 // return null; 18 19 List<RawCapture> values = new List<RawCapture>(); 20 foreach (var i in PacketList) 21 { 22 if (id.Equals(id.CreatID(i))) 23 { 24 values.Add(i); 25 } 26 } 27 28 return values.ToArray(); 29 } 30 catch (Exception ex) 31 { 32 MessageBox.Show("正在獲取網絡獲取相關數據,請稍后再試!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); 33 return null; 34
通過這段代碼,我們最終獲取到了這組TCP數據,接下來就是載荷數據的提取
二,TCP載荷數據的提取與組合
我們現在獲取到的數據包都是TCP,接下來我們要面對兩個重要的問題:
1、對於HTTP協議中傳輸的文本,圖片,文件等數據有可能需要多個TCP數據包才能發送或接受完整;
2、存在一些載荷數據為0的TCP數據包,這些數據包對於TCP的會話特別重要(在TCP中完成確認或重傳機制,保證數據可靠性),但是對於我們分析數據並沒有太多的意義。
根據以上兩點,我們需要對得到的這組會話數據進行重新的組合,這里的基本思路是,建立一個單向鏈表結構,以第一個載荷數據不為空的數據包作進行拆包提取特征量和載荷數據,然后移到下一個載荷數據不為空的數據包,提取數據包特征量,與當前節點特征量比較,如果方向一致(端口號相同)則將載荷數據緩存到當前節點,如果不一致(端口號不相同)則生成新的節點,生成特征,提取載荷數據緩存,並把上個節點指向新節點,
新結構示意圖
對於每個節點的定義如下:
1 /// <summary> 2 /// TCP單向載荷數據節點 3 /// </summary> 4 class DataNode 5 { 6 /// <summary> 7 /// 構造 8 /// </summary> 9 /// <param name="port"> 端口</param> 10 public DataNode(ushort port) 11 { 12 this.Port = port; 13 this.Buffer = new List<byte>(); 14 } 15 16 public DataNode NextNode { get; set; } 17 18 19 public ushort Port { get; private set; } 20 public List<byte> Buffer { get; private set; } 21 22 23 public void InsertData(byte[] data) 24 { 25 this.Buffer.AddRange(data); 26 } 27 }
構建單向列表
/// <summary> /// 頭部節點 /// </summary> private DataNode HeadNode; //當前比對的節點 private DataNode flagNode; public void InsertData(byte[] data, ushort port) { // 載荷數據判空 if (data == null || data.Length == 0) return; // 生成頭部節點 if (HeadNode == null) { flagNode = new DataNode(port); HeadNode = flagNode; } // 插入數據 和 生成后續節點 if (port == flagNode.Port) { flagNode.InsertData(data); } else { var tmpNode = new DataNode(port); flagNode.NextNode = tmpNode; tmpNode.InsertData(data); flagNode = tmpNode; } }
TCP會話的轉換
1 hp = new HttpHelper.HttpHelper(); 2 3 foreach (var rawPacket in PacketList) 4 { 5 Packet packet = Packet.ParsePacket(rawPacket.LinkLayerType, rawPacket.Data); 6 TcpPacket tcp = TcpPacket.GetEncapsulated(packet); 7 if (tcp != null)//TCP 8 { 9 hp.InsertData(tcp.PayloadData, tcp.SourcePort); 10 } 11 }
自此,我們已經獲取到了,期望的數據結構模型,那么下一節就要着手開始提取HTTP信息了。
三,http特征量提取
我們知道常規的http協議包含消息頭和數據兩部分:消息頭是由ASCII編碼的多條命令行數據組成,每行之間使用CRLF(回車和換行)進行分割,消息頭結束后,需要多增加一個CRLF,
一個典型的http請求與回復會話(根據上一節內容,這段數據,紅色部分為頭部節點,藍色部分在第二個節點中)
有圖可知,客戶端發起一個 get 的請求(紅色部分),該請求並沒有攜帶數據;服務端回復 http/1.1 200 OK 表示已經接受請求,並返回數據(藍色部分),服務請返回的數據除了http消息頭 還有自己所帶的數據。
我們可以很輕松的看到http協議具體的內容,但是對於數據部分是一對亂碼,那么接下來任務就是要根據http消息頭獲取這些亂碼的信息
因為本篇以實踐為主,並不想討論具體的http命令,所以此處只對部分命令做一些詳細分析,其他命令,網上內容很多請自行搜索。
通過http協議對回復數據(第二個節點)查找到以下兩個字段:
1. Content-Type: application/zip 通過查詢MIME 我們知道這是一個*.zip文件,那也就是消息頭后面的亂碼事實上是一個zip壓縮文件,
2. Content-Length: 7261 由此我們判斷出這個zip文件的大小為 7261個字節
對於一些其他的命令對我們后續還原數據不大,直接跳過。
至此我們就找到了http協議特征量,但是因為http傳輸數據格式的多樣性,傳輸過程的復雜性,有些服務器的http協議特征更為復雜
HTTP/1.1 200 OK
Date: Wed, 11 May 2016 14:47:47 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Cache-Control: public, max-age=137
Expires: Wed, 11 May 2016 14:50:05 GMT
Last-Modified: Wed, 11 May 2016 14:45:05 GMT
Vary: *
X-UA-Compatible: IE=10
Content-Encoding: gzip
這段http回復中可以看到數據類型變為 Content-Type: text/html外,沒有了content-Length 字段,但是增加幾條新的字段,分別是:
1. charset=utf-8 這部分對於數據類型為字符串的還原非常重要,尤其是對於非英語文本的還原,
2. Transfer-Encoding: chunked 這個字段,用於標出動態加載的數據 詳細信息請參見:http://www.cnblogs.com/zhaozhan/archive/2010/08/24/1807639.html
3. Content-Encoding: gzip 為了縮小數據傳輸量,使用該字段進行數據壓縮,所以我們在還原數據的時候遇到該字段就需要解壓縮
好了,扯了一大堆,那我們開始准備提取這些特征字段吧
先定義一個http數據結構

1 public class HttpEntity 2 { 3 /// <summary> 4 /// 消息頭 5 /// </summary> 6 public string HeadStr { get; set; } 7 /// <summary> 8 /// 數據 文本,圖片或二進制數據 9 /// </summary> 10 public object Data { get; set; } 11 /// <summary> 12 /// 數據類型 13 /// </summary> 14 public Type DataType { get; set; } 15 /// <summary> 16 /// 擴展名 17 /// </summary> 18 public string ExtName { get; set; } 19 20 }
接下來對上一節提到的數據鏈表以此進行消息頭提取
1 /// <summary> 2 /// 獲取http實體列表 3 /// </summary> 4 /// <param name="encode">預指定的文本編碼方式</param> 5 /// <returns></returns> 6 public List<HttpEntity> GetHttpEntityList(Encoding encode) 7 { 8 List<HttpEntity> DataList = new List<HttpEntity>(); 9 10 for (DataNode tmp = HeadNode; tmp != null; tmp = tmp.NextNode) 11 { 12 for (int i = 0; i <= tmp.Buffer.Count - 4; i++) 13 { 14 if (tmp.Buffer[i] == 0x0D && tmp.Buffer[i + 1] == 0x0A && tmp.Buffer[i + 2] == 0x0D && tmp.Buffer[i + 3] == 0x0A) 15 { 16 string headStr = Encoding.ASCII.GetString(tmp.Buffer.Take(i + 1).ToArray()); 17 DataList.Add(getInnerStr(tmp.Buffer.Skip(i + 4).ToArray(), headStr, encode)); 18 break; 19 } 20 } 21 } 22 23 return DataList; 24 }
這里使用for循環,從鏈表表頭開始以此查找兩組CRLF(0x0D 0x0A 0x0D 0x0A)直到直到之后,截取前面的數據並使用ASCII進行解碼為headStr ,而后面的數據就可以根據前面獲取到的消息頭進行進一步還原了,在 getInnerStr() 方法中完成
接下來我們繼續看getInnerStr()方法的前半部分
1 private HttpEntity getInnerStr(byte[] data, string headFlag, Encoding defaultEncode) 2 { 3 4 //最終數據呈現 5 HttpEntity result = new HttpEntity(); 6 result.HeadStr = headFlag; 7 8 9 if (data == null || data.Length == 0) 10 return result; 11 12 StringBuilder sbr = new StringBuilder(); 13 14 //是否使用chunked 15 bool isChunked = headFlag.ToLower().Contains("transfer-encoding: chunked"); 16 bool isGzip = headFlag.ToLower().Contains("content-encoding: gzip"); 17 string contentType = ""; 18 string charSet = ""; 19 int ContentLength = 0; 20 Encoding enCode = defaultEncode; 21 var mtype = Regex.Match(headFlag, @"Content-Type:\s*(\w+\/[\w\.\-\+]+)", RegexOptions.IgnoreCase); 22 if (mtype.Success && mtype.Groups.Count >= 2) 23 { 24 contentType = mtype.Groups[1].Value.Trim().ToLower(); 25 } 26 var Mchar = Regex.Match(headFlag, @"charset=\s*([-\w]+)", RegexOptions.IgnoreCase); 27 if (Mchar.Success && Mchar.Groups.Count >= 2) 28 { 29 charSet = Mchar.Groups[1].Value.Trim(); 30 if (charSet.ToLower() == "utf8") 31 { 32 charSet = "utf-8"; 33 } 34 try 35 { 36 enCode = Encoding.GetEncoding(charSet); 37 } 38 catch (Exception) 39 { 40 enCode = defaultEncode; 41 } 42 } 43 44 var MLength = Regex.Match(headFlag, @"Content-Length:\s*(\d+)", RegexOptions.IgnoreCase); 45 if (MLength.Success && MLength.Groups.Count >= 2) 46 { 47 ContentLength = int.Parse(MLength.Groups[1].Value.Trim()); 48 } 49 …………
這里使用正則表達式和字符串查找功能,在消息頭中分別查找和去取對應的字段(對於字符編碼,有些服務器使用的utf8 在解碼是需要改為utf-8 否則會引起異常)
代碼相對比較簡短,這里就不做過多介紹了。
四,http數據解析(包括chunked 和 gzip)
通過上一節,我們已經拿到了http消息頭的特征信息和數據,這部分就開始還原數據,具體有三個步驟:
1. 數據提取,數據提取基本就是找開始位置和數據長度,根據頁面加載方式的不同,數據頭提取分為 通過Content-Length 提取和 通過 Chunked 提取兩種方法,
2. 數據解壓縮,有時候我們獲得的數據可能經過gzip壓縮的,所以這個時候要對數據進行解壓縮處理。
3. 數據還原,對於不同的數據有着不同的還原方式,轉為字符串、保存為圖片,生成文件等等
那接下來就開始看代碼吧,首先我們補上上一節方法的后半部分
1 /// 數據整合 2 byte[] rawData = null; 3 if (isChunked)// 通過Chunked獲取數據 4 { 5 GetchunkedData(data); 6 rawData = ChunkBuffer.ToArray(); 7 } 8 else if (ContentLength > 0 && ContentLength <= data.Length)//通過ContentLength 獲取數 9 { 10 rawData = data.Take(ContentLength).ToArray(); 11 } 12 else 13 { 14 rawData = data; 15 } 16 if (rawData == null && rawData.Length == 0) 17 return result; 18 19 /// 數據解壓縮 20 if (isGzip) 21 { 22 rawData = Tools.GzipDecompress(rawData); 23 } 24 if (rawData == null && rawData.Length == 0) 25 return result; 26 27 28 29 //獲取擴展名 30 string extName = ""; 31 if (EimeData.EimeDic.Keys.Contains(contentType)) 32 { 33 extName = EimeData.EimeDic[contentType]; 34 } 35 result.ExtName = extName; 36 //獲取數據或類型 37 switch (Tools.GetMimeType(contentType)) 38 { 39 case MimeType.Text: 40 result.Data = Tools.ShowPopFormStr(enCode.GetString(rawData)); 41 result.DataType = typeof(string); 42 break; 43 case MimeType.Image: 44 result.Data = rawData; 45 result.DataType = typeof(System.Drawing.Image); 46 break; 47 default: 48 result.Data = rawData; 49 result.DataType = typeof(byte[]); 50 break; 51 } 52 return result;
首先是整合數據,我們通過chunked、content-length等特征字段對獲取的數據進行重新整合,這里涉及到Chunked數據整合,那讓我們一起來看看代碼吧
(具體chunked數據形式自行搜索)
1 /// <summary> 2 /// Chunked數據緩存列表 3 /// </summary> 4 List<byte> ChunkBuffer = new List<byte>(); 5 private void GetchunkedData(byte[] data) 6 { 7 if (data.Length == 0) 8 return; 9 for (int i = 0; i < data.Length; i++) 10 { 11 //查找CRCL標志 12 if (data[i] == 0x0D && data[i + 1] == 0x0A) 13 { 14 int count = data.Length - 2; 15 try 16 { 17 //獲取下一段數據的長度 bytes -> ASCII字符 ->加上0x前綴 (如:0x34) -> 轉為數值(下一塊的數量長度) 18 count = Convert.ToInt32("0x" + Encoding.ASCII.GetString(data.Take(i).ToArray()), 16); 19 } 20 catch (Exception ex) 21 { 22 //產生異常的直接返回 23 ChunkBuffer.AddRange(data.Skip(i - 2).ToArray()); 24 break; 25 } 26 //如果為0表示完成Chunked數據轉化跳出循環返回 27 if (count == 0) 28 break; 29 30 if (i + 2 + count <= data.Length) 31 { 32 //加入已經計算好的數據 33 ChunkBuffer.AddRange(data.Skip(i + 2).Take(count)); 34 //遞歸 進行下一輪匹配 35 GetchunkedData(data.Skip(i + 4 + count).ToArray()); 36 } 37 else 38 { 39 //對於存在數據不完整的 直接返回 40 ChunkBuffer.AddRange(data.Skip(i - 2).ToArray()); 41 } 42 break; 43 } 44 } 45 }
在這里預先定義一個緩存列表 ChunkBuffer, 然后通過GetchunkedData()方法遞歸調用最后獲取到需要的數據。
此時我們已經獲取到了數據,那么就可以考慮是否解壓縮的問題了,解壓縮比較簡單,這里依然直接貼代碼了
1 /// <summary> 2 /// 通過Gzip方式解壓縮數據 3 /// </summary> 4 /// <param name="data">字節數據</param> 5 /// <returns></returns> 6 public static byte[] GzipDecompress(byte[] data) 7 { 8 //解壓 9 using (MemoryStream dms = new MemoryStream()) 10 { 11 using (MemoryStream cms = new MemoryStream(data)) 12 { 13 using (System.IO.Compression.GZipStream gzip = new System.IO.Compression.GZipStream(cms, System.IO.Compression.CompressionMode.Decompress)) 14 { 15 byte[] bytes = new byte[1024]; 16 int len = 0; 17 int totalLen = 0; 18 //讀取壓縮流,同時會被解壓 19 try 20 { 21 while ((len = gzip.Read(bytes, 0, bytes.Length)) > 0) 22 { 23 dms.Write(bytes, 0, len); 24 totalLen += len; 25 } 26 } 27 catch (InvalidDataException ex) 28 { 29 //dms.Write(data.Skip(totalLen).ToArray(),0,data.Length-totalLen); 30 } 31 } 32 } 33 return dms.ToArray(); 34 } 35 }
這樣我們就拿到了真正需要的數據了
最后我們就可以通過 Content-Type 來判斷將數據還原為那種類型,就這樣我們就獲得了需要的數據
接下來我們看看解析完的數據
首先是zip的數據:
導出為文件后
最后是哪個帶有Chunked 和gzip 標志的數據還原
五,更加可靠的數據還原
通過上面的數據還原方法,已經基本滿足我們的數據還原需求,但是因為網絡、計算機、以及我們程序本身的問題,我們拿到的數據包並不會嚴格的按照發包的先后順序獲取到數據包,而且因為TCP協議確認重傳,有可能發生在某個數據包中,及發送端發送了一個1440的數據包,接收端有可能只取700個字節,剩余的需要重現傳輸,對於監控端,如果沒有處理這種情形的機制,就會造成數據差生誤差而不能進行正確還原,更別說還有丟包的情況。
而這種機制就是TCP的數據重組,通過建立合理的模型對tcp狀態遷移、序列號、數量等一系列變量進行統一的規划提取還原,最終生成完成而且正確的數據。因為本篇只是簡單討論http數據的還原,所以此處不打算深入的展開, 如果有時間,再寫一篇關於TCP重做的文章吧。
感謝你的閱讀,歡迎使用NetAnalyzer,歡迎關注NetAnalyzer公眾平台。