API WITH CHAT GUI

ผู้เขียนบทความ : นายเลิศฤทธิ์ เจริญมาก COE #12

1.ความเป็นมา

        เนื่องจากโลกของเรามีโปรแกรมแชทอยู่มากมาย การพูดคุยในโลกอินเทอร์เน็ตจึงเป็นเรื่องปกติในชีวิตประจำวันแต่บางครั้งการพูดคุยการที่คิดว่าเป็นการส่วนตัวมันอาจจะไม่เป็นเช่นนั้น เนื่องจากบางครั้งรัฐบาลอาจจะขอข้อมูลการพูดคุยของเราจากผู้ให้บริการโปรแกรมแชทต่างๆทำให้เกิดความไม่เป็นส่วนตัว

        ผมจึงได้ทำโปรแกรม Api Chat Gui ขึ้นมาเพื่อแก้ปัญหาการพูดคุยแล้วเกิดความไม่เป็นส่วนตัวขึ้น เนื่องจากโปรแกรมนี้การพูดคุยเมื่อมีการปิดโปรแกรมข้อมูลการพูดคุยทั้งหมดจะไม่ถูกบันทึกเก็บไว้ทำให้มีความเป็นส่วนตัวมาก

2.วัตถุประสงค์

1.เพื่อศึกษาหาความรู้ในการใช้ภาษา Python

2.เพื่อศึกษาหาความรู้ในการสร้าง API

3.เพื่อพัฒนาโปรแกรม Chat ที่มีความเป็นส่วนตัว

4.เพื่อนำความรู้ที่ได้ไปใช้ต่อยอดในการศึกษาและการทำงาน

3.ขอบเขต

1.สามารถพูดคุยผ่านโปรแกรม Chat ได้

2.สามารถสร้าง Api เพื่อที่จะเก็บข้อมูลของห้อง Chat แต่ละห้องได้

4.ประโยชน์ที่คาดว่าจะได้รับ

1.สามารถที่จะนำระบบไปต่อยอดหรือประยุกต์ในการพัฒนาต่อได้

2.การพูดคุยผ่านโปรแกรม chat มาความเป็นส่วนตัว

3.สามารถเรียนรู้และเข้าใจในเรื่องของ Socket Tkinter และ Fastapi 

5. ความรู้ที่เกี่ยวข้อง

Socket คือ ?

Socket Programming นั้นเป็นการเขียนโปรแกรมที่ใช้ Socket เป็นช่องทางที่ใช้สำหรับการรับส่งข้อมูลกันระหว่างเครื่องหรือโปรแกรม โดยอาจจะเป็นระหว่าง server กับ client ซึ่งการรับส่งข้อมูลสามารถทำได้ทั้งสองทางและเก็บสถานะการติดต่อแต่ละครั้งเอาไว้ได้ ในขณะที่ถ้าเป็นการรับส่งข้อมูลแบบ REST จะไม่จำสถานะการติดต่อของแต่ละครั้ง และการติดต่อต้องเริ่มจากฝั่งใดฝั่งหนึ่งเท่านั้น
การใช้งาน socket จะแบ่งเป็น 2 ส่วนได้แก่ Server และ Client
ในการเขียนคำสั่งของผั่ง Server นั้นจะต้องทำการ import module ต่าง ๆ มาใช้งานในระบบ

import socket
import datetime
import threading

จากนั้นกำหนด IP  PORT และ BUFSIZE 

HOST = '172.16.241.143'
POST = 9090
BUFSIZE = 4096

คำสั่งสำหรับการเชื่อมต่อและจัดการ Server

def handler(client,addr):
    while True:
        try:
            data = client.recv(BUFSIZE)
            check = data.decode('utf-8').split('|')
            if check[0] == 'NAME':
                cdict[str(addr)] = check[1]
        except:
            clist.remove(client)
            break
        if (not data) or (data.decode('utf-8') == 'q'):
            clist.remove(client)
            print('OUT: ',client)
            break
        try:
            user = cdict[str(addr)]
            msg = user + ': ' + data.decode('utf-8')
        except:
            msg = str(addr) + ': ' + data.decode('utf-8')
        print('USER: ',msg)
        print('-----------')
        
        try:
            check = data.decode('utf-8').split('|')
            if check[0] == 'NAME':
                pass
            else:
                for c in clist:
                    c.sendall(msg.encode('utf-8'))
        except:
            for c in clist:
                c.sendall(msg.encode('utf-8'))

    client.close()

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
server.bind((HOST,POST))
server.listen(5)

