org.beepcore.beep.transport.tcp.TCPSession.java Source code

Java tutorial

Introduction

Here is the source code for org.beepcore.beep.transport.tcp.TCPSession.java

Source

/*
 * TCPSession.java  $Revision: 1.36 $ $Date: 2003/11/18 14:03:10 $
 *
 * Copyright (c) 2001 Invisible Worlds, Inc.  All rights reserved.
 * Copyright (c) 2001-2003 Huston Franklin.  All rights reserved.
 *
 * The contents of this file are subject to the Blocks Public License (the
 * "License"); You may not use this file except in compliance with the License.
 *
 * You may obtain a copy of the License at http://www.beepcore.org/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied.  See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * Source code in 3rd-party is licensed and owned by their respective
 * copyright holders.
 *
 * All other source code is copyright Tresys Technology and licensed as below.
 *
 * Copyright (c) 2014 Tresys Technology LLC, Columbia, Maryland, USA
 *
 * This software was developed by Tresys Technology LLC
 * with U.S. Government sponsorship.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.beepcore.beep.transport.tcp;

import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;

import java.net.Socket;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.beepcore.beep.core.BEEPException;
import org.beepcore.beep.core.Channel;
import org.beepcore.beep.core.Frame;
import org.beepcore.beep.core.ProfileRegistry;
import org.beepcore.beep.core.Session;
import org.beepcore.beep.core.SessionImpl;
import org.beepcore.beep.core.SessionCredential;
import org.beepcore.beep.core.SessionTuningProperties;
import org.beepcore.beep.util.BufferSegment;
import org.beepcore.beep.util.HeaderParser;
import org.beepcore.beep.util.StringUtil;

/**
 * Provides the TCP transport mapping for BEEP according to RFC 3081.
 *
 * @author Eric Dixon
 * @author Huston Franklin
 * @author Jay Kint
 * @author Scott Pead
 * @version $Revision: 1.36 $, $Date: 2003/11/18 14:03:10 $
 */
public class TCPSession extends SessionImpl {

    // Constants
    private static final char[] MESSAGE_TYPE_SEQ = new char[] { 'S', 'E', 'Q' };
    private static final int MAX_RECEIVE_BUFFER_SIZE = 64 * 1024;
    private static final String TCP_MAPPING = "TCP Mapping";
    private static final String CRLF = "\r\n";
    private static final int MIN_SEQ_HEADER_SIZE = (3 // msg type
            + 1 // space
            + 1 // channel number
            + 1 // space
            + 1 // acknum
            + 1 // space
            + 1 // window
            + CRLF.length());

    private static final int CHANNEL_START_ODD = 1;
    private static final int CHANNEL_START_EVEN = 2;

    // Instance Data
    private Log log = LogFactory.getLog(this.getClass());

    private byte headerBuffer[] = new byte[Frame.MAX_HEADER_SIZE];
    private byte[] outputBuf = new byte[0];
    private Object writerLock;
    private Socket socket;
    private boolean running;
    private static int THREAD_COUNT = 0;
    private static final String THREAD_NAME = "TCPSession Thread #";
    private Thread thread;

    /**
     * @param sock the Socket for this TCPConnection
     *
     * @registry the ProfileRegistry (set of profiles) to be used on
     * this Session.
     *
     * @firstChannel the integer indicating the
     * ordinality for this Peer, which determines whether Channels
     * started by this peer have odd or even numbers.
     *
     * @param registry
     * @param localCred
     * @param peerCred
     *
     * The hack-ish part of this is the credential parameter.  Here's
     * the deal.  (1) TCPSessionCreator has public methods that don't
     * expose the credential (2) Therefore if it's called by the User,
     * it can only have a null credential.  (3) If it's called via a
     * Tuning Profile reset though, it can have a real value.  (4)
     * When that's the case, we call a 'different' init method that
     * doesn't block the thread of the former (soon to die) session.
     *
     * @param firstChannel
     *
     * @throws BEEPException
     */
    private TCPSession(Socket sock, ProfileRegistry registry, int firstChannel, SessionCredential localCred,
            SessionCredential peerCred, SessionTuningProperties tuning, String servername) throws BEEPException {
        super(registry, firstChannel, localCred, peerCred, tuning, servername);

        socket = sock;
        writerLock = new Object();

        if ((peerCred != null) || (localCred != null) || (tuning != null)) {
            tuningInit();
        } else {
            init();
        }

        try {
            socket.setReceiveBufferSize(MAX_RECEIVE_BUFFER_SIZE);
        } catch (Exception x) {
            log.debug("Socket doesn't support setting receive buffer size");
        }
    }

