我正在尝试使用NIO编写一个简单的客户端服务器模型(以了解它是如何工作的)。有两种类型的消息可以发送Connection(服务器在连接时发送给客户端的消息,提供与客户端ID类似的信息)和Ping。
如果客户未被按下>的时间段。 4秒钟服务器将ping客户端,客户端将以乒乓响应。
当我初始化我的服务器时,我这样做:
try{
selector = Selector.open();
selector.wakeup();
serverChannel = selector.provider().openServerSocketChannel();
serverChannel.socket().bind(new InetSocketAddress(port));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}catch (IOException e){
log.Error(e);
throw new RuntimeException("Error opening selector.", e);
}
发生这种情况后,我们进入我们的服务器循环,它将找到已选择的键并在键上执行所选操作
int select = 0;
while (running){
try{
select = selector.select(timeout);
if(select == 0){
continue;
}
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
while(selectedKeys.hasNext()){
SelectionKey key = selectedKeys.next();
selectedKeys.remove();
Connection conn = (Connection)key.attachment();
if(conn != null){
if(key.isValid() && key.isReadable()){
//handle key reads
}
if(key.isValid() && key.isWritable()){
//handle key writes
}
continue;
}
if(key.isValid() && key.isAcceptable()){
Connection newConn = new Connection(transport, packetFactory, nextConnectionId);
try{
SelectionKey selectionKey = newConn.getConnection().handleAccept(selector, serverChannel.accept());
selectionKey.attach(newConn);
//send the client a connection message
nextConnectionId++;
}catch(IOException e){
newConn.close();
log.Error(e);
}
}
}
long time = System.nanoTime();
for(Connection c : connections){
if(c.getConnection().isTimedOut(time)){
c.close();
}
if(c.needsPing(time)) {
c.sendPing();
}
}
}catch(IOException e){
log.Error(e);
}
}
handleAccept()方法看起来像这样
public SelectionKey handleAccept(Selector selector, SocketChannel ch) {
try{
socketChannel = ch;
socketChannel.configureBlocking(false);
lastReadTime = System.nanoTime();
selectionKey = ch.register(selector, SelectionKey.OP_READ);
return selectionKey;
}catch(IOException e){
close();
}
}
handleAccept()方法是连接对象的一部分,它保存诸如连接socketChannel,selectionkey之类的信息。每当连接被读/写时,它们都会更新如下:
public void handleWrite(SelectionKey key) throws IOException {
socketChannel = (SocketChannel) key.channel();
this.selectionKey = key;
//do other stuff
}
最后,当我们需要向客户端写东西时,我们告诉socketChannel我们有兴趣写作,一旦我们完成写作,我们告诉它我们想要读。这可以通过访问连接选择键并将其interestOps()
设置为正确的字段来完成。
当我运行此代码时,服务器执行以下操作:
[2014/05/25 01:49:23] [INFO] [com.adammflax.net.ServerEndPoint] Started Server on port 9001
[2014/05/25 01:49:26] [INFO] [com.adammflax.net.ServerEndPoint] Added the connection com.adammflax.net.Connection@4838ddcc to the list of connections on the server
[2014/05/25 01:49:26] [INFO] [com.adammflax.net.ServerEndPoint] Client com.adammflax.net.Connection@4838ddcc connected to the server given a Id of 0
30
[CLIENT RECEIVES CONNECTION PACKET]
[2014/05/25 01:49:26] [DEBUG] [com.adammflax.net.ServerEndPoint] Received ping from client now replying
[CLIENT RECEIVES Ping PACKET]
然后然后服务器卡在selector.select()上(它总是超时并返回0)。任何人都可以向我解释为什么会这样。我知道这很多代码需要深入研究(缺少一些代码来尝试和浓缩这些代码)但是对此问题的任何帮助都会很惊人。
编辑 添加更多代码以显示我们如何检测ping是否需要发生(属于连接对象)
public boolean needsPing(long time) {
if(!isConnected()){
return false;
}
if(pingTime <= 0){
return false;
}
if(time <= lastPingSentTime + pingTime){
return false;
}
lastPingSentTime = System.nanoTime();
return true;
}
send ping将pingPacket对象转换为字节并调用send方法,该方法将字节添加到名为messages的messageQueue中。
public void send(byte[] message) {
messages.add(message);
selectionKey.interestOps(SelectionKey.OP_WRITE);
}
因为我们现在在OP_WRITE handleWrite
中最终会被调用
public void handleWrite(SelectionKey key) throws IOException {
socketChannel = (SocketChannel) key.channel();
if(socketChannel == null){
key.cancel();
throw new SocketException("Socket connection has closed!");
}
while(!messages.isEmpty()){
try{
writeBuffer = ByteBuffer.wrap(messages.peek());
if(socketChannel.write(writeBuffer) == 0){
break;
}
if (writeBuffer.remaining() > 0) {
break;
}
writeBuffer.compact();
messages.poll(); //everything went well so remove head
}catch(IOException e){
log.Error("failed to write : " + messages.peek() + " at " +
this.socketChannel.getRemoteAddress());
}
}
selectionKey.interestOps(SelectionKey.OP_READ);
}