Netty-NioServerSocketChannel与NioSocketChannel

NioServerSocketChannel

NioServerSocketChannel
  NioServerSocketChannel是netty服务端的channel。在ServerbootStrap的bind方法中,通过反射,实例化对象NioServerSocketChannel。
  NioServerSocketChannel对象实例化的过程中。

  1. AbstractChannel中实例化channel的id,unsafe,channelpipeline。
  2. AbstractNioChannel保存ServerSocketChannel,并设置感兴趣的事件SelectionKey.OP_ACCEPT,设置ServerSocketChannel#configureBlocking为false。
  3. NioServerSocketChannel中对NioServerSocketChannelConfig对象进行构建,爆保存到类变量config中。NioServerSocketChannelConfig中包含了当前对象和通过ServerSocketChannel获取到的serverSocket对象。

  AbstractNioMessageChannel这个父类中没有对属性赋值。它主要是对AbstractChannel中的抽象方法newUnsafe的实现。Unsafe的落地实现主要是处理NioEventLoop--》run--》processSelectedKeys--》processSelectedKey--》unsafe.read(),其中unsafe是当前channel父类abstractChannel中的unsafe。也就是说,当NioEventLoop中处理IO事件的时候,读取数据的时候,落地实现在AbstractNioMessageChannel的内部类NioMessageUnsafe
  NioMessageUnsafe中的doReadMessages方法,将数据读取到readBuf中。doReadMessages也是一个抽象方法,落地实现是NioServerSocketChannel。主要是做的事情是通过ServerSocketChannel.accept方法获取到SocketChannel,将这个SocketChannel封装成NioSocketChannel,添加到readBuf中
在这里插入图片描述

  NioMessageUnsafe中的fireChannelRead方法,将readBuf集合中的数据遍历调用channelPipeline的fireChannelRead方法。readBuf集合中存储的是NioSocketChannel。之前的学习我们知道,NioServerSocketChannel中的channelPipeline的链表结构为:headContext-->ServerBootstrapAcceptor-->tailContext
ServerBootstrapAcceptor中的channelRead方法,将客户端的请求socketChannel绑定到childGroup中的一个NioEventLoop上。
在这里插入图片描述
  下面是AbstractNioMessageChannel 的原代码。

