Nieblokującymi socketami zainteresowałem się dosyć niedawno, a było to związane z grą nad którą obecnie pracuję i która już nie bawem powinna ukazać się na urządzenia mobilne. Tworząc serwer do niej napotkałem problem z wykonaniem poczekalni w grze, bowiem chciałem aby gracze mogli się komunikować ze sobą za pomocą czatu ale wykonanego w jednym wątku. Sama java dostarcza począwszy od wersji 1.4 bibliotekę o nazwie .nio. Która służy do obsługi nieblokujących wejść i wyjść. W wersji 1.7 wyszła także nowsza wersja .nio2 ale z niej korzystać nie będziemy bo na androida zalecane jest pisanie w javie do 1.6.
W programowaniu nieblokującym zmienia się trochę koncepcja, bowiem nie czekamy tutaj na nasze dane tylko biegniemy cały czas naprzód. Dlatego potrzebujemy dla świeżo przybywających porcji danych jakiegoś akumulatora gdzie się będą te porcje gromadzić. Takim akumulatorem w naszym wypadku jest buffor, który przechowuje tyle informacji ile w danej chwili do niego trafiło. Oczywiście są różne rodzaje bufforów, o różnych pojemnościach i różnym przeznaczeniu. Podstawowym bufforem jest bufor bajtu. Ale są także bufory char, long, int, double czyli typów podstawowych. Oto ich lista:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
I gdy mamy interesujący nas bufor możemy wykorzystać do przechowywania w nim danych, powoli przybywających z obiegu pętli na obieg,
Gdy tworzymy buffor naszą podstawową czynnością powinno być określenie jego rozmiaru, które definiujemy bezpośrednio, lub pośrednio za pomocą interesującego nas obiektu:
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
ByteBuffer buffer2 = ByteBuffer.allocateDirect(1024);
ByteBuffer buffer3 = ByteBuffer.wrap(new String("hello").getBytes());
W ten sposób ustalamy pojemność naszego buffora.
Kwestia zapisu:
// Writing on a buffer
IntBuffer buffer = IntBuffer.allocate(10);
for (int i=0; i < buffer.capacity(); i++) {
buffer.put(i);
}
i odczytu:
// Reading from a buffer
buffer.position(0);
while (buffer.hasRemaining()) {
int i = buffer.get();
System.out.println("i="+i);
}
Teraz posiadając nasze akumulatory(bufory) możemy przejść do komunikacji sieciowej, choć warto dodać że pakiet nio jest wykorzystywana także do innych rzeczy np. do odczytywania/zapisywania danych z plików.
Nim jednak przejdziemy do pokazania kodu, warto porównać działanie tych dwóch przeciwnych podejść w ujęciu pracy programu:
Programowanie blokujące:
Programowanie nieblokujące:
Jak widzimy, nieblokujące programowanie komplikuje nam życie i o ile w tym przykładowym kodzie serwer/client możemy to z "pewną dozą łatwości" zrozumieć:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import
java.nio.channels.ReadableByteChannel;
import
java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import
java.nio.channels.WritableByteChannel;
public class HelloServer {
/**
* Tekst wysyłany do klienta
*/
private static final String HELLO_REPLY = "Hello World!";
public static void main(String[] args) {
// Deklarujemy
buffor o rozmiarze buffora równym obiektowi String HELLO_REPLY
ByteBuffer buffer = ByteBuffer.wrap(HELLO_REPLY.getBytes());
/*
* Tworzymy obiekt klasy ServerSocketChannel, w
programowaniu nie blokującym działamy na strumieniach!
*/
ServerSocketChannel ssc = null;
try {
/*
* Otwieramy strumień serwerowego socketa i
bindujemy z nim nasz adres sieciowy i konkretny port
* I teraz NAJWAŻNIEJSZA opcja to wyłączenie
blokowania! bez tego nasz program będzie czekał na dane i się blokował do
* czasu ich nadejścia
*/
ssc = ServerSocketChannel.open();
ssc.socket().bind(new
InetSocketAddress(8765));
ssc.configureBlocking(false);
/*
* W pętli tworzymy ciało naszego
serwera, czyli przemiał informacji.
*/
while (true) {
/*
* Tutaj co obieg akceptujemy kanał socketa, i
jeśli okaże się że nic tam nie ma to znaczy że żadna informacja do
* nas jeszcze nie dotarła, a jeśli dostaniemy
dostęp do kanału, to znak że pojawiła się tam conajmniej 1 porcja
* danych i musimy ją przetworzyć.
*/
SocketChannel sc =
ssc.accept();
// if sc == null, that means there
is no connection yet
// do something else
if (sc == null) {
// pretend to do
something useful here
System.out.println("Doing
something useful....");
try {
Thread.sleep(3000);
} catch
(InterruptedException e) {
e.printStackTrace();
}
} else { // received an
incoming connection
System.out.println("Received an
incoming connection from " +
sc.socket().getRemoteSocketAddress());
printRequest(sc);
buffer.rewind();
sc.write(buffer);
sc.close();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ssc != null) {
try {
ssc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* Napisanie odpowiedzi
* @param sc
* @throws IOException
*/
private static void
printRequest(SocketChannel sc) throws IOException {
/*
* Do kanału bajtowego odczytu bindujemy nasz strumień
*/
ReadableByteChannel rbc = Channels.newChannel(
sc.socket().getInputStream());
//
To samo robimy z kanałem zapisu
WritableByteChannel wbc = Channels.newChannel(System.out);
//
System.out.println(" ");
//Tworzymy
dynamiczny buffor dla odczytanych danych, które nastepnie od razu wyświetlamy w
konsoli
ByteBuffer b = ByteBuffer.allocate(8);
//
read 8 bytes
while (rbc.read(b) != -1)
{
b.flip();
while (b.hasRemaining()) {
wbc.write(b);
}
b.clear();
}
}
}
---------------------------------------------------------------------------------------------------------
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class HelloClient {
public static final String HELLO_REQUEST = "Hello!";
public static void main(String[] args) {
SocketChannel sc = null;
try {
sc = SocketChannel.open();
sc.configureBlocking(false);
// make sure to call sc.connect() or else
// calling sc.finishConnect() will throw
// java.nio.channels.NoConnectionPendingException
sc.connect(new
InetSocketAddress(8765));
// if the socket has connected, sc.finishConnect() should
// return false
while (!sc.finishConnect()) {
// pretend to do something useful here
System.out.println("Doing something useful...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
while(true){
System.out.println("Sending a request to HelloServer");
ByteBuffer buffer = ByteBuffer.wrap(HELLO_REQUEST.getBytes());
sc.write(buffer);
try {
Thread.sleep(3000);
}
catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (sc != null) {
try {
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Tak już sprawa z Selectorami nie wygląda zbyt intuicyjne. Teoretycznie jest ona logiczna. Ponieważ z naszej aplikacji mogą korzystać dziesiątki czy setki użytkowników.
to powstaje problem segregacji danych przychodzących z różnych źródeł. W tym celu stosuje się właśnie Selector, który nam to wszystko segreguje:
jednak powoduje on kilka problemów, które omówię w kolejnej części.
Bibliografia:
http://www.onjava.com/pub/a/onjava/2002/09/04/nio.html?page=1
http://tutorials.jenkov.com/java-nio/nio-vs-io.html