Package net.yura.lobby.client

Source Code of net.yura.lobby.client.TcpClient

package net.yura.lobby.client;

import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.UnresolvedAddressException;
import java.nio.channels.WritableByteChannel;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
//import javax.annotation.PostConstruct;

/**
* A simple NIO TCP client
* Assumptions:
* - the client should always be connected,
*   once it gets disconnected it reconnects
* - the exception thrown by onRead means protocol error
*   so client disconnects and reconnects
* - the incoming flow is higher than outgoing, so
*   direct channel write method is not implemented
*
* @author Vladimir Lysyy (mail@bobah.net)
*
*/
public abstract class TcpClient implements Runnable {
  protected static final Logger LOG = Logger.getLogger(TcpClient.class.getName());
  private static final long INITIAL_RECONNECT_INTERVAL = 500; // 500 ms.
  private static final long MAXIMUM_RECONNECT_INTERVAL = 30000; // 30 sec.
  private static final int READ_BUFFER_SIZE = 0x100000;
  private static final int WRITE_BUFFER_SIZE = 0x100000;

  private long reconnectInterval = INITIAL_RECONNECT_INTERVAL;

  private ByteBuffer readBuf = ByteBuffer.allocateDirect(READ_BUFFER_SIZE); // 1Mb
  private ByteBuffer writeBuf = ByteBuffer.allocateDirect(WRITE_BUFFER_SIZE); // 1Mb

  private final Thread thread = new Thread(null,this,"TCP-Client",100000000);
 
  // we store as String and int as android does not allow the creating of a new InetSocketAddress in the event thread
  private String address;
  private int port;
 
  private Selector selector;
  private SocketChannel channel;

  private final AtomicBoolean connected = new AtomicBoolean(false);

  private AtomicLong bytesOut = new AtomicLong(0L);
  private AtomicLong bytesIn = new AtomicLong(0L);

  public TcpClient() {
   
  }

//  @PostConstruct
  public void init() {
    assert address != null: "server address missing";
  }

  public void start() {
    LOG.info("starting event loop");
    thread.start();
  }

  public void join() throws InterruptedException {
    if (Thread.currentThread().getId() != thread.getId()) thread.join();
  }

  public void stop() {
    LOG.info("stopping event loop");
    thread.interrupt();
    selector.wakeup();
  }

  public boolean isConnected() {
    return connected.get();
  }

  /**
   * @param buffer data to send, the buffer should be flipped (ready for read)
   * @throws InterruptedException
   * @throws IOException
   */
  public void send(ByteBuffer buffer) throws InterruptedException, IOException {
    if (!connected.get()) throw new IOException("not connected");
    synchronized (writeBuf) {
      // try direct write of what's in the buffer to free up space
      if (writeBuf.remaining() < buffer.remaining()) {
        writeBuf.flip();
        int bytesOp = 0, bytesTotal = 0;
        while (writeBuf.hasRemaining() && (bytesOp = channel.write(writeBuf)) > 0) bytesTotal += bytesOp;
        writeBuf.compact();
      }

      // if didn't help, wait till some space appears
      if (Thread.currentThread().getId() != thread.getId()) {
        while (writeBuf.remaining() < buffer.remaining()) writeBuf.wait();
      }
      else {
        if (writeBuf.remaining() < buffer.remaining()) throw new IOException("send buffer full"); // TODO: add reallocation or buffers chain
      }
      writeBuf.put(buffer);

      // try direct write to decrease the latency
      writeBuf.flip();
      int bytesOp = 0, bytesTotal = 0;
      while (writeBuf.hasRemaining() && (bytesOp = channel.write(writeBuf)) > 0) bytesTotal += bytesOp;
      writeBuf.compact();

      if (writeBuf.hasRemaining()) {
        SelectionKey key = channel.keyFor(selector);
        key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
        selector.wakeup();
      }
    }
  }

  /**
   * Override with something meaningful
   * @param buf
   */
  protected abstract void onRead(ByteBuffer buf) throws Exception;
 
  /**
   * Override with something meaningful
   * @param buf
   */
  protected abstract void onConnected() throws Exception;

  /**
   * Override with something meaningful
   * @param buf
   */
  protected abstract void onDisconnected();

  private void configureChannel(SocketChannel channel) throws IOException {
    channel.configureBlocking(false);
    channel.socket().setSendBufferSize(0x100000); // 1Mb
    channel.socket().setReceiveBufferSize(0x100000); // 1Mb
    channel.socket().setKeepAlive(true);
    channel.socket().setReuseAddress(true);
    channel.socket().setSoLinger(false, 0);
    channel.socket().setSoTimeout(0);
    channel.socket().setTcpNoDelay(true);
  }

