如何使用socket进行java网络编程(三)
本篇文章继续记录java网络通讯编程的学习。在本系列笔记的第一篇中曾经记录过一个项目中的程序,当时还处于项目早期,还未进入与第三方公司的联调阶段,笔者只是用java写了一个client程序模拟了一下第三方发送报文。 在client程序printer.println(datagram)后,server程序可以接收报文并能执行:解析->报文转换->转发前置机->接收前置机处理结果->报文转换 这样一个处理过程。但进入与第三方公司的联调后,server程序暴露了一些个问题。
(一)、
首先,上述 解析->报文转换->转发前置机->接收前置机处理结果->报文转换 实际上是不完整的,完整的应该是 解析->报文转换->转发前置机->接收前置机处理结果->报文转换->将结果写回原socket的输出流
原socket指的是第一步读取第三方报文时候的那个socket,这个socket在server.accept()的时候已经得到了,并且我们不需要知道他的地址和端口,只需把返回报文直接写进它的输出流就好了。 在第一篇笔记中结尾处的注释里笔者居然想新new一个带第三方地址和端口的socket发送返回报文,简直太土了。- -!
所以,补齐SocketHandler程序:
logger.info("开始向第三方返回结果报文..."); OutputStream os = this.socket.getOutputStream(); os.write(toThirdDatagram.getBytes()); os.close(); logger.info("向第三方返回结果报文结束");
或:
logger.info("开始向第三方返回结果报文..."); PrintWriter thirdPrinter = new PrintWriter (new OutputStreamWriter(this.socket.getOutputStream())); thirdPrinter.println(toThirdDatagram); thirdPrinter.flush(); thirdPrinter.close(); logger.info("向第三方返回结果报文结束");
由于我们处理的报文是文本形式,所以推荐使用reader/writer的第二种方法。
(二)、
另外,接下来才是本次联调测试发现的重要的问题:
对方通过其公司的通讯平台发送报文过来,我的server程序可以接收,但对方收不到我返回给他的报文。
在我的log4j日志上面没有发现有什么报错信息,但对方发送报文之后一直收不到我返回的报文,最后本地的通讯平台报了socket超时。而对方通过telnet发送同样的报文,又是可以正确接受到返回报文的。两种方法在我这边的日志上都是没有报错的。
两天的时间里,我们一直是觉得是我在发送返回报文的时候出了问题,或者是对方在接受我的返回报文时出了问题。但其实,问题出现在一开始我接收对方报文的时候。
很意外,因为我的日志里显示对方的报文我是正确的接收下来了的。
问题究竟出在哪呢?
BufferedReader reader = new BufferedReader(new InputStreamReader( this.socket.getInputStream())); datagram = reader.readLine(); logger.info("接收到第三方报文" + datagram);
问题就出在上面的红色代码处。readLine()方法是个阻塞方法,在收到换行或回车符或者socket超时之前,它会一直阻塞,而对方通过其通讯平台发给我的报文并不属于这样结尾,所以就发送了阻塞(对方通过telnet发送报文不会阻塞,因为敲了回车),等到socket超时之后,我的server程序继续向下执行,记录了“接收到第三方报文xxxxxx”这样的日志,并在最后向socket写入了返回报文,而此时socket对于对方来说早已经超时了,对方当然不会收到我的返回了!在超时之后记录到我的日志上的“成功”信息都是假象!
改正程序:
BufferedReader reader = new BufferedReader(new InputStreamReader( this.socket.getInputStream())); //datagram = reader.readLine(); char[] cbuf = new char[4000]; int l = reader.read(cbuf); datagram = new String(cbuf,0,l); logger.info("接收到第三方报文" + datagram);
read(cbuf)方法可以正确的识别一次数据包的结束并将接受到的报文存放在cbuf进行缓冲。不会造成阻塞。
至此,程序继续向下执行,并正确的返回报文给了第三方。
修正后的SocketHandler如下:
package com.zjjs.server; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.Socket; import java.nio.CharBuffer; import org.apache.log4j.Logger; import com.zjjs.trans.Trans; import com.zjjs.trans.ZjjsTrans; import com.zjjs.util.ConfigUtil; /** * create by linyang 2015-09-18 * 套接字请求处理线程 * 完成 接收第三方--->发送前置机--->接收前置机--->发送第三方 等步奏 * **/ public class SocketHandler implements Runnable { /*前置机服务的地址和端口*/ private static final String _adress_ = ConfigUtil.getConfigValue("front_address"); private static final int _port_ = Integer.parseInt(ConfigUtil.getConfigValue("front_port")); private Socket socket = null; private Socket frontSocket = null; private static Logger logger = Logger.getLogger(SocketHandler.class); public SocketHandler(Socket socket) { super(); this.socket = socket; } @Override public void run() { String datagram = null; String send2frontDatagram = null; ZjjsTrans transProcesser = null; String transCode = ""; Trans trans = null; try { BufferedReader reader = new BufferedReader(new InputStreamReader( this.socket.getInputStream())); //datagram = reader.readLine(); char[] cbuf = new char[4000]; int l = reader.read(cbuf); datagram = new String(cbuf,0,l); logger.info("接收到第三方报文" + datagram); transCode = datagram.substring(4, 8); try { try { trans = (Trans) Class.forName("com.zjjs.trans.Trans" + transCode) .newInstance(); } catch (InstantiationException inse) { inse.printStackTrace(); logger.error("实例化交易失败,交易类型" + "Trans" + transCode + ", " + inse); } catch (IllegalAccessException ille) { ille.printStackTrace(); logger.error("实例化交易失败IllegalAccessException,交易类型" + "Trans" + transCode + ", " + ille); } } catch (ClassNotFoundException e) { e.printStackTrace(); logger.error("未找到交易类型" + "com.zjjs.trans.Trans" + transCode + ", " + e); } transProcesser = new ZjjsTrans(trans); logger.info("解析收到第三方报文开始..."); transProcesser.parserDatagramReceive(datagram); logger.info("解析收到第三方报文结束"); logger.info("开始生成转发到前置机报文..."); send2frontDatagram = transProcesser.toFrontServerDatagram(); logger.info("结束生成转发到前置机报文,报文内容[" + send2frontDatagram + "]"); frontSocket = new Socket(_adress_, _port_); logger.info("开始向前置机发送报文..."); PrintWriter printer = new PrintWriter (new OutputStreamWriter(frontSocket.getOutputStream())); /* OutputStreamWriter writer = new OutputStreamWriter( frontSocket.getOutputStream()); */ printer.println(send2frontDatagram); printer.flush(); //printer.close(); //这里close()的话会把socket本身也close掉 logger.info("向前置机发送报文结束"); logger.info("开始接收前置机返回报文..."); BufferedReader frontreader = new BufferedReader(new InputStreamReader(frontSocket.getInputStream())); String frontReturnDatagram = frontreader.readLine(); printer.close(); frontreader.close(); logger.info("前置机返回报文[" + frontReturnDatagram + "]"); logger.info("接收前置机返回报文结束"); logger.info("开始将前置机返回报文转换为返回给第三方报文..."); String toThirdDatagram = transProcesser.toThirdServerDatagram(frontReturnDatagram); logger.info("转换后返回第三方报文[" + toThirdDatagram + "]"); logger.info("将前置机返回报文转换为返回给第三方报文结束"); logger.info("开始向第三方返回结果报文..."); PrintWriter thirdPrinter = new PrintWriter (new OutputStreamWriter(this.socket.getOutputStream())); thirdPrinter.println(toThirdDatagram); thirdPrinter.flush(); thirdPrinter.close(); reader.close(); logger.info("向第三方返回结果报文结束"); } catch (IOException ioe) { logger.error("I/O error:" + ioe); }finally{ try { if(this.socket!=null) this.socket.close(); } catch (IOException e) {} } } }
(三)、
那么改正后的程序就完备了吗?
没有。 通过跟同事交流,至少在以下几个地方,程序还有改进的空间。
1、对于每个第三方请求都启动一个线程进行处理这种方式,性能上不会太好,使用线程池可以改进。
2、在reader.read(cbuf)进行读取对方发送报文的地方,其实是只读取一个数据包。而tcp协议对于数据量比较大的报文是分包进行发送的。这就要求我们编写的应用层程序在应用层协议这个层面上自行判断报文的结束点。对多个分包进行循环读取。这才是上述程序的最大隐患。
2017-8-15补充: 1、线程池可以使用apache commons pool来实现,通用的对象池。可以用来封装数据库连接、笔者之前也用来封装过kafka的connect池化。
当然也能用来封装线程。
2、关于第2点,其实之前也提到过 “在应用层协议这个层面上自行判断报文的结束点”,这个其实只要设计好报文格式,
在报文头里说明本次通信所发送的报文体长度就可以了。
3、其实原程序严格说还有一个隐患,只不过在当时应该是预估好报文长度不会超过4000,所以不会出问题。 对于不知道长度范围的情况,应该是循环读取,
int c;
while( (c = reader.read(cbuf, 0, cbuf.length)) != -1 ){
}
BufferedReader reader = new BufferedReader(new InputStreamReader( this.socket.getInputStream())); //datagram = reader.readLine(); char[] cbuf = new char[4000]; int l = reader.read(cbuf); datagram = new String(cbuf,0,l); logger.info("接收到第三方报文" + datagram);