while True:
    client, addr = server.accept()
    clist.append(client)
    print('ALL CLIENT', clist)

    task = threading.Thread(target=handler, args=(client, addr))
    task.start()

ในส่วนของทางด้าน Client จะมีการใช้ module tkinter เข้าในการสร้าง Gui หรือ Graphical User Interface เพื่อสร้างหน้าต่างโปรแกรม Chat
โดยจะต้องทำการเรียกใช้ import module เหล่านี้เข้ามาก่อน

from tkinter import *
from tkinter import ttk, messagebox
import tkinter.scrolledtext as st
from tkinter import simpledialog
import socket
import threading
import sys

และจะต้องกำหนด IP  POST และ BUFSIZE ให้ตรงกับทางฝั่ง Server 

HOST = '172.16.241.143'
POST = 9090
BUFSIZE = 4096

และจะต้องทำการสร้าง Gui ขึ้นมาเพื่อใช้ในการพูดคุย

gui = Tk()

w = 650
h = 680

ws = gui.winfo_screenwidth()
hs = gui.winfo_screenheight()
print(ws,hs)

x = (ws/2) - (w/2)
y = (hs/2) - (h/2)

gui.geometry(f'{w}x{h}+{x:.0f}+{y:.0f}')

gui.title('ห้องแชททั่วไป')

Font1 = ("TH SarabunPSK",20)
Font2 = ("TH SarabunPSK",20)
################### BOX ####################
F1 = Frame(gui)
F1.place(x=5,y=5)

allmsg = StringVar()

chatbox = st.ScrolledText(F1,width=62,heigh=13,font=Font1)
chatbox.pack(expand=True, fill='x')
#################### Messsage ####################
v_msg = StringVar()

F2 = Frame(gui)
F2.place(x=20,y=570)

E1 = ttk.Entry(F2,textvariable=v_msg,font=Font2,width=45)
E1.pack(ipady=20)
#################### Button ####################
def Sendmessage(event=None):
    msg = v_msg.get()
    client.sendall(msg.encode('utf-8'))
    chatbox.delete('1.0','end')
    chatbox.insert(INSERT,allmsg.get())
    chatbox.yview(END)
    v_msg.set('')
    E1.focus()

F3 = Frame(gui)
F3.place(x=500,y=570)
B1 = ttk.Button(F3,text='Send',command=Sendmessage)
B1.pack(ipadx=25,ipady=25)

E1.bind('<Return>',Sendmessage)

user = StringVar()

global getname
getname = ''

###################### dialog ###################
def gui2dialog():
    gui2 = Toplevel()
    gui2.attributes('-topmost',True)
    w = 300
    h = 200

    ws = gui2.winfo_screenwidth()
    hs = gui2.winfo_screenheight()
    print(ws,hs)

    x = (ws/2) - (w/2)
    y = (hs/2) - (h/2)

    gui2.geometry(f'{w}x{h}+{x:.0f}+{y:.0f}')

    v_getname = StringVar()

    L = ttk.Label(gui2, text='Name',font=Font2).pack()

    EN1 = ttk.Entry(gui2,textvariable=v_getname,font=('TH SarabunPSK',20),width=40)
    EN1.pack(padx=30,pady=00)

    def Entername(event=None):
        global getname
        getname = v_getname.get()
        gui2.withdraw()
        gui.attributes('-topmost',True)
        E1.focus()
        gui.attributes('-topmost',False)
        import random
        
        print('GETNAME',getname)
        if getname == '' or getname == None:
            num = random.randint(10000,99999)
            getname = str(num)

        user.set(getname)
        chatbox.insert(INSERT,'สวัสดี' + getname)

Fastapi คือ ?

