How to implement a python socket code without using while true?

Asked

Viewed 293 times

0

I’m doing a chat in Phyton that has the Guis implemented with Tkinter, according to the errors I had and by what I researched about the Text module of Tkinter (module where the messages will be displayed), I need immediate responses to the integration of my system with Python sockets. Therefore, this while True loop of the.py client and server.py would be unviable. There is another way to do this integration without needing the client/server side of the socket or without needing this loop?

Tela_chat.py

# Tkinter imports
from tkinter import *
from tkinter.filedialog import askopenfilename
import tkinter.scrolledtext as scrolledtext
from Server import *
from Cliente import *

# Funçoes imports
import random
from datetime import datetime, time

# Outros arquivos imports
from BD_chatMessages import *

# Variáveis globais
tam = "600x400"
camIco = "Icones\chat.ico"


# Classe da Janela de chat
class chatWindow():
# Função construtora da classe
def __init__(self, nome):
    self.chatJanela = 0
    self.textbox = 0
    self.msgbox = 0
    self.botaoenviar = 0
    self.botaoatt = 0
    self.filename = ""
    self.bFrame = 0
    self.data_e_hora_em_texto = 0
    self.nome = nome
    self.cor = "black"
    self.clipPng = "Icones\clippng.png"
    self.tamFrase = 0



#---------------------------MAIN------------------------------#

# Função MAIN da tela de chat => Gerencia tudo o que pode     acontecer 
def chatTela(self):
    # Chamada de funções de formatação
    self.formata_janela()

    # Envia a entrada do usuário para o banco de dados
    self.botaoenviar = Button(self.bFrame, text="Enviar mensagem", command=self.envia_msgs)
    self.botaoenviar.grid(row=0, column=2, padx=10, pady=10)

    self.botaoatt = Button(self.bFrame, text="Atualizar\nmensagens", command=self.atualiza_textbox)
    self.botaoatt.grid(row=0, column=0, padx=10, pady=10)

    #Indica que a tela atual sempre estará em loop (comando obrigatório do Tkinter para a tela funcionar)
    self.chatJanela.mainloop()



#---------------------------GERENCIAMENTO DO CHAT------------------------------#


# Método responsável por enviar a mensagem digitada diretamente para o BD
def envia_msgs(self):
    msg = self.msgbox.get("1.0", "end")
    

    # Se houver algo escrito, manda pro BD
    if len(msg) > 1:
        # Calcula o timestamp e apaga a caixa de texto onde o user escreveu
        self.datamsg()
        #self.apaga_msgbox()
        self.apaga_chatbox()
        
        # Envia pro BD
        criar_tabela()
        inserir_msg(self.data_e_hora_em_texto, self.nome, msg)

        self.atualiza_textbox()
        
    chamar_cliente(self.nome, msg)



# Função responsavel por atualizar o bate papo com as msgs do BD
def atualiza_textbox(self):
    self.apaga_chatbox()
    aux = seleciona_imprime(0)

    # Formata e insere linha por linha do banco de dados no textbox
    for x in aux:
        self.textbox.insert(INSERT, x[0])
        self.textbox.insert(INSERT, " - ")
        self.textbox.insert(INSERT, x[1])
        self.textbox.insert(INSERT, ": ")
        self.textbox.insert(INSERT, x[2])
        

    self.textbox.yview_moveto(1)
    self.pesquisa_usuario()

    # Função necessária para não permitir que o textbox seja editado
    self.textbox.bind("<Key>", lambda e: "break")
        

# Busca o nome do usuário no textbox, marca-o com uma tag e colore seu nome
def pesquisa_usuario(self):
    # Remove a tag 'found' do index 1 até o final (END)
    self.textbox.tag_remove('found', '1.0', END)
    
    # Se o usuário existir, vamos colorir seu nome
    if self.nome:
        # Index do começo da string no ScrooledText é sempre 1.0
        idx = '1.0'
        while 1:
            #Encontra a string desejada a partir do index 1
            idx = self.textbox.search(self.nome, idx, nocase=1, stopindex=END)

            # Se não encontra a String, sai do loop
            if not idx:
                break

            # Ultima soma de posição da posição atual e do tamanho do texto
            lastidx = '%s+%dc' % (idx, len(self.nome))
            
            # Marca a palavra encontrada com uma tag 'found'
            self.textbox.tag_add('found', idx, lastidx)
            idx = lastidx

        # Marca a string encontrada com uma cor
        self.textbox.tag_config('found', foreground=self.cor)


#---------------------------MÉTODOS AUXILIARES------------------------------#

# Escolhe radomicamente uma cor da lista para ser a cor daquele user
def random_colors(self, x):
    cores = ["yellow","blue", "navy blue", "gold", "orange", "brown", "pink", "purple", "green", "red", "violet"]
    if x==0:
        self.cor = random.choice(cores)
    else:
        return random.choice(cores)

