İstemci sunucudan ayrıldıktan sonra sunucunun ayrılan istemciyle ilgili işlem yapmaya devam etmesini nasıl engellerim?

Python’un asyncio kütüphanesini kullanan basit bir sohbet programı yazdım. İstemciler beklemeden iletişim kurabiliyorlar. Ayrıca, sunucuya bir istemci bağlandığında / bağlantısı kesildiğinde, sunucu konsol penceresine bir bilgi yazdırıyor. Ayrıca, istemcilerin tüm etkinlikleri sunucu tarafından bir günlük dosyasına yazılıyor.

Konsol pencerelerine baktığımda program sorunsuz bir şekilde çalışıyor görünüyor. Bir istemcinin bağlantısı kesildikten bir saniye sonra, bu istemcinin sunucudan ayrıldığı ile ilgili bir bilgi sunucunun konsol penceresine yazılır. Ancak günlük dosyasına baktığımda, sunucunun, bağlı tüm istemcilere boş mesajlar göndermeye çalıştığı görülmektedir. Ancak bu mesajlar istemcilerin pencerelerinde gösterilmiyor (Daha doğrusu bunu engelledim.).

Öyle görünüyor ki, sunucu, bağlantısı kesilmiş istemcileri düzgün şekilde kapatmıyor.

Bu sorunu çözmek için paylaştığım kodlarda nasıl bir değişiklik yapmam gerekiyor? Bu arada günlük dosyasını da paylaşıyorum.

server.py

#!/usr/bin/env python3.7
# -*- coding: utf-8 -*-

import asyncio
import logging

logging.basicConfig(
    filename="server.log",
    format="- %(levelname)s - %(asctime)s - %(message)s", 
    level=logging.DEBUG,
    datefmt="%d.%m.%Y %H:%M:%S"
)    


class Server:

    def __init__(self):
        self.clients = []

    def run(self):
        asyncio.run(self.main())

    async def client_connected(self, reader, writer):
        client = f"{writer.get_extra_info('peername')}"
        print(f"{client} is connected.")
        logging.info(f"{client} is connected.")
        self.clients.append((writer, reader))
        while True:
            try:
                data = await reader.readline()
            except (BrokenPipeError, ConnectionResetError):
                data = "".encode()
                await asyncio.sleep(1)
            for i in self.clients:
                msg = f"{client}: {data.decode()}"
                try:
                    i[0].write(msg.encode())
                    logging.debug(msg[:-1])
                    await i[0].drain()
                except (BrokenPipeError, ConnectionResetError):
                    i[0].close()
                    self.clients.remove(i)
                    print(f"{client} is disconnected.")
                    logging.info(f"{client} is disconnected.")
                    await asyncio.sleep(2)
                    break

    async def main(self):
        server = await asyncio.start_server(
            client_connected_cb=self.client_connected,
            host="127.0.0.1",
            port=12345
        )
        print(f"Server started on {server.sockets[0].getsockname()}")
        logging.info(f"Server started on {server.sockets[0].getsockname()}")
        async with server:
            await server.wait_closed()


if __name__ == "__main__":
    Server().run()

client.py

#!/usr/bin/env python3.7
# -*- coding: utf-8 -*-

import sys
import asyncio
import threading


class Client:
    
    def __init__(self):
        self.nick = input("/nick ")
        while not self.nick:
            self.nick = input("/nick ")
            
    def run(self):
        asyncio.run(self.main())

    async def read(self, reader, writer):
        data = await reader.readline()
        socket = writer.get_extra_info('socket').getsockname()
        if str(socket) not in data.decode() or \
                self.nick not in data.decode():
            if data.decode().count("('") >= 1:
                data = data.decode().split(": ")[-2:]
                print(": ".join(data)[:-1])
    
    async def write(self, writer):
        t = threading.Thread(
            target=lambda: writer.write(
                f"{self.nick}: {sys.stdin.readline()}".encode()
            )
        )
        t.daemon = True
        t.start()
        t.join(0.1)
        await writer.drain()
        
    async def main(self):
        reader, writer = await asyncio.open_connection("127.0.0.1", 12345)
        print(f"Connected to {writer.get_extra_info('peername')}")
        while reader and writer:
            await asyncio.gather(
                self.read(reader, writer), 
                self.write(writer)
            )


