原文:http://www.cnblogs.com/haiq/archive/2011/08/01/2124292.html

(写的蛮好,保存下来)

由于项目需要,用到了 mina 框架进行 tcp 通讯。我是初次接触 mina,于是从 Hello world 开始学习了 mina 。期间遇到了一个奇怪的发送数据的延迟问题,解决的过程是曲折的,但找出的原因却令我“吐血”(没真的吐……)。不管怎样,还是贴出来一下作反面案例,希望初次学习 mina 的时候能够绕过这个地雷。

 
hello world 演示很简单,分为两部分,server 和 client 。
 
server 在 8800 端口上起 tcp 侦听,将 client 发送来的消息打印到标准输出(System.out)。只有两个类 HelloServer.java 和 HelloHandler.java,代码如下:

public class HelloServer {

public static void main(String[] args) {

IoBuffer.setUseDirectBuffer(false);

IoBuffer.setAllocator(new SimpleBufferAllocator());

IoAcceptor acceptor = new NioSocketAcceptor();

acceptor.getFilterChain().addLast("logger", new LoggingFilter());

acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory(

Charset.forName("UTF-8"))));

acceptor.getSessionConfig().setReadBufferSize(2048);

acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);

acceptor.setHandler(new HelloHandler());

try {

acceptor.bind(new InetSocketAddress(8800));

System.out.println("hello server is running!");

catch (IOException e) {

e.printStackTrace();

}

}

}

 

public class HelloHandler extends IoHandlerAdapter {

@Override

public void sessionCreated(IoSession session) throws Exception {

System.out.println("sessiong created ......");

}

@Override

public void sessionOpened(IoSession session) throws Exception {

System.out.println("session opened ......");

}

@Override

public void sessionClosed(IoSession session) throws Exception {

System.out.println("session closed .");

}

@Override

public void messageReceived(IoSession session, Object message)

throws Exception {

String msgText = message.toString();

System.out.println(getNow() + "message received!-- msg:"

+ msgText);

}

private String getNow(){

Date now = new Date();

DateFormat df = new SimpleDateFormat("[yyyy-MM-dd hh:mm:ss] ");

return df.format(now);

}

@Override

public void messageSent(IoSession session, Object message) throws Exception {

String msgText = message.toString();

System.out.println("message sent!-- msg:" + msgText);

}

@Override

public void exceptionCaught(IoSession session, Throwable cause)

throws Exception {

System.out.println("exception occurred!!! -- " + cause.getMessage());

cause.printStackTrace();

session.close(false);

}

}

 
client 通过 8800 端口连接 server ,发送两条字符串消息。只有两个类 HelloClient.java 和 HelloSender.java, 代码如下:

public class HelloClient {

public static void main(String[] args) {

NioSocketConnector connector = new NioSocketConnector();

connector.setConnectTimeoutMillis(1000 * 15);

connector.getFilterChain().addLast("logger", new LoggingFilter());

connector.getFilterChain().addLast(

"codec",

new ProtocolCodecFilter(new TextLineCodecFactory(Charset

.forName("UTF-8"))));

connector.setHandler(new HelloSender(new String[] {

"Hello message 1 !", "Hello 2" }));

try {

ConnectFuture future = connector.connect(new InetSocketAddress(

"127.0.0.1", 8800));

System.out.println("connect ...");

future.awaitUninterruptibly();

System.out.println("connect future awaitUniterruptibly ...");

IoSession session = future.getSession();

System.out.println("get session");

session.getCloseFuture().awaitUninterruptibly();

System.out.println("session close future awaitUniterruptibly ...");

connector.dispose();

catch (Exception e) {

System.out.println("error !!! --" + e.getMessage());

e.printStackTrace();

}

}

}

 

public class HelloSender extends IoHandlerAdapter {

private String[] msgArray;

public HelloSender(String[] msgArray) {

this.msgArray = msgArray;

}

@Override

public void sessionOpened(IoSession session) throws Exception {

SendMessage(session);

session.close(false);

System.out.println("client handler close session ......");

}

private void SendMessage(IoSession session)throws Exception{

for (int i = 0; i < msgArray.length; i++) {

WriteFuture wf = session.write(msgArray[i]);

wf.addListener(new IoFutureListener<IoFuture>(){

public void operationComplete(IoFuture future) {

System.out.println(getNow() + "futrue -- write completed!");

}

});

System.out.println(getNow() + "--write msg " + i);

Thread.sleep(3000);

}

System.out.println(getNow() + "send completed");

}

private String getNow(){

Date now = new Date();

DateFormat df = new SimpleDateFormat("[yyyy-MM-dd hh:mm:ss] ");

return df.format(now);

}

}

 
先启动服务端,再启动客户端。
 
服务端日志输出如下:
sessiong created ......
session opened ......
[2011-07-13 09:59:19] message received!-- msg:Hello message 1 !
[2011-07-13 09:59:19] message received!-- msg:Hello 2
session closed .
 
 
客户端日志输出如下:
connect ...
connect future awaitUniterruptibly ...
get session
[2011-07-13 09:59:13] --write msg 0
[2011-07-13 09:59:16] --write msg 1
[2011-07-13 09:59:19] send completed
client handler close session ......
[2011-07-13 09:59:19] futrue -- write completed!
[2011-07-13 09:59:19] futrue -- write completed!
session close future awaitUniterruptibly ...
 
仔细观察日志输出就能发现奇怪的问题:客户端是前后相隔 3 秒往 session 中写入数据,但是却在最后同时地发送出去,而服务端也是同时收到这两条消息的,而不是前后相隔 3 秒。
 
一、关于 write 和 flush 的猜想
这也就是说,session 在 write 完成之后并没有立即发送。初步猜测,write 操作时有可能数据只是被放置到缓冲区而已,只有执行 flush 一类的操作才会真正发送出去。
但是 IoSession 类型并没有找到类似 flush 的方法。通过查看源码及 API ,发现 write 的时候,主要逻辑如下:

// Now, we can write the message. First, create a future

WriteFuture writeFuture = new DefaultWriteFuture(this);

WriteRequest writeRequest = new DefaultWriteRequest(message, writeFuture, remoteAddress);

// Then, get the chain and inject the WriteRequest into it

IoFilterChain filterChain = getFilterChain();

filterChain.fireFilterWrite(writeRequest);

每一次写入,都产生一个 WriteRequest 实例,并将该实例传递给 IoSession 的过滤链 filterChain 进行处理。
先说说 IoFilterChain ,其数据结构一个保持 IoFilter 的双向链表。 IoFilterChain 包含两个特殊的 filter : head 和 tail ,用户加入的 filter 都放置于head 和 tail 之间。filterChain.fireFilterWrite(writeRequest) 操作将从 tail 开始往前,逐个地调用 IoFilter 的 filterWrite 方法,最终调用抵达 head 的 filterWrite 方法。想必写入操作就落实在此方法上,进去一探究竟。
 
处于 head 位置的 IoFilter 为 DefaultIoFilter 的内部私有类 HeadFilter 的实例,其主要逻辑如下:

s.getWriteRequestQueue().offer(s, writeRequest);

if (!s.isWriteSuspended()) {

s.getProcessor().flush(s);

}

 
至此,已证实猜测:writeRequest 被放置到一个队列中,当 IoSession 的 WriteSuspended 状态为 false 时,会直接 flush 。但 flush 操作并不是 IoSession 的方法,而是 IoProcessor 的方法。
那就在客户端的 HelloSender 的 SendMessage 方法中语句 WriteFuture wf = session.write(msgArray[i]);   之后加上一句

((AbstractIoSession) session).getProcessor().flush(session);

调试运行 client ,但日志输出和之前一样,两次消息最终还是同时被发送出去的,这招不管用。
跟踪发现 session 的 WriteSuspended 默认就是 false,每次 write 操作都会执行一次 flush,而不需要在 HelloSender 中再次调用 flush 操作。
既然前面补上 flush 操作是多余的,那原因何在?难道 flush 操作中到调用真实的 Channel 进行 send 之间还有缓存?但这与 flush 的语义并不一致。继续查看源码一探究竟。
IoProcessor 的 flush 操作的主要逻辑是把 session 加入到一个 flushingSessions  队列中,然后执行 selector.wakeup 线程执行发送。这 flush 并没有诡异之处,造成先后两次写入的消息同时被发送的原因显然不在于此。
 
二、关于 SocketSessionConfig 的猜想
在如此简单的演示案例中,既然 HelloSender 中的 write 和 flush 操作似乎都没什么问题,难道问题出在 HelloClient 上?
HelloClient 的 main 方法的主要逻辑就以下几步:
1、创建 connector;
2、设置 filterChain;
3、设置 IoHandler 为 HelloSender 的实例;
4、连接服务端,产生会话;
 
这几步操作都是标准的调用,不应该会有问题。但这里面并未对 IoSessionConfig 进行配置,是否是 IoSessionConfig 的默认配置的引起的呢?
于是仔细查看了 IoSessionConfig 的所有属性,并没有属性是与此相关的。
再查看其 IoSessionConfig 派生接口 SocketSessionConfig ,发现有一个 TcpNoDelay 属性。从字面上看,似乎有点关系。
于是在第2步之后加上语句

connector.getSessionConfig().setTcpNoDelay(true);

运行 HelloClient ……

日志输出显示的结果还是一如既往。

NoDelay 是 TCP 层的一个选项,其指示是否将缓存区中的数据合并发送。但在此无论设置为 true 还是 false,都不影响测试的结果。

三、IoHandler 的问题

在进行了以上两方面的尝试后,将怀疑的目光转向了 HelloSender ,这是本例中的 IoHandler 的实现。

发送操作是在 HelloSender 的 sessionOpened 方法执行:

@Override

public void sessionOpened(IoSession session) throws Exception {

SendMessage(session);

session.close(false);

System.out.println("client handler close session ......");

}

当 session 创建后,sessionOpened 便被执行,此时发送数据,并在发送完成后关闭会话的,然后,sessionOpened 方法返回。

仔细想想,似乎在 SendMessage 和 sessionOpened 方法返回之间有些问题:对 sessionOpened 的调用应该是建立连接和会话后初始化过程的一部分,而在初始化过程尚未返回的时候就对 IoSession 写入数据,这也许不是一个恰当的调用方式。也许正是这样,使得先后两次写入的数据都被保持在队列中,直到会话初始化完成后才被处理。在 sessionOpened 方法中创建一个新线程来执行 SendMessage 操作就能验证这一设想,于是 sessionOpened 方法改为如下:

@Override

public void sessionOpened(IoSession session) throws Exception {

final IoSession s = session;

Thread thrd = new Thread(new Runnable() {

public void run() {

try {

SendMessage(s);

Thread.sleep(5000);

s.close(false);

catch (Exception e) {

System.out.println("Send message error!!!--" + e.getMessage());

e.printStackTrace();

}

}

});

thrd.start();

System.out.println("client handler close session ......");

}

 
再次运行测试,日志输出如下:
服务端日志:
hello server is running!
sessiong created ......
session opened ......
[2011-07-14 11:43:01] message received!-- msg:Hello message 1 !
[2011-07-14 11:43:04] message received!-- msg:Hello 2
session closed .
客户端日志:
connect ...
tcpNoDelay=false
connect future awaitUniterruptibly ...
get session
client handler close session ......
[2011-07-14 11:43:01] futrue -- write completed!
[2011-07-14 11:43:01] --write msg 0
[2011-07-14 11:43:04] --write msg 1
[2011-07-14 11:43:04] futrue -- write completed!
[2011-07-14 11:43:07] send completed
session close future awaitUniterruptibly ...
 
日志显示先后间隔 3 秒写入的两条消息在服务端也同样是先后间隔 3 秒被接收到的,一切恢复正常。前面的设想得到证实,原因就是:向 IoSession 写入数据的时机不对。
 
虽然结果是有点雷人,但探索的过程还是收获不少,加深了对 mina 框架的理解。

mina客户端发送消息延迟问题分析的更多相关文章

  1. 使用Java客户端发送消息和消费的应用

    体验链接:https://developer.aliyun.com/adc/scenario/fb1b72ee956a4068a95228066c3a40d6 实验简介 本教程将Demo演示使用jav ...

  2. Netty客户端发送消息并同步获取结果

    客户端发送消息并同步获取结果,其实是违背Netty的设计原则的,但是有时候不得不这么做的话,那么建议进行如下的设计: 比如我们的具体用法如下: NettyRequest request = new N ...

  3. java socket 一个服务器对应多个客户端,可以互相发送消息

    直接上代码,这是网上找的demo,然后自己根据需求做了一定的修改.代码可以直接运行 服务器端: package socket; import java.io.BufferedReader; impor ...

  4. Socket通讯-C#客户端与Java服务端通讯(发送消息和文件)

    设计思路 使用websocket通信,客户端采用C#开发界面,服务端使用Java开发,最终实现Java服务端向C#客户端发送消息和文件,C#客户端实现语音广播的功能. Java服务端设计 packag ...

  5. Spring Boot+Socket实现与html页面的长连接,客户端给服务器端发消息,服务器给客户端轮询发送消息,附案例源码

    功能介绍 客户端给所有在线用户发送消息 客户端给指定在线用户发送消息 服务器给客户端发送消息(轮询方式) 项目搭建 项目结构图 pom.xml <?xml version="1.0&q ...

  6. server-sent-event使用流信息向客户端发送数据

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. socket 服务器向指定的客户端发消息

    一.需求 需求如题. 当多个客户端连接服务器时,服务器如何给指定的客户端发送消息. 二.解决方案 核心思想: 在服务器端,需保存不同客户端的socket列表及客户端相关信息. socket含有发送方和 ...

  8. zookeeper源码分析之三客户端发送请求流程

    znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...

  9. RTMPdump(libRTMP) 源代码分析 8: 发送消息(Message)

    ===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...

随机推荐

  1. h5中的结构元素header、nav、article、aside、section、footer介绍

    结构元素不具有任何样式,只是使页面元素的的语义更加明确. header元素 header元素是一种具有引导和导航作用的的结构元素,该元素可以包含所有通常放在页面头部的内容.header元素通常用来放置 ...

  2. springboot打包的jar项目,不需要安装jdk环境启动

    因为服务器上是JDK7,而springboot至少用JDK8才行,但是又因为是很老的项目,不是很清楚能不能换JDK8,有风险,因此选择以前项目用JDK7,而新的springboot用JDK8.步骤一: ...

  3. CVPR2019目标检测方法进展综述

    CVPR2019目标检测方法进展综述 置顶 2019年03月20日 14:14:04 SIGAI_csdn 阅读数 5869更多 分类专栏: 机器学习 人工智能 AI SIGAI   版权声明:本文为 ...

  4. 怎样理解DOM

    一句话总结: DOM 是一个 js 对象. 他可以赋予 js 控制 html 文档的能力.  全称: Document Object Model. DOM 的最小组成单位是: 节点 , 节点有7种类型 ...

  5. C++中重载函数详解

    函数的重载详解 什么时函数重载: 函数重载是指在同一作用域内,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数.重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,避 ...

  6. 【原创】Linux基础之重定向stdout/stderr

    启动进程后查看日志(stdout和stderr) 1 nohup+tail # nohup $cmd > /path/to/file 2>&1 & # tail -f /p ...

  7. Oracle连接字符串总结(转)

    Oracle XE 标准连接 Oracle XE(或者"Oracle Database 10g Express Edition")是一个简单免费发布的版本. 以下是语法格式: Dr ...

  8. mui APP 微信登录授权

    一.在微信平台上申请appid.appsecret. 二.app --> manifest.json-->SDK配置(填写申请好的appid和appsecret) 三.在登录页,点击微信登 ...

  9. c语言测试芯片好坏

    问题描述有n个(2<n<20)芯片,好的或坏的,并且有比坏的芯片更多的已知的好的芯片.每个芯片都可以用来测试其他芯片.当用一个好的芯片测试其他芯片时,它可以正确地给出被测芯片是好是坏.当用 ...

  10. Oracle中undo表空间的切换

    查看操作系统: SQL>  !cat /etc/redhat-releaseRed Hat Enterprise Linux Server release 7.4 (Maipo)查看数据库版本: ...