【发布时间】:2014-11-06 14:04:13
【问题描述】:
我需要您对 Java NIO 包的建议。我在通过网络发送数据包时遇到延迟问题。原始代码实际上是我的port of the SFML book source code to Java,但在这里我将只向您展示一个最小的工作示例,其中重现了问题。尽管此代码确实包含 SFML 库中的一些片段(实际上是创建一个窗口和一个事件循环),但我相信这对问题没有影响。
这里我只展示部分代码,完整版可用here。
所以,程序有两个实体:服务器和客户端。如果您以服务器模式启动应用程序,则会创建一个 Server,开始侦听新的连接,并自动创建一个新的 Client 并尝试连接到该 Server。在客户端模式下,只会创建一个客户端并连接到服务器。
该应用程序还会创建一个新的基本 GUI 窗口并启动一个事件循环,一切都会在其中发生。
客户端向服务器发送数据包。它只通过记录接受的事实来处理它们。客户端可以发送两种类型的数据包:定期数据包(带有递增 ID)和事件数据包(应用程序对按下 SPACE 或 M 按钮作出反应)。
客户端发送数据包:
public void update(Time dt) throws IOException {
if (!isConnected) return;
if (tickClock.getElapsedTime().compareTo(Time.getSeconds(1.f / 20.f)) > 0) {
Packet intervalUpdatePacket = new Packet();
intervalUpdatePacket.append(PacketType.INTERVAL_UPDATE);
intervalUpdatePacket.append(intervalCounter++);
PacketReaderWriter.send(socketChannel, intervalUpdatePacket);
tickClock.restart();
}
}
public void handleEvent(Event event) throws IOException {
if (isConnected && (event.type == Event.Type.KEY_PRESSED)) {
KeyEvent keyEvent = event.asKeyEvent();
if (keyEvent.key == Keyboard.Key.SPACE) {
LOGGER.info("press SPACE");
Packet spacePacket = new Packet();
spacePacket.append(PacketType.SPACE_BUTTON);
PacketReaderWriter.send(socketChannel, spacePacket);
}
if (keyEvent.key == Keyboard.Key.M) {
LOGGER.info("press M");
Packet mPacket = new Packet();
mPacket.append(PacketType.M_BUTTON);
PacketReaderWriter.send(socketChannel, mPacket);
}
}
}
服务器接受数据包:
private void handleIncomingPackets() throws IOException {
readSelector.selectNow();
Set<SelectionKey> readKeys = readSelector.selectedKeys();
Iterator<SelectionKey> it = readKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
SocketChannel channel = (SocketChannel) key.channel();
Packet packet = null;
try {
packet = PacketReaderWriter.receive(channel);
} catch (NothingToReadException e) {
e.printStackTrace();
}
if (packet != null) {
// Interpret packet and react to it
handleIncomingPacket(packet, channel);
}
}
}
private void handleIncomingPacket(Packet packet, SocketChannel channel) {
PacketType packetType = (PacketType) packet.get();
switch (packetType) {
case INTERVAL_UPDATE:
int intervalId = (int) packet.get();
break;
case SPACE_BUTTON:
LOGGER.info("handling SPACE button");
break;
case M_BUTTON:
LOGGER.info("handling M button");
break;
}
}
这是一个PacketReaderWriter 对象:
package server;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class PacketReaderWriter {
private static final int PACKET_SIZE_LENGTH = 4;
private static final ByteBuffer packetSizeReadBuffer = ByteBuffer.allocate(PACKET_SIZE_LENGTH);
private static ByteBuffer clientReadBuffer;
private static byte[] encode(Packet packet) throws IOException {
try (
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)
) {
oos.writeObject(packet);
return baos.toByteArray();
}
}
private static Packet decode(byte[] encodedPacket) throws IOException, ClassNotFoundException {
try (ObjectInputStream oi = new ObjectInputStream(new ByteArrayInputStream(encodedPacket))) {
return (Packet) oi.readObject();
}
}
public static void send(SocketChannel channel, Packet packet) throws IOException {
byte[] encodedPacket = encode(packet);
ByteBuffer packetSizeBuffer = ByteBuffer.allocate(PACKET_SIZE_LENGTH).putInt(encodedPacket.length);
packetSizeBuffer.flip();
// Send packet size
channel.write(packetSizeBuffer);
// Send packet content
ByteBuffer packetBuffer = ByteBuffer.wrap(encodedPacket);
channel.write(packetBuffer);
}
public static Packet receive(SocketChannel channel) throws IOException, NothingToReadException {
int bytesRead;
// Read packet size
packetSizeReadBuffer.clear();
bytesRead = channel.read(packetSizeReadBuffer);
if (bytesRead == -1) {
channel.close();
throw new NothingToReadException();
}
if (bytesRead == 0) return null;
packetSizeReadBuffer.flip();
int packetSize = packetSizeReadBuffer.getInt();
// Read packet
clientReadBuffer = ByteBuffer.allocate(packetSize);
bytesRead = channel.read(clientReadBuffer);
if (bytesRead == -1) {
channel.close();
throw new NothingToReadException();
}
if (bytesRead == 0) return null;
clientReadBuffer.flip();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(clientReadBuffer.array(), 0, bytesRead);
clientReadBuffer.clear();
try {
return decode(baos.toByteArray());
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
这就是问题所在:在按下按钮(并从客户端发送相应的数据包)和在服务器上接受此数据包之间存在相当大的延迟。如果我在客户端模式下启动应用程序的新实例(简而言之,只需添加一个新客户端),延迟会变得更大。
我看不出为什么这些周期性数据包会产生如此多的网络负载,以至于其他数据包无法通过,但也许我只是遗漏了一些东西。在这里我不得不说我不是Java专家,所以不要太责怪我没有看到明显的东西:)
有人有什么想法吗?
谢谢!
【问题讨论】:
-
定义“相当大的延迟”,并说明您认为这会造成“如此多的网络负载”的原因。我想说你在这里创建了太多的对象,尤其是太多的
ByteBuffers。尝试在频道的整个生命周期中为每个频道使用一个,也许两个,一个用于阅读,一个用于写作。 -
当我按“SPACE”时,大约 2/3 秒后,我看到一条关于在服务器端接受包的消息。我会尽量减少缓冲区对象。
-
我们能看到
PacketReaderWriter吗? -
这是我问题中的最后一段代码。
-
因为没有工作代码(没有设置我们的小测试)来重现您的问题。这是一个猜测:这个
int packetSize = packetSizeReadBuffer.getInt();可能是一个问题,当你在这个地方bytesRead = channel.read(packetSizeReadBuffer);你读不到四个字节时。