本篇文章继续记录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);
												

如何使用socket进行java网络编程(三)的更多相关文章

  1. 如何使用socket进行java网络编程(二)

    通过在如何使用socket进行java网络编程(一)中程序的编写,可以总结出一些常用的java socket编程的范例来. ServerSocket server = new ServerSocket ...

  2. 如何使用socket进行java网络编程(四)

    在上一篇的结尾,提到过用来处理每一个服务端accept到的socket,我们由原来最开始的单线程改成了多线程去处理,但是对每一个接收到的socket都new一个thread去处理,这样效率太低,我们需 ...

  3. Java网络编程三--基于TCP协议的网络编程

    ServerSocket对象用于监听来自客户端的Socket连接,如果没有连接,它将一直处于等待状体 Socket accept():如果接收到客户端的连接请求,该方法返回一个与客户端对应Socket ...

  4. 如何使用socket进行java网络编程(五)

    本篇记录: 1.再谈readLine()方法 2.什么是真正的长连接 最近又参与了一个socket的项目,又遇到了老生常谈的readLine()问题:对方通过其vb程序向我方socketServer程 ...

  5. 如何使用socket进行java网络编程(一)

    笔者进来遇到一个项目,一家公司的系统需要在完成自身业务逻辑的同时,接入到某银行的核心系统(这里准确说应该是前置机)进行一系列的账务处理,然后再将账务处理结果返回给该公司系统. 网络通信采用TCP协议. ...

  6. java网络编程三次握手四次挥手

    第一次握手:client设置syn=1,随机产生一个序列号seq=x,将数据包发送到server.client进入syn_send状态, 等待server确认. 第二次握手:server查看clien ...

  7. java 网络编程复习(转)

    好久没有看过Java网络编程了,现在刚好公司有机会接触,顺便的拾起以前的东西 参照原博客:http://www.cnblogs.com/linzheng/archive/2011/01/23/1942 ...

  8. Java 网络编程(转)

    一,网络编程中两个主要的问题 一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输. 在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可 ...

  9. 关于Java网络编程

    一,网络编程中两个主要的问题 一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输. 在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可 ...

随机推荐

  1. spring配置数据库连接池druid

    连接池原理 连接池基本的思想是在系统初始化的时候,将数据库连接作为对象存储在内存中,当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象.使用完毕后,用户也并非将连 ...

  2. 构建openssl debug版

    一.简介 作为一种安全协议,openssl囊括了主要的密码算法.常用的密钥和证书封装管理功能以及SSL协议,并提供了丰富的应用程序供测试或其它目的使用. 参考: http://www.linuxidc ...

  3. Ubuntu12.04添加环境变量

    环境变量分为系统级和用户级. 系统级变量设置环境为/etc/environment和/etc/profile等,不要轻易修改,否则可能造成系统错误. 用户级变量设置路径为-/.bashrc和~/.pr ...

  4. python使用wmi模块获取windows下的系统信息监控系统-乾颐堂

    Python用WMI模块获取Windows系统的硬件信息:硬盘分区.使用情况,内存大小,CPU型号,当前运行的进程,自启动程序及位置,系统的版本等信息. 本文实例讲述了python使用wmi模块获取w ...

  5. vs不同

    写了很多却错误关闭,无语,直接上内容,因为在公司年限长和德国.波兰.英国公司都有合作,而且他们的开发工具各不相同,因此我电脑上有Visual Studio 2008,Visual Studio 201 ...

  6. gdb 调试带参数程序

    在gdb中,运行程序使用r或是run命令. 程序的运行,你有可能需要设置下面四方面的事. 1.程序运行参数. set args 可指定运行时参数.(如:set args 10 20 30 40 50) ...

  7. Zookeeper 系列(一)基本概念

    Zookeeper 系列(一)基本概念 https://www.cnblogs.com/wuxl360/p/5817471.html 一.分布式协调技术 在给大家介绍 ZooKeeper 之前先来给大 ...

  8. 为了记忆和方便翻阅 vue构建后的结构目录说明

    一. ├── build              // 项目构建(webpack)相关代码             记忆:(够贱)    9个 │ ├── build.js       // 生产环 ...

  9. 201709011工作日记--Volley源码详解(二)

    1.Cache接口和DiskBasedCache实现类 首先,DiskBasedCache类是Cache接口的实现类,因此我们需要先把Cache接口中的方法搞明白. 首先分析下Cache接口中的东西, ...

  10. noip第7课作业

    1.    求平均值 [问题描述] 在一次运动会方队表演中,学校安排了十名老师进行打分.对于给定的每个参赛班级的不同打分(百分制整数),按照去掉一个最高分.去掉一个最低分,再算出平均分的方法,得到改班 ...