blob: 0a999b8a59665c9c5c4eb5c723ceb86f3e3d387f [file] [log] [blame]
/*
* Copyright (c) 2002, 2004, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.net.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import sun.net.www.MessageHeader;
import java.util.*;
/**
* This class implements a simple HTTP server. It uses multiple threads to
* handle connections in parallel, and also multiple connections/requests
* can be handled per thread.
* <p>
* It must be instantiated with a {@link HttpCallback} object to which
* requests are given and must be handled.
* <p>
* Simple synchronization between the client(s) and server can be done
* using the {@link #waitForCondition(String)}, {@link #setCondition(String)} and
* {@link #rendezvous(String,int)} methods.
*
* NOTE NOTE NOTE NOTE NOTE NOTE NOTE
*
* If changes are made here, please sure they are propagated to
* the HTTPS equivalent in the JSSE regression test suite.
*
* NOTE NOTE NOTE NOTE NOTE NOTE NOTE
*/
public class HttpServer {
ServerSocketChannel schan;
int threads;
int cperthread;
HttpCallback cb;
Server[] servers;
/**
* Create a <code>HttpServer<code> instance with the specified callback object
* for handling requests. One thread is created to handle requests,
* and up to ten TCP connections will be handled simultaneously.
* @param cb the callback object which is invoked to handle each
* incoming request
*/
public HttpServer (HttpCallback cb) throws IOException {
this (cb, 1, 10, 0);
}
/**
* Create a <code>HttpServer<code> instance with the specified number of
* threads and maximum number of connections per thread. This functions
* the same as the 4 arg constructor, where the port argument is set to zero.
* @param cb the callback object which is invoked to handle each
* incoming request
* @param threads the number of threads to create to handle requests
* in parallel
* @param cperthread the number of simultaneous TCP connections to
* handle per thread
*/
public HttpServer (HttpCallback cb, int threads, int cperthread)
throws IOException {
this (cb, threads, cperthread, 0);
}
/**
* Create a <code>HttpServer<code> instance with the specified number
* of threads and maximum number of connections per thread and running on
* the specified port. The specified number of threads are created to
* handle incoming requests, and each thread is allowed
* to handle a number of simultaneous TCP connections.
* @param cb the callback object which is invoked to handle
* each incoming request
* @param threads the number of threads to create to handle
* requests in parallel
* @param cperthread the number of simultaneous TCP connections
* to handle per thread
* @param port the port number to bind the server to. <code>Zero</code>
* means choose any free port.
*/
public HttpServer (HttpCallback cb, int threads, int cperthread, int port)
throws IOException {
schan = ServerSocketChannel.open ();
InetSocketAddress addr = new InetSocketAddress (port);
schan.socket().bind (addr);
this.threads = threads;
this.cb = cb;
this.cperthread = cperthread;
servers = new Server [threads];
for (int i=0; i<threads; i++) {
servers[i] = new Server (cb, schan, cperthread);
servers[i].start();
}
}
/** Tell all threads in the server to exit within 5 seconds.
* This is an abortive termination. Just prior to the thread exiting
* all channels in that thread waiting to be closed are forceably closed.
*/
public void terminate () {
for (int i=0; i<threads; i++) {
servers[i].terminate ();
}
}
/**
* return the local port number to which the server is bound.
* @return the local port number
*/
public int getLocalPort () {
return schan.socket().getLocalPort ();
}
static class Server extends Thread {
ServerSocketChannel schan;
Selector selector;
SelectionKey listenerKey;
SelectionKey key; /* the current key being processed */
HttpCallback cb;
ByteBuffer consumeBuffer;
int maxconn;
int nconn;
ClosedChannelList clist;
boolean shutdown;
Server (HttpCallback cb, ServerSocketChannel schan, int maxconn) {
this.schan = schan;
this.maxconn = maxconn;
this.cb = cb;
nconn = 0;
consumeBuffer = ByteBuffer.allocate (512);
clist = new ClosedChannelList ();
try {
selector = Selector.open ();
schan.configureBlocking (false);
listenerKey = schan.register (selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
System.err.println ("Server could not start: " + e);
}
}
/* Stop the thread as soon as possible */
public synchronized void terminate () {
shutdown = true;
}
public void run () {
try {
while (true) {
selector.select (1000);
Set selected = selector.selectedKeys();
Iterator iter = selected.iterator();
while (iter.hasNext()) {
key = (SelectionKey)iter.next();
if (key.equals (listenerKey)) {
SocketChannel sock = schan.accept ();
if (sock == null) {
/* false notification */
iter.remove();
continue;
}
sock.configureBlocking (false);
sock.register (selector, SelectionKey.OP_READ);
nconn ++;
if (nconn == maxconn) {
/* deregister */
listenerKey.cancel ();
listenerKey = null;
}
} else {
if (key.isReadable()) {
boolean closed;
SocketChannel chan = (SocketChannel) key.channel();
if (key.attachment() != null) {
closed = consume (chan);
} else {
closed = read (chan, key);
}
if (closed) {
chan.close ();
key.cancel ();
if (nconn == maxconn) {
listenerKey = schan.register (selector, SelectionKey.OP_ACCEPT);
}
nconn --;
}
}
}
iter.remove();
}
clist.check();
if (shutdown) {
clist.terminate ();
return;
}
}
} catch (IOException e) {
System.out.println ("Server exception: " + e);
// TODO finish
}
}
/* read all the data off the channel without looking at it
* return true if connection closed
*/
boolean consume (SocketChannel chan) {
try {
consumeBuffer.clear ();
int c = chan.read (consumeBuffer);
if (c == -1)
return true;
} catch (IOException e) {
return true;
}
return false;
}
/* return true if the connection is closed, false otherwise */
private boolean read (SocketChannel chan, SelectionKey key) {
HttpTransaction msg;
boolean res;
try {
InputStream is = new BufferedInputStream (new NioInputStream (chan));
String requestline = readLine (is);
MessageHeader mhead = new MessageHeader (is);
String clen = mhead.findValue ("Content-Length");
String trferenc = mhead.findValue ("Transfer-Encoding");
String data = null;
if (trferenc != null && trferenc.equals ("chunked"))
data = new String (readChunkedData (is));
else if (clen != null)
data = new String (readNormalData (is, Integer.parseInt (clen)));
String[] req = requestline.split (" ");
if (req.length < 2) {
/* invalid request line */
return false;
}
String cmd = req[0];
URI uri = null;
try {
uri = new URI (req[1]);
msg = new HttpTransaction (this, cmd, uri, mhead, data, null, key);
cb.request (msg);
} catch (URISyntaxException e) {
System.err.println ("Invalid URI: " + e);
msg = new HttpTransaction (this, cmd, null, null, null, null, key);
msg.sendResponse (501, "Whatever");
}
res = false;
} catch (IOException e) {
res = true;
}
return res;
}
byte[] readNormalData (InputStream is, int len) throws IOException {
byte [] buf = new byte [len];
int c, off=0, remain=len;
while (remain > 0 && ((c=is.read (buf, off, remain))>0)) {
remain -= c;
off += c;
}
return buf;
}
private void readCRLF(InputStream is) throws IOException {
int cr = is.read();
int lf = is.read();
if (((cr & 0xff) != 0x0d) ||
((lf & 0xff) != 0x0a)) {
throw new IOException(
"Expected <CR><LF>: got '" + cr + "/" + lf + "'");
}
}
byte[] readChunkedData (InputStream is) throws IOException {
LinkedList l = new LinkedList ();
int total = 0;
for (int len=readChunkLen(is); len!=0; len=readChunkLen(is)) {
l.add (readNormalData(is, len));
total += len;
readCRLF(is); // CRLF at end of chunk
}
readCRLF(is); // CRLF at end of Chunked Stream.
byte[] buf = new byte [total];
Iterator i = l.iterator();
int x = 0;
while (i.hasNext()) {
byte[] b = (byte[])i.next();
System.arraycopy (b, 0, buf, x, b.length);
x += b.length;
}
return buf;
}
private int readChunkLen (InputStream is) throws IOException {
int c, len=0;
boolean done=false, readCR=false;
while (!done) {
c = is.read ();
if (c == '\n' && readCR) {
done = true;
} else {
if (c == '\r' && !readCR) {
readCR = true;
} else {
int x=0;
if (c >= 'a' && c <= 'f') {
x = c - 'a' + 10;
} else if (c >= 'A' && c <= 'F') {
x = c - 'A' + 10;
} else if (c >= '0' && c <= '9') {
x = c - '0';
}
len = len * 16 + x;
}
}
}
return len;
}
private String readLine (InputStream is) throws IOException {
boolean done=false, readCR=false;
byte[] b = new byte [512];
int c, l = 0;
while (!done) {
c = is.read ();
if (c == '\n' && readCR) {
done = true;
} else {
if (c == '\r' && !readCR) {
readCR = true;
} else {
b[l++] = (byte)c;
}
}
}
return new String (b);
}
/** close the channel associated with the current key by:
* 1. shutdownOutput (send a FIN)
* 2. mark the key so that incoming data is to be consumed and discarded
* 3. After a period, close the socket
*/
synchronized void orderlyCloseChannel (SelectionKey key) throws IOException {
SocketChannel ch = (SocketChannel)key.channel ();
ch.socket().shutdownOutput();
key.attach (this);
clist.add (key);
}
synchronized void abortiveCloseChannel (SelectionKey key) throws IOException {
SocketChannel ch = (SocketChannel)key.channel ();
Socket s = ch.socket ();
s.setSoLinger (true, 0);
ch.close();
}
}
/**
* Implements blocking reading semantics on top of a non-blocking channel
*/
static class NioInputStream extends InputStream {
SocketChannel channel;
Selector selector;
ByteBuffer chanbuf;
SelectionKey key;
int available;
byte[] one;
boolean closed;
ByteBuffer markBuf; /* reads may be satisifed from this buffer */
boolean marked;
boolean reset;
int readlimit;
public NioInputStream (SocketChannel chan) throws IOException {
this.channel = chan;
selector = Selector.open();
chanbuf = ByteBuffer.allocate (1024);
key = chan.register (selector, SelectionKey.OP_READ);
available = 0;
one = new byte[1];
closed = marked = reset = false;
}
public synchronized int read (byte[] b) throws IOException {
return read (b, 0, b.length);
}
public synchronized int read () throws IOException {
return read (one, 0, 1);
}
public synchronized int read (byte[] b, int off, int srclen) throws IOException {
int canreturn, willreturn;
if (closed)
return -1;
if (reset) { /* satisfy from markBuf */
canreturn = markBuf.remaining ();
willreturn = canreturn>srclen ? srclen : canreturn;
markBuf.get(b, off, willreturn);
if (canreturn == willreturn) {
reset = false;
}
} else { /* satisfy from channel */
canreturn = available();
if (canreturn == 0) {
block ();
canreturn = available();
}
willreturn = canreturn>srclen ? srclen : canreturn;
chanbuf.get(b, off, willreturn);
available -= willreturn;
if (marked) { /* copy into markBuf */
try {
markBuf.put (b, off, willreturn);
} catch (BufferOverflowException e) {
marked = false;
}
}
}
return willreturn;
}
public synchronized int available () throws IOException {
if (closed)
throw new IOException ("Stream is closed");
if (reset)
return markBuf.remaining();
if (available > 0)
return available;
chanbuf.clear ();
available = channel.read (chanbuf);
if (available > 0)
chanbuf.flip();
else if (available == -1)
throw new IOException ("Stream is closed");
return available;
}
/**
* block() only called when available==0 and buf is empty
*/
private synchronized void block () throws IOException {
//assert available == 0;
int n = selector.select ();
//assert n == 1;
selector.selectedKeys().clear();
available ();
}
public void close () throws IOException {
if (closed)
return;
channel.close ();
closed = true;
}
public synchronized void mark (int readlimit) {
if (closed)
return;
this.readlimit = readlimit;
markBuf = ByteBuffer.allocate (readlimit);
marked = true;
reset = false;
}
public synchronized void reset () throws IOException {
if (closed )
return;
if (!marked)
throw new IOException ("Stream not marked");
marked = false;
reset = true;
markBuf.flip ();
}
}
static class NioOutputStream extends OutputStream {
SocketChannel channel;
ByteBuffer buf;
SelectionKey key;
Selector selector;
boolean closed;
byte[] one;
public NioOutputStream (SocketChannel channel) throws IOException {
this.channel = channel;
selector = Selector.open ();
key = channel.register (selector, SelectionKey.OP_WRITE);
closed = false;
one = new byte [1];
}
public synchronized void write (int b) throws IOException {
one[0] = (byte)b;
write (one, 0, 1);
}
public synchronized void write (byte[] b) throws IOException {
write (b, 0, b.length);
}
public synchronized void write (byte[] b, int off, int len) throws IOException {
if (closed)
throw new IOException ("stream is closed");
buf = ByteBuffer.allocate (len);
buf.put (b, off, len);
buf.flip ();
int n;
while ((n = channel.write (buf)) < len) {
len -= n;
if (len == 0)
return;
selector.select ();
selector.selectedKeys().clear ();
}
}
public void close () throws IOException {
if (closed)
return;
channel.close ();
closed = true;
}
}
/**
* Utilities for synchronization. A condition is
* identified by a string name, and is initialized
* upon first use (ie. setCondition() or waitForCondition()). Threads
* are blocked until some thread calls (or has called) setCondition() for the same
* condition.
* <P>
* A rendezvous built on a condition is also provided for synchronizing
* N threads.
*/
private static HashMap conditions = new HashMap();
/*
* Modifiable boolean object
*/
private static class BValue {
boolean v;
}
/*
* Modifiable int object
*/
private static class IValue {
int v;
IValue (int i) {
v =i;
}
}
private static BValue getCond (String condition) {
synchronized (conditions) {
BValue cond = (BValue) conditions.get (condition);
if (cond == null) {
cond = new BValue();
conditions.put (condition, cond);
}
return cond;
}
}
/**
* Set the condition to true. Any threads that are currently blocked
* waiting on the condition, will be unblocked and allowed to continue.
* Threads that subsequently call waitForCondition() will not block.
* If the named condition did not exist prior to the call, then it is created
* first.
*/
public static void setCondition (String condition) {
BValue cond = getCond (condition);
synchronized (cond) {
if (cond.v) {
return;
}
cond.v = true;
cond.notifyAll();
}
}
/**
* If the named condition does not exist, then it is created and initialized
* to false. If the condition exists or has just been created and its value
* is false, then the thread blocks until another thread sets the condition.
* If the condition exists and is already set to true, then this call returns
* immediately without blocking.
*/
public static void waitForCondition (String condition) {
BValue cond = getCond (condition);
synchronized (cond) {
if (!cond.v) {
try {
cond.wait();
} catch (InterruptedException e) {}
}
}
}
/* conditions must be locked when accessing this */
static HashMap rv = new HashMap();
/**
* Force N threads to rendezvous (ie. wait for each other) before proceeding.
* The first thread(s) to call are blocked until the last
* thread makes the call. Then all threads continue.
* <p>
* All threads that call with the same condition name, must use the same value
* for N (or the results may be not be as expected).
* <P>
* Obviously, if fewer than N threads make the rendezvous then the result
* will be a hang.
*/
public static void rendezvous (String condition, int N) {
BValue cond;
IValue iv;
String name = "RV_"+condition;
/* get the condition */
synchronized (conditions) {
cond = (BValue)conditions.get (name);
if (cond == null) {
/* we are first caller */
if (N < 2) {
throw new RuntimeException ("rendezvous must be called with N >= 2");
}
cond = new BValue ();
conditions.put (name, cond);
iv = new IValue (N-1);
rv.put (name, iv);
} else {
/* already initialised, just decrement the counter */
iv = (IValue) rv.get (name);
iv.v --;
}
}
if (iv.v > 0) {
waitForCondition (name);
} else {
setCondition (name);
synchronized (conditions) {
clearCondition (name);
rv.remove (name);
}
}
}
/**
* If the named condition exists and is set then remove it, so it can
* be re-initialized and used again. If the condition does not exist, or
* exists but is not set, then the call returns without doing anything.
* Note, some higher level synchronization
* may be needed between clear and the other operations.
*/
public static void clearCondition(String condition) {
BValue cond;
synchronized (conditions) {
cond = (BValue) conditions.get (condition);
if (cond == null) {
return;
}
synchronized (cond) {
if (cond.v) {
conditions.remove (condition);
}
}
}
}
}