if __name__ == "__main__":
    Client().run()

server.log

- DEBUG - 30.09.2019 16:29:36 - Using selector: EpollSelector
- INFO - 30.09.2019 16:29:36 - Server started on ('127.0.0.1', 12345)
- INFO - 30.09.2019 16:29:45 - ('127.0.0.1', 46328) is connected.
- INFO - 30.09.2019 16:29:51 - ('127.0.0.1', 46330) is connected.
- INFO - 30.09.2019 16:30:01 - ('127.0.0.1', 46332) is connected.
- DEBUG - 30.09.2019 16:30:30 - ('127.0.0.1', 46328): Client1: selam
- DEBUG - 30.09.2019 16:30:30 - ('127.0.0.1', 46328): Client1: selam
- DEBUG - 30.09.2019 16:30:30 - ('127.0.0.1', 46328): Client1: selam
- DEBUG - 30.09.2019 16:30:34 - ('127.0.0.1', 46328): Client1: naber?
- DEBUG - 30.09.2019 16:30:34 - ('127.0.0.1', 46328): Client1: naber?
- DEBUG - 30.09.2019 16:30:34 - ('127.0.0.1', 46328): Client1: naber?
- DEBUG - 30.09.2019 16:30:37 - ('127.0.0.1', 46328): Client1: iyi misiniz?
- DEBUG - 30.09.2019 16:30:37 - ('127.0.0.1', 46328): Client1: iyi misiniz?
- DEBUG - 30.09.2019 16:30:37 - ('127.0.0.1', 46328): Client1: iyi misiniz?
- DEBUG - 30.09.2019 16:31:57 - ('127.0.0.1', 46328):
- DEBUG - 30.09.2019 16:31:57 - ('127.0.0.1', 46328):
- DEBUG - 30.09.2019 16:31:57 - ('127.0.0.1', 46328):
- DEBUG - 30.09.2019 16:31:57 - ('127.0.0.1', 46328):
- INFO - 30.09.2019 16:31:57 - ('127.0.0.1', 46328) is disconnected.
- DEBUG - 30.09.2019 16:31:58 - ('127.0.0.1', 46332):
- DEBUG - 30.09.2019 16:31:58 - ('127.0.0.1', 46332):
- DEBUG - 30.09.2019 16:31:58 - ('127.0.0.1', 46332):
- DEBUG - 30.09.2019 16:31:58 - ('127.0.0.1', 46332):
- INFO - 30.09.2019 16:31:58 - ('127.0.0.1', 46332) is disconnected.
- DEBUG - 30.09.2019 16:31:58 - ('127.0.0.1', 46330):
- DEBUG - 30.09.2019 16:31:58 - ('127.0.0.1', 46330):
- INFO - 30.09.2019 16:31:58 - ('127.0.0.1', 46330) is disconnected.

Ekran Görüntüleri:

Kodunuzu indirip çalıştırdım. Hataya sebep olan kısım,

Bir client bağlantısı koptuğu halde, onun için asenkron olarak çalıştırılan fonksiyon sona ermiyor. Bu durumda, bağlantı koptuktan sonra yukarıdaki kod kısmında hep hata meydana geliyor ve mesaj boş olarak belirleniyor. Sonra da alt kısıma geçiliyor ve tüm herkese belirlenen bu boş mesaj gidiyor.

Ek olarak, kodunuzda tutarsızlık var biraz. Bir client’i kontrol eden fonksiyon içerisinde birbirinden bağımsız iki iş yapmışsınız. Bu da kodun yönetimini zorlaştırıyor. Client dinlemesi ayrı bir fonksiyon olarak, tüm client’lere mesajın gönderilmesi ayrı bir fonksiyon olarak yazılmalıdır.

Evet, o kısım biraz sorunlu. try: except: blokunu kaldırsak, sadece data = await reader.readline() yazsak da sorun çözülmüyor.

except kısmına continue yazsanız şimdilik çözülecektir.

while True:
    try:
        data = await reader.readline()
    except (BrokenPipeError, ConnectionResetError):
        continue

Yo çözülmüyor denedim bunu daha önce.

Pardon, tüm kodu atayım. Ben biraz değişiklik yaptım.

#!/usr/bin/env python3.7
# -*- coding: utf-8 -*-

