Tornado Kullanarak Basit Bir Python WebSocket Sunucusu Nasıl Oluşturulur
Yayınlanan: 2022-03-11Gerçek zamanlı web uygulamalarının popülaritesinin artmasıyla birlikte WebSockets, uygulamalarında kilit bir teknoloji haline geldi. Sunucudan güncellemeleri almak için sürekli olarak yeniden yükle düğmesine basmanız gereken günler geride kaldı. Gerçek zamanlı güncellemeler sağlamak isteyen web uygulamalarının artık değişiklikler için sunucuyu yoklaması gerekmiyor - bunun yerine sunucular, değişiklikleri olduğu gibi akışı aşağı itiyor. Sağlam web çerçeveleri, WebSockets'i kutudan çıkarmaya başladı. Örneğin Ruby on Rails 5, bunu daha da ileri götürdü ve aksiyon kabloları için destek ekledi.
Python dünyasında birçok popüler web çerçevesi mevcuttur. Django gibi çerçeveler, web uygulamaları oluşturmak için gereken hemen hemen her şeyi sağlar ve eksik olan her şey, Django için mevcut binlerce eklentiden biri ile yapılabilir. Bununla birlikte, Python'un veya web çerçevelerinin çoğunun çalışma şekli nedeniyle, uzun ömürlü bağlantıları yönetmek hızla bir kabusa dönüşebilir. Dişli model ve global yorumlayıcı kilidi genellikle Python'un aşil topuğu olarak kabul edilir.
Ama bunların hepsi değişmeye başladı. Python 3'ün belirli yeni özellikleri ve Tornado gibi Python için zaten mevcut olan çerçeveler sayesinde, uzun ömürlü bağlantıları yönetmek artık bir zorluk değil. Tornado, Python'da, özellikle uzun ömürlü bağlantıların işlenmesinde yararlı olan web sunucusu yetenekleri sağlar.
Bu yazıda, Tornado kullanarak Python'da basit bir WebSocket sunucusunun nasıl oluşturulabileceğine bir göz atacağız. Demo uygulaması, sekmeyle ayrılmış değerler (TSV) dosyası yüklememize, onu ayrıştırmamıza ve içeriğini benzersiz bir URL'de kullanıma sunmamıza olanak tanır.
Tornado ve WebSockets
Tornado, eşzamansız bir ağ kitaplığıdır ve olay odaklı ağ oluşturma konusunda uzmanlaşmıştır. Doğal olarak on binlerce açık bağlantıyı aynı anda tutabildiğinden, bir sunucu bundan faydalanabilir ve tek bir düğüm içinde çok sayıda WebSocket bağlantısını işleyebilir. WebSocket, tek bir TCP bağlantısı üzerinden tam çift yönlü iletişim kanalları sağlayan bir protokoldür. Açık bir soket olduğu için, bu teknik bir web bağlantısını durumsal hale getirir ve sunucuya ve sunucudan gerçek zamanlı veri aktarımını kolaylaştırır. İstemcilerin durumlarını tutan sunucu, WebSockets tabanlı gerçek zamanlı sohbet uygulamalarının veya web oyunlarının uygulanmasını kolaylaştırır.
WebSockets, web tarayıcılarında ve sunucularında uygulanmak üzere tasarlanmıştır ve şu anda tüm büyük web tarayıcılarında desteklenmektedir. Bir bağlantı bir kez açılır ve bağlantı kapatılmadan önce mesajlar birçok kez ileri geri gidebilir.
Tornado'yu kurmak oldukça basittir. PyPI'de listelenmiştir ve pip veya easy_install kullanılarak kurulabilir:
pip install tornadoTornado, kendi WebSockets uygulamasıyla birlikte gelir. Bu makalenin amaçları doğrultusunda, ihtiyacımız olan tek şey bu.
WebSockets İş Başında
WebSocket kullanmanın avantajlarından biri durum bilgisidir. Bu, tipik olarak istemci-sunucu iletişimini düşünme biçimimizi değiştirir. Bunun özel bir kullanım durumu, sunucunun uzun, yavaş işlemler gerçekleştirmesi ve sonuçları kademeli olarak istemciye geri göndermesi gerektiği yerdir.
Örnek uygulamamızda kullanıcı WebSocket üzerinden dosya upload edebilecektir. Bağlantının tüm ömrü boyunca sunucu, ayrıştırılan dosyayı bellekte tutacaktır. İstek üzerine, sunucu dosyanın bölümlerini ön uca geri gönderebilir. Ayrıca, dosya daha sonra birden fazla kullanıcı tarafından görüntülenebilecek bir URL'de sunulacaktır. Aynı URL'ye başka bir dosya yüklenirse, ona bakan herkes yeni dosyayı hemen görebilir.
Ön uç için AngularJS kullanacağız. Bu çerçeve ve kitaplıklar, dosya yüklemelerini ve sayfalandırmayı kolayca işlememize olanak tanır. Ancak WebSockets ile ilgili her şey için standart JavaScript işlevlerini kullanacağız.
Bu basit uygulama üç ayrı dosyaya bölünecektir:
- parser.py: Tornado sunucumuzun istek işleyicileriyle birlikte uygulandığı yer
- templates/index.html: ön uç HTML şablonu
- static/parser.js: Ön uç JavaScript'imiz için
WebSocket Açma
Ön uçtan, bir WebSocket nesnesi başlatılarak bir WebSocket bağlantısı kurulabilir:
new WebSocket(WEBSOCKET_URL);Bu, sayfa yüklemede yapmamız gereken bir şey. Bir WebSocket nesnesi başlatıldığında, üç önemli olayı işlemek için işleyiciler eklenmelidir:
- open: bir bağlantı kurulduğunda ateşlenir
- mesaj: sunucudan bir mesaj alındığında ateşlenir
- kapat: bir bağlantı kapatıldığında ateşlenir
$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();Bu olay işleyiciler, AngularJS'nin $scope yaşam döngüsünü otomatik olarak tetiklemeyeceklerinden, işleyici işlevinin içeriğinin $apply içine sarılması gerekir. İlgileniyorsanız, WebSocket'i AngularJS uygulamalarına entegre etmeyi kolaylaştıran AngularJS'ye özel paketler mevcuttur.
Bırakılan WebSocket bağlantılarının otomatik olarak yeniden kurulmadığını ve kapatma olay işleyicisi tetiklendiğinde uygulamanın yeniden bağlanmayı denemesini gerektireceğini belirtmekte fayda var. Bu, bu makalenin kapsamını biraz aşıyor.
Yüklenecek Dosyayı Seçme
AngularJS kullanarak tek sayfalık bir uygulama oluşturduğumuz için, dosyaları içeren formları eski yollardan göndermeye çalışmak işe yaramaz. İşleri kolaylaştırmak için Danial Farid'in ng-file-upload kitaplığını kullanacağız. Bunu kullanarak, bir kullanıcının dosya yüklemesine izin vermek için tek yapmamız gereken, belirli AngularJS yönergeleriyle ön uç şablonumuza bir düğme eklemektir:
<button class="btn btn-default" type="file" ngf-select="uploadFile($file, $invalidFiles)" accept=".tsv" ngf-max-size="10MB">Select File</button> Kitaplık, birçok şeyin yanı sıra, kabul edilebilir dosya uzantısı ve boyutu belirlememize olanak tanır. Bu butona tıklamak, tıpkı herhangi bir <input type=”file”> öğesi gibi, standart dosya seçiciyi açacaktır.
Dosyayı Yükleme
İkili verileri aktarmak istediğinizde dizi arabelleği ve blob arasından seçim yapabilirsiniz. Bir görüntü dosyası gibi yalnızca ham verilerse, blob'u seçin ve sunucuda düzgün şekilde işleyin. Dizi arabelleği, sabit uzunluklu ikili arabellek içindir ve TSV gibi bir metin dosyası, bayt dizesi biçiminde aktarılabilir. Bu kod parçacığı, bir dosyanın dizi arabellek biçiminde nasıl yükleneceğini gösterir.

