Multi-Display Communications API

แอปที่มีสิทธิ์ระดับระบบใน AAOS สามารถใช้ Multi-Display Communications API เพื่อสื่อสารกับแอปเดียวกัน (ชื่อแพ็กเกจเดียวกัน) ที่ทำงานในโซนที่มีการเข้าใช้อื่นๆ ในรถ หน้านี้อธิบายวิธีผสานรวม API หากต้องการเรียนรู้ เพิ่มเติม คุณสามารถดู CarOccupantZoneManager.OccupantZoneInfo ได้ด้วย

โซนที่มีการเข้าใช้

แนวคิดเรื่อง โซนที่มีการเข้าใช้ จะจับคู่ผู้ใช้กับชุดจอแสดงผล โซนที่มีการเข้าใช้แต่ละโซนจะมีจอแสดงผลประเภท DISPLAY_TYPE_MAIN นอกจากนี้ โซนที่มีการเข้าใช้อาจมีจอแสดงผลเพิ่มเติม เช่น จอแสดงผลคลัสเตอร์ ระบบจะกำหนดผู้ใช้ Android ให้กับโซนที่มีการเข้าใช้แต่ละโซน ผู้ใช้แต่ละคนจะมีบัญชีและแอปของตนเอง

การกำหนดค่าฮาร์ดแวร์

Comms API รองรับ SoC เดียวเท่านั้น ในโมเดล SoC เดียว โซนที่มีการเข้าใช้และผู้ใช้ทั้งหมดจะทำงานบน SoC เดียวกัน Comms API ประกอบด้วย 3 องค์ประกอบ ได้แก่

  • Power Management API ช่วยให้ไคลเอ็นต์จัดการกำลังไฟฟ้าของจอแสดงผลในโซนที่มีการเข้าใช้ได้

  • Discovery API ช่วยให้ไคลเอ็นต์ตรวจสอบสถานะของโซนที่มีการเข้าใช้อื่นๆ ในรถ และตรวจสอบไคลเอ็นต์เทียบเท่าในโซนที่มีการเข้าใช้เหล่านั้นได้ ใช้ Discovery API ก่อนใช้ Connection API

  • Connection API ช่วยให้ไคลเอ็นต์เชื่อมต่อกับไคลเอ็นต์เทียบเท่าในโซนที่มีการเข้าใช้อื่น และส่งเพย์โหลดไปยังไคลเอ็นต์เทียบเท่าได้

ต้องใช้ Discovery API และ Connection API ในการเชื่อมต่อ Power Management API เป็นตัวเลือก

Comms API ไม่รองรับการสื่อสารระหว่างแอปต่างๆ แต่ได้รับการออกแบบมาเท่านั้น สำหรับการสื่อสารระหว่างแอปที่มีชื่อแพ็กเกจเดียวกัน และใช้เท่านั้น สำหรับการสื่อสารระหว่างผู้ใช้ที่มองเห็นได้

คู่มือการผสานรวม

ใช้ AbstractReceiverService

หากต้องการรับ Payload แอปผู้รับต้องใช้เมธอดแบบนามธรรมที่กำหนดไว้ใน AbstractReceiverService เช่น

public class MyReceiverService extends AbstractReceiverService {

    @Override
    public void onConnectionInitiated(@NonNull OccupantZoneInfo senderZone) {
    }

    @Override
    public void onPayloadReceived(@NonNull OccupantZoneInfo senderZone,
            @NonNull Payload payload) {
    }
}

ระบบจะเรียกใช้ onConnectionInitiated() เมื่อไคลเอ็นต์ผู้ส่งขอเชื่อมต่อกับไคลเอ็นต์ผู้รับนี้ หากต้องมีการยืนยันจากผู้ใช้เพื่อสร้างการเชื่อมต่อ MyReceiverService สามารถ ลบล้างเมธอดนี้เพื่อเปิดใช้กิจกรรมการขอสิทธิ์ และเรียกใช้ acceptConnection() หรือ rejectConnection() ตามผลลัพธ์ หรือ MyReceiverService สามารถ เรียกใช้ acceptConnection() ได้เลย

