View Javadoc

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