Java网络游戏服务器系统开发与架构设计研究
本文还有配套的精品资源,点击获取
简介:Java网络游戏服务器系统基于Java技术构建,旨在支持大量玩家在线交互。系统开发涵盖网络编程、多线程处理、数据库操作、并发控制、负载均衡等关键技术。通过使用Socket编程实现客户端与服务器通信,结合多线程模型提升并发处理能力,利用JDBC进行游戏数据管理,并通过分布式架构和负载均衡策略增强系统扩展性与稳定性。此外,系统还涉及NIO性能优化、安全性机制、异常处理与日志记录等内容,全面覆盖游戏服务器从设计到部署的关键环节。
1. Java网络游戏服务器系统概述
网络游戏服务器作为游戏世界的“大脑”,负责管理玩家连接、数据同步、逻辑运算及事件处理等核心功能。随着在线玩家数量的激增,服务器系统的稳定性、扩展性与性能优化成为开发过程中的关键挑战。Java凭借其跨平台性、成熟的多线程支持以及丰富的网络编程库,成为构建高性能游戏服务器的首选语言之一。
本章将从网络游戏服务器的基本职责入手,逐步解析其典型架构组成,包括通信层、逻辑层、数据层与分布式协调层。同时,我们将对比传统单体服务器与现代分布式服务器的差异,帮助读者建立对服务器系统整体架构的认知,为后续深入学习打下坚实基础。
2. Socket网络通信编程实现
网络通信是游戏服务器开发的基石,而Java提供了丰富的Socket编程支持,使得开发者能够快速构建稳定、高效的通信模型。本章将从Socket通信的基本原理出发,逐步深入讲解Java中基于Socket的网络通信实现方式,并结合多客户端连接处理、自定义协议设计等高级实践,帮助读者构建完整的通信系统。
2.1 Socket通信的基本原理
Socket是网络通信的核心抽象机制,它为不同主机上的进程提供了端到端的数据传输能力。理解Socket通信原理是开发网络应用的前提,尤其在网络游戏服务器开发中,它直接决定了通信的稳定性、效率与扩展性。
2.1.1 网络通信协议与Socket接口的关系
网络通信协议(如TCP/IP)定义了数据在网络中的传输规则,而Socket接口则是操作系统为应用程序提供的访问网络协议栈的编程接口。通过Socket,应用程序可以像操作文件一样读写网络数据流。
在TCP/IP模型中,Socket接口位于传输层与应用层之间,屏蔽了底层网络协议的复杂性,使得开发者只需关注连接建立、数据发送与接收等逻辑。
网络协议栈与Socket接口的关系图(mermaid流程图):
graph TD
A[应用层] -->|HTTP/FTP/自定义协议| B(传输层)
B -->|TCP/UDP| C[Socket接口]
C -->|IP地址+端口| D[网络层]
D -->|路由寻址| E[链路层]
协议栈各层功能说明:
| 层级 | 功能描述 |
|---|---|
| 应用层 | 提供用户接口,定义数据格式(如HTTP请求、自定义游戏协议) |
| 传输层 | 负责端到端通信(TCP可靠传输、UDP快速传输) |
| 网络层 | 实现主机间通信,负责IP寻址与路由 |
| 链路层 | 处理物理介质上的数据传输(如以太网帧、WiFi帧) |
2.1.2 TCP与UDP协议的对比与适用场景
TCP(传输控制协议)和UDP(用户数据报协议)是传输层的两大核心协议。它们在网络通信中各有优势,适用于不同的应用场景。
TCP与UDP特性对比表:
| 特性 | TCP | UDP |
|---|---|---|
| 是否可靠 | 是 | 否 |
| 是否连接 | 是(三次握手) | 否 |
| 数据顺序 | 保证顺序 | 不保证 |
| 传输效率 | 较低(握手、确认、重传) | 高 |
| 适用场景 | 精确数据传输(如登录、交易) | 实时性要求高(如战斗、移动同步) |
在游戏服务器中的典型使用:
- TCP :用于玩家登录、数据同步、物品交易等对数据完整性要求高的操作。
- UDP :用于玩家移动同步、战斗状态更新等对实时性要求高但允许少量丢包的场景。
2.2 Java中Socket编程的实现
Java的 java.net 包中提供了丰富的Socket编程类,支持TCP和UDP通信。本节将通过具体代码示例,展示如何使用Java构建一个基本的Socket通信模型。
2.2.1 服务器端Socket的创建与监听
Java中使用 ServerSocket 类来监听客户端连接请求。以下是一个简单的TCP服务器端代码示例:
import java.io.*;
import java.net.*;
public class TCPServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888); // 监听8888端口
System.out.println("服务器已启动,等待客户端连接...");
while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待客户端连接
System.out.println("客户端已连接:" + socket.getInetAddress());
// 启动线程处理客户端通信
new ClientHandler(socket).start();
}
}
}
class ClientHandler extends Thread {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("收到消息:" + inputLine);
out.println("服务器回复:" + inputLine);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
代码逻辑分析:
-
ServerSocket绑定端口8888并监听客户端连接。 -
accept()方法阻塞等待客户端连接,一旦连接成功返回一个Socket对象。 - 为每个客户端创建一个独立线程进行处理,避免阻塞主线程。
- 使用
BufferedReader读取客户端发送的数据,使用PrintWriter发送响应。
参数说明:
-
ServerSocket(8888):监听本地8888端口。 -
socket.getInputStream():获取客户端输入流。 -
socket.getOutputStream():获取客户端输出流。
2.2.2 客户端Socket的连接与数据交互
客户端使用 Socket 类连接服务器,并通过输入输出流进行数据交互:
import java.io.*;
import java.net.*;
public class TCPClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888); // 连接本地服务器
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
String userInput;
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput); // 发送用户输入
System.out.println("服务器回复:" + in.readLine()); // 接收服务器响应
}
socket.close();
}
}
代码逻辑分析:
- 创建
Socket连接服务器IP地址127.0.0.1和端口8888。 - 获取输入输出流,分别用于接收服务器响应和发送客户端请求。
- 通过标准输入获取用户输入,发送至服务器并接收回复。
2.2.3 数据的发送与接收处理
在实际开发中,数据的发送与接收需要考虑编码格式、粘包问题、数据完整性等。Java中可以使用 ObjectOutputStream 和 ObjectInputStream 来传输对象,也可以使用 ByteBuffer 进行更底层的字节操作。
使用 ObjectOutputStream 发送对象示例:
// 服务器端
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
User user = new User("Alice", 25);
oos.writeObject(user);
// 客户端
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
User receivedUser = (User) ois.readObject();
System.out.println(receivedUser.getName());
数据粘包问题解决思路:
- 使用固定长度消息头标识消息体长度。
- 使用分隔符标记消息边界(如换行符
)。 - 使用
ByteBuffer解析数据流。
2.3 高级Socket编程实践
在实际开发中,仅实现基础的Socket通信是不够的。面对多个客户端并发连接、数据协议定制等需求,需要引入更高级的编程实践。
2.3.1 多客户端连接的处理机制
在高并发场景下,传统的每个客户端连接一个线程的方式(Thread-per-Connection)存在资源浪费问题。为此,可以采用线程池(ThreadPool)来管理连接线程。
使用线程池处理客户端连接示例:
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class ThreadPoolTCPServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
ExecutorService executor = Executors.newFixedThreadPool(10); // 创建固定大小线程池
while (true) {
Socket socket = serverSocket.accept();
executor.submit(new ClientHandler(socket)); // 提交任务给线程池
}
}
}
class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("收到消息:" + inputLine);
out.println("服务器回复:" + inputLine);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
线程池优势:
- 资源复用:避免频繁创建和销毁线程。
- 提高响应速度:任务提交后可立即执行。
- 控制并发数:防止资源耗尽。
2.3.2 自定义通信协议的设计与实现
在网络游戏通信中,为了提高数据传输效率与安全性,通常需要设计自定义通信协议。常见的协议结构包括:
- 消息头(Header) :标识消息长度、类型、序列号等元信息。
- 消息体(Body) :实际数据内容。
自定义协议格式示例:
public class GameMessage {
private int length; // 消息总长度
private short type; // 消息类型(如登录、移动、攻击)
private int sequence; // 序列号
private byte[] data; // 消息数据
// 构造函数、getter/setter等略
}
使用 ByteBuffer 封装协议数据:
public byte[] toBytes() {
ByteBuffer buffer = ByteBuffer.allocate(4 + 2 + 4 + data.length);
buffer.putInt(length);
buffer.putShort(type);
buffer.putInt(sequence);
buffer.put(data);
return buffer.array();
}
使用 DataInputStream 解析协议数据:
DataInputStream dis = new DataInputStream(socket.getInputStream());
int length = dis.readInt();
short type = dis.readShort();
int sequence = dis.readInt();
byte[] data = new byte[length - 4 - 2 - 4];
dis.readFully(data);
协议设计优势:
- 结构清晰 :易于扩展与维护。
- 高效传输 :减少网络带宽消耗。
- 兼容性好 :便于不同语言客户端对接。
通过本章的学习,我们从Socket通信的基本原理出发,深入探讨了Java中Socket编程的实现方式,并结合多客户端连接处理、自定义协议设计等高级实践,为构建高性能、可扩展的游戏服务器通信系统打下了坚实基础。在后续章节中,我们将进一步探讨多线程模型与并发请求处理,提升服务器的并发能力与稳定性。
3. 多线程模型与并发请求处理
在现代网络游戏服务器中,高并发是必须面对的核心挑战之一。面对成千上万的玩家同时在线,服务器需要高效地处理大量并发请求,确保每个玩家的操作都能被及时响应。Java 提供了强大的多线程机制,使得开发者能够构建高性能、可扩展的服务器系统。本章将深入探讨 Java 多线程的基本概念、线程池管理机制以及并发请求的处理模型,并通过实际代码示例展示如何构建一个高并发的服务器架构。
3.1 Java多线程基础
Java 的多线程机制是其并发编程的核心基础之一。通过多线程,程序可以同时执行多个任务,从而提高资源利用率和响应速度。理解线程的创建、启动方式以及其状态转换机制,是构建高性能服务器的第一步。
3.1.1 线程的创建与启动方式
Java 中创建线程主要有两种方式:
- 继承 Thread 类
- 实现 Runnable 接口
示例代码:通过实现 Runnable 接口创建线程
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 正在运行");
}
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable(), "线程-A");
Thread thread2 = new Thread(new MyRunnable(), "线程-B");
thread1.start();
thread2.start();
}
}
代码逻辑分析:
-
MyRunnable实现了Runnable接口,并重写了run()方法。 -
Thread构造函数接收一个Runnable实例和线程名称作为参数。 -
start()方法用于启动线程,JVM 会自动调用该线程的run()方法。 -
Thread.currentThread().getName()获取当前线程的名称,用于区分不同线程。
参数说明:
-
Runnable是一个函数式接口,只包含一个run()方法。 -
start()方法启动线程,而run()方法只是普通方法调用,不会创建新线程。
线程创建方式对比:
| 创建方式 | 是否支持多继承 | 线程任务与类职责分离 | 是否支持 Lambda 表达式 |
|---|---|---|---|
| 继承 Thread | 否 | 否 | 否 |
| 实现 Runnable | 是 | 是 | 是 |
推荐使用
Runnable接口来创建线程,因为它更灵活,支持 Lambda 表达式,也更符合面向对象设计原则。
3.1.2 线程状态与调度机制
线程在其生命周期中会经历多个状态,这些状态由 JVM 管理。理解线程状态有助于优化并发性能。
Java 线程的生命周期状态:
| 状态 | 说明 |
|---|---|
| NEW | 线程刚被创建,尚未启动 |
| RUNNABLE | 线程正在 JVM 中执行,可能正在等待操作系统资源(如 CPU) |
| BLOCKED | 线程因等待锁而阻塞 |
| WAITING | 线程无限期等待其他线程执行特定操作 |
| TIMED_WAITING | 线程在指定时间内等待其他线程操作 |
| TERMINATED | 线程已完成执行 |
线程状态转换图(Mermaid 流程图):
graph LR
A[NEW] --> B[RUNNABLE]
B --> C[BLOCKED]
B --> D[WAITING]
B --> E[TIMED_WAITING]
D --> B
E --> B
C --> B
B --> F[TERMINATED]
线程调度机制:
Java 的线程调度器负责将线程分配给 CPU 执行,其调度策略由 JVM 实现决定。常见的调度方式包括:
- 抢占式调度 :优先级高的线程可以打断低优先级线程的执行。
- 时间片轮转 :每个线程分配固定的时间片,轮流执行。
线程优先级范围为 1~10 ,默认为 5 ,可以通过 setPriority() 设置。
3.2 线程池与任务调度
在高并发场景下,频繁创建和销毁线程会带来显著的性能开销。Java 提供了线程池机制来复用线程,提高资源利用率和任务响应速度。
3.2.1 Executor框架的使用
Java 的 java.util.concurrent.Executor 框架提供了一套灵活的线程池实现,主要包括:
-
ExecutorService:用于管理线程池和提交任务。 -
Executors:工厂类,提供常见线程池的创建方法。
示例代码:使用固定大小线程池处理任务
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小为3的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交多个任务
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executorService.submit(() -> {
System.out.println("任务 " + taskId + " 正在由线程 " + Thread.currentThread().getName() + " 执行");
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executorService.shutdown();
}
}
代码逻辑分析:
-
Executors.newFixedThreadPool(3)创建一个固定大小为 3 的线程池。 -
submit()方法用于提交任务,支持Runnable或Callable。 - 使用 Lambda 表达式简化任务定义。
-
shutdown()方法关闭线程池,等待所有任务执行完毕。
线程池类型对比:
| 线程池类型 | 适用场景 | 是否复用线程 |
|---|---|---|
| newFixedThreadPool | 固定线程数,适用于任务量稳定 | 是 |
| newCachedThreadPool | 动态线程数,适用于任务量波动 | 是 |
| newSingleThreadExecutor | 单线程顺序执行任务 | 是 |
| newScheduledThreadPool | 支持定时任务和周期任务 | 是 |
3.2.2 线程池的配置与性能优化
线程池的性能优化主要体现在以下几个方面:
- 核心线程数与最大线程数 :根据 CPU 核心数和任务类型合理设置。
- 任务队列容量 :控制等待任务的数量,避免内存溢出。
- 拒绝策略 :当任务队列满时如何处理新任务(如抛出异常、丢弃等)。
示例代码:自定义线程池配置
import java.util.concurrent.*;
public class CustomThreadPool {
public static void main(String[] args) {
int corePoolSize = 4;
int maxPoolSize = 8;
long keepAliveTime = 60;
BlockingQueue queue = new LinkedBlockingQueue<>(100);
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime, TimeUnit.SECONDS,
queue,
handler
);
for (int i = 0; i < 20; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("执行任务 " + taskId + ",当前线程:" + Thread.currentThread().getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executor.shutdown();
}
}
代码逻辑分析:
- 使用
ThreadPoolExecutor自定义线程池。 -
corePoolSize和maxPoolSize分别设置核心和最大线程数。 -
keepAliveTime控制非核心线程空闲时间。 -
queue为任务队列,用于暂存等待执行的任务。 -
handler定义任务被拒绝时的处理策略。
线程池配置建议:
| 配置项 | 推荐值说明 |
|---|---|
| corePoolSize | 通常设为 CPU 核心数(Runtime.getRuntime().availableProcessors()) |
| maxPoolSize | 一般设为 corePoolSize 的 2~3 倍 |
| keepAliveTime | 60秒左右,避免线程长时间占用资源 |
| BlockingQueue | 推荐使用 LinkedBlockingQueue 或 ArrayBlockingQueue |
| RejectedHandler | CallerRunsPolicy(由调用线程执行)或 DiscardPolicy(丢弃) |
3.3 并发请求的处理模型
在网络游戏服务器中,如何高效地处理并发请求是性能优化的关键。本节将介绍请求队列的设计与实现,以及工作线程模型的优化策略。
3.3.1 请求队列的设计与实现
请求队列用于暂存客户端发送的请求数据,防止线程过载,提升系统的稳定性。
示例代码:基于 BlockingQueue 的请求队列
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class RequestQueue {
private BlockingQueue queue = new LinkedBlockingQueue<>(100);
public void addRequest(String request) {
try {
queue.put(request); // 阻塞式添加
System.out.println("请求已加入队列:" + request);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public String takeRequest() {
try {
return queue.take(); // 阻塞式取出
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
}
工作线程消费队列:
public class WorkerThread extends Thread {
private RequestQueue requestQueue;
public WorkerThread(RequestQueue requestQueue, String name) {
super(name);
this.requestQueue = requestQueue;
}
@Override
public void run() {
while (true) {
String request = requestQueue.takeRequest();
if (request != null) {
System.out.println(getName() + " 正在处理请求:" + request);
// 模拟业务处理
try {
Thread.sleep(300);
} catch (InterruptedException e) {
break;
}
}
}
}
}
主函数启动线程:
public class Main {
public static void main(String[] args) {
RequestQueue queue = new RequestQueue();
for (int i = 1; i <= 3; i++) {
new WorkerThread(queue, "Worker-" + i).start();
}
// 模拟客户端请求
for (int i = 1; i <= 10; i++) {
final int requestId = i;
new Thread(() -> {
queue.addRequest("请求-" + requestId);
}).start();
}
}
}
代码逻辑分析:
-
RequestQueue使用BlockingQueue来实现线程安全的请求队列。 -
WorkerThread持续从队列中取出请求并处理。 - 多个客户端线程并发提交请求,线程池中的工作线程按顺序处理。
请求队列优势:
| 优势点 | 描述 |
|---|---|
| 资源控制 | 避免线程爆炸,控制并发任务数量 |
| 异步处理 | 客户端无需等待处理完成,提升响应速度 |
| 负载均衡 | 多个工作线程协同处理请求,提高吞吐量 |
3.3.2 工作线程模型的优化策略
在高并发环境下,线程模型的优化可以从以下几个方面入手:
- 使用线程局部变量(ThreadLocal)减少锁竞争
- 避免线程阻塞,提升 CPU 利用率
- 合理设置线程数量,避免资源争抢
示例代码:使用 ThreadLocal 提升线程本地变量访问效率
public class ThreadLocalExample {
private static ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Runnable task = () -> {
threadLocal.set((int) (Math.random() * 100));
System.out.println(Thread.currentThread().getName() + " 的本地值:" + threadLocal.get());
};
new Thread(task, "线程-A").start();
new Thread(task, "线程-B").start();
}
}
代码逻辑分析:
-
ThreadLocal为每个线程提供独立的变量副本,避免多线程间的竞争。 - 在网络游戏服务器中,常用于存储线程上下文、Session、用户状态等信息。
优化策略总结:
| 优化策略 | 说明 |
|---|---|
| 使用 ThreadLocal | 减少线程间共享变量的锁竞争 |
| 避免线程阻塞 | 使用非阻塞 IO、异步处理避免线程挂起 |
| 动态调整线程数量 | 根据负载动态增加或减少线程数 |
| 使用 NIO 模型 | 替代传统的 BIO 模型,提高 IO 处理效率 |
本章内容通过理论与实践相结合,深入讲解了 Java 多线程模型在网络游戏服务器中的应用,从线程创建到线程池管理,再到并发请求的处理模型,为后续章节中构建完整的高并发服务器系统奠定了坚实的基础。
4. Java并发控制机制(synchronized、ReentrantLock)
在高并发场景中,多个线程对共享资源的访问可能引发数据不一致、竞态条件甚至死锁等严重问题。Java提供了多种并发控制机制来保障线程安全,其中最常用的是 synchronized 关键字和 ReentrantLock 类。本章将从基础原理、使用方式到高级应用逐步深入,帮助读者理解并发控制的核心机制,并通过代码示例展示如何在实际项目中合理使用。
4.1 synchronized关键字详解
synchronized 是Java中最早的同步机制之一,它能够确保同一时刻只有一个线程执行某段代码或某个方法,从而避免数据竞争。
4.1.1 方法同步与代码块同步
方法同步
当 synchronized 修饰一个方法时,该方法在被调用时会自动获取对象锁(如果是静态方法,则获取类锁)。例如:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
逐行分析 :
-public synchronized void increment():声明该方法为同步方法,调用时自动加锁。
-count++:为共享变量,必须在同步块中执行,以防止多线程同时修改造成数据不一致。
适用场景 :适用于对整个方法的操作都需要线程安全的简单对象。
代码块同步
若只需对部分代码进行同步,可以使用同步代码块:
public class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
}
逐行分析 :
-synchronized (lock):指定一个对象作为锁,线程进入该代码块前需获取该锁。
-count++:仅在该代码块内同步操作,减少锁的粒度,提高并发性能。
优势 :可以指定锁对象,实现更灵活的控制。
| 特性 | 方法同步 | 代码块同步 |
|---|---|---|
| 锁对象 | this(实例方法)或类(静态方法) | 可自定义任意对象 |
| 粒度 | 粗 | 细 |
| 灵活性 | 低 | 高 |
| 适用性 | 简单同步 | 复杂场景下的锁控制 |
4.1.2 同步锁的获取与释放机制
Java中每个对象都有一个内置锁(monitor lock),当线程进入 synchronized 块或方法时,会自动获取该锁,执行完毕后释放。如果锁已被其他线程持有,则当前线程将被阻塞直到锁可用。
锁的重入机制
Java的内置锁支持重入(reentrant),即一个线程可以多次获取同一个锁而不会死锁。例如:
public class ReentrantTest {
public synchronized void methodA() {
System.out.println("methodA");
methodB();
}
public synchronized void methodB() {
System.out.println("methodB");
}
}
解释 :
- 当线程调用methodA()时获取锁;
- 在methodA()内部调用methodB()时,由于是同一线程,允许再次获取该锁,不会造成死锁。
锁的释放时机
- 正常退出同步块或方法时自动释放;
- 抛出异常也会释放锁;
- 不能手动释放。
4.2 ReentrantLock与Condition机制
相较于 synchronized , ReentrantLock 提供了更灵活的锁机制,支持尝试锁、超时锁、公平锁等特性,是更高级的并发控制工具。
4.2.1 可重入锁的特性与优势
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
逐行分析 :
-lock.lock():显式加锁;
-try-finally:确保即使发生异常,锁也能被释放;
-count++:线程安全地修改共享变量。
ReentrantLock 的主要特性
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 是否可尝试获取锁 | 否 | 是( tryLock() ) |
| 是否可设置超时 | 否 | 是( tryLock(timeout, unit) ) |
| 是否支持公平锁 | 否 | 是(构造函数传入 true ) |
| 是否可中断 | 否 | 是( lockInterruptibly() ) |
| 是否支持多个条件变量 | 否 | 是(配合 Condition ) |
优势总结
- 更细粒度的控制;
- 更高的灵活性和可扩展性;
- 支持高级并发模式(如生产者-消费者模型)。
4.2.2 Condition的使用与生产者-消费者模型
Condition 是与 ReentrantLock 配合使用的条件变量,用于实现线程等待与通知机制。
示例:生产者-消费者模型
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private Object[] items = new Object[10];
private int putIndex, takeIndex, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 等待缓冲区不满
}
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0;
++count;
notEmpty.signal(); // 通知消费者可以取数据了
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 等待缓冲区不为空
}
Object x = items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
--count;
notFull.signal(); // 通知生产者可以继续生产
return x;
} finally {
lock.unlock();
}
}
}
逐行分析 :
-lock.newCondition():创建两个条件变量;
-notFull.await():当缓冲区满时,生产者等待;
-notEmpty.signal():通知消费者有数据可取;
-notEmpty.await():消费者等待数据;
-notFull.signal():通知生产者可以继续生产。
线程交互流程图(mermaid)
sequenceDiagram
participant Producer
participant Consumer
participant Lock
participant ConditionFull
participant ConditionEmpty
Producer->>Lock: lock()
Lock-->>Producer: 获取锁
Producer->>ConditionFull: await() if full
Lock-->>Producer: 释放锁并等待
Producer->>Lock: 重新获取锁
Producer->>ConditionEmpty: signal()
Lock-->>Consumer: 通知消费者
Producer->>Lock: unlock()
Consumer->>Lock: lock()
Lock-->>Consumer: 获取锁
Consumer->>ConditionEmpty: await() if empty
Lock-->>Consumer: 释放锁并等待
Consumer->>Lock: 重新获取锁
Consumer->>ConditionFull: signal()
Lock-->>Producer: 通知生产者
Consumer->>Lock: unlock()
4.3 并发工具类与原子操作
Java并发包 java.util.concurrent.atomic 提供了多种原子变量类,适用于高并发下对共享变量的原子操作,避免使用锁的开销。
4.3.1 Atomic包中的原子变量
示例:使用 AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
逐行分析 :
-AtomicInteger:线程安全的整型变量;
-incrementAndGet():原子性地将值加1并返回结果;
-get():获取当前值。
优势 :
- 无需显式加锁;
- 使用CAS(Compare and Swap)实现,性能更高;
- 适用于计数器、状态标志等场景。
4.3.2 CountDownLatch与CyclicBarrier的应用
CountDownLatch
CountDownLatch 是一个倒计时门闩,允许一个或多个线程等待其他线程完成操作。
import java.util.concurrent.CountDownLatch;
public class LatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务");
latch.countDown();
}).start();
}
latch.await(); // 等待所有线程完成
System.out.println("所有任务完成");
}
}
说明 :
-latch.countDown():每次线程执行完毕后减少计数;
-latch.await():主线程等待计数归零。
CyclicBarrier
CyclicBarrier 用于多个线程相互等待,到达屏障点后一起继续执行。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class BarrierExample {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达屏障,继续执行");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 到达屏障");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 继续执行");
}).start();
}
}
}
说明 :
-barrier.await():线程到达屏障点后等待;
- 所有线程到达后执行屏障动作;
- 屏障可重复使用,适合循环任务。
小结
本章深入讲解了Java中用于实现线程安全的两种主要机制: synchronized 和 ReentrantLock ,并介绍了并发工具类如 AtomicInteger 、 CountDownLatch 与 CyclicBarrier 的使用方式。通过对比分析和实际代码示例,帮助开发者理解不同机制的适用场景和性能差异。在实际开发中,应根据业务需求和性能要求合理选择并发控制手段,以实现高效、稳定的多线程系统。
5. JDBC数据库连接与数据操作
在网络游戏服务器开发中,数据库是玩家数据、任务状态、物品库存、排行榜等信息的持久化存储核心。Java Database Connectivity(JDBC)作为Java标准API,提供了与数据库交互的能力。本章将深入探讨JDBC的使用方法、连接管理机制、SQL执行流程以及数据库事务的控制与优化。
5.1 JDBC基础与连接建立
5.1.1 JDBC驱动类型与连接字符串
JDBC通过驱动程序实现与不同数据库的通信。常见的JDBC驱动类型包括:
| 类型 | 描述 |
|---|---|
| Type-1 | 使用JDBC-ODBC桥接器,依赖ODBC驱动,性能较差,已逐渐淘汰 |
| Type-2 | 使用本地API和JVM之间的桥梁,依赖特定数据库客户端库 |
| Type-3 | 通过纯Java客户端与中间件通信,中间件再与数据库交互 |
| Type-4 | 纯Java驱动,直接与数据库通信,性能最佳,推荐使用 |
连接字符串格式 (以MySQL为例):
jdbc:mysql://[host]:[port]/[database]?user=[username]&password=[password]
例如:
jdbc:mysql://localhost:3306/game_db?user=root&password=123456
5.1.2 使用DriverManager与DataSource
DriverManager方式
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DBConnection {
public static Connection getConnection() throws SQLException {
String url = "jdbc:mysql://localhost:3306/game_db";
String user = "root";
String password = "123456";
return DriverManager.getConnection(url, user, password);
}
}
逐行解读分析:
-
import java.sql.*;:导入JDBC相关的类和接口。 -
DriverManager.getConnection():使用给定的URL、用户名和密码建立数据库连接。 -
throws SQLException:数据库操作可能抛出SQL异常,需进行捕获或声明。
DataSource方式(推荐)
使用 DataSource 接口(如 HikariDataSource )可以更好地管理连接池:
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
public class HikariCP {
private static HikariDataSource dataSource;
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/game_db");
config.setUsername("root");
config.setPassword("123456");
config.setMaximumPoolSize(10);
config.setIdleTimeout(30000);
config.setMaxLifetime(1800000);
dataSource = new HikariDataSource(config);
}
public static HikariDataSource getDataSource() {
return dataSource;
}
}
逻辑分析:
-
HikariConfig:配置Hikari连接池参数。 -
setMaximumPoolSize:最大连接数。 -
setIdleTimeout:空闲连接超时时间(毫秒)。 -
setMaxLifetime:连接最大存活时间(毫秒)。 -
HikariDataSource:创建连接池实例。
优势对比:
| 项目 | DriverManager | DataSource |
|---|---|---|
| 连接复用 | 无 | 支持连接池 |
| 性能 | 一般 | 高 |
| 配置灵活性 | 低 | 高 |
| 线程安全 | 否 | 是 |
5.2 数据库操作与SQL执行
5.2.1 Statement与PreparedStatement的使用
Statement
适用于静态SQL语句:
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
public class QueryPlayer {
public static void getPlayerInfo(int playerId) {
String sql = "SELECT * FROM players WHERE id = " + playerId;
try (Connection conn = DBConnection.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
System.out.println("Player Name: " + rs.getString("name"));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
逻辑分析:
-
Statement:用于执行静态SQL语句。 -
executeQuery():执行查询语句,返回ResultSet结果集。 -
try-with-resources:自动关闭资源,防止内存泄漏。
⚠️ 风险:存在SQL注入风险(如拼接
playerId),不推荐用于用户输入。
PreparedStatement
适用于动态SQL语句,防止SQL注入:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class SafePlayerQuery {
public static void getPlayerInfo(int playerId) {
String sql = "SELECT * FROM players WHERE id = ?";
try (Connection conn = DBConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, playerId); // 设置参数
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println("Player Name: " + rs.getString("name"));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
逐行解读分析:
-
PreparedStatement:预编译SQL语句,可设置参数。 -
pstmt.setInt(1, playerId):为SQL中的第一个参数赋值。 - 使用预编译可以有效防止SQL注入,是推荐做法。
5.2.2 查询结果的处理与封装
结果集处理
ResultSet 对象表示查询结果的数据表,可以通过列名或索引访问字段:
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
int level = rs.getInt("level");
System.out.println("ID: " + id + ", Name: " + name + ", Level: " + level);
}
数据封装(ORM雏形)
可将查询结果封装为Java对象:
public class Player {
private int id;
private String name;
private int level;
// Getter / Setter
}
public class PlayerDAO {
public static Player getPlayerById(int id) {
String sql = "SELECT * FROM players WHERE id = ?";
try (Connection conn = DBConnection.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
Player player = new Player();
player.setId(rs.getInt("id"));
player.setName(rs.getString("name"));
player.setLevel(rs.getInt("level"));
return player;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
流程图:
graph TD
A[数据库连接] --> B[创建PreparedStatement]
B --> C[设置参数]
C --> D[执行查询]
D --> E[处理ResultSet]
E --> F[封装为Java对象]
F --> G[返回结果]
5.3 数据库事务与性能优化
5.3.1 事务的ACID特性与控制方式
事务(Transaction)是数据库操作的最小单位,具备ACID特性:
| 属性 | 描述 |
|---|---|
| 原子性(Atomicity) | 事务中的操作要么全部成功,要么全部失败 |
| 一致性(Consistency) | 事务执行前后,数据库保持一致状态 |
| 隔离性(Isolation) | 多个事务并发执行时,彼此隔离,互不干扰 |
| 持久性(Durability) | 事务一旦提交,对数据库的更改是永久的 |
示例代码:事务处理
public class TransactionExample {
public static void transferMoney(int fromId, int toId, int amount) {
Connection conn = null;
try {
conn = DBConnection.getConnection();
conn.setAutoCommit(false); // 关闭自动提交
// 扣除from用户余额
String deductSql = "UPDATE players SET balance = balance - ? WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(deductSql)) {
pstmt.setInt(1, amount);
pstmt.setInt(2, fromId);
pstmt.executeUpdate();
}
// 增加to用户余额
String addSql = "UPDATE players SET balance = balance + ? WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(addSql)) {
pstmt.setInt(1, amount);
pstmt.setInt(2, toId);
pstmt.executeUpdate();
}
conn.commit(); // 提交事务
} catch (Exception e) {
try {
if (conn != null) {
conn.rollback(); // 回滚事务
}
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
try {
if (conn != null) {
conn.setAutoCommit(true); // 恢复自动提交
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
关键逻辑说明:
-
setAutoCommit(false):关闭自动提交,开启事务。 -
commit():手动提交事务。 -
rollback():事务失败时回滚。 - 使用try-with-resources确保资源关闭,避免连接泄漏。
5.3.2 连接池技术(如HikariCP)的集成与优化
优化目标
- 减少频繁创建/关闭连接的开销
- 提高数据库并发处理能力
- 有效控制连接数量,防止资源耗尽
HikariCP配置建议(游戏服务器)
| 参数 | 建议值 | 说明 |
|---|---|---|
| maximumPoolSize | 20~50 | 最大连接数,视并发量而定 |
| idleTimeout | 30000 | 空闲连接超时时间(30秒) |
| maxLifetime | 1800000 | 连接最大存活时间(30分钟) |
| connectionTestQuery | SELECT 1 | 检查连接是否可用的SQL |
| poolName | GameDBPool | 自定义连接池名称 |
示例代码:HikariCP封装使用
public class PlayerService {
private HikariDataSource dataSource;
public PlayerService(HikariDataSource dataSource) {
this.dataSource = dataSource;
}
public boolean updatePlayerName(int playerId, String newName) {
String sql = "UPDATE players SET name = ? WHERE id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, newName);
pstmt.setInt(2, playerId);
int rows = pstmt.executeUpdate();
return rows > 0;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
性能优化技巧:
- 使用连接池代替
DriverManager - 合理设置连接池大小,避免连接等待
- SQL语句尽量使用
PreparedStatement - 使用索引字段进行查询,避免全表扫描
- 定期分析慢查询日志,优化SQL执行效率
性能对比表:
| 项目 | 不使用连接池 | 使用HikariCP |
|---|---|---|
| 单次连接时间 | 50~200ms | 1~5ms |
| 并发连接数 | 有限 | 可配置至数百 |
| 资源占用 | 高 | 低 |
| 异常处理 | 复杂 | 易于管理 |
| 可维护性 | 差 | 好 |
总结与延伸:
本章深入讲解了JDBC在网络游戏服务器中的应用,包括数据库连接的建立、SQL执行流程、事务控制与性能优化策略。通过结合连接池技术(如HikariCP)和面向对象封装,可以构建出高效、稳定、可扩展的数据访问层。下一章将进入分布式系统架构的探讨,介绍Java RMI和JMS在游戏服务器集群中的应用,实现跨节点通信与异步任务处理。
6. 分布式系统架构与Java RMI/JMS应用
在现代网络游戏服务器开发中,随着用户规模的扩大和业务逻辑的复杂化,传统的单机服务器架构已无法满足高并发、高可用性和可扩展性的需求。因此,分布式系统架构逐渐成为主流选择。Java平台为构建分布式系统提供了强大的支持,其中RMI(Remote Method Invocation)和JMS(Java Message Service)是两个关键的技术组件。本章将深入探讨分布式系统的理论基础,并通过Java RMI和JMS实现跨节点通信与异步消息处理。
6.1 分布式系统基础理论
分布式系统是由多个相互协作的计算机节点组成的系统,它们通过网络进行通信与协调,对外表现为一个统一的整体。理解分布式系统的核心理论对于设计高性能、可扩展的游戏服务器至关重要。
6.1.1 CAP定理与分布式系统设计原则
CAP定理指出,在分布式系统中,一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)三者不可兼得,最多只能同时满足其中两个。
| 属性 | 含义 | 举例 |
|---|---|---|
| 一致性(C) | 所有节点在同一时间看到相同的数据 | 数据库事务提交后所有节点数据一致 |
| 可用性(A) | 每个请求都能在合理时间内得到响应 | HTTP服务的快速响应 |
| 分区容忍性(P) | 网络分区时系统仍然可以继续运行 | 数据中心之间的网络中断 |
在游戏服务器架构中,通常更注重分区容忍性(P)和可用性(A),例如在跨区域服务器部署时,优先保证服务可用,而采用最终一致性策略。
6.1.2 微服务与服务注册发现机制
微服务架构将系统拆分为多个独立的服务模块,每个服务可以独立部署、扩展和维护。服务注册与发现机制(如ZooKeeper、Eureka、Consul)是微服务通信的核心。
// 示例:使用Netflix Eureka进行服务注册
@SpringBootApplication
@EnableEurekaClient
public class GameServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GameServiceApplication.class, args);
}
}
服务注册流程如下:
graph TD
A[服务启动] --> B[向注册中心注册]
B --> C[注册中心保存服务元数据]
D[服务消费者] --> E[从注册中心获取服务列表]
E --> F[调用对应服务实例]
6.2 Java RMI远程调用实现
Java RMI(Remote Method Invocation)是一种基于Java的远程过程调用(RPC)机制,允许一个JVM中的对象调用另一个JVM中的对象方法,非常适合用于游戏服务器之间的远程服务调用。
6.2.1 RMI服务的定义与注册
要使用RMI,首先需要定义一个远程接口,并实现其服务类。接着在服务端注册该服务,客户端即可远程调用。
// 1. 定义远程接口
public interface GameService extends Remote {
String login(String username, String password) throws RemoteException;
}
// 2. 实现远程服务类
public class GameServiceImpl extends UnicastRemoteObject implements GameService {
public GameServiceImpl() throws RemoteException {
super();
}
@Override
public String login(String username, String password) throws RemoteException {
return "Login successful for user: " + username;
}
}
// 3. 服务端注册RMI服务
public class RMIServer {
public static void main(String[] args) {
try {
GameService service = new GameServiceImpl();
Naming.rebind("rmi://localhost/GameService", service);
System.out.println("RMI服务已注册");
} catch (Exception e) {
e.printStackTrace();
}
}
}
6.2.2 远程接口与Stub/Skeleton机制
Java RMI底层通过Stub和Skeleton机制实现远程调用:
- Stub :客户端代理对象,负责将调用请求序列化并发送到服务端。
- Skeleton :服务端代理对象,接收请求并调用实际服务对象的方法。
调用流程如下:
sequenceDiagram
participant Client
participant Stub
participant Skeleton
participant Server
Client->>Stub: 调用远程方法
Stub->>Skeleton: 发送调用请求与参数
Skeleton->>Server: 调用实际服务方法
Server-->>Skeleton: 返回结果
Skeleton-->>Stub: 返回结果
Stub-->>Client: 返回调用结果
6.3 JMS消息队列与异步通信
JMS(Java Message Service)是Java平台下的消息中间件API,支持点对点(Queue)和发布/订阅(Topic)两种消息模型。在游戏服务器中,使用JMS可以实现异步通信、任务解耦、事件驱动等高级架构模式。
6.3.1 消息中间件的基本概念
| 概念 | 描述 |
|---|---|
| 消息队列(Queue) | 点对点模式,消息被一个消费者消费 |
| 主题(Topic) | 发布/订阅模式,消息被多个订阅者消费 |
| 生产者(Producer) | 发送消息到消息队列或主题 |
| 消费者(Consumer) | 从队列或主题接收并处理消息 |
6.3.2 ActiveMQ与Kafka的集成与使用
ActiveMQ是Java平台下广泛使用的JMS实现,而Kafka则是一个高吞吐量的分布式消息系统,适用于大规模数据流处理。
ActiveMQ示例(点对点队列):
// 发送消息
ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection connection = factory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("game.login.queue");
MessageProducer producer = session.createProducer(queue);
TextMessage message = session.createTextMessage("User login: player1");
producer.send(message);
connection.close();
// 接收消息
MessageConsumer consumer = session.createConsumer(queue);
consumer.setMessageListener(msg -> {
TextMessage textMsg = (TextMessage) msg;
try {
System.out.println("Received: " + textMsg.getText());
} catch (JMSException e) {
e.printStackTrace();
}
});
Kafka示例(生产者):
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer producer = new KafkaProducer<>(props);
ProducerRecord record = new ProducerRecord<>("game-events", "player login");
producer.send(record);
producer.close();
6.3.3 异步任务处理与事件驱动架构
在游戏服务器中,使用消息队列可以实现任务异步处理,如:
- 玩家登录日志记录
- 游戏内事件广播
- 异步任务处理(如排行榜更新、任务进度处理)
事件驱动架构如下图所示:
graph LR
A[玩家登录] --> B[发布登录事件]
B --> C[Kafka/ActiveMQ]
C --> D[日志服务消费事件]
C --> E[排行榜服务消费事件]
C --> F[通知服务推送消息]
通过这种方式,各个服务模块之间解耦,提升系统的可扩展性和容错能力。
本章通过理论与实践相结合,详细介绍了分布式系统的基本原理,并演示了Java RMI和JMS在游戏服务器开发中的具体应用。下一章将继续深入探讨如何通过Spring Boot与微服务框架构建可扩展的游戏后端系统。
本文还有配套的精品资源,点击获取
简介:Java网络游戏服务器系统基于Java技术构建,旨在支持大量玩家在线交互。系统开发涵盖网络编程、多线程处理、数据库操作、并发控制、负载均衡等关键技术。通过使用Socket编程实现客户端与服务器通信,结合多线程模型提升并发处理能力,利用JDBC进行游戏数据管理,并通过分布式架构和负载均衡策略增强系统扩展性与稳定性。此外,系统还涉及NIO性能优化、安全性机制、异常处理与日志记录等内容,全面覆盖游戏服务器从设计到部署的关键环节。
本文还有配套的精品资源,点击获取








