安卓通過DHCP協議獲取IP地址的過程

安卓通過DHCP協議的DORA Discovery發現 Offer提供 Request請求 Ack確認 獲取IP地址的過程
安卓N之前 5.0 6.0通過 android_net_utils_runDhcp 方法運行 /system/bin/dhcpcd 獲取ip地址
安卓N之後 N不要了runDhcpcd(),而是通過DhcpClient
DhcpClient是通過framework發送dhcpcd協議的UDP請求包直接去拿IP,不再使用開源的dhcpcd
google還用了一個狀態機 IpManager 來管理dhcpcd成功還是失敗等狀態,
將ip賦值給IpConfiguration和LinkProperties傳遞到上層的framework

/frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java

// 當WifiStateMachine狀態機進入狀態ObtainingIpState時 獲取IP地址
class ObtainingIpState extends State {
@Override
public void enter() {
mIpManager.startProvisioning(IpManager.ProvisioningConfiguration prov); //啓動獲取IP地址
}
}
ObtainingIpState所在狀態機

ObtainingIpState所在狀態機

/frameworks/base/services/net/java/android/net/ip/IpManager.java

class StartedState extends State {} IpManager中的三個狀態機
class StoppedState extends State {}
class StoppingState extends State {}

public void startProvisioning(ProvisioningConfiguration req) {
    getNetworkInterface();

    mCallback.setNeighborDiscoveryOffload(true);
    // 給初始化的狀態機 StoppedState 發送消息CMD_START
    sendMessage(CMD_START, new ProvisioningConfiguration(req));
}

========================================================================
class StoppedState extends State {
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
…..
case CMD_START:
mConfiguration = (ProvisioningConfiguration) msg.obj;
// 接收到 CMD_START 會進行狀態的切換,調用 StartedState的enter()方法
transitionTo(mStartedState);
break;
…..
}

==========================================

class StartedState extends State {
void enter(){
if(startIPv4()) // 狀態 StartedState 的enter 進入方法,調用startIPv4()函數
}
}

==========================================

private boolean startIPv4() {
    // If we have a StaticIpConfiguration attempt to apply it and handle the result accordingly.
    if (mConfiguration.mStaticIpConfig != null) { // 如果有靜態IP
        if (setIPv4Address(mConfiguration.mStaticIpConfig.ipAddress)) {
            handleIPv4Success(new DhcpResults(mConfiguration.mStaticIpConfig));
        } else {
            if (VDBG) { Log.d(mTag, "onProvisioningFailure()"); }
            recordMetric(IpManagerEvent.PROVISIONING_FAIL);
            mCallback.onProvisioningFailure(new LinkProperties(mLinkProperties));
            return false;
        }
    } else {
        // Start DHCPv4.  創建DhcpClient
        mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpManager.this, mInterfaceName);
        mDhcpClient.registerForPreDhcpNotification(); // mRegisteredForPreDhcpNotification = true

        //接收前面發過來的mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP);跟着跳轉到DhcpInitState:
        mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP); // 發送CMD_START_DHCP消息

        if (mConfiguration.mProvisioningTimeoutMs > 0) {
            final long alarmTime = SystemClock.elapsedRealtime() +
                    mConfiguration.mProvisioningTimeoutMs;
            mProvisioningTimeoutAlarm.schedule(alarmTime); // 在36秒後啓動timeout超時操作
        }
    }

    return true;
}


private boolean setIPv4Address(LinkAddress address) {
    final InterfaceConfiguration ifcg = new InterfaceConfiguration();
    ifcg.setLinkAddress(address);
    try {
    final INetworkManagementService mNwService.setInterfaceConfig(mInterfaceName, ifcg);
        if (VDBG) Log.d(mTag, "IPv4 configuration succeeded");
    } catch (IllegalStateException | RemoteException e) {
        Log.e(mTag, "IPv4 configuration failed: ", e);
        return false;
    }
    return true;
}

==========================================
//接收前面發過來的mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP);跟着跳轉到 DhcpInitState:
mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP); // 發送CMD_START_DHCP消息

/frameworks/base/services/net/java/android/net/dhcp/DhcpClient.java