คือ Framework สำหรับการสร้าง API เขียนด้วย Python โดยมี uvicorn เป็นตัวจัดการ run server และด้วยความที่ใช้ uvicorn ที่เป็น ASGI ทำให้รองรับการทำงานแบบ Asynchronous โดย FastAPI จะ generate document มาตรฐาน ตามแบบของ OpenAPI ให้อัตโนมัติ โดยใช้ข้อมูลต่าง ๆ เช่น type ของ input/output และ comment ที่เราเขียนไว้ในโค้ด โดยที่เราไม่ต้องทำอะไรเพิ่มเติมทำให้มีความสะดวกในการใช้ รวมทั้งสนับสนุน JSON Schema สำหรับการ validate JSON อีกด้วย โดยจะมีทั้ง Swagger UI และ Redoc ให้เลย
อย่างแรกเราจะต้องทำการ install fastapi และ uvicorn เข้ามาก่อนด้วยคำสั่ง

pip install fastapi
pip install uvicorn

เมื่อทำการ install เข้ามาแล้วจะต้องทำการสร้าง venv เพื่อใช้เป็นพื้นที่ในการทำงานของ Fastapi 

.\venv\scripts\activate

จากนั้นก็ทำการ import module ของ Fastapi เข้ามา

from fastapi import FastAPI
from typing import Optional
from pydantic import BaseModel

จากนั้นก็ทำการสร้าง Class เพื่อทำการเก็บข้อมูล

class Room(BaseModel):
    nameroom: str
    HOST: Optional[str] = None
    POST: str

จากนั้นทำการสร้างฟังก์ชัน Post Get Put และ Delete ขึ้นมาเพื่อใช้ในการใส่ข้อมูล ดูข้อมูล อัพเดทข้อมูล และ ลบข้อมูล

@app.post('/addroom/')
def AddRoom(room: Room):
    room_stock.append(room.dict())
    print(room_stock)
    return room

@app.get('/roomchat/')
def AllRoom():
    return room_stock

@app.put('/update/{ID}')
def UpdateRoom(ID: int, room: Room):
    room_stock[ID] = room.dict()
    return {'ID':ID,'message':'update','data': room.dict()}

@app.delete('/delete/{ID}')
def DeleteRoom(ID: int):
    data = room_stock[ID]
    del room_stock[ID]
    return {'ID':ID,'message':'delete','data':data }

เมื่อทำการสร้างทั้งหมดเสร็จสมบูรณ์แล้ว ก็จะทำการรันคำสั่งด้วย

uvicorn main:app --reload --port 8000 --host 0.0.0.0

โดยก่อนที่เราจะทำการรันคำสั่งจะต้องทำการ Forward port ด้วย Ngrok ก่อน ด้วยคำสั่ง

ngrok http 8000

6.ผลการดำเนินงาน

ภาพการทำงานของระบบ ดูได้ตามรูปนี้

เริ่มต้นการทำงานด้วยการรัน Server ใน cmd 

python s2.py

จากนั้นทำการรัน Client ด้วยคำสั่ง ใน VS Code 

python c2.py

เมื่อทำการรันแล้วจะเกิดหน้าต่าง Gui ขึ้นมา

จากนั้นทำการรัน API ด้วยคำสั่ง 

uvicorn main:app --reload --port 8000 --host 0.0.0.0

จากนั้นก็ทำการ Forward port ด้วย Ngrok จากคำสั่ง

ngrok http 8000

เมื่อทำการรัน API แล้วให้เข้าไปที่ localhost ของ เพื่อทำการใส่ข้อมูล

จะปรากฏหน้าต่างของ FastAPI โดยสร้าง document มาตรฐาน ตามแบบของ OpenAPI ให้อัตโนมัติ โดยใช้ข้อมูลต่าง ๆ เช่น type ของ input/output และ comment ที่เราเขียนไว้ในโค้ด

หน้าตาของลิ้ง API 

และข้อมูลที่เราทำการใส่เราไป

โค้ดทั้งหมดของ Server

import socket
import datetime
import threading


HOST = '172.16.241.143'
POST = 9090
BUFSIZE = 4096

clist = []
cdict = {}