ระบบจะเรียกใช้ onPayloadReceived() เมื่อ MyReceiverService ได้รับ Payload จากไคลเอ็นต์ผู้ส่ง MyReceiverService สามารถ ลบล้างเมธอดนี้เพื่อทำสิ่งต่อไปนี้

  • ส่งต่อ Payload ไปยังปลายทางผู้รับที่เกี่ยวข้อง(หากมี) หากต้องการรับปลายทางผู้รับที่ลงทะเบียนไว้ ให้เรียกใช้ getAllReceiverEndpoints() หากต้องการส่งต่อ Payload ไปยังปลายทางผู้รับที่ระบุ ให้เรียกใช้ forwardPayload()

หรือ

  • แคช Payload และส่งเมื่อลงทะเบียนปลายทางผู้รับที่คาดไว้ ซึ่ง MyReceiverService จะได้รับการแจ้งเตือนผ่าน onReceiverRegistered()

ประกาศ AbstractReceiverService

แอปผู้รับต้องประกาศ AbstractReceiverService ที่ใช้ในไฟล์ Manifest เพิ่มตัวกรอง Intent ที่มีการดำเนินการ android.car.intent.action.RECEIVER_SERVICE สำหรับบริการนี้ และกำหนดให้ต้องมีสิทธิ์ android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE

<service android:name=".MyReceiverService"
         android:permission="android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE"
         android:exported="true">
    <intent-filter>
        <action android:name="android.car.intent.action.RECEIVER_SERVICE" />
    </intent-filter>
</service>

สิทธิ์ android.car.occupantconnection.permission.BIND_RECEIVER_SERVICE ช่วยให้มั่นใจได้ว่าเฉพาะเฟรมเวิร์กเท่านั้นที่จะผูกกับบริการนี้ได้ หากบริการนี้ไม่จำเป็นต้องมีสิทธิ์ แอปอื่นอาจผูกกับบริการนี้และส่ง Payload ไปยังบริการนี้โดยตรงได้

ประกาศสิทธิ์

แอปไคลเอ็นต์ต้องประกาศสิทธิ์ในไฟล์ Manifest

<!-- This permission is needed for connection API -->
<uses-permission android:name="android.car.permission.MANAGE_OCCUPANT_CONNECTION"/>
<!-- This permission is needed for discovery API -->
<uses-permission android:name="android.car.permission.MANAGE_REMOTE_DEVICE"/>
<!-- This permission is needed if the client app calls CarRemoteDeviceManager#setOccupantZonePower() -->
<uses-permission android:name="android.car.permission.CAR_POWER"/>

สิทธิ์ทั้ง 3 รายการข้างต้นเป็นสิทธิ์ที่มีอภิสิทธิ์ ซึ่งต้องได้รับอนุญาตล่วงหน้าจากไฟล์รายการที่อนุญาต ตัวอย่างเช่น นี่คือไฟล์รายการที่อนุญาตของแอป MultiDisplayTest

// packages/services/Car/data/etc/com.google.android.car.multidisplaytest.xml
<permissions>
    <privapp-permissions package="com.google.android.car.multidisplaytest">
         
        <permission name="android.car.permission.MANAGE_OCCUPANT_CONNECTION"/>
        <permission name="android.car.permission.MANAGE_REMOTE_DEVICE"/>
        <permission name="android.car.permission.CAR_POWER"/>
    </privapp-permissions>
</permissions>

รับเครื่องมือจัดการรถยนต์

หากต้องการใช้ API แอปไคลเอ็นต์ต้องลงทะเบียน CarServiceLifecycleListener เพื่อรับเครื่องมือจัดการรถยนต์ที่เชื่อมโยง

private CarRemoteDeviceManager mRemoteDeviceManager;
private CarOccupantConnectionManager mOccupantConnectionManager;

private final Car.CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
   if (!ready) {
       Log.w(TAG, "Car service crashed");
       mRemoteDeviceManager = null;
       mOccupantConnectionManager = null;
       return;
   }
   mRemoteDeviceManager = car.getCarManager(CarRemoteDeviceManager.class);
   mOccupantConnectionManager = car.getCarManager(CarOccupantConnectionManager.class);
};

Car.createCar(getContext(), /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
       mCarServiceLifecycleListener);

(ผู้ส่ง) ค้นหา

ก่อนเชื่อมต่อกับไคลเอ็นต์ผู้รับ ไคลเอ็นต์ผู้ส่งควรค้นหาไคลเอ็นต์ผู้รับโดยลงทะเบียน CarRemoteDeviceManager.StateCallback