public abstract class AbstractNioMessageChannel extends AbstractNioChannel {boolean inputShutdown;/*** @see AbstractNioChannel#AbstractNioChannel(Channel, SelectableChannel, int)*/protected AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp) {super(parent, ch, readInterestOp);}@Overrideprotected AbstractNioUnsafe newUnsafe() {// 看这里,看这里,看这里。是对AbstractChannel的newUnsafe抽象方法的实现。return new NioMessageUnsafe();}@Overrideprotected void doBeginRead() throws Exception {if (inputShutdown) {return;}super.doBeginRead();}protected boolean continueReading(RecvByteBufAllocator.Handle allocHandle) {return allocHandle.continueReading();}// 看这里,看这里,看这里。内部类实现Unsafeprivate final class NioMessageUnsafe extends AbstractNioUnsafe {private final List<Object> readBuf = new ArrayList<Object>();@Overridepublic void read() {assert eventLoop().inEventLoop();final ChannelConfig config = config();final ChannelPipeline pipeline = pipeline();final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();allocHandle.reset(config);boolean closed = false;Throwable exception = null;try {try {do {// 看这里,看这里,看这里。doReadMessages也是一个抽象方法,落地实现是NioServerSocketChannel。主要是做的事情是通过ServerSocketChannel.accept方法获取到SocketChannel,将这个SocketChannel封装成NioSocketChannel,添加到readBuf中。int localRead = doReadMessages(readBuf);if (localRead == 0) {break;}if (localRead < 0) {closed = true;break;}allocHandle.incMessagesRead(localRead);} while (continueReading(allocHandle));} catch (Throwable t) {exception = t;}int size = readBuf.size();for (int i = 0; i < size; i ++) {readPending = false;// 看这里,看这里,看这里。调用NioServerSocketChannel的channelPipeline进行事件发布。readBuf里面存的是NioSocketChannel。pipeline.fireChannelRead(readBuf.get(i));}readBuf.clear();allocHandle.readComplete();pipeline.fireChannelReadComplete();if (exception != null) {closed = closeOnReadError(exception);pipeline.fireExceptionCaught(exception);}if (closed) {inputShutdown = true;if (isOpen()) {close(voidPromise());}}} finally {// Check if there is a readPending which was not processed yet.// This could be for two reasons:// * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method// * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method//// See https://github.com/netty/netty/issues/2254if (!readPending && !config.isAutoRead()) {removeReadOp();}}}}@Overrideprotected void doWrite(ChannelOutboundBuffer in) throws Exception {final SelectionKey key = selectionKey();final int interestOps = key.interestOps();int maxMessagesPerWrite = maxMessagesPerWrite();while (maxMessagesPerWrite > 0) {Object msg = in.current();if (msg == null) {break;}try {boolean done = false;for (int i = config().getWriteSpinCount() - 1; i >= 0; i--) {if (doWriteMessage(msg, in)) {done = true;break;}}if (done) {maxMessagesPerWrite--;in.remove();} else {break;}} catch (Exception e) {if (continueOnWriteError()) {maxMessagesPerWrite--;in.remove(e);} else {throw e;}}}if (in.isEmpty()) {// Wrote all messages.if ((interestOps & SelectionKey.OP_WRITE) != 0) {key.interestOps(interestOps & ~SelectionKey.OP_WRITE);}} else {// Did not write all messages.if ((interestOps & SelectionKey.OP_WRITE) == 0) {key.interestOps(interestOps | SelectionKey.OP_WRITE);}}}/*** Returns {@code true} if we should continue the write loop on a write error.*/protected boolean continueOnWriteError() {return false;}protected boolean closeOnReadError(Throwable cause) {if (!isActive()) {// If the channel is not active anymore for whatever reason we should not try to continue reading.return true;}if (cause instanceof PortUnreachableException) {return false;}if (cause instanceof IOException) {// ServerChannel should not be closed even on IOException because it can often continue// accepting incoming connections. (e.g. too many open files)return !(this instanceof ServerChannel);}return true;}/*** Read messages into the given array and return the amount which was read.*/protected abstract int doReadMessages(List<Object> buf) throws Exception;/*** Write a message to the underlying {@link java.nio.channels.Channel}.** @return {@code true} if and only if the message has been written*/protected abstract boolean doWriteMessage(Object msg, ChannelOutboundBuffer in) throws Exception;
}

NioSocketChannel

NioSocketChannel
  NioSocketChannel是netty封装的客户端请求的channel。
  与上面的NioServerSocketChannel相比。继承类中将AbstractNioMessageChannel 替换为了AbstractNioByteChannel。从名称上看,是对字节数据的处理。下面就单独研究一下AbstractNioByteChannel。
  AbstractNioByteChannel。同样的,在NioSocketChannel实例化的时候,这个类没有属性赋值。它主要也是实现AbstractChannel中的newUnsafe抽象方法。
  newUnsafe的实现也是AbstractNioByteChannel的内部类NioByteUnsafe 。NioByteUnsafe 读取数据是调用抽象方法doReadBytes。doReadBytes的落地实现在NioSocketChannel中。
在这里插入图片描述

  以下是AbstractNioByteChannel 的原码。

