View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.websocket.common.io;
20  
21  import java.io.EOFException;
22  import java.io.IOException;
23  import java.net.InetSocketAddress;
24  import java.net.SocketTimeoutException;
25  import java.nio.ByteBuffer;
26  import java.util.ArrayList;
27  import java.util.List;
28  import java.util.concurrent.Executor;
29  import java.util.concurrent.RejectedExecutionException;
30  import java.util.concurrent.atomic.AtomicBoolean;
31  import java.util.concurrent.atomic.AtomicLong;
32  
33  import org.eclipse.jetty.io.AbstractConnection;
34  import org.eclipse.jetty.io.ByteBufferPool;
35  import org.eclipse.jetty.io.EndPoint;
36  import org.eclipse.jetty.util.BufferUtil;
37  import org.eclipse.jetty.util.component.ContainerLifeCycle;
38  import org.eclipse.jetty.util.component.Dumpable;
39  import org.eclipse.jetty.util.log.Log;
40  import org.eclipse.jetty.util.log.Logger;
41  import org.eclipse.jetty.util.thread.Scheduler;
42  import org.eclipse.jetty.websocket.api.BatchMode;
43  import org.eclipse.jetty.websocket.api.CloseException;
44  import org.eclipse.jetty.websocket.api.StatusCode;
45  import org.eclipse.jetty.websocket.api.SuspendToken;
46  import org.eclipse.jetty.websocket.api.WebSocketPolicy;
47  import org.eclipse.jetty.websocket.api.WriteCallback;
48  import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
49  import org.eclipse.jetty.websocket.api.extensions.Frame;
50  import org.eclipse.jetty.websocket.common.CloseInfo;
51  import org.eclipse.jetty.websocket.common.ConnectionState;
52  import org.eclipse.jetty.websocket.common.Generator;
53  import org.eclipse.jetty.websocket.common.LogicalConnection;
54  import org.eclipse.jetty.websocket.common.Parser;
55  import org.eclipse.jetty.websocket.common.WebSocketSession;
56  import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener;
57  
58  /**
59   * Provides the implementation of {@link LogicalConnection} within the framework of the new {@link Connection} framework of {@code jetty-io}.
60   */
61  public abstract class AbstractWebSocketConnection extends AbstractConnection implements LogicalConnection, ConnectionStateListener, Dumpable
62  {
63      private class Flusher extends FrameFlusher
64      {
65          private Flusher(ByteBufferPool bufferPool, Generator generator, EndPoint endpoint)
66          {
67              super(bufferPool,generator,endpoint,getPolicy().getMaxBinaryMessageBufferSize(),8);
68          }
69  
70          @Override
71          protected void onFailure(Throwable x)
72          {
73              session.notifyError(x);
74  
75              if (ioState.wasAbnormalClose())
76              {
77                  LOG.ignore(x);
78                  return;
79              }
80  
81              if (LOG.isDebugEnabled())
82                  LOG.debug("Write flush failure",x);
83              ioState.onWriteFailure(x);
84          }
85      }
86  
87      public class OnDisconnectCallback implements WriteCallback
88      {
89          private final boolean outputOnly;
90  
91          public OnDisconnectCallback(boolean outputOnly)
92          {
93              this.outputOnly = outputOnly;
94          }
95  
96          @Override
97          public void writeFailed(Throwable x)
98          {
99              disconnect(outputOnly);
100         }
101 
102         @Override
103         public void writeSuccess()
104         {
105             disconnect(outputOnly);
106         }
107     }
108 
109     public class OnCloseLocalCallback implements WriteCallback
110     {
111         private final WriteCallback callback;
112         private final CloseInfo close;
113 
114         public OnCloseLocalCallback(WriteCallback callback, CloseInfo close)
115         {
116             this.callback = callback;
117             this.close = close;
118         }
119 
120         public OnCloseLocalCallback(CloseInfo close)
121         {
122             this(null,close);
123         }
124 
125         @Override
126         public void writeFailed(Throwable x)
127         {
128             try
129             {
130                 if (callback != null)
131                 {
132                     callback.writeFailed(x);
133                 }
134             }
135             finally
136             {
137                 onLocalClose();
138             }
139         }
140 
141         @Override
142         public void writeSuccess()
143         {
144             try
145             {
146                 if (callback != null)
147                 {
148                     callback.writeSuccess();
149                 }
150             }
151             finally
152             {
153                 onLocalClose();
154             }
155         }
156 
157         private void onLocalClose()
158         {
159             if (LOG.isDebugEnabled())
160                 LOG.debug("Local Close Confirmed {}",close);
161             if (close.isAbnormal())
162             {
163                 ioState.onAbnormalClose(close);
164             }
165             else
166             {
167                 ioState.onCloseLocal(close);
168             }
169         }
170     }
171 
172     public static class Stats
173     {
174         private AtomicLong countFillInterestedEvents = new AtomicLong(0);
175         private AtomicLong countOnFillableEvents = new AtomicLong(0);
176         private AtomicLong countFillableErrors = new AtomicLong(0);
177 
178         public long getFillableErrorCount()
179         {
180             return countFillableErrors.get();
181         }
182 
183         public long getFillInterestedCount()
184         {
185             return countFillInterestedEvents.get();
186         }
187 
188         public long getOnFillableCount()
189         {
190             return countOnFillableEvents.get();
191         }
192     }
193     
194     private static enum ReadMode
195     {
196         PARSE,
197         DISCARD,
198         EOF
199     }
200 
201     private static final Logger LOG = Log.getLogger(AbstractWebSocketConnection.class);
202 
203     /**
204      * Minimum size of a buffer is the determined to be what would be the maximum framing header size (not including payload)
205      */
206     private static final int MIN_BUFFER_SIZE = Generator.MAX_HEADER_LENGTH;
207 
208     private final ByteBufferPool bufferPool;
209     private final Scheduler scheduler;
210     private final Generator generator;
211     private final Parser parser;
212     private final WebSocketPolicy policy;
213     private final AtomicBoolean suspendToken;
214     private final FrameFlusher flusher;
215     private WebSocketSession session;
216     private List<ExtensionConfig> extensions;
217     private boolean isFilling;
218     private ByteBuffer buffer;
219     private ReadMode readMode = ReadMode.PARSE;
220     private IOState ioState;
221     private Stats stats = new Stats();
222 
223     public AbstractWebSocketConnection(EndPoint endp, Executor executor, Scheduler scheduler, WebSocketPolicy policy, ByteBufferPool bufferPool)
224     {
225         super(endp,executor,EXECUTE_ONFILLABLE); // TODO review if this is best. Specifically with MUX
226         this.policy = policy;
227         this.bufferPool = bufferPool;
228         this.generator = new Generator(policy,bufferPool);
229         this.parser = new Parser(policy,bufferPool);
230         this.scheduler = scheduler;
231         this.extensions = new ArrayList<>();
232         this.suspendToken = new AtomicBoolean(false);
233         this.ioState = new IOState();
234         this.ioState.addListener(this);
235         this.flusher = new Flusher(bufferPool,generator,endp);
236         this.setInputBufferSize(policy.getInputBufferSize());
237         this.setMaxIdleTimeout(policy.getIdleTimeout());
238     }
239 
240     @Override
241     public Executor getExecutor()
242     {
243         return super.getExecutor();
244     }
245 
246     @Override
247     public void close()
248     {
249         close(StatusCode.NORMAL,null);
250     }
251 
252     /**
253      * Close the connection.
254      * <p>                    fillInterested();
255 
256      * This can result in a close handshake over the network, or a simple local abnormal close
257      * 
258      * @param statusCode
259      *            the WebSocket status code.
260      * @param reason
261      *            the (optional) reason string. (null is allowed)
262      * @see StatusCode
263      */
264     @Override
265     public void close(int statusCode, String reason)
266     {
267         if (LOG.isDebugEnabled())
268             LOG.debug("close({},{})",statusCode,reason);
269         CloseInfo close = new CloseInfo(statusCode,reason);
270         this.outgoingFrame(close.asFrame(),new OnCloseLocalCallback(close),BatchMode.OFF);
271     }
272 
273     @Override
274     public void disconnect()
275     {
276         disconnect(false);
277     }
278 
279     private void disconnect(boolean onlyOutput)
280     {
281         if (LOG.isDebugEnabled())
282             LOG.debug("{} disconnect({})",policy.getBehavior(),onlyOutput?"outputOnly":"both");
283         // close FrameFlusher, we cannot write anymore at this point.
284         flusher.close();
285         EndPoint endPoint = getEndPoint();
286         // We need to gently close first, to allow
287         // SSL close alerts to be sent by Jetty
288         if (LOG.isDebugEnabled())
289             LOG.debug("Shutting down output {}",endPoint);
290         endPoint.shutdownOutput();
291         if (!onlyOutput)
292         {
293             LOG.debug("Closing {}",endPoint);
294             endPoint.close();
295         }
296     }
297 
298     protected void execute(Runnable task)
299     {
300         try
301         {
302             getExecutor().execute(task);
303         }
304         catch (RejectedExecutionException e)
305         {
306             if (LOG.isDebugEnabled())
307                 LOG.debug("Job not dispatched: {}",task);
308         }
309     }
310 
311     @Override
312     public void fillInterested()
313     {
314         stats.countFillInterestedEvents.incrementAndGet();
315         super.fillInterested();
316     }
317 
318     @Override
319     public ByteBufferPool getBufferPool()
320     {
321         return bufferPool;
322     }
323 
324     /**
325      * Get the list of extensions in use.
326      * <p>
327      * This list is negotiated during the WebSocket Upgrade Request/Response handshake.
328      * 
329      * @return the list of negotiated extensions in use.
330      */
331     public List<ExtensionConfig> getExtensions()
332     {
333         return extensions;
334     }
335 
336     public Generator getGenerator()
337     {
338         return generator;
339     }
340 
341     @Override
342     public long getIdleTimeout()
343     {
344         return getEndPoint().getIdleTimeout();
345     }
346 
347     @Override
348     public IOState getIOState()
349     {
350         return ioState;
351     }
352 
353     @Override
354     public long getMaxIdleTimeout()
355     {
356         return getEndPoint().getIdleTimeout();
357     }
358 
359     public Parser getParser()
360     {
361         return parser;
362     }
363 
364     @Override
365     public WebSocketPolicy getPolicy()
366     {
367         return this.policy;
368     }
369 
370     @Override
371     public InetSocketAddress getRemoteAddress()
372     {
373         return getEndPoint().getRemoteAddress();
374     }
375 
376     public Scheduler getScheduler()
377     {
378         return scheduler;
379     }
380 
381     @Override
382     public WebSocketSession getSession()
383     {
384         return session;
385     }
386 
387     public Stats getStats()
388     {
389         return stats;
390     }
391 
392     @Override
393     public boolean isOpen()
394     {
395         return getIOState().isOpen() && getEndPoint().isOpen();
396     }
397 
398     @Override
399     public boolean isReading()
400     {
401         return isFilling;
402     }
403 
404     /**
405      * Physical connection disconnect.
406      * <p>
407      * Not related to WebSocket close handshake.
408      */
409     @Override
410     public void onClose()
411     {
412         if (LOG.isDebugEnabled())
413             LOG.debug("{} onClose()",policy.getBehavior());
414         super.onClose();
415         // ioState.onDisconnected();
416         flusher.close();
417     }
418 
419     @Override
420     public void onConnectionStateChange(ConnectionState state)
421     {
422         if (LOG.isDebugEnabled())
423             LOG.debug("{} Connection State Change: {}",policy.getBehavior(),state);
424         switch (state)
425         {
426             case OPEN:
427                 if (BufferUtil.isEmpty(buffer))
428                 {
429                     if (LOG.isDebugEnabled())
430                         LOG.debug("fillInterested");
431                     fillInterested();
432                 }
433                 else
434                     onFillable();
435                 break;
436             case CLOSED:
437                 if (ioState.wasAbnormalClose())
438                 {
439                     // Fire out a close frame, indicating abnormal shutdown, then disconnect
440                     CloseInfo abnormal = new CloseInfo(StatusCode.SHUTDOWN,"Abnormal Close - " + ioState.getCloseInfo().getReason());
441                     outgoingFrame(abnormal.asFrame(),new OnDisconnectCallback(false),BatchMode.OFF);
442                 }
443                 else
444                 {
445                     // Just disconnect
446                     this.disconnect(false);
447                 }
448                 break;
449             case CLOSING:
450                 // First occurrence of .onCloseLocal or .onCloseRemote use
451                 if (ioState.wasRemoteCloseInitiated())
452                 {
453                     CloseInfo close = ioState.getCloseInfo();
454                     // reply to close handshake from remote
455                     outgoingFrame(close.asFrame(),new OnCloseLocalCallback(new OnDisconnectCallback(true),close),BatchMode.OFF);
456                 }
457             default:
458                 break;
459         }
460     }
461 
462     @Override
463     public void onFillable()
464     {
465         if (LOG.isDebugEnabled())
466             LOG.debug("{} onFillable()",policy.getBehavior());
467         stats.countOnFillableEvents.incrementAndGet();
468         if (buffer==null)
469             buffer = bufferPool.acquire(getInputBufferSize(),true);
470         try
471         {
472             isFilling = true;
473 
474             if(readMode == ReadMode.PARSE)
475             {
476                 readMode = readParse(buffer);
477             } 
478             else
479             {
480                 readMode = readDiscard(buffer);
481             }
482         }
483         finally
484         {
485             bufferPool.release(buffer);
486             buffer=null;
487         }
488 
489         if ((readMode != ReadMode.EOF) && (suspendToken.get() == false))
490         {
491             fillInterested();
492         }
493         else
494         {
495             isFilling = false;
496         }
497     }
498 
499     
500 
501     @Override
502     protected void onFillInterestedFailed(Throwable cause)
503     {
504         LOG.ignore(cause);
505         stats.countFillInterestedEvents.incrementAndGet();
506         super.onFillInterestedFailed(cause);
507     }
508 
509     protected void prefill(ByteBuffer prefilled)
510     {
511         buffer=prefilled;
512     }
513     
514     @Override
515     public void onOpen()
516     {
517         super.onOpen();
518         this.ioState.onOpened();
519     }
520 
521     /**
522      * Event for no activity on connection (read or write)
523      */
524     @Override
525     protected boolean onReadTimeout()
526     {
527         IOState state = getIOState();
528         ConnectionState cstate = state.getConnectionState();
529         if (LOG.isDebugEnabled())
530             LOG.debug("{} Read Timeout - {}",policy.getBehavior(),cstate);
531 
532         if (cstate == ConnectionState.CLOSED)
533         {
534             // close already completed, extra timeouts not relevant
535             // allow underlying connection and endpoint to disconnect on its own
536             return true;
537         }
538 
539         try
540         {
541             session.notifyError(new SocketTimeoutException("Timeout on Read"));
542         }
543         finally
544         {
545             // This is an Abnormal Close condition
546             close(StatusCode.SHUTDOWN,"Idle Timeout");
547         }
548 
549         return false;
550     }
551 
552     /**
553      * Frame from API, User, or Internal implementation destined for network.
554      */
555     @Override
556     public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode)
557     {
558         if (LOG.isDebugEnabled())
559         {
560             LOG.debug("outgoingFrame({}, {})",frame,callback);
561         }
562 
563         flusher.enqueue(frame,callback,batchMode);
564     }
565 
566     private ReadMode readDiscard(ByteBuffer buffer)
567     {
568         EndPoint endPoint = getEndPoint();
569         try
570         {
571             while (true)
572             {
573                 int filled = endPoint.fill(buffer);
574                 if (filled == 0)
575                 {
576                     return ReadMode.DISCARD;
577                 }
578                 else if (filled < 0)
579                 {
580                     LOG.debug("read - EOF Reached (remote: {})",getRemoteAddress());
581                     return ReadMode.EOF;
582                 }
583                 else
584                 {
585                     if (LOG.isDebugEnabled())
586                     {
587                         LOG.debug("Discarded {} bytes - {}",filled,BufferUtil.toDetailString(buffer));
588                     }
589                 }
590             }
591         }
592         catch (IOException e)
593         {
594             LOG.ignore(e);
595             return ReadMode.EOF;
596         }
597         catch (Throwable t)
598         {
599             LOG.ignore(t);
600             return ReadMode.DISCARD;
601         }
602     }
603     
604     private ReadMode readParse(ByteBuffer buffer)
605     {
606         EndPoint endPoint = getEndPoint();
607         try
608         {
609             while (true) // TODO: should this honor the LogicalConnection.suspend() ?
610             {
611                 int filled = endPoint.fill(buffer);
612                 if (filled == 0)
613                 {
614                     return ReadMode.PARSE;
615                 }
616                 else if (filled < 0)
617                 {
618                     LOG.debug("read - EOF Reached (remote: {})",getRemoteAddress());
619                     ioState.onReadFailure(new EOFException("Remote Read EOF"));
620                     return ReadMode.EOF;
621                 }
622                 else
623                 {
624                     if (LOG.isDebugEnabled())
625                     {
626                         LOG.debug("Filled {} bytes - {}",filled,BufferUtil.toDetailString(buffer));
627                     }
628                     parser.parse(buffer);
629                 }
630             }
631         }
632         catch (IOException e)
633         {
634             LOG.warn(e);
635             close(StatusCode.PROTOCOL,e.getMessage());
636             return ReadMode.DISCARD;
637         }
638         catch (CloseException e)
639         {
640             LOG.debug(e);
641             close(e.getStatusCode(),e.getMessage());
642             return ReadMode.DISCARD;
643         }
644         catch (Throwable t)
645         {
646             LOG.warn(t);
647             close(StatusCode.ABNORMAL,t.getMessage());
648             // TODO: should probably only switch to discard if a non-ws-endpoint error
649             return ReadMode.DISCARD;
650         }
651     }
652 
653     @Override
654     public void resume()
655     {
656         if (suspendToken.getAndSet(false))
657         {
658             fillInterested();
659         }
660     }
661 
662     /**
663      * Get the list of extensions in use.
664      * <p>
665      * This list is negotiated during the WebSocket Upgrade Request/Response handshake.
666      * 
667      * @param extensions
668      *            the list of negotiated extensions in use.
669      */
670     public void setExtensions(List<ExtensionConfig> extensions)
671     {
672         this.extensions = extensions;
673     }
674 
675     @Override
676     public void setInputBufferSize(int inputBufferSize)
677     {
678         if (inputBufferSize < MIN_BUFFER_SIZE)
679         {
680             throw new IllegalArgumentException("Cannot have buffer size less than " + MIN_BUFFER_SIZE);
681         }
682         super.setInputBufferSize(inputBufferSize);
683     }
684 
685     @Override
686     public void setMaxIdleTimeout(long ms)
687     {
688         getEndPoint().setIdleTimeout(ms);
689     }
690 
691     @Override
692     public void setSession(WebSocketSession session)
693     {
694         this.session = session;
695     }
696 
697     @Override
698     public SuspendToken suspend()
699     {
700         suspendToken.set(true);
701         return this;
702     }
703 
704     @Override
705     public String dump()
706     {
707         return ContainerLifeCycle.dump(this);
708     }
709 
710     @Override
711     public void dump(Appendable out, String indent) throws IOException
712     {
713         out.append(toString()).append(System.lineSeparator());
714     }
715 
716     @Override
717     public String toString()
718     {
719         return String.format("%s{f=%s,g=%s,p=%s}",super.toString(),flusher,generator,parser);
720     }
721 
722 }