SSL Socket 通信与本地 Mock Server 实践指南
一、背景概念
1.1 什么是 Socket 通信
Socket(套接字)是网络通信的基础抽象。两个程序通过 Socket 建立连接后,可以像读写文件一样发送和接收数据。
与 HTTP 的区别:
| 对比项 | HTTP | 原生 Socket |
|---|---|---|
| 协议层次 | 应用层协议(基于 TCP) | 传输层直接通信(TCP) |
| 数据格式 | 有固定的请求/响应格式(Header + Body) | 无固定格式,双方自行约定报文结构 |
| 连接方式 | 短连接为主(请求-响应后断开) | 可以长连接或短连接 |
| 工具支持 | Postman、curl 等现成工具 | 需要自己编写客户端/服务端程序 |
| 典型场景 | Web API、微服务 | 银行/金融系统、硬件通信、私有协议 |
1.2 什么是 SSL/TLS
SSL(Secure Sockets Layer)和 TLS(Transport Layer Security)是加密通信协议,用来保证数据在网络传输中不被窃听和篡改。TLS 是 SSL 的升级版本,当前主流使用 TLS 1.2 或 TLS 1.3。
通信过程简化理解:
客户端 服务端
| |
|-- 1. 发起连接(ClientHello) ------->|
|<-- 2. 返回证书(ServerHello) ------|
|-- 3. 验证证书是否可信 ------------> |
|-- 4. 协商加密密钥 ---------------> |
|<==== 5. 加密通道建立 ============> |
|-- 6. 发送业务数据(加密的)-------> |
|<-- 7. 接收响应数据(加密的)------|
| |
关键概念:
- 证书(Certificate):服务端的"身份证",包含公钥信息,由 CA(证书颁发机构)签发
- 信任库(TrustStore):客户端存放"我信任的证书"的容器。客户端收到服务端证书后,会检查它是否在信任库中
- 密钥库(KeyStore):服务端存放"自己的私钥和证书"的容器,用来证明自己的身份
1.3 什么是 JKS
JKS(Java KeyStore)是 Java 特有的密钥库文件格式,用来存储:
- 私钥 + 对应的证书链(服务端用)
- 信任的第三方证书(客户端用)
一个 .jks 文件就像一个加密的"保险箱",里面可以存多把"钥匙"(按 alias 别名区分),打开保险箱需要密码。
1.4 通信模型总结
┌─────────────┐ ┌─────────────┐
│ 客户端 │ │ 服务端 │
│ │ │ │
│ TrustStore │ ←验证证书是否可信→ │ KeyStore │
│ (ylh.jks) │ │(server.jks) │
│ │ │ │
│ SSLSocket │ ====== TLS 1.2 ===== │SSLServerSocket│
│ │ │ │
│ 发送报文 │ ------TCP数据流----→ │ 接收报文 │
│ 接收响应 │ ←----TCP数据流------ │ 发送响应 │
└─────────────┘ └─────────────┘
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、报文协议格式
2.1 自定义协议结构
由于是私有协议,双方约定了固定的报文格式:
[外层前缀 18字节][内层报文头 10+141=151字节][XML业务数据][MD5校验和 32字节]
\_____________________169字节____________________/
客户端收到响应后的解析逻辑:
java
String response = rawData.substring(169, rawData.length() - 32);
// 跳过前169字节报文头,去掉最后32字节校验和,中间就是XML
2.2 报文各部分说明
外层前缀(18字节):
| 位置 | 长度 | 内容 | 说明 |
|---|---|---|---|
| 1-3 | 3 | "BADAO" | 固定标识 |
| 4-8 | 5 | "1001" | 系统编码 |
| 9-18 | 10 | "0000000xxx" | 后续内容总长度(补零到10位) |
内层长度(10字节):
| 位置 | 长度 | 内容 | 说明 |
|---|---|---|---|
| 19-28 | 10 | "0000000xxx" | 内层报文体总长度(补零到10位) |
内层报文头(141字节):
| 位置 | 长度 | 字段名 | 示例值 |
|---|---|---|---|
| 1-3 | 3 | 业务类型 | "TEST" |
| 4 | 1 | 报文版本 | "1" |
| 5-9 | 5 | 发起系统编码 | "1001" |
| 10-14 | 5 | 目标系统编码 | "1002" |
| 15-18 | 4 | 交易类型 | "001" |
| 19-20 | 2 | 操作类型 | "02" |
| 21-22 | 2 | 报文编码 | "01" |
| 23-24 | 2 | 通讯协议 | "01" |
| 25-26 | 2 | 数据协议 | "01" |
| 27-46 | 20 | 交易流水号 | "12345678901234567890" |
| 47-56 | 10 | XML报文长度 | "0000000350" |
| 57-70 | 14 | 交易时间 | "20260608120000" |
| 71-72 | 2 | 请求类型 | "01"请求/"02"响应 |
| 73-88 | 16 | 交易签名 | MD5签名截取 |
| 89-91 | 3 | 附件个数 | "000" |
| 92-141 | 50 | 预留位 | 全"0" |
XML业务数据(变长):
xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<scf>
<header>
<txId>交易流水号</txId>
<txTime>交易时间</txTime>
<rtCode>状态码</rtCode>
<message>错误信息</message>
</header>
<body>
<!-- 业务数据 -->
</body>
</scf>
MD5校验和(32字节):
对"内层报文头 + XML"部分做 MD5 哈希,得到 32 位十六进制大写字符串,附在最后用于校验数据完整性。
三、keytool 证书工具详解
keytool 是 JDK 自带的密钥和证书管理工具,用来创建和管理 JKS 文件。
3.1 常用命令
生成密钥对(创建 KeyStore):
bash
keytool -genkeypair \
-alias mykey \ # 别名,标识这对密钥
-keyalg RSA \ # 算法类型(RSA 最常用)
-keysize 2048 \ # 密钥长度(2048位足够安全)
-keystore server.jks \ # 输出的 JKS 文件名
-storepass 123456 \ # JKS 文件的密码
-dname "CN=localhost" \ # 证书主体信息(CN=域名)
-validity 365 # 证书有效期(天)
导出证书:
bash
keytool -exportcert \
-alias mykey \ # 要导出的别名
-keystore server.jks \ # 从哪个 JKS 导出
-storepass 123456 \ # JKS 密码
-file server.cer # 导出的证书文件
导入证书到信任库:
bash
keytool -importcert \
-alias mykey \ # 导入后在信任库中的别名
-keystore client.jks \ # 客户端的信任库文件
-storepass 654321 \ # 信任库密码
-file server.cer \ # 要导入的证书文件
-noprompt # 不询问直接导入
查看 JKS 中有哪些证书:
bash
keytool -list \
-keystore server.jks \
-storepass 123456
3.2 dname 参数说明
-dname 指定证书的主体信息(Distinguished Name),格式为:
CN=Common Name, OU=Organization Unit, O=Organization, L=Locality, ST=State, C=Country
本地测试时只需要 CN=localhost 或 CN=127.0.0.1。
四、完整独立示例(与业务无关)
以下是一个最简化的 SSL Socket 通信示例,包含服务端和客户端,演示整个 SSL 通信 + 自定义协议解析的过程。
4.1 项目结构
ssl-socket-demo/
├── generate-certs.bat # Windows 证书生成脚本
├── server.jks # 服务端密钥库(运行脚本后生成)
├── client.jks # 客户端信任库(运行脚本后生成)
├── SslServer.java # SSL 服务端
└── SslClient.java # SSL 客户端
4.2 证书生成脚本(generate-certs.bat)
bat
@echo off
echo === 生成 SSL 证书 ===
REM 1. 生成服务端密钥库
keytool -genkeypair -alias server -keyalg RSA -keysize 2048 -keystore server.jks -storepass changeit -dname "CN=localhost, OU=Demo, O=Demo, L=City, ST=State, C=CN" -validity 365
REM 2. 导出服务端证书
keytool -exportcert -alias server -keystore server.jks -storepass changeit -file server.cer
REM 3. 将服务端证书导入客户端信任库
keytool -importcert -alias server -keystore client.jks -storepass changeit -file server.cer -noprompt
REM 4. 清理证书文件
del server.cer
echo === 完成 ===
echo 生成了 server.jks(服务端用)和 client.jks(客户端用)
pause
4.3 SSL 服务端(SslServer.java)
java
import javax.net.ssl.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.MessageDigest;
/**
* SSL Socket 服务端示例.
* 监听端口,接收客户端请求,返回自定义协议格式的响应.
*/
public class SslServer {
private static final int PORT = 9999;
private static final String KEYSTORE_PATH = "server.jks";
private static final String KEYSTORE_PASS = "changeit";
public static void main(String[] args) throws Exception {
// 1. 加载服务端密钥库
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(KEYSTORE_PATH), KEYSTORE_PASS.toCharArray());
// 2. 初始化 KeyManager(管理服务端自己的证书和私钥)
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, KEYSTORE_PASS.toCharArray());
// 3. 创建 SSL 上下文
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(kmf.getKeyManagers(), null, null);
// 4. 创建 SSL Server Socket
SSLServerSocketFactory factory = sslContext.getServerSocketFactory();
SSLServerSocket serverSocket =
(SSLServerSocket) factory.createServerSocket(PORT);
serverSocket.setEnabledProtocols(new String[]{"TLSv1.2"});
System.out.println("SSL 服务端启动,监听端口: " + PORT);
// 5. 循环接收客户端连接
while (true) {
java.net.Socket client = serverSocket.accept();
System.out.println("客户端已连接: " + client.getInetAddress());
InputStream input = client.getInputStream();
OutputStream output = client.getOutputStream();
// 读取请求
byte[] buf = new byte[4096];
int len = input.read(buf);
String request = new String(buf, 0, len, StandardCharsets.UTF_8);
System.out.println("收到请求: " + request);
// 构造响应(自定义协议格式)
String xmlBody = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<response>"
+ "<code>200</code>"
+ "<message>Hello from SSL Server</message>"
+ "<data>This is the response data</data>"
+ "</response>";
String fullResponse = buildProtocolMessage(xmlBody);
System.out.println("发送响应,总长度: " + fullResponse.length());
output.write(fullResponse.getBytes(StandardCharsets.UTF_8));
output.flush();
client.close();
}
}
/**
* 按自定义协议封装报文.
* 格式: [前缀18字节][内层长度10字节][报文头141字节][XML][MD5校验32字节]
* 总报文头 = 18 + 10 + 141 = 169字节
*/
private static String buildProtocolMessage(String xml) throws Exception {
// 141字节报文头(简化版,用固定值填充)
StringBuilder header = new StringBuilder();
header.append("BIZ"); // 业务类型 3位
header.append("1"); // 版本 1位
header.append("SRVID"); // 发起系统 5位
header.append("CLTID"); // 目标系统 5位
header.append("0001"); // 交易类型 4位
header.append("02"); // 操作类型 2位
header.append("01"); // 报文编码 2位
header.append("01"); // 通讯协议 2位
header.append("01"); // 数据协议 2位
header.append("00000000000000000001"); // 流水号 20位
header.append(String.format("%010d",
xml.getBytes(StandardCharsets.UTF_8).length)); // XML长度 10位
header.append("20260609120000"); // 时间 14位
header.append("02"); // 响应标识 2位
header.append("ABCDEF0123456789"); // 签名 16位
header.append("000"); // 附件数 3位
// 预留50位
header.append(
"00000000000000000000000000000000000000000000000000");
// 以上合计 141 字符
// 内层 = 报文头 + XML
String innerContent = header.toString() + xml;
// MD5校验和(32字符)
String checkSum = md5(innerContent.getBytes(StandardCharsets.UTF_8));
String innerWithChecksum = innerContent + checkSum;
// 内层长度(10位)
String innerLengthStr = String.format("%010d",
innerWithChecksum.getBytes(StandardCharsets.UTF_8).length);
// 外层 = "ENA" + 系统编码5位 + 外层长度10位 + 内层长度 + 内层内容
String innerMsg = innerLengthStr + innerWithChecksum;
String outerLengthStr = String.format("%010d",
innerMsg.getBytes(StandardCharsets.UTF_8).length);
return "ENA" + "SRVID" + outerLengthStr + innerMsg;
}
private static String md5(byte[] data) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(data);
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02X", 0xFF & b));
}
return sb.toString();
}
}
4.4 SSL 客户端(SslClient.java)
java
import javax.net.ssl.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
/**
* SSL Socket 客户端示例.
* 连接服务端,发送请求,接收并解析自定义协议格式的响应.
*/
public class SslClient {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 9999;
private static final String TRUSTSTORE_PATH = "client.jks";
private static final String TRUSTSTORE_PASS = "changeit";
public static void main(String[] args) throws Exception {
// 1. 加载客户端信任库(包含服务端证书)
KeyStore ts = KeyStore.getInstance("JKS");
ts.load(new FileInputStream(TRUSTSTORE_PATH), TRUSTSTORE_PASS.toCharArray());
// 2. 初始化 TrustManager(验证服务端证书)
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ts);
// 3. 创建 SSL 上下文
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(null, tmf.getTrustManagers(), null);
// 4. 建立 SSL 连接
SSLSocketFactory factory = sslContext.getSocketFactory();
SSLSocket socket =
(SSLSocket) factory.createSocket(SERVER_HOST, SERVER_PORT);
System.out.println("已连接到服务端: " + SERVER_HOST + ":" + SERVER_PORT);
// 5. 发送请求
OutputStream output = socket.getOutputStream();
String request = "Hello, this is a test request from client";
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
System.out.println("已发送请求: " + request);
// 6. 接收响应
InputStream input = socket.getInputStream();
byte[] buf = new byte[4096];
StringBuilder sb = new StringBuilder();
int len;
while ((len = input.read(buf)) > 0) {
sb.append(new String(buf, 0, len, StandardCharsets.UTF_8));
if (len < buf.length) {
break;
}
}
String rawResponse = sb.toString();
System.out.println("收到响应,总长度: " + rawResponse.length());
// 7. 按协议解析响应
// 跳过前169字节报文头,去掉最后32字节校验和,中间是XML
String xml = rawResponse.substring(169, rawResponse.length() - 32);
System.out.println("解析出 XML:\n" + xml);
socket.close();
}
}
4.5 运行步骤
bash
# 1. 生成证书
generate-certs.bat
# 2. 编译
javac SslServer.java
javac SslClient.java
# 3. 启动服务端(新开一个终端窗口)
java SslServer
# 4. 运行客户端(另一个终端窗口)
java SslClient
4.6 预期输出
服务端:
SSL 服务端启动,监听端口: 9999
客户端已连接: /127.0.0.1
收到请求: Hello, this is a test request from client
发送响应,总长度: 452
客户端:
已连接到服务端: 127.0.0.1:9999
已发送请求: Hello, this is a test request from client
收到响应,总长度: 452
解析出 XML:
<?xml version="1.0" encoding="UTF-8"?><response><code>200</code><message>Hello from SSL Server</message><data>This is the response data</data></response>
五、为什么不能用 Postman 等 HTTP 工具测试
| 原因 | 说明 |
|---|---|
| 协议不同 | Postman 基于 HTTP 协议,而 Socket 通信是裸 TCP 数据流 |
| 报文格式不同 | HTTP 有固定的 GET /path HTTP/1.1 格式,Socket 私有协议是自定义的二进制/文本格式 |
| 连接方式不同 | HTTP 有握手过程(协议协商),Socket 直接建立 TCP + TLS 连接后收发数据 |
| 端口行为不同 | HTTP 服务端期望收到 HTTP 格式数据,收到私有格式会报错 |
所以测试这类接口只能:
- 写代码模拟(本文方案)
- 使用专业的 TCP 调试工具(如 Hercules、PacketSender)发送原始字节
- 抓包工具(Wireshark)观察数据
六、关键概念速查表
| 概念 | 一句话解释 |
|---|---|
| Socket | 程序之间网络通信的端点,类似"电话两端" |
| SSL/TLS | 在 Socket 上加一层加密,防窃听防篡改 |
| JKS | Java 的密钥/证书存储文件格式 |
| KeyStore | 存自己的私钥+证书,服务端用来证明身份 |
| TrustStore | 存信任的证书,客户端用来验证服务端身份 |
| keytool | JDK 自带的证书管理命令行工具 |
| Certificate | 数字证书,包含公钥和身份信息 |
| MD5 checksum | 报文完整性校验,防止数据被篡改 |
| 报文头 | 固定长度的元数据区域,描述报文类型和长度等信息 |
| JAXB | Java 的 XML 与对象互转框架 |
七、总结
整个通信链路可以概括为:
1. 客户端用 TrustStore 验证服务端证书 → 建立加密通道
2. 客户端按约定格式(报文头+XML+校验和)组装请求 → 通过加密通道发送
3. 服务端收到请求 → 处理业务 → 按同样格式组装响应 → 发回
4. 客户端收到响应 → 跳过报文头、去掉校验和 → 取出XML → 反序列化为Java对象
本地 Mock 的核心思路就是:自己启动一个 SSL Server,按照协议格式返回固定数据,让客户端以为在和真实服务端通信。
八、测试效果
