/*
* Minha.pt: middleware testing platform.
* Copyright (c) 2011-2014, Universidade do Minho.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package pt.minha.models.global.net;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.PriorityQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pt.minha.kernel.simulation.Event;
import pt.minha.kernel.simulation.Timeline;
import pt.minha.models.global.io.BlockingHelper;
import pt.minha.models.global.io.Buffer;
/**
* Simulated TCP connection.
*
* @author jop
*/
public class ClientTCPSocket extends AbstractSocket {
public static final int MTU = 1500;
public static final int PIPE_BUF = 512;
public static final int WINDOW_SIZE = 64*1024;
private PriorityQueue<TCPPacket> incoming = new PriorityQueue<TCPPacket>();
private ClientTCPSocket peer;
private int seqOut = 0, ackedOut = -1, seqIn = 0, ackedIn = -1;
private Buffer out = new Buffer(), in = new Buffer();
private boolean synSent;
private boolean finSent;
public BlockingHelper readers, writers, connectors;
/**
* Create a TCP connection at connecting side.
*/
public ClientTCPSocket(NetworkStack stack) {
super(stack);
init();
stack.addTCPConnection(this);
}
public void bind(InetSocketAddress addr) throws SocketException {
// FIXME: Bind address is ignored
if (addr != null && addr.getPort()!=0)
super.bind(stack.getTCPBindAddress(addr.getPort()));
else
super.bind(stack.getTCPBindAddress(0));
}
/**
* Initiate a connection.
*
* @param addr server address
*/
public void connect(InetSocketAddress addr) throws SocketException {
if (getLocalAddress()==null)
bind(null);
synSent = true;
sendPacket(new TCPPacket(this, null, seqOut, seqIn, new byte[0], 0), addr);
}
/**
* Create a TCP connection at accepting side.
*
* @param syn initial packet received
*/
public ClientTCPSocket(NetworkStack stack, InetSocketAddress local, TCPPacket syn) {
super(stack, local);
init();
stack.addTCPConnection(this);
synSent = true;
receivePacket(syn);
}
private void init() {
readers = new BlockingHelper() {
public boolean isReady() {
return in.getUsed()>0 || in.isClosed();
}
};
writers = new BlockingHelper() {
public boolean isReady() {
return out.getFree()>=PIPE_BUF;
}
};
connectors = new BlockingHelper() {
public boolean isReady() {
return ackedIn>=0;
}
};
}
/**
* Handle all packets received (except the initial SYN at the server side)
*
* @param p packet
*/
protected void receivePacket(TCPPacket p) {
// Is the remote peer reseting the connection?
if ((p.getControl()&TCPPacket.RST)!=0) {
incoming.clear();
finSent = true;
out.close(false);
in.close(true);
readers.wakeup();
writers.wakeup();
connectors.wakeup();
return;
}
// Oportunistically handle acknowledgment, even if out of order
if (p.getAcknowledgement() > ackedOut) {
ackedOut = p.getAcknowledgement();
writers.wakeup();
// Established?
if (peer==null && ackedOut >= 0) {
peer = p.getSource();
connectors.wakeup();
}
}
// Handle packet processing in sequence
incoming.add(p);
handlePacket();
}
/**
* Send all packets received (except the initial SYN at the client side)
*
* @param p packet
*/
protected void sendPacket(TCPPacket p) {
stack.relayTCPData(p);
}
/**
* Send the initial SYN packet at the client side
*
* @param p packet
*/
protected void sendPacket(TCPPacket p, InetSocketAddress serverAddress) {
stack.relayTCPConnect(serverAddress, p);
}
public Event scheduleRead(final TCPPacket p) {
return new Event(stack.getTimeline()) {
public void run() {
receivePacket(p);
}
};
}
private void handlePacket() {
// Wait until next in sequence is available
if (incoming.isEmpty() || incoming.peek().getSequence() != seqIn || incoming.peek().getSize() > in.getFree())
return;
TCPPacket p = incoming.poll();
// Should I reset the connection?
if (p.getSize()>0 && finSent && in.isClosed()) {
sendPacket(new TCPPacket(this, peer, seqOut, seqIn, new byte[0], TCPPacket.RST));
return;
}
// Handle data
in.push(p.getData(), 0, p.getSize());
seqIn += p.getSize();
// Handle connection closing
if ((p.getControl()&TCPPacket.FIN)!=0)
in.close(true);
if (readers.isReady())
readers.wakeup();
uncork();
}
public void uncork() {
// Prepare data
int op = out.getUsed();
if (op > MTU)
op = MTU;
// Flow control
if (op >= WINDOW_SIZE-(seqOut-ackedOut))
op = WINDOW_SIZE-(seqOut-ackedOut);
byte[] data = new byte[op];
out.pop(data, 0, op);
// Prepare connection close
int flags = 0;
if (out.isClosed() && op==0 && !finSent) {
flags = TCPPacket.FIN;
finSent = true;
}
writers.wakeup();
// Is there anything new to send?
if (seqIn == ackedIn && op == 0 && flags == 0)
return;
// Send the packet and record sequence numbers
if (peer != null)
sendPacket(new TCPPacket(this, peer, seqOut, seqIn, data, flags));
seqOut += op;
ackedIn = seqIn;
}
/**
* Get remote socket address after connected.
*
* @return
*/
public InetSocketAddress getRemoteAddress() {
return peer==null?null:peer.getLocalAddress();
}
/**
* Send data.
*
* @param b data array
* @param off offset in the array
* @param len number of bytes to send
* @return number of bytes written
* @throws SocketException in case of broken pipe condition
*/
public int write(byte b[], int off, int len) throws SocketException {
if (out.isClosed())
throw new SocketException("broken pipe");
if (len <= PIPE_BUF && out.getFree() <= len)
return 0;
int op = out.push(b, off, len);
return op;
}
/**
* Send data.
*
* @param b data array
* @return number of bytes written
* @throws SocketException in case of broken pipe condition
*/
public int write(ByteBuffer b) throws SocketException {
if (out.isClosed())
throw new SocketException("broken pipe");
if (b.remaining() <= PIPE_BUF && out.getFree() <= b.remaining())
return 0;
int op = out.push(b);
return op;
}
/**
* Receive data.
*
* @param b data array
* @param off offset into array
* @param len maximum amout to read
* @return number of bytes read, -1 for EOF
*/
public int read(byte b[], int off, int len) {
int op = in.pop(b, off, len);
handlePacket();
return op;
}
/**
* Receive data.
*
* @param b data array
* @param off offset into array
* @param len maximum amout to read
* @return number of bytes read, -1 for EOF
*/
public int read(ByteBuffer b) {
int op = in.pop(b);
handlePacket();
return op;
}
/**
* Close outgoing stream.
*/
public void shutdownOutput() {
if (out.isClosing())
return;
out.close(true);
uncork();
if (in.isClosing())
stack.removeTCPConn(this);
}
/**
* Close incoming stream.
*/
public void shutdownInput() {
if (in.isClosing())
return;
in.close(false);
if (out.isClosing())
stack.removeTCPConn(this);
}
public boolean isConnecting() {
return synSent;
}
public String toString() {
return "["+getLocalAddress().getAddress().getHostAddress()+"-"+getRemoteAddress().getAddress().getHostAddress()+"] "+
"s("+seqOut+" "+ackedOut+" "+out.isClosing()+" "+finSent+") "+
"r("+seqIn+" "+ackedIn+" "+in.isClosing()+") ";
}
private static Logger logger = LoggerFactory.getLogger("pt.minha.TCP");
public void readAt(long l) {
logger.info("[ {} s ] {}:{}-{}:{} recv to {} bytes", Timeline.toSeconds(l), getLocalAddress().getAddress().getHostAddress(), getLocalAddress().getPort(), getRemoteAddress().getAddress().getHostAddress(), getRemoteAddress().getPort(), seqIn-in.getUsed());
}
public void writeAt(long l) {
logger.info("[ {} s ] {}:{}-{}:{} sent to {} bytes", Timeline.toSeconds(l), getLocalAddress().getAddress().getHostAddress(), getLocalAddress().getPort(), getRemoteAddress().getAddress().getHostAddress(), getRemoteAddress().getPort(), seqOut+out.getUsed());
}
}