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/TLSSSLSecure Sockets Layer和 TLSTransport 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 什么是 JKSJKSJava 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字节][内层报文头 10141151字节][XML业务数据][MD5校验和 32字节] \_____________________169字节____________________/客户端收到响应后的解析逻辑StringresponserawData.substring(169,rawData.length()-32);// 跳过前169字节报文头去掉最后32字节校验和中间就是XML2.2 报文各部分说明外层前缀18字节位置长度内容说明1-33“BADAO”固定标识4-85“1001”系统编码9-1810“0000000xxx”后续内容总长度补零到10位内层长度10字节位置长度内容说明19-2810“0000000xxx”内层报文体总长度补零到10位内层报文头141字节位置长度字段名示例值1-33业务类型“TEST”41报文版本“1”5-95发起系统编码“1001”10-145目标系统编码“1002”15-184交易类型“001”19-202操作类型“02”21-222报文编码“01”23-242通讯协议“01”25-262数据协议“01”27-4620交易流水号“12345678901234567890”47-5610XML报文长度“0000000350”57-7014交易时间“20260608120000”71-722请求类型01请求/02响应73-8816交易签名MD5签名截取89-913附件个数“000”92-14150预留位全0XML业务数据变长?xml version1.0 encodingUTF-8 standaloneyes?scfheadertxId交易流水号/txIdtxTime交易时间/txTimertCode状态码/rtCodemessage错误信息/message/headerbody!-- 业务数据 --/body/scfMD5校验和32字节对内层报文头 XML部分做 MD5 哈希得到 32 位十六进制大写字符串附在最后用于校验数据完整性。三、keytool 证书工具详解keytool是 JDK 自带的密钥和证书管理工具用来创建和管理 JKS 文件。3.1 常用命令生成密钥对创建 KeyStorekeytool-genkeypair\-aliasmykey\# 别名标识这对密钥-keyalgRSA\# 算法类型RSA 最常用-keysize2048\# 密钥长度2048位足够安全-keystoreserver.jks\# 输出的 JKS 文件名-storepass123456\# JKS 文件的密码-dnameCNlocalhost\# 证书主体信息CN域名-validity365# 证书有效期天导出证书keytool-exportcert\-aliasmykey\# 要导出的别名-keystoreserver.jks\# 从哪个 JKS 导出-storepass123456\# JKS 密码-fileserver.cer# 导出的证书文件导入证书到信任库keytool-importcert\-aliasmykey\# 导入后在信任库中的别名-keystoreclient.jks\# 客户端的信任库文件-storepass654321\# 信任库密码-fileserver.cer\# 要导入的证书文件-noprompt# 不询问直接导入查看 JKS 中有哪些证书keytool-list\-keystoreserver.jks\-storepass1234563.2 dname 参数说明-dname指定证书的主体信息Distinguished Name格式为CNCommon Name, OUOrganization Unit, OOrganization, LLocality, STState, CCountry本地测试时只需要CNlocalhost或CN127.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.batecho off echo 生成 SSL 证书 REM 1. 生成服务端密钥库 keytool -genkeypair -alias server -keyalg RSA -keysize 2048 -keystore server.jks -storepass changeit -dname CNlocalhost, OUDemo, ODemo, LCity, STState, CCN -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客户端用 pause4.3 SSL 服务端SslServer.javaimportjavax.net.ssl.*;importjava.io.*;importjava.nio.charset.StandardCharsets;importjava.security.KeyStore;importjava.security.MessageDigest;/** * SSL Socket 服务端示例. * 监听端口接收客户端请求返回自定义协议格式的响应. */publicclassSslServer{privatestaticfinalintPORT9999;privatestaticfinalStringKEYSTORE_PATHserver.jks;privatestaticfinalStringKEYSTORE_PASSchangeit;publicstaticvoidmain(String[]args)throwsException{// 1. 加载服务端密钥库KeyStoreksKeyStore.getInstance(JKS);ks.load(newFileInputStream(KEYSTORE_PATH),KEYSTORE_PASS.toCharArray());// 2. 初始化 KeyManager管理服务端自己的证书和私钥KeyManagerFactorykmfKeyManagerFactory.getInstance(SunX509);kmf.init(ks,KEYSTORE_PASS.toCharArray());// 3. 创建 SSL 上下文SSLContextsslContextSSLContext.getInstance(TLSv1.2);sslContext.init(kmf.getKeyManagers(),null,null);// 4. 创建 SSL Server SocketSSLServerSocketFactoryfactorysslContext.getServerSocketFactory();SSLServerSocketserverSocket(SSLServerSocket)factory.createServerSocket(PORT);serverSocket.setEnabledProtocols(newString[]{TLSv1.2});System.out.println(SSL 服务端启动监听端口: PORT);// 5. 循环接收客户端连接while(true){java.net.SocketclientserverSocket.accept();System.out.println(客户端已连接: client.getInetAddress());InputStreaminputclient.getInputStream();OutputStreamoutputclient.getOutputStream();// 读取请求byte[]bufnewbyte[4096];intleninput.read(buf);StringrequestnewString(buf,0,len,StandardCharsets.UTF_8);System.out.println(收到请求: request);// 构造响应自定义协议格式StringxmlBody?xml version\1.0\ encoding\UTF-8\?responsecode200/codemessageHello from SSL Server/messagedataThis is the response data/data/response;StringfullResponsebuildProtocolMessage(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字节 */privatestaticStringbuildProtocolMessage(Stringxml)throwsException{// 141字节报文头简化版用固定值填充StringBuilderheadernewStringBuilder();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 字符// 内层 报文头 XMLStringinnerContentheader.toString()xml;// MD5校验和32字符StringcheckSummd5(innerContent.getBytes(StandardCharsets.UTF_8));StringinnerWithChecksuminnerContentcheckSum;// 内层长度10位StringinnerLengthStrString.format(%010d,innerWithChecksum.getBytes(StandardCharsets.UTF_8).length);// 外层 ENA 系统编码5位 外层长度10位 内层长度 内层内容StringinnerMsginnerLengthStrinnerWithChecksum;StringouterLengthStrString.format(%010d,innerMsg.getBytes(StandardCharsets.UTF_8).length);returnENASRVIDouterLengthStrinnerMsg;}privatestaticStringmd5(byte[]data)throwsException{MessageDigestmdMessageDigest.getInstance(MD5);byte[]digestmd.digest(data);StringBuildersbnewStringBuilder();for(byteb:digest){sb.append(String.format(%02X,0xFFb));}returnsb.toString();}}4.4 SSL 客户端SslClient.javaimportjavax.net.ssl.*;importjava.io.*;importjava.nio.charset.StandardCharsets;importjava.security.KeyStore;/** * SSL Socket 客户端示例. * 连接服务端发送请求接收并解析自定义协议格式的响应. */publicclassSslClient{privatestaticfinalStringSERVER_HOST127.0.0.1;privatestaticfinalintSERVER_PORT9999;privatestaticfinalStringTRUSTSTORE_PATHclient.jks;privatestaticfinalStringTRUSTSTORE_PASSchangeit;publicstaticvoidmain(String[]args)throwsException{// 1. 加载客户端信任库包含服务端证书KeyStoretsKeyStore.getInstance(JKS);ts.load(newFileInputStream(TRUSTSTORE_PATH),TRUSTSTORE_PASS.toCharArray());// 2. 初始化 TrustManager验证服务端证书TrustManagerFactorytmfTrustManagerFactory.getInstance(SunX509);tmf.init(ts);// 3. 创建 SSL 上下文SSLContextsslContextSSLContext.getInstance(TLSv1.2);sslContext.init(null,tmf.getTrustManagers(),null);// 4. 建立 SSL 连接SSLSocketFactoryfactorysslContext.getSocketFactory();SSLSocketsocket(SSLSocket)factory.createSocket(SERVER_HOST,SERVER_PORT);System.out.println(已连接到服务端: SERVER_HOST:SERVER_PORT);// 5. 发送请求OutputStreamoutputsocket.getOutputStream();StringrequestHello, this is a test request from client;output.write(request.getBytes(StandardCharsets.UTF_8));output.flush();System.out.println(已发送请求: request);// 6. 接收响应InputStreaminputsocket.getInputStream();byte[]bufnewbyte[4096];StringBuildersbnewStringBuilder();intlen;while((leninput.read(buf))0){sb.append(newString(buf,0,len,StandardCharsets.UTF_8));if(lenbuf.length){break;}}StringrawResponsesb.toString();System.out.println(收到响应总长度: rawResponse.length());// 7. 按协议解析响应// 跳过前169字节报文头去掉最后32字节校验和中间是XMLStringxmlrawResponse.substring(169,rawResponse.length()-32);System.out.println(解析出 XML:\nxml);socket.close();}}4.5 运行步骤# 1. 生成证书generate-certs.bat# 2. 编译javac SslServer.java javac SslClient.java# 3. 启动服务端新开一个终端窗口javaSslServer# 4. 运行客户端另一个终端窗口javaSslClient4.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 version1.0 encodingUTF-8?responsecode200/codemessageHello from SSL Server/messagedataThis 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 上加一层加密防窃听防篡改JKSJava 的密钥/证书存储文件格式KeyStore存自己的私钥证书服务端用来证明身份TrustStore存信任的证书客户端用来验证服务端身份keytoolJDK 自带的证书管理命令行工具Certificate数字证书包含公钥和身份信息MD5 checksum报文完整性校验防止数据被篡改报文头固定长度的元数据区域描述报文类型和长度等信息JAXBJava 的 XML 与对象互转框架七、总结整个通信链路可以概括为1. 客户端用 TrustStore 验证服务端证书 → 建立加密通道 2. 客户端按约定格式报文头XML校验和组装请求 → 通过加密通道发送 3. 服务端收到请求 → 处理业务 → 按同样格式组装响应 → 发回 4. 客户端收到响应 → 跳过报文头、去掉校验和 → 取出XML → 反序列化为Java对象本地 Mock 的核心思路就是自己启动一个 SSL Server按照协议格式返回固定数据让客户端以为在和真实服务端通信。八、测试效果