# Calcula o timestamp da msg e retorna já formatado
def datamsg(self):
    data_e_hora_atuais = datetime.now()
    self.data_e_hora_em_texto = data_e_hora_atuais.strftime(
        "%d/%m/%Y %H:%M:%S")

# Apaga a caixa de bate-papo (onde as mensagens são exibidas)
def apaga_chatbox(self):
    #self.textbox.destroy()
    self.textbox.delete(1.0, END)

# Apaga a caixa de mensagem
def apaga_msgbox(self):
    self.msgbox.delete(1.0, END)



#---------------------------DESIGN & FORMATAÇÃO DA JANELA------------------------------#

# Função responsável por formatar a janela da aplicação
def formata_janela(self):
    # Definiçoes iniciais
    self.chatJanela = Tk()
    self.chatJanela.title("Chattttô!")
    self.chatJanela.wm_iconbitmap(camIco)
    self.chatJanela.focus_force()
    self.chatJanela.geometry(tam)

    # Define a cor do usuário
    self.random_colors(0)

    # Tela onde aparecem as mensagens enviadas => atualiza a medida que são enviadas novas mensagens
    self.textbox = scrolledtext.ScrolledText(self.chatJanela, height=15, width=80)
    self.textbox.pack(padx=20, pady=20)
    self.textbox.insert(1.0, "Bem Vindo ao Chattttô @" + self.nome + "!!!\n")

    # Esse frame é uma especie de "caixa" que posiciona elementos dentro dele com o .grid
    self.bFrame = Frame(self.chatJanela)
    self.bFrame.pack()

    # Entrada de texto do usuário
    self.msgbox = Text(self.bFrame, height=3, width=40)
    self.msgbox.grid(row=0, column=1, padx=20, pady=20)
    self.msgbox.focus_force()


c = chatWindow("Julho")
c.chatTela()

Customer.py

import socket
import select
import errno, sys

HEADER_LENGTH = 10

IP = "127.0.0.1"
PORT = 1234
my_username = input("Username: ")

# Create a socket
# socket.AF_INET - address family, IPv4, some otehr possible are AF_INET6, AF_BLUETOOTH, AF_UNIX
# socket.SOCK_STREAM - TCP, conection-based, socket.SOCK_DGRAM - UDP, connectionless, datagrams, socket.SOCK_RAW - raw IP packets
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Connect to a given ip and port
client_socket.connect((IP, PORT))

# Set connection to non-blocking state, so .recv() call won;t block, just return some exception we'll handle
client_socket.setblocking(False)

# Prepare username and header and send them
# We need to encode username to bytes, then count number of bytes and prepare header of fixed size, that we encode to bytes as well
username = my_username.encode('utf-8')
username_header = f"{len(username):<{HEADER_LENGTH}}".encode('utf-8')
client_socket.send(username_header + username)

while True:

    # Wait for user to input a message
    message = input(f'{my_username} > ')

    # If message is not empty - send it
    if message:

        # Encode message to bytes, prepare header and convert to bytes, like for username above, then send
        message = message.encode('utf-8')
        message_header = f"{len(message):<{HEADER_LENGTH}}".encode('utf-8')
        client_socket.send(message_header + message)

    try:
        # Now we want to loop over received messages (there might be more than one) and print them
        while True:

            # Receive our "header" containing username length, it's size is defined and constant
            username_header = client_socket.recv(HEADER_LENGTH)

            # If we received no data, server gracefully closed a connection, for example using socket.close() or socket.shutdown(socket.SHUT_RDWR)
            if not len(username_header):
                print('Connection closed by the server')
                sys.exit()

            # Convert header to int value
            username_length = int(username_header.decode('utf-8').strip())

            # Receive and decode username
            username = client_socket.recv(username_length).decode('utf-8')

            # Now do the same for message (as we received username, we received whole message, there's no need to check if it has any length)
            message_header = client_socket.recv(HEADER_LENGTH)
            message_length = int(message_header.decode('utf-8').strip())
            message = client_socket.recv(message_length).decode('utf-8')

            # Print message
            print(f'{username} > {message}')

    except IOError as e:
        # This is normal on non blocking connections - when there are no incoming data error is going to be raised
        # Some operating systems will indicate that using AGAIN, and some using WOULDBLOCK error code
        # We are going to check for both - if one of them - that's expected, means no incoming data, continue as normal
        # If we got different error code - something happened
        if e.errno != errno.EAGAIN and e.errno != errno.EWOULDBLOCK:
            print('Reading error: {}'.format(str(e)))
            sys.exit()

        # We just did not receive anything
        continue

    except Exception as e:
        # Any other exception - something happened, exit
        print('Reading error: '.format(str(e)))
        sys.exit()