class StoppedState extends LoggingState {
    @Override
    public boolean processMessage(Message message) {
        super.processMessage(message);
        switch (message.what) {
            case CMD_START_DHCP:
                if (mRegisteredForPreDhcpNotification) {
                    transitionTo(mWaitBeforeStartState);
                } else {
                    transitionTo(mDhcpInitState);  // 狀態跳轉到 DhcpInitState
                }
                return HANDLED;
            default:
                return NOT_HANDLED;
        }
    }

}

class DhcpInitState extends PacketRetransmittingState {
    public DhcpInitState() {
        super();
    }

//進入狀態時啓動一次startNewTransaction
//DORA Discover發現 Offer提供 Request請求 ACK確認 開始Discovery 發現?
@Override
public void enter() {
super.enter(); // 調用父類的 enter 方法中
startNewTransaction();
}

    protected boolean sendPacket() {
        return sendDiscoverPacket(); // 發送  DiscoverPacket 發現包
    }

    protected void receivePacket(DhcpPacket packet) {
        if (!isValidPacket(packet)) return;
        if (!(packet instanceof DhcpOfferPacket)) return;
        mOffer = packet.toDhcpResults();
        if (mOffer != null) {
            Log.d(TAG, "Got pending lease: " + mOffer);
            transitionTo(mDhcpRequestingState); // 接收到了 Offer包  接下來發送 Request 請求包
        }
    }
}

    private void startNewTransaction() {
    mTransactionId = mRandom.nextInt();  // 傳輸包的id號?
    mTransactionStartMillis = SystemClock.elapsedRealtime();
}
==================================================
PacketRetransmittingState

   abstract class PacketRetransmittingState extends LoggingState {

    private int mTimer;
    protected int mTimeout = 0;

    @Override
    public void enter() {
        super.enter();
        initTimer();
        maybeInitTimeout();
        sendMessage(CMD_KICK);  // 發送消息  CMD_KICK
    }

    @Override
    public boolean processMessage(Message message) {
        super.processMessage(message);
        switch (message.what) {
            case CMD_KICK:

// 調用了 sendPacket()抽象方法 所以就是之前的DhcpInitState的具體的實現 sendPacket()
sendPacket();
scheduleKick();
return HANDLED;
case CMD_RECEIVED_PACKET:
receivePacket((DhcpPacket) message.obj);
return HANDLED;
case CMD_TIMEOUT:
timeout();
return HANDLED;
default:
return NOT_HANDLED;
}
}

    public void exit() {
        mKickAlarm.cancel();
        mTimeoutAlarm.cancel();
    }

    }
    abstract protected boolean sendPacket();

abstract protected void receivePacket(DhcpPacket packet);

class DhcpInitState extends PacketRetransmittingState {

    protected boolean sendPacket() {
        return sendDiscoverPacket(); // 發送  DiscoverPacket 發現包
    }


private boolean sendDiscoverPacket() {
    ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
            DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
            DO_UNICAST, REQUESTED_PARAMS);  // 創建 ByteBuffer的UDP包 

return transmitPacket(packet, “DHCPDISCOVER”, DhcpPacket.ENCAP_L2, INADDR_BROADCAST); // 發送
}

}

 private boolean transmitPacket(ByteBuffer buf, String description, int encap, Inet4Address to) {
    try {
        if (encap == DhcpPacket.ENCAP_L2) {
            if (DBG) Log.d(TAG, "Broadcasting " + description);
            // 送這裏發送出去
            Os.sendto(mPacketSock, buf.array(), 0, buf.limit(), 0, mInterfaceBroadcastAddr);
        } else if (encap == DhcpPacket.ENCAP_BOOTP && to.equals(INADDR_BROADCAST)) {
            if (DBG) Log.d(TAG, "Broadcasting " + description);
            // N.B.: We only need this codepath because DhcpRequestPacket
            // hardcodes the source IP address to 0.0.0.0. We could reuse
            // the packet socket if this ever changes.
            Os.sendto(mUdpSock, buf, 0, to, DhcpPacket.DHCP_SERVER);
        } else {
            if (DBG) Log.d(TAG, String.format("Unicasting %s to %s",description, Os.getpeername(mUdpSock)));
            Os.write(mUdpSock, buf);
        }
    } catch(ErrnoException|IOException e) {
        Log.e(TAG, "Can't send packet: ", e);
        return false;
    }
    return true;
}