$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); } }ng-file-upload yönergesi bir uploadFile işlevi sağlar. Burada, bir FileReader kullanarak dosyayı bir dizi arabelleğine dönüştürebilir ve WebSocket aracılığıyla gönderebilirsiniz.
Büyük dosyaları WebSocket üzerinden dizi arabelleklerine okuyarak göndermenin, bunları yüklemek için en uygun yol olmayabileceğini unutmayın, çünkü bunlar hızlı bir şekilde çok fazla bellek kaplayabilir ve bu da kötü bir deneyime neden olabilir.
Dosyayı Sunucudan Alın
Tornado, 4 bit işlem kodunu kullanarak mesaj türünü belirler ve ikili veri için str ve metin için unicode döndürür.
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)Tornado web sunucusunda, dizi arabelleği str türünde alınır.
Bu örnekte beklediğimiz içerik türü TSV'dir, bu nedenle dosya ayrıştırılır ve bir sözlüğe dönüştürülür. Tabii ki, gerçek uygulamalarda, rastgele yüklemelerle uğraşmanın daha mantıklı yolları vardır.
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())Sayfa İste
Amacımız, yüklenen TSV verilerini küçük sayfalar halinde göstermek olduğundan, belirli bir sayfayı istemek için bir araca ihtiyacımız var. İşleri basit tutmak için, sayfa numarasını sunucumuza göndermek için aynı WebSocket bağlantısını kullanacağız.
$scope.pageChanged = function() { ws = $scope.ws; ws.send($scope.currentPage); }Sunucu bu mesajı unicode olarak alacaktır:
def on_message(self, message): if isinstance(message, unicode): page_no = int(message) self.write_message(self.make_message(page_no))Tornado WebSocket sunucusundan bir dikte ile yanıt vermeye çalışmak, onu otomatik olarak JSON biçiminde kodlayacaktır. Bu nedenle, 100 satır içerik içeren bir dikte göndermek tamamen sorun değil.
Erişimi Başkalarıyla Paylaşma
Aynı yüklemeye erişimi birden fazla kullanıcıyla paylaşabilmek için yüklemeleri benzersiz bir şekilde tanımlayabilmemiz gerekir. Bir kullanıcı WebSocket üzerinden sunucuya bağlandığında, rastgele bir UUID oluşturulur ve bağlantılarına atanır.
def open(self, doc_uuid=None): if doc_uuid is None: self.uuid = str(uuid.uuid4()) uuid.uuid4() rastgele bir UUID oluşturur ve str() bir UUID'yi standart biçimde bir onaltılık basamak dizisine dönüştürür.
UUID'si olan başka bir kullanıcı sunucuya bağlanırsa, ilgili FileHandler örneği anahtar olarak UUID ile bir sözlüğe eklenir ve bağlantı kapatıldığında kaldırılır.
@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]İstemciler sözlüğü, istemcileri aynı anda eklerken veya kaldırırken bir KeyError verebilir. Tornado asenkron bir ağ kütüphanesi olduğundan, senkronizasyon için kilitleme mekanizmaları sağlar. Eşyordamlı basit bir kilit, bu müşteri sözlüğünü işleme durumuna uyar.
Herhangi bir kullanıcı bir dosya yüklerse veya sayfalar arasında geçiş yaparsa, aynı UUID'ye sahip tüm kullanıcılar aynı sayfayı görüntüler.
@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)Nginx'in Arkasında Koşmak
WebSockets'i uygulamak çok basittir, ancak onu üretim ortamlarında kullanırken dikkate alınması gereken bazı zor şeyler vardır. Tornado bir web sunucusudur, bu nedenle kullanıcıların isteklerini doğrudan alabilir, ancak Nginx'in arkasına dağıtmak birçok nedenden dolayı daha iyi bir seçim olabilir. Ancak, WebSockets'i Nginx aracılığıyla kullanabilmek için biraz daha fazla çaba gerekir:
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"; } } } İki proxy_set_header yönergesi, Nginx'in bağlantıyı WebSocket'e yükseltmek için gerekli olan arka uç sunuculara gerekli başlıkları geçirmesini sağlar.
Sıradaki ne?
Bu makalede, sunucu ve istemcilerin her biri arasında kalıcı bağlantıları sürdürmek için WebSockets kullanan basit bir Python web uygulaması uyguladık. Tornado gibi modern asenkron ağ çerçeveleri ile Python'da on binlerce açık bağlantıyı aynı anda tutmak tamamen mümkündür.
Bu demo uygulamasının bazı uygulama yönleri farklı şekilde yapılmış olsa da, umarım yine de https://www.toptal.com/tornado çerçevesinde WebSockets kullanımının gösterilmesine yardımcı olmuştur. Demo uygulamasının kaynak kodu GitHub'da mevcuttur.
