Java ServerSocketChannel和SocketChannel(回调)

5

我正在学习Java,希望能够实现一个简单的联网四子棋游戏和聊天功能。

我想要我的网络逻辑是非阻塞的,经过研究后,我发现SocketChannel符合我的需求。

然而,仍然让我感到困惑的是SocketChannels中缺少回调函数,这与C#中的实现方式不同。

我的问题是:如何将接收到的数据传递到聊天或游戏窗口(JFrame)中?

希望能够得到一些指导。


我建议您使用多线程而不是非阻塞I/O。 - Steve Emmerson
1个回答

15
您需要使用选择器。首先创建一个选择器来接收事件:
Selector selector = Selector.open()

然后,您需要将ServerSocketChannel注册到选择器中:

SelectionKey acceptKey = server.register(selector, SelectionKey.OP_ACCEPT);

接下来,您需要使用选择器来处理事件(可以将其视为过程的“回调”部分):

while(true){
  //how many channel keys are available
  int available = selector.select(); 
  //select is blocking, but should only return if available is >0, this is more of a sanity check
  if(available == 0) continue;

  Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
  while(keys.hasNext()){
    SelectionKey key = keys.next();
    keys.remove();
    //someone is trying to connect to the server socket
    if(key.isAcceptable())  doAccept(key); 
    //someone is sending us data
    else if(key.isReadable()) doRead(key); 
    //we are trying to (and can) send data
    else if(key.isWritable()) doWrite(key);
}

肉在doAccept()、doRead()和doWrite()中。对于接受键,选择键将包含创建新套接字所需的信息。
doAccept(SelectionKey key){

//create the new socket
SocketChannel socket = ((ServerSocketChannel)key.channel()).accept(); 
//make it non-blocking as well
socket.configureBlocking(false);

...
//here you would likely have some code to init your game objects / communication protocol, etc. and generate an identifier object (used below).
//and be able to find the socket created above
...

//Since it is non blocking it needs a selector as well, and we register for both read and write events
SelectionKey socketKey = socket.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);
// so we can identify the events as they come in
socketKey.attach(someSocketIndentifier);
}

最后一行代码向键中添加了一些对象,以便将从选择器接收到的事件归属于某个连接(例如,它可能是游戏中的玩家)。现在你可以接受新的连接,只需要读取和写入即可。
doRead(SelectionKey key){
  //here we retrieve the key we attached earlier, so we now what to do / wheer the data is coming from
  MyIdentifierType myIdentifier = (MyIdentifierType)key.attachment();
  //This is then used to get back to the SocketChannel and Read the Data
  myIdentifier.readTheData();
}

同样的,对于写入操作。
doWrite(SelectionKey key){
  //here we retrieve the key we attached earlier, so we now what to do / wheer the data is coming from
  MyIdentifierType myIdentifier = (MyIdentifierType)key.attachment();
  //This is then used to get back to the SocketChannel and Read the Data
  myIdentifier.getSocketHandler().writePendingData();
}

阅读操作相对简单,只需创建一个 ByteBuffer 对象,然后调用 SocketChannel 的 read(ByteBuffer) 方法(或其变体之一),将通道上准备好的数据读取直至为空。

写入操作有些棘手,因为通常需要缓冲要写入的数据直到接收到写入事件:

class MyNetworkClass{
  ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
  SocketChannel commchannel; //from the server accept processing

  ...

  public void write(byte[] data){
    //here the class writeBuffer object is filled with the data
    //but it isn't actually sent over the socket
    ...
  }

  public void writePendingData(){
    //here actually write the data to the socket
    commchannel.write(writeBuffer);
  }
}

请注意,在类中管理缓冲区时需要适当的代码,以防它变满,或在写入挂起方法中适当地修改它,如果没有将缓冲区中的所有数据写出到套接字,则需要进行修改,还有可能在过程中抛出各种异常。希望这能帮助你入门。

这非常有用,我现在更好地理解了这个过程。 - iTEgg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接