001    /**
002     * Copyright (C) 2012 FuseSource, Inc.
003     * http://fusesource.com
004     *
005     * Licensed under the Apache License, Version 2.0 (the "License");
006     * you may not use this file except in compliance with the License.
007     * You may obtain a copy of the License at
008     *
009     *    http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.fusesource.hawtdispatch.transport;
018    
019    import org.fusesource.hawtdispatch.*;
020    
021    import java.io.IOException;
022    import java.net.*;
023    import java.nio.channels.DatagramChannel;
024    import java.nio.channels.ReadableByteChannel;
025    import java.nio.channels.SelectionKey;
026    import java.nio.channels.WritableByteChannel;
027    import java.util.LinkedList;
028    
029    /**
030     * <p>
031     * </p>
032     *
033     * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
034     */
035    public class UdpTransport extends ServiceBase implements Transport {
036    
037        public static final SocketAddress ANY_ADDRESS = new SocketAddress() {
038            @Override
039            public String toString() {
040                return "*:*";
041            }
042        };
043    
044    
045        abstract static class SocketState {
046            void onStop(Task onCompleted) {
047            }
048            void onCanceled() {
049            }
050            boolean is(Class<? extends SocketState> clazz) {
051                return getClass()==clazz;
052            }
053        }
054    
055        static class DISCONNECTED extends SocketState{}
056    
057        class CONNECTED extends SocketState {
058    
059            public CONNECTED() {
060                localAddress = channel.socket().getLocalSocketAddress();
061                remoteAddress = channel.socket().getRemoteSocketAddress();
062                if(remoteAddress == null ) {
063                    remoteAddress = ANY_ADDRESS;
064                }
065            }
066    
067            void onStop(Task onCompleted) {
068                trace("CONNECTED.onStop");
069                CANCELING state = new CANCELING();
070                socketState = state;
071                state.add(createDisconnectTask());
072                state.onStop(onCompleted);
073            }
074            void onCanceled() {
075                trace("CONNECTED.onCanceled");
076                CANCELING state = new CANCELING();
077                socketState = state;
078                state.add(createDisconnectTask());
079                state.onCanceled();
080            }
081            Task createDisconnectTask() {
082                return new Task(){
083                    public void run() {
084                        listener.onTransportDisconnected();
085                    }
086                };
087            }
088        }
089    
090        class CANCELING extends SocketState {
091            private LinkedList<Task> runnables =  new LinkedList<Task>();
092            private int remaining;
093            private boolean dispose;
094    
095            public CANCELING() {
096                if( readSource!=null ) {
097                    remaining++;
098                    readSource.cancel();
099                }
100                if( writeSource!=null ) {
101                    remaining++;
102                    writeSource.cancel();
103                }
104            }
105            void onStop(Task onCompleted) {
106                trace("CANCELING.onCompleted");
107                add(onCompleted);
108                dispose = true;
109            }
110            void add(Task onCompleted) {
111                if( onCompleted!=null ) {
112                    runnables.add(onCompleted);
113                }
114            }
115            void onCanceled() {
116                trace("CANCELING.onCanceled");
117                remaining--;
118                if( remaining!=0 ) {
119                    return;
120                }
121                try {
122                    channel.close();
123                } catch (IOException ignore) {
124                }
125                socketState = new CANCELED(dispose);
126                for (Task runnable : runnables) {
127                    runnable.run();
128                }
129                if (dispose) {
130                    dispose();
131                }
132            }
133        }
134    
135        class CANCELED extends SocketState {
136            private boolean disposed;
137    
138            public CANCELED(boolean disposed) {
139                this.disposed=disposed;
140            }
141    
142            void onStop(Task onCompleted) {
143                trace("CANCELED.onStop");
144                if( !disposed ) {
145                    disposed = true;
146                    dispose();
147                }
148                onCompleted.run();
149            }
150        }
151    
152        protected URI remoteLocation;
153        protected URI localLocation;
154        protected TransportListener listener;
155        protected ProtocolCodec codec;
156    
157        protected DatagramChannel channel;
158    
159        protected SocketState socketState = new DISCONNECTED();
160    
161        protected DispatchQueue dispatchQueue;
162        private DispatchSource readSource;
163        private DispatchSource writeSource;
164        protected CustomDispatchSource<Integer, Integer> drainOutboundSource;
165        protected CustomDispatchSource<Integer, Integer> yieldSource;
166    
167        protected boolean useLocalHost = true;
168    
169        int receiveBufferSize = 1024*64;
170        int sendBufferSize = 1024*64;
171    
172    
173        public static final int IPTOS_LOWCOST = 0x02;
174        public static final int IPTOS_RELIABILITY = 0x04;
175        public static final int IPTOS_THROUGHPUT = 0x08;
176        public static final int IPTOS_LOWDELAY = 0x10;
177    
178        int trafficClass = IPTOS_THROUGHPUT;
179    
180        SocketAddress localAddress;
181        SocketAddress remoteAddress = ANY_ADDRESS;
182    
183    
184        private final Task CANCEL_HANDLER = new Task() {
185            public void run() {
186                socketState.onCanceled();
187            }
188        };
189    
190        static final class OneWay {
191            final Object command;
192            final Retained retained;
193    
194            public OneWay(Object command, Retained retained) {
195                this.command = command;
196                this.retained = retained;
197            }
198        }
199    
200        public void connected(DatagramChannel channel) throws IOException, Exception {
201            this.channel = channel;
202            initializeChannel();
203            this.socketState = new CONNECTED();
204        }
205    
206        protected void initializeChannel() throws Exception {
207            this.channel.configureBlocking(false);
208            DatagramSocket socket = channel.socket();
209            try {
210                socket.setReuseAddress(true);
211            } catch (SocketException e) {
212            }
213            try {
214                socket.setTrafficClass(trafficClass);
215            } catch (SocketException e) {
216            }
217            try {
218                socket.setReceiveBufferSize(receiveBufferSize);
219            } catch (SocketException e) {
220            }
221            try {
222                socket.setSendBufferSize(sendBufferSize);
223            } catch (SocketException e) {
224            }
225            if( channel!=null && codec!=null ) {
226                initializeCodec();
227            }
228        }
229    
230        protected void initializeCodec() throws Exception {
231            codec.setReadableByteChannel(readChannel());
232            codec.setWritableByteChannel(writeChannel());
233        }
234    
235        public void connecting(URI remoteLocation, URI localLocation) throws IOException, Exception {
236            this.channel = DatagramChannel.open();
237            initializeChannel();
238            this.remoteLocation = remoteLocation;
239            this.localLocation = localLocation;
240    
241            if (localLocation != null) {
242                InetSocketAddress localAddress = new InetSocketAddress(InetAddress.getByName(localLocation.getHost()), localLocation.getPort());
243                channel.socket().bind(localAddress);
244            }
245    
246            String host = resolveHostName(remoteLocation.getHost());
247            InetSocketAddress remoteAddress = new InetSocketAddress(host, remoteLocation.getPort());
248            channel.connect(remoteAddress);
249            this.socketState = new CONNECTED();
250        }
251    
252    
253        public DispatchQueue getDispatchQueue() {
254            return dispatchQueue;
255        }
256    
257        public void setDispatchQueue(DispatchQueue queue) {
258            this.dispatchQueue = queue;
259            if(readSource!=null) readSource.setTargetQueue(queue);
260            if(writeSource!=null) writeSource.setTargetQueue(queue);
261            if(drainOutboundSource!=null) drainOutboundSource.setTargetQueue(queue);
262            if(yieldSource!=null) yieldSource.setTargetQueue(queue);
263        }
264    
265        public void _start(Task onCompleted) {
266            try {
267                if (socketState.is(CONNECTED.class) ) {
268                    dispatchQueue.execute(new Task() {
269                        public void run() {
270                            try {
271                                trace("was connected.");
272                                onConnected();
273                            } catch (IOException e) {
274                                 onTransportFailure(e);
275                            }
276                        }
277                    });
278                } else {
279                    System.err.println("cannot be started.  socket state is: "+socketState);
280                }
281            } finally {
282                if( onCompleted!=null ) {
283                    onCompleted.run();
284                }
285            }
286        }
287    
288        public void _stop(final Task onCompleted) {
289            trace("stopping.. at state: "+socketState);
290            socketState.onStop(onCompleted);
291        }
292    
293        protected String resolveHostName(String host) throws UnknownHostException {
294            String localName = InetAddress.getLocalHost().getHostName();
295            if (localName != null && isUseLocalHost()) {
296                if (localName.equals(host)) {
297                    return "localhost";
298                }
299            }
300            return host;
301        }
302    
303        protected void onConnected() throws IOException {
304            yieldSource = Dispatch.createSource(EventAggregators.INTEGER_ADD, dispatchQueue);
305            yieldSource.setEventHandler(new Task() {
306                public void run() {
307                    drainInbound();
308                }
309            });
310            yieldSource.resume();
311            drainOutboundSource = Dispatch.createSource(EventAggregators.INTEGER_ADD, dispatchQueue);
312            drainOutboundSource.setEventHandler(new Task() {
313                public void run() {
314                    flush();
315                }
316            });
317            drainOutboundSource.resume();
318    
319            readSource = Dispatch.createSource(channel, SelectionKey.OP_READ, dispatchQueue);
320            writeSource = Dispatch.createSource(channel, SelectionKey.OP_WRITE, dispatchQueue);
321    
322            readSource.setCancelHandler(CANCEL_HANDLER);
323            writeSource.setCancelHandler(CANCEL_HANDLER);
324    
325            readSource.setEventHandler(new Task() {
326                public void run() {
327                    drainInbound();
328                }
329            });
330            writeSource.setEventHandler(new Task() {
331                public void run() {
332                    flush();
333                }
334            });
335            listener.onTransportConnected();
336        }
337    
338        Task onDispose;
339    
340        private void dispose() {
341            if( readSource!=null ) {
342                readSource.cancel();
343                readSource=null;
344            }
345    
346            if( writeSource!=null ) {
347                writeSource.cancel();
348                writeSource=null;
349            }
350            this.codec = null;
351            if(onDispose!=null) {
352                onDispose.run();
353                onDispose = null;
354            }
355        }
356    
357        public void onTransportFailure(IOException error) {
358            listener.onTransportFailure(error);
359            socketState.onCanceled();
360        }
361    
362    
363        public boolean full() {
364            return codec==null || codec.full();
365        }
366    
367        boolean rejectingOffers;
368    
369        public boolean offer(Object command) {
370            dispatchQueue.assertExecuting();
371            try {
372                if (!socketState.is(CONNECTED.class)) {
373                    throw new IOException("Not connected.");
374                }
375                if (getServiceState() != STARTED) {
376                    throw new IOException("Not running.");
377                }
378    
379                ProtocolCodec.BufferState rc = codec.write(command);
380                rejectingOffers = codec.full();
381                switch (rc ) {
382                    case FULL:
383                        return false;
384                    default:
385                        drainOutboundSource.merge(1);
386                        return true;
387                }
388            } catch (IOException e) {
389                onTransportFailure(e);
390                return false;
391            }
392    
393        }
394    
395        boolean writeResumedForCodecFlush = false;
396    
397        /**
398         *
399         */
400        public void flush() {
401            dispatchQueue.assertExecuting();
402            if (getServiceState() != STARTED || !socketState.is(CONNECTED.class)) {
403                return;
404            }
405            try {
406                if( codec.flush() == ProtocolCodec.BufferState.EMPTY && transportFlush() ) {
407                    if( writeResumedForCodecFlush) {
408                        writeResumedForCodecFlush = false;
409                        suspendWrite();
410                    }
411                    rejectingOffers = false;
412                    listener.onRefill();
413    
414                } else {
415                    if(!writeResumedForCodecFlush) {
416                        writeResumedForCodecFlush = true;
417                        resumeWrite();
418                    }
419                }
420            } catch (IOException e) {
421                onTransportFailure(e);
422            }
423        }
424    
425        protected boolean transportFlush() throws IOException {
426            return true;
427        }
428    
429        protected void drainInbound() {
430            if (!getServiceState().isStarted() || readSource.isSuspended()) {
431                return;
432            }
433            try {
434                long initial = codec.getReadCounter();
435                // Only process upto 2 x the read buffer worth of data at a time so we can give
436                // other connections a chance to process their requests.
437                while( codec.getReadCounter()-initial < codec.getReadBufferSize()<<2 ) {
438                    Object command = codec.read();
439                    if ( command!=null ) {
440                        try {
441                            listener.onTransportCommand(command);
442                        } catch (Throwable e) {
443                            e.printStackTrace();
444                            onTransportFailure(new IOException("Transport listener failure."));
445                        }
446    
447                        // the transport may be suspended after processing a command.
448                        if (getServiceState() == STOPPED || readSource.isSuspended()) {
449                            return;
450                        }
451                    } else {
452                        return;
453                    }
454                }
455                yieldSource.merge(1);
456            } catch (IOException e) {
457                onTransportFailure(e);
458            }
459        }
460    
461        public SocketAddress getLocalAddress() {
462            return localAddress;
463        }
464    
465        public SocketAddress getRemoteAddress() {
466            return remoteAddress;
467        }
468    
469        private boolean assertConnected() {
470            try {
471                if ( !isConnected() ) {
472                    throw new IOException("Not connected.");
473                }
474                return true;
475            } catch (IOException e) {
476                onTransportFailure(e);
477            }
478            return false;
479        }
480    
481        public void suspendRead() {
482            if( isConnected() && readSource!=null ) {
483                readSource.suspend();
484            }
485        }
486    
487    
488        public void resumeRead() {
489            if( isConnected() && readSource!=null ) {
490                _resumeRead();
491            }
492        }
493    
494        private void _resumeRead() {
495            readSource.resume();
496            dispatchQueue.execute(new Task(){
497                public void run() {
498                    drainInbound();
499                }
500            });
501        }
502    
503        protected void suspendWrite() {
504            if( isConnected() && writeSource!=null ) {
505                writeSource.suspend();
506            }
507        }
508    
509        protected void resumeWrite() {
510            if( isConnected() && writeSource!=null ) {
511                writeSource.resume();
512            }
513        }
514    
515        public TransportListener getTransportListener() {
516            return listener;
517        }
518    
519        public void setTransportListener(TransportListener transportListener) {
520            this.listener = transportListener;
521        }
522    
523        public ProtocolCodec getProtocolCodec() {
524            return codec;
525        }
526    
527        public void setProtocolCodec(ProtocolCodec protocolCodec) throws Exception {
528            this.codec = protocolCodec;
529            if( channel!=null && codec!=null ) {
530                initializeCodec();
531            }
532        }
533    
534        public boolean isConnected() {
535            return socketState.is(CONNECTED.class);
536        }
537    
538        public boolean isClosed() {
539            return getServiceState() == STOPPED;
540        }
541    
542        public boolean isUseLocalHost() {
543            return useLocalHost;
544        }
545    
546        /**
547         * Sets whether 'localhost' or the actual local host name should be used to
548         * make local connections. On some operating systems such as Macs its not
549         * possible to connect as the local host name so localhost is better.
550         */
551        public void setUseLocalHost(boolean useLocalHost) {
552            this.useLocalHost = useLocalHost;
553        }
554    
555        private void trace(String message) {
556            // TODO:
557        }
558    
559        public DatagramChannel getDatagramChannel() {
560            return channel;
561        }
562    
563        public ReadableByteChannel readChannel() {
564            return channel;
565        }
566    
567        public WritableByteChannel writeChannel() {
568            return channel;
569        }
570    
571        public int getTrafficClass() {
572            return trafficClass;
573        }
574    
575        public void setTrafficClass(int trafficClass) {
576            this.trafficClass = trafficClass;
577        }
578    
579        public int getReceiveBufferSize() {
580            return receiveBufferSize;
581        }
582    
583        public void setReceiveBufferSize(int receiveBufferSize) {
584            this.receiveBufferSize = receiveBufferSize;
585        }
586    
587        public int getSendBufferSize() {
588            return sendBufferSize;
589        }
590    
591        public void setSendBufferSize(int sendBufferSize) {
592            this.sendBufferSize = sendBufferSize;
593        }
594    
595    }