==================================================================
另外一邊有一個接收線程ReceiveThread run(),一直在收dhcp server的的數據包。

class ReceiveThread extends Thread {

    private final byte[] mPacket = new byte[DhcpPacket.MAX_LENGTH];
    private volatile boolean mStopped = false;

    public void halt() {
        mStopped = true;
        closeSockets();  // Interrupts the read() call the thread is blocked in.
    }

    @Override
    public void run() {
        if (DBG) Log.d(TAG, "Receive thread started");
        while (!mStopped) {
            int length = 0;  // Or compiler can't tell it's initialized if a parse error occurs.
            try {

                //讀取DHCP服務發出的OFFER提交包
                length = Os.read(mPacketSock 【FileDescriptor】, mPacket, 0, mPacket.length); 
                DhcpPacket packet = null;
                packet = DhcpPacket.decodeFullPacket(mPacket, length, DhcpPacket.ENCAP_L2);
                if (DBG) Log.d(TAG, "Received packet: " + packet);
                sendMessage(CMD_RECEIVED_PACKET, packet); // 發送接收到消息 CMD_RECEIVED_PACKET
            } catch (IOException|ErrnoException e) {
                if (!mStopped) {
                    Log.e(TAG, "Read error", e);
                    DhcpErrorEvent.logReceiveError(mIfaceName);
                }
            } catch (DhcpPacket.ParseException e) {
                Log.e(TAG, "Can't parse packet: " + e.getMessage());
                if (PACKET_DBG) {
                    Log.d(TAG, HexDump.dumpHexString(mPacket, 0, length));
                }
                DhcpErrorEvent.logParseError(mIfaceName, e.errorCode);
            }
        }
        if (DBG) Log.d(TAG, "Receive thread stopped");
    }

}

abstract class PacketRetransmittingState extends LoggingState {

    @Override
    public boolean processMessage(Message message) {
        super.processMessage(message);
        switch (message.what) {
            case CMD_RECEIVED_PACKET:
                receivePacket((DhcpPacket) message.obj); // 調用子類具體實現的receivePacket 方法
                return HANDLED;
            default:
                return NOT_HANDLED;
        }
    }
}

class DhcpInitState extends PacketRetransmittingState {

class DhcpInitState extends PacketRetransmittingState {

    protected void receivePacket(DhcpPacket packet) {   // 完成 DORA 中的 Offer的階段 
        if (!isValidPacket(packet)) return;
        if (!(packet instanceof DhcpOfferPacket)) return;
        mOffer = packet.toDhcpResults();
        if (mOffer != null) {
            Log.d(TAG, "Got pending lease: " + mOffer);
    // 接收到了來自DHCP服務器的OFFER包,切換狀態到  DhcpRequestingState  並進入到 enter() 方法
            transitionTo(mDhcpRequestingState); 
        }
    }
}

}

===================================================================

class DhcpRequestingState extends PacketRetransmittingState { // 進入到父類的enter 方法
    public DhcpRequestingState() {
        mTimeout = DHCP_TIMEOUT_MS / 2;  
    }


    public void enter() {// 進入到父類的enter 方法
        super.enter();
        initTimer();
        maybeInitTimeout();
        sendMessage(CMD_KICK); // 再次發送 CMD_KICK 消息
    }

            public boolean processMessage(Message message) {
        super.processMessage(message);
        switch (message.what) {
            case CMD_KICK:   //  收到 CMD_KICK 消息 
                sendPacket();  // 發送包方法,此時調用的是具體子類 DhcpRequestingState的發送方法
                scheduleKick();
                return HANDLED;
            default:
                return NOT_HANDLED;
        }
    }


     // 發送請求   完成  DORA中的  Request的階段
     // 此時 繼續在接收線程中等待接收來自服務器的ACK DHCP數據包
    protected boolean sendPacket() {
        return sendRequestPacket(
                INADDR_ANY,                                    // ciaddr
                (Inet4Address) mOffer.ipAddress.getAddress(),  // DHCP_REQUESTED_IP
                (Inet4Address) mOffer.serverAddress,           // DHCP_SERVER_IDENTIFIER
                INADDR_BROADCAST);                             // packet destination address
    }

