NIO best practice

J

John

Hi All, I have developed a simple NIO web server and was just looking
to elicit some comments on my coding style and the like. I would also
appreciate it if you could out any potentially dangerous/unsafe NIO
code that I have used. As I have been debugging my program a lot of the
NIO stuff doesn't seem to be entirely intuitive. The program works OK.
for the most part but occasionally I do get the exception "An existing
connection
was forcibly closed by the remote host " occasionally. Any ideas ?

The code is attached inline below:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.*;

class ChannelState
{
public ChannelState(FileChannel fchannel, long write, long total) {
this.fchannel=fchannel;
this.position=write;
this.total=total;
}
public FileChannel fchannel;
long position;
long total;
}

public class proxy
{
public static void main(String[] args)
{
HashMap<SocketChannel,ChannelState> remote_clients=new HashMap();

//File wwwBase=new File("h:/workspace/web server/");
File wwwBase=new File("/home/jacasey/workspace/web server/");
ByteBuffer buffer=ByteBuffer.allocateDirect(16384);
try {
ServerSocketChannel channel= ServerSocketChannel.open();

InetSocketAddress address=new InetSocketAddress(8080);
channel.socket().bind(address);

channel.configureBlocking(false);

Selector selector=Selector.open();
channel.register(selector,SelectionKey.OP_ACCEPT);

while(true)
{
int n=selector.select();

if(n==0)
{
continue;
}
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> ready = readyKeys.iterator();

while(ready.hasNext())
{
SelectionKey selkey=ready.next();
ready.remove();
try
{
if(selkey.isAcceptable() && selkey.isValid())
{
ServerSocketChannel server=(ServerSocketChannel)
selkey.channel();
SocketChannel client=server.accept();

if(client==null)
return;

client.configureBlocking(false);
client.register(selector,SelectionKey.OP_READ);
}
else if(selkey.isReadable() && selkey.isValid())
{
if(selkey.isValid())
{
handle_ready_read(wwwBase,buffer,selkey,remote_clients);
}
}
else if(selkey.isWritable() && selkey.isValid())
{
if(selkey.isValid())
{
handle_write_ready(remote_clients, selector, selkey);

}
}
}
catch(IOException err)
{
selkey.channel().close();
selkey.cancel();
err.printStackTrace();
}

}

}
} catch (IOException e) {

e.printStackTrace();
}


}

public static void handle_write_ready(HashMap<SocketChannel,
ChannelState> remote_clients, Selector selector, SelectionKey selkey)
throws IOException, ClosedChannelException {
SocketChannel client=(SocketChannel) selkey.channel();
ChannelState state=remote_clients.get(client);
FileChannel fchannel=state.fchannel;

long write=fchannel.transferTo(state.position,16384,client);

if(write>0)
{
state.position+=write;
}
else
{
state.fchannel.close();
selkey.channel().close();
selkey.cancel();
}
}

public static void handle_ready_read(File wwwBase, ByteBuffer buffer,
SelectionKey selkey, HashMap<SocketChannel, ChannelState>
remote_clients) throws IOException, MalformedURLException,
FileNotFoundException {


SocketChannel client=(SocketChannel) selkey.channel();
int read=client.read(buffer);


// process the request if we actually read anything in
if(read>1)
{
// determine what sort of request it is

String requestMethod="";

byte[] request=new byte[buffer.position()];
buffer.flip();
buffer.get(request);
buffer.clear();
int i=0;
for(;i<request.length;i++)
{
if(request==' ')
{
i++;
break;
}
requestMethod+=(char)request;
}

// extract the query_string if we are dealing with a get request
StringBuffer queryString=new StringBuffer(100);
if(requestMethod.equalsIgnoreCase("GET"))
{
for(;i<request.length;i++)
{
if(request==' ')
break;

queryString.append((char)request);
}
}
else
{
System.err.println("unhandled method"+requestMethod);
// unhandled method
}


File data=new File(wwwBase+queryString.toString());
FileInputStream fis=new FileInputStream(data);
FileChannel fchannel=fis.getChannel();

// use the inbuilt class to guess the type of data we are going to
write back to the client
String contentType="application/octed-stream";
System.out.println("request: "+data.toString());
contentType=URLConnection.guessContentTypeFromName(data.toString());

if(data.isFile())
{
StringBuffer output=new StringBuffer();
output.append("HTTP/1.0 200 OK\r\n");
output.append("Server: proxy\r\n");
output.append("Connection: close\r\n");
output.append("Content-Type: "+contentType+"\r\n");
output.append(("Content-Length: "+data.length()+"\r\n"));
output.append("\r\n");

ByteBuffer header=ByteBuffer.allocate(1024);
header.put(output.toString().getBytes());
header.flip();

long n=client.write(header);
if(n<output.length())
{
System.err.println("something weird happening");

}

// pipe the file channel data back to our socket channel
long write=fchannel.transferTo(0,16384,client);

// if we have a short write select OP_WRITE
if(write<1)
{
selkey.cancel();
client.close();
}
else if(write<data.length())
{
remote_clients.put(client,new
ChannelState(fchannel,write,data.length()));
client.register(selkey.selector(),SelectionKey.OP_WRITE);
}
else
{
selkey.cancel();
client.close();
}
}
}
else
{
selkey.cancel();
client.close();
}
}
}
 
