跳到主要内容

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数据连接​

  1. 登录TUS管理控制台
  2. 点击"数据连接",然后选择“新建”-“数据连接”

conn.png

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

conn-socket.png

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

conn-add.png

  1. 测试连接并保存

打开逻辑组件脚本开关​

系统默认逻辑组件中的自定义脚本组件是隐藏状态,需要在使用前手工打开。

在运维中心,点击右上方“系统管理”-“配置管理”,在“基本配置”tab页中,逻辑组件脚本启用选择“是”

script_open.png

创建流程集成​

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

esb-add.png

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

soap_api.png

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

soap_choose.png

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

tostring_add.png

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

tostring_body.png

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

define1_add.png

此组件使用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)
}
  1. 右键单击 “+”,在“出站端点”tab页面选择SOCKET组件

socket_add.png

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

socket_datail.png

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

define1_add.png

接收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.在编辑页面点击“开始调试”,使用接口工具调用调试地址。当调试通过后,点击“结束调试”。

debug.png

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

deploy.png