    protected void receivePacket(DhcpPacket packet) {
        if (!isValidPacket(packet)) return;
        if ((packet instanceof DhcpAckPacket)) {
            DhcpResults results = packet.toDhcpResults();
            if (results != null) {
                setDhcpLeaseExpiry(packet);
                acceptDhcpResults(results, "Confirmed");
                transitionTo(mConfiguringInterfaceState);
            }
        } else if (packet instanceof DhcpNakPacket) {
            // TODO: Wait a while before returning into INIT state.
            Log.d(TAG, "Received NAK, returning to INIT");
            mOffer = null;
            transitionTo(mDhcpInitState);
        }
    }



private boolean sendRequestPacket(
        Inet4Address clientAddress, Inet4Address requestedAddress,
        Inet4Address serverAddress, Inet4Address to) {
    // TODO: should we use the transaction ID from the server?
    final int encap = INADDR_ANY.equals(clientAddress)
            ? DhcpPacket.ENCAP_L2 : DhcpPacket.ENCAP_BOOTP;

    ByteBuffer packet = DhcpPacket.buildRequestPacket(
            encap, mTransactionId, getSecs(), clientAddress,
            DO_UNICAST, mHwAddr, requestedAddress,
            serverAddress, REQUESTED_PARAMS, null);
    String serverStr = (serverAddress != null) ? serverAddress.getHostAddress() : null;
    String description = "DHCPREQUEST ciaddr=" + clientAddress.getHostAddress() +
                         " request=" + requestedAddress.getHostAddress() +
                         " serverid=" + serverStr;
    return transmitPacket(packet, description, encap, to);  // 發送數據包
}



return transmitPacket(packet, "DHCPDISCOVER", DhcpPacket.ENCAP_L2, INADDR_BROADCAST); // 發送
}

}

 private boolean transmitPacket(ByteBuffer buf, String description, int encap, Inet4Address to) {
    try {
        if (encap == DhcpPacket.ENCAP_L2) {
            if (DBG) Log.d(TAG, "Broadcasting " + description);
            // 送這裏發送出去  真正發送
            Os.sendto(mPacketSock, buf.array(), 0, buf.limit(), 0, mInterfaceBroadcastAddr);
        } else if (encap == DhcpPacket.ENCAP_BOOTP && to.equals(INADDR_BROADCAST)) {
            if (DBG) Log.d(TAG, "Broadcasting " + description);
            // N.B.: We only need this codepath because DhcpRequestPacket
            // hardcodes the source IP address to 0.0.0.0. We could reuse
            // the packet socket if this ever changes.
            Os.sendto(mUdpSock, buf, 0, to, DhcpPacket.DHCP_SERVER);
        } else {
            if (DBG) Log.d(TAG, String.format("Unicasting %s to %s",description, Os.getpeername(mUdpSock)));
            Os.write(mUdpSock, buf);
        }
    } catch(ErrnoException|IOException e) {
        Log.e(TAG, "Can't send packet: ", e);
        return false;
    }
    return true;
}

===========================================================

另外一邊有一個接收線程ReceiveThread run(),一直在收dhcp server的的數據包。

class ReceiveThread extends Thread {

    private final byte[] mPacket = new byte[DhcpPacket.MAX_LENGTH];
    private volatile boolean mStopped = false;

    public void halt() {
        mStopped = true;
        closeSockets();  // Interrupts the read() call the thread is blocked in.
    }