J

John

John said:
Hi All, I have developed a simple NIO web server and was just looking
to elicit some comments on my coding style and the like. I would also
appreciate it if you could out any potentially dangerous/unsafe NIO
code that I have used. As I have been debugging my program a lot of the
NIO stuff doesn't seem to be entirely intuitive. The program works OK.
for the most part but occasionally I do get the exception "An existing
connection
was forcibly closed by the remote host " occasionally. Any ideas ?

I should also note that the error comes up as a "Broken Pipe" on linux
and only seems to occur when refreshing a page that hasn't been
completely rendered yet.
 
T

Timo Stamm

John said:
I should also note that the error comes up as a "Broken Pipe" on linux
and only seems to occur when refreshing a page that hasn't been
completely rendered yet.

That's perfectly normal. If you refresh a page that is not completely
loaded or cancel the loading of a page, the web broser will close the
socket connection.

The server can no longer write to this connection and an exception is
raised. In this case, you can safely drop the exception after catching it.


Timo
 
J

John

Timo said:
That's perfectly normal. If you refresh a page that is not completely
loaded or cancel the loading of a page, the web broser will close the
socket connection.

The server can no longer write to this connection and an exception is
raised. In this case, you can safely drop the exception after catching it.

OK. another question I have also noticed that once the web browser
closes the connection I get a lot of TIME_WAIT messages. Eventually
this seems to chew up the network resources of my OS (windows XP) and
can momentarily prevent new clients connecting properly. Is this also
normal behaviour ?
 
E

EJP

Several comments interspersed below.
class ChannelState
{
...
public FileChannel fchannel;
long position;
long total;
}

