Socket系统集成
Socket(套接字)是网络通信的编程接口,用于不同主机(或同一主机)上的进程间通信(IPC)。它是操作系统提供的抽象层,允许应用程序通过TCP/IP或UDP/IP协议进行数据传输。
Web Service 是一种基于标准化协议(如HTTP/XML/JSON)的跨平台、跨语言的网络服务,允许不同系统通过互联网进行数据交互。其中基于XML的消息协议SOAP,即SOAP + WSDL + XML在使用上严格标准化,适合企业级复杂业务(如银行交易)。
本实践完成了Socket报文与SOAP报文的相互转换。
先将SOAP报文按照规则转为定长的Socket报文,包括5字节(协议)+122字节(报文头)+1282字节(交易明细),转换后将报文发送到Socket报文接收系统,收到返回值后,将返回的Socket再按照规则转为SOAP报文。
创建Socket数据连接
- 登录TUS管理控制台
- 点击"数据连接",然后选择“新建”-“数据连接”

- 展开"其他",然后点击“SOCKET”

- 填写连接属性配置:
- 通信协议
- 主机地址
- 端口
- 编码器,选择字节数组编码器
- 解码器,选择字节数组解码器

- 测试连接并保存
打开逻辑组件脚本开关
系统默认逻辑组件中的自定义脚本组件是隐藏状态,需要在使用前手工打开。
在运维中心,点击右上方“系统管理”-“配置管理”,在“基本配置”tab页中,逻辑组件脚本启用选择“是”

创建流程集成
- 点击“流程集成”,选择“组”然后选择新建,点击“流程集成”

- 在右侧流程设置中输入名称,在左侧画布中左键单击“+”,再右键单击“+”选择入站端点-SOAP API

- 配置SOAP API:
- 输入path,以/开头,如/socket_tran
- 上传WSDL文件,上传后选择方法

- 右键单击 “+”,在“函数”tab页面选择toString组件

此组件的作用是将请求数据转化为字符串,存到body中。body是TUS系统内置的当前组件传递消息体的对象。

- 右键单击 “+”,在“逻辑”tab页面选择自定义脚本组件