// The maps are accessed by the main thread only, so there is no multi-thread issue.
private final ArrayMap<OccupantZoneInfo, Integer> mOccupantZoneStateMap = new ArrayMap<>();
private final ArrayMap<OccupantZoneInfo, Integer> mAppStateMap = new ArrayMap<>();

private final StateCallback mStateCallback = new StateCallback() {
        @Override
        public void onOccupantZoneStateChanged(
                @androidx.annotation.NonNull OccupantZoneInfo occupantZone,
                int occupantZoneStates) {
            mOccupantZoneStateMap.put(occupantZone, occupantZoneStates);
        }
        @Override
        public void onAppStateChanged(
                @androidx.annotation.NonNull OccupantZoneInfo occupantZone,
                int appStates) {
            mAppStateMap.put(occupantZone, appStates);
        }
    };

if (mRemoteDeviceManager != null) {
   mRemoteDeviceManager.registerStateCallback(getActivity().getMainExecutor(),
           mStateCallback);
}

ก่อนขอเชื่อมต่อกับผู้รับ ผู้ส่งควรตรวจสอบว่าได้ตั้งค่าแฟล็กทั้งหมดของโซนที่มีการเข้าใช้ของผู้รับและแอปผู้รับแล้ว มิฉะนั้นอาจเกิดข้อผิดพลาดได้ เช่น

private boolean canRequestConnectionToReceiver(OccupantZoneInfo receiverZone) {
    Integer zoneState = mOccupantZoneStateMap.get(receiverZone);
    if ((zoneState == null) || (zoneState.intValue() & (FLAG_OCCUPANT_ZONE_POWER_ON
            // FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED is not implemented yet. Right now
            // just ignore this flag.
            //  | FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED
            | FLAG_OCCUPANT_ZONE_CONNECTION_READY)) == 0) {
        return false;
    }
    Integer appState = mAppStateMap.get(receiverZone);
    if ((appState == null) ||
        (appState.intValue() & (FLAG_CLIENT_INSTALLED
            | FLAG_CLIENT_SAME_LONG_VERSION | FLAG_CLIENT_SAME_SIGNATURE
            | FLAG_CLIENT_RUNNING | FLAG_CLIENT_IN_FOREGROUND)) == 0) {
        return false;
    }
    return true;
}

เราขอแนะนำให้ผู้ส่งขอเชื่อมต่อกับผู้รับเมื่อตั้งค่าแฟล็กทั้งหมดของผู้รับแล้วเท่านั้น อย่างไรก็ตาม มีข้อยกเว้นดังนี้

  • FLAG_OCCUPANT_ZONE_CONNECTION_READY และ FLAG_CLIENT_INSTALLED เป็นข้อกำหนดขั้นต่ำที่จำเป็นในการสร้างการเชื่อมต่อ

  • หากแอปผู้รับต้องแสดง UI เพื่อขออนุมัติการเชื่อมต่อจากผู้ใช้ FLAG_OCCUPANT_ZONE_POWER_ON และ FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED จะกลายเป็นข้อกำหนดเพิ่มเติม นอกจากนี้ เรายังแนะนำให้ใช้ FLAG_CLIENT_RUNNING และ FLAG_CLIENT_IN_FOREGROUND เพื่อให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดียิ่งขึ้น มิฉะนั้นผู้ใช้อาจรู้สึกประหลาดใจ

  • ขณะนี้ (Android 15) ระบบยังไม่ได้ใช้ FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED แอปไคลเอ็นต์สามารถข้ามแฟล็กนี้ไปได้

  • ขณะนี้ (Android 15) Comms API รองรับผู้ใช้หลายรายในอินสแตนซ์ Android เดียวกันเท่านั้น เพื่อให้แอปเทียบเท่ามีรหัสเวอร์ชันยาว (FLAG_CLIENT_SAME_LONG_VERSION) และลายเซ็น (FLAG_CLIENT_SAME_SIGNATURE) เดียวกันได้ ด้วยเหตุนี้ แอปจึงไม่จำเป็นต้องยืนยันว่าค่าทั้ง 2 ค่าตรงกัน