Server.py

import socket
import select

HEADER_LENGTH = 10

IP = "127.0.0.1"
PORT = 1234

# Create a socket
# socket.AF_INET - address family, IPv4, some otehr possible are AF_INET6, AF_BLUETOOTH, AF_UNIX
# socket.SOCK_STREAM - TCP, conection-based, socket.SOCK_DGRAM - UDP, connectionless, datagrams, socket.SOCK_RAW - raw IP packets
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# SO_ - socket option
# SOL_ - socket option level
# Sets REUSEADDR (as a socket option) to 1 on socket
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# Bind, so server informs operating system that it's going to use given IP and port
# For a server using 0.0.0.0 means to listen on all available interfaces, useful to connect locally to 127.0.0.1 and remotely to LAN interface IP
server_socket.bind((IP, PORT))

# This makes server listen to new connections
server_socket.listen()

# List of sockets for select.select()
sockets_list = [server_socket]

# List of connected clients - socket as a key, user header and name as data
clients = {}

print(f'Listening for connections on {IP}:{PORT}...')

# Handles message receiving
def receive_message(client_socket):

    try:

        # Receive our "header" containing message length, it's size is defined and constant
        message_header = client_socket.recv(HEADER_LENGTH)

        # If we received no data, client gracefully closed a connection, for example using socket.close() or socket.shutdown(socket.SHUT_RDWR)
        if not len(message_header):
            return False

        # Convert header to int value
        message_length = int(message_header.decode('utf-8').strip())

        # Return an object of message header and message data
        return {'header': message_header, 'data': client_socket.recv(message_length)}

    except:

        # If we are here, client closed connection violently, for example by pressing ctrl+c on his script
        # or just lost his connection
        # socket.close() also invokes socket.shutdown(socket.SHUT_RDWR) what sends information about closing the socket (shutdown read/write)
        # and that's also a cause when we receive an empty message
        return False

while True:

    # Calls Unix select() system call or Windows select() WinSock call with three parameters:
    #   - rlist - sockets to be monitored for incoming data
    #   - wlist - sockets for data to be send to (checks if for example buffers are not full and socket is ready to send some data)
    #   - xlist - sockets to be monitored for exceptions (we want to monitor all sockets for errors, so we can use rlist)
    # Returns lists:
    #   - reading - sockets we received some data on (that way we don't have to check sockets manually)
    #   - writing - sockets ready for data to be send thru them
    #   - errors  - sockets with some exceptions
    # This is a blocking call, code execution will "wait" here and "get" notified in case any action should be taken
    read_sockets, _, exception_sockets = select.select(sockets_list, [], sockets_list)


    # Iterate over notified sockets
    for notified_socket in read_sockets:

        # If notified socket is a server socket - new connection, accept it
        if notified_socket == server_socket:

            # Accept new connection
            # That gives us new socket - client socket, connected to this given client only, it's unique for that client
            # The other returned object is ip/port set
            client_socket, client_address = server_socket.accept()

            # Client should send his name right away, receive it
            user = receive_message(client_socket)

            # If False - client disconnected before he sent his name
            if user is False:
                continue

            # Add accepted socket to select.select() list
            sockets_list.append(client_socket)

            # Also save username and username header
            clients[client_socket] = user

            print('Accepted new connection from {}:{}, username: {}'.format(*client_address, user['data'].decode('utf-8')))

        # Else existing socket is sending a message
        else:

            # Receive message
            message = receive_message(notified_socket)

            # If False, client disconnected, cleanup
            if message is False:
                print('Closed connection from: {}'.format(clients[notified_socket]['data'].decode('utf-8')))

                # Remove from list for socket.socket()
                sockets_list.remove(notified_socket)

                # Remove from our list of users
                del clients[notified_socket]

                continue

            # Get user by notified socket, so we will know who sent the message
            user = clients[notified_socket]

            print(f'Received message from {user["data"].decode("utf-8")}: {message["data"].decode("utf-8")}')

            # Iterate over connected clients and broadcast message
            for client_socket in clients:

                # But don't sent it to sender
                if client_socket != notified_socket:

                    # Send user and message (both with their headers)
                    # We are reusing here message header sent by sender, and saved username header send by user when he connected
                    client_socket.send(user['header'] + user['data'] + message['header'] + message['data'])

    # It's not really necessary to have this, but will handle some socket exceptions just in case
    for notified_socket in exception_sockets:

        # Remove from list for socket.socket()
        sockets_list.remove(notified_socket)

        # Remove from our list of users
        del clients[notified_socket]
No answers

Browser other questions tagged

You are not signed in. Login or sign up in order to post.