    /**
     * Creates a TCPSession for a Socket that was created by
     * initiating a connection.
     *
     *
     * @param sock
     * @param registry
     * @param servername
     *
     * @throws BEEPException
     *
     */
    public static TCPSession createInitiator(Socket sock, ProfileRegistry registry, String servername)
            throws BEEPException {
        return new TCPSession(sock, (ProfileRegistry) registry.clone(), CHANNEL_START_ODD, null, null, null,
                servername);
    }

    /**
     * Creates a TCPSession for a Socket that was created by
     * initiating a connection.
     *
     *
     * @param sock
     * @param registry
     *
     * @throws BEEPException
     *
     */
    public static TCPSession createInitiator(Socket sock, ProfileRegistry registry) throws BEEPException {
        return createInitiator(sock, registry, null);
    }

    /**
     * Creates a TCPSession for a Socket that was created by
     * listening and accepting a connection.
     *
     *
     * @param sock
     * @param registry
     *
     * @throws BEEPException
     *
     */
    public static TCPSession createListener(Socket sock, ProfileRegistry registry) throws BEEPException {
        return new TCPSession(sock, (ProfileRegistry) registry.clone(), CHANNEL_START_EVEN, null, null, null, null);
    }

    // Overrides method in Session
    public synchronized void close() throws BEEPException {
        super.close();

        if (socket != null) {
            try {
                socket.close();
            } catch (IOException e) {
            }

            socket = null;
        }
    }

    public Socket getSocket() {
        return this.socket;
    }