แอปไคลเอ็นต์ผู้ส่ง**สามารถ** แสดง UI ได้หากไม่ได้ตั้งค่าแฟล็ก เช่น หากไม่ได้ตั้งค่า FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED ผู้ส่งสามารถแสดงข้อความโทสต์หรือกล่องโต้ตอบเพื่อแจ้งให้ผู้ใช้ปลดล็อกหน้าจอของโซนที่มีการเข้าใช้ของผู้รับ

เมื่อผู้ส่งไม่จำเป็นต้องค้นหาผู้รับอีกต่อไป (เช่น เมื่อผู้ส่ง พบผู้รับทั้งหมดและสร้างการเชื่อมต่อแล้ว หรือเมื่อไม่มีการใช้งาน) ผู้ส่งสามารถ หยุดการค้นหาได้

if (mRemoteDeviceManager != null) {
    mRemoteDeviceManager.unregisterStateCallback();
}

เมื่อหยุดการค้นหา การเชื่อมต่อที่มีอยู่จะไม่ได้รับผลกระทบ ผู้ส่งสามารถส่ง Payload ไปยังผู้รับที่เชื่อมต่ออยู่ต่อไปได้

(ผู้ส่ง) ขอเชื่อมต่อ

เมื่อตั้งค่าแฟล็กทั้งหมดของผู้รับแล้ว ผู้ส่ง**สามารถ** ขอเชื่อมต่อ กับผู้รับได้

    private final ConnectionRequestCallback mRequestCallback = new ConnectionRequestCallback() {
        @Override
        public void onConnected(OccupantZoneInfo receiverZone) {
        }

        @Override
        public void onFailed(OccupantZoneInfo receiverZone, int connectionError) {
        }

        @Override
        public void onDisconnected(OccupantZoneInfo receiverZone) {
        }
    };

if (mOccupantConnectionManager != null && canRequestConnectionToReceiver(receiverZone)) {
    mOccupantConnectionManager.requestConnection(receiverZone,
                getActivity().getMainExecutor(), mRequestCallback);
}

(บริการผู้รับ) ยอมรับการเชื่อมต่อ

เมื่อผู้ส่งขอเชื่อมต่อกับผู้รับ AbstractReceiverService ในแอปผู้รับจะผูกโดยบริการรถยนต์ และ AbstractReceiverService.onConnectionInitiated() จะถูกเรียกใช้ ตามที่ อธิบายไว้ใน (ผู้ส่ง) ขอเชื่อมต่อ, onConnectionInitiated() เป็นเมธอดแบบนามธรรมและแอปไคลเอ็นต์ต้องใช้เมธอดนี้

เมื่อผู้รับยอมรับคำขอเชื่อมต่อ ระบบจะเรียกใช้ ConnectionRequestCallback.onConnected() ของผู้ส่ง จากนั้นการเชื่อมต่อจะสร้างขึ้น

(ผู้ส่ง) ส่งเพย์โหลด

เมื่อสร้างการเชื่อมต่อแล้ว ผู้ส่ง**สามารถ** ส่ง Payload ไปยัง ผู้รับได้

if (mOccupantConnectionManager != null) {
    Payload payload = ...;
    try {
        mOccupantConnectionManager.sendPayload(receiverZone, payload);
    } catch (CarOccupantConnectionManager.PayloadTransferException e) {
        Log.e(TAG, "Failed to send Payload to " + receiverZone);
    }
}

ผู้ส่งสามารถใส่ออบเจ็กต์ Binder หรืออาร์เรย์ไบต์ใน Payload ได้ หากผู้ส่งต้องการส่งข้อมูลประเภทอื่น ผู้ส่งต้องซีเรียลไลซ์ข้อมูลเป็นอาร์เรย์ไบต์ ใช้อาร์เรย์ไบต์เพื่อสร้างออบเจ็กต์ Payload และส่ง Payload จากนั้นไคลเอ็นต์ผู้รับจะได้รับอาร์เรย์ไบต์จาก Payload ที่ได้รับ และดีซีเรียลไลซ์อาร์เรย์ไบต์เป็นออบเจ็กต์ข้อมูลที่คาดไว้ ตัวอย่างเช่น หากผู้ส่งต้องการส่งสตริง hello ไปยังปลายทางผู้รับที่มีรหัส FragmentB ผู้ส่งสามารถใช้ Proto Buffers เพื่อกำหนดประเภทข้อมูลดังนี้

