of the new interfaces. Still exists entirely in parallel with the old code. git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@7010 75d07b2b-3a1a-0410-a2c5-0572b91ccdca3.0
parent
f18fb0b287
commit
c551a29022
@ -0,0 +1,111 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network; |
||||
|
||||
|
||||
/** |
||||
* Represents a remote connection to a server that can be used |
||||
* for sending and receiving messages. |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public interface Client extends MessageConnection |
||||
{ |
||||
/** |
||||
* Starts the client allowing it to begin processing incoming |
||||
* messages and delivering them to listeners. |
||||
*/ |
||||
public void start(); |
||||
|
||||
/** |
||||
* Returns true if this client is fully connected to the |
||||
* host. |
||||
*/ |
||||
public boolean isConnected(); |
||||
|
||||
/** |
||||
* Returns a unique ID for this client within the remote |
||||
* server or -1 if this client isn't fully connected to the |
||||
* server. |
||||
*/ |
||||
public long getId(); |
||||
|
||||
/** |
||||
* Sends a message to the server. |
||||
*/ |
||||
public void send( Message message ); |
||||
|
||||
/** |
||||
* Closes this connection to the server. |
||||
*/ |
||||
public void close(); |
||||
|
||||
/** |
||||
* Adds a listener that will be notified about connection |
||||
* state changes. |
||||
*/ |
||||
public void addClientStateListener( ClientStateListener listener ); |
||||
|
||||
/** |
||||
* Removes a previously registered connection listener. |
||||
*/ |
||||
public void removeClientStateListener( ClientStateListener listener ); |
||||
|
||||
/** |
||||
* Adds a listener that will be notified when any message or object |
||||
* is received from the server. |
||||
*/ |
||||
public void addMessageListener( MessageListener<? super Client> listener ); |
||||
|
||||
/** |
||||
* Adds a listener that will be notified when messages of the specified |
||||
* types are received. |
||||
*/ |
||||
public void addMessageListener( MessageListener<? super Client> listener, Class... classes ); |
||||
|
||||
/** |
||||
* Removes a previously registered wildcard listener. This does |
||||
* not remove this listener from any type-specific registrations. |
||||
*/ |
||||
public void removeMessageListener( MessageListener<? super Client> listener ); |
||||
|
||||
/** |
||||
* Removes a previously registered type-specific listener from |
||||
* the specified types. |
||||
*/ |
||||
public void removeMessageListener( MessageListener<? super Client> listener, Class... classes ); |
||||
|
||||
} |
||||
|
||||
|
@ -0,0 +1,56 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network; |
||||
|
||||
|
||||
/** |
||||
* Listener that is notified about the connection state of |
||||
* a Client. |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public interface ClientStateListener |
||||
{ |
||||
/** |
||||
* Called when the specified client is fully connected to |
||||
* the remote server. |
||||
*/ |
||||
public void clientConnected( Client c ); |
||||
|
||||
/** |
||||
* Called when the client has disconnected from the remote |
||||
* server. |
||||
*/ |
||||
public void clientDisconnected( Client c ); |
||||
} |
@ -0,0 +1,56 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network; |
||||
|
||||
|
||||
/** |
||||
* Listener that is notified about connection arrivals and |
||||
* removals within a server. |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public interface ConnectionListener |
||||
{ |
||||
/** |
||||
* Called when a connection has been added to the specified server and |
||||
* is fully setup. |
||||
*/ |
||||
public void connectionAdded( Server server, HostedConnection conn ); |
||||
|
||||
/** |
||||
* Called when a connection has been removed from the specified |
||||
* server. |
||||
*/ |
||||
public void connectionRemoved( Server server, HostedConnection conn ); |
||||
} |
@ -0,0 +1,55 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network; |
||||
|
||||
|
||||
/** |
||||
* This is the connection back to a client that is being |
||||
* hosted in a server instance. |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public interface HostedConnection extends MessageConnection |
||||
{ |
||||
/** |
||||
* Returns the server-unique ID for this client. |
||||
*/ |
||||
public long getId(); |
||||
|
||||
/** |
||||
* Closes and removes this connection from the server |
||||
* sending the optional reason to the remote client. |
||||
*/ |
||||
public void close( String reason ); |
||||
} |
@ -0,0 +1,47 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network; |
||||
|
||||
import com.jme3.network.serializing.Serializable; |
||||
|
||||
/** |
||||
* Interface implemented by all network messages. |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
@Serializable() |
||||
public interface Message |
||||
{ |
||||
public boolean isReliable(); |
||||
} |
@ -0,0 +1,50 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network; |
||||
|
||||
|
||||
/** |
||||
* The source of a received message and the common abstract interface
|
||||
* of client->server and server->client objects. |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public interface MessageConnection |
||||
{ |
||||
/** |
||||
* Sends a message to the other end of the connection. |
||||
*/ |
||||
public void send( Message message ); |
||||
} |
||||
|
@ -0,0 +1,45 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network; |
||||
|
||||
|
||||
/** |
||||
* Listener notified about new messages |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public interface MessageListener<S> |
||||
{ |
||||
public void messageReceived( S source, Message m ); |
||||
} |
@ -0,0 +1,92 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network; |
||||
|
||||
import java.io.IOException; |
||||
import java.net.InetAddress; |
||||
|
||||
import com.jme3.network.base.DefaultClient; |
||||
import com.jme3.network.base.DefaultServer; |
||||
import com.jme3.network.kernel.tcp.SelectorKernel; |
||||
import com.jme3.network.kernel.tcp.SocketConnector; |
||||
import com.jme3.network.kernel.udp.UdpConnector; |
||||
import com.jme3.network.kernel.udp.UdpKernel; |
||||
|
||||
/** |
||||
* The main service provider for conveniently creating |
||||
* server and client instances. |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public class Network |
||||
{ |
||||
/** |
||||
* Creates a Server that will utilize both reliable and fast |
||||
* transports to communicate with clients. The specified port |
||||
* will be used for both TCP and UDP communication. |
||||
*/ |
||||
public static Server createServer( int port ) throws IOException |
||||
{ |
||||
InetAddress local = InetAddress.getLocalHost(); |
||||
|
||||
UdpKernel fast = new UdpKernel(local, port); |
||||
SelectorKernel reliable = new SelectorKernel(local,port); |
||||
|
||||
return new DefaultServer( reliable, fast ); |
||||
} |
||||
|
||||
/** |
||||
* Creates a Client that communicates with the specified host and port |
||||
* using both reliable and fast transports. The localUdpPort specifies the |
||||
* local port to use for listening for incoming 'fast' UDP messages. |
||||
*/ |
||||
public static Client connectToServer( String host, int hostPort, int localUdpPort ) throws IOException |
||||
{ |
||||
return connectToServer( InetAddress.getByName(host), hostPort, localUdpPort ); |
||||
} |
||||
|
||||
/** |
||||
* Creates a Client that communicates with the specified address and port |
||||
* using both reliable and fast transports. The localUdpPort specifies the |
||||
* local port to use for listening for incoming 'fast' messages. |
||||
*/ |
||||
public static Client connectToServer( InetAddress address, int port, int localUdpPort ) throws IOException |
||||
{ |
||||
InetAddress local = InetAddress.getLocalHost(); |
||||
UdpConnector fast = new UdpConnector( local, localUdpPort, address, port ); |
||||
SocketConnector reliable = new SocketConnector( address, port ); |
||||
|
||||
return new DefaultClient( reliable, fast ); |
||||
} |
||||
} |
@ -0,0 +1,121 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network; |
||||
|
||||
import java.util.Collection; |
||||
|
||||
/** |
||||
* Represents a host that can send and receive messages to |
||||
* a set of remote client connections. |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public interface Server |
||||
{ |
||||
/** |
||||
* Sends the specified message to all connected clients. |
||||
*/ |
||||
public void broadcast( Message message ); |
||||
|
||||
/** |
||||
* Sends the specified message to all connected clients that match |
||||
* the filter. |
||||
*/ |
||||
public void broadcast( Object filter, Message message ); |
||||
|
||||
/** |
||||
* Start the server so that it will began accepting new connections |
||||
* and processing messages. |
||||
*/ |
||||
public void start(); |
||||
|
||||
/** |
||||
* Returns true if the server has been started. |
||||
*/ |
||||
public boolean isRunning(); |
||||
|
||||
/** |
||||
* Closes all client connections, stops and running processing threads, and |
||||
* closes the host connection. |
||||
*/ |
||||
public void close(); |
||||
|
||||
/** |
||||
* Retrieves a hosted connection by ID. |
||||
*/ |
||||
public HostedConnection getConnection( long id ); |
||||
|
||||
/** |
||||
* Retrieves a read-only collection of all currently connected connections. |
||||
*/ |
||||
public Collection<HostedConnection> getConnections(); |
||||
|
||||
/** |
||||
* Adds a listener that will be notified when new hosted connections |
||||
* arrive. |
||||
*/ |
||||
public void addConnectionListener( ConnectionListener listener ); |
||||
|
||||
/** |
||||
* Removes a previously registered connection listener. |
||||
*/ |
||||
public void removeConnectionListener( ConnectionListener listener ); |
||||
|
||||
/** |
||||
* Adds a listener that will be notified when any message or object |
||||
* is received from one of the clients. |
||||
*/ |
||||
public void addMessageListener( MessageListener<? super HostedConnection> listener ); |
||||
|
||||
/** |
||||
* Adds a listener that will be notified when messages of the specified |
||||
* types are received from one of the clients. |
||||
*/ |
||||
public void addMessageListener( MessageListener<? super HostedConnection> listener, Class... classes ); |
||||
|
||||
/** |
||||
* Removes a previously registered wildcard listener. This does |
||||
* not remove this listener from any type-specific registrations. |
||||
*/ |
||||
public void removeMessageListener( MessageListener<? super HostedConnection> listener ); |
||||
|
||||
/** |
||||
* Removes a previously registered type-specific listener from |
||||
* the specified types. |
||||
*/ |
||||
public void removeMessageListener( MessageListener<? super HostedConnection> listener, Class... classes ); |
||||
|
||||
|
||||
} |
||||
|
@ -0,0 +1,142 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network.base; |
||||
|
||||
import java.io.IOException; |
||||
import java.nio.ByteBuffer; |
||||
import java.util.concurrent.atomic.AtomicBoolean; |
||||
|
||||
import com.jme3.network.Message; |
||||
import com.jme3.network.MessageListener; |
||||
import com.jme3.network.kernel.Connector; |
||||
import com.jme3.network.serializing.Serializer; |
||||
|
||||
/** |
||||
* Wraps a single Connector and forwards new messages |
||||
* to the supplied message dispatcher. This is used |
||||
* by DefaultClient to manage its connector objects. |
||||
* This is only responsible for message reading and provides |
||||
* no support for buffering writes. |
||||
* |
||||
* <p>This adapter assumes a simple protocol where two |
||||
* bytes define a (short) object size with the object data |
||||
* to follow. Note: this limits the size of serialized |
||||
* objects to 32676 bytes... even though, for example, |
||||
* datagram packets can hold twice that. :P</p> |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public class ConnectorAdapter extends Thread |
||||
{ |
||||
private Connector connector; |
||||
private MessageListener dispatcher; |
||||
private AtomicBoolean go = new AtomicBoolean(true); |
||||
|
||||
public ConnectorAdapter( Connector connector, MessageListener dispatcher ) |
||||
{ |
||||
super( String.valueOf(connector) ); |
||||
this.connector = connector; |
||||
this.dispatcher = dispatcher; |
||||
setDaemon(true); |
||||
} |
||||
|
||||
public void close() |
||||
{ |
||||
go.set(false); |
||||
|
||||
// Kill the connector
|
||||
connector.close(); |
||||
} |
||||
|
||||
protected void createAndDispatch( ByteBuffer buffer ) |
||||
{ |
||||
try { |
||||
Object obj = Serializer.readClassAndObject( buffer ); |
||||
Message m = (Message)obj; |
||||
dispatcher.messageReceived( null, m ); |
||||
} catch( IOException e ) { |
||||
throw new RuntimeException( "Error deserializing object", e ); |
||||
} |
||||
} |
||||
|
||||
public void run() |
||||
{ |
||||
ByteBuffer current = null; |
||||
int size = 0; |
||||
|
||||
while( go.get() ) { |
||||
ByteBuffer buffer = connector.read(); |
||||
|
||||
// push the data from the buffer into as
|
||||
// many messages as we can
|
||||
while( buffer.remaining() > 0 ) { |
||||
|
||||
if( current == null ) { |
||||
// We are not currently reading an object so
|
||||
// grab the size.
|
||||
// Note: this is somewhat limiting... int would
|
||||
// be better.
|
||||
size = buffer.getShort(); |
||||
current = ByteBuffer.allocate(size); |
||||
} |
||||
|
||||
if( current.remaining() <= buffer.remaining() ) { |
||||
// We have at least one complete object so
|
||||
// copy what we can into current, create a message,
|
||||
// and then continue pulling from buffer.
|
||||
|
||||
// Artificially set the limit so we don't overflow
|
||||
int extra = buffer.remaining() - current.remaining(); |
||||
buffer.limit( buffer.position() + current.remaining() ); |
||||
|
||||
// Now copy the data
|
||||
current.put( buffer ); |
||||
current.flip(); |
||||
|
||||
// Now set the limit back to a good value
|
||||
buffer.limit( buffer.position() + extra ); |
||||
|
||||
createAndDispatch( current ); |
||||
|
||||
current = null; |
||||
} else { |
||||
|
||||
// Not yet a complete object so just copy what we have
|
||||
current.put( buffer ); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,258 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network.base; |
||||
|
||||
import java.io.IOException; |
||||
import java.nio.ByteBuffer; |
||||
import java.util.*; |
||||
import java.util.concurrent.*; |
||||
import java.util.logging.Level; |
||||
import java.util.logging.Logger; |
||||
|
||||
import com.jme3.network.*; |
||||
import com.jme3.network.message.ClientRegistrationMessage; //hopefully temporary
|
||||
import com.jme3.network.kernel.Connector; |
||||
import com.jme3.network.serializing.Serializer; |
||||
|
||||
/** |
||||
* A default implementation of the Client interface that delegates |
||||
* its network connectivity to a kernel.Connector. |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public class DefaultClient implements Client |
||||
{ |
||||
static Logger log = Logger.getLogger(DefaultClient.class.getName()); |
||||
|
||||
private long id = -1; |
||||
private boolean isRunning = false; |
||||
private Connector reliable; |
||||
private Connector fast; |
||||
private MessageListenerRegistry<Client> messageListeners = new MessageListenerRegistry<Client>(); |
||||
private List<ClientStateListener> stateListeners = new CopyOnWriteArrayList<ClientStateListener>(); |
||||
private Redispatch dispatcher = new Redispatch(); |
||||
private ConnectorAdapter reliableAdapter; |
||||
private ConnectorAdapter fastAdapter; |
||||
|
||||
public DefaultClient( Connector reliable, Connector fast ) |
||||
{ |
||||
this.reliable = reliable; |
||||
this.fast = fast; |
||||
if( reliable != null ) { |
||||
reliableAdapter = new ConnectorAdapter(reliable, dispatcher); |
||||
} |
||||
if( fast != null ) { |
||||
fastAdapter = new ConnectorAdapter(fast, dispatcher); |
||||
} |
||||
} |
||||
|
||||
protected void checkRunning() |
||||
{ |
||||
if( !isRunning ) |
||||
throw new IllegalStateException( "Client is not started." ); |
||||
} |
||||
|
||||
public void start() |
||||
{ |
||||
if( isRunning ) |
||||
throw new IllegalStateException( "Client is already started." ); |
||||
|
||||
// Start up the threads and stuff
|
||||
if( reliableAdapter != null ) { |
||||
reliableAdapter.start(); |
||||
} |
||||
if( fastAdapter != null ) { |
||||
fastAdapter.start(); |
||||
} |
||||
|
||||
// Send our connection message with a generated ID until
|
||||
// we get one back from the server. We'll hash time in
|
||||
// millis and time in nanos.
|
||||
long tempId = System.currentTimeMillis() ^ System.nanoTime(); |
||||
|
||||
// Set it true here so we can send some messages.
|
||||
isRunning = true; |
||||
|
||||
ClientRegistrationMessage reg; |
||||
if( reliable != null ) { |
||||
reg = new ClientRegistrationMessage(); |
||||
reg.setId(tempId); |
||||
reg.setReliable(true); |
||||
send(reg); |
||||
} |
||||
if( fast != null ) { |
||||
// We create two different ones to prepare for someday
|
||||
// when there will probably be threaded sending.
|
||||
reg = new ClientRegistrationMessage(); |
||||
reg.setId(tempId); |
||||
reg.setReliable(false); |
||||
send(reg); |
||||
} |
||||
} |
||||
|
||||
public boolean isConnected() |
||||
{ |
||||
return id != -1; // for now
|
||||
} |
||||
|
||||
public long getId() |
||||
{ |
||||
return id; |
||||
} |
||||
|
||||
protected ByteBuffer messageToBuffer( Message message ) |
||||
{ |
||||
ByteBuffer buffer = ByteBuffer.allocate( 32767 + 2 ); |
||||
|
||||
try { |
||||
buffer.position( 2 ); |
||||
Serializer.writeClassAndObject( buffer, message ); |
||||
buffer.flip(); |
||||
short dataLength = (short)(buffer.remaining() - 2); |
||||
buffer.putShort( dataLength ); |
||||
buffer.position( 0 ); |
||||
|
||||
return buffer; |
||||
} catch( IOException e ) { |
||||
throw new RuntimeException( "Error serializing message", e ); |
||||
} |
||||
} |
||||
|
||||
public void send( Message message ) |
||||
{ |
||||
checkRunning(); |
||||
|
||||
// For now just send direclty. We allocate our
|
||||
// own buffer each time because this method might
|
||||
// be called from multiple threads. If writing
|
||||
// is queued into its own thread then that could
|
||||
// be shared.
|
||||
ByteBuffer buffer = messageToBuffer(message); |
||||
if( message.isReliable() || fast == null ) { |
||||
if( reliable == null ) |
||||
throw new RuntimeException( "No reliable connector configured" ); |
||||
reliable.write(buffer); |
||||
} else { |
||||
fast.write(buffer); |
||||
} |
||||
} |
||||
|
||||
public void close() |
||||
{ |
||||
checkRunning(); |
||||
|
||||
// Send a close message
|
||||
|
||||
// Tell the thread it's ok to die
|
||||
if( fastAdapter != null ) { |
||||
fastAdapter.close(); |
||||
} |
||||
if( reliableAdapter != null ) { |
||||
reliableAdapter.close(); |
||||
} |
||||
|
||||
// Wait for the threads?
|
||||
|
||||
fireDisconnected(); |
||||
|
||||
isRunning = false; |
||||
} |
||||
|
||||
public void addClientStateListener( ClientStateListener listener ) |
||||
{ |
||||
stateListeners.add( listener ); |
||||
} |
||||
|
||||
public void removeClientStateListener( ClientStateListener listener ) |
||||
{ |
||||
stateListeners.remove( listener ); |
||||
} |
||||
|
||||
public void addMessageListener( MessageListener<? super Client> listener ) |
||||
{ |
||||
messageListeners.addMessageListener( listener ); |
||||
} |
||||
|
||||
public void addMessageListener( MessageListener<? super Client> listener, Class... classes ) |
||||
{ |
||||
messageListeners.addMessageListener( listener, classes ); |
||||
} |
||||
|
||||
public void removeMessageListener( MessageListener<? super Client> listener ) |
||||
{ |
||||
messageListeners.removeMessageListener( listener ); |
||||
} |
||||
|
||||
public void removeMessageListener( MessageListener<? super Client> listener, Class... classes ) |
||||
{ |
||||
messageListeners.removeMessageListener( listener, classes ); |
||||
} |
||||
|
||||
protected void fireConnected() |
||||
{ |
||||
for( ClientStateListener l : stateListeners ) { |
||||
l.clientConnected( this ); |
||||
} |
||||
} |
||||
|
||||
protected void fireDisconnected() |
||||
{ |
||||
for( ClientStateListener l : stateListeners ) { |
||||
l.clientDisconnected( this ); |
||||
} |
||||
} |
||||
|
||||
protected void dispatch( Message m ) |
||||
{ |
||||
// Pull off the connection management messages we're
|
||||
// interested in and then pass on the rest.
|
||||
if( m instanceof ClientRegistrationMessage ) { |
||||
// Then we've gotten our real id
|
||||
this.id = ((ClientRegistrationMessage)m).getId(); |
||||
log.log( Level.INFO, "Connection established, id:{0}.", this.id ); |
||||
fireConnected(); |
||||
return; |
||||
} |
||||
|
||||
messageListeners.messageReceived( this, m ); |
||||
} |
||||
|
||||
protected class Redispatch implements MessageListener |
||||
{ |
||||
public void messageReceived( Object source, Message m ) |
||||
{ |
||||
dispatch( m ); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,390 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network.base; |
||||
|
||||
import java.io.IOException; |
||||
import java.nio.ByteBuffer; |
||||
import java.util.*; |
||||
import java.util.concurrent.*; |
||||
import java.util.concurrent.atomic.AtomicLong; |
||||
|
||||
import com.jme3.network.*; |
||||
import com.jme3.network.kernel.*; |
||||
import com.jme3.network.message.ClientRegistrationMessage; //hopefully temporary
|
||||
import com.jme3.network.serializing.Serializer; |
||||
|
||||
/** |
||||
* A default implementation of the Server interface that delegates |
||||
* its network connectivity to kernel.Kernel. |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public class DefaultServer implements Server |
||||
{ |
||||
private boolean isRunning = false; |
||||
private AtomicLong nextId = new AtomicLong(0); |
||||
private Kernel reliable; |
||||
private KernelAdapter reliableAdapter; |
||||
private Kernel fast; |
||||
private KernelAdapter fastAdapter; |
||||
private Redispatch dispatcher = new Redispatch(); |
||||
private Map<Long,HostedConnection> connections = new ConcurrentHashMap<Long,HostedConnection>(); |
||||
private Map<Endpoint,HostedConnection> endpointConnections |
||||
= new ConcurrentHashMap<Endpoint,HostedConnection>(); |
||||
|
||||
// Keeps track of clients for whom we've only received the UDP
|
||||
// registration message
|
||||
private Map<Long,Connection> connecting = new ConcurrentHashMap<Long,Connection>(); |
||||
|
||||
private MessageListenerRegistry<HostedConnection> messageListeners |
||||
= new MessageListenerRegistry<HostedConnection>(); |
||||
private List<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<ConnectionListener>(); |
||||
|
||||
public DefaultServer( Kernel reliable, Kernel fast ) |
||||
{ |
||||
this.reliable = reliable; |
||||
this.fast = fast; |
||||
|
||||
if( reliable != null ) { |
||||
reliableAdapter = new KernelAdapter( this, reliable, dispatcher ); |
||||
} |
||||
if( fast != null ) { |
||||
fastAdapter = new KernelAdapter( this, fast, dispatcher ); |
||||
} |
||||
} |
||||
|
||||
public void start() |
||||
{ |
||||
if( isRunning ) |
||||
throw new IllegalStateException( "Server is already started." ); |
||||
|
||||
// Initialize the kernels
|
||||
if( reliable != null ) { |
||||
reliable.initialize(); |
||||
} |
||||
if( fast != null ) { |
||||
fast.initialize(); |
||||
} |
||||
|
||||
// Start em up
|
||||
if( reliableAdapter != null ) { |
||||
reliableAdapter.start(); |
||||
} |
||||
if( fastAdapter != null ) { |
||||
fastAdapter.start(); |
||||
} |
||||
|
||||
isRunning = true; |
||||
} |
||||
|
||||
public boolean isRunning() |
||||
{ |
||||
return isRunning; |
||||
} |
||||
|
||||
public void close() |
||||
{ |
||||
if( !isRunning ) |
||||
throw new IllegalStateException( "Server is not started." ); |
||||
|
||||
try { |
||||
// Kill the adpaters, they will kill the kernels
|
||||
if( fastAdapter != null ) { |
||||
fastAdapter.close(); |
||||
} |
||||
if( reliableAdapter != null ) { |
||||
reliableAdapter.close(); |
||||
|
||||
isRunning = false; |
||||
} |
||||
} catch( InterruptedException e ) { |
||||
throw new RuntimeException( "Interrupted while closing", e ); |
||||
} |
||||
} |
||||
|
||||
protected ByteBuffer messageToBuffer( Message message ) |
||||
{ |
||||
ByteBuffer buffer = ByteBuffer.allocate( 32767 + 2 ); |
||||
|
||||
try { |
||||
buffer.position( 2 ); |
||||
Serializer.writeClassAndObject( buffer, message ); |
||||
buffer.flip(); |
||||
short dataLength = (short)(buffer.remaining() - 2); |
||||
buffer.putShort( dataLength ); |
||||
buffer.position( 0 ); |
||||
|
||||
return buffer; |
||||
} catch( IOException e ) { |
||||
throw new RuntimeException( "Error serializing message", e ); |
||||
} |
||||
} |
||||
|
||||
public void broadcast( Message message ) |
||||
{ |
||||
broadcast( null, message ); |
||||
} |
||||
|
||||
public void broadcast( Object filter, Message message ) |
||||
{ |
||||
ByteBuffer buffer = messageToBuffer(message); |
||||
|
||||
// Ignore the filter for the moment
|
||||
if( message.isReliable() || fast == null ) { |
||||
if( reliable == null ) |
||||
throw new RuntimeException( "No reliable kernel configured" ); |
||||
reliable.broadcast( filter, buffer, true ); |
||||
} else { |
||||
fast.broadcast( filter, buffer, false ); |
||||
} |
||||
} |
||||
|
||||
public HostedConnection getConnection( long id ) |
||||
{ |
||||
return connections.get(id); |
||||
} |
||||
|
||||
public Collection<HostedConnection> getConnections() |
||||
{ |
||||
return Collections.unmodifiableCollection((Collection<HostedConnection>)connections.values()); |
||||
} |
||||
|
||||
public void addConnectionListener( ConnectionListener listener ) |
||||
{ |
||||
connectionListeners.add(listener); |
||||
} |
||||
|
||||
public void removeConnectionListener( ConnectionListener listener ) |
||||
{ |
||||
connectionListeners.remove(listener); |
||||
} |
||||
|
||||
public void addMessageListener( MessageListener<? super HostedConnection> listener ) |
||||
{ |
||||
messageListeners.addMessageListener( listener ); |
||||
} |
||||
|
||||
public void addMessageListener( MessageListener<? super HostedConnection> listener, Class... classes ) |
||||
{ |
||||
messageListeners.addMessageListener( listener, classes ); |
||||
} |
||||
|
||||
public void removeMessageListener( MessageListener<? super HostedConnection> listener ) |
||||
{ |
||||
messageListeners.removeMessageListener( listener ); |
||||
} |
||||
|
||||
public void removeMessageListener( MessageListener<? super HostedConnection> listener, Class... classes ) |
||||
{ |
||||
messageListeners.removeMessageListener( listener, classes ); |
||||
} |
||||
|
||||
protected void dispatch( HostedConnection source, Message m ) |
||||
{ |
||||
messageListeners.messageReceived( source, m ); |
||||
} |
||||
|
||||
protected void fireConnectionAdded( HostedConnection conn ) |
||||
{ |
||||
for( ConnectionListener l : connectionListeners ) { |
||||
l.connectionAdded( this, conn ); |
||||
} |
||||
} |
||||
|
||||
protected void fireConnectionRemoved( HostedConnection conn ) |
||||
{ |
||||
for( ConnectionListener l : connectionListeners ) { |
||||
l.connectionRemoved( this, conn ); |
||||
} |
||||
} |
||||
|
||||
protected void registerClient( KernelAdapter ka, Endpoint p, ClientRegistrationMessage m ) |
||||
{ |
||||
Connection addedConnection = null; |
||||
|
||||
// generally this will only be called by one thread but it's
|
||||
// important enough I won't take chances
|
||||
synchronized( this ) { |
||||
// Grab the random ID that the client created when creating
|
||||
// its two registration messages
|
||||
long tempId = m.getId(); |
||||
|
||||
// See if we already have one
|
||||
Connection c = connecting.remove(tempId); |
||||
if( c == null ) { |
||||
c = new Connection(); |
||||
} |
||||
|
||||
// Fill in what we now know
|
||||
if( ka == fastAdapter ) { |
||||
c.fast = p; |
||||
|
||||
if( c.reliable == null ) { |
||||
// Tuck it away for later
|
||||
connecting.put(tempId, c); |
||||
} |
||||
|
||||
} else { |
||||
// It must be the reliable one
|
||||
c.reliable = p; |
||||
|
||||
if( c.fast == null && fastAdapter != null ) { |
||||
// Still waiting for the fast connection to
|
||||
// register
|
||||
connecting.put(tempId, c); |
||||
} |
||||
} |
||||
|
||||
if( !connecting.containsKey(tempId) ) { |
||||
|
||||
// Then we are fully connected
|
||||
if( connections.put( c.getId(), c ) == null ) { |
||||
|
||||
if( c.fast != null ) { |
||||
endpointConnections.put( c.fast, c ); |
||||
} |
||||
endpointConnections.put( c.reliable, c ); |
||||
|
||||
addedConnection = c; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Best to do this outside of the synch block to avoid
|
||||
// over synchronizing which is the path to deadlocks
|
||||
if( addedConnection != null ) { |
||||
// Nnow we can notify the listeners about the
|
||||
// new connection.
|
||||
fireConnectionAdded( addedConnection ); |
||||
|
||||
// Send the ID back to the client letting it know it's
|
||||
// fully connected.
|
||||
m = new ClientRegistrationMessage(); |
||||
m.setId( addedConnection.getId() ); |
||||
m.setReliable(true); |
||||
addedConnection.send(m); |
||||
} |
||||
} |
||||
|
||||
protected HostedConnection getConnection( Endpoint endpoint ) |
||||
{ |
||||
return endpointConnections.get(endpoint); |
||||
} |
||||
|
||||
protected void connectionClosed( Endpoint p ) |
||||
{ |
||||
// Try to find the endpoint in all ways that it might
|
||||
// exist. Note: by this point the channel is closed
|
||||
// already.
|
||||
|
||||
// Also note: this method will be called twice per
|
||||
// HostedConnection if it has two endpoints.
|
||||
|
||||
Connection removed = null; |
||||
synchronized( this ) { |
||||
// Just in case the endpoint was still connecting
|
||||
connecting.values().remove(p); |
||||
|
||||
// And the regular management
|
||||
removed = (Connection)endpointConnections.remove(p); |
||||
if( removed != null ) { |
||||
connections.remove( removed.getId() ); |
||||
} |
||||
} |
||||
|
||||
// Better not to fire events while we hold a lock
|
||||
// so always do this outside the synch block.
|
||||
if( removed != null ) { |
||||
|
||||
// Make sure both endpoints are closed. Note: reliable
|
||||
// should always already be closed through all paths that I
|
||||
// can conceive... but it doesn't hurt to be sure.
|
||||
if( removed.reliable != null && removed.reliable.isConnected() ) { |
||||
removed.reliable.close(); |
||||
} |
||||
if( removed.fast != null && removed.fast.isConnected() ) { |
||||
removed.fast.close(); |
||||
} |
||||
|
||||
fireConnectionRemoved( removed ); |
||||
} |
||||
} |
||||
|
||||
protected class Connection implements HostedConnection |
||||
{ |
||||
private long id; |
||||
private Endpoint reliable; |
||||
private Endpoint fast; |
||||
|
||||
public Connection() |
||||
{ |
||||
id = nextId.getAndIncrement(); |
||||
} |
||||
|
||||
public long getId() |
||||
{ |
||||
return id; |
||||
} |
||||
|
||||
public void send( Message message ) |
||||
{ |
||||
ByteBuffer buffer = messageToBuffer(message); |
||||
if( message.isReliable() || fast == null ) { |
||||
reliable.send( buffer ); |
||||
} else { |
||||
fast.send( buffer ); |
||||
} |
||||
} |
||||
|
||||
public void close( String reason ) |
||||
{ |
||||
// Send a reason
|
||||
|
||||
// Just close the reliable endpoint
|
||||
// fast will be cleaned up as a side-effect
|
||||
if( reliable != null ) { |
||||
reliable.close(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
protected class Redispatch implements MessageListener<HostedConnection> |
||||
{ |
||||
public void messageReceived( HostedConnection source, Message m ) |
||||
{ |
||||
dispatch( source, m ); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,210 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network.base; |
||||
|
||||
import java.io.IOException; |
||||
import java.nio.ByteBuffer; |
||||
import java.util.concurrent.atomic.AtomicBoolean; |
||||
|
||||
import com.jme3.network.*; |
||||
import com.jme3.network.kernel.Endpoint; |
||||
import com.jme3.network.kernel.EndpointEvent; |
||||
import com.jme3.network.kernel.Envelope; |
||||
import com.jme3.network.kernel.Kernel; |
||||
import com.jme3.network.message.ClientRegistrationMessage; //hopefully temporary
|
||||
import com.jme3.network.serializing.Serializer; |
||||
|
||||
/** |
||||
* Wraps a single Kernel and forwards new messages |
||||
* to the supplied message dispatcher and new endpoint |
||||
* events to the connection dispatcher. This is used |
||||
* by DefaultServer to manage its kernel objects. |
||||
* |
||||
* <p>This adapter assumes a simple protocol where two |
||||
* bytes define a (short) object size with the object data |
||||
* to follow. Note: this limits the size of serialized |
||||
* objects to 32676 bytes... even though, for example, |
||||
* datagram packets can hold twice that. :P</p> |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public class KernelAdapter extends Thread |
||||
{ |
||||
private DefaultServer server; // this is unfortunate
|
||||
private Kernel kernel; |
||||
private MessageListener messageDispatcher; |
||||
private AtomicBoolean go = new AtomicBoolean(true); |
||||
|
||||
public KernelAdapter( DefaultServer server, Kernel kernel, MessageListener messageDispatcher ) |
||||
{ |
||||
super( String.valueOf(kernel) ); |
||||
this.server = server; |
||||
this.kernel = kernel; |
||||
this.messageDispatcher = messageDispatcher; |
||||
setDaemon(true); |
||||
} |
||||
|
||||
public void close() throws InterruptedException |
||||
{ |
||||
go.set(false); |
||||
|
||||
// Kill the kernel
|
||||
kernel.terminate(); |
||||
} |
||||
|
||||
protected HostedConnection getConnection( Endpoint p ) |
||||
{ |
||||
return server.getConnection(p); |
||||
} |
||||
|
||||
protected void connectionClosed( Endpoint p ) |
||||
{ |
||||
server.connectionClosed(p); |
||||
} |
||||
|
||||
protected void createAndDispatch( Endpoint p, ByteBuffer buffer ) |
||||
{ |
||||
try { |
||||
Object obj = Serializer.readClassAndObject( buffer ); |
||||
Message m = (Message)obj; |
||||
|
||||
// Because this class is the only one with the information
|
||||
// to do it... we need to pull of the registration message
|
||||
// here.
|
||||
if( m instanceof ClientRegistrationMessage ) { |
||||
server.registerClient( this, p, (ClientRegistrationMessage)m ); |
||||
return; |
||||
} |
||||
|
||||
HostedConnection source = getConnection(p); |
||||
messageDispatcher.messageReceived( source, m ); |
||||
} catch( IOException e ) { |
||||
throw new RuntimeException( "Error deserializing object", e ); |
||||
} |
||||
} |
||||
|
||||
protected void createAndDispatch( Envelope env ) |
||||
{ |
||||
byte[] data = env.getData(); |
||||
ByteBuffer buffer = ByteBuffer.wrap(data); |
||||
|
||||
ByteBuffer current = null; |
||||
int size = 0; |
||||
|
||||
// push the data from the buffer into as
|
||||
// many messages as we can
|
||||
while( buffer.remaining() > 0 ) { |
||||
|
||||
if( current == null ) { |
||||
// We are not currently reading an object so
|
||||
// grab the size.
|
||||
// Note: this is somewhat limiting... int would
|
||||
// be better.
|
||||
size = buffer.getShort(); |
||||
current = ByteBuffer.allocate(size); |
||||
} |
||||
|
||||
if( current.remaining() <= buffer.remaining() ) { |
||||
// We have at least one complete object so
|
||||
// copy what we can into current, create a message,
|
||||
// and then continue pulling from buffer.
|
||||
|
||||
// Artificially set the limit so we don't overflow
|
||||
int extra = buffer.remaining() - current.remaining(); |
||||
buffer.limit( buffer.position() + current.remaining() ); |
||||
|
||||
// Now copy the data
|
||||
current.put( buffer ); |
||||
current.flip(); |
||||
|
||||
// Now set the limit back to a good value
|
||||
buffer.limit( buffer.position() + extra ); |
||||
|
||||
createAndDispatch( env.getSource(), current ); |
||||
|
||||
current = null; |
||||
} else { |
||||
|
||||
// Not yet a complete object so just copy what we have
|
||||
current.put( buffer ); |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
protected void createAndDispatch( EndpointEvent event ) |
||||
{ |
||||
// Only need to tell the server about disconnects
|
||||
if( event.getType() == EndpointEvent.Type.REMOVE ) { |
||||
connectionClosed( event.getEndpoint() ); |
||||
} |
||||
} |
||||
|
||||
protected void flushEvents() |
||||
{ |
||||
EndpointEvent event; |
||||
while( (event = kernel.nextEvent()) != null ) |
||||
{ |
||||
createAndDispatch( event ); |
||||
} |
||||
} |
||||
|
||||
public void run() |
||||
{ |
||||
while( go.get() ) { |
||||
|
||||
try { |
||||
// Check for pending events
|
||||
flushEvents(); |
||||
|
||||
// Grab the next envelope
|
||||
Envelope e = kernel.read(); |
||||
|
||||
// Check for pending events that might have
|
||||
// come in while we were blocking. This is usually
|
||||
// when the connection add events come through
|
||||
flushEvents(); |
||||
|
||||
createAndDispatch( e ); |
||||
} catch( InterruptedException ex ) { |
||||
if( !go.get() ) |
||||
return; |
||||
throw new RuntimeException( "Unexpected interruption", ex ); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
|
@ -0,0 +1,106 @@ |
||||
/* |
||||
* Copyright (c) 2011 jMonkeyEngine |
||||
* All rights reserved. |
||||
* |
||||
* Redistribution and use in source and binary forms, with or without |
||||
* modification, are permitted provided that the following conditions are |
||||
* met: |
||||
* |
||||
* * Redistributions of source code must retain the above copyright |
||||
* notice, this list of conditions and the following disclaimer. |
||||
* |
||||
* * Redistributions in binary form must reproduce the above copyright |
||||
* notice, this list of conditions and the following disclaimer in the |
||||
* documentation and/or other materials provided with the distribution. |
||||
* |
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||
* may be used to endorse or promote products derived from this software |
||||
* without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
package com.jme3.network.base; |
||||
|
||||
import java.util.*; |
||||
import java.util.concurrent.*; |
||||
|
||||
import com.jme3.network.Message; |
||||
import com.jme3.network.MessageListener; |
||||
|
||||
/** |
||||
* Keeps track of message listeners registered to specific |
||||
* types or to any type. |
||||
* |
||||
* @version $Revision$ |
||||
* @author Paul Speed |
||||
*/ |
||||
public class MessageListenerRegistry<S> implements MessageListener<S> |
||||
{ |
||||
private List<MessageListener<? super S>> listeners = new CopyOnWriteArrayList<MessageListener<? super S>>(); |
||||
private Map<Class,List<MessageListener<? super S>>> typeListeners |
||||
= new ConcurrentHashMap<Class,List<MessageListener<? super S>>>(); |
||||
|
||||
public MessageListenerRegistry() |
||||
{ |
||||
} |
||||
|
||||
public void messageReceived( S source, Message m ) |
||||
{ |
||||
for( MessageListener<? super S> l : listeners ) { |
||||
l.messageReceived( source, m ); |
||||
} |
||||
|
||||
for( MessageListener<? super S> l : getListeners(m.getClass(),false) ) { |
||||
l.messageReceived( source, m ); |
||||
} |
||||
} |
||||
|
||||
protected List<MessageListener<? super S>> getListeners( Class c, boolean create ) |
||||
{ |
||||
List<MessageListener<? super S>> result = typeListeners.get(c); |
||||
if( result == null && create ) { |
||||
result = new CopyOnWriteArrayList<MessageListener<? super S>>(); |
||||
typeListeners.put( c, result ); |
||||
} |
||||
|
||||
if( result == null ) { |
||||
result = Collections.emptyList(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
public void addMessageListener( MessageListener<? super S> listener ) |
||||
{ |
||||
listeners.add(listener); |
||||
} |
||||
|
||||
public void removeMessageListener( MessageListener<? super S> listener ) |
||||
{ |
||||
listeners.remove(listener); |
||||
} |
||||
|
||||
public void addMessageListener( MessageListener<? super S> listener, Class... classes ) |
||||
{ |
||||
for( Class c : classes ) { |
||||
getListeners(c, true).add(listener); |
||||
} |
||||
} |
||||
|
||||
public void removeMessageListener( MessageListener<? super S> listener, Class... classes ) |
||||
{ |
||||
for( Class c : classes ) { |
||||
getListeners(c, false).remove(listener); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue