View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.mina.filter.keepalive;
21  
22  import org.apache.mina.core.buffer.IoBuffer;
23  import org.apache.mina.core.filterchain.IoFilter;
24  import org.apache.mina.core.filterchain.IoFilterAdapter;
25  import org.apache.mina.core.filterchain.IoFilterChain;
26  import org.apache.mina.core.service.IoHandler;
27  import org.apache.mina.core.session.AttributeKey;
28  import org.apache.mina.core.session.IdleStatus;
29  import org.apache.mina.core.session.IoEventType;
30  import org.apache.mina.core.session.IoSession;
31  import org.apache.mina.core.session.IoSessionConfig;
32  import org.apache.mina.core.write.DefaultWriteRequest;
33  import org.apache.mina.core.write.WriteRequest;
34  
35  /**
36   * An {@link IoFilter} that sends a keep-alive request on
37   * {@link IoEventType#SESSION_IDLE} and sends back the response for the
38   * sent keep-alive request.
39   *
40   * <h2>Interference with {@link IoSessionConfig#setIdleTime(IdleStatus, int)}</h2>
41   *
42   * This filter adjusts <tt>idleTime</tt> of the {@link IdleStatus}s that
43   * this filter is interested in automatically (e.g. {@link IdleStatus#READER_IDLE}
44   * and {@link IdleStatus#WRITER_IDLE}.)  Changing the <tt>idleTime</tt>
45   * of the {@link IdleStatus}s can lead this filter to a unexpected behavior.
46   * Please also note that any {@link IoFilter} and {@link IoHandler} behind
47   * {@link KeepAliveFilter} will not get any {@link IoEventType#SESSION_IDLE}
48   * event.  To receive the internal {@link IoEventType#SESSION_IDLE} event,
49   * you can call {@link #setForwardEvent(boolean)} with <tt>true</tt>.
50   *
51   * <h2>Implementing {@link KeepAliveMessageFactory}</h2>
52   *
53   * To use this filter, you have to provide an implementation of
54   * {@link KeepAliveMessageFactory}, which determines a received or sent
55   * message is a keep-alive message or not and creates a new keep-alive
56   * message:
57   *
58   * <table border="1" summary="Message">
59   *   <tr>
60   *     <th>Name</th><th>Description</th><th>Implementation</th>
61   *   </tr>
62   *   <tr valign="top">
63   *     <td>Active</td>
64   *     <td>
65   *       You want a keep-alive request is sent when the reader is idle.
66   *       Once the request is sent, the response for the request should be
67   *       received within <tt>keepAliveRequestTimeout</tt> seconds.  Otherwise,
68   *       the specified {@link KeepAliveRequestTimeoutHandler} will be invoked.
69   *       If a keep-alive request is received, its response also should be sent back.
70   *     </td>
71   *     <td>
72   *       Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
73   *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
74   *       return a non-<tt>null</tt>.
75   *     </td>
76   *   </tr>
77   *   <tr valign="top">
78   *     <td>Semi-active</td>
79   *     <td>
80   *       You want a keep-alive request to be sent when the reader is idle.
81   *       However, you don't really care if the response is received or not.
82   *       If a keep-alive request is received, its response should
83   *       also be sent back.
84   *     </td>
85   *     <td>
86   *       Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
87   *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
88   *       return a non-<tt>null</tt>, and the <tt>timeoutHandler</tt> property
89   *       should be set to {@link KeepAliveRequestTimeoutHandler#NOOP},
90   *       {@link KeepAliveRequestTimeoutHandler#LOG} or the custom {@link KeepAliveRequestTimeoutHandler}
91   *       implementation that doesn't affect the session state nor throw an exception.
92   *     </td>
93   *   </tr>
94   *   <tr valign="top">
95   *     <td>Passive</td>
96   *     <td>
97   *       You don't want to send a keep-alive request by yourself, but the
98   *       response should be sent back if a keep-alive request is received.
99   *     </td>
100  *     <td>
101  *       {@link KeepAliveMessageFactory#getRequest(IoSession)} must return
102  *       <tt>null</tt> and {@link KeepAliveMessageFactory#getResponse(IoSession, Object)}
103  *       must return a non-<tt>null</tt>.
104  *     </td>
105  *   </tr>
106  *   <tr valign="top">
107  *     <td>Deaf Speaker</td>
108  *     <td>
109  *       You want a keep-alive request to be sent when the reader is idle, but
110  *       you don't want to send any response back.
111  *     </td>
112  *     <td>
113  *       {@link KeepAliveMessageFactory#getRequest(IoSession)} must return
114  *       a non-<tt>null</tt>,
115  *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
116  *       return <tt>null</tt> and the <tt>timeoutHandler</tt> must be set to
117  *       {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}.
118  *     </td>
119  *   </tr>
120  *   <tr valign="top">
121  *     <td>Silent Listener</td>
122  *     <td>
123  *       You don't want to send a keep-alive request by yourself nor send any
124  *       response back.
125  *     </td>
126  *     <td>
127  *       Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
128  *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
129  *       return <tt>null</tt>.
130  *     </td>
131  *   </tr>
132  * </table>
133  * 
134  * Please note that you must implement
135  * {@link KeepAliveMessageFactory#isRequest(IoSession, Object)} and
136  * {@link KeepAliveMessageFactory#isResponse(IoSession, Object)} properly
137  * whatever case you chose.
138  *
139  * <h2>Handling timeout</h2>
140  *
141  * {@link KeepAliveFilter} will notify its {@link KeepAliveRequestTimeoutHandler}
142  * when {@link KeepAliveFilter} didn't receive the response message for a sent
143  * keep-alive message.  The default handler is {@link KeepAliveRequestTimeoutHandler#CLOSE},
144  * but you can use other presets such as {@link KeepAliveRequestTimeoutHandler#NOOP},
145  * {@link KeepAliveRequestTimeoutHandler#LOG} or {@link KeepAliveRequestTimeoutHandler#EXCEPTION}.
146  * You can even implement your own handler.
147  *
148  * <h3>Special handler: {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}</h3>
149  *
150  * {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER} is a special handler which is
151  * dedicated for the 'deaf speaker' mode mentioned above.  Setting the
152  * <tt>timeoutHandler</tt> property to {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}
153  * stops this filter from waiting for response messages and therefore disables
154  * response timeout detection.
155  *
156  * @author <a href="http://mina.apache.org">Apache MINA Project</a>
157  * @org.apache.xbean.XBean
158  */
159 public class KeepAliveFilter extends IoFilterAdapter {
160 
161     private final AttributeKeyl#AttributeKey">AttributeKey WAITING_FOR_RESPONSE = new AttributeKey(getClass(), "waitingForResponse");
162 
163     private final AttributeKeyttributeKey">AttributeKey IGNORE_READER_IDLE_ONCE = new AttributeKey(getClass(), "ignoreReaderIdleOnce");
164 
165     private final KeepAliveMessageFactory messageFactory;
166 
167     private final IdleStatus interestedIdleStatus;
168 
169     private volatile KeepAliveRequestTimeoutHandler requestTimeoutHandler;
170 
171     private volatile int requestInterval;
172 
173     private volatile int requestTimeout;
174 
175     private volatile boolean forwardEvent;
176 
177     /**
178      * Creates a new instance with the default properties.
179      * The default property values are:
180      * <ul>
181      * <li><tt>interestedIdleStatus</tt> - {@link IdleStatus#READER_IDLE}</li>
182      * <li><tt>policy</tt> = {@link KeepAliveRequestTimeoutHandler#CLOSE}</li>
183      * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
184      * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
185      * </ul>
186      * 
187      * @param messageFactory The message factory to use 
188      */
189     public KeepAliveFilter(KeepAliveMessageFactory messageFactory) {
190         this(messageFactory, IdleStatus.READER_IDLE, KeepAliveRequestTimeoutHandler.CLOSE);
191     }
192 
193     /**
194      * Creates a new instance with the default properties.
195      * The default property values are:
196      * <ul>
197      * <li><tt>policy</tt> = {@link KeepAliveRequestTimeoutHandler#CLOSE}</li>
198      * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
199      * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
200      * </ul>
201      * 
202      * @param messageFactory The message factory to use 
203      * @param interestedIdleStatus The IdleStatus the filter is interested in
204      */
205     public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus) {
206         this(messageFactory, interestedIdleStatus, KeepAliveRequestTimeoutHandler.CLOSE, 60, 30);
207     }
208 
209     /**
210      * Creates a new instance with the default properties.
211      * The default property values are:
212      * <ul>
213      * <li><tt>interestedIdleStatus</tt> - {@link IdleStatus#READER_IDLE}</li>
214      * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
215      * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
216      * </ul>
217      * 
218      * @param messageFactory The message factory to use 
219      * @param policy The TimeOut handler policy
220      */
221     public KeepAliveFilter(KeepAliveMessageFactory messageFactory, KeepAliveRequestTimeoutHandler policy) {
222         this(messageFactory, IdleStatus.READER_IDLE, policy, 60, 30);
223     }
224 
225     /**
226      * Creates a new instance with the default properties.
227      * The default property values are:
228      * <ul>
229      * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
230      * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
231      * </ul>
232      * 
233      * @param messageFactory The message factory to use 
234      * @param interestedIdleStatus The IdleStatus the filter is interested in
235      * @param policy The TimeOut handler policy
236      */
237     public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus,
238             KeepAliveRequestTimeoutHandler policy) {
239         this(messageFactory, interestedIdleStatus, policy, 60, 30);
240     }
241 
242     /**
243      * Creates a new instance.
244      * 
245      * @param messageFactory The message factory to use 
246      * @param interestedIdleStatus The IdleStatus the filter is interested in
247      * @param policy The TimeOut handler policy
248      * @param keepAliveRequestInterval the interval to use
249      * @param keepAliveRequestTimeout The timeout to use
250      */
251     public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus,
252             KeepAliveRequestTimeoutHandler policy, int keepAliveRequestInterval, int keepAliveRequestTimeout) {
253         if (messageFactory == null) {
254             throw new IllegalArgumentException("messageFactory");
255         }
256         
257         if (interestedIdleStatus == null) {
258             throw new IllegalArgumentException("interestedIdleStatus");
259         }
260         
261         if (policy == null) {
262             throw new IllegalArgumentException("policy");
263         }
264 
265         this.messageFactory = messageFactory;
266         this.interestedIdleStatus = interestedIdleStatus;
267         requestTimeoutHandler = policy;
268 
269         setRequestInterval(keepAliveRequestInterval);
270         setRequestTimeout(keepAliveRequestTimeout);
271     }
272 
273     /**
274      * @return The {@link IdleStatus} 
275      */
276     public IdleStatus getInterestedIdleStatus() {
277         return interestedIdleStatus;
278     }
279 
280     /**
281      * @return The timeout request handler
282      */
283     public KeepAliveRequestTimeoutHandler getRequestTimeoutHandler() {
284         return requestTimeoutHandler;
285     }
286 
287     /**
288      * Set the timeout handler
289      * 
290      * @param timeoutHandler The instance of {@link KeepAliveRequestTimeoutHandler} to use
291      */
292     public void setRequestTimeoutHandler(KeepAliveRequestTimeoutHandler timeoutHandler) {
293         if (timeoutHandler == null) {
294             throw new IllegalArgumentException("timeoutHandler");
295         }
296         requestTimeoutHandler = timeoutHandler;
297     }
298 
299     /**
300      * @return the interval for keep alive messages
301      */
302     public int getRequestInterval() {
303         return requestInterval;
304     }
305 
306     /**
307      * Sets the interval for keepAlive messages
308      * 
309      * @param keepAliveRequestInterval the interval to set
310      */
311     public void setRequestInterval(int keepAliveRequestInterval) {
312         if (keepAliveRequestInterval <= 0) {
313             throw new IllegalArgumentException("keepAliveRequestInterval must be a positive integer: "
314                     + keepAliveRequestInterval);
315         }
316         
317         requestInterval = keepAliveRequestInterval;
318     }
319 
320     /**
321      * @return The timeout
322      */
323     public int getRequestTimeout() {
324         return requestTimeout;
325     }
326 
327     /**
328      * Sets the timeout
329      * 
330      * @param keepAliveRequestTimeout The timeout to set
331      */
332     public void setRequestTimeout(int keepAliveRequestTimeout) {
333         if (keepAliveRequestTimeout <= 0) {
334             throw new IllegalArgumentException("keepAliveRequestTimeout must be a positive integer: "
335                     + keepAliveRequestTimeout);
336         }
337         
338         requestTimeout = keepAliveRequestTimeout;
339     }
340 
341     /**
342      * @return The message factory
343      */
344     public KeepAliveMessageFactory getMessageFactory() {
345         return messageFactory;
346     }
347 
348     /**
349      * @return <tt>true</tt> if and only if this filter forwards
350      * a {@link IoEventType#SESSION_IDLE} event to the next filter.
351      * By default, the value of this property is <tt>false</tt>.
352      */
353     public boolean isForwardEvent() {
354         return forwardEvent;
355     }
356 
357     /**
358      * Sets if this filter needs to forward a
359      * {@link IoEventType#SESSION_IDLE} event to the next filter.
360      * By default, the value of this property is <tt>false</tt>.
361      * 
362      * @param forwardEvent a flag set to tell if the filter has to forward a {@link IoEventType#SESSION_IDLE} event
363      */
364     public void setForwardEvent(boolean forwardEvent) {
365         this.forwardEvent = forwardEvent;
366     }
367 
368     /**
369      * {@inheritDoc}
370      */
371     @Override
372     public void onPreAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
373         if (parent.contains(this)) {
374             throw new IllegalArgumentException("You can't add the same filter instance more than once. "
375                     + "Create another instance and add it.");
376         }
377     }
378 
379     /**
380      * {@inheritDoc}
381      */
382     @Override
383     public void onPostAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
384         resetStatus(parent.getSession());
385     }
386 
387     /**
388      * {@inheritDoc}
389      */
390     @Override
391     public void onPostRemove(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
392         resetStatus(parent.getSession());
393     }
394 
395     /**
396      * {@inheritDoc}
397      */
398     @Override
399     public void messageReceived(NextFilter nextFilter, IoSession session, Object message) throws Exception {
400         try {
401             if (messageFactory.isRequest(session, message)) {
402                 Object pongMessage = messageFactory.getResponse(session, message);
403 
404                 if (pongMessage != null) {
405                     nextFilter.filterWrite(session, new DefaultWriteRequest(pongMessage));
406                 }
407             }
408 
409             if (messageFactory.isResponse(session, message)) {
410                 resetStatus(session);
411             }
412         } finally {
413             if (!isKeepAliveMessage(session, message)) {
414                 nextFilter.messageReceived(session, message);
415             }
416         }
417     }
418 
419     /**
420      * {@inheritDoc}
421      */
422     @Override
423     public void messageSent(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws Exception {
424         Object message = writeRequest.getOriginalMessage();
425         
426         if (message == null)
427         {
428             if (writeRequest.getMessage() instanceof IoBuffer) {
429                 message = ((IoBuffer)writeRequest.getMessage()).duplicate().flip();
430             }
431         }
432         
433         if (!isKeepAliveMessage(session, message)) {
434             nextFilter.messageSent(session, writeRequest);
435         }
436     }
437 
438     /**
439      * {@inheritDoc}
440      */
441     @Override
442     public void sessionIdle(NextFilter nextFilter, IoSession session, IdleStatus status) throws Exception {
443         if (status == interestedIdleStatus) {
444             if (!session.containsAttribute(WAITING_FOR_RESPONSE)) {
445                 Object pingMessage = messageFactory.getRequest(session);
446                 
447                 if (pingMessage != null) {
448                     nextFilter.filterWrite(session, new DefaultWriteRequest(pingMessage));
449 
450                     // If policy is OFF, there's no need to wait for
451                     // the response.
452                     if (getRequestTimeoutHandler() != KeepAliveRequestTimeoutHandler.DEAF_SPEAKER) {
453                         markStatus(session);
454                         if (interestedIdleStatus == IdleStatus.BOTH_IDLE) {
455                             session.setAttribute(IGNORE_READER_IDLE_ONCE);
456                         }
457                     } else {
458                         resetStatus(session);
459                     }
460                 }
461             } else {
462                 handlePingTimeout(session);
463             }
464         } else if (status == IdleStatus.READER_IDLE) {
465             if (session.removeAttribute(IGNORE_READER_IDLE_ONCE) == null) {
466                 if (session.containsAttribute(WAITING_FOR_RESPONSE)) {
467                     handlePingTimeout(session);
468                 }
469             }
470         }
471 
472         if (forwardEvent) {
473             nextFilter.sessionIdle(session, status);
474         }
475     }
476 
477     private void handlePingTimeout(IoSession session) throws Exception {
478         resetStatus(session);
479         KeepAliveRequestTimeoutHandler handler = getRequestTimeoutHandler();
480         if (handler == KeepAliveRequestTimeoutHandler.DEAF_SPEAKER) {
481             return;
482         }
483 
484         handler.keepAliveRequestTimedOut(this, session);
485     }
486 
487     private void markStatus(IoSession session) {
488         session.getConfig().setIdleTime(interestedIdleStatus, 0);
489         session.getConfig().setReaderIdleTime(getRequestTimeout());
490         session.setAttribute(WAITING_FOR_RESPONSE);
491     }
492 
493     private void resetStatus(IoSession session) {
494         session.getConfig().setReaderIdleTime(0);
495         session.getConfig().setWriterIdleTime(0);
496         session.getConfig().setIdleTime(interestedIdleStatus, getRequestInterval());
497         session.removeAttribute(WAITING_FOR_RESPONSE);
498     }
499 
500     private boolean isKeepAliveMessage(IoSession session, Object message) {
501         return messageFactory.isRequest(session, message) || messageFactory.isResponse(session, message);
502     }
503 }