一、问题背景

        我们系统调用银行核心接口,通讯方式是Socket,报文格式是XML。
        业务量少时,正常,但是我们发起批量操作时,明明核心转账成功的,但是我们解析回执,认为转账失败。
        这种情况首先排查接收报文,报文是类似这样的(真实报文内容不是这样的):


iOS socket收到的数据 socket接收数据不全_java

错误的回执

二、解决思路      

       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。后续深入探讨。