在项目中需要与第三方服务对接,通过tcp协议发送和接收数据。因为觉得Apache的开源框架mina比较方便,所以在项目中直接使用了。主要的业务逻辑是先建立连接,然后在连接上进行数据的交互,等整个业务流程结束,主动断开连接。但是在实际对接第三方服务时,发现虽然可以成功和服务器建立连接,但是mina发送请求参数后,服务器返回的数据mina一直接收不到,而且也和对方技术确认过,数据确实已经返回了。在尝试修改编码格式(utf8、gbk等)和使用不同的ProtocolCodecFilter之后,发现并不是这些原因导致。
那么为什么mina接收不到第三方服务器返回的数据呢,后来百度了很多关于tcp相关的基本资料,因为本人对tcp相关的知识掌握不多。在查询资料时了解到,tcp交互中有可能出现丢包、粘包等情况,详细内容可以自行百度。问题就出在这个粘包的处理上。mina封装的方法有对粘包问题的处理。默认TextLineCodecFactory这个工厂在发送和接收请求时,构造方法中会实例化两个对象,一个是TextLineEncoder编码器,另一个是TextLineDecoder解码器,分别负责发送数据和接收数据的编码解码。默认的构造器为:
/**
* Creates a new instance with the specified {@link Charset}. The
* encoder uses a UNIX {@link LineDelimiter} and the decoder uses
* the AUTO {@link LineDelimiter}.
*
* @param charset
* The charset to use in the encoding and decoding
*/
public TextLineCodecFactory(Charset charset) {
encoder = new TextLineEncoder(charset, LineDelimiter.UNIX);
decoder = new TextLineDecoder(charset, LineDelimiter.AUTO);
}
我们可以看到编码器和解码器中都有LineDelimiter,意思是分隔符。这两个作为分隔符的常量分别是:
/**
* The line delimiter constant of UNIX (<tt>"\n"</tt>)
*/
public static final LineDelimiter UNIX = new LineDelimiter("\n");
/**
* A special line delimiter which is used for auto-detection of
* EOL in {@link TextLineDecoder}. If this delimiter is used,
* {@link TextLineDecoder} will consider both <tt>'\r'</tt> and
* <tt>'\n'</tt> as a delimiter.
*/
public static final LineDelimiter AUTO = new LineDelimiter("");
所以这里编码器在每一行的最后加上“\n”字符,标记行的结尾。解码器使用的标记我们可以看注释,它可以对应“\r”和“\n”,这样在解析的时候就可以区分一次数据交互的末尾,解决粘包的问题;
和我对接的第三方服务是用c++语言编写的,他们在接收和发送tcp数据时,也使用了这种方法,只不过他们定义的数据结尾是“****”,所以只要调用TextLineCodecFactory的(Charset charset, String encodingDelimiter, String decodingDelimiter)这个构造函数,将对方指定的“****”当作decodingDelimiter,就可以接收数据了,主要代码如下:
//创建客户端连接
NioSocketConnector connector = new NioSocketConnector();
connector.getSessionConfig().setReceiveBufferSize(10240); // 设置接收缓冲区的大小
connector.getSessionConfig().setSendBufferSize(10240);// 设置输出缓冲区的大小
//创建接收数据的过滤器
DefaultIoFilterChainBuilder chain = connector.getFilterChain();
//设置此过滤器为一行一行的读取数据
// 创建接受数据的过滤器
//此处的解码分隔符使用“****”是为了和服务器通信时知晓接收到的数据的终点,来让解码器解析
TextLineCodecFactory textLineCodecFactory = new TextLineCodecFactory(Charset.forName("GBK"),LineDelimiter.UNIX.getValue(),"****");
textLineCodecFactory.setDecoderMaxLineLength(10240);//设置行解码器长度
textLineCodecFactory.setEncoderMaxLineLength(10240);
// 设定这个过滤器将一行一行的读取数据
connector.getFilterChain().addLast("codec1", new ProtocolCodecFilter(textLineCodecFactory));
这样,我们就可以使用mina接收第三方服务返回的数据了,另外需要注意对方的数据编码格式,我这边需要使用“GBK”才能成功接收数据,否则在解析中文字符时会报错。