def handler(client,addr):
    while True:
        try:
            data = client.recv(BUFSIZE)
            check = data.decode('utf-8').split('|')
            if check[0] == 'NAME':
                cdict[str(addr)] = check[1]
        except:
            clist.remove(client)
            break
        if (not data) or (data.decode('utf-8') == 'q'):
            clist.remove(client)
            print('OUT: ',client)
            break
        try:
            user = cdict[str(addr)]
            msg = user + ': ' + data.decode('utf-8')
        except:
            msg = str(addr) + ': ' + data.decode('utf-8')
        print('USER: ',msg)
        print('-----------')
        
        try:
            check = data.decode('utf-8').split('|')
            if check[0] == 'NAME':
                pass
            else:
                for c in clist:
                    c.sendall(msg.encode('utf-8'))
        except:
            for c in clist:
                c.sendall(msg.encode('utf-8'))

    client.close()

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
server.bind((HOST,POST))
server.listen(5)

while True:
    client, addr = server.accept()
    clist.append(client)
    print('ALL CLIENT', clist)

    task = threading.Thread(target=handler, args=(client, addr))
    task.start()
        

โค้ดทั้งหมดของ Client

from tkinter import *
from tkinter import ttk, messagebox
import tkinter.scrolledtext as st
from tkinter import simpledialog
import socket
import threading
import sys

HOST = '172.16.241.143'
POST = 9090
BUFSIZE = 4096

global client

def handler(client):
    while TRUE:
        try:
            data = client.recv(BUFSIZE)
        except:
            print('ERROR')
            break
        if (not data) or (data.decode('utf-8') == 'q'):
            print('OUT!')
            break
        
        allmsg.set(allmsg.get() + data.decode('utf-8') + '\n')
        chatbox.delete('1.0','end')
        chatbox.insert(INSERT,allmsg.get())
        chatbox.yview(END)
        #print('USER :', data.decode('utf+8'))
        

    client.close()
    messagebox.showerror('Connection Failed', 'ตัดการเชื่อมต่อ')

###############################################
gui = Tk()

w = 650
h = 680

ws = gui.winfo_screenwidth()
hs = gui.winfo_screenheight()
print(ws,hs)

x = (ws/2) - (w/2)
y = (hs/2) - (h/2)

gui.geometry(f'{w}x{h}+{x:.0f}+{y:.0f}')

gui.title('ห้องแชททั่วไป')

Font1 = ("TH SarabunPSK",20)
Font2 = ("TH SarabunPSK",20)
################### BOX ####################
F1 = Frame(gui)
F1.place(x=5,y=5)

allmsg = StringVar()

chatbox = st.ScrolledText(F1,width=62,heigh=13,font=Font1)
chatbox.pack(expand=True, fill='x')
#################### Messsage ####################
v_msg = StringVar()

F2 = Frame(gui)
F2.place(x=20,y=570)

E1 = ttk.Entry(F2,textvariable=v_msg,font=Font2,width=45)
E1.pack(ipady=20)
#################### Button ####################
def Sendmessage(event=None):
    msg = v_msg.get()
    client.sendall(msg.encode('utf-8'))
    chatbox.delete('1.0','end')
    chatbox.insert(INSERT,allmsg.get())
    chatbox.yview(END)
    v_msg.set('')
    E1.focus()

F3 = Frame(gui)
F3.place(x=500,y=570)
B1 = ttk.Button(F3,text='Send',command=Sendmessage)
B1.pack(ipadx=25,ipady=25)

E1.bind('<Return>',Sendmessage)

user = StringVar()

global getname
getname = ''

