一、Socket 編程
Socket(套接字)是網絡通信的端點,是對 TCP/IP 協議的編程抽象,用于實現兩臺主機間的數據交換。
通俗來說:
-
可以把 Socket 理解為“電話插口”,插上后客戶端和服務端才能“通話”。
-
Socket = IP 地址 + 端口號(唯一標識一個通信端)
1.1?TCP 通信原理與 Java 實現
1)TCP 特點
特性 | 描述 |
---|---|
面向連接 | 通信前需建立連接(三次握手) |
可靠傳輸 | 保證數據順序、完整、無丟失 |
基于字節流 | 數據以流的形式傳輸 |
雙向通信 | 可同時讀寫 |
2)通信流程圖(標準 TCP 模型)
客戶端 服務端| ---------> connect() ----------> || <-------- accept() <------------ || ---------> OutputStream --------|| <--------- InputStream ---------|| ---------> close() ------------>|
3)服務端代碼示例(ServerSocket)
import java.io.*; // 導入輸入輸出相關類(BufferedReader、InputStreamReader、PrintWriter 等)
import java.net.*; // 導入網絡相關類(ServerSocket、Socket 等)public class TCPServer { // 定義一個名為 TCPServer 的類public static void main(String[] args) throws IOException { // 主函數,拋出 IO 異常,避免必須 try-catchServerSocket serverSocket = new ServerSocket(8888); // 創建一個服務器端的 ServerSocket,監聽 8888 端口System.out.println("服務端啟動,等待連接..."); // 控制臺提示:服務端已啟動,等待客戶端連接Socket socket = serverSocket.accept(); // 監聽并接受客戶端連接(阻塞式等待,直到客戶端連接為止)System.out.println("客戶端已連接:" + socket.getInetAddress()); // 打印連接上來的客戶端 IP 地址// 讀取客戶端消息BufferedReader reader = new BufferedReader( // 創建 BufferedReader,從 socket 的輸入流中讀取數據new InputStreamReader(socket.getInputStream())); // 使用 InputStreamReader 把字節流轉為字符流String msg = reader.readLine(); // 讀取客戶端發送的一行字符串(以換行符為結束標志)System.out.println("收到客戶端消息: " + msg); // 打印收到的客戶端消息// 給客戶端回應PrintWriter writer = new PrintWriter( // 創建 PrintWriter 向客戶端發送數據socket.getOutputStream(), true); // 獲取 socket 的輸出流,true 表示自動刷新緩沖區writer.println("你好,客戶端,我收到你的消息了。"); // 向客戶端發送一條消息作為回應socket.close(); // 關閉與客戶端通信的 socket(釋放資源)serverSocket.close(); // 關閉服務器的 ServerSocket(停止監聽)}
}
4)客戶端代碼示例(Socket)
import java.io.*; // 導入 Java 輸入輸出相關類(如 BufferedReader、PrintWriter 等)
import java.net.*; // 導入 Java 網絡通信相關類(如 Socket)public class TCPClient { // 定義一個 TCPClient 客戶端類public static void main(String[] args) throws IOException { // 主函數,拋出 IOException 處理網絡IO異常Socket socket = new Socket("127.0.0.1", 8888); // 創建 Socket 對象,連接本地 IP 的 8888 端口(連接服務端)// 發送消息PrintWriter writer = new PrintWriter( // 創建輸出流 writer,用于向服務端發送數據socket.getOutputStream(), true); // 獲取 socket 的輸出流,true 表示自動刷新緩沖區writer.println("你好,我是客戶端"); // 向服務端發送一行字符串// 讀取回應BufferedReader reader = new BufferedReader( // 創建輸入流 reader,用于讀取服務端返回的數據new InputStreamReader(socket.getInputStream())); // 將字節輸入流包裝成字符輸入流,再用 BufferedReader 包裝方便讀取String response = reader.readLine(); // 讀取服務端返回的一行文本(以換行符為結束標志)System.out.println("服務端回應: " + response); // 打印服務端返回的內容socket.close(); // 通信完成后關閉 socket 釋放資源}
}
5)多線程服務端(支持多個客戶端連接)
while (true) { // 無限循環,持續接收客戶端連接Socket socket = serverSocket.accept(); // 阻塞等待客戶端連接,一旦有客戶端連接就返回 Socketnew Thread(() -> { // 為每個連接創建一個新線程,避免阻塞主線程try {// 讀取客戶端發送的消息BufferedReader reader = new BufferedReader( // 創建 BufferedReader 包裝輸入流new InputStreamReader(socket.getInputStream())); // 從 socket 獲取輸入流,并轉為字符流String msg = reader.readLine(); // 讀取客戶端發送的一行消息System.out.println("客戶端消息:" + msg); // 打印客戶端發送的內容// 向客戶端發送回應PrintWriter writer = new PrintWriter( // 創建 PrintWriter 用于向客戶端輸出socket.getOutputStream(), true); // 獲取 socket 的輸出流,true 表示自動 flushwriter.println("服務端回應:" + msg); // 將收到的消息再返回給客戶端作為回應socket.close(); // 關閉連接,釋放資源} catch (IOException e) {e.printStackTrace(); // 異常處理,打印錯誤信息}}).start(); // 啟動新線程,異步處理該客戶端請求
}
1.2?UDP 通信原理與 Java 實現
1)UDP 特點
特性 | 描述 |
---|---|
無連接 | 不建立連接,直接發送數據 |
不可靠 | 可能丟包、亂序 |
面向報文 | 一次發送 = 一個數據報 |
快速高效 | 適合實時場景,如直播、游戲 |
2)通信模型
發送方:DatagramSocket.send(DatagramPacket)
接收方:DatagramSocket.receive(DatagramPacket)
3)UDP 發送端
import java.net.*; // 導入 Java 網絡通信相關類(DatagramSocket、DatagramPacket、InetAddress)public class UDPClient { // 定義一個名為 UDPClient 的類public static void main(String[] args) throws Exception { // 主函數,拋出所有異常DatagramSocket socket = new DatagramSocket(); // 創建 UDP 套接字,用于發送數據(系統自動分配端口)String msg = "Hello UDP Server"; // 要發送的字符串消息byte[] data = msg.getBytes(); // 將字符串消息轉換成字節數組(UDP 傳輸的是字節)InetAddress address = InetAddress.getByName("127.0.0.1"); // 獲取本機 IP 地址(目標地址)DatagramPacket packet = new DatagramPacket( // 創建 UDP 數據報(數據包)data, data.length, // 數據內容和數據長度address, 9090); // 目標 IP 和目標端口(即服務端的監聽端口)socket.send(packet); // 發送數據報System.out.println("發送完成"); // 控制臺打印發送成功socket.close(); // 關閉 socket,釋放資源}
}
4)UDP 接收端
import java.net.*; // 導入 Java 網絡通信相關類(DatagramSocket、DatagramPacket)public class UDPServer { // 定義一個名為 UDPServer 的類public static void main(String[] args) throws Exception { // 主函數,拋出所有異常DatagramSocket socket = new DatagramSocket(9090); // 創建 DatagramSocket 并綁定端口 9090,等待接收客戶端發送的 UDP 數據byte[] buffer = new byte[1024]; // 創建一個字節數組作為接收緩沖區,最大可接收 1024 字節數據DatagramPacket packet = new DatagramPacket(buffer, buffer.length); // 創建接收用的數據包對象,使用上述緩沖區socket.receive(packet); // 阻塞等待接收客戶端發送的數據包(接收到后將數據寫入 packet 中)String msg = new String(packet.getData(), 0, packet.getLength()); // 將接收到的字節數據轉為字符串(只取有效長度部分)System.out.println("收到客戶端信息:" + msg); // 打印接收到的客戶端消息內容socket.close(); // 關閉 socket,釋放綁定的端口資源}
}
1.3?TCP vs UDP 對比
特性 | TCP | UDP |
---|---|---|
是否連接 | 需要連接(三次握手) | 無連接 |
可靠性 | 可靠,保證順序、無丟包 | 不可靠,可能丟包、亂序 |
傳輸單位 | 字節流 | 數據報(Datagram) |
速度 | 較慢 | 快 |
場景 | 文件傳輸、網頁、數據庫連接 | 視頻流、語音、DNS、廣播等 |
二、URL、HttpURLConnection 請求發送
Java 的標準庫提供了 java.net.URL
和 java.net.HttpURLConnection
,它們是 HTTP 客戶端的核心類,用于從 Java 應用中發送 GET、POST 等請求,獲取 Web 服務器的響應數據。
2.1?URL 類:用于封裝鏈接地址
URL url = new URL("https://httpbin.org/get");
常用方法:
url.getProtocol(); // 返回 "https"
url.getHost(); // 返回 "httpbin.org"
url.getPath(); // 返回 "/get"
url.openConnection(); // 返回 URLConnection 對象(默認是 HttpURLConnection)
2.2?HttpURLConnection 詳解(發送請求的核心)
獲取連接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
設置請求方法(GET、POST 等)
conn.setRequestMethod("GET"); // 或 POST
設置連接參數(常用配置)
conn.setConnectTimeout(5000); // 連接超時時間
conn.setReadTimeout(5000); // 讀取超時時間
conn.setRequestProperty("User-Agent", "Java-HttpClient"); // 設置請求頭
2.3?GET 請求示例(帶參數)
import java.io.*; // 導入輸入輸出相關類(如 BufferedReader、InputStreamReader)
import java.net.*; // 導入網絡相關類(如 URL、HttpURLConnection)public class GetExample { // 定義一個名為 GetExample 的類public static void main(String[] args) throws Exception { // 主函數,拋出所有異常以簡化處理String params = "name=test&age=22"; // 定義 GET 請求的參數字符串(URL 編碼格式)URL url = new URL("https://httpbin.org/get?" + params); // 創建 URL 對象,將參數拼接到 URL 后面(GET 請求通過 URL 傳參)HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 打開連接,并強制轉換為 HttpURLConnection 對象conn.setRequestMethod("GET"); // 設置請求方法為 GET(默認其實就是 GET,但寫清楚更規范)BufferedReader reader = new BufferedReader( // 創建字符緩沖輸入流,用于讀取響應內容new InputStreamReader(conn.getInputStream())); // 從連接中獲取輸入流(即響應體內容)String line; // 定義每行讀取的臨時變量while ((line = reader.readLine()) != null) { // 逐行讀取響應內容,直到為 null(即讀完)System.out.println(line); // 輸出當前行內容到控制臺}reader.close(); // 關閉讀取流,釋放資源conn.disconnect(); // 斷開 HTTP 連接,釋放網絡資源}
}
2.4?POST 請求示例(表單提交)
import java.io.*; // 導入輸入輸出相關類
import java.net.*; // 導入網絡相關類public class PostExample { // 定義 PostExample 類public static void main(String[] args) throws Exception { // 主函數,拋出異常URL url = new URL("https://httpbin.org/post"); // 創建目標 URL,指向 httpbin 的 POST 測試接口HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 打開 HTTP 連接conn.setRequestMethod("POST"); // 設置請求方法為 POSTconn.setDoOutput(true); // 允許向連接寫入請求體(必須設置)conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // 設置請求頭,表明發送的是表單數據// 寫入請求參數(表單格式)String params = "username=test&password=123456"; // POST 參數,URL 編碼格式字符串OutputStream os = conn.getOutputStream(); // 獲取連接的輸出流os.write(params.getBytes()); // 將參數轉換成字節流寫入請求體os.flush(); // 刷新緩沖區,確保所有數據發送出去os.close(); // 關閉輸出流// 讀取服務器響應BufferedReader reader = new BufferedReader( // 包裝輸入流,方便按行讀取響應內容new InputStreamReader(conn.getInputStream())); // 獲取連接的輸入流(響應體)String line;while ((line = reader.readLine()) != null) { // 循環讀取每一行System.out.println(line); // 打印響應內容到控制臺}reader.close(); // 關閉輸入流conn.disconnect(); // 斷開 HTTP 連接,釋放資源}
}
2.5?設置請求頭(模擬瀏覽器)
常用請求頭設置:
conn.setRequestProperty("User-Agent", "Mozilla/5.0");
conn.setRequestProperty("Referer", "https://example.com/");
conn.setRequestProperty("Cookie", "session=abc123; token=xyz");
2.6?獲取響應狀態與頭部信息
int code = conn.getResponseCode(); // 200、302、404...
String contentType = conn.getHeaderField("Content-Type");
String setCookie = conn.getHeaderField("Set-Cookie");
2.7?處理 gzip 壓縮響應
某些服務器返回的數據是壓縮的,需解壓讀取:
InputStream in = conn.getInputStream(); // 從 HttpURLConnection 獲取響應的輸入流(原始數據流)
String encoding = conn.getContentEncoding(); // 獲取服務器響應頭中的 Content-Encoding 字段(告訴你數據是否壓縮)if ("gzip".equalsIgnoreCase(encoding)) { // 判斷響應內容是否使用 gzip 壓縮(忽略大小寫比較)in = new GZIPInputStream(in); // 如果是 gzip,則用 GZIPInputStream 對流進行解壓處理
}BufferedReader reader = new BufferedReader( // 用 BufferedReader 包裝輸入流,方便逐行讀取字符數據new InputStreamReader(in)); // InputStreamReader 把字節流轉換成字符流,結合編碼使用
2.8?完整通用封裝(支持 GET/POST)
public static String send(String urlStr, String method, String body, Map<String, String> headers) throws IOException {URL url = new URL(urlStr); // 創建 URL 對象,傳入請求地址字符串HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 打開 HTTP 連接并強制轉換為 HttpURLConnectionconn.setRequestMethod(method); // 設置請求方法(GET、POST 等)conn.setConnectTimeout(5000); // 設置連接超時時間(5秒)conn.setReadTimeout(5000); // 設置讀取響應超時時間(5秒)if (headers != null) { // 如果傳入了請求頭集合for (Map.Entry<String, String> entry : headers.entrySet()) { // 遍歷所有請求頭conn.setRequestProperty(entry.getKey(), entry.getValue()); // 設置請求頭鍵值對}}if ("POST".equalsIgnoreCase(method) && body != null) { // 如果是 POST 請求且請求體不為空conn.setDoOutput(true); // 允許向連接寫入數據(必須)OutputStream os = conn.getOutputStream(); // 獲取輸出流os.write(body.getBytes()); // 將請求體寫入輸出流(默認字符集)os.close(); // 關閉輸出流,完成請求體寫入}BufferedReader reader = new BufferedReader( // 讀取響應輸入流,方便按行讀取文本new InputStreamReader(conn.getInputStream())); // 從連接獲取響應流并轉換成字符流StringBuilder sb = new StringBuilder(); // 創建 StringBuilder 用于拼接響應內容String line;while ((line = reader.readLine()) != null) { // 循環逐行讀取響應sb.append(line).append("\n"); // 將每行添加到 StringBuilder 并換行}reader.close(); // 關閉讀取流,釋放資源conn.disconnect(); // 斷開 HTTP 連接return sb.toString(); // 返回拼接好的完整響應字符串
}
2.9?HttpURLConnection 使用建議
使用點 | 建議與說明 |
---|---|
多次請求同一主機 | 推薦使用 Apache HttpClient(連接池) |
請求體為 JSON | 設置 Content-Type: application/json 并用 Writer 傳入 JSON 字符串 |
遇到跳轉(302) | 默認不自動重定向,需要手動處理 |
設置代理抓包調試 | 可使用 System.setProperty("http.proxyHost", "127.0.0.1") |
異常處理 | 捕獲 IOException , MalformedURLException |
2.10?常見問題
問題 | 原因 |
---|---|
請求超時 | 網絡不可達、超時時間太短 |
POST 參數無效 | setDoOutput(true) 未設置 |
響應亂碼 | 編碼未設對,建議用 UTF-8 |
302 重定向失敗 | HttpURLConnection 默認不跟隨 POST 重定向 |
2.11 小結
URL url = new URL("...");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET/POST");
conn.setRequestProperty(...);
conn.getOutputStream().write(...); // POST 情況
InputStream in = conn.getInputStream(); // 響應讀取
conn.disconnect();
三、ServerSocket 創建服務端監聽
ServerSocket
是 Java 網絡編程中用于實現 TCP 服務端監聽的類。它能在指定端口持續監聽客戶端連接,一旦有連接到達,就會生成一個 Socket
對象,用于與該客戶端進行后續的通信。
3.1?核心概念:TCP 通信三步走
1. 服務端創建 ServerSocket(綁定端口)
2. 客戶端發起連接 Socket.connect()
3. 服務端 accept() 接收連接,返回 Socket
3.2?ServerSocket 類常用方法
方法 | 作用 |
---|---|
new ServerSocket(port) | 創建監聽端口 |
accept() | 阻塞式等待客戶端連接 |
getInetAddress() | 獲取連接地址 |
close() | 關閉服務端監聽 |
3.3?最小運行示例(單客戶端)
import java.io.*; // 導入輸入輸出相關類(BufferedReader、InputStreamReader、PrintWriter 等)
import java.net.*; // 導入網絡相關類(ServerSocket、Socket 等)public class SimpleServer { // 定義一個名為 SimpleServer 的類public static void main(String[] args) throws IOException { // 主函數,拋出 IOException 以處理網絡通信異常ServerSocket serverSocket = new ServerSocket(8888); // 創建服務器監聽套接字,綁定端口 8888System.out.println("服務端啟動,監聽端口 8888"); // 控制臺提示服務已啟動Socket socket = serverSocket.accept(); // 阻塞等待客戶端連接(連接成功返回 Socket 對象)System.out.println("客戶端已連接:" + socket.getInetAddress()); // 打印連接的客戶端 IP 地址BufferedReader reader = new BufferedReader( // 創建字符緩沖輸入流,讀取客戶端發來的數據new InputStreamReader(socket.getInputStream())); // 獲取輸入流并轉為字符流String msg = reader.readLine(); // 讀取一行數據(以換行符為結束標志)System.out.println("收到消息:" + msg); // 打印客戶端消息內容PrintWriter writer = new PrintWriter( // 創建輸出流,用于發送數據給客戶端socket.getOutputStream(), true); // 第二個參數 true 表示自動刷新(無需手動 flush)writer.println("服務端收到你的消息啦"); // 向客戶端發送響應socket.close(); // 通信結束后關閉客戶端連接serverSocket.close(); // 關閉服務端監聽端口,釋放資源}
}
3.4?配套客戶端(測試使用)
import java.io.*; // 導入輸入輸出相關類(PrintWriter、BufferedReader 等)
import java.net.*; // 導入網絡通信相關類(Socket)public class SimpleClient { // 定義客戶端類 SimpleClientpublic static void main(String[] args) throws IOException { // 主函數,拋出 IOException 異常(用于網絡通信處理)Socket socket = new Socket("127.0.0.1", 8888); // 創建一個 Socket,連接本機 IP 的 8888 端口(服務端)PrintWriter writer = new PrintWriter( // 創建字符輸出流,向服務端發送數據socket.getOutputStream(), true); // 獲取 socket 的輸出流,true 表示自動刷新緩沖區writer.println("你好,我是客戶端!"); // 發送一行文本數據給服務端BufferedReader reader = new BufferedReader( // 創建字符輸入流,用于讀取服務端返回的數據new InputStreamReader(socket.getInputStream())); // 獲取 socket 的輸入流,并轉為字符流String response = reader.readLine(); // 讀取服務端返回的一行字符串System.out.println("服務端回應:" + response); // 打印服務端返回的消息socket.close(); // 關閉 socket 連接,釋放資源}
}
3.5?支持多客戶端連接(多線程服務端)
一個服務端接收多個客戶端,必須使用 多線程 處理每個連接。
import java.io.*; // 導入輸入輸出相關類(BufferedReader、PrintWriter 等)
import java.net.*; // 導入網絡通信相關類(ServerSocket、Socket 等)public class MultiClientServer { // 主類:服務端public static void main(String[] args) throws IOException { // 主方法,拋出 IOException(用于處理網絡通信錯誤)ServerSocket serverSocket = new ServerSocket(8888); // 創建服務器監聽套接字,監聽端口 8888System.out.println("服務端啟動,監聽端口 8888"); // 提示服務端已啟動while (true) { // 無限循環,持續接受客戶端連接Socket client = serverSocket.accept(); // 阻塞等待客戶端連接,連接成功返回 Socketnew Thread(new ClientHandler(client)).start(); // 每一個客戶端連接都啟動一個新線程處理(多線程處理并發)}}
}class ClientHandler implements Runnable { // 定義客戶端處理器類,實現 Runnable 接口(可被線程執行)private Socket socket; // 每個對象持有一個客戶端連接 Socketpublic ClientHandler(Socket socket) { // 構造函數,接收客戶端 Socket 并賦值this.socket = socket;}public void run() { // run 方法是線程執行的主體try {System.out.println("連接客戶端:" + socket.getInetAddress()); // 打印客戶端 IP 地址BufferedReader reader = new BufferedReader( // 獲取輸入流,用于讀取客戶端發來的數據new InputStreamReader(socket.getInputStream()));String msg = reader.readLine(); // 讀取客戶端發來的消息(阻塞直到有數據)System.out.println("收到消息:" + msg); // 打印收到的消息PrintWriter writer = new PrintWriter( // 獲取輸出流,用于回應客戶端socket.getOutputStream(), true);writer.println("收到你的消息啦,線程ID:" + Thread.currentThread().getId()); // 回復內容中加入當前線程 IDsocket.close(); // 通信完成后關閉 Socket 連接} catch (IOException e) {e.printStackTrace(); // 異常打印(例如連接斷開時)}}
}
3.6?ServerSocket 構造函數詳解
new ServerSocket(port); // 默認本地地址監聽
new ServerSocket(port, backlog); // 指定連接隊列長度
new ServerSocket(port, backlog, InetAddress); // 指定本地 IP(如多網卡)
-
port
: 要監聽的端口(0~65535,推薦 >= 1024) -
backlog
: 同時等待連接的最大數量(默認50) -
InetAddress
: 指定綁定地址(如綁定公網IP)
3.7?服務端監聽常見應用場景
場景 | 實現思路 |
---|---|
HTTP Web 服務模擬 | 客戶端發送 HTTP 請求報文,服務端解析并構造響應 |
聊天室服務端 | 多客戶端連接,廣播消息給所有人 |
命令控制接口 | 服務端接受控制指令,執行并返回結果 |
文件傳輸服務 | 客戶端上傳/下載文件 |
3.8?服務端監聽與請求結構(HTTP 示例)
客戶端請求:
GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.58.0服務端可讀取:
GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.58.0
服務端可返回:
HTTP/1.1 200 OK
Content-Type: text/plainHello from Server
3.9?常見錯誤排查
錯誤 | 說明 |
---|---|
BindException: Address already in use | 端口被占用 |
ConnectException: Connection refused | 客戶端找不到服務端 |
SocketException: Socket closed | 連接未正常關閉 |
EOFException | 客戶端提前關閉連接 |
3.10 小結
ServerSocket server = new ServerSocket(8888);while (true) {Socket client = server.accept(); // 阻塞接收連接new Thread(() -> {InputStream in = client.getInputStream();OutputStream out = client.getOutputStream();// 處理請求 + 返回響應}).start();
}
四、底層協議包抓包分析
抓包(Packet Sniffing)是逆向分析、協議還原、爬蟲繞過、參數定位的核心技能之一。
抓包可以:
-
查看真實請求頭/參數/內容
-
分析加密數據的位置和結構
-
識別 Cookie / Token / Headers
-
還原移動 APP / 小程序 / 加密 JS 的真實通信行為
4.1?抓包工具對比
工具 | 特點 | 適用場景 |
---|---|---|
Wireshark | 抓底層 TCP/IP 包,包含所有協議 | TCP/UDP/SSL 分析,低層協議 |
mitmproxy | 抓取 HTTPS 應用層請求,支持中間人證書 | 抓取 HTTP/HTTPS 請求、反爬分析 |
Fiddler | Windows 專用 GUI 抓包工具 | 類似 mitmproxy,圖形界面 |
Charles | 跨平臺 GUI 抓包工具 | 抓取手機/瀏覽器通信 |
mitmproxy
,它是 跨平臺 + 支持腳本分析 + 免費 + 支持 CLI/GUI 的強力工具。
4.2?mitmproxy 抓包工具安裝與使用
1)安裝(需 Python 環境)
pip install mitmproxy
2)啟動 Web GUI 界面(推薦)
mitmweb
默認監聽 127.0.0.1:8080
,并打開 Web UI 界面(http://127.0.0.1:8081)
4.3?mitmproxy 實現“中間人”原理
[Java程序/瀏覽器] →→ mitmproxy(偽裝服務器) →→ 目標網站mitmproxy 攔截請求并生成“偽造證書”
HTTPS 抓包必須讓系統或瀏覽器信任 mitmproxy 的證書。
4.4?mitmproxy 證書安裝(用于抓取 HTTPS)
手機抓包(Android):
-
手機設置代理為:WiFi → 高級 → HTTP代理 → 手動(填入
電腦IP:8080
) -
手機瀏覽器訪問:
http://mitm.it
-
下載 Android 根證書并安裝(系統或用戶證書)
電腦抓包(Java 應用):
Java 默認不信任 mitmproxy 的證書。可用兩種方式解決:
方法 1:禁用 SSL 驗證(開發調試可用)
import javax.net.ssl.*; // 引入 Java 提供的 SSL 通信相關的類和接口public class SSLBypass {static {try {// 創建一個“信任所有證書”的 TrustManager 數組TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {} // 不檢查客戶端證書public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {} // 不檢查服務端證書public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } // 返回 null 表示接受所有}};// 初始化一個 SSLContext,使用上面的“信任所有證書”的 TrustManagerSSLContext sc = SSLContext.getInstance("SSL");sc.init(null, trustAllCerts, new java.security.SecureRandom());// 將默認的 SSLSocketFactory 替換成上面這個“信任所有證書”的版本HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());// 創建并設置一個“信任所有主機名”的 HostnameVerifier(域名不匹配也通過)HostnameVerifier hv = (hostname, session) -> true;HttpsURLConnection.setDefaultHostnameVerifier(hv);} catch (Exception e) {e.printStackTrace(); // 捕獲并打印異常(一般不會觸發)}}
}
方法 2:導入 mitmproxy 證書到 Java 信任庫
# 導出 mitmproxy 證書(例如下載到 mitmproxy-ca-cert.pem)
openssl x509 -inform PEM -in mitmproxy-ca-cert.pem -out mitmproxy.crt# 導入到 JDK 信任庫
keytool -import -trustcacerts -alias mitmproxy -file mitmproxy.crt -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit
4.5?Java 設置代理(讓請求流經 mitmproxy)
設置系統代理(全局)
System.setProperty("http.proxyHost", "127.0.0.1");
System.setProperty("http.proxyPort", "8080");
System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("https.proxyPort", "8080");
設置單次代理(推薦):
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8080));
HttpURLConnection conn = (HttpURLConnection) new URL("https://httpbin.org/get").openConnection(proxy);
4.6?實際抓包分析內容
一旦 Java 程序流量經過 mitmproxy,將在 mitmweb 中看到:
抓到的內容 | 說明 |
---|---|
請求方法與地址 | GET /api/login |
請求頭(Headers) | User-Agent、Cookie、Referer |
請求體(POST body) | 登錄用戶名密碼、form 表單內容 |
響應內容 | HTML 頁面、JSON 數據、狀態碼 |
響應 Set-Cookie | 登錄狀態維持的重要標志 |
4.7?使用 mitmproxy 腳本進一步分析/修改流量
創建 modify_request.py
:
def request(flow):if flow.request.pretty_url.startswith("https://target.com/api"):print("請求參數:", flow.request.text)flow.request.headers["X-Intercepted"] = "True"
運行:
mitmdump -s modify_request.py
4.8?示意流程圖
[ Java應用 ]↓ 代理設置 (127.0.0.1:8080)
[ mitmproxy ] ← 攔截 HTTPS 流量,解密數據↓
[ 真實服務器 ]
4.9 小結
步驟 | 工具/方式 |
---|---|
抓取網頁或 APP 的請求 | mitmproxy、Fiddler、Charles |
設置 Java 代理 | Proxy 類 或 System.setProperty |
證書處理(HTTPS) | 導入證書 / 忽略驗證 |
分析參數結構 | Headers、Form、Cookie、URL 參數 |
還原請求 | Java 模擬 GET/POST,構造對應參數 |
五、模擬登錄、表單提交、cookie 操作
真實網站很多頁面必須登錄后才能訪問,比如:
-
用戶中心、購物車、訂單頁
-
發帖、點贊、評論等操作
-
管理后臺數據采集
所以需要學會:
? 模擬登錄 ? 模擬表單提交 ? 維持 Cookie ? 訪問受限頁面
真實網站登錄流程簡化圖
1. GET 登錄頁面(拿 Cookie + token)
2. POST 表單提交(用戶名+密碼+token)
3. 響應中返回 Set-Cookie(登錄成功)
4. 后續請求需帶 Cookie
5.1?實戰步驟詳解(Java 實現)
Step 1:請求登錄頁(獲取初始 Cookie)
URL loginPageUrl = new URL("https://example.com/login"); // 創建一個 URL 對象,指向登錄頁面地址
HttpURLConnection conn = (HttpURLConnection) loginPageUrl.openConnection(); // 打開連接,強制轉換為 HttpURLConnection
conn.setRequestProperty("User-Agent", "Mozilla/5.0"); // 設置請求頭中的 User-Agent,模擬瀏覽器訪問String setCookie = conn.getHeaderField("Set-Cookie"); // 獲取響應頭中名為 Set-Cookie 的字段(服務器下發的 Cookie)
System.out.println("初始 Cookie:" + setCookie); // 打印獲取到的 Cookie 字符串
Step 2:構造 POST 請求,提交表單(模擬用戶登錄)
URL loginAction = new URL("https://example.com/doLogin"); // 構造登錄請求的 URL(POST 接口地址)
HttpURLConnection loginConn = (HttpURLConnection) loginAction.openConnection(); // 打開連接并強轉為 HttpURLConnection
loginConn.setRequestMethod("POST"); // 設置請求方法為 POST
loginConn.setDoOutput(true); // 允許向請求體寫數據(POST 必須)// 偽裝成瀏覽器請求,設置常見請求頭
loginConn.setRequestProperty("User-Agent", "Mozilla/5.0"); // 模擬瀏覽器的 User-Agent
loginConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // 設置請求體內容類型為表單格式
loginConn.setRequestProperty("Cookie", setCookie); // 附帶之前獲取的 Cookie(比如登錄頁返回的 Cookie)// 構造 POST 表單內容,注意參數需要 URL 編碼
String body = "username=admin&password=123456"; // 登錄參數(賬號密碼)
OutputStream os = loginConn.getOutputStream(); // 獲取連接的輸出流
os.write(body.getBytes()); // 將表單參數寫入請求體(默認編碼)
os.flush(); // 刷新緩沖區,確保數據發送出去
os.close(); // 關閉流// 讀取服務器返回的新 Cookie(登錄成功后通常會更新會話 Cookie)
String loginCookie = loginConn.getHeaderField("Set-Cookie"); // 獲取響應頭中新的 Set-Cookie
System.out.println("登錄后的 Cookie:" + loginCookie); // 打印登錄后返回的 Cookie 字符串
Step 3:訪問登錄后的頁面(帶上 Cookie)
URL userCenter = new URL("https://example.com/user/home"); // 構造用戶主頁 URL 地址
HttpURLConnection userConn = (HttpURLConnection) userCenter.openConnection(); // 打開連接并轉為 HttpURLConnection
userConn.setRequestProperty("User-Agent", "Mozilla/5.0"); // 設置 User-Agent 模擬瀏覽器請求
userConn.setRequestProperty("Cookie", loginCookie); // 攜帶登錄時服務器返回的 Cookie,保持登錄狀態BufferedReader reader = new BufferedReader( // 創建緩沖字符輸入流,讀取響應內容new InputStreamReader(userConn.getInputStream())); // 獲取連接的輸入流,讀取響應體
String line;
while ((line = reader.readLine()) != null) { // 按行讀取響應內容,直到讀完System.out.println(line); // 打印每一行到控制臺
}
reader.close(); // 關閉輸入流,釋放資源
5.2?Cookie 詳解(登錄狀態維持)
-
服務端登錄成功后返回
Set-Cookie
-
后續所有請求都需要帶這個
Cookie
否則會被認為是“未登錄” -
Java 中可以:
-
手動設置 Cookie
-
或使用
CookieManager
自動管理多個 Cookie
-
5.3?完整封裝工具類
public class HttpUtils { // 定義 HttpUtils 工具類,封裝 HTTP 登錄和頁面請求功能public static String loginAndGetCookie(String loginUrl, String params, String initCookie) throws IOException {URL url = new URL(loginUrl); // 創建登錄接口的 URL 對象HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 打開連接,強制轉換為 HttpURLConnectionconn.setRequestMethod("POST"); // 設置請求方法為 POST,表示發送數據conn.setDoOutput(true); // 允許向連接輸出數據(寫請求體)conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // 設置請求體格式為表單格式conn.setRequestProperty("User-Agent", "Mozilla/5.0"); // 設置請求頭 User-Agent,模擬瀏覽器訪問if (initCookie != null) // 如果有傳入初始 Cookie,則設置 Cookie 請求頭conn.setRequestProperty("Cookie", initCookie);OutputStream os = conn.getOutputStream(); // 獲取連接的輸出流,用于寫入請求體數據os.write(params.getBytes()); // 將登錄參數(字符串)寫入請求體,默認編碼os.close(); // 關閉輸出流,結束請求體寫入return conn.getHeaderField("Set-Cookie"); // 從響應頭獲取服務器返回的 Set-Cookie,返回給調用方}public static String getPage(String urlStr, String cookie) throws IOException {URL url = new URL(urlStr); // 創建要訪問頁面的 URL 對象HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 打開連接,強轉為 HttpURLConnectionconn.setRequestProperty("User-Agent", "Mozilla/5.0"); // 設置請求頭 User-Agent,模擬瀏覽器訪問if (cookie != null) // 如果傳入 Cookie,設置 Cookie 請求頭,帶上登錄態conn.setRequestProperty("Cookie", cookie);BufferedReader reader = new BufferedReader( // 創建緩沖字符輸入流,方便逐行讀取響應體new InputStreamReader(conn.getInputStream())); // 獲取響應輸入流并轉換成字符流StringBuilder sb = new StringBuilder(); // 創建 StringBuilder 用于拼接讀取的頁面內容String line; // 定義變量存儲每行讀取的字符串while ((line = reader.readLine()) != null) // 循環讀取響應的每一行,直到結束sb.append(line).append("\n"); // 將每行內容添加到 StringBuilder 并換行return sb.toString(); // 返回拼接完成的完整頁面內容字符串}
}
5.4?如何分析真實網站的登錄行為?
借助瀏覽器/mitmproxy 抓包:
Chrome:
-
打開 DevTools → Network → login 接口
-
查看:
-
請求方法(POST/GET)
-
表單字段名(username/password/captcha/token)
-
Headers(Referer/User-Agent/Cookie)
-
響應中的 Set-Cookie
-
mitmproxy:
抓 HTTPS 全流量,分析:
-
真實請求體參數
-
登錄是否加密(sign/token)
-
Cookie 是登錄成功的關鍵
5.5?應對驗證碼、token、重定向、加密參數
問題 | 處理方法 |
---|---|
登錄有驗證碼 | 用 Python 識別圖形驗證碼 / OCR + 人工輔助 |
表單中有 CSRF token | 抓登錄頁 HTML,提取隱藏字段 name="csrf_token" |
登錄成功后 302 | 跟蹤 Location 頭,發起二次請求 |
有 JS 加密參數 | 需逆向 JS 算法,還原加密邏輯(推薦 mitmproxy + 調試) |
5.6 小結
模擬登錄 = 抓登錄請求結構 + 提交表單字段 + 解析返回Cookie + 維持 Cookie 請求受限頁
如果要做自動化平臺、信息采集或爬蟲登錄:
-
封裝好 Cookie + 登錄邏輯類
-
實現 Cookie 自動保存(本地文件/數據庫)
-
搭配驗證碼識別或逆向 JS 還原參數