Cara Membuat Server WebSocket Python Sederhana Menggunakan Tornado
Diterbitkan: 2022-03-11Dengan meningkatnya popularitas aplikasi web real-time, WebSockets telah menjadi teknologi kunci dalam implementasinya. Hari-hari di mana Anda harus terus-menerus menekan tombol muat ulang untuk menerima pembaruan dari server sudah lama berlalu. Aplikasi web yang ingin memberikan pembaruan waktu nyata tidak lagi harus melakukan polling ke server untuk perubahan - sebagai gantinya, server mendorong perubahan ke aliran saat terjadi. Kerangka kerja web yang kuat telah mulai mendukung WebSockets di luar kotak. Ruby on Rails 5, misalnya, melangkah lebih jauh dan menambahkan dukungan untuk kabel aksi.
Di dunia Python, ada banyak kerangka kerja web populer. Kerangka kerja seperti Django menyediakan hampir semua yang diperlukan untuk membangun aplikasi web, dan apa pun yang kurang dapat dibuat dengan salah satu dari ribuan plugin yang tersedia untuk Django. Namun, karena cara kerja Python atau sebagian besar kerangka kerja webnya, menangani koneksi yang berumur panjang dapat dengan cepat menjadi mimpi buruk. Model berulir dan kunci penerjemah global sering dianggap sebagai kelemahan Python.
Namun semua itu sudah mulai berubah. Dengan fitur-fitur baru tertentu dari Python 3 dan kerangka kerja yang sudah ada untuk Python, seperti Tornado, menangani koneksi yang berumur panjang tidak lagi menjadi tantangan. Tornado menyediakan kemampuan server web dengan Python yang secara khusus berguna dalam menangani koneksi yang berumur panjang.
Pada artikel ini, kita akan melihat bagaimana server WebSocket sederhana dapat dibangun dengan Python menggunakan Tornado. Aplikasi demo akan memungkinkan kita untuk mengupload file tab-separated values (TSV), mengurainya dan membuat isinya tersedia di URL yang unik.
Tornado dan WebSockets
Tornado adalah pustaka jaringan asinkron dan berspesialisasi dalam menangani jaringan yang digerakkan oleh peristiwa. Karena secara alami dapat menampung puluhan ribu koneksi terbuka secara bersamaan, server dapat memanfaatkan ini dan menangani banyak koneksi WebSocket dalam satu node. WebSocket adalah protokol yang menyediakan saluran komunikasi dupleks penuh melalui satu koneksi TCP. Karena merupakan soket terbuka, teknik ini membuat koneksi web menjadi stateful dan memfasilitasi transfer data real-time ke dan dari server. Server, menjaga status klien, memudahkan penerapan aplikasi obrolan waktu nyata atau game web berdasarkan WebSockets.
WebSockets dirancang untuk diimplementasikan di browser dan server web, dan saat ini didukung di semua browser web utama. Sambungan dibuka sekali dan pesan dapat bolak-balik beberapa kali sebelum sambungan ditutup.
Menginstal Tornado agak sederhana. Itu terdaftar di PyPI dan dapat diinstal menggunakan pip atau easy_install:
pip install tornado
Tornado hadir dengan implementasi WebSockets-nya sendiri. Untuk keperluan artikel ini, cukup banyak yang kita butuhkan.
WebSocket beraksi
Salah satu keuntungan menggunakan WebSocket adalah properti stateful-nya. Ini mengubah cara kita biasanya berpikir tentang komunikasi client-server. Salah satu kasus penggunaan khusus ini adalah di mana server diharuskan melakukan proses lambat yang panjang dan secara bertahap mengalirkan hasil kembali ke klien.
Dalam contoh aplikasi kami, pengguna akan dapat mengunggah file melalui WebSocket. Untuk seluruh masa koneksi, server akan menyimpan file yang diurai dalam memori. Atas permintaan, server kemudian dapat mengirim kembali bagian file ke front-end. Selanjutnya, file akan tersedia di URL yang kemudian dapat dilihat oleh banyak pengguna. Jika file lain diunggah pada URL yang sama, semua orang yang melihatnya akan dapat segera melihat file baru tersebut.
Untuk front-end, kami akan menggunakan AngularJS. Kerangka kerja dan pustaka ini akan memungkinkan kita menangani unggahan file dan pagination dengan mudah. Namun, untuk semua yang terkait dengan WebSockets, kami akan menggunakan fungsi JavaScript standar.
Aplikasi sederhana ini akan dipecah menjadi tiga file terpisah:
- parser.py: tempat server Tornado kami dengan penangan permintaan diimplementasikan
- templates/index.html: template HTML front-end
- static/parser.js: Untuk JavaScript front-end kami
Membuka WebSocket
Dari front-end, koneksi WebSocket dapat dibuat dengan membuat instance objek WebSocket:
new WebSocket(WEBSOCKET_URL);
Ini adalah sesuatu yang harus kita lakukan pada pemuatan halaman. Setelah objek WebSocket dipakai, penangan harus dilampirkan untuk menangani tiga peristiwa penting:
- terbuka: dipecat ketika koneksi dibuat
- pesan: dipecat ketika pesan diterima dari server
- tutup: dipecat ketika koneksi ditutup
$scope.init = function() { $scope.ws = new WebSocket('ws://' + location.host + '/parser/ws'); $scope.ws.binaryType = 'arraybuffer'; $scope.ws.onopen = function() { console.log('Connected.') }; $scope.ws.onmessage = function(evt) { $scope.$apply(function () { message = JSON.parse(evt.data); $scope.currentPage = parseInt(message['page_no']); $scope.totalRows = parseInt(message['total_number']); $scope.rows = message['data']; }); }; $scope.ws.onclose = function() { console.log('Connection is closed...'); }; } $scope.init();
Karena event handler ini tidak akan secara otomatis memicu $scope lifecycle AngularJS, konten dari fungsi handler perlu dibungkus dengan $apply. Jika Anda tertarik, ada paket khusus AngularJS yang memudahkan integrasi WebSocket dalam aplikasi AngularJS.
Perlu disebutkan bahwa koneksi WebSocket yang terputus tidak secara otomatis dibangun kembali, dan akan memerlukan aplikasi untuk mencoba menghubungkan kembali saat pengendali kejadian tutup dipicu. Ini sedikit di luar cakupan artikel ini.
Memilih File untuk Diunggah
Karena kami sedang membangun aplikasi satu halaman menggunakan AngularJS, mencoba mengirimkan formulir dengan file dengan cara lama tidak akan berhasil. Untuk mempermudah, kita akan menggunakan perpustakaan ng-file-upload Danial Farid. Dengan menggunakan which, yang perlu kita lakukan untuk mengizinkan pengguna mengunggah file adalah menambahkan tombol ke template front-end kita dengan arahan AngularJS tertentu:
<button class="btn btn-default" type="file" ngf-select="uploadFile($file, $invalidFiles)" accept=".tsv" ngf-max-size="10MB">Select File</button>
Pustaka, di antara banyak hal, memungkinkan kita untuk mengatur ekstensi dan ukuran file yang dapat diterima. Mengklik tombol ini, sama seperti elemen <input type=”file”>
lainnya, akan membuka pemilih file standar.
Mengunggah File
Saat Anda ingin mentransfer data biner, Anda dapat memilih antara buffer array dan blob. Jika itu hanya data mentah seperti file gambar, pilih gumpalan dan tangani dengan benar di server. Buffer array adalah untuk buffer biner dengan panjang tetap dan file teks seperti TSV dapat ditransfer dalam format string byte. Cuplikan kode ini menunjukkan cara mengunggah file dalam format buffer array.

