/* Copyright (c) 2006, Sriram Srinivasan * * You may distribute this software under the terms of the license * specified in the file "License" */ package kilim.nio; import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.nio.channels.spi.AbstractSelectableChannel; import kilim.Mailbox; import kilim.Pausable; import kilim.Task; /** * The EndPoint represents an open socket connection. It is a wrapper over a non-blocking socket (channel) and belongs * to a {@link SessionTask}. It serves as the bridge between the SessionTask and the {@link NioSelectorScheduler}, using * a pair of mailboxes for exchanging socket registration and socket ready events. * <p> * The other purpose of this class is to provide convenience methods that read from a socket into a bytebuffer, or write * from a bytebuffer to the socket. If the socket is not ready for business, the endpoint (and hence the task) simply * yields, <i>without</i> registering with the {@link NioSelectorScheduler}. The idea is to give the other runnable * tasks a chance to run before retrying the operation (on resumption); this avoids waking up the selector -- an * expensive operation -- as much as possible, and introduces a delay between retries. If, after a fixed number of times * ({@link #YIELD_COUNT}), the operation still hasn't succeeded, the endpoint registers itself with the * {@link NioSelectorScheduler}, and waits for a socket-ready event from the selector. * * This scheme is adaptive to load, in that the delay between retries is proportional to the number of runnable tasks. * Busy sockets tend to get serviced more often as the socket is always ready. */ public class EndPoint extends Mailbox<SockEvent> { // Mailbox for receiving socket ready events. // TODO: This too must be made adaptive. static final int YIELD_COUNT = Integer.parseInt(System.getProperty("kilim.nio.yieldCount", "4")); /** * The socket channel wrapped by the EndPoint. See #dataChannel() */ public AbstractSelectableChannel sockch; /** * The NioSelectorScheduler's mailbox to which to send registration events. */ public Mailbox<SockEvent> sockEvMbx; public EndPoint() { super(2, 2); // Expecting only one event, but don't want the NioSelectorScheduler to // pause for lack of space (due to unforeseen bugs). } public EndPoint(Mailbox<SockEvent> mbx, AbstractSelectableChannel ch) { this.sockch = ch; this.sockEvMbx = mbx; } public SocketChannel dataChannel() { return (SocketChannel) sockch; } /** * Write buf.remaining() bytes to dataChannel(). */ public void write(ByteBuffer buf) throws IOException, Pausable { SocketChannel ch = dataChannel(); int remaining = buf.remaining(); if (remaining == 0) return; int n = ch.write(buf); remaining -= n; int yieldCount = 0; while (remaining > 0) { if (n == 0) { yieldCount++; if (yieldCount < YIELD_COUNT) { Task.yield(); // don't go back to selector yet. } else { pauseUntilWritable(); yieldCount = 0; } } n = ch.write(buf); remaining -= n; } } /** * Read <code>atleastN</code> bytes more into the buffer if there's space. Otherwise, allocate a bigger * buffer that'll accomodate the earlier contents and atleastN more bytes. * * @param buf * ByteBuffer to be filled * @param atleastN * At least this many bytes to be read. * @throws IOException */ public ByteBuffer fill(ByteBuffer buf, int atleastN) throws IOException, Pausable { if (buf.remaining() < atleastN) { ByteBuffer newbb = ByteBuffer.allocate(Math.max(buf.capacity() * 3 / 2, buf.position() + atleastN)); buf.rewind(); newbb.put(buf); buf = newbb; } SocketChannel ch = dataChannel(); if (!ch.isOpen()) { throw new EOFException(); } int yieldCount = 0; do { int n = ch.read(buf); // System.out.println(buf); if (n == -1) { close(); throw new EOFException(); } if (n == 0) { yieldCount++; if (yieldCount < YIELD_COUNT) { // Avoid registering with the selector because it requires waking up the selector, context switching // between threads and calling the OS just to register. Just yield, let other tasks have a go, then // check later. Do this at most YIELD_COUNT times before going back to the selector. Task.yield(); } else { pauseUntilReadble(); yieldCount = 0; } } atleastN -= n; } while (atleastN > 0); return buf; } /** * Reads a length-prefixed message in its entirety. * * @param bb The bytebuffer to fill, assuming there is sufficient space (including the bytes for the length). Otherwise, the * contents are copied into a sufficiently big buffer, and the new buffer is returned. * * @param lengthLength The number of bytes occupied by the length. Must be 1, 2 or 4, assumed to be in big-endian order. * @param lengthIncludesItself true if the packet length includes lengthLength * @return the buffer bb passed in if the message fits or a new buffer. Either way, the buffer returned has the entire * message including the initial length prefix. * @throws IOException * @throws Pausable */ public ByteBuffer fillMessage(ByteBuffer bb, int lengthLength, boolean lengthIncludesItself) throws IOException, Pausable { int pos = bb.position(); int opos = pos; // save orig pos bb = fill(bb, lengthLength); byte a, b, c, d; a = b = c = d = 0; switch (lengthLength) { case 4: a = bb.get(pos); pos++; b = bb.get(pos); pos++; // fall through case 2: c = bb.get(pos); pos++; // fall through case 1: d = bb.get(pos); break; default: throw new IllegalArgumentException("Incorrect lengthLength (may only be 1, 2 or 4): " + lengthLength); } int contentLen = ((a << 24) + (b << 16) + (c << 8) + (d << 0)); // TODO: put a limit on len if (lengthIncludesItself) { contentLen -= lengthLength; } // If the fill() above hasn't read in all the content, read the remaining int remaining = contentLen - (bb.position() - opos - lengthLength); if (remaining > 0) { bb = fill(bb, remaining); } return bb; } public void pauseUntilReadble() throws Pausable, IOException { SockEvent ev = new SockEvent(this, sockch, SelectionKey.OP_READ); sockEvMbx.putnb(ev); // TODO. Need to introduce session timeouts super.get(); // wait on self } public void pauseUntilWritable() throws Pausable, IOException { SockEvent ev = new SockEvent(this, sockch, SelectionKey.OP_WRITE); sockEvMbx.putnb(ev); // TODO. Need to introduce session timeouts super.get(); // wait on self } public void pauseUntilAcceptable() throws Pausable, IOException { SockEvent ev = new SockEvent(this, sockch, SelectionKey.OP_ACCEPT); sockEvMbx.putnb(ev); super.get(); // wait on self } /** * Write a file to the endpoint using {@link FileChannel#transferTo} * * @param fc FileChannel to copy to endpoint * @param start Start offset * @param length Number of bytes to be written * @throws IOException * @throws Pausable */ public void write(FileChannel fc, long start, long length) throws IOException, Pausable { SocketChannel ch = dataChannel(); long remaining = length - start; if (remaining == 0) return; long n = fc.transferTo(start, remaining, ch); start += n; remaining -= n; int yieldCount = 0; while (remaining > 0) { if (n == 0) { yieldCount++; if (yieldCount < YIELD_COUNT) { Task.yield(); // don't go back to selector yet. } else { pauseUntilWritable(); yieldCount = 0; } } n = fc.transferTo(start, remaining, ch); start += n; remaining -= n; } } /** * Close the endpoint */ public void close() { try { // if (sk != null && sk.isValid()) { // sk.attach(null); // sk.cancel(); // sk = null; // } sockch.close(); } catch (Exception ignore) { ignore.printStackTrace(); } } }