API การสื่อสารแบบหลายจอแสดงผล

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

โซนผู้พักอาศัย

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

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

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

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

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

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

ต้องมี Discovery API และ Connection API สำหรับการเชื่อมต่อ พลัง 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()is invoked whenMyReceiverServicehas received aPayloadfrom the sender client.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);

(ผู้ส่ง) Discover

ก่อนที่จะเชื่อมต่อกับไคลเอ็นต์ตัวรับ ไคลเอ็นต์ผู้ส่งควรค้นพบ ไคลเอ็นต์ตัวรับด้วยการลงทะเบียน 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) ด้วยเหตุนี้ แอปจึงไม่ต้องยืนยันว่า สองค่าตกลงกัน

เพื่อประสบการณ์ของผู้ใช้ที่ดียิ่งขึ้น ไคลเอ็นต์ผู้ส่ง CAN จะแสดง UI หากไม่มี Flag ตั้งค่า ตัวอย่างเช่น ถ้าไม่ได้ตั้งค่า 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() จากนั้นการเชื่อมต่อ แล้ว

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

เมื่อเชื่อมต่อแล้ว ผู้ส่ง CAN จะส่ง 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 สามารถใช้บัฟเฟอร์ของโปรโตเพื่อกำหนดประเภทข้อมูล ดังนี้

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

รูปที่ 1 แสดงขั้นตอน Payload

ส่งเพย์โหลด

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

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

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

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

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

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

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

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

แอปไคลเอ็นต์ CAN จะลงทะเบียนเครื่องรับปลายทางหลายตัวตาม 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
    

Null CarRemoteDeviceManager และ CarOccupantConnectionManager

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

  1. บริการรถชน ตามที่เห็นในภาพก่อนหน้านี้ ผู้จัดการทั้ง 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 รายการในไฟล์การกำหนดค่า

      // 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() ไม่มีการเชื่อมต่อที่สร้างขึ้น

Client1 สามารถส่งเพย์โหลดไปยัง client2 ได้ แต่ส่งเพย์โหลดไปยัง client2 ไม่ได้

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