一、问题背景
我们系统调用银行核心接口,通讯方式是Socket,报文格式是XML。
业务量少时,正常,但是我们发起批量操作时,明明核心转账成功的,但是我们解析回执,认为转账失败。
这种情况首先排查接收报文,报文是类似这样的(真实报文内容不是这样的):
错误的回执
二、解决思路
1. 从源头出发
遇到这个问题,最初的想法是,怀疑银行的服务器问题(虽然这需要很大的勇气),但是行方排查并找到当时他们服务器的日志,显示的回执内容是正常的。
2. 查看自身代码
查看代码,看到每次先读取约定前6位,这个是报文长度。那是不是回执的报文长度不对,但是看其它正常接收的,银行返回的都有长度。
百度,尝试各种组合关键字搜索。找到一些博客,但是没有说的很具体。
3. 模拟复现
接着,想本地模拟一下。找到原来的测试程序,修修改改。服务端代码如下:
@RunWith(JUnit4.class)
public class ServerSocketTest {
private static String DATA_GRAM = "<?xml version=\"1.0\" encoding=\"GBK\"?><Voucher><Id>2411190</Id><AdmDivCode>441600</AdmDivCode><StYear>2019</StYear><VtCode>8207</VtCode><VouDate>20191031</VouDate><VoucherNo>SZ2230010001041</VoucherNo><OriginalVtCode>8202</OriginalVtCode><OriginalVoucherNo>SZ2230010001041</OriginalVoucherNo><ChildPackNum>1</ChildPackNum><CurPackNo>1</CurPackNo><SumAmt>220.00</SumAmt><AgencyCode>223001</AgencyCode><AgencyName>guangdongheyuan</AgencyName><PayAcctNo>80020000009829508</PayAcctNo><PayAcctName>guangdongheyuan</PayAcctName><PayAcctBankName>guangdongheyuan</PayAcctBankName><PayBankCode>020</PayBankCode><PayBankName>guangdongheyuan</PayBankName><BusinessTypeCode>2</BusinessTypeCode><BusinessTypeName>guangdongheyuan</BusinessTypeName><XPayDate></XPayDate><XSumAmt></XSumAmt><Hold1></Hold1><Hold2></Hold2><PayAmt>220.00</PayAmt><DetailList><Detail><Id>2411189</Id><PayeeAcctNo>6217282013100008792</PayeeAcctNo><PayeeAcctName>guangdongheyuan</PayeeAcctName><PayeeAcctBankName>guangdongheyuan</PayeeAcctBankName><PayAmt>100.00</PayAmt><Remark></Remark><XPayDate></XPayDate><XAgentBusinessNo></XAgentBusinessNo><XpayAmt></XpayAmt><XPayeeAcctBankName></XPayeeAcctBankName><XPayeeAcctNo></XPayeeAcctNo><XPayeeAcctName></XPayeeAcctName><XAddWord></XAddWord><Hold1></Hold1><Hold2></Hold2><Hold3></Hold3><Hold4></Hold4><CONSUMEINFO></CONSUMEINFO><PROVIDEADDRESS>guangdongheyuan</PROVIDEADDRESS><PROVIDEINFO>guangdongheyuan</PROVIDEINFO><XRETURNFLAG>1</XRETURNFLAG><PKCARDDETAIL>252267</PKCARDDETAIL><PayeeAccountCardName>guangdongheyuan</PayeeAccountCardName><PayeeAccountCardBank>guangdongheyuan</PayeeAccountCardBank><PayeeAccountCardNo>6217282013100008792</PayeeAccountCardNo><ConsumeDate>20190807</ConsumeDate><VoucherBillNo>SZ2230010001041</VoucherBillNo><AgentBusinessNo>005041333</AgentBusinessNo></Detail><Detail><Id>2411189</Id><PayeeAcctNo>6217282013100008792</PayeeAcctNo><PayeeAcctName>guangdongheyuan</PayeeAcctName><PayeeAcctBankName>guangdongheyuan</PayeeAcctBankName><PayAmt>120.00</PayAmt><Remark></Remark><XPayDate></XPayDate><XAgentBusinessNo></XAgentBusinessNo><XpayAmt></XpayAmt><XPayeeAcctBankName></XPayeeAcctBankName><XPayeeAcctNo></XPayeeAcctNo><XPayeeAcctName></XPayeeAcctName><XAddWord></XAddWord><Hold1></Hold1><Hold2></Hold2><Hold3></Hold3><Hold4></Hold4><CONSUMEINFO></CONSUMEINFO><PROVIDEADDRESS>guangdongheyuan</PROVIDEADDRESS><PROVIDEINFO>guangdongheyuan</PROVIDEINFO><XRETURNFLAG>1</XRETURNFLAG><PKCARDDETAIL>252266</PKCARDDETAIL><PayeeAccountCardName>guangdongheyuan</PayeeAccountCardName><PayeeAccountCardBank>guangdongheyuan</PayeeAccountCardBank><PayeeAccountCardNo>6217282013100008792</PayeeAccountCardNo><ConsumeDate>20190804</ConsumeDate><VoucherBillNo>SZ2230010001041</VoucherBillNo><AgentBusinessNo>019060816</AgentBusinessNo></Detail></DetailList></Voucher>";
@Test
public void testServer(){
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(8000));
while(true){
Socket socket = serverSocket.accept();
PrintWriter pw = new PrintWriter(socket.getOutputStream(),true);
StringBuffer sb = new StringBuffer("027900");
sb.append(DATA_GRAM);
sb.append(DATA_GRAM);
sb.append(DATA_GRAM);
sb.append(DATA_GRAM);
sb.append(DATA_GRAM);
sb.append(DATA_GRAM);
sb.append(DATA_GRAM);
sb.append(DATA_GRAM);
sb.append(DATA_GRAM);
sb.append(DATA_GRAM);
pw.println(sb.toString());
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
for(String msg = br.readLine();msg!=null&&msg.length()>0;msg = br.readLine()){
System.out.println(msg);
}
pw.close();
br.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
客户端的很简单,关键的地方如下:
InputStream in = socket.getInputStream();
logger.info("######已返回回执报文");
// 读取响应报文长度
byte[] lenbytes=new byte[6];
in.read(lenbytes);
String len = new String(lenbytes, 0, 6, "GBK");
int messageLen = Integer.parseInt(len);
byte[] message = new byte[messageLen];
// 普通的读取措施
in.read(message);
最初的服务端的字符串没有那么长,只有1个sb(大家都明白我指的是什么意思吧)。测试时使用for循环发起200次请求,甚至2000次,均未出现问题。
考虑请求使用多线程,目的是为了模拟服务端忙碌的情况。也没有出现问题。而且我们原本的批量业务代码也是循环发起的。一时很无奈。
考虑网络上的问题,会不会是TCP拆包粘包导致的呢?但是前段时间看的谢希仁《计算机网络》,回忆了一下,但是目前的App层有很好的约定--前6位是报文体长度。拆包不会受到影响。想不到问题就,意淫是不是前6位被拆开了,没有读全。但是如果是这样,回执报文应该变短。
参考了一些网上的类似情况,考虑模拟的报文长度可能太短了,只有2790。因此拼了10次,总长度达到27900。先是多线程的情况,200请求出现了1次。改为for循环的,200次出现7、8次。自己也很疑惑为什么会是这样。。。
三、最终方案
更改客户端的代码:
enhancedRead(in, message);
private static void enhancedRead(InputStream in, byte[] bytes) throws IOException {
int remaining = bytes.length;
while (remaining > 0) {
int location = bytes.length - remaining;
int count = in.read(bytes, location, remaining);
if (count == -1) {
break;
}
remaining -= count;
}
}
测试单线程循环和多线程都不会出现问题了。
分析:
in.read(int[] bytes)是尽量读取数组长度的字节并返回读取到的字节数目,读取0个字节就返回0;读取到文件的结尾EOF(End of File)会返回-1。但是纵使我英语不赖,没找到没读完的情况(目前实践说明,读不到后续的也会返回)。
至于为什么会读不全,可能是拆包的问题,可能是接收端缓存的问题(滑动窗口,只是有点印象,书上是什么都忘了)。
假期看了Netty,几个简单的例子,避免了与数据流直接交互,对于拆包、编码等问题也有对应的处理Handler。后续深入探讨。