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()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_SERVICEpermission ช่วยให้มั่นใจได้ว่ามีเพียงเฟรมเวิร์กเท่านั้นที่จะเชื่อมโยงกับบริการนี้ได้ หากบริการนี้ไม่จําเป็นต้องใช้สิทธิ์ แอปอื่นอาจเชื่อมโยงกับบริการนี้และส่ง 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);
}

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

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 ทั้งหมดของผู้รับแล้วเท่านั้น อย่างไรก็ตาม มีข้อยกเว้นดังนี้

  • 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 ตัวอย่างเช่น หากไม่ได้ตั้งค่า FLAG_OCCUPANT_ZONE_SCREEN_UNLOCKED ผู้ส่งจะแสดงข้อความแจ้งหรือกล่องโต้ตอบเพื่อแจ้งให้ผู้ใช้ปลดล็อกหน้าจอของโซนผู้นั่งในรถฝั่งที่รับได้

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

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

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

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

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

    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() ดังที่อธิบายไว้ใน(Sender) Request Connection 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() เพื่อลงทะเบียนปลายทางของผู้รับ Use Case ทั่วไปคือ ข้อมูลโค้ดย่อยต้องรับ Payload ดังนั้นจึงลงทะเบียนปลายทางของผู้รับ ดังนี้

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

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

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

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

  • ผู้ส่งระบุ 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 ไปยังผู้รับอีกต่อไป (เช่น 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 รายการเป็นค่าที่ไม่ใช่ 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() อินสแตนซ์CarRemoteDeviceManagerนี้ไม่ได้ลงทะเบียนStateCallback
  • registerReceiver() receiverEndpointId มีการจดทะเบียนอยู่แล้ว
  • unregisterReceiver() receiverEndpointId ไม่ได้จดทะเบียน
  • requestConnection() มีการเชื่อมต่อที่รอดำเนินการหรือเชื่อมต่อแล้ว
  • cancelConnection() ไม่มีการเชื่อมต่อที่รอดำเนินการเพื่อยกเลิก
  • sendPayload() ไม่มีการเชื่อมต่อ
  • disconnect() ไม่มีการเชื่อมต่อ

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

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