/******************************************************************************* * Copyright (c) 2015 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *******************************************************************************/ package jsettlers.network.infrastructure.channel; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; import java.net.UnknownHostException; import java.util.HashMap; import jsettlers.network.NetworkConstants; import jsettlers.network.NetworkConstants.ENetworkKey; import jsettlers.network.infrastructure.channel.packet.Packet; import jsettlers.network.infrastructure.channel.ping.IPingUpdateListener; import jsettlers.network.infrastructure.channel.ping.IRoundTripTimeSupplier; import jsettlers.network.infrastructure.channel.ping.PingPacket; import jsettlers.network.infrastructure.channel.ping.PingPacketListener; import jsettlers.network.infrastructure.channel.ping.RoundTripTime; import jsettlers.network.infrastructure.channel.reject.RejectPacket; import jsettlers.network.infrastructure.channel.socket.ISocket; import jsettlers.network.infrastructure.channel.socket.ISocketFactory; import jsettlers.network.infrastructure.log.ConsoleLogger; import jsettlers.network.infrastructure.log.Logger; import jsettlers.network.infrastructure.log.SwitchableLogger; /** * This class builds up a logical channel between to network partners. The class allows to send data of type {@link Packet} to the partner and to * register {@link IChannelListener}s to receive incoming data as a callback. * * @author Andreas Eberle * */ public class Channel implements Runnable, IRoundTripTimeSupplier { private final Thread thread; private final SwitchableLogger logger; private final ISocket socket; private final DataOutputStream outStream; private final DataInputStream inStream; private final ByteArrayOutputStream byteBufferOutStream = new ByteArrayOutputStream(); private final DataOutputStream bufferDataOutStream = new DataOutputStream(byteBufferOutStream); private final HashMap<ENetworkKey, IChannelListener> listenerRegistry = new HashMap<ENetworkKey, IChannelListener>(); private final PingPacketListener pingPacketListener; private IChannelClosedListener channelClosedListener; private boolean started; /** * Creates a new Channel with the given socket as the underlying communication method. * * @param socket * The socket to be used for communication. * @throws IOException * If an I/O error occurs when creating the channel or if the socket is not connected. */ public Channel(ISocket socket) throws IOException { this(new ConsoleLogger(socket.toString()), socket); } public Channel(String host, int port) throws UnknownHostException, IOException { this(ISocketFactory.DEFAULT_FACTORY.generateSocket(host, port)); } public Channel(Logger logger, ISocket socket) throws IOException { this.logger = new SwitchableLogger(logger); this.socket = socket; outStream = new DataOutputStream(socket.getOutputStream()); inStream = new DataInputStream(socket.getInputStream()); pingPacketListener = new PingPacketListener(this.logger, this); registerListener(pingPacketListener); thread = new Thread(this, "ChannelForSocket_" + socket); } /** * Starts the message receiving of this {@link Channel}. * <p /> * NOTE: This method may only be called once! * * @throws IllegalThreadStateException * If the thread was already started. * * @see <code>Thread.start()</code> */ public void start() { started = true; thread.start(); } public synchronized void sendPacket(ENetworkKey key, Packet packet) { if (socket.isClosed()) return; try { sendPacketData(key, packet); } catch (IOException e) { } } private void sendPacketData(ENetworkKey key, Packet packet) throws IOException { bufferDataOutStream.flush(); byteBufferOutStream.reset(); packet.serialize(bufferDataOutStream); // write packet to buffer to calculate length bufferDataOutStream.flush(); final int length = byteBufferOutStream.size(); key.writeTo(outStream); // write key, length and the data outStream.writeInt(length); byteBufferOutStream.writeTo(outStream); outStream.flush(); } /** * Registers the given listener to receive data of the type it specifies with it's getKeys() method. * * @param listener * The listener that shall be registered. */ public void registerListener(IChannelListener listener) { ENetworkKey[] keys = listener.getKeys(); for (int i = 0; i < keys.length; i++) { listenerRegistry.put(keys[i], listener); } } public void removeListener(ENetworkKey key) { listenerRegistry.remove(key); } @Override public void run() { while (!socket.isClosed()) { try { ENetworkKey key = ENetworkKey.readFrom(inStream); int length = inStream.readInt(); DataInputStream bufferIn = readBytesToBuffer(inStream, length); IChannelListener listener = listenerRegistry.get(key); if (listener != null) { try { listener.receive(key, length, bufferIn); if (bufferIn.available() > 0) { logger.warn("Deserialization did not read all bytes of input: " + key + " " + length + " " + bufferIn.available()); } } catch (Exception e) { // ignore exceptions thrown in receive e.printStackTrace(); } } else { logger.warn("NO LISTENER FOUND for key: " + key + " (" + socket + ")"); if (key != NetworkConstants.ENetworkKey.REJECT_PACKET) { // prevent endless loop sendPacket(NetworkConstants.ENetworkKey.REJECT_PACKET, new RejectPacket(NetworkConstants.ENetworkMessage.NO_LISTENER_FOUND, key)); } } } catch (Exception e) { try { socket.close(); } catch (IOException ex) { } } } close(); // release the resources if (channelClosedListener != null) { channelClosedListener.channelClosed(); } logger.info("Channel listener shut down: " + socket); } private DataInputStream readBytesToBuffer(DataInputStream inStream, int length) throws IOException { byte[] data = new byte[length]; int alreadyRead = 0; while (length - alreadyRead > 0) { int numberOfBytesRead = inStream.read(data, alreadyRead, length - alreadyRead); if (numberOfBytesRead < 0) { throw new IOException("Stream ended to early!"); } alreadyRead += numberOfBytesRead; } return new DataInputStream(new ByteArrayInputStream(data)); } /** * Closes this {@link Channel} and releases the contained {@link Socket} and the stream resources. */ public void close() { try { inStream.close(); } catch (IOException e1) { } try { outStream.close(); } catch (IOException e1) { } try { socket.close(); } catch (IOException e) { } thread.interrupt(); } /** * Gets the round trip time of this {@link Channel}. * * @return Returns the current round trip time of this {@link Channel}. */ @Override public RoundTripTime getRoundTripTime() { return pingPacketListener.getRoundTripTime(); } /** * Initialize the pinging by sending a first {@link PingPacket}. */ public void initPinging() { pingPacketListener.initPinging(); } public void setPingUpdateListener(IPingUpdateListener pingUpdateListener) { pingPacketListener.setPingUpdateListener(pingUpdateListener); } /** * Sets an {@link IChannelClosedListener} to this {@link Channel}. The given listener will be informed when the {@link Channel} has been shut * down. * <p /> * NOTE: To remove a listener, just call this method with <code>null</code> as argument.<br> * NOTE2: Only one listener may be registered at a time. By setting a new listener, the old one will be replaced. * * @param channelClosedListener * The new {@link IChannelClosedListener} that shall be registered on this {@link Channel}. */ public void setChannelClosedListener(IChannelClosedListener channelClosedListener) { this.channelClosedListener = channelClosedListener; } public boolean isClosed() { return socket.isClosed(); } public boolean isStarted() { return started; } public void setLogger(Logger newLogger) { this.logger.setLogger(newLogger); } }