- 浏览: 35351 次
文章分类
最新评论
Mina网络应用框架及TCP/UDP开发范例(编著)
个简单的例子
让我们使用一个简单的例子来展示一下Mina的状态机是如何工作的。下面的图片展示了一个录音机的状态机。其中的椭圆是状态,箭头表示事务。每个事务都有一个事件的名字来标记该事务。
初始化时,录音机的状态是空的。当磁带放如录音机的时候,加载的事件被触发,录音机进入到加载状态。在加载的状态下,退出的事件会使录音机进入到空的状态,播放的事件会使加载的状态进入到播放状态。等等......我想你可以推断后后面的结果:)
现在让我们写一些代码。外部(录音机中使用该代码的地方)只能看到
6.1.1 录音机的接口TapeDeck
行为方法,是时。
public interface TapeDeck
{
void load(String nameOfTape);
void eject();
void start();
void pause();
void stop();
}
下面我们开始编写真正执行的代码,这些代码在一个事务被触发时,会在状态机中执行。首先我们定义一个状态。这些状态都使用字符串常量来定义,并且使用@state标记来声明。
状态类,是间,即方法运行完后的状态。
public class TapeDeckHandler
{
@State public static final String EMPTY = "Empty";
@State public static final String LOADED = "Loaded";
@State public static final String PLAYING = "Playing";
@State public static final String PAUSED = "Paused";
}
现在我们已经定义了录音机中的所有状态,我们可以根据每个事务来创建相应的代码。每个事务都和一个TapeDeckHandler的方法对应。每个事务的方法都使用@Transtration标签来声明,这个标签定义了事件的ID,该ID会触发事务的执行。事务开始时的状态使用start,事务结束使用next,事务正在运行使用on。
6.1.2 状态类实现TapeDeckHandler
public class TapeDeckHandler
{
@State public static final String EMPTY = "Empty";
@State public static final String LOADED = "Loaded";
@State public static final String PLAYING = "Playing";
@State public static final String PAUSED = "Paused";
@Transition(on = "load", in = EMPTY, next = LOADED)
public void loadTape(String nameOfTape)
{
System.out.println("Tape '" + nameOfTape + "' loaded");
}
@Transitions({
@Transition(on = "play", in = LOADED, next = PLAYING),
@Transition(on = "play", in = PAUSED, next = PLAYING)
})
public void playTape()
{
System.out.println("Playing tape");
}
@Transition(on = "pause", in = PLAYING, next = PAUSED)
public void pauseTape()
{
System.out.println("Tape paused");
}
@Transition(on = "stop", in = PLAYING, next = LOADED)
public void stopTape()
{
System.out.println("Tape stopped");
}
@Transition(on = "eject", in = LOADED, next = EMPTY)
public void ejectTape()
{
System.out.println("Tape ejected");
}
}
请注意,TapeDeckHandler类没有实现TapeDeck ,呵呵,这是故意的。现在让我们亲密接触一下这个代码。在loadTape方法上的@Transition标签:
@Transition(on = "load", in = EMPTY, next = LOADED)
public void loadTape(String nameOfTape) {}
指定了这个状态后,当录音机处于空状态时,磁带装载事件启动后会触发loadTape方法,并且录音机状态将会变换到Loaded状态。@Transition标签中关于pauseTape,stopTape,ejectTape的方法就不需要在多介绍了。关于playTape的标签和其他的标签看起来不太一样。从上面的图中我们可以知道,当录音机的状态在Loaded或者Paused时,play事件都会播放磁带。当多个事务同时条用同一个方法时,@Transition标签需要按下面的方法使用:
@Transitions({
@Transition(on = "play", in = LOADED, next = PLAYING),
@Transition(on = "play", in = PAUSED, next = PLAYING)
})
public void playTape(){}
@Transition标签清晰的列出了声明的方法被多个事务调用的情况。
###############################################################
要点:更多关于@Transition标签的参数
(1)如果你省略了on参数,系统会将该值默认为“*”,这样任何事件都可以触发该方法。
(2)如果你省略了next参数,系统会将默认值改为“_self_”,这个是和当前的状态相关的,如果你要实现一个循环的事务,你所需要做的就是省略状态机中的next参数。
(3)weight参数用于定义事务的查询顺序,一般的状态的事务是根据weight的值按升序排列的,weight默认的是0.
现在最后一步就是使用声明类创建一个状态机的对象,并且使用这个状态机的实例创建一个代理对象,该代理对象实现了TapeDeck接口:
6.1.3 程序的调用程序
public static void main(String[] args)
{
// 创建录音机事件的句柄
TapeDeckHandler handler = new TapeDeckHandler();
// 创建录音机的状态机
StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY, handler);
// 使用上面的状态机,通过一个代理创建一个TapeDeck的实例
TapeDeck deck = new StateMachineProxyBuilder().create(TapeDeck.class, sm);
// 加载磁带
deck.load("The Knife - Silent Shout");
// 播放
deck.play();
// 暂停
deck.pause();
// 播放
deck.play();
// 停止
deck.stop();
// 退出
deck.eject();
}
TapeDeckHandler handler = new TapeDeckHandler();
StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY, handler);
使用TapeDeckHandler创建一个状态机的实例。StateMachineFactory.getInstance(...)调用的方法中使用的Transition.class 是通知工厂我们使用@Transition 标签创建一个状态机。我们指定了状态机开始时状态是空的。一个状态机是一个基本的指示图。状态对象和图中的节点对应,事务对象和箭头指向的方向对应。我们在TapeDeckHandler中使用的每一个@Transition标签都和一个事务的实例对应。
要点:那么, @Transition和 Transition 有什么不同吗?
@Transition 是你用来标记当事务在状态之间变化时应该使用那个方法。在后台处理中,
Mina的状态机会为MethodTransition中每一个事务标签创建一个事务的实例。MethodTransition实现了Transition 接口。作为一个Mina状态机的使用者,你不用直接使用Transition或者MethodTransition 类型的对象。
录音机TapeDeck的实例是通过调用StateMachineProxyBuilder来创建的:
TapeDeck deck = new StateMachineProxyBuilder().create(TapeDeck.class, sm);
StateMachineProxyBuilder.create()使用的接口需要由代理的对象来实现,状态机的实例将接收由代理产生的事件所触发的方法。当代码执行时,输出的结果如下:
Tape 'The Knife - Silent Shout' loaded
Playing tape
Tape paused
Playing tape
Tape stopped
Tape ejected
要点:这和Mina有什么关系?或许你已经注意到,在这个例子中没有对Mina进行任何配置。但是不要着急。稍后我们将会看到如何为Mina的IoHandler接口创建一个状态机。
6.2 Mnia的工作机制
6.2.1 它是怎样工作的?
让我们走马观花的看看当代理调用一个方法的时候发生了什么。查看一个StateContext(状态的上下文)对象状态上下文之所以重要是因为它保存了当前的状态。代理调用一个方法时,状态上下文会通知StateContextLookup实例去方法的参数中获取一个状态的上下文。一般情况下,StateContextLookup的实现将会循环方法中的参数,并查找一个指定类型的对象,并且使用这个对象反转出一个上下文对象。如果没有声明一个状态上下文,StateContextLookup将会创一个,并将其存放到对象中。
当代理Mina的IoHandler接口时,我们将使用IoSessoinStateContextLookup实例,该实例用来查询一个IoSession中的方法参数。它将会使用 IoSession的属性值为每一个Mina的session来存放一个独立的状态上下文的实例。这中方式下,同样的状态机可以让所有的Mina的会话使用,而不会使每个会话彼此产生影响。
要点:在上面的例子中,当我们使用StateMachineProxyBuilder创建一个代理时,我们一直没有我们一直没有配置StateContextLookup使用哪种实现。如果没有配置,系统会使用SingletonStateContextLookup。
SingletonStateContextLookup总是不理会方法中传递给它的参数,它一直返回一个相同的状态上下文。很明显,这中方式在多个客户端并发的情况下使用同一个同一个状态机是没有意义的。这种情况下的配置会在后面的关于IoHandler的代理配置时进行说明。
将方法请求反转成一个事件对象所有在代理对象上的方法请求都会有代理对象转换成事件对象。一个事件有一个ID或者0个或多个参数。事件的ID和方法的名字相当,事件的参数和方法的参数相当。调用方法deck.load("The Knife - Silent Shout") 相当于事件{id = "load", arguments = ["The Knife - Silent Shout"]}.事件对象中包含一个状态上下文的引用,该状态上下文是当前查找到的。
6.2.2 触发状态机
一旦事件对象被创建,代理会调用StateMachine.handle(Event).方法。StateMachine.handle(Event)遍历事务对象中当前的状态,来查找能够接收当前事件的事务的实例。这个过程会在事务的实例找到后停止。这个查询的顺序是由事务的重量值来决定的(重量值一般在@Transition标签中指定)。
6.2.3 执行事务
最后一部就是在Transition中调用匹配事件对象的Transition.execute(Event)方法。当事件已经执行,这个状态机将更新当前的状态,更新后的值是你在事务中定义的后面的状态。
要点:事务是一个接口。每次你使用@Transition标签时,MethodTransition对象将会被创建。
MethodTransition(方法事务)
MethodTransition非常重要,它还需要一些补充说明。如果事件ID和@Transition标签中的on参数匹配,事件的参数和@Transition中的参数匹配,那么MethodTransition和这个事件匹配。所以如果事件看起来像{id = "foo", arguments = [a, b, c]},那么下面的方法:
@Transition(on = "foo")
public void someMethod(One one, Two two, Three three) { ... }
只和这个事件匹配((a instanceof One && b instanceof Two && c instanceof Three) == true).。当匹配时,这个方法将会被与其匹配的事件使用绑定的参数调用。
要点: Integer, Double, Float,等也和他们的基本类型int, double, float, 等匹配。
因此,上面的状态是一个子集,需要和下面的方法匹配:
@Transition(on = "foo")
public void someMethod(Two two) { ... }
上面的方法和((a instanceof Two || b instanceof Two || c instanceof Two) == true)是等价的。在这种情况下,第一个被匹配的事件的参数将会和该方法绑定,在它被调用的时候。一个方法如果没有参数,在其事件的ID匹配时,仍然会被调用:
@Transition(on = "foo")
public void someMethod() { ... }
这样做让事件的处理变得有点复杂,开始的两个方法的参数和事件的类及状态的上下文接口相匹配。这意味着:
@Transition(on = "foo")
public void someMethod(Event event, StateContext context, One one, Two two, Three three) { ... }
@Transition(on = "foo")
public void someMethod(Event event, One one, Two two, Three three) { ... }
@Transition(on = "foo")
public void someMethod(StateContext context, One one, Two two, Three three) { ... }上面的方法和事件{id = "foo", arguments = [a, b, c]} if ((a instanceof One && b instanceof Two&& c instanceof Three) == true)是匹配的。当前的事件对象和事件的方法绑定,当前的状态上下文和该方法被调用时的上下文绑定。在此之前一个事件的参数的集合将会被使用。当然,一个指定的状态上下文的实现将会被指定,以用来替代通用的上下文接口。
@Transition(on = "foo")
public void someMethod(MyStateContext context, Two two) { ... }
要点:方法中参数的顺序很重要。若方法需要访问当前的事件,它必须被配置为第一个方法参数。当事件为第一个参数的时候,状态上下问不能配置为第二个参数,它也不能配置为第一个方法的参数。事件的参数也要按正确的顺序进行匹配。方法的事务不会在查找匹配事件方法的时候重新排序。
到现在如果你已经掌握了上面的内容,恭喜你!我知道上面的内容会有点难以消化。希望下面的例子能让你对上面的内容有更清晰的了解。注意这个事件Event {id = "messageReceived", arguments = [ArrayList a = [...], Integer b = 1024]}。下面的方法将和这个事件是等价的:
// All method arguments matches all event arguments directly
@Transition(on = "messageReceived")
public void messageReceived(ArrayList l, Integer i) { ... }
// Matches since ((a instanceof List && b instanceof Number) == true)
@Transition(on = "messageReceived")
public void messageReceived(List l, Number n) { ... }
// Matches since ((b instanceof Number) == true)
@Transition(on = "messageReceived")
public void messageReceived(Number n) { ... }
// Methods with no arguments always matches
@Transition(on = "messageReceived")
public void messageReceived() { ... }
// Methods only interested in the current Event or StateContext always matches
@Transition(on = "messageReceived")
public void messageReceived(StateContext context) { ... }
// Matches since ((a instanceof Collection) == true)
@Transition(on = "messageReceived")
public void messageReceived(Event event, Collection c) { ... }
但是下面的方法不会和这个事件相匹配:
// Incorrect ordering
@Transition(on = "messageReceived")
public void messageReceived(Integer i, List l) { ... }
// ((a instanceof LinkedList) == false)
@Transition(on = "messageReceived")
public void messageReceived(LinkedList l, Number n) { ... }
// Event must be first argument
@Transition(on = "messageReceived")
public void messageReceived(ArrayList l, Event event) { ... }
// StateContext must be second argument if Event is used
@Transition(on = "messageReceived")
public void messageReceived(Event event, ArrayList l, StateContext context) { ... }
// Event must come before StateContext
@Transition(on = "messageReceived")
public void messageReceived(StateContext context, Event event) { ... }
6.2.4 状态继承
状态的实例将会有一个父类的状态。如果StateMachine.handle(Event)的方法不能找到一个事务和当前的事件在当前的状态中匹配,它将会寻找父类中的装。如果仍然没有找到,那么事务将会自动寻找父类的父类,知道找到为止。这个特性很有用,当你想为所有的状态添加一些通用的代码时,不需要为每一个状态的方法来声明事务。这里你可以创建一个类的继承体系,使用下面的方法即可:
@State public static final String A = "A";
@State(A) public static final String B = "A->B";
@State(A) public static final String C = "A->C";
@State(B) public static final String D = "A->B->D";
@State(C) public static final String E = "A->C->E";
使用状态继承来处理错误信息让我们回到录音机的例子。如果录音机里没有磁带,当你调用deck.play()方法时将会怎样?让我们试试:
示例代码:
public static void main(String[] args) {
...
deck.load("The Knife - Silent Shout");
deck.play();
deck.pause();
deck.play();
deck.stop();
deck.eject();
deck.play();
}
运行结果:
Tape stopped
Tape ejected
Exception in thread "main" o.a.m.sm.event.UnhandledEventException:
Unhandled event: org.apache.mina.statemachine.event.Event@15eb0a9[id=play,...]
at org.apache.mina.statemachine.StateMachine.handle(StateMachine.java:285)
at org.apache.mina.statemachine.StateMachine.processEvents(StateMachine.java:142)
...
哦,我们得到了一个无法处理的异常UnhandledEventException,这是因为在录音机的空状态时,没有事务来处理播放的状态。我们将添加一个指定的事务来处理所有不能匹配的事件。
@Transitions({
@Transition(on = "*", in = EMPTY, weight = 100),
@Transition(on = "*", in = LOADED, weight = 100),
@Transition(on = "*", in = PLAYING, weight = 100),
@Transition(on = "*", in = PAUSED, weight = 100)
})
public void error(Event event) {
System.out.println("Cannot '" + event.getId() + "' at this time");
}
现在当你运行上面的main()方法时,你将不会再得到一个异常,输出如下:
...
Tape stopped
Tape ejected
Cannot 'play' at this time.
现在这些看起来运行的都很好,是吗?但是如果们有30个状态而不是4个,那该怎么办?那么我们需要在上面的错误方法处理中配置30个事务的声明。这样不好。让我们用状态继承来解决:
public static class TapeDeckHandler
{
@State public static final String ROOT = "Root";
@State(ROOT) public static final String EMPTY = "Empty";
@State(ROOT) public static final String LOADED = "Loaded";
@State(ROOT) public static final String PLAYING = "Playing";
@State(ROOT) public static final String PAUSED = "Paused";
...
@Transition(on = "*", in = ROOT)
public void error(Event event) {
System.out.println("Cannot '" + event.getId() + "' at this time");
}
}
这个运行的结果和上面的是一样的,但是它比要每个方法都配置声明要简单的多。
6.2.5 Mina的状态机和IoHandler配合使用
现在我们将上面的录音机程序改造成一个TCP服务器,并扩展一些方法。服务器将接收一些命令类似于:
load <tape>, play, stop等等。服务器响应的信息将会是+ <message>或者是- <message>。协议是基于
Mina自身提供的一个文本协议,所有的命令和响应编码都是基于UTF-8。这里有一个简单的会话示例:
telnet localhost 12345
S: + Greetings from your tape deck!
C: list
S: + (1: "The Knife - Silent Shout", 2: "Kings of convenience - Riot on an empty street")
C: load 1
S: + "The Knife - Silent Shout" loaded
C: play
S: + Playing "The Knife - Silent Shout"
C: pause
S: + "The Knife - Silent Shout" paused
C: play
S: + Playing "The Knife - Silent Shout"
C: info
S: + Tape deck is playing. Current tape: "The Knife - Silent Shout"
C: eject
S: - Cannot eject while playing
C: stop
S: + "The Knife - Silent Shout" stopped
C: eject
S: + "The Knife - Silent Shout" ejected
C: quit
S: + Bye! Please come back!
该程序完整的代码在org.apache.mina.example.tapedeck包中,这个可以通过检出Mina源码的SVN库中的mina-example来得到。代码使用。
MinaProtocolCodecFilter来编解码传输的二进数据对象。这里只是为每个状态对服务器的请求实现了一个简单的编解码器。在此不在对Mina中编解码的实现做过多的讲解。现在我们看一下这个服务器是如何工作的。这里面一个重要的类就是实现了录音机程序的TapeDeckServer类。这里我们要做的第一件事情就是去定义这些状态:
@State public static final String ROOT = "Root";
@State(ROOT) public static final String EMPTY = "Empty";
@State(ROOT) public static final String LOADED = "Loaded";
@State(ROOT) public static final String PLAYING = "Playing";
@State(ROOT) public static final String PAUSED = "Paused";
在这里没有什么新增的内容。然而,但是处理这些事件的方法看起来将会不一样。让我们看看playTape的方法。
@IoHandlerTransitions({
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = LOADED, next = PLAYING),
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = PAUSED, next = PLAYING)
})
public void playTape(TapeDeckContext context, IoSession session, PlayCommand cmd) {
session.write("+ Playing \"" + context.tapeName + "\"");
}
这里没有使用通用的@Transition和@Transitions的事务声明,而是使用了Mina指定的 @IoHandlerTransition和@IoHandlerTransitions声明。当为Mina的IoHandler创建一个状态机时,它会选择让你使用Java enum (枚举)类型来替代我们上面使用的字符串类型。这个在Mina的IoFilter中也是一样的。我们现在使用MESSAGE_RECEIVED来替代"play"来作为事件的名字(on是@IoHandlerTransition的一个属性)。这个常量是在org.apache.mina.statemachine.event.IoHandlerEvents中定义的,它的值是"messageReceived",这个和Mina的IoHandler中的messageReceived()方法是一致的。谢谢Java 5中的静态导入,我们在使用该变量的时候就不用再通过类的名字来调用该常量,我们只需要按下面的方法导入该类:
import static org.apache.mina.statemachine.event.IoHandlerEvents.*;
这样状态内容就被导入了。另外一个要改变的内容是我们自定了一个StateContext状态上下文的实现--TapeDeckContext。这个类主要是用于返回当前录音机的状态的名字。
static class TapeDeckContext extends AbstractStateContext
{
public String tapeName;
}
要点:为什么不把状态的名字保存到IoSession中?我们可以将录音机状态的名字保存到IoSession中,但是使用一个自定义的StateContext来保存这个状态将会使这个类型更加安全。
最后需要注意的事情是playTape()方法使用了PlayCommand命令来作为它的最后的一个参数。最后一个参数和IoHandler's messageReceived(IoSession session, Object message)方法匹配。这意味着只有在客户端发送的信息被编码成layCommand命令时,该方法才会被调用。在录音机开始进行播放前,它要做的事情就是要装载磁带。当装载的命令从客户端发送过来时,服务器提供的磁带的数字代号将会从磁带列表中将可用的磁带的名字取出:
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = EMPTY, next = LOADED)
public void loadTape(TapeDeckContext context, IoSession session, LoadCommand cmd) {
if (cmd.getTapeNumber() < 1 || cmd.getTapeNumber() > tapes.length) {
session.write("- Unknown tape number: " + cmd.getTapeNumber());
StateControl.breakAndGotoNext(EMPTY);
} else {
context.tapeName = tapes[cmd.getTapeNumber() - 1];
session.write("+ \"" + context.tapeName + "\" loaded");
}
}
这段代码使用了StateControl状态控制器来重写了下一个状态。如果用户指定了一个非法的数字,我们将不会将加载状态删除,而是使用一个空状态来代替。代码如下所示:StateControl.breakAndGotoNext(EMPTY);
状态控制器将会在后面的章节中详细的讲述。connect()方法将会在Mina开启一个会话并调用sessionOpened()方法时触发。@IoHandlerTransition(on = SESSION_OPENED, in = EMPTY)
public void connect(IoSession session) {
session.write("+ Greetings from your tape deck!");
}
它所做的工作就是向客户端发送欢迎的信息。状态机将会保持空的状态。pauseTape(), stopTape()和 ejectTape() 方法和 playTape()很相似。这里不再进行过多的讲述。listTapes(), info()和 quit() 方法也很容易理,也不再进行过多的讲解。请注意后面的三个方法是在根状态下使用的。这意味着listTapes(), info()和 quit() 可以在任何状态中使用。
现在让我们看一下错误处理。error()将会在客户端发送一个非法的操作时触发:
@IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT, weight = 10)
public void error(Event event, StateContext context, IoSession session, Command cmd) {
session.write("- Cannot " + cmd.getName() + " while "
+ context.getCurrentState().getId().toLowerCase());
}error()已经被指定了一个高于listTapes(), info()和 quit() 的重量值来阻止客户端调用上面的方法。注意error()方法是怎样使用状态上下文来保存当前状态的ID的。字符串常量值由@State annotation (Empty, Loaded etc) 声明。这个将会由Mina的状态机当成状态的ID来使用。
commandSyntaxError()方法将会在ProtocolDecoder抛CommandSyntaxException异常时被调用。它将会简单的输出客户端发送的信息不能解码为一个状态命令。
exceptionCaught() 方法将会在任何异常发生时调用,除CommandSyntaxException 异常(这个异常有一个较高的重量值)。它将会立刻关闭会话。最后一个@IoHandlerTransition的方法是unhandledEvent(),它将会在@IoHandlerTransition中的方法没有事件匹配时调用。我们需要这个方法是因为我们没有@IoHandlerTransition的方法来处理所有可能的事件 (例如:我们没有处理messageSent(Event)方法)。没有这个方法,Mina的状态机将会在执行一个事件的时候抛出一个异常。最后一点我们要看的是那个类创建了IoHandler的代理,main()方法也在其中:
private static IoHandler createIoHandler() {
StateMachine sm = StateMachineFactory.getInstance(IoHandlerTransition.class).create(EMPTY, new TapeDeckServer());
return new StateMachineProxyBuilder().setStateContextLookup(
new IoSessionStateContextLookup(new StateContextFactory() {
public StateContext create() {
return new TapeDeckContext();
}
})).create(IoHandler.class, sm);
}
// This code will work with MINA 1.0/1.1:
public static void main(String[] args) throws Exception {
SocketAcceptor acceptor = new SocketAcceptor();
SocketAcceptorConfig config = new SocketAcceptorConfig();
config.setReuseAddress(true);
ProtocolCodecFilter pcf = new ProtocolCodecFilter(
new TextLineEncoder(), new CommandDecoder());
config.getFilterChain().addLast("codec", pcf);
acceptor.bind(new InetSocketAddress(12345), createIoHandler(), config);
}
// This code will work with MINA trunk:
public static void main(String[] args) throws Exception {
SocketAcceptor acceptor = new NioSocketAcceptor();
acceptor.setReuseAddress(true);
ProtocolCodecFilter pcf = new ProtocolCodecFilter(
new TextLineEncoder(), new CommandDecoder());
acceptor.getFilterChain().addLast("codec", pcf);
acceptor.setHandler(createIoHandler());
acceptor.setLocalAddress(new InetSocketAddress(PORT));
acceptor.bind();
}
createIoHandler() 方法创建了一个状态机,这个和我们之前所做的相似。除了我们一个IoHandlerTransition.class类来代替Transition.class在StateMachineFactory.getInstance(...)方法中。这是我们在使用 @IoHandlerTransition声明的时候必须要做的。当然这时我们使用了一个IoSessionStateContextLookup和一个自定义的StateContextFactory类,这个在我们创建一个IoHandler代理时被使用到了。如果我们没有使用IoSessionStateContextLookup,那么所有的客户端将会使用同一个状态机,这是我们不希望看到的。
main()方法创建了SocketAcceptor实例,并且绑定了一个ProtocolCodecFilter,它用于编解码命令对象。最后它绑定了12345端口和IoHandler的实例。这个oHandler实例是由createIoHandler()方法创建的。
7 Mian的异步和同步
7.1 分析
在解释Half Sync/Half Async模式之前,先介绍一个亲身经历的项目。曾经使用一个通讯支撑模块EMF,该模块完成了底层的socket通讯功能,和外部应用建立长连接,同时为上层应用提供一个回调接口如下:
public interface Hook
{
void callback(Message msg);
}
上层应用可以根据自己业务逻辑的需要,实现该接口。
class MyHook implements Hook{
public void callback(Message msg)
{
//应用的业务逻辑
}
}
然后应用可以将该实现注册到通讯支撑模块中。
//注册应用的回调实现
EmfHook hook = new MyHook();
EmfService.regist(hook);
在EMF的实现中负责将受到的socket数据拼装成应用需要的消息结构,然后在一定的匹配规则,找到一个应用的Hook,回调之(其实是多个hook,有可能多个应用对同一种消息感兴趣,简化之)。
//从socket上收到数据,Byte[] buf=.....;
//将数据解析成应用的消息结构Message msg=parse(buf);
//根据消息内容查找相应的EmfHookEmfHook hook = findHook(msg);
//调用应用的实现hook.callback(msg);
一开始,这个模块运行的很好,性能不错,应用扩展也很方便,大家都很是满意,看来这个月的kpi一定不错,呵呵。然有一天,在做压力测试的时候发现,性能很差了,好久才能处理一条消息,数据吞吐量非常小,cpu却也不忙。这是怎么回事哪?
后来发现问题在于EMF的socket数据读取并解析工作和上层应用的hook操作是在一个线程中的,而某些应用的Hook实现有一些比较耗时操作,而所以导致在执行上层应用操作的时候,EMF并没有去进行I/O操作,读取数据,整个系统停在哪儿了。
最后稍作改动,将hook.callback(data)放在另外一个线程池中,这样EMF的线程和应用逻辑的线程就不再相互干扰,性能大大的提供。
从这个项目中得到的一个教训是:[size=small;]I/O密集操作的线程应该和业务逻辑的线程尽量分开[/size]。
7.2 引用
半同步半异步(Half Sync/Half Async)体系结构模式将并发系统中的异步和同步服务分开,简化了编程,同时又没有降低性能。
通俗一点说,Half Sync/Half Async模式其实就是将异步请求排队到一个同步队列中,然后再从队列中取出请求处理,其实就是一个扩大的生产者/消费者问题。在socket编程的关键在于,将业务逻辑操作和底层的io操作分散到不同的线程中,避免业务逻辑操作可能导致整个线程的堵塞。
在mina中也存在Half Sync/Half Async模式,在默认情况下,mina的业务逻辑处理接口IoHandler的实现类就是在NioProcessor的worker现在中执行的(NioProcessor的执行机制请参考)。如果业务逻辑接口堵塞或者耗时都将导致NioProcessor线程无法充分利用。
ExecutorFilter
IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory( Charset.forName( "UTF-8" ))));
acceptor.getFilterChain().addLast("executor", new ExecutorFilter());
acceptor.setHandler( new TimeServerHandler() );
acceptor.getSessionConfig().setReadBufferSize( 2048 );
acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );
acceptor.bind( new InetSocketAddress(PORT) );
如上述代码,通过在FilterChain中增加一个ExecutorFilter,mina将NioProcessor的IO操作线程和TimeServerHandler的业务处理线程分开了。在上述例子中,当NioProcessor收到socket上的数据,filter和handler的执行顺序为:logger->codec->executor->TimeServerHandler(有关IoFilter内容具体请参考http://uniseraph.javaeye.com/blog/228194)。其中logger->codec就在NioProcessor的工作者线程worker中,而executor和TimeServerHandler则是在executorFilter的线程池中。具体参考代码如下:
private ExecutorFilter(Executor executor, boolean createdExecutor, IoEventType... eventTypes) {
if (executor == null) {
throw new NullPointerException("executor");
}/*这里将evenType初始化为所有事件类型的集合*/
if (eventTypes == null || eventTypes.length == 0) {
eventTypes = new IoEventType[] { IoEventType.EXCEPTION_CAUGHT,
IoEventType.MESSAGE_RECEIVED, IoEventType.MESSAGE_SENT,
IoEventType.SESSION_CLOSED, IoEventType.SESSION_IDLE,
IoEventType.SESSION_OPENED, };
}
//下面省略
}
public final void messageReceived(NextFilter nextFilter, IoSession session,
Object message) {
/*executorFilter对该事件有效*/ if (eventTypes.contains(IoEventType.MESSAGE_RECEIVED)) {
fireEvent(new IoFilterEvent(nextFilter,
IoEventType.MESSAGE_RECEIVED, session, message));
} else {
nextFilter.messageReceived(session, message);
}
}
protected void fireEvent(IoFilterEvent event) {
getExecutor().execute(event);
}
由上可知,如果executorFilter对某个事件有效,那么将在线程池中执行该事件,如果无效则在原有的NioProcessor.Worker线程中执行下一个IoFilter或者IoHandler。当然也可以不用ExecutorFilter,而是在IoHandler的实现类中放一个线程池,但是多数情况下是没有这样的必要。
8 Mina的reactor机制
8.1 Reactor概念
从上面来两个图可以看出:与传统的单个Reactor模式实现不同,mina中采用了Multiple Reactor的方式。NioSocketAcceptor和NioProcessor使用不同selector,能够更加充分的榨取服务器的性能。
8.1.1 acctptor主要负责
(1) 绑定一个/多个端口,开始监听(五查询)
(2) 处理客户端的建链请求 (建场所)connect
(3) 关闭一个/多个监听端口 (断链接)close
8.1.2 processor主要负责
(1) 接受客户端发送的数据,并转发给业务逻辑成处理
(2) 发送数据到客户端
8.1.3 实例说明
首先看一个最简单的mina服务端程序,
SocketAcceptor acceptor = new NioSocketAcceptor();//设定一个事件处理器
acceptor.setHandler(new EchoProtocolHandler()); //绑定一个监听端口
acceptor.bind(new InetSocketAddress(PORT));
8.2 NioSocketAcceptor的机制 (招生处)
8.2.1 构造
在NioSocketAcceptor的构造函数中,把NioProcessor也构造出来了
protected AbstractPollingIoAcceptor(IoSessionConfig sessionConfig,
Class<? extends IoProcessor<T>> processorClass)
{
this(sessionConfig, null, new SimpleIoProcessorPool<T>(processorClass), true);
}
8.2.2 建立池(装备处)
为用户来的信息创建空间;即信号量大小,可以有多少处理线程。
SimpleIoProcessorPool是一个NioProcessor池,默认大小是cpu个数+1,这样能够充分利用多核的cpu。private static final int DEFAULT_SIZE =Runtime.getRuntime().availableProcessors() + 1;
public SimpleIoProcessorPool(Class<? extends IoProcessor<T>> processorType)
{
this(processorType, null, DEFAULT_SIZE);
}
8.2.3 初始化
创建一个部门;调用init方法,准备selector
protected void init() throws Exception
{
selector = Selector.open();
}
到此,生产线已经装配好,就等按下开关,就可以正常运行了。
8.2.4 绑定端口
给部门一个业务。在调用NioSocketAcceptor.bind()函数的时候最终调用,AbstractPollingIoAcceptor.bind0,这是NioSocketAcceptor的关键所在
protected final Set<SocketAddress> bind0(
List<? extends SocketAddress> localAddresses) throws Exception
{
/*2.1.1registerQueue是一个待监听动作列表,每次要新绑定一个端口,增加一个register动作,在工作者线程中处理*/
AcceptorOperationFuture request = new AcceptorOperationFuture( localAddresses);
registerQueue.add(request);
/*2.1.2 创建并启动工作者线程*/
startupWorker();
wakeup();
//堵塞直至监听成功
request.awaitUninterruptibly();
//以下省略
}
通过实现生产者/消费者问题来处理绑定端口开始监听动作,其中生产者是bind动作,消费者在Work.run方法中,registQueue是消息队列
8.2.5 等待请求
堵塞直至绑定监听端口成功。招生处至到有学生来。
public class DefaultIoFuture implements IoFuture
{
public IoFuture awaitUninterruptibly()
{
synchronized (lock)
{
while (!ready)
{//当ready为true时候,跳出循环
waiters++;
try
{
lock.wait(DEAD_LOCK_CHECK_INTERVAL);
} catch (InterruptedException ie)
{
// Do nothing : this catch is just mandatory by contract
} finally
{
waiters--;
if (!ready)
{
checkDeadLock();
}
}
}
}
return this;
}
}
真正的绑定端口开始监听动作是在Woker线程中执行的 ,取消绑定操作与绑定操作类似,暂时先不描述。 AbstractPollingIoAcceptor的工作者线程是NioSocketAcceptor的核心所在,完成了以下三个主要功能:
1、处理绑定监听端口请求
2、处理取消监听端口绑定请求
3、处理socket连接请求
1,2主要是在系统初始化或者系统关闭的时候在registerQuerue/cancelQueue中增加一个消息,3是系统运行时NioSocketAcceptor处理socket建链请求的关键。1,2,3的主要内容在工作者线程Work.run方法中。 NioSocketAcceptor继承自父类AbstractPollingIoAcceptor的Worker.run。
private class Worker implements Runnable
{
public void run()
{
int nHandles = 0;
while (selectable)
{
try {
boolean selected = select();
// this actually sets the selector to OP_ACCEPT,
// and binds to the port in which this class will
// listen on
nHandles += registerHandles();
if (selected)
{
processHandles(selectedHandles());
}
// check to see if any cancellation request has been made.
nHandles -= unregisterHandles();
if (nHandles == 0) {
synchronized (lock) {
if (registerQueue.isEmpty()
&& cancelQueue.isEmpty()) {
worker = null;
break;
}
}
}
} catch (Throwable e) {
ExceptionMonitor.getInstance().exceptionCaught(e);
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
ExceptionMonitor.getInstance().exceptionCaught(e1);
}
}
}
if (selectable && isDisposing())
{
selectable = false;
try {
if (createdProcessor) {
processor.dispose();
}
} finally {
try {
synchronized (disposalLock) {
if (isDisposing()) {
destroy();
}
}
} catch (Exception e) {
ExceptionMonitor.getInstance().exceptionCaught(e);
} finally {
disposalFuture.setDone();
}
}
}
}
8.2.5.1 监听连接请求循环
3.1 首先系统在一个无限循环中不停的允许,处理socket建链请求/端口监听/取消端口监听请求,selectable是一个标志位,初始化的时候置为true,要关闭时候置为false,则系统退出循环。
3.2 使用selector监听是否有连接请求操作。
public void run()
{
int nHandles = 0;
while (selectable)
{
try {
// Detect if we have some keys ready to be processed
boolean selected = select();
//以下省略
}
}
}
注意,虽然 boolean selected = select();是一个堵塞操作,但是run方法不会陷入死循环。因为即使没有新的连接请求到达,但是每次bind/unbind都会调用NioSocketAccepto.wakeup唤醒处于select状态的selctor。
protected void wakeup()
{
selector.wakeup();
}
而系统退出是,关闭的acceptor的dispose方法最终会调用unbind,所以退出时不会有问题。 3.2 从registerQueue中获取绑定请求消息,开始绑定某个端口,并开始监听,准备相关上下文信息
public void run()
{
int nHandles = 0;
while (selectable)
{
try {
// Detect if we have some keys ready to be processed
boolean selected = select();
// this actually sets the selector to OP_ACCEPT,
// and binds to the port in which this class will
// listen on
nHandles += registerHandles();
//以下省略
}
对于registerHandleres真正完成了对端口的监听
private int registerHandles()
{
for (;;)
{
AcceptorOperationFuture future = registerQueue.poll();
if (future == null)
{
return 0;
}
Map<SocketAddress, H> newHandles = new HashMap<SocketAddress, H>();
List<SocketAddress> localAddresses = future.getLocalAddresses();
try {
for (SocketAddress a : localAddresses)
{
H handle = open(a);
newHandles.put(localAddress(handle), handle);
}
boundHandles.putAll(newHandles);
// and notify.
future.setDone();
return newHandles.size();
} catch (Exception e)
{
future.setException(e);
} finally
{
// Roll back if failed to bind all addresses.
if (future.getException() != null) {
for (H handle : newHandles.values()) {
try {
close(handle);
} catch (Exception e) {
ExceptionMonitor.getInstance().exceptionCaught(e);
}
}
wakeup();
}
}
}
}
3.2.1 registerHandlers将registerQueue中的所有绑定监听消息取出来,在循环中处理,注意一个细节是,一个绑定监听消息AcceptorOperationFuture可能要绑定监听多个ip的同一个端口。3.2.2 registerHandlers的一个关键动作是 H handle = open(a);参看NioSocketAcceptor.open方法的实现,发现这就是关键
protected ServerSocketChannel open(SocketAddress localAddress)
throws Exception
{
ServerSocketChannel c = ServerSocketChannel.open();
boolean success = false;
try {
c.configureBlocking(false);
// Configure the server socket,
c.socket().setReuseAddress(isReuseAddress());
// XXX: Do we need to provide this property? (I think we need to remove it.)
c.socket().setReceiveBufferSize(
getSessionConfig().getReceiveBufferSize());
// and bind.
c.socket().bind(localAddress, getBacklog());
c.register(selector, SelectionKey.OP_ACCEPT);
success = true;
} finally {
if (!success) {
close(c);
}
}
return c;
}
8.2.5.2 开始监听一个端口
在这个方法中真正开始监听一个端口,并设置相关细节
1.采用非堵塞方式
2.可从用端口
3.socket读取的缓冲区大小
4.listen队列的长度
并且在selector中注册了对SelectionKey.OP_ACCEPT的关注。 3.2.3通过 future.setDone();唤醒bind线程,告知绑定操作已经成功,否则bind线程还将继续堵塞。 3.3如果发现有连接请求过来,则处理之
private class Worker implements Runnable
{
public void run()
{
int nHandles = 0;
while (selectable)
{
try {
// Detect if we have some keys ready to be processed
boolean selected = select();
// this actually sets the selector to OP_ACCEPT,
// and binds to the port in which this class will
// listen on
nHandles += registerHandles();
if (selected)
{
processHandles(selectedHandles());
}
8.2.5.3 完成新建一个连接
processHandles是负责完成新建一个连接,并将这个连接交给NioProcessor监控
private void processHandles(Iterator<H> handles) throws Exception
{
while (handles.hasNext())
{
H handle = handles.next();
handles.remove();
T session = accept(processor, handle);
if (session == null)
{
break;
}
finishSessionInitialization(session, null, null);
// add the session to the SocketIoProcessor
session.getProcessor().add(session);
}
}
3.3.1 accept方法创建了一个NioSocketSession, 3.3.2 finishSessionInitialization对这个session进行初始化,3.3.3将session交给NioProcessor管理,这里有两个地方需要注意。3.3.3.1 processor是一个SimpleIoProcessorPool,里面有个多个NioProcessor,SimpleIoProcessor将轮询取一个processor负责管理新建的NioSocketSession。3.3.3.2 NioProcessor.add方法只是将NioSocketSession放到一个newSessions的队列中,并启动NioProcessor的工作者线程。不会马上生效,要等NioProcessor的worker线程执行addNew的时候,才会真正开始管理新增的session,这个与Acceptor的bind类似。
public final void add(T session)
{
if (isDisposing())
{
throw new IllegalStateException("Already disposed.");
}
newSessions.add(session);
startupWorker();
}
至此NioSocketAcceptor的基本实现已经描述完毕,相信读者对也有一个初步的认识。(思考:1.描述一下NioSocketAcceptor处理一个新连接请求的全过程?
2. 下面代码中,mina做了什么,是怎么关闭监听端口的? NioSocketAcceptor acceptor = new NioSocketAcceptor(); accetpro.dispose();)
8.3 NioProcessor 的机制(财务处、装备处)
8.3.1 基本功能
NioProcessor是mina中的另一个核心部分,与NioSocketAcceptor类似,NioProcessor三个主要功能是:
1、接受一个NioSession
2、出来NioSession上的read、write等事件
3、关闭一个NioSession
与NioSocketAcceptor类似,NioProcessor的实现采用了template模式,以上功能整体流程在NioProcessor的父类AbstractPollingIoProcessor中基本完成了,NioSocketAcceptor只是针对Nio的情况完成实现。如上图,NioSocketAcceptor创建了SimpleIoProcessorPool,SimpleIoProcessorPool中默认存在cpu数+1个NioProcessor,并且这些NioProcessor的工作者线程共享一个线程池。(开通了N条跑道,可以同时有N架飞机起落。)(1)接受一个NioSession :与NioSocketAcceptor新增一个端口绑定类似,NioProcessor.addSession只是将NioSocketAcceptor新建的NioSession放入一个消息队列中,由工作者线程负责初始化该NioSession,在selector为该session注册OP_READ事件。(2)关闭一个NioSession:与接受一个NioSession类似,不再描述。(3)NioSession的数据处理:为NioProcessor继承自AbstractPollingIoProcessor的工作者线程中完成主要功能。
8.3.2 Selector事件管理(财务处)
注意在NioProcessor中的select超时时间为1秒,这意味着最多一秒钟的时候,NioProcessor.Worker线程唤醒一次。而在NioSocketAcceptor.Work.run中select是没有超时时间的。下面Worker线程两次唤醒之间简称为一个周期,易知一个周期的长度小于等于一秒。
public void run()
{
int nSessions = 0;
lastIdleCheckTime = System.currentTimeMillis();
for (;;)
{
try {
boolean selected = select(1000);
nSessions += add();
updateTrafficMask();
if (selected)
{
process();
}
long currentTime = System.currentTimeMillis();
flush(currentTime);
nSessions -= remove();
notifyIdleSessions(currentTime);
if (nSessions == 0)
{
synchronized (lock)
{
if (newSessions.isEmpty() && isSelectorEmpty()) {
worker = null;
break;
}
}
}
// Disconnect all sessions immediately if disposal has been
// requested so that we exit this loop eventually.
if (isDisposing())
{
for (Iterator<T> i = allSessions(); i.hasNext(); )
{
scheduleRemove(i.next());
}
wakeup();
}
} catch (Throwable t)
{
ExceptionMonitor.getInstance().exceptionCaught(t);
try
{
Thread.sleep(1000);
} catch (InterruptedException e1)
{
ExceptionMonitor.getInstance().exceptionCaught(e1);
}
}
}
try {
synchronized (disposalLock)
{
if (isDisposing())
{
dispose0();
}
}
} catch (Throwable t)
{
ExceptionMonitor.getInstance().exceptionCaught(t);
} finally
{
disposalFuture.setValue(true);
}
}
8.3.2.1 获取一个请求队列
在addSession中从新增session队列newSeesion获取一个新增NioSession,并开始监控之。
private int add()
{
int addedSessions = 0;
for (;;)
{
T session = newSessions.poll();
if (session == null)
{
break;
}
if (addNow(session))
{
addedSessions ++;
}
}
return addedSessions;
}
2.1 在addNow(T session)中完成了单个NioSession的初始化
private boolean addNow(T session)
{
boolean registered = false;
boolean notified = false;
try
{
init(session);
registered = true;
// Build the filter chain of this session.
session.getService().getFilterChainBuilder().buildFilterChain(
session.getFilterChain());
// DefaultIoFilterChain.CONNECT_FUTURE is cleared inside here
// in AbstractIoFilterChain.fireSessionOpened().
((AbstractIoService) session.getService()).getListeners().fireSessionCreated(session);
notified = true;
} catch (Throwable e)
{
if (notified)
{
scheduleRemove(session);
session.getFilterChain().fireExceptionCaught(e);
wakeup();
} else
{
ExceptionMonitor.getInstance().exceptionCaught(e);
try
{
destroy(session);
} catch (Exception e1)
{
ExceptionMonitor.getInstance().exceptionCaught(e1);
} finally
{
registered = false;
}
}
}
return registered;
}
8.3.2.2 触发一个请求事件
2.1.1 初始化session,这里是Template Method的又一个体现,因为不同的类型的session初始化实现不同,在NioProcessor中包括:设置非堵塞模式.为该session注册OP_READ事件。
@Override
protected void init(NioSession session) throws Exception
{
SelectableChannel ch = (SelectableChannel) session.getChannel();
ch.configureBlocking(false);
session.setSelectionKey(ch.register(selector, SelectionKey.OP_READ, session));
}
2.1.2 构建IoFilterChain,具体请参考http://uniseraph.javaeye.com/blog/228194
2.1.3 触发NioSession上的相关事件,依次为sessionCreated->sessionOpened->
IoServiceListener的sessionCreated。从trafficControllingSessions获取需要修改session的traffic参数,具体与新增NioSession类似,不再详细描述。
8.3.3 Process响应处理(装备处)
关键内容来了,如果有NioSession的channel处理于OP_READ状态,则处理之
if (selected)
{
process();
}
process方法对于所有发生了OP_READ或OP_WRITE的NioSession依次进行处理,注意虽然在NioSession初始化的时候只注册了OP_READ事件,但是在上一周期调用session.write方法的时候,上一周期的flush方法将会注册OP_WRITE方法。本周期发送的数据都是上一周期确定的。
8.3.3.1 状态查询
private void process(T session)
{
if (isReadable(session) && session.getTrafficMask().isReadable())
{
read(session);
}
if (isWritable(session) && session.getTrafficMask().isWritable())
{
scheduleFlush(session);
}
}
在read方法中读取socket上的数据,调用发IoFilter,逐层传递到IoHander(具体参考http://uniseraph.javaeye.com/blog/228194);如果读到-1,则增加一个关闭连接消息到队列中;如果发生异常,则异常调用IoFilter和IoHandler的fireExceptionCaught方法
8.3.3.2 接收响应
private void read(T session)
{
IoSessionConfig config = session.getConfig();
IoBuffer buf = IoBuffer.allocate(config.getReadBufferSize());
final boolean hasFragmentation =
session.getTransportMetadata().hasFragmentation();
try
{
int readBytes = 0;
int ret;
try {
if (hasFragmentation)
{
while ((ret = read(session, buf)) > 0)
{
readBytes += ret;
if (!buf.hasRemaining())
{
break;
}
}
} else
{
ret = read(session, buf);
if (ret > 0)
{
readBytes = ret;
}
}
} finally
{
buf.flip();
}
if (readBytes > 0)
{
session.getFilterChain().fireMessageReceived(buf);
buf = null;
if (hasFragmentation)
{
if (readBytes << 1 < config.getReadBufferSize())
{
session.decreaseReadBufferSize();
} else if (readBytes == config.getReadBufferSize())
{
session.increaseReadBufferSize();
}
}
}
if (ret < 0)
{
scheduleRemove(session);
}
} catch (Throwable e)
{
if (e instanceof IOException)
{
scheduleRemove(session);
}
session.getFilterChain().fireExceptionCaught(e);
}
}
8.3.3.3 关闭连接
如果没有消息积累,也没有新创建的连接,则关闭线程池。
相关推荐
收集几个mina的通讯例子,实现TCP、UDP通讯
Apache Mina Server 是一个网络通信应用框架 基于 TCP/IP、UDP/IP协议栈的通信框架 支持串口和虚拟机内部的管道等传输方式 Mina 可以帮助我们快速开发高性能、高扩展性的网络通信应用 Mina 提供了事件驱动、异步操作...
Apache MINA(Multipurpose Infrastructure for Network Applications) 是Apache组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。当前发行的MINA版本支持基于Java NIO技术的TCP/...
当前发行的 MINA 版本支持基于 Java NIO 技术的 TCP/UDP 应用程序开发、串口通讯程序(只在最新的预览版中提供),MINA 所支持的功能也在进一步的扩展中。 目前正在使用 MINA 的软件包括有:Apache Directory ...
1.请检查端口服务类型(服务端端口是TCP/UDP)。 2.检查网络环境。 3.默认回车换行断包。所以注意发送内容后面一定要添加回车换行。 注:由于时间问题,加了心跳机制,但是没加客户端回应,也没加服务端接收到心跳...
Apache的Mina(Multipurpose Infrastructure Networked Applications)是一个网络应用框架,可以帮助用户开发高性能和高扩展性的网络应用程序;它提供了一个抽象的、事件驱动的异步API,使Java NIO在各种传输协议...
Apache的Mina(Multipurpose Infrastructure Networked Applications)是一个网络应用框架,可以帮助用户开发高性能和高扩展性的网络应用程序;它提供了一个抽象的、事件驱动的异步API,使Java NIO在各种传输协议...
Java NIO开发技术的 TCP/UDP通信协议Mina框架的实例开发,一个简单的小栗子!
利用 Mina 可以高效地完成以下任务: <br>TCP/IP 和 UDP/IP 通讯 串口通讯 VM 间的管道通讯... 不过不管怎样,如果你在使用 Java 进行并发网络应用开发, Mina 绝对是一个值得推荐和学习的优秀工具!
一个网络应用框架,可以帮助用户开发高性能和高扩展性的网络应用程序;它提供了一个抽象的、事件驱动的异步API,使Java NIO在各种传输协议(如TCP/IP,UDP/IP协议等)下快速高效开发。 Apache Mina也称为: NIO...
apache-mina-2.0.4 架包 源码 学习教程.apache mina是Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。当前发行的 MINA 版本支持基于 Java NIO 技术的 TCP/UDP 应用程序...
Apache的Mina(Multipurpose Infrastructure Networked Applications)是一个网络应用框架,可以帮助用户开发高性能和高扩展性的网络应用程序;它提供了一个抽象的、事件驱动的异步API,使Java NIO在各种传输协议...
socket的tcp/udp(多播) nio的tcp/udp mina 均有服务端/客服端
Apache MINA(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。当前发行的 MINA 版本支持基于 Java NIO 技术的...
Apache Mina Server 是一个网络通信应用框架,也就是说,它主要是对基于TCP/IP、UDP/IP 协议栈的通信框架(当然,也可以提供JAVA 对象的序列化服务、虚拟机管道通信服务等), Mina 可以帮助我们快速开发高性能、高...
TCP通讯框架资源包,mina 通讯框架,TCP通讯框架,满足常用的TCP网络开发需求。亲测无任何问题。
Apache Mina开发必备神器! TCP&UDP;测试工具,模拟传输客户端和服务器端的数据处理 java.net.PortUnreachableException
Apache的Mina(Multipurpose Infrastructure Networked Applications)是一个网络应用框架,可以帮助用户开发高性能和高扩展性的网络应用程序;它提供了一个抽象的、事件驱动的异步API,使Java NIO在各种传输协议...
Apache的Mina(Multipurpose Infrastructure Networked Applications)是一个网络应用框架,可以帮助用户开发高性能和高扩展性的网络应用程序;它提供了一个抽象的、事件驱动的异步API,使Java NIO在各种传输协议...
可以使用 TCP/IP、UDP/IP、串口和虚拟机内部的管道等传输方式,是一个开发高性能和高可伸缩性网络应用程序的网络应用框架