    // Overrides method in Session
    public void terminate(String reason) {
        super.terminate(reason);

        if (socket != null) {
            try {
                socket.close();
            } catch (IOException e) {
            }

            socket = null;
            running = false;
        }
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    public String toString() {
        return super.toString() + " (" + socket.getLocalAddress() + ":" + socket.getLocalPort() + "-"
                + socket.getInetAddress() + ":" + socket.getPort() + ")";
    }

    // Implementation of method declared in Session
    protected void disableIO() {
        running = false;
    }

    // Implementation of method declared in Session
    protected void enableIO() {
        running = false;
        thread = null;

        if (thread == null) {
            String threadName;

            synchronized (THREAD_NAME) {
                threadName = new String(THREAD_NAME + THREAD_COUNT++);
            }

            thread = new Thread(threadName) {
                public void run() {
                    processNextFrame();
                }
            };

            thread.setDaemon(true);
            thread.start();
        }
    }

    // Implementation of method declared in Session
    protected int getMaxFrameSize() {
        /**
         * @todo - test this and find an optimal frame size, key it up
         * to approximate ethernet packet size, less header, trailer
         */
        return 1400;
    }

    /**
     * Generates a header, then writes the header, payload, and
     * trailer to the wire.
     *
     * @param f the Frame to send.
     * @return boolean true of the frame was sent, false otherwise.
     *
     * @throws BEEPException
     *
     * @todo make this one write operation later.
     */
    protected void sendFrame(Frame f) throws BEEPException {
        try {

            OutputStream os = socket.getOutputStream();

            synchronized (writerLock) {
                /* Inspite of the extra data copy if is faster to have
                 * a single call to write() (at least with the JVMs we
                 * have tested with).
                 */
                BufferSegment[] bs = f.getBytes();

                int n = 0;
                for (int i = 0; i < bs.length; ++i) {
                    n += bs[i].getLength();
                }

                if (n > outputBuf.length) {
                    outputBuf = new byte[n];
                }

                int off = 0;

                for (int i = 0; i < bs.length; ++i) {
                    System.arraycopy(bs[i].getData(), bs[i].getOffset(), outputBuf, off, bs[i].getLength());

                    off += bs[i].getLength();
                }

                os.write(outputBuf, 0, n);
                os.flush();

                if (log.isTraceEnabled()) {
                    log.trace("Wrote the following\n" + new String(outputBuf, 0, n));
                }
            }
        } catch (IOException e) {
            throw new BEEPException(e);
        } catch (Exception e) {
            throw new BEEPException(e);
        }
    }

    // Implementation of method declared in Session
    protected Session reset(SessionCredential localCred, SessionCredential peerCred, SessionTuningProperties tuning,
            ProfileRegistry reg, Object argument) throws BEEPException {
        if (log.isTraceEnabled()) {
            log.trace("Reset as " + (isInitiator() ? "INITIATOR" : "LISTENER"));
        }

        Socket s = null;

        try {
            s = (Socket) argument;
        } catch (ClassCastException x) {
            s = socket;
        }

        if (reg == null) {
            reg = this.getProfileRegistry();
        }

        Session newSession = new TCPSession(s, reg, (isInitiator() ? CHANNEL_START_ODD : CHANNEL_START_EVEN),
                localCred, peerCred, tuning, null);

        this.fireSessionReset(newSession);

        return newSession;
    }

    /**
     * Update the channel window size with the remote peer by sending
     * SEQ frames as per RFC 3081.
     *
     *
     * @return true if the Receive Buffer Size was updated
     *
     * @throws BEEPException if a specified buffer size is larger
     *    than what's available on the Socket.
     *
     */
    protected boolean updateMyReceiveBufferSize(Channel channel, long currentSeq, int currentAvail)
            throws BEEPException {
        StringBuffer sb = new StringBuffer(Frame.MAX_HEADER_SIZE);

        sb.append(MESSAGE_TYPE_SEQ);
        sb.append(' ');
        sb.append(this.getChannelNumberAsString(channel));
        sb.append(' ');
        sb.append(Long.toString(currentSeq));
        sb.append(' ');
        sb.append(Integer.toString(currentAvail));
        sb.append(CRLF);

        try {
            if (log.isDebugEnabled()) {
                log.debug("Wrote: " + sb.toString());
            }

            OutputStream os = socket.getOutputStream();

            synchronized (writerLock) {
                os.write(StringUtil.stringBufferToAscii(sb));
                os.flush();
            }
        } catch (IOException x) {
            throw new BEEPException("Unable to send SEQ", x);
        }

        return true;
    }

    private void processNextFrame() {
        running = true;

        if (socket == null) {
            running = false;
            return;
        }

        try {
            InputStream is = socket.getInputStream();

            while (running) {
                if (log.isTraceEnabled()) {
                    log.trace("Processing next frame");
                }

                int amountRead;
                headerBuffer = new byte[Frame.MAX_HEADER_SIZE];

                try {
                    do {
                        amountRead = is.read(headerBuffer, 0, MIN_SEQ_HEADER_SIZE);

                        if (amountRead == -1) {
                            throw new SessionAbortedException();
                        }
                    } while (amountRead == 0);

                } catch (java.net.SocketException e) {
                    if (running) {
                        throw e;
                    }

                    // socket closed intentionally (session closing)
                    // so just return
                    return;
                }

                if (headerBuffer[0] == (byte) MESSAGE_TYPE_SEQ[0]) {
                    processSEQFrame(headerBuffer, amountRead, is);
                    continue;
                } else {
                    if (processCoreFrame(headerBuffer, amountRead, is) == false) {
                        break;
                    }
                }
            }
        } catch (IOException e) {
            log.error(e);

            socket = null;

            terminate(e.getMessage());
        } catch (SessionAbortedException e) {
            terminate("Session aborted by remote peer.");
        } catch (Throwable e) {
            log.error(e);
            terminate(e.getMessage());
        }

        if (log.isDebugEnabled()) {
            log.debug("Session listener thread exiting.  State = " + TCPSession.this.getState());
        }
    }

    private boolean processCoreFrame(byte[] headerBuffer, int amountRead, InputStream is)
            throws SessionAbortedException, BEEPException, IOException {
        int headerLength = 0;
        int amountToRead = Frame.MIN_FRAME_SIZE - amountRead;

        headerFound: while (true) {
            int tokenCount = 6;

            try {
                int n = is.read(headerBuffer, amountRead, amountToRead);

                if (n == -1) {
                    throw new SessionAbortedException();
                }

                if (n == 0) {
                    continue;
                }

                amountRead += n;

            } catch (java.net.SocketException e) {
                if (running) {
                    throw e;
                }

                // socket closed intentionally (session closing)
                // so just return
                return false;
            }

            while (headerLength < amountRead) {
                if (headerBuffer[headerLength] == '\n') {
                    if (headerLength == 0 || headerBuffer[headerLength - 1] != '\r') {
                        throw new BEEPException("Malformed BEEP header");
                    }

                    ++headerLength;
                    break headerFound;
                }

                if (headerBuffer[headerLength] == ' ') {
                    if (tokenCount > 1) { // This is for ANS frames
                        --tokenCount;
                    }
                }

                ++headerLength;
            }

            if (headerLength > Frame.MAX_HEADER_SIZE) {
                throw new BEEPException("Malformed BEEP header, no CRLF");
            }

            /* 2 = 1 for the min token size and 1 is for the separator ' '
             * or "\r\n"
             * amountToRead = (tokenCount * 2) + Frame.TRAILER.length();
             */
            /* TLH: changed amountToRead for header to be 1 byte at a time
             * Temporary fix. Found issues when using tokenCount * 2,
             * where 1st byte of ANS was accidentally read from NUL header
             * causing Malformed BEEP Header exception
             */
            amountToRead = 1;
        }

        if (log.isTraceEnabled()) {
            log.trace(new String(headerBuffer, 0, headerLength));
        }

        Frame f = super.createFrame(headerBuffer, headerLength - CRLF.length());
        byte[] payload = new byte[f.getSize()];

        int count = amountRead - headerLength;
        if (count > payload.length) {
            System.arraycopy(headerBuffer, headerLength, payload, 0, payload.length);
            count -= payload.length;

            for (int i = 0; i < Frame.TRAILER.length(); ++i) {
                int b;
                if (count > 0) {
                    b = headerBuffer[headerLength + payload.length + i];
                    --count;
                } else {
                    b = is.read();

                    if (b == -1) {
                        throw new SessionAbortedException();
                    }
                }

                if (((byte) b) != ((byte) Frame.TRAILER.charAt(i))) {
                    throw new BEEPException("Malformed BEEP frame, " + "invalid trailer");
                }
            }
        } else {
            System.arraycopy(headerBuffer, headerLength, payload, 0, count);

            while (count < payload.length) {
                int n = is.read(payload, count, payload.length - count);
                if (n == -1) {
                    throw new SessionAbortedException();
                }
                count += n;
            }

            if (log.isTraceEnabled()) {
                log.trace(new String(payload));
            }

            for (int i = 0; i < Frame.TRAILER.length(); ++i) {
                int b = is.read();

                if (b == -1) {
                    throw new SessionAbortedException();
                }

                if (((byte) b) != ((byte) Frame.TRAILER.charAt(i))) {
                    throw new BEEPException("Malformed BEEP frame, " + "invalid trailer");
                }
            }
        }

        f.addPayload(new BufferSegment(payload));

        return super.postFrame(f);
    }

    private void processSEQFrame(byte[] headerBuffer, int amountRead, InputStream is)
            throws BEEPException, IOException, SessionAbortedException {
        int headerLength = 0;
        int tokenCount = 4;

        headerFound: while (true) {

            while (headerLength < amountRead) {
                if (headerBuffer[headerLength] == '\n') {
                    if (headerLength == 0 || headerBuffer[headerLength - 1] != '\r') {
                        throw new BEEPException("Malformed BEEP header");
                    }

                    ++headerLength;
                    break headerFound;
                }

                if (headerBuffer[headerLength] == ' ') {
                    if (tokenCount > 1) {
                        --tokenCount;
                    }
                }

                ++headerLength;
            }

            if (headerLength > Frame.MAX_HEADER_SIZE) {
                throw new BEEPException("Malformed BEEP header, no CRLF");
            }

            int amountToRead = headerBuffer[headerLength - 1] == '\r' ? 1 : tokenCount * 2;
            try {
                /* 2 = 1 for the min token size and 1 is for the separator ' '
                 * or "\r\n"
                 */
                int n = is.read(headerBuffer, amountRead, amountToRead);

                if (n == -1) {
                    throw new SessionAbortedException();
                }

                if (n == 0) {
                    continue;
                }

                amountRead += n;

            } catch (java.net.SocketException e) {
                if (running) {
                    throw e;
                }

                // socket closed intentionally (session closing)
                // so just return
                return;
            }
        }

        // Process the header
        HeaderParser header = new HeaderParser(headerBuffer, headerLength - CRLF.length());

        char[] type = header.parseType();
        if (java.util.Arrays.equals(type, MESSAGE_TYPE_SEQ) == false) {
            throw new BEEPException("Malformed BEEP header");
        }

        int channelNum = header.parseInt();
        long ackNum = header.parseUnsignedInt();
        int window = header.parseInt();

        if (header.hasMoreTokens()) {
            throw new BEEPException("Malformed BEEP Header");
        }
        log.debug("Read: SEQ " + channelNum + " " + ackNum + " " + window);

        // update the channel with the new receive window size
        this.updatePeerReceiveBufferSize(channelNum, ackNum, window);
    }

    private static class SessionAbortedException extends Exception {
    }
}