import asyncio
import logging

logging.basicConfig(
    filename="server.log",
    format="- %(levelname)s - %(asctime)s - %(message)s", 
    level=logging.DEBUG,
    datefmt="%d.%m.%Y %H:%M:%S"
    )    


class Server:

    def __init__(self):
        self.clients = {}

    def run(self):
        asyncio.run(self.main())

    async def client_connected(self, reader, writer):
        client = f"{writer.get_extra_info('peername')}"
        print(f"{client} is connected.")
        logging.info(f"{client} is connected.")
        self.clients[client] = (writer, reader)
        while True:
            try:
                if(self.clients.get(client,False)):
                    data = await reader.readline()
                else:
                    continue
            except (BrokenPipeError, ConnectionResetError):
                writer.close()
                del self.clients[client]
                await asyncio.sleep(1)
            for i in self.clients:
                msg = f"{client}: {data.decode()}"
                try:    
                        if(self.clients[i][0].is_closing()):
                                print("Client({0}) kapalı".format(client))
                                raise BrokenPipeError
                        self.clients[i][0].write(msg.encode())
                        logging.debug(msg[:-1])
                        await self.clients[i][0].drain()
                except (BrokenPipeError, ConnectionResetError):
                    self.clients[i][0].close()
                    del self.clients[i]
                    print(f"{client} is disconnected.")
                    logging.info(f"{client} is disconnected.")
                    await asyncio.sleep(1)
                    break

    async def main(self):
        server = await asyncio.start_server(
            client_connected_cb=self.client_connected,
            host="127.0.0.1",
            port=12345
        )
        print(f"Server started on {server.sockets[0].getsockname()}")
        logging.info(f"Server started on {server.sockets[0].getsockname()}")
        async with server:
            await server.wait_closed()


if __name__ == "__main__":
    Server().run()

Client aynı. Bu şekilde boş mesaj gönderilmesini engelledim.

Hmm. Bu sefer de sunucudan ayrılan istemcinin ayrıldığına dair mesaj sunucu ekranına yazdırılmadı. Bu sorunla bir iki gündür uğraşıyorum, bir şeyi düzeltmeye çalışırken, başka bir yer garip davranışlar veriyor genelde. :confused:

1 Like

Dediğim gibi, biraz karışık olduğu için sadece problemin çözümüne dair bir çözüm uygulamaya çalıştım. Diğer kısımlar için ayrıca uğraşmak gerek.

1 Like

Neyse, biraz daha üzerinde uğraşayım. :slight_smile:

1 Like

Kolay gelsin :slight_smile: Dediğim gibi, size tavsiyem bu iki işi ayrı fonksiyonlarda yapmak. Böylece kodu daha rahat yönetebilirsiniz, bir değişiklik yaptığınızda daha az domino etkisi yaratmış olur diye düşünüyorum.

1 Like

Bu arada server.py'deki client_connected fonksiyonu aslında main gibi bir fonksiyondu. Yani mesaj alma gönderme işlemleri başka fonksiyonlarda yapılıyordu. Bu fonksiyon altında da await asyncio.gather() ile bu fonksiyonlar çalıştırılıyordu. Ama sonra kodu sadeleştirdim. Yani sanki farklı fonksiyonlarda olmasına gerek yokmuş gibi düşündüm o an ve bütün işlemleri client_connected isimli fonksiyona taşıdım, çünkü server’ın yapacağı şey şuydu: istemciden gelen mesajları bütün istemcilere göndermek, dolayısıyla çakışma ihtimali olan işlem yoktu.

Bir client bağlantı yaptığı zaman, o bağlantı için client_connected fonksiyonu asenkron olarak çalıştırılıyor ve bu bağlantı için iki kaynak ayrılıyor, writer ve reader. Bu asenkron çalışan fonksiyon, aksi belirtilmedikçe döngüye devam ediyor ve mesaj alışverişi yapıyor. Ancak dikkat edin, aksi belirtilmedikçe çalışıyor yani bağlantı kopana kadar değil. Bu da bağlantı koptuğu halde mesaj alışverişine devam etmeye çalıştığını gösteriyor ve dolayısıyla boş mesajlar gönderiyor bu fonksiyon. Algoritma olarak doğru, ama sonlandırma koşulu yanlış. Anlatmak istediğim de bu. Yani işlem çakışması olmasa dahi, ortada düzgün sonlanmayan bir fonksiyon var.

