cfwebsocket: aplikasi masa nyata dalam CFML
Panduan cfwebsocket dalam CFML: tag Adobe tidak disokong dalam Lucee; guna sambungan WebSocket, listener dan saluran, serta bila pilih SSE atau pengundian.

Kandungan
- Apa itu cfwebsocket dan kenapa ia mengelirukan
- Ujian dalam enjin teras: apa yang benar-benar ada
- Laluan sebenar Lucee: sambungan WebSocket
- Saluran dan listener
- Listener komponen pertama anda
- Skop static, bukan variables
- Menolak data dari luar sambungan
- Klien JavaScript
- WebSocket lawan SSE lawan pengundian
- Proksi, penimbang beban dan sesi melekit
- Bila SSE atau pengundian lebih baik
- Soalan Lazim
- Sumber
cfwebsocket ialah tag Adobe ColdFusion untuk membuka sambungan WebSocket, tetapi Lucee tidak menyokongnya. Dalam Lucee, aplikasi masa nyata dibina dengan sambungan WebSocket yang memetakan satu komponen CFML kepada satu saluran, dan klien menyambung melalui API WebSocket piawai pelayar. Artikel ini menjelaskan kedua-duanya supaya anda tidak menyalin contoh Adobe ke pelayan Lucee dan gagal, kemudian memandu anda melalui laluan Lucee yang sebenar.
Apa itu cfwebsocket dan kenapa ia mengelirukan
Dalam Adobe ColdFusion, <cfwebsocket> ialah tag yang membuka sambungan WebSocket dan mengikatnya kepada saluran. Bersamanya datang fungsi seperti SubscribeTo() dan Publish(), skop websocket, dan klien JavaScript cfwebsocket. Ini API yang kemas, tetapi ia milik Adobe. Ramai pembangun yang berpindah dari ColdFusion ke Lucee menyalin contoh ini terus ke pelayan mereka dan mendapat ralat, kerana Lucee tidak menghantar API tersebut dalam enjin terasnya.
Dokumentasi rasmi Lucee meletakkan <cfwebsocket> di bawah kategori "Unsupported Tags and Functions", dan halaman rujukannya menyatakan terus: "The <cfwebsocket> tag is not supported by Lucee." Maka istilah carian "cfwebsocket" membawa orang ke jawapan yang mereka tidak jangka: dalam Lucee, cara melakukannya berbeza sama sekali.
Ujian dalam enjin teras: apa yang benar-benar ada
Supaya anda tidak bergantung pada dakwaan, ini hasil yang diuji pada Lucee 7.1 (enjin teras tanpa sebarang sambungan):
<cfwebsocket>: engjin memulangkanundefined tag [cfwebsocket]dan HTTP 500. Tag itu tidak wujud.isDefined("SubscribeTo")danisDefined("Publish"): kedua-duanyafalse. Daripada 779 fungsi bawaan, tiada satu pun bernamaSubscribeToatauPublish.- Skop
websocket:isDefined("websocket")memulangkanfalse. Skop itu tidak wujud.
Kesimpulan yang jujur: <cfwebsocket>, SubscribeTo(), Publish() dan skop websocket ialah API Adobe, bukan API Lucee. Menulisnya ke dalam artikel "tutorial Lucee" tanpa nota ini akan menjadikan artikel itu tutorial yang rosak.
Laluan sebenar Lucee: sambungan WebSocket
Lucee menyediakan WebSocket melalui sambungan rasmi org.lucee:websocket-extension, dengan syarat minimum Lucee 6.2. Sambungan ini memuatkan pengikatan API yang betul secara automatik: javax.websocket pada Lucee 6.x (Tomcat 9) dan jakarta.websocket pada Lucee 7.x (Tomcat 11).
Pasang melalui Administrator Lucee, atau pin dalam persekitaran anda:
LUCEE_EXTENSIONS=org.lucee:websocket-extension:3.0.0.20-SNAPSHOT
WebSocket berjalan pada port yang sama dengan pelayan HTTP anda. Jika Tomcat mendengar pada port 8888, URL sambungan ialah ws://localhost:8888/ws/nama-listener, atau wss:// apabila berada di belakang HTTPS.
Saluran dan listener
Dalam model Lucee, saluran bukan rentetan yang anda daftar secara dinamik. Satu komponen CFML ialah satu saluran. Anda menulis listener sebagai fail .cfc, meletakkannya dalam direktori yang dikonfigurasi (lalainya {lucee-config}/websockets/), dan Lucee mendaftarkan titik akhir /ws/<NamaListener> untuk anda.
Ini berbeza dengan Adobe yang menggunakan atribut channel dalam tag. Kesan praktikalnya sama: klien yang menyambung ke titik akhir yang sama berada dalam saluran yang sama, dan pelayan boleh menolak mesej kepada mereka secara individu atau secara siaran.
Listener komponen pertama anda
Drop fail ini ke direktori listener sebagai EchoListener.cfc. Ia memantul balik mesej masuk dan menyimpan rujukan klien mengikut id pengguna supaya kod lain boleh menolak kepada pengguna tertentu.
component {
// Satu komponen = satu saluran. Keadaan saluran disimpan dalam skop `static`
// supaya ia kongsi merentas setiap panggilan balik dan setiap penolak luar.
static {
clientsByUser = {}; // userId -> wsClient
}
function onOpen( wsClient ) {
arguments.wsClient.send( "SAMBUNG" );
}
function onMessage( wsClient, message ) {
arguments.wsClient.send( "GEMA:" & arguments.message );
}
function onClose( wsClient, reasonPhrase ) {
// Bersihkan rujukan klien yang tertutup.
}
function onError( wsClient, cfCatch ) {
systemOutput( "Ralat WebSocket: " & arguments.cfCatch.message, true );
}
// Penolak statik: boleh dipanggil dari mana-mana kod CFML
// (tugas berjadual, gerbang acara, endpoint REST).
public static boolean function sendToUser( required string userId, required any message ) {
if ( !structKeyExists( static.clientsByUser, arguments.userId ) )
return false;
var client = static.clientsByUser[ arguments.userId ];
if ( !client.isOpen() ) {
structDelete( static.clientsByUser, arguments.userId );
return false;
}
client.send( arguments.message );
return true;
}
}
Setiap panggilan balik menerima objek wsClient yang membawa kaedah send() untuk menghantar kepada klien itu, broadcast() untuk menghantar kepada semua, isOpen() untuk memeriksa sambungan, dan close() untuk menamatkannya. Anda juga boleh memulangkan rentetan daripada onOpen atau onMessage dan rangka kerja menghantarnya untuk anda.
Skop static, bukan variables
Godaan pertama ialah menyimpan senarai klien dalam variables.clientsByUser. Itu tidak berfungsi: setiap panggilan balik boleh menerima instans baharu komponen anda, jadi apa yang disimpan semasa onOpen tidak semestinya kelihatan apabila mesej seterusnya tiba. Skop application juga tidak sesuai kerana setiap panggilan balik berjalan dalam PageContext sintetik tanpa Application.cfc, dan saluran sebenarnya terikat kepada komponen listener itu sendiri, bukan kepada aplikasi.
Skop static ialah jawapan yang tepat. Ia hidup pada kelas komponen, bukan pada instans, jadi setiap panggilan balik dan setiap kod luar melihat stor yang sama. Untuk perincian penuh tentang perbezaan skop ini, lihat memahami skop dalam CFML.
Menolak data dari luar sambungan
WebSocket adalah dua arah, tetapi peraturannya: jangan memacu penolakan daripada gelung dalam onFirstOpen. requestTimeout (lalai 50 saat) akan membunuh gelung itu. Sebaliknya, panggil penolak statik listener daripada tugas berjadual, gerbang acara atau endpoint REST:
// Dalam tugas berjadual, gerbang acara atau endpoint REST:
peristiwa = [ { userId: "42", mesej: "Harga dikemas kini" } ];
for (e in peristiwa) {
EchoListener::sendToUser( userId = e.userId, message = serializeJSON(e) );
}
Ini corak yang sama digunakan oleh endpoint REST untuk memberitahu klien selepas rekod berubah, jadi ia berpasangan semula jadi dengan membina REST API dalam Lucee. Untuk kerja tak segerak yang lebih panjang, cfthread dan tugas berjadual ialah alat yang betul, bukan gelung dalam panggilan balik.
Klien JavaScript
Klien menggunakan API WebSocket piawai pelayar. Tiada perpustakaan cfwebsocket diperlukan, dan tiada lucee.js untuk dimuatkan.
const socket = new WebSocket( "wss://lucee.my/ws/EchoListener" );
socket.onopen = ( evt ) => socket.send( "hello" );
socket.onmessage = ( event ) => console.log( "Terima:", event.data );
socket.onclose = ( evt ) => console.log( "Sambungan ditutup" );
socket.onerror = ( error ) => console.error( "Ralat WebSocket:", error );
Listener yang sama menyokong pertanyaan dalam URL, jadi anda boleh menghantar identiti pengguna pada masa sambungan, contohnya wss://lucee.my/ws/EchoListener?userId=42, dan membacanya dalam onOpen untuk pengesahan.
WebSocket lawan SSE lawan pengundian
WebSocket bukan satu-satunya cara membina ciri masa nyata, dan selalunya bukan yang paling murah. Pilih berdasarkan arah data dan apa yang infrastruktur anda sanggup tanggung.
| Teknik | Arah | Kerumitan | Bila sesuai |
|---|---|---|---|
| WebSocket | Dua hala, pelayan boleh menolak pada bila-bila masa | Tinggi: sambungan kekal, proksi, sesi melekit | Sembang, permainan, terminal, kolaborasi langsung |
| SSE (Server-Sent Events) | Satu hala, pelayan ke klien sahaja | Sederhana: HTTP biasa, sambungan semula automatik | Suapan harga, notis, log langsung, papan pemuka |
| Pengundian (polling) | Satu hala, klien menarik secara berkala | Rendah: permintaan HTTP biasa sahaja | Data jarang berubah, sasaran kecil, tiada sokongan proksi |
Proksi, penimbang beban dan sesi melekit
WebSocket memerlukan sokongan proksi kerana sambungan itu kekal dan mesti mengekalkan peningkatan HTTP (Upgrade). Traefik menyokong WebSocket, tetapi masa menunggu terbiar lalainya sekitar 60 saat akan memutuskan sambungan yang diam, jadi tetapkan idleTimeout yang lebih panjang dalam label atau entrypoint anda. Nginx pula memerlukan proxy_read_timeout dan proxy_send_timeout yang dinaikkan, dan Apache ProxyTimeout. Gejala klasiknya ialah sambungan putus tepat selepas 60 saat, dan itu hampir selalu proksi, bukan enjin.
Apabila anda menjalankan lebih daripada satu instans, klien mesti kekal pada instans yang sama sepanjang hayat sambungannya. Ini kerana keadaan saluran, iaitu skop static dan peta clientsByUser, wujud dalam memori satu JVM sahaja. Konfigurasikan sesi melekit (session affinity) pada penimbang beban anda, dan ingat bahawa siaran daripada satu instans tidak sampai kepada klien pada instans lain melainkan anda menambah bas mesej kongsi seperti Redis. Untuk pelaksanaan Docker, lihat deploy Lucee dengan Docker.
Bila SSE atau pengundian lebih baik
SSE sesuai apabila data mengalir satu arah sahaja. Ia berjalan di atas HTTP biasa, menyambung semula secara automatik apabila terputus, dan tidak memerlukan perubahan proksi atau sesi melekit. Untuk papan pemuka yang menunjukkan harga atau log langsung, SSE melakukan kerja WebSocket dengan separuh kerumitan.
Pengundian pula sesuai apabila perubahan jarang dan kependaman tidak kritikal. Satu permintaan setInterval setiap 30 saat dengan caching di hadapan selalunya lebih murah untuk dioperasi daripada mengekalkan beribu sambungan WebSocket. Peraturannya mudah: jika ciri itu berfungsi dengan polling dan tiada siapa perasan kelewatan, jangan bayar harga WebSocket.
Soalan Lazim
Adakah tag cfwebsocket disokong oleh Lucee?
Tidak. Lucee tidak menyokong <cfwebsocket>, fungsi SubscribeTo() dan Publish(), atau skop websocket. Ini API Adobe ColdFusion. Dalam Lucee, guna sambungan WebSocket dengan komponen listener.
Apakah versi Lucee minimum untuk WebSocket?
Lucee 6.2 dan ke atas. Sambungan memuatkan pengikatan javax.websocket pada Lucee 6.x dan jakarta.websocket pada Lucee 7.x secara automatik, jadi versi Tomcat anda tidak perlu anda urus secara manual.
Bagaimana saya menolak mesej kepada pengguna tertentu?
Simpan rujukan wsClient dalam peta static seperti clientsByUser semasa onOpen, kemudian dedahkan penolak statik seperti sendToUser() yang boleh dipanggil daripada tugas berjadual atau endpoint REST.
Mengapa sambungan saya putus selepas kira-kira 60 saat?
Itu hampir selalu masa menunggu terbiar proksi terbalik anda, bukan Lucee. Naikkan proxy_read_timeout dan proxy_send_timeout pada Nginx, ProxyTimeout pada Apache, atau idleTimeout pada Traefik.
Bilakah saya patut guna SSE atau pengundian dan bukan WebSocket?
Guna SSE apabila data mengalir satu arah dari pelayan ke klien, dan pengundian apabila perubahan jarang dan kependaman tidak penting. Kedua-duanya lebih mudah dioperasi kerana tidak memerlukan sokongan proksi khas atau sesi melekit.