此组件使用Groovy语言,实现将SOAP请求转化为Socket请求的功能,该脚本一共有5个主要步骤:
- 首先是确定交易码,从body中解析出NS1servic的值,根据此值判断,此示例中根据值将交易分为贷款账户查询和默认交易码,默认交易码执行的是登录操作;
- 其次是解析SOAP请求,根据交易码不同,分别解析出不同的变量值,比如登录操作解析出ibLoginID和funcCode;
- 第三步是构建报文头,其中有固定字段,也有从请求中解析出的动态字段;
- 第四步是构建报文体,根据要求构建后转为GB18030;
- 最后组装报文,将报文复制给消息体向下一个组件传递。
下面是代码示例:
import java.nio.charset.Charset
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import groovy.xml.XmlUtil
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.Arrays
// SOAP到Bancs Socket报文转换
def convertSoapToBancs(data,xmlStr) {
try {
LOG.info("开始转换SOAP到Bancs报文")
// 1. 确定交易码
def transCode = determineTransCode(data.service)
LOG.info("确定交易码: {}", transCode)
exchange.setProperty("transCode",transCode)
// 2. 解析SOAP请求
def soapData = parseSoapRequest(data,transCode,xmlStr)
LOG.info("解析SOAP请求成功,服务: {}", soapData.service)
// 3. 构建BANCS报文头 (122字节)
def (headerStr, headerBytes) = buildBancsHeader(soapData, transCode)
LOG.info("构建报文头完成,长度: {}", headerBytes.length)
// 4. 构建交易报文体
def bodyBytes = buildTransactionBody(soapData, transCode)
LOG.info("构建报文体完成,长度: {}", bodyBytes.length)
// 5. 组装完整报文
def bancsData = assembleFullMessage(headerBytes, bodyBytes, headerStr)
LOG.info("成功转换SOAP到Bancs报文,总长度: {} 字节", bancsData.length)
return bancsData
} catch (Exception e) {
LOG.error("转换SOAP到Bancs报文时发生错误", e)
throw e
}
}
// 确定交易码
def determineTransCode(serviceName) {
switch(serviceName) {
case "SRV.LON.getLoansAccountDetails":
return "010400" // 贷款账户查询
// 可以添加更多服务映射
default:
LOG.warn("未知服务: {}, 使用默认交易码", serviceName)
return "078397" // 默认交易码
}
}
// 解析SOAP请求
def parseSoapRequest(data,transCode,xmlStr) {
try {
def xmlObj = new XmlSlurper().parseText(xmlStr)
LOG.info("xmlObj:{}",xmlObj)
// 外层header
def outerHeader = xmlObj.'**'.find { it.name() == 'header' }
def callApp = xmlObj.'**'.find { it.name() == 'callApp' }
// 内层header
def innerHeader = callApp.'**'.find { it.name() == 'header' }
// 内层header
def innerBody = callApp.'**'.find { it.name() == 'body' }
data.reqJournalNo = outerHeader.'NS1:reqJournalNo'.text()
data.acDate = innerHeader.'acDate'.text().replaceAll("-", "")
if("078397"==transCode) {
data.UUID = innerHeader.'UUID'.text()
data.ibLoginID = innerBody.'**'.find { it.name()?.endsWith('ibLogin') }?.'ibLoginID'.text()
data.funcCode = innerBody.'**'.find { it.name()?.endsWith('ibLogin') }?.'funcCode'.text()
LOG.info("解析SOAP字段: service={}, ibLoginID={}, funcCode={}",data.service, data.ibLoginID, data.funcCode)
}
if("010400"==transCode) {
def accountDetails = callApp.'**'.find { it.name() == 'getLoansAccountDetails' }
// 17位,不够左边补全0
data.accountNo = accountDetails?.accountNo?.text()?.padLeft(17, '0') ?: ''.padLeft(17)
LOG.info("解析SOAP字段: service={}, accountNo={}",data.service, data.accountNo)
}
data.channel = innerHeader.'channel'.text()
data.previewFlag = innerHeader.'previewFlag'.text()
return data
} catch (Exception e) {
LOG.error("解析SOAP请求时发生错误", e)
throw e
}
}
// 构建BANCS报文头 (122字节)
def buildBancsHeader(data, transCode) {
try {
def header = new StringBuilder()
// 报文头字段填充 (固定结构)
appendField(header, "00", 2) // Filler_2
appendField(header, "00", 2) // MsgType
appendField(header, "1282", 4) // 报文长度 - 初始值,后面会修正
// 动态字段
appendField(header, (data.UUID ?: "").padLeft(12, '0'), 12) // UUID
appendField(header, "** ", 4) // SegNo
......
appendField(header, data.acDate ?: LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")), 8) // HeaderDate
// 确保总长度为122字节
fillRemaining(header, 122)
def headerStr = header.toString()
LOG.info("构建报文头: {}", headerStr)
//轉為定長 Socket 請求(GB18030)
return [headerStr, encodeToBytes(headerStr, 'GB18030')]
} catch (Exception e) {
LOG.error("构建报文头时发生错误", e)
throw e
}
}
// 构建交易报文体(1282)
def buildTransactionBody(data, transCode) {
try {
def body = new StringBuilder()
switch(transCode) {
case "010400": // 贷款账户查询
// 账号 (17字节)
appendField(body, data.accountNo, 17)
// 查询选项 (1字节)
appendField(body, "1", 1)
break
case "078397": // 網銀登錄交易
// PR78397-IN-EB-AC (20字节)
appendField(body, data.ibLoginID, 20)
//PR78397-IN-FUNC-CODE (2字节)
appendField(body, data.funcCode, 2)
break
default: // 默认处理
if (data.accountNo) {
// 账号 (17字节)
appendField(body, data.accountNo, 17)
}
break
}
// 填充剩余部分到1282字节 交易体长度982+兑换信息长度166+保留区长度22=1282
fillRemaining(body, 1282)
def bodyStr = body.toString()
LOG.info("构建报文体: {}", bodyStr)
//轉為定長 Socket 請求(GB18030)
return encodeToBytes(bodyStr, 'GB18030')
} catch (Exception e) {
LOG.error("构建报文体时发生错误", e)
throw e
}
}
// 组装完整报文
def assembleFullMessage(headerBytes, bodyBytes, headerStr) {
try {
// 计算报文长度 (协议头之后的部分)
def messageLength = headerBytes.length + bodyBytes.length
def lengthStr = String.format("%04d", messageLength)
// 创建完整报文
def fullMessage = new ByteArrayOutputStream()
// 协议头 (5字节)
// 写一个空格
fullMessage.write(32) // 协议标识
fullMessage.write(lengthStr.getBytes("UTF8")) // 长度字段
// 报文头
fullMessage.write(headerBytes)
// 交易明细
fullMessage.write(bodyBytes)
def result = fullMessage.toByteArray()
LOG.info("组装完整报文: 协议头长度={}, 报文头长度={}, 报文体长度={}, 总长度={}",
5, headerBytes.length, bodyBytes.length, result.length)
return result
} catch (Exception e) {
LOG.error("组装完整报文时发生错误", e)
throw e
}
}
// 辅助方法:追加字段并确保长度
def appendField(StringBuilder builder, String value, int length) {
if (value == null) value = ""
if (value.length() > length) {
builder.append(value.substring(0, length))
} else if (value.length() < length) {
// 长度不够默认用空格补全
builder.append(value.padRight(length))
} else {
builder.append(value)
}
}
// 辅助方法:填充剩余空间
def fillRemaining(StringBuilder builder, int totalLength) {
int currentLength = builder.length()
if (currentLength < totalLength) {
builder.append(" " * (totalLength - currentLength))
}
}
// 辅助方法:字符串编码处理
def encodeToBytes(String text, String charsetName) {
try {
def charset = Charset.forName(charsetName)
def bytes = text.getBytes(charset)
// 生僻字处理 (简化版)
for (int i = 0; i < bytes.length; i++) {
if (bytes[i] == 0x3F) { // 0x3F是?的编码
bytes[i] = '?' as byte
}
}
return bytes
} catch (Exception e) {
LOG.error("编码字符串时发生错误: {}", text, e)
throw e
}
}
try {
LOG.info("soap消息体内容:{}",${body})
def xml = new XmlSlurper(false, false).parseText(${body})
// 获得交易码
def data = [:]
data.service = xml.'soapenv:Body'
.'NS1:callService'
.'NS1:header'
.'NS1:service'.text()
// 执行转换
def bancsData = convertSoapToBancs(data,${exchange.getMessage().getBody(String.class)})
// 输出结果
LOG.info("协议: {}", new String(Arrays.copyOfRange(bancsData, 0, 5), "GB18030"))
LOG.info("header部分: {}", new String(Arrays.copyOfRange(bancsData, 6, 122), "GB18030"))
LOG.info("body部分: {}", new String(Arrays.copyOfRange(bancsData, 122,bancsData.length), "GB18030"))
LOG.info("转换后的Bancs报文内容:{}",new String(bancsData,"GB18030"))
// 赋值给消息体向下传递
${request.body} = bancsData
} catch (Exception e) {
LOG.error("执行示例时发生错误", e)
}
- 右键单击 “+”,在“出站端点”tab页面选择SOCKET组件

此组件的作用是发送Socket消息。

- 右键单击 “+”,在“逻辑”tab页面选择自定义脚本组件

接收Socket返回消息后,此组件将Socket格式消息转为SOAP,该脚本一共有3个主要步骤:
- 将Socket返回值转换为字节数组
- 根据定义好的位置解析Socket消息
- 构建SOAP消息并输出
下面是代码示例:
import groovy.transform.Field
import java.nio.charset.Charset
import groovy.xml.MarkupBuilder
@Field
// 字符编码定义
def GB18030 = Charset.forName("GB18030")
@Field
def UTF8 = Charset.forName("UTF-8")
@Field
// 字段位置定义
def FIELD_POSITIONS = [
// 协议头 (5字节)
PROTOCOL_MARKER: [offset: 0, length: 1],
MESSAGE_LENGTH: [offset: 1, length: 4],
// 报文头 (122字节)
FILLER_2: [offset: 5, length: 2],
MSG_TYPE: [offset: 7, length: 2],
......
OUTPUT_CODE: [offset: 127, length: 2],
// 交易报文体 (1282字节)
IB_ACC: [offset: 129, length: 20],
IB_STATUS: [offset: 149, length: 1],
CIF_NO: [offset: 150, length: 16],
ID_TYPE: [offset: 166, length: 2],
ID_NUM: [offset: 168, length: 18],
SEX: [offset: 186, length: 1],
CUST_CNAME: [offset: 187, length: 40],
CUST_ENAME: [offset: 227, length: 40],
MOBILE_NO: [offset: 267, length: 16],
]
// 解析Socket响应
def parseSocketResponse(byte[] responseBytes) {
def result = [:]
// 解析所有字段
FIELD_POSITIONS.each { fieldName, pos ->
def bytes = Arrays.copyOfRange(responseBytes, pos.offset, pos.offset + pos.length)
LOG.info(fieldName+":"+new String(bytes))
def value = new String(bytes, GB18030).trim()
result[fieldName] = value
}
return result
}
// 构建SOAP响应
def buildSoapResponse(Map parsedData) {
def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.mkp.xmlDeclaration(version: "1.0", encoding: "UTF-8")
xml."NS1:callServiceResponse"('xmlns:NS1': 'http://www.taifungbank.com/TFBService') {
'NS1:header' {
'NS1:version'("0.1")
'NS1:source'("IBS")
'NS1:proxy'("IBCP")
'NS1:service'("SRV.LON.getLoansAccountDetails")
'NS1:date'("2018-11-01")
'NS1:time'("09:11:57")
'NS1:reqJournalNo'("124231485")
'NS1:resJournalNo'(parsedData.JOURNAL_NO_1 ?: "")
'NS1:returnCode'("N")
}
'NS1:body' {
'NS1:callAppResponse' {
'NS1:header' {
'NS1:acDate'(formatDate(parsedData.HEADER_DATE))
'NS1:pendingApproval'('false')
'NS1:previewFlag'(parsedData.PREVIEW_FLAG ?: "Y")
'NS1:instNo'(parsedData.INSTITUTION_NO ?: "003")
'NS1:semiAutoFlag'(parsedData.SEMI_AUTO_FLAG ?: "")
'NS1:tType'(parsedData.TTYPE ?: "")
'NS1:flag1'(parsedData.FLAG1 ?: "")
'NS1:flag2'(parsedData.FLAG2 ?: "6")
'NS1:flag3'(parsedData.FLAG3 ?: "")
'NS1:oldAcctFlag'(parsedData.OLD_ACCT_FLAG ?: "0")
'NS1:superVisorId'(parsedData.SUPERVISOR_ID ?: "0000000")
'NS1:supVisorId2'(parsedData.SUPERVISOR_ID2 ?: "0000000")
'NS1:superR1Ovrd'(parsedData.SUPER_R1_OVRD ?: "0000")
'NS1:superR2Ovrd'(parsedData.SUPER_R2_OVRD ?: "0000")
}
'NS1:body' {
'NS1:getLoansAccountDetailsResponse' {
'NS1:IB_ACC'(parsedData.IB_ACC ?: "")
'NS1:IB_STATUS'(parsedData.IB_STATUS ?: "")
'NS1:CIF_NO'(parsedData.CIF_NO ?: "")
'NS1:ID_TYPE'(parsedData.ID_TYPE ?: "")
'NS1:ID_NUM'(parsedData.ID_NUM ?: "")
'NS1:SEX'(parsedData.SEX ?: "")
'NS1:CUST_CNAME'(parsedData.CUST_CNAME ?: "")
'NS1:CUST_ENAME'(parsedData.CUST_ENAME ?: "")
'NS1:MOBILE_NO'(parsedData.MOBILE_NO ?: "")
}
}
}
}
}
return writer.toString()
}
// 格式化日期 (yyyyMMdd -> yyyy-MM-dd)
def formatDate(String dateStr) {
if (dateStr && dateStr.length() == 8) {
return ${dateStr.substring(4,8)}+"-"+${dateStr.substring(2, 4)}+"-"+${dateStr.substring(0, 2)}
}
return dateStr
}
def bytesBody =exchange.getMessage().getBody(byte[].class);
LOG.info("socket response:{}",bytesBody)
// 1. 转换为字节数组 (使用GB18030编码)
def responseBytes =bytesBody
// 2. 解析Socket响应
def parsedData = parseSocketResponse(responseBytes)
// 3. 构建SOAP响应
def soapResponse = buildSoapResponse(parsedData)
LOG.info("生成的SOAP响应:{}",soapResponse);
${request.body} = soapResponse;
调试与部署
1.在编辑页面点击“开始调试”,使用接口工具调用调试地址。当调试通过后,点击“结束调试”。

2.调试完成后,点击“部署”按钮进行部署操作