Ya aslında benim merak ettiğim şey şu; istemci için ayrılan reader ve writer'ı nasıl yönetebiliriz?

Bir kullanıcının sunucuya bağlı olup olmadığını anlamak için şöyle yapıyorum: sunucu istemcilere mesaj gönderiyor ve bu esnada bir ConnectionResetError hatası yükseltilirse, o hatayı yükselten istemci sunucudan kopmuştur diye düşünüyorum. Dolayısıyla bu hatayı almayı bekliyorum, ancak bu hatadan sonra bağlantısı kopmuş olan istemcinin writer ve reader özellikleri nasıl kapatılır, bunu öğrenemedim henüz.

Ben bir istemcinin sunucudan kopup kopmadığını writer.is_closing() metodunu kullanarak çözmeye çalıştım. Akış kapatılmışsa, istemci ile iletişim de kopmuştur ve dolayısıyla bir hata oluşmasını beklememe gerek kalmaz.

Bu örnekte, writer.is_closing() True değerini verse bile sunucu o boş mesajları göndermeye devam ediyor. Sunucudan ayrılan istemci, sunucunun tuhaflaşmasına neden oluyor şimdilik. Bu tuhaflık görünürde yok. Ancak log dosyasında bir tuhaflık olduğu görünüyor.

Bunu nasıl tespit ettiniz? Çünkü bende eğer akış kapalıysa raise BrokenPipeError ile except kısmına geçiyorum. Orada da kapalı olan akışı tekrar kapatıyorum, bunu neden yapıyorum bilmiyorum :smiley: Az önce koda baktım da, biraz daha incelemem gerekiyor.

Sizin değiştirdiğiniz kodlardaki şu kısma is_closing() sorgusu ekledim:

except (BrokenPipeError, ConnectionResetError):
                    self.clients[i][0].close()
                    del self.clients[i]
                    if writer.is_closing():
                        print(f"{client} is disconnected.")
                    else:
                        print("kapatılamadı.")

Ancak bu durumda ekrana ... is disconnected yazısı çıktı. Bu arada dediğim gibi ikinci istemcinin sunucudan koptuğunun bilgisi ekrana da log dosyasına da yazdırılmıyor.

- DEBUG - 30.09.2019 21:17:03 - Using selector: EpollSelector
- INFO - 30.09.2019 21:17:03 - Server started on ('127.0.0.1', 12345)
- INFO - 30.09.2019 21:17:09 - ('127.0.0.1', 48646) is connected.
- DEBUG - 30.09.2019 21:17:10 - ('127.0.0.1', 48646): ali: ahahaha
- INFO - 30.09.2019 21:17:15 - ('127.0.0.1', 48648) is connected.
- DEBUG - 30.09.2019 21:17:16 - ('127.0.0.1', 48648): veli: ahahaha
- DEBUG - 30.09.2019 21:17:16 - ('127.0.0.1', 48648): veli: ahahaha
- DEBUG - 30.09.2019 21:17:17 - ('127.0.0.1', 48648): veli: ahaha
- DEBUG - 30.09.2019 21:17:17 - ('127.0.0.1', 48648): veli: ahaha
- DEBUG - 30.09.2019 21:17:19 - ('127.0.0.1', 48648):
- DEBUG - 30.09.2019 21:17:19 - ('127.0.0.1', 48648):
- DEBUG - 30.09.2019 21:17:19 - ('127.0.0.1', 48648):
- DEBUG - 30.09.2019 21:17:19 - ('127.0.0.1', 48648):
- INFO - 30.09.2019 21:17:19 - ('127.0.0.1', 48648) is disconnected.

Peki, disconnected yazdıktan sonra halen daha bu client mesaj göndermeye devam ediyor mu?

Yo, hayır, mesaj göndermesi durdu, fakat disconnect olana kadar (1 saniye içinde gönderebildiği kadarıyla, yaklaşık 4-5 tane) boş mesaj gönderiyor.

Hmm. Sanırım bu aşamada bir sıkıntı var yani akış tam sonlanmadığı için gönderime devam ediyor. Akış tamamen sonlandığında mesaj gönderimi duruyor.