/** * Copyright (C) 2009-2014 Cars and Tracks Development Project (CTDP). * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package net.ctdp.rfdynhud.plugins.datasender; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Stack; import org.jagatoo.util.streams.LimitedInputStream; /** * Connects to the editor via a socket and sends/receives data (client side). * * @author Marvin Froehlich (CTDP) */ public abstract class AbstractUDPClientCommunicator extends AbstractClientCommunicator { private String connectionString = null; private DatagramSocket socket = null; private InetAddress serverAddress = null; private int serverPort = -1; private long datagramOrdinal = 0L; private volatile boolean running = false; private volatile boolean connected = false; private volatile boolean restartSender = true; private volatile boolean restartReceiver = true; private volatile boolean closeRequested = false; private final Stack<DatagramPacket> datagrams = new Stack<DatagramPacket>(); private boolean commandInProgress = false; private short currentCommand = 0; public final String getLastConnectionString() { return ( connectionString ); } @Override public final boolean isRunning() { return ( running ); } @Override public final boolean isConnected() { return ( connected ); } @Override protected void startCommandImpl( short code ) { synchronized ( eventsBuffer ) { if ( commandInProgress ) throw new IllegalStateException( "Another command (" + currentCommand + ") has been started, but not ended." ); currentCommand = code; commandInProgress = true; try { eventsBuffer.writeLong( datagramOrdinal++ ); eventsBuffer.writeShort( code ); } catch ( IOException e ) { log( e ); } } } @Override protected void endCommandImpl() { byte[] buffer = null; int usedBufferLength = -1; synchronized ( eventsBuffer ) { if ( !commandInProgress ) throw new IllegalStateException( "No command had been started." ); buffer = eventsBuffer0.toByteArray(); // TODO: Optimize by reusing byte arrays! eventsBuffer0.reset(); usedBufferLength = buffer.length; currentCommand = 0; commandInProgress = false; } //if ( buffer != null ) { DatagramPacket datagram = new DatagramPacket( buffer, usedBufferLength ); synchronized ( datagrams ) { datagrams.push( datagram ); } } } private DatagramPacket getDatagramForSimpleCommand( short code ) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream( baos ); try { dos.writeLong( datagramOrdinal++ ); dos.writeShort( code ); dos.close(); return ( new DatagramPacket( baos.toByteArray(), baos.size(), serverAddress, serverPort ) ); } catch ( IOException e ) { log( e ); return ( null ); } } private final Runnable sender = new Runnable() { private final List<DatagramPacket> datagramsCopy = new ArrayList<DatagramPacket>(); @Override public void run() { datagramsCopy.clear(); datagramsCopy.add( getDatagramForSimpleCommand( CommunicatorConstants.CONNECTION_REQUEST ) ); while ( running ) { DatagramPacket closeDatagram = null; synchronized ( datagrams ) { // If close requested, send a close datagram. if ( closeRequested ) { closeDatagram = getDatagramForSimpleCommand( CommunicatorConstants.CONNECTION_CLOSED ); if ( closeDatagram != null ) { running = false; closeRequested = false; // Forgit pending datagrams. datagrams.clear(); datagramsCopy.add( closeDatagram ); } } else if ( !datagrams.isEmpty() ) { // Copy pending datagrams to a local buffer... for ( int i = 0; i < datagrams.size(); i++ ) datagramsCopy.add( datagrams.get( i ) ); datagrams.clear(); } } // Send all pending datagrams... if ( !datagramsCopy.isEmpty() ) { for ( int i = 0; i < datagramsCopy.size(); i++ ) { DatagramPacket datagram = datagramsCopy.get( i ); datagram.setAddress( serverAddress ); datagram.setPort( serverPort ); try { socket.send( datagram ); } catch ( SocketException e ) { if ( !closeRequested ) { log( "Connection closed unexpectedly" ); log( e ); running = false; close( true ); } break; } catch ( IOException e ) { log( e ); } if ( datagram != closeDatagram ) { // TODO: push used datagrams to a pool to reuse them. } } datagramsCopy.clear(); } try { Thread.sleep( 10L ); } catch ( InterruptedException e ) { log( e ); } } running = false; connected = false; closeRequested = false; if ( socket != null ) { socket.close(); socket = null; } synchronized ( eventsBuffer ) { eventsBuffer0.reset(); } onConnectionClosed(); if ( restartSender ) { try { Thread.sleep( 200L ); } catch ( InterruptedException e ) { } new Thread( this ).start(); } } }; private final Runnable receiver = new Runnable() { @Override public void run() { byte[] buffer = new byte[ 1024 * 1024 ]; ByteArrayInputStream bais = new ByteArrayInputStream( buffer ); LimitedInputStream lin = new LimitedInputStream( bais, buffer.length ); DataInputStream din = new DataInputStream( lin ); DatagramPacket datagram = new DatagramPacket( buffer, buffer.length ); while ( running ) { try { socket.receive( datagram ); bais.reset(); lin.resetLimit( datagram.getLength() ); while ( din.available() >= 10 ) { //plugin.debug( "in.available: ", din.available() ); @SuppressWarnings( "unused" ) long receivedDatagramOrdinal = din.readLong(); readInput( din ); } } catch ( SocketException e ) { if ( !closeRequested ) { log( "Connection closed unexpectedly" ); log( e ); //close( true ); } break; } catch ( IOException e ) { log( e ); } try { Thread.sleep( 10L ); } catch ( InterruptedException e ) { log( e ); } } running = false; connected = false; closeRequested = false; if ( socket != null ) { socket.close(); socket = null; } synchronized ( eventsBuffer ) { eventsBuffer0.reset(); } if ( restartReceiver ) { try { Thread.sleep( 200L ); } catch ( InterruptedException e ) { } new Thread( this ).start(); } } }; public static Object[] parseConnectionString( String connectionString ) { String host = connectionString; int port = 9876; int p = host.indexOf( ':' ); if ( p >= 0 ) { port = Integer.parseInt( host.substring( p + 1 ) ); host = host.substring( 0, p ); } return ( new Object[] { host, port } ); } @Override public void connect( String connectionString ) { if ( running ) return; Object[] parsed = parseConnectionString( connectionString ); this.connectionString = connectionString; try { this.serverAddress = InetAddress.getByName( (String)parsed[0] ); } catch ( UnknownHostException e ) { throw new Error( e ); } this.serverPort = (Integer)parsed[1]; try { if ( socket == null ) { //socket = new DatagramSocket( serverPort ); socket = new DatagramSocket(); socket.setReuseAddress( true ); } datagramOrdinal = 0L; } catch ( Throwable t ) { running = false; log( t ); return; } running = true; closeRequested = false; new Thread( sender ).start(); new Thread( receiver ).start(); } protected void beforeClosed() { } @Override protected final void close( boolean restart ) { if ( connected ) { beforeClosed(); closeRequested = true; } else { running = false; } this.restartSender = restart; this.restartReceiver = restart; if ( socket != null ) { socket.close(); socket = null; } } public AbstractUDPClientCommunicator() { } }