  //@Override
  public void run() {
    LOG.info("event loop running");
    try {
      while(! Thread.interrupted()) { // reconnection loop
        try {
          selector = Selector.open();
          channel = SocketChannel.open();
          configureChannel(channel);

          channel.connect(new InetSocketAddress(address,port));
          channel.register(selector, SelectionKey.OP_CONNECT);

          while(!thread.isInterrupted() && channel.isOpen()) { // events multiplexing loop
            if (selector.select() > 0) processSelectedKeys(selector.selectedKeys());
          }
        } catch (IOException e) {
          LOG.log(Level.INFO,"io exception", e); // ConnectException extends SocketException extends IOException
        } catch (UnresolvedAddressException e) {
          LOG.log(Level.INFO,"exception", e);
        } catch (CancelledKeyException e) {
          LOG.log(Level.INFO,"CancelledKeyException", e); // happens when i restart the server
        } catch (Exception e) {
          LOG.log(Level.WARNING,"exception", e);
        } finally {
          connected.set(false);
          onDisconnected();
          writeBuf.clear();
          readBuf.clear();
          if (channel != null) channel.close();
          if (selector != null) selector.close();
          LOG.info("connection closed");
        }

        try {
          Thread.sleep(reconnectInterval);
          if (reconnectInterval < MAXIMUM_RECONNECT_INTERVAL) reconnectInterval *= 2;
          LOG.info("reconnecting to " + address+":"+port);
        } catch (InterruptedException e) {
          break;
        }
      }
    } catch (Exception e) {
      LOG.log(Level.WARNING,"unrecoverable error", e);
    }
  
    LOG.info("event loop terminated");
  }

  private void processSelectedKeys(Set keys) throws Exception {
    Iterator itr = keys.iterator();
    while (itr.hasNext()) {
      SelectionKey key = (SelectionKey)itr.next();
      if (key.isReadable()) processRead(key);
      if (key.isWritable()) processWrite(key);
      if (key.isConnectable()) processConnect(key);
      if (key.isAcceptable()) ;
      itr.remove();
    }
  }

  private void processConnect(SelectionKey key) throws Exception {
    SocketChannel ch = (SocketChannel) key.channel();
    if (ch.finishConnect()) {
      LOG.info("connected to " + address+":"+port);
      key.interestOps(key.interestOps() ^ SelectionKey.OP_CONNECT);
      key.interestOps(key.interestOps() | SelectionKey.OP_READ);
      reconnectInterval = INITIAL_RECONNECT_INTERVAL;
      connected.set(true);
      onConnected();
    }
  }

  private void processRead(SelectionKey key) throws Exception {
    ReadableByteChannel ch = (ReadableByteChannel)key.channel();

    int bytesOp = 0, bytesTotal = 0;
    while (readBuf.hasRemaining() && (bytesOp = ch.read(readBuf)) > 0) bytesTotal += bytesOp;

    if (bytesTotal > 0) {
      readBuf.flip();
      onRead(readBuf);
      readBuf.compact();
    }
    else if (bytesOp == -1) {
      LOG.info("peer closed read channel");
      ch.close();
    }

    bytesIn.addAndGet(bytesTotal);
  }

  private void processWrite(SelectionKey key) throws IOException {
    WritableByteChannel ch = (WritableByteChannel)key.channel();
    synchronized (writeBuf) {
      writeBuf.flip();

      int bytesOp = 0, bytesTotal = 0;
      while (writeBuf.hasRemaining() && (bytesOp = ch.write(writeBuf)) > 0) bytesTotal += bytesOp;

      bytesOut.addAndGet(bytesTotal);

      if (writeBuf.remaining() == 0) {
        key.interestOps(key.interestOps() ^ SelectionKey.OP_WRITE);
      }

      if (bytesTotal > 0) writeBuf.notify();
      else if (bytesOp == -1) {
        LOG.info("peer closed write channel");
        ch.close();
      }

      writeBuf.compact();
    }
  }

  //public SocketAddress getAddress() {
  //  return address;
  //}

  public void setAddress(String address,int port) {
    this.address = address;
    this.port = port;
  }

  public long getBytesOut() {
    return bytesOut.get();
  }

  public long getBytesIn() {
    return bytesIn.get();
  }


  /**
   * can be used for testing
   */
  public static void main(String[] args) throws Exception {
    //BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d{yyyyMMdd-HH:mm:ss} %-10t %-5p %-20C{1} - %m%n")));
    //Logger.getRootLogger().setLevel(Level.INFO);
    final TcpClient client = new TcpClient() {
      protected void onRead(ByteBuffer buf) throws Exception { buf.position(buf.limit()); }
      protected void onDisconnected() { }
      protected void onConnected() throws Exception { }
    };

    client.setAddress("127.0.0.1", 20001);
    client.start();

    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
      public void run() {
        LOG.info("out bytes: " + client.bytesOut.get());
        LOG.info("in bytes:  " + client.bytesIn.get());
      }
    }, 5000, 5000);

    while(!client.isConnected()) Thread.sleep(500);

    LOG.info("starting server flood");
    ByteBuffer buf = ByteBuffer.allocate(65535);
    Random rnd = new Random();
    while (true) {
      short len = (short) rnd.nextInt(Short.MAX_VALUE - 2);
      byte[] bytes = new byte[len];
      rnd.nextBytes(bytes);
      buf.putShort((short)len);
      buf.put(bytes);
      buf.flip();
      try {
        client.send(buf);
      } catch (Exception e) {
        LOG.log(Level.WARNING,"exception: " + e.getMessage());
        while (!client.isConnected()) Thread.sleep(1000);
      }
      buf.clear();
      Thread.sleep(10);
    }
  }

    public int getPort() {
        return port;
    }
}
TOP

Related Classes of net.yura.lobby.client.TcpClient

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.