在 Java 游戏服务器开发中,网络通讯是核心组成部分,它主要负责客户端与服务器之间的数据交换。
Java 提供了多种网络编程 API,适用于不同的应用场景:
在前面提供的示例中,我们使用了 Java AIO 实现游戏服务器的网络通讯。下面详细解释其工作原理:
创建线程池和服务器通道:
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
group = AsynchronousChannelGroup.withThreadPool(executor);
serverChannel = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(port));
接受客户端连接:
serverChannel.accept(null, new CompletionHandler() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
// 处理新连接
acceptConnections(); // 继续接受下一个连接
}
});
读取数据:
channel.read(readBuffer, null, new CompletionHandler() {
@Override
public void completed(Integer bytesRead, Void attachment) {
// 处理读取到的数据
read(); // 继续读取下一次数据
}
});
写入数据:
channel.write(writeBuffer, null, new CompletionHandler() {
@Override
public void completed(Integer bytesWritten, Void attachment) {
// 继续写入剩余数据
if (writeBuffer.hasRemaining()) {
channel.write(writeBuffer, null, this);
}
}
});
Java AIO 的核心是异步回调机制:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class GameClient {
private final String host;
private final int port;
private AsynchronousSocketChannel channel;
private final ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private final Scanner scanner = new Scanner(System.in);
public GameClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws IOException {
// 打开客户端通道
channel = AsynchronousSocketChannel.open();
// 连接到服务器
channel.connect(new InetSocketAddress(host, port), null, new CompletionHandler() {
@Override
public void completed(Void result, Void attachment) {
System.out.println("已连接到服务器: " + host + ":" + port);
// 开始读取服务器消息
read();
// 启动用户输入处理线程
new Thread(GameClient.this::handleUserInput).start();
}
@Override
public void failed(Throwable exc, Void attachment) {
System.err.println("连接服务器失败: " + exc.getMessage());
}
});
}
private void read() {
readBuffer.clear();
channel.read(readBuffer, null, new CompletionHandler() {
@Override
public void completed(Integer bytesRead, Void attachment) {
if (bytesRead == -1) {
// 服务器关闭了连接
System.out.println("服务器断开连接");
close();
return;
}
readBuffer.flip();
byte[] data = new byte[bytesRead];
readBuffer.get(data);
String message = new String(data, StandardCharsets.UTF_8);
// 显示服务器消息
System.out.print("\r" + message);
System.out.print("> ");
// 继续读取
read();
}
@Override
public void failed(Throwable exc, Void attachment) {
System.err.println("读取消息失败: " + exc.getMessage());
close();
}
});
}
private void handleUserInput() {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
try {
while (channel.isOpen()) {
System.out.print("> ");
String message = reader.readLine();
if (message == null || message.equalsIgnoreCase("/exit")) {
close();
break;
}
sendMessage(message);
}
} catch (IOException e) {
System.err.println("读取用户输入失败: " + e.getMessage());
close();
}
}
private void sendMessage(String message) {
ByteBuffer buffer = ByteBuffer.wrap((message + "\n").getBytes(StandardCharsets.UTF_8));
channel.write(buffer, null, new CompletionHandler() {
@Override
public void completed(Integer bytesWritten, Void attachment) {
// 继续写入剩余数据,如果有的话
if (buffer.hasRemaining()) {
channel.write(buffer, null, this);
}
}
@Override
public void failed(Throwable exc, Void attachment) {
System.err.println("发送消息失败: " + exc.getMessage());
close();
}
});
}
public void close() {
try {
if (channel.isOpen()) {
channel.close();
System.out.println("客户端已关闭");
}
} catch (IOException e) {
System.err.println("关闭客户端失败: " + e.getMessage());
}
}
public static void main(String[] args) {
GameClient client = new GameClient("localhost", 8080);
try {
client.start();
// 保持主线程运行
Thread.currentThread().join();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
client.close();
}
}
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class GameServer {
private final int port;
private AsynchronousChannelGroup group;
private AsynchronousServerSocketChannel serverChannel;
private final Map sessions = new HashMap<>();
public GameServer(int port) {
this.port = port;
}
public void start() throws IOException {
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
group = AsynchronousChannelGroup.withThreadPool(executor);
// 打开服务器通道
serverChannel = AsynchronousServerSocketChannel.open(group)
.bind(new InetSocketAddress(port));
System.out.println("游戏服务器启动,监听端口: " + port);
// 开始接受连接
acceptConnections();
}
private void acceptConnections() {
serverChannel.accept(null, new CompletionHandler() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
// 继续接受下一个连接
acceptConnections();
// 处理新连接
handleNewConnection(client);
}
@Override
public void failed(Throwable exc, Void attachment) {
System.err.println("接受连接失败: " + exc.getMessage());
}
});
}
private void handleNewConnection(AsynchronousSocketChannel client) {
try {
String sessionId = UUID.randomUUID().toString();
PlayerSession session = new PlayerSession(sessionId, client, this);
// 存储会话
sessions.put(sessionId, session);
System.out.println("新玩家连接: " + sessionId + " 来自 " + client.getRemoteAddress());
// 开始读取客户端消息
session.read();
// 发送欢迎消息
session.send("欢迎加入游戏服务器! 您的ID: " + sessionId);
} catch (IOException e) {
System.err.println("处理新连接失败: " + e.getMessage());
try {
client.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
public void broadcast(String message, String excludeSessionId) {
for (PlayerSession session : sessions.values()) {
if (!session.getSessionId().equals(excludeSessionId)) {
session.send(message);
}
}
}
public void removeSession(String sessionId) {
sessions.remove(sessionId);
System.out.println("玩家断开连接: " + sessionId);
}
public void stop() {
try {
// 关闭所有会话
for (PlayerSession session : sessions.values()) {
session.close();
}
// 关闭服务器通道和组
if (serverChannel != null && serverChannel.isOpen()) {
serverChannel.close();
}
if (group != null && !group.isShutdown()) {
group.shutdownNow();
}
System.out.println("游戏服务器已停止");
} catch (IOException e) {
System.err.println("停止服务器失败: " + e.getMessage());
}
}
public static void main(String[] args) {
GameServer server = new GameServer(8080);
try {
server.start();
// 让服务器保持运行
Thread.currentThread().join();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
server.stop();
}
}
}
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;
public class PlayerSession {
private final String sessionId;
private final AsynchronousSocketChannel channel;
private final GameServer server;
private final ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private final ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
// 玩家状态
private String username;
private int x, y;
private boolean isLoggedIn = false;
public PlayerSession(String sessionId, AsynchronousSocketChannel channel, GameServer server) {
this.sessionId = sessionId;
this.channel = channel;
this.server = server;
}
public String getSessionId() {
return sessionId;
}
public void read() {
readBuffer.clear();
channel.read(readBuffer, null, new CompletionHandler() {
@Override
public void completed(Integer bytesRead, Void attachment) {
if (bytesRead == -1) {
// 客户端关闭了连接
close();
return;
}
readBuffer.flip();
byte[] data = new byte[bytesRead];
readBuffer.get(data);
String message = new String(data, StandardCharsets.UTF_8).trim();
// 处理消息
handleMessage(message);
// 继续读取
read();
}
@Override
public void failed(Throwable exc, Void attachment) {
System.err.println("读取消息失败: " + exc.getMessage());
close();
}
});
}
private void handleMessage(String message) {
System.out.println("收到来自 " + sessionId + " 的消息: " + message);
// 简单的命令处理
if (message.startsWith("/login ")) {
handleLogin(message.substring(7));
} else if (message.startsWith("/move ")) {
handleMove(message.substring(6));
} else if (message.equals("/logout")) {
handleLogout();
} else if (message.equals("/players")) {
sendPlayerList();
} else {
// 广播消息给其他玩家
if (isLoggedIn) {
server.broadcast("[" + username + "] " + message, sessionId);
} else {
send("请先登录 /login <用户名>");
}
}
}
private void handleLogin(String username) {
if (isLoggedIn) {
send("您已经登录为 " + this.username);
return;
}
this.username = username;
this.isLoggedIn = true;
this.x = 0;
this.y = 0;
send("登录成功,欢迎 " + username);
server.broadcast(username + " 加入了游戏", sessionId);
}
private void handleMove(String direction) {
if (!isLoggedIn) {
send("请先登录 /login <用户名>");
return;
}
switch (direction.toLowerCase()) {
case "up": y--; break;
case "down": y++; break;
case "left": x--; break;
case "right": x++; break;
default:
send("无效的移动方向: " + direction);
return;
}
send("您移动到了位置 (" + x + ", " + y + ")");
server.broadcast(username + " 移动到了位置 (" + x + ", " + y + ")", sessionId);
}
private void handleLogout() {
if (!isLoggedIn) {
send("您尚未登录");
return;
}
server.broadcast(username + " 离开了游戏", sessionId);
this.isLoggedIn = false;
send("您已登出");
}
private void sendPlayerList() {
StringBuilder builder = new StringBuilder("在线玩家列表:\n");
// 实际应用中应该遍历所有玩家并添加到列表
builder.append(username).append(" (").append(x).append(", ").append(y).append(")\n");
send(builder.toString());
}
public void send(String message) {
writeBuffer.clear();
writeBuffer.put((message + "\n").getBytes(StandardCharsets.UTF_8));
writeBuffer.flip();
channel.write(writeBuffer, null, new CompletionHandler() {
@Override
public void completed(Integer bytesWritten, Void attachment) {
// 继续写入剩余数据,如果有的话
if (writeBuffer.hasRemaining()) {
channel.write(writeBuffer, null, this);
}
}
@Override
public void failed(Throwable exc, Void attachment) {
System.err.println("发送消息失败: " + exc.getMessage());
close();
}
});
}
public void close() {
try {
if (channel.isOpen()) {
channel.close();
server.removeSession(sessionId);
}
} catch (IOException e) {
System.err.println("关闭会话失败: " + e.getMessage());
}
}
}
虽然AIO 提供了高效的异步非阻塞编程模型,适合开发高性能的游戏服务器。但是手撸Java 游戏服务器的网络通讯相对比较复杂,需要综合考虑性能、可靠性、安全性等多个方面。上面的代码是最简单的实现
在实际开发中,通常会使用成熟的网络框架如 Netty,以简化开发流程并提高系统稳定性。