$scope.uploadFile = function(file, errFiles) { ws = $scope.ws; $scope.f = file; $scope.errFile = errFiles && errFiles[0]; if (file) { reader = new FileReader(); rawData = new ArrayBuffer(); reader.onload = function(evt) { rawData = evt.target.result; ws.send(rawData); } reader.readAsArrayBuffer(file); } }
Arahan ng-file-upload menyediakan fungsi uploadFile. Di sini Anda dapat mengubah file menjadi buffer array menggunakan FileReader, dan mengirimkannya melalui WebSocket.
Perhatikan bahwa mengirim file besar melalui WebSocket dengan membacanya ke dalam buffer array mungkin bukan cara yang paling optimal untuk mengunggahnya karena dapat dengan cepat menempati banyak memori sehingga menghasilkan pengalaman yang buruk.
Terima File di Server
Tornado menentukan jenis pesan menggunakan opcode 4bit, dan mengembalikan str untuk data biner dan unicode untuk teks.
if opcode == 0x1: # UTF-8 data self._message_bytes_in += len(data) try: decoded = data.decode("utf-8") except UnicodeDecodeError: self._abort() return self._run_callback(self.handler.on_message, decoded) elif opcode == 0x2: # Binary data self._message_bytes_in += len(data) self._run_callback(self.handler.on_message, data)
Di server web Tornado, buffer array diterima dalam tipe str.
Dalam contoh ini jenis konten yang kami harapkan adalah TSV, sehingga file diurai dan diubah menjadi kamus. Tentu saja, dalam aplikasi nyata, ada cara yang lebih waras untuk menangani unggahan sewenang-wenang.
def make_message(self, page_no=1): page_size = 100 return { "page_no": page_no, "total_number": len(self.rows), "data": self.rows[page_size * (page_no - 1):page_size * page_no] } def on_message(self, message): if isinstance(message, str): self.rows = [csv.reader([line], delimiter="\t").next() for line in (x.strip() for x in message.splitlines()) if line] self.write_message(self.make_message())
Minta Halaman
Karena tujuan kami adalah menampilkan data TSV yang diunggah dalam potongan halaman kecil, kami memerlukan sarana untuk meminta halaman tertentu. Untuk mempermudah, kami hanya akan menggunakan koneksi WebSocket yang sama untuk mengirim nomor halaman ke server kami.
$scope.pageChanged = function() { ws = $scope.ws; ws.send($scope.currentPage); }
Server akan menerima pesan ini sebagai unicode:
def on_message(self, message): if isinstance(message, unicode): page_no = int(message) self.write_message(self.make_message(page_no))
Mencoba merespons dengan dict dari server Tornado WebSocket akan secara otomatis menyandikannya dalam format JSON. Jadi tidak apa-apa untuk hanya mengirim dict yang berisi 100 baris konten.
Berbagi Akses dengan Orang Lain
Untuk dapat berbagi akses ke unggahan yang sama dengan banyak pengguna, kami harus dapat mengidentifikasi unggahan secara unik. Setiap kali pengguna terhubung ke server melalui WebSocket, UUID acak akan dibuat dan ditetapkan ke koneksi mereka.
def open(self, doc_uuid=None): if doc_uuid is None: self.uuid = str(uuid.uuid4())
uuid.uuid4()
menghasilkan UUID acak dan str() mengubah UUID menjadi string digit hex dalam bentuk standar.
Jika pengguna lain dengan UUID terhubung ke server, instance FileHandler yang sesuai ditambahkan ke kamus dengan UUID sebagai kuncinya dan dihapus saat koneksi ditutup.
@classmethod @tornado.gen.coroutine def add_clients(cls, doc_uuid, client): with (yield lock.acquire()): if doc_uuid in cls.clients: clients_with_uuid = FileHandler.clients[doc_uuid] clients_with_uuid.append(client) else: FileHandler.clients[doc_uuid] = [client] @classmethod @tornado.gen.coroutine def remove_clients(cls, doc_uuid, client): with (yield lock.acquire()): if doc_uuid in cls.clients: clients_with_uuid = FileHandler.clients[doc_uuid] clients_with_uuid.remove(client) if len(clients_with_uuid) == 0: del cls.clients[doc_uuid]
Kamus klien dapat memunculkan KeyError saat menambahkan atau menghapus klien secara bersamaan. Karena Tornado adalah pustaka jaringan asinkron, ia menyediakan mekanisme penguncian untuk sinkronisasi. Kunci sederhana dengan coroutine cocok untuk menangani kamus klien ini.
Jika ada pengguna yang mengunggah file atau berpindah antar halaman, semua pengguna dengan UUID yang sama akan melihat halaman yang sama.
@classmethod def send_messages(cls, doc_uuid): clients_with_uuid = cls.clients[doc_uuid] message = cls.make_message(doc_uuid) for client in clients_with_uuid: try: client.write_message(message) except: logging.error("Error sending message", exc_info=True)
Berlari di Belakang Nginx
Menerapkan WebSockets sangat sederhana, tetapi ada beberapa hal rumit yang perlu dipertimbangkan saat menggunakannya di lingkungan produksi. Tornado adalah server web, sehingga bisa mendapatkan permintaan pengguna secara langsung, tetapi menerapkannya di belakang Nginx mungkin merupakan pilihan yang lebih baik karena berbagai alasan. Namun, dibutuhkan sedikit lebih banyak upaya untuk dapat menggunakan WebSockets melalui Nginx:
http { upstream parser { server 127.0.0.1:8080; } server { location ^~ /parser/ws { proxy_pass http://parser; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } }
Dua arahan proxy_set_header
membuat Nginx meneruskan header yang diperlukan ke server back-end yang diperlukan untuk meningkatkan koneksi ke WebSocket.
Apa berikutnya?
Pada artikel ini, kami mengimplementasikan aplikasi web Python sederhana yang menggunakan WebSockets untuk memelihara koneksi persisten antara server dan setiap klien. Dengan kerangka kerja jaringan asinkron modern seperti Tornado, memegang puluhan ribu koneksi terbuka secara bersamaan dengan Python sepenuhnya layak dilakukan.
Meskipun aspek implementasi tertentu dari aplikasi demo ini dapat dilakukan secara berbeda, saya harap ini tetap membantu menunjukkan penggunaan WebSockets di kerangka https://www.toptal.com/tornado. Kode sumber aplikasi demo tersedia di GitHub.