message MyData {
  required string receiver_endpoint_id = 1;
  required string data = 2;
}

รูปที่ 1 แสดงโฟลว์ Payload

ส่งเพย์โหลด

รูปที่ 1 ส่งเพย์โหลด

(บริการผู้รับ) รับและส่งเพย์โหลด

เมื่อแอปผู้รับได้รับ Payload ระบบจะเรียกใช้ AbstractReceiverService.onPayloadReceived() ของแอป ตามที่อธิบายไว้ใน ส่งเพย์โหลด onPayloadReceived() เป็น เมธอดแบบนามธรรมและแอปไคลเอ็นต์ต้องใช้เมธอดนี้ ในเมธอดนี้ ไคลเอ็นต์**สามารถ** ส่งต่อ Payload ไปยังปลายทางผู้รับที่เกี่ยวข้อง หรือ แคช Payload แล้วส่งเมื่อลงทะเบียนปลายทางผู้รับที่ คาดไว้

(ปลายทางผู้รับ) ลงทะเบียนและยกเลิกการลงทะเบียน

แอปผู้รับควรเรียกใช้ registerReceiver() เพื่อลงทะเบียนปลายทางผู้รับ กรณีการใช้งานทั่วไปคือ Fragment ต้องรับ Payload จึงลงทะเบียนปลายทางผู้รับ

private final PayloadCallback mPayloadCallback = (senderZone, payload) -> {
    
};

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.registerReceiver("FragmentB",
                getActivity().getMainExecutor(), mPayloadCallback);
}

เมื่อ AbstractReceiverService ในไคลเอ็นต์ผู้รับส่ง Payload ไปยังปลายทางผู้รับ ระบบจะเรียกใช้ PayloadCallback ที่เชื่อมโยง

แอปไคลเอ็นต์**สามารถ** ลงทะเบียนปลายทางผู้รับหลายรายการได้ตราบใดที่ receiverEndpointIdของปลายทางเหล่านั้นไม่ซ้ำกันในแอปไคลเอ็นต์ receiverEndpointId จะถูกใช้โดย AbstractReceiverService เพื่อตัดสินใจว่าจะส่งเพย์โหลดไปยังปลายทางผู้รับ ใด เช่น

  • ผู้ส่งระบุ receiver_endpoint_id:FragmentB ใน Payload เมื่อ ได้รับ Payload AbstractReceiverService ในผู้รับจะเรียกใช้ forwardPayload("FragmentB", payload) เพื่อส่งเพย์โหลดไปยัง FragmentB
  • ผู้ส่งระบุ data_type:VOLUME_CONTROL ใน Payload เมื่อ ได้รับ Payload AbstractReceiverService ในผู้รับจะทราบ ว่าควรส่ง Payload ประเภทนี้ไปยัง FragmentB จึงเรียกใช้ forwardPayload("FragmentB", payload)
if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.unregisterReceiver("FragmentB");
}

(ผู้ส่ง) สิ้นสุดการเชื่อมต่อ

เมื่อผู้ส่งไม่จำเป็นต้องส่ง Payload ไปยังผู้รับอีกต่อไป (เช่น เมื่อไม่มีการใช้งาน) ผู้ส่งควรสิ้นสุดการเชื่อมต่อ

if (mOccupantConnectionManager != null) {
    mOccupantConnectionManager.disconnect(receiverZone);
}

เมื่อตัดการเชื่อมต่อแล้ว ผู้ส่งจะไม่สามารถส่ง Payload ไปยังผู้รับได้อีก

โฟลว์การเชื่อมต่อ

รูปที่ 2 แสดงโฟลว์การเชื่อมต่อ

ขั้นตอนการเชื่อมต่อ

รูปที่ 2 โฟลว์การเชื่อมต่อ

การแก้ปัญหา

ตรวจสอบบันทึก

วิธีตรวจสอบบันทึกที่เกี่ยวข้อง

  1. เรียกใช้คำสั่งต่อไปนี้เพื่อบันทึก

    adb shell setprop log.tag.CarRemoteDeviceService VERBOSE && adb shell setprop log.tag.CarOccupantConnectionService VERBOSE && adb logcat -s "AbstractReceiverService","CarOccupantConnectionManager","CarRemoteDeviceManager","CarRemoteDeviceService","CarOccupantConnectionService"
  2. หากต้องการดัมพ์สถานะภายในของ CarRemoteDeviceService และ CarOccupantConnectionService ให้ทำดังนี้

    adb shell dumpsys car_service --services CarRemoteDeviceService && adb shell dumpsys car_service --services CarOccupantConnectionService

