SSL Socket 通信与本地 Mock Server 实践指南

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=localhostCN=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 格式数据,收到私有格式会报错

所以测试这类接口只能:

  1. 写代码模拟(本文方案)
  2. 使用专业的 TCP 调试工具(如 Hercules、PacketSender)发送原始字节
  3. 抓包工具(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,按照协议格式返回固定数据,让客户端以为在和真实服务端通信。

八、测试效果

相关推荐
SDWAN_Cheap2 小时前
网络基础扫盲第二弹:DNS、DHCP、ARP、NAT、TCP/IP,从原理到实践
网络·tcp/ip·dns·网络基础
z9209810232 小时前
常用的改机软件 MTK 高通 展讯 紫光展锐 改串 一键新机 怎么做?修改SN NV数据 qcn
网络
Dynadot_tech2 小时前
掘金优质中文域名——Nameclub中文IDN交易板块重磅上线
网络·域名·中文域名·dynadot·域名管理·域名交易
Bobolink_2 小时前
海外拍卖直播风控数据上报:跨境网络加密传输方案设计实践
网络·数据传输·跨境直播·直播网络
宇砾2 小时前
HTTPS的工作流程
网络协议·http·https
梦想的旅途22 小时前
企业微信自动化系统从 0 到 1:架构设计与踩坑实录
网络·机器人·自动化·企业微信·rpa
2501_941982052 小时前
# 企业微信群管理机器人的技术实现:从创建到解散的完整方案
网络·机器人·自动化·企业微信·rpa
酉鬼女又兒2 小时前
零基础入门计算机网络:集线器与交换机区别、以太网交换机自学习转发流程及生成树协议STP全解析
服务器·网络·网络协议·tcp/ip·计算机网络·考研·职场和发展
大棉花哥哥2 小时前
Cybellum 固件包上传扫描流程操作手册
网络·安全性测试