public abstract class AbstractNioByteChannel extends AbstractNioChannel {private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16);private static final String EXPECTED_TYPES =" (expected: " + StringUtil.simpleClassName(ByteBuf.class) + ", " +StringUtil.simpleClassName(FileRegion.class) + ')';private final Runnable flushTask = new Runnable() {@Overridepublic void run() {// Calling flush0 directly to ensure we not try to flush messages that were added via write(...) in the// meantime.((AbstractNioUnsafe) unsafe()).flush0();}};private boolean inputClosedSeenErrorOnRead;/*** Create a new instance** @param parent            the parent {@link Channel} by which this instance was created. May be {@code null}* @param ch                the underlying {@link SelectableChannel} on which it operates*/protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {super(parent, ch, SelectionKey.OP_READ);}/*** Shutdown the input side of the channel.*/protected abstract ChannelFuture shutdownInput();protected boolean isInputShutdown0() {return false;}@Overrideprotected AbstractNioUnsafe newUnsafe() {// 看这里,看这里,看这里。实现是内部类NioByteUnsafereturn new NioByteUnsafe();}@Overridepublic ChannelMetadata metadata() {return METADATA;}final boolean shouldBreakReadReady(ChannelConfig config) {return isInputShutdown0() && (inputClosedSeenErrorOnRead || !isAllowHalfClosure(config));}private static boolean isAllowHalfClosure(ChannelConfig config) {return config instanceof SocketChannelConfig &&((SocketChannelConfig) config).isAllowHalfClosure();}// 看这里,看这里,看这里,实现newUnsafe的内部类protected class NioByteUnsafe extends AbstractNioUnsafe {private void closeOnRead(ChannelPipeline pipeline) {if (!isInputShutdown0()) {if (isAllowHalfClosure(config())) {shutdownInput();pipeline.fireUserEventTriggered(ChannelInputShutdownEvent.INSTANCE);} else {close(voidPromise());}} else if (!inputClosedSeenErrorOnRead) {inputClosedSeenErrorOnRead = true;pipeline.fireUserEventTriggered(ChannelInputShutdownReadComplete.INSTANCE);}}private void handleReadException(ChannelPipeline pipeline, ByteBuf byteBuf, Throwable cause, boolean close,RecvByteBufAllocator.Handle allocHandle) {if (byteBuf != null) {if (byteBuf.isReadable()) {readPending = false;pipeline.fireChannelRead(byteBuf);} else {byteBuf.release();}}allocHandle.readComplete();pipeline.fireChannelReadComplete();pipeline.fireExceptionCaught(cause);// If oom will close the read event, release connection.// See https://github.com/netty/netty/issues/10434if (close || cause instanceof OutOfMemoryError || cause instanceof IOException) {closeOnRead(pipeline);}}// 看这里,看这里,看这里,我们重点看看read()方法。@Overridepublic final void read() {// 获取NioSocketChannelConfig对象final ChannelConfig config = config();if (shouldBreakReadReady(config)) {clearReadPending();return;}// 获取NioSocketChannel的channelPipelinefinal ChannelPipeline pipeline = pipeline();final ByteBufAllocator allocator = config.getAllocator();final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();allocHandle.reset(config);ByteBuf byteBuf = null;boolean close = false;try {do {byteBuf = allocHandle.allocate(allocator);// 看这里,看这里,看这里,将数据读取到byteBuf中。doReadBytesallocHandle.lastBytesRead(doReadBytes(byteBuf));if (allocHandle.lastBytesRead() <= 0) {// nothing was read. release the buffer.byteBuf.release();byteBuf = null;close = allocHandle.lastBytesRead() < 0;if (close) {// There is nothing left to read as we received an EOF.readPending = false;}break;}allocHandle.incMessagesRead(1);readPending = false;// 看这里,看这里,看这里,将读取到的数据在pipeline中进行事件传播。pipeline.fireChannelRead(byteBuf);byteBuf = null;} while (allocHandle.continueReading());allocHandle.readComplete();pipeline.fireChannelReadComplete();if (close) {closeOnRead(pipeline);}} catch (Throwable t) {handleReadException(pipeline, byteBuf, t, close, allocHandle);} finally {// Check if there is a readPending which was not processed yet.// This could be for two reasons:// * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method// * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method//// See https://github.com/netty/netty/issues/2254if (!readPending && !config.isAutoRead()) {removeReadOp();}}}}/*** Write objects to the OS.* @param in the collection which contains objects to write.* @return The value that should be decremented from the write quantum which starts at* {@link ChannelConfig#getWriteSpinCount()}. The typical use cases are as follows:* <ul>*     <li>0 - if no write was attempted. This is appropriate if an empty {@link ByteBuf} (or other empty content)*     is encountered</li>*     <li>1 - if a single call to write data was made to the OS</li>*     <li>{@link ChannelUtils#WRITE_STATUS_SNDBUF_FULL} - if an attempt to write data was made to the OS, but no*     data was accepted</li>* </ul>* @throws Exception if an I/O exception occurs during write.*/protected final int doWrite0(ChannelOutboundBuffer in) throws Exception {Object msg = in.current();if (msg == null) {// Directly return here so incompleteWrite(...) is not called.return 0;}return doWriteInternal(in, in.current());}private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception {if (msg instanceof ByteBuf) {ByteBuf buf = (ByteBuf) msg;if (!buf.isReadable()) {in.remove();return 0;}final int localFlushedAmount = doWriteBytes(buf);if (localFlushedAmount > 0) {in.progress(localFlushedAmount);if (!buf.isReadable()) {in.remove();}return 1;}} else if (msg instanceof FileRegion) {FileRegion region = (FileRegion) msg;if (region.transferred() >= region.count()) {in.remove();return 0;}long localFlushedAmount = doWriteFileRegion(region);if (localFlushedAmount > 0) {in.progress(localFlushedAmount);if (region.transferred() >= region.count()) {in.remove();}return 1;}} else {// Should not reach here.throw new Error();}return WRITE_STATUS_SNDBUF_FULL;}@Overrideprotected void doWrite(ChannelOutboundBuffer in) throws Exception {int writeSpinCount = config().getWriteSpinCount();do {Object msg = in.current();if (msg == null) {// Wrote all messages.clearOpWrite();// Directly return here so incompleteWrite(...) is not called.return;}writeSpinCount -= doWriteInternal(in, msg);} while (writeSpinCount > 0);incompleteWrite(writeSpinCount < 0);}@Overrideprotected final Object filterOutboundMessage(Object msg) {if (msg instanceof ByteBuf) {ByteBuf buf = (ByteBuf) msg;if (buf.isDirect()) {return msg;}return newDirectBuffer(buf);}if (msg instanceof FileRegion) {return msg;}throw new UnsupportedOperationException("unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES);}protected final void incompleteWrite(boolean setOpWrite) {// Did not write completely.if (setOpWrite) {setOpWrite();} else {// It is possible that we have set the write OP, woken up by NIO because the socket is writable, and then// use our write quantum. In this case we no longer want to set the write OP because the socket is still// writable (as far as we know). We will find out next time we attempt to write if the socket is writable// and set the write OP if necessary.clearOpWrite();// Schedule flush again later so other tasks can be picked up in the meantimeeventLoop().execute(flushTask);}}/*** Write a {@link FileRegion}** @param region        the {@link FileRegion} from which the bytes should be written* @return amount       the amount of written bytes*/protected abstract long doWriteFileRegion(FileRegion region) throws Exception;/*** Read bytes into the given {@link ByteBuf} and return the amount.*/protected abstract int doReadBytes(ByteBuf buf) throws Exception;/*** Write bytes form the given {@link ByteBuf} to the underlying {@link java.nio.channels.Channel}.* @param buf           the {@link ByteBuf} from which the bytes should be written* @return amount       the amount of written bytes*/protected abstract int doWriteBytes(ByteBuf buf) throws Exception;protected final void setOpWrite() {final SelectionKey key = selectionKey();// Check first if the key is still valid as it may be canceled as part of the deregistration// from the EventLoop// See https://github.com/netty/netty/issues/2104if (!key.isValid()) {return;}final int interestOps = key.interestOps();if ((interestOps & SelectionKey.OP_WRITE) == 0) {key.interestOps(interestOps | SelectionKey.OP_WRITE);}}protected final void clearOpWrite() {final SelectionKey key = selectionKey();// Check first if the key is still valid as it may be canceled as part of the deregistration// from the EventLoop// See https://github.com/netty/netty/issues/2104if (!key.isValid()) {return;}final int interestOps = key.interestOps();if ((interestOps & SelectionKey.OP_WRITE) != 0) {key.interestOps(interestOps & ~SelectionKey.OP_WRITE);}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/824518.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

3DGS渐进式渲染 - 离线生成渲染视频

总览 输入&#xff1a;环绕Object拍摄的RGB视频 输出&#xff1a;自定义相机路径的渲染视频&#xff08;包含渐变效果&#xff09; 实现过程 首先&#xff0c;编译3DGS的C代码&#xff0c;并跑通convert.py、train.py和render.py。教程如下&#xff1a; github网址&#xf…

HarmonyOS开发实例:【分布式数据服务】

介绍 分布式数据服务(Distributed Data Service&#xff0c;DDS)为应用程序提供不同设备间数据分布式的能力。通过调用分布式数据接口&#xff0c;应用程序将数据保存到分布式数据库中。通过结合帐号、应用和分布式数据服务对属于不同的应用的数据进行隔离&#xff0c;保证不同…

Java项目实现Excel导出(Hutool)

官网&#xff1a; Excel生成-ExcelWriter (hutool.cn) 1.使用Hutool工具实现Excel导出&#xff08;.xlsx格式&#xff09; 业务场景&#xff1a; 使用SpringCloudmysqlmybatis-plus需要将数据库中的数据导出到Excel文件中 前端为Vue2 第零步&#xff1a;导入依赖 <!-…

ASP.NET Core 标识(Identity)框架系列(四):闲聊 JWT 的缺点,和一些解决思路

前言 前面的几篇文章讲了很多 JWT 的优点&#xff0c;但作为技术人员都知道&#xff0c;没有一种技术是万能的 “银弹”&#xff0c;所谓有矛就有盾&#xff0c;相比 Session、Cookie 等传统的身份验证方式&#xff0c;JWT 在拥有很多优点的同时&#xff0c;也有着不可忽视的缺…

49.HarmonyOS鸿蒙系统 App(ArkUI)Tab导航组件的使用

HarmonyOS鸿蒙系统 App(ArkUI)Tab导航组件的使用 图片显示 Row() {Image($r(app.media.leaf)).height(100).width(100)Image($r(app.media.icon)).height(100).width(100) } 左侧导航 import prompt from ohos.prompt; import promptAction from ohos.promptAction; Entry C…

适用于Windows电脑的最佳数据恢复软件是哪些?10佳数据恢复软件

丢失我们系统中可用的宝贵信息是很烦人的。我们可以尝试几种手动方法来重新获取丢失的数据。然而&#xff0c;当我们采用非自动方法来恢复数据时&#xff0c;这是一项令人厌烦和乏味的工作。在这种情况下&#xff0c;我们可以尝试使用一些正版硬盘恢复软件进行数据恢复。此页面…

pytest学习-pytorch单元测试

pytorch单元测试 一.公共模块[common.py]二.普通算子测试[test_clone.py]三.集合通信测试[test_ccl.py]四.测试命令五.测试报告 希望测试pytorch各种算子、block、网络等在不同硬件平台,不同软件版本下的计算误差、耗时、内存占用等指标. 本文基于torch.testing._internal 一…

wsl安装与日常使用

文章目录 一、前向配置1、搜索功能2、勾选下面几个功能&#xff0c;进行安装二、安装WSL1、打开Windows PowerShell,查找你要安装的linux版本2、选择对应版本进行安装3、输入用户名以及密码 三、配置终端代理1、打开powershell,查看自己的IP把以下信息加入到~/.bashrc中 四、更…

Transformer with Transfer CNN for Remote-Sensing-Image Object Detection

遥感图像&#xff08;RSI&#xff09;中的目标检测始终是遥感界一个充满活力的研究主题。 最近&#xff0c;基于深度卷积神经网络 (CNN) 的方法&#xff0c;包括基于区域 CNN 和基于 You-Only-Look-Once 的方法&#xff0c;已成为 RSI 目标检测的事实上的标准。 CNN 擅长局部特…

夸克AI PPT初体验:一键生成大纲,一键生成PPT,一键更换模板!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…

JavaScript(JS)三种使用方式,三种输出方式,及快速注释。---[用于后续web渗透内容]

JavaScript&#xff08;JS&#xff09;是一种广泛使用的编程语言&#xff0c;允许在网页中添加交互性和动态效果。在HTML中&#xff0c;<script>标签用于引入和执行JavaScript代码。 JS代码 js1.html \\js三种使用方式<!DOCTYPE html> <html lang"en&quo…

vulhub weblogic全系列靶场

简介 Oracle WebLogic Server 是一个统一的可扩展平台&#xff0c;专用于开发、部署和运行 Java 应用等适用于本地环境和云环境的企业应用。它提供了一种强健、成熟和可扩展的 Java Enterprise Edition (EE) 和 Jakarta EE 实施方式。 需要使用的工具 ysoserial使用不同库制作的…

自动驾驶时代的物联网与车载系统安全:挑战与应对策略

随着特斯拉CEO埃隆马斯克近日对未来出行景象的描绘——几乎所有汽车都将实现自动驾驶&#xff0c;这一愿景愈发接近现实。马斯克生动比喻&#xff0c;未来的乘客步入汽车就如同走进一部自动化的电梯&#xff0c;无需任何手动操作。这一转变预示着汽车行业正朝着高度智能化的方向…

Python学习之-typing详解

前言&#xff1a; Python的typing模块自Python 3.5开始引入&#xff0c;提供了类型系统的扩展&#xff0c;能够帮助程序员定义变量、函数的参数和返回值类型等。这使得代码更易于理解和检查&#xff0c;也方便了IDE和一些工具进行类型检查&#xff0c;提升了代码的质量。 typ…

【每日刷题】Day17

【每日刷题】Day17 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 19. 删除链表的倒数第 N 个结点 - 力扣&#xff08;LeetCode&#xff09; 2. 162. 寻找峰值 - 力扣…

Scratch四级:第02讲 字符串

第02讲 字符串 教练:老马的程序人生 微信:ProgrammingAssistant 博客:https://lsgogroup.blog.csdn.net/ 讲课目录 运算模块:有关字符串的积木块遍历字符串项目制作:“解密”项目制作:“成语接龙”项目制作:“加减法混合运算器”字符串 计算机学会(GESP)中属于三级的内…

YOLOv9改进策略 | 损失函数篇 | EIoU、SIoU、WIoU、DIoU、FocusIoU等二十余种损失函数

一、本文介绍 这篇文章介绍了YOLOv9的重大改进&#xff0c;特别是在损失函数方面的创新。它不仅包括了多种IoU损失函数的改进和变体&#xff0c;如SIoU、WIoU、GIoU、DIoU、EIOU、CIoU&#xff0c;还融合了“Focus”思想&#xff0c;创造了一系列新的损失函数。这些组合形式的…

腾讯AI Lab:“自我对抗”提升大模型的推理能力

本文介绍了一种名为“对抗性禁忌”&#xff08;Adversarial Taboo&#xff09;的双人对抗语言游戏&#xff0c;用于通过自我对弈提升大型语言模型的推理能力。 &#x1f449; 具体的流程 1️⃣ 游戏设计&#xff1a;在这个游戏中&#xff0c;有两个角色&#xff1a;攻击者和防守…

基于Ultrascale+系列GTY收发器64b/66b编码方式的数据传输(一)——Async Gearbox使用及上板测试

于20世纪80年代左右由IBM提出的传统8B/10B编码方式在编码效率上较低&#xff08;仅为80%&#xff09;&#xff0c;为了提升编码效率&#xff0c;Dgilent Techologies公司于2000年左右提出了64b/66b编码并应用于10G以太网中。Xilinx GT手册中没有过多64b/66b编码介绍&#xff0c…

绝地求生:PUBG地形破坏功能上线!分享你的游玩感受及反馈赢丰厚奖励

随着29.1版本更新&#xff0c;地形破坏功能及新道具“镐”正式在荣都地图亮相&#xff01;大家现在可以在荣都地图体验“动手挖呀挖”啦。 快来分享你的游玩感受及反馈&#xff0c;即可参与活动赢取精美奖励&#xff01; 参与方式 以发帖/投稿的形式&#xff0c;在 #一决镐下#…