CarRemoteDeviceManager และ CarOccupantConnectionManager เป็น Null

ตรวจสอบสาเหตุที่เป็นไปได้ดังนี้

  1. บริการรถยนต์ขัดข้อง ตามที่แสดงไว้ก่อนหน้านี้ ระบบจะรีเซ็ตเครื่องมือจัดการทั้ง 2 รายการให้เป็น null โดยเจตนาเมื่อบริการรถยนต์ขัดข้อง เมื่อรีสตาร์ทบริการรถยนต์ ระบบจะตั้งค่าเครื่องมือจัดการทั้ง 2 รายการเป็นค่าที่ไม่ใช่ Null

  2. ไม่ได้เปิดใช้ CarRemoteDeviceService หรือ CarOccupantConnectionService หากต้องการตรวจสอบว่าได้เปิดใช้บริการใดบริการหนึ่งหรือไม่ ให้เรียกใช้คำสั่งต่อไปนี้

    adb shell dumpsys car_service --services CarFeatureController
    • มองหา mDefaultEnabledFeaturesFromConfig ซึ่งควรมี car_remote_device_service และ car_occupant_connection_service เช่น

      mDefaultEnabledFeaturesFromConfig:[car_evs_service, car_navigation_service, car_occupant_connection_service, car_remote_device_service, car_telemetry_service, cluster_home_service, com.android.car.user.CarUserNoticeService, diagnostic, storage_monitoring, vehicle_map_service]
      
    • โดยค่าเริ่มต้น ระบบจะปิดใช้บริการทั้ง 2 รายการนี้ เมื่ออุปกรณ์รองรับจอแสดงผลหลายจอ คุณต้องวางซ้อนไฟล์การกำหนดค่านี้ คุณสามารถเปิดใช้บริการทั้ง 2 รายการในไฟล์การกำหนดค่าได้ดังนี้

      // packages/services/Car/service/res/values/config.xml
      <string-array translatable="false" name="config_allowed_optional_car_features">
           <item>car_occupant_connection_service</item>
           <item>car_remote_device_service</item>
            
      </string-array>
      

ข้อยกเว้นเมื่อเรียก API

หากแอปไคลเอ็นต์ไม่ได้ใช้ API ตามที่ตั้งใจไว้ อาจเกิดข้อยกเว้นได้ ในกรณีนี้ แอปไคลเอ็นต์สามารถตรวจสอบข้อความในข้อยกเว้นและสแต็กการขัดข้องเพื่อแก้ไขปัญหาได้ ตัวอย่างการใช้ API ในทางที่ผิดมีดังนี้

  • registerStateCallback() ไคลเอ็นต์นี้ลงทะเบียน StateCallback ไว้แล้ว
  • unregisterStateCallback() ไม่ได้ลงทะเบียน StateCallback โดยอินสแตนซ์ CarRemoteDeviceManager นี้
  • registerReceiver() receiverEndpointId ลงทะเบียนไว้แล้ว
  • unregisterReceiver() ไม่ได้ลงทะเบียน receiverEndpointId
  • requestConnection() มีการเชื่อมต่อที่รอดำเนินการหรือสร้างไว้แล้ว
  • cancelConnection() ไม่มีการเชื่อมต่อที่รอดำเนินการให้ยกเลิก
  • sendPayload() ไม่มีการเชื่อมต่อที่สร้างไว้
  • disconnect() ไม่มีการเชื่อมต่อที่สร้างไว้

ไคลเอ็นต์ 1 ส่งเพย์โหลดไปยังไคลเอ็นต์ 2 ได้ แต่ไคลเอ็นต์ 2 ส่งเพย์โหลดไปยังไคลเอ็นต์ 1 ไม่ได้

การเชื่อมต่อได้รับการออกแบบมาให้เป็นแบบทางเดียว หากต้องการสร้างการเชื่อมต่อแบบ 2 ทาง ทั้ง client1 และ client2 ต้องขอเชื่อมต่อกันและกัน จากนั้นจึง ขออนุมัติ