    @Override
    public void run() {
        if (DBG) Log.d(TAG, "Receive thread started");
        while (!mStopped) {
            int length = 0;  // Or compiler can't tell it's initialized if a parse error occurs.
            try {

                //讀取DHCP服務發出的ACK確認包
                length = Os.read(mPacketSock 【FileDescriptor】, mPacket, 0, mPacket.length); 
                DhcpPacket packet = null;
                packet = DhcpPacket.decodeFullPacket(mPacket, length, DhcpPacket.ENCAP_L2);
                if (DBG) Log.d(TAG, "Received packet: " + packet);

                // 發送接收到消息 CMD_RECEIVED_PACKET  此時處理的狀態是 DhcpRequestingState
                sendMessage(CMD_RECEIVED_PACKET, packet); 
            } catch (IOException|ErrnoException e) {
                if (!mStopped) {
                    Log.e(TAG, "Read error", e);
                    DhcpErrorEvent.logReceiveError(mIfaceName);
                }
            } catch (DhcpPacket.ParseException e) {
                Log.e(TAG, "Can't parse packet: " + e.getMessage());
                if (PACKET_DBG) {
                    Log.d(TAG, HexDump.dumpHexString(mPacket, 0, length));
                }
                DhcpErrorEvent.logParseError(mIfaceName, e.errorCode);
            }
        }
        if (DBG) Log.d(TAG, "Receive thread stopped");
    }

}

// 處理消息 CMD_RECEIVED_PACKET 對應的數據包是 ACK數據包 UDP
class DhcpRequestingState extends PacketRetransmittingState {

        public boolean processMessage(Message message) {
        super.processMessage(message);
        switch (message.what) {
            case CMD_RECEIVED_PACKET:
                receivePacket((DhcpPacket) message.obj);  //DhcpRequestingState處理接收到的數據包
                return HANDLED;
            default:
                return NOT_HANDLED;
        }
    }


        protected void receivePacket(DhcpPacket packet) {
        if (!isValidPacket(packet)) return;
        if ((packet instanceof DhcpAckPacket)) {
            DhcpResults results = packet.toDhcpResults();
            if (results != null) {
                setDhcpLeaseExpiry(packet);
//這裏會調用notifySuccess,發送CMD去通知IpManager說,IP拿到了 分析 acceptDhcpResults
                acceptDhcpResults(results, "Confirmed");  // 分叉

//進入新的狀態 ConfiguringInterfaceState enter方法  
                transitionTo(mConfiguringInterfaceState); 
            }
        } else if (packet instanceof DhcpNakPacket) { // 收到的是NACK 解決數據包的話
            // TODO: Wait a while before returning into INIT state.
            Log.d(TAG, "Received NAK, returning to INIT");
            mOffer = null;
            transitionTo(mDhcpInitState);
        }
    }

}

//這裏會調用notifySuccess,發送CMD去通知IpManager說,IP拿到了 分析 acceptDhcpResults
acceptDhcpResults(results, “Confirmed”);

    private void acceptDhcpResults(DhcpResults results, String msg) {
    mDhcpLease = results;
    mOffer = null;
    Log.d(TAG, msg + " lease: " + mDhcpLease);
    notifySuccess(); // 通知成功拿到IP地址了
}

private void notifySuccess() {
    mController【StateMachine】.sendMessage( CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, new DhcpResults(mDhcpLease));
}



  class StartedState extends State {

    @Override
    public boolean processMessage(Message msg) {
        switch (msg.what) {
              case DhcpClient.CMD_POST_DHCP_ACTION:
                stopDhcpAction();

                switch (msg.arg1) {
                    case DhcpClient.DHCP_SUCCESS:
                        handleIPv4Success((DhcpResults) msg.obj); // 處理 handleIPv4Success IPV4地址
                        break;
                    case DhcpClient.DHCP_FAILURE:
                        handleIPv4Failure();
                        break;
                    default:
                        Log.e(mTag, "Unknown CMD_POST_DHCP_ACTION status:" + msg.arg1);
                }
                break;
  }
  }

}

private void handleIPv4Success(DhcpResults dhcpResults) {
    mDhcpResults = new DhcpResults(dhcpResults);
    final LinkProperties newLp = assembleLinkProperties();
    final ProvisioningChange delta = setLinkProperties(newLp);

    if (VDBG) {
        Log.d(mTag, "onNewDhcpResults(" + Objects.toString(dhcpResults) + ")");
    }
    mCallback.onNewDhcpResults(dhcpResults);
    dispatchCallback(delta, newLp);   //這裏分發dhcpResults 把IPv4地址發出去
}




    private void dispatchCallback(ProvisioningChange delta, LinkProperties newLp) {
    switch (delta) {
        case GAINED_PROVISIONING:
            if (VDBG) { Log.d(mTag, "onProvisioningSuccess()"); }
            recordMetric(IpManagerEvent.PROVISIONING_OK);
            mCallback.onProvisioningSuccess(newLp); // 回調
            break;

    }
}