Normally a per-channel ByteBuffer would be declared in this class.
public class proxy
{
public static void main(String[] args)
{
HashMap<SocketChannel,ChannelState> remote_clients=new HashMap();

You don't need this map: you can attach the ChannelState to the channel
via the key attachment.
ByteBuffer buffer=ByteBuffer.allocateDirect(16384);

One byte buffer for all the channels? See above.
SelectionKey selkey=ready.next();
ready.remove();
try
{

I would add 'if (!selkey.isValid()) continue;' at this point and get rid
of all the other isValid() tests, especially the double one at isReadable().
if(client==null)
return;

Surely you mean 'continue'?
catch(IOException err)
{
selkey.channel().close();
selkey.cancel();

The cancel() is redundant, it happens with the close(); Applies several
times below.
public static void handle_ready_write(...)

In this routine and the other write routines below you are closing the
channel once you have transferred 16384 bytes. Is this really what you mean?
public static void handle_ready_read(File wwwBase, ByteBuffer buffer,
SelectionKey selkey, HashMap<SocketChannel, ChannelState>
remote_clients) throws IOException, MalformedURLException,
FileNotFoundException {

SocketChannel client=(SocketChannel) selkey.channel();
int read=client.read(buffer);

// process the request if we actually read anything in
if(read>1)
{
...
StringBuffer queryString=new StringBuffer(100);
if(requestMethod.equalsIgnoreCase("GET"))

Don't you mean read >= 1? and what if the readcount was only 1? or 2?
How can the request method ever be "GET" in those cases?

This technique exhibits the fallacy that the entire request will be read
in one attempt. Not necessarily; and this is why you can't use a single
read buffer. You need to be much more defensive than this: use a buffer
per channel and accumulate reads *until* you have a complete request.
You are in danger of missing some requests altogether with this
implementation.

And if read < 0 and the request is incomplete you should just close the
channel.
long n=client.write(header);
if(n<output.length())
{
System.err.println("something weird happening");

}

Nothing weird about it! It just means the send buffer didn't have room
for the entire write. Again you have to defend against this properly.
// pipe the file channel data back to our socket channel
long write=fchannel.transferTo(0,16384,client);

// if we have a short write select OP_WRITE
if(write<1)
{
selkey.cancel();
client.close();

Why? It just means the send buffers were full. You should handle this
the same as the partial write case. If the channel doesn't turn up as
writable within a reasonable period of time you should *then* bail out
of the transfer.

And if this write gets any kind of SocketException it means the peer has
gone away, one way or the other, so you should close the channel and
forget about it.
 
E

EJP

John said:
Thanks for all your suggestions that was great :)

Let us know how you're getting along after these improvements.

The key to not going into TIME-WAIT is to be the peer that *receives*
the close, not the peer that *initiates* the close. For a server, this
means not closing the channel after writing the data, but just
continuing to select on OP_READ and reading when it's ready, until the
peer closes his end and you get an EOF on reading. You then close your
SocketChannel and your port will then go from CLOSE-WAIT via LAST-ACK to
CLOSED. If you are the one that closes first, you go from ESTABLISHED to
FIN-WAIT-1 to FIN-WAIT-2 to TIME-WAIT to CLOSED, with a 2*MSL delay in
TIME-WAIT. See http://www.ietf.org/rfc/rfc793.txt #3.5 and Figure 13.

Basically you should be assuming a kept-alive HTTP connection with
multiple requests, and only closing when the client closes.

Of course that doesn't mean that you don't want to time the next read
out ... you do, but you want to give the client a chance to close first
for the reason above. You also have to be prepared for SocketExceptions
when reading, as I've heard that some browsers from the north west don't
do orderly closes, they do resets instead, very naughty.
 
J

John

EJP said:
Let us know how you're getting along after these improvements.

Well the code is still a little bit of a work in progress but I have
integrated your suggestions into the following code base. Its amazing
how simple it is to implement persistant connections in http. I had
originally thought it was going to be a little tougher buts its amazing
how easy really is! :)

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.*;

class ChannelState
{
public ChannelState(FileChannel fchannel, long position, long total,
ByteBuffer buffer) {
this.fchannel=fchannel;
this.position=position;
this.total_file_size=total;
this.buffer=buffer;
}
public FileChannel fchannel;
public ByteBuffer buffer;
public long position;
public long total_file_size;
}

public class webserver
{
public static void main(String[] args)
{
//File wwwBase=new File("h:/workspace/web server/");
//File wwwBase=new File("/home/jacasey/workspace/web server/");

File wwwBase=new File("c:/Eclipse/workspace/web server/");

try {
ServerSocketChannel channel= ServerSocketChannel.open();

InetSocketAddress address=new InetSocketAddress(8080);
channel.socket().bind(address);
channel.configureBlocking(false);

Selector selector=Selector.open();
channel.register(selector,SelectionKey.OP_ACCEPT);

while(true)
{
int n=selector.select();

if(n==0)
{
continue;
}
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> ready = readyKeys.iterator();

while(ready.hasNext())
{
SelectionKey selkey=ready.next();
ready.remove();
try
{
if(selkey.isValid()==false)
{
continue;
}
else if(selkey.isAcceptable())
{
ServerSocketChannel server=(ServerSocketChannel)
selkey.channel();
SocketChannel client=server.accept();

if(client==null)
continue;

client.configureBlocking(false);
client.register(selector,SelectionKey.OP_READ);
ByteBuffer buffer=ByteBuffer.allocate(32768);

SelectionKey key=client.keyFor(selector);
key.attach(buffer);
}
else if(selkey.isReadable())
{
handle_ready_read(wwwBase,selkey);
}
else if(selkey.isWritable())
{
handle_write_ready(selector, selkey);
}
}
catch(IOException err)
{
selkey.channel().close();
err.printStackTrace();
}

}

}
} catch (IOException e) {

e.printStackTrace();
}


}

public static void handle_write_ready(Selector selector, SelectionKey
selkey) throws IOException, ClosedChannelException {
SocketChannel client=(SocketChannel) selkey.channel();

ChannelState state=(ChannelState) selkey.attachment();

if(state.buffer!=null)
{
client.write(state.buffer);

if(state.buffer.hasRemaining())
{
state.buffer.compact();
client.register(selkey.selector(),SelectionKey.OP_WRITE);
selkey.attach(state);
}
else
{
transfer_file(selkey, client, state);
}
}
else
{
transfer_file(selkey, client, state);
}
}

public static void handle_ready_read(File wwwBase, SelectionKey
selkey) throws IOException, MalformedURLException,
FileNotFoundException
{
ByteBuffer buffer=(ByteBuffer) selkey.attachment();
SocketChannel client=(SocketChannel) selkey.channel();
long BYTES_READ=client.read(buffer);
int position=buffer.position();
buffer.flip();

if(BYTES_READ>=4)
{
// String tmp=new String(buffer.array());
// System.err.println(tmp);

boolean short_read = process_request(buffer);
if(short_read==false)
{
// determine what sort of request it is
String requestMethod="";

String queryString = process_query_string(buffer, requestMethod);

File data=new File(wwwBase+queryString);
FileInputStream fis=new FileInputStream(data);
FileChannel fchannel=fis.getChannel();

// use the inbuilt class to guess the type of data we are going to
write back to the client
String contentType="application/octed-stream";
System.out.println("request: "+data.toString());
contentType=URLConnection.guessContentTypeFromName(data.toString());

if(data.isFile())
{
StringBuffer output=new StringBuffer();
output.append("HTTP/1.0 200 OK\r\n");
output.append("Server: proxy\r\n");
//output.append("Connection: close\r\n"); // eventually I will
have to detect whether a client is capable of persistance OK for now.
output.append("Connection: keep-alive\r\n");
output.append("Content-Type: "+contentType+"\r\n");
output.append(("Content-Length: "+data.length()+"\r\n"));
output.append("\r\n");

buffer.clear();
buffer.put(output.toString().getBytes());
buffer.flip();

client.write(buffer);

if(buffer.hasRemaining())
{
// short header write
buffer.compact();

client.register(selkey.selector(),SelectionKey.OP_WRITE);
selkey.attach(buffer);
}
else
{
transfer_file(selkey, client, new ChannelState(fchannel, 0,
data.length(),buffer));
}
}
else
{
// file does not exist
}
}
else
{
// short read
client.register(selkey.selector(),SelectionKey.OP_READ);

// reset the buffer so that we can read more data in
buffer.position(position);
buffer.limit(buffer.capacity());
selkey.attach(buffer);
}
}
else if(BYTES_READ>0)
{
// short read
client.register(selkey.selector(),SelectionKey.OP_READ);

// reset the buffer so that we can read more data in
buffer.position(position);
buffer.limit(buffer.capacity());
selkey.attach(buffer);
}
else if(BYTES_READ==-1)
{
System.err.println("closed");
client.close();
}
}

public static String process_query_string(ByteBuffer buffer, String
requestMethod) {
byte[] request=new byte[buffer.position()];
buffer.flip();
buffer.get(request);
buffer.clear();
int i=0;
for(;i<request.length;i++)
{
if(request==' ')
{
i++;
break;
}
requestMethod+=(char)request;
}

// extract the query_string if we are dealing with a get request
StringBuffer queryString=new StringBuffer(100);
if(requestMethod.equalsIgnoreCase("GET"))
{
for(;i<request.length;i++)
{
if(request==' ')
break;

queryString.append((char)request);
}
}
else
{
System.err.println("unhandled method"+requestMethod);
// unhandled method
}
return queryString.toString();
}

public static boolean process_request(ByteBuffer buffer) {
boolean short_read=false;
byte END_HTTP[]="\r\n\r\n".getBytes();
byte END_OF_REQUEST[]=new byte[4];

buffer.position(buffer.limit()-4);
buffer.get(END_OF_REQUEST,0,4);

for(int j=0;j<END_OF_REQUEST.length;j++)
{
if(END_HTTP[j]!=END_OF_REQUEST[j])
{
short_read=true;
break;
}
}
return short_read;
}

public static void transfer_file(SelectionKey selkey, SocketChannel
client, ChannelState cs) throws IOException, ClosedChannelException {

ByteBuffer buffer=cs.buffer;
// pipe the file channel data back to our socket channel
long write=cs.fchannel.transferTo(cs.position,32768,client);

// if we have a short write select OP_WRITE
if(cs.position+write<cs.total_file_size)
{
client.register(selkey.selector(),SelectionKey.OP_WRITE);
cs.position+=write;
selkey.attach(cs);
}
else
{
cs.fchannel.close();
client.register(selkey.selector(),SelectionKey.OP_READ);
buffer.clear();
selkey.attach(buffer);
}
}
}
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,070
Latest member
BiogenixGummies

Latest Threads

Top