###################### dialog ###################
def gui2dialog():
    gui2 = Toplevel()
    gui2.attributes('-topmost',True)
    w = 300
    h = 200

    ws = gui2.winfo_screenwidth()
    hs = gui2.winfo_screenheight()
    print(ws,hs)

    x = (ws/2) - (w/2)
    y = (hs/2) - (h/2)

    gui2.geometry(f'{w}x{h}+{x:.0f}+{y:.0f}')

    v_getname = StringVar()

    L = ttk.Label(gui2, text='Name',font=Font2).pack()

    EN1 = ttk.Entry(gui2,textvariable=v_getname,font=('TH SarabunPSK',20),width=40)
    EN1.pack(padx=30,pady=00)

    def Entername(event=None):
        global getname
        getname = v_getname.get()
        gui2.withdraw()
        gui.attributes('-topmost',True)
        E1.focus()
        gui.attributes('-topmost',False)
        import random
        
        print('GETNAME',getname)
        if getname == '' or getname == None:
            num = random.randint(10000,99999)
            getname = str(num)

        user.set(getname)
        chatbox.insert(INSERT,'สวัสดี' + getname)

        ################ RUN SERVER ###########################
        global client
        client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)

        try:
            client.connect((HOST,POST))
            firsttext = 'NAME|' + user.get()
            client.send(firsttext.encode('utf-8'))
            task = threading.Thread(target=handler,args=(client,))
            task.start()
        except:
            print('ERROR!')
            messagebox.showerror('Connection Failed', 'ไม่สามารถเชื่อมต่อกับ server ได้')

    BB2 = ttk.Button(gui2,text='Enter Chatroom',command=Entername)
    BB2.pack(ipady=10,ipadx=20,pady=20)

    EN1.bind('<Return>',Entername)

    def Close():
        gui2.attributes('-topmost',False)
        checkenter = messagebox.askyesno('ยืนยันการปิดโปรแกรม','หากไม่กรอกชื่อจะไม่สามารถใช้งานได้\nคุณต้องการออกจากโปรแกรมหรือไม่?')
        print('CHECK: ',checkenter)
        if  checkenter == True:
            gui2.destroy()
        else:
            gui2.attributes('-topmost',True)
    
    gui2.protocol('WM_DELETE_WINDOW',Close)
    gui2.mainloop()

gui2dialog()
gui.mainloop()

โค้ดทั้งหมดของ FastAPI

from fastapi import FastAPI
from typing import Optional
from pydantic import BaseModel

app = FastAPI()
class Room(BaseModel):
    nameroom: str
    HOST: Optional[str] = None
    POST: str

room_stock = []

@app.post('/addroom/')
def AddRoom(room: Room):
    room_stock.append(room.dict())
    print(room_stock)
    return room

@app.get('/roomchat/')
def AllRoom():
    return room_stock

@app.put('/update/{ID}')
def UpdateRoom(ID: int, room: Room):
    room_stock[ID] = room.dict()
    return {'ID':ID,'message':'update','data': room.dict()}

@app.delete('/delete/{ID}')
def DeleteRoom(ID: int):
    data = room_stock[ID]
    del room_stock[ID]
    return {'ID':ID,'message':'delete','data':data }

7.สรุปผลและข้อเสนอแนะ

จากผลการทดลองพบว่าการพูดคุยผ่านโปรแกรม Chat สามารถทำงานได้ปกติ แต่ทางด้านของ API เมื่อทำการปิด Server ของ API ในเครื่องจะไม่สามารถเข้าไปดูหน้า API ได้เนื่องจากทางผู้เขียนไม่มี Server ในการเก็บข้อมูลของ API ทำให้เมื่อทำการปิดแล้วจะไม่สามมารถเข้าไปดูหน้า API ได้

8.ข้อมูลอ้างอิง

1.การสร้าง Server และ Client ของโปรแกรม Chat

ข้อมูลเกี่ยวกับ Socket 

https://www.borntodev.com/2020/05/12/socket-programming-%E0%B8%84%E0%B8%B7%E0%B8%AD%E0%B8%AD%E0%B8%B0%E0%B9%84%E0%B8%A3%E0%B8%AB%E0%B8%A7%E0%B9%88%E0%B8%B2/

ข้อมูลเกี่ยวกับ Tkinter

https://realpython.com/python-gui-tkinter/

2.การสร้าง API

ข้อมูลเพิ่มเติมเกี่ยวกับ FastAPI

https://fastapi.tiangolo.com/

ข้อมูลเพิ่มเติมเกี่ยวกับ FastAPI

https://p-srinikorn.medium.com/review-%E0%B8%AA%E0%B8%A3%E0%B9%89%E0%B8%B2%E0%B8%87-api-%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2-fastapi-%E0%B9%80%E0%B8%A3%E0%B9%87%E0%B8%A7-%E0%B9%81%E0%B8%A3%E0%B8%87-%E0%B8%9F%E0%B8%B4%E0%B9%89%E0%B8%A7-%E0%B9%86-148b10416a70

Share

You may also like...

Leave a Reply