dispatchCallback 的結果最後會作用到 WaitForProvisioningCallback 


public static class WaitForProvisioningCallback extends Callback {
    private LinkProperties mCallbackLinkProperties;

    public LinkProperties waitForProvisioning() {
        synchronized (this) {
            try {
                wait();
            } catch (InterruptedException e) {}
            return mCallbackLinkProperties;
        }
    }

    @Override
    public void onProvisioningSuccess(LinkProperties newLp) {   // 回調
        synchronized (this) {
            mCallbackLinkProperties = newLp;  // 把當前的IPv4保存起來
            notify();
        }
    }

    @Override
    public void onProvisioningFailure(LinkProperties newLp) {
        synchronized (this) {
            mCallbackLinkProperties = null;
            notify();
        }
    }

} acceptDhcpResults 分析到此爲止 開始分析新的狀態

//進入新的狀態 ConfiguringInterfaceState enter方法
ConfiguringInterfaceState

class ConfiguringInterfaceState extends LoggingState {
    @Override
    public void enter() {
        super.enter();

// 發送消息 CMD_CONFIGURE_LINKADDRESS 被StartedState 處理
mController.sendMessage(CMD_CONFIGURE_LINKADDRESS, mDhcpLease.ipAddress);
}

    @Override
    public boolean processMessage(Message message) {
        super.processMessage(message);
        switch (message.what) {
            case EVENT_LINKADDRESS_CONFIGURED:
                transitionTo(mDhcpBoundState);
                return HANDLED;
            default:
                return NOT_HANDLED;
        }
    }
}


========================

    class StartedState extends State {
    @Override
    public boolean processMessage(Message msg) {
        switch (msg.what) {
                   case DhcpClient.CMD_CONFIGURE_LINKADDRESS: {
                final LinkAddress ipAddress = (LinkAddress) msg.obj;
                if (setIPv4Address(ipAddress)) {  // 設置 IPv4地址
                    mDhcpClient.sendMessage(DhcpClient.EVENT_LINKADDRESS_CONFIGURED);
                } else {
                    Log.e(mTag, "Failed to set IPv4 address!");
                    dispatchCallback(ProvisioningChange.LOST_PROVISIONING,
                            new LinkProperties(mLinkProperties));
                    transitionTo(mStoppingState);
                }
                break;
    }
    }

=========================================
// 回到開頭的 setIPv4Address 至此獲得了IP地址
private boolean setIPv4Address(LinkAddress address) {
final InterfaceConfiguration ifcg = new InterfaceConfiguration();
ifcg.setLinkAddress(address);
try {
final INetworkManagementService mNwService.setInterfaceConfig(mInterfaceName, ifcg);
if (VDBG) Log.d(mTag, “IPv4 configuration succeeded”);
} catch (IllegalStateException | RemoteException e) {
Log.e(mTag, “IPv4 configuration failed: “, e);
return false;
}
return true;
}

NetworkManagementService.java
@Override
public void setInterfaceConfig(String iface, InterfaceConfiguration cfg) {
    mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
    LinkAddress linkAddr = cfg.getLinkAddress();
    if (linkAddr == null || linkAddr.getAddress() == null) {
        throw new IllegalStateException("Null LinkAddress given");
    }

    final Command cmd = new Command("interface", "setcfg", iface,
            linkAddr.getAddress().getHostAddress(),
            linkAddr.getPrefixLength());
    for (String flag : cfg.getFlags()) {
        cmd.appendArg(flag);
    }

    try {
        mConnector.execute(cmd);  //  執行命令
    } catch (NativeDaemonConnectorException e) {
        throw e.rethrowAsParcelableException();
    }
}

http://m.blog.csdn.net/xiaoxiangyuhai/article/details/75219357

發佈了71 篇原創文章 · 獲贊 36 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章