先看下面例子:
public
class
SelectSockets {
private
ByteBuffer buffer = ByteBuffer.allocateDirect(
1024
);
public
static
void
main(String[] argv)
throws
Exception {
new
SelectSockets().go();
}
public
void
go()
throws
Exception {
int
port =
1234
;
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverChannel.socket();
serverSocket.bind(
new
InetSocketAddress(port));
serverChannel.configureBlocking(
false
);
Selector selector = Selector.open();
/*
* 把serverChannel注册到选择器里,这里没有绑定某个对象,register可以通过第三个参数指定要绑定的对象,比如,绑定一个
* ByteBuffer对象等,也可以通过key.attach (myObject)绑定,其中key是serverChannel.register(...)的返回对象,
* 这里接受连接和读取数据都只有一个通道,无需加以区分,如果有多个,可以通过绑定对应的对象加以区分
*/
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while
(
true
) {
int
n = selector.select();
//阻塞一直等待,也可以通过select的参数设置阻塞的最长时间
if
(n ==
0
) {
continue
;
}
Iterator it = selector.selectedKeys().iterator();
while
(it.hasNext()) {
SelectionKey key = (SelectionKey)it.next();
it.remove();
if
(key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
if
(channel ==
null
)
continue
;
channel.configureBlocking(
false
);
channel.register(selector, SelectionKey.OP_READ);
buffer.clear();
buffer.put(
"Hi there!\r\n"
.getBytes());
buffer.flip();
channel.write(buffer);
}
else
if
(key.isReadable()) {
readDataFromSocket(key);
}
}
}
}
protected
void
readDataFromSocket(SelectionKey key)
throws
Exception {
SocketChannel socketChannel = (SocketChannel) key.channel();
int
count;
buffer.clear();
while
((count = socketChannel.read(buffer)) >
0
) {
buffer.flip();
while
(buffer.hasRemaining()) {
socketChannel.write(buffer);
//这里只是简单地把收到的数据发送回去
}
buffer.clear();
}
if
(count <
0
) {
/*
* 当收到EOF时,count<0,可以调用close方法关闭该TCP连接,当然,也可以在收到某条消息时或需要的时候调用close方法关闭该连接,
* 调用close之后,与之关联的selectionKey也随之失效,
同时该selector会在下一次调用select方法被注销掉
*/
socketChannel.close();
}
}
}
选择器对象是线程安全的,如某个线程调用selector对象的close方法时,另一个线程正在执行该selector对象的select方法,则close需要等待select方法执行完才可执行。selector对象虽然是线程安全的,但它们包含的键集合不是,通过 keys( )和 selectKeys( )返回的键的集合是 Selector对象内部的私有的 Set 对象集合的
直接引用。这些集合可能在任意时间被改变。已注册的键的集合是只读的。如果您试图修改它,那么得到的将是一个 java.lang.UnsupportedOperationException,但是当您在观察它们的时候,它们可能发生了改变,您仍然会遇到麻烦。Iterator 对象是快速失败的(fail-fast):如果底层的 Set 被改变了,它们将会抛出 java.util.ConcurrentModificationException,因此如果您期望在多个线程间共享选择器和/或键,请对此做好准备。您可以直接修改选择键,但请注意您这么做时可能会彻底破坏另一个线程的Iterator。 如果在多个线程并发地访问一个选择器的键的集合的时候存在任何问题,您可以采取一些步骤 来合理地同步访问。在执行选择操作时,选择器在 Selector 对象上进行同步,然后是已注册的键的集合,最后是已选择的键的集合,按照这样的顺序。已取消的键的集合也在选择过程的的第 1 步和第 3步之间保持同步(当与已取消的键的集合相关的通道被注销时)。 在多线程的场景中,如果您需要对任何一个键的集合进行更改,不管是直接更改还是其他操作 带来的副作用,您都需要首先以相同的顺序,在同一对象上进行同步。锁的过程是非常重要的。如果竞争的线程没有以相同的顺序请求锁,就将会有死锁的潜在隐患。如果您可以确保否其他线程不会同时访问选择器,那么就不必要进行同步了。
Selector 类的 close( )方法与 select( )方法的同步方式是一样的,因此也有一直阻塞的可能性。在选择过程还在进行的过程中,所有对close( )的调用都会被阻塞,直到选择过程结束,或者执行选择的线程进入睡眠。在后面的情况下,执行选择的线程将会在执行关闭的线程获得锁时立即被唤醒,并关闭选择器。
使用线程池来为通道提供服务
public
class
SelectSocketsThreadPool
extends
SelectSockets {
private
static
final
int
MAX_THREADS =
5
;
private
ThreadPool pool =
new
ThreadPool(MAX_THREADS);
public
static
void
main(String[] argv)
throws
Exception {
new
SelectSocketsThreadPool().go();
}
@Override
protected
void
readDataFromSocket(SelectionKey key)
throws
Exception {
WorkerThread worker = pool.getWorker();
if
(worker ==
null
) {
// No threads available. Do nothing. The selection loop will keep calling this method
//until a thread becomes available. This design could be improved.
return
;
}
worker.serviceChannel(key);
}
private
class
ThreadPool {
List<WorkerThread> idle =
new
LinkedList<WorkerThread>();
ThreadPool(
int
poolSize) {
for
(
int
i =
0
; i < poolSize; i++) {
WorkerThread thread =
new
WorkerThread(
this
);
thread.setName(
"Worker"
+ (i +
1
));
thread.start();
idle.add(thread);
}
}
WorkerThread getWorker() {
WorkerThread worker =
null
;
synchronized
(idle) {
if
(idle.size() >
0
) {
worker = (WorkerThread) idle.remove(
0
);
}
}
return
(worker);
}
void
returnWorker(WorkerThread worker) {
synchronized
(idle) {
idle.add(worker);
}
}
}
private
class
WorkerThread
extends
Thread {
private
ByteBuffer buffer = ByteBuffer.allocate(
1024
);
private
ThreadPool pool;
private
SelectionKey key;
WorkerThread(ThreadPool pool) {
this
.pool = pool;
}
public
synchronized
void
run() {
System.out.println(
this
.getName() +
" is ready"
);
while
(
true
) {
try
{
// Sleep and release object lock
this
.wait();
}
catch
(InterruptedException e) {
e.printStackTrace();
// Clear interrupt status
this
.interrupted();
}
if
(key ==
null
) {
continue
;
}
System.out.println(
this
.getName() +
" has been awakened"
);
try
{
drainChannel(key);
}
catch
(Exception e) {
System.out.println(
"Caught '"
+ e +
"' closing channel"
);
try
{
key.channel().close();
}
catch
(IOException ex) {
ex.printStackTrace();
}
key.selector().wakeup();
}
key =
null
;
// Done. Ready for more. Return to pool
this
.pool.returnWorker(
this
);
}
}
synchronized
void
serviceChannel(SelectionKey key) {
this
.key = key;
/**
* 由于执行完readDataFromSocket方法,或者说执行完serviceChannel方法时,选择过程的线程将重新循环并几乎立即再次调用 select(),
* 这时通道中的数据实际上还没有取出,因为取数据在另一个线程中进行,故选择器重复认为数据可读,从而重复地调用readDataFromSocket方法,
* 所以,这里对键的interest集合修改,将interest(感兴趣的操作)从读取就绪(read-rreadiness)状态中移除
*/
key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
this
.notify();
}
void
drainChannel(SelectionKey key)
throws
Exception {
SocketChannel channel = (SocketChannel) key.channel();
int
count;
buffer.clear();
while
((count = channel.read(buffer)) >
0
) {
buffer.flip();
while
(buffer.hasRemaining()) {
channel.write(buffer);
}
buffer.clear();
}
if
(count <
0
) {
channel.close();
return
;
}
//当工作线程结束为通道提供的服务时,它将再次更新键的ready集合,将interest重新放到读取就绪集合中。
key.interestOps(key.interestOps() | SelectionKey.OP_READ);
/**
* 在选择器上显式地调用 wakeup(),因为主线程可能在select()处阻塞,这将使它继续执行。
* 这个选择循环会再次执行一个轮回(可能什么也没做)并带着被更新的键重新进入select()。这样做的原因是:
* key所对应的通道在取数据的过程中被从对读感兴趣的集合中删掉,有可能在从buffer中取出数据的时候又有新的数据到来,
* 所以在把该通道加到读集合中之后,主动唤醒选择器,让选择器检测是否有读就绪,否则选择器可能一直阻塞,而实际上数据已经就绪了
*/
key.selector().wakeup();
}
}
}