Python3小白笔记-用tkinter制作简易的Socket聊天窗口


前言

本内容为python课作业而做的简要笔记。
我使用的工具为python3,pycharm专业版(试用30天)


一、什么是 Socket?

链接: 菜鸟教程(Python 网络编程)
链接: Python 手册 (网络编程的基本概念)
也许刚接触 “套接字”的人会直接懵了,又翻译为“插座”,但是这个名字叫什么其实并不重要,现阶段不用去纠结它的中文意思。

1、socket()函数

					socket.socket([family[, type[, proto]]])

1.1、参数

family: 套接字家族可以使 AF_UNIX 或者 AF_INET。
(AF_INET 表示 IPv4,AF_UNIX 表示 IPv6,既你要使用的IP类型)

type: 套接字类型可以根据是面向连接的还是非连接分为 SOCK_STREAM 或 SOCK_DGRAM。
(SOCK_STREAM 为 TCP协议,SOCK_DGRAM 为 UDP协议)

protocol: 一般不填默认为 0。

Socket就是网络中每个主机进程之间交互信息的网络协议

二、什么是tkinter

该Tkinter模块(“Tk接口”)是Tk GUI工具包的标准Python接口。Tk和Tkinter在大多数Unix平台以及Windows系统上均可用。(Tk本身不是Python的一部分;它保存在ActiveState中。)

链接: tkinter视频学习
链接: Python 手册(Tkinter)
链接: Python 官网( Interfaces with Tk)

三、源码(仅供参考)

1.服务端server.py

import socket
import threading
import time

# 创建TCP Socket, 类型为服务器之间网络通信,流式Socket
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定服务器端的IP和端口
mySocket.bind((socket.gethostbyname('localhost'), 10000))
# 开始监听TCP传入连接,并设置操作系统可以挂起的最大连接数量
mySocket.listen(5)

print('服务器已启动 ', socket.gethostbyname('localhost'), '正在连接 ...')
# 创建字典,用于存储客户端的用户
mydict = dict()
# 创建字典,用于存储用户名和密码
userDict = {'aaa': '123123', 'bbb': '123123', 'ccc': '123123', 'ddd': '123123', 'eee': '123123'}
# 创建列表,用于存储客户端的连接
mylist = list()
# 创建列表,用户单独存储用户名
userList = list()

"""
000: 用户发送的信息
100: 所有用户名的列表
 110: 聊天窗口关闭
200: 系统发送到聊天窗口的信息
300: 返回到客户端的弹窗信息
400: 注册信息
  401: 注册成功
  402: 注册失败
500: 用户登录(ID和密码)
999: 跳过(收到提示)
"""

# 把 信息 发送给所有人(包括自己)
def chatMsgToAllOne(chatMsg):
    for c in mylist:
        try:
            # 向客户端发送消息
            c.send(chatMsg.encode("utf-8"))
        except:
            pass


# 保持与客户端连接的子线程的处理逻辑
def subThreadProcess(myconnection, connNumber, username):  # connNumber为标记符
    global mydict, mylist
    # 接收客户端消息
    print('客户端连接标记符:', connNumber, ' 昵称:', username)
    chatMsgToAllOne('200*系统提示:' + username + '已经进入聊天室,赶快和他(她)打招呼吧*')
    while True:
        try:
            # 接收客户端消息
            recvedMsg = str(myconnection.recv(1024).decode("utf-8")).rstrip().lstrip()
            prefix = recvedMsg[0:3]  # 信息标记符
            recvedMsg = recvedMsg[3:]  # 可见信息
            if prefix == '000':  # 000表示用户输入框的信息
                chatMsgToAllOne('000' + mydict[connNumber] + ':' + recvedMsg)
            elif prefix == '110':
                userList.remove(recvedMsg)  # 用户名删除
                mylist.remove(myconnection)  # 客户端链接删除
                chatMsgToAllOne('100' + str(userList))  # 重新发送用户列表
        except (OSError, ConnectionResetError):
            try:
                mylist.remove(myconnection)
            except:
                pass
            print(mydict[connNumber], '已存在, ', len(mylist), ' 人员保存!')
            chatMsgToAllOne('200*系统提示:' + mydict[connNumber] + ' 已经离开聊天室*')
            mydict.pop(connNumber, '没有找到key')
            myconnection.close()
            return


def recvThreadProcess():
    global mydict, userList
    while True:
        # 接受TCP连接并返回(connection,address),其中connection是新的Socket对象,可以用来接收和发送数据,address是连接客户端的地址。
        connection, address = mySocket.accept()  # 阻塞,等待消息
        print("server connection:", connection)
        print('新的连接访问', connection.getsockname(), '标记符:' + str(connection.fileno()))
        try:
            # 接收客户端消息
            buf = connection.recv(1024).decode("utf-8")
            if buf == '1':
                # 向客户端发送消息
                connection.send('连接成功, 欢迎来到聊天室!'.encode("utf-8"))  # (只发给自己,不发给他人)
                mylist.append(connection)  # 用户地址信息
                while True:
                    clientInfo = connection.recv(1024).decode("utf-8")
                    # print("clientInfo1:", clientInfo)
                    cLprefix = clientInfo[0:3]
                    clientInfo = clientInfo[3:]
                    # print("clientInfo1:", clientInfo, "clprefix:", cLprefix)
                    if cLprefix == '400':  # 注册信息
                        clientInfo = eval(clientInfo)
                        if clientInfo[0] in userDict:
                            print("no")
                            chatMsgToAllOne('402'+'用户名存在')
                        else:
                            print("yes")
                            userDict[clientInfo[0]] = clientInfo[1]
                            chatMsgToAllOne('401'+'注册成功')
                            print("userDict[", clientInfo[0], "]=", userDict[clientInfo[0]])
                    elif cLprefix == '500':  # 用户登录信息
                        clientInfo = eval(clientInfo)
                        if clientInfo[0] in userDict:  # 校验用户名
                            tempDict = clientInfo[1]
                            if tempDict[str(clientInfo[0])] == userDict[str(clientInfo[0])]:  # 检验密码
                                chatMsgToAllOne('999')
                                time.sleep(0.5)
                                mydict[connection.fileno()] = clientInfo[0]  # 按 标记符 存放 用户名
                                userList.append(clientInfo[0])
                                # 把更新后的 mylist 发送给所有用户
                                chatMsgToAllOne('100' + str(userList))
                                time.sleep(1)

                                # 为当前连接创建一个新的子线程来保持通信
                                myThread = threading.Thread(target=subThreadProcess,
                                                            args=(connection, connection.fileno(), clientInfo[0]))  # .fileno()为标记符
                                myThread.setDaemon(True)
                                myThread.start()
                                break
                            else:
                                chatMsgToAllOne('300')  # 300——客户端弹窗信息
                                print("用户名或密码错误")
                        else:
                            chatMsgToAllOne('300')  # 300——客户端弹窗信息
                            print("用户名或密码错误")

                """ 查看线程数量"""
                # count = len(threading.enumerate())
                # print("当前线程的数量:", count)
            else:
                # 向客户端发送消息
                connection.send('200连接失败, 请离开!'.encode("utf-8"))
                connection.close()
        except:
            pass

# 类型判断
def typeCheck(string):
    if string[0] == '[' and string[-1] == ']':
        return 'list'
    elif string[0] == '{' and string[-1] == '}':
        return 'dict'
    elif string[0] == '(' and string[-1] == ')':
        if isinstance(string, tuple):
            return 'tuple'
        else:
            return 'string'
    else:
        return 'string'

if __name__ == '__main__':
    recvThreadProcess()


1.1 服务器如何发送信息到客户端?

1)在thread()方法的第4行,accept()返回一个元组类型,其中connection是新的Socket对象(与发送信息的客户端所创建的Socket不是同一个Socket)。
2)connection中存储着客户端的IP地址和端口,每个新connection唯一指向一个客户端(网络主机进程),用mylist列表存储各个客户端的Socket。
3)由connection.recv()接收客户端的发来的信息(服务端连接了多少个客户端,服务端就开了多少个subThreadProcess()线程,每个子线程唯一对应一个客户端)
4)在chatMsgToAllOne()方法中发送信息给所有客户端

1.2 自己的客户端如何发送信息到其他主机的客户端?

1)在Client.py中,客户端用socket.send()发送输入的信息给服务器,服务器在subThreadProcess()方法中接收到某个客户端发来的信息,紧接着调用chatMsgToAllOne()方法把信息发给所有已连接的客户端(mylist),再在客户端通过socket.recv()来接收服务器信息,随之打印出来。
2)不管是服务器还是客户端,接收信息的socket.recv()方法都在死循环中,用线程将它们分隔开来就可以做其他的事情了

1.3 如何发送特殊指令不让指令显示在客户端信息窗?

1)可以模仿网络通信协议给信息打包,到指定主机再一一解包。
2)这里我采用简单的标记符的方式给信息分门别类。规定发送的任何信息前三个字符必须在000-999区间,用户发送的信息默认会在首部添加‘000’标记符,标记符由服务器客户端一同进行处理,其他客户端收到的信息不含有标记符,并且用户发送的信息不会干扰到标记符的正常处理。

2.客户端client.py

import socket
import threading
from tkinter import *
import time
import tkinter.messagebox as messagebox
import sys


fon = ("宋体", 18)
usersList = {}  # 所有用户名列表
usersDict = {}  # 用户名:密码
myName = ""  # 用户名

# 注册信息
tempUandP = []

"""
000: 用户发送的信息
100: 所有用户名的列表
 110: 聊天窗口关闭
200: 系统发送到聊天窗口的信息
300: 返回到客户端的弹窗信息
400: 注册信息
  401: 注册成功
  402: 注册失败
500: 用户登录(ID和密码)
999: 跳过(收到提示)
"""

"""聊天窗口800x600"""
class Application():

    def __init__(self):
        # 创建容器
        # 顶部——标签
        self.titleTop = Label(root, text="聊天群", fg="black", font=fon)
        self.titleTop.pack(side='top', fill='both')
        # 底部——输入框
        self.f1 = Frame(root, width=10, height=100)
        self.f1.pack(side="bottom", fill='x')
        # 中部左——聊天信息显示界面
        self.listboxLeft = Listbox(root, width=55, height=16, font=fon, yscrollcommand="true")
        self.listboxLeft.pack(side='left', fill='y')
        # 中部右——用户列表
        self.fRight = Frame(root, width=140)
        self.fRight.pack(side='right', fill="both", expand="no")
        self.f_labelTop = Label(self.fRight, text="用户列表", fg="black", font=fon, padx=20)
        self.f_labelTop.pack(side='top',fill='y')
        self.f_listboxBottom = Listbox(self.fRight, font=fon)
        self.f_listboxBottom.pack(side='top', fill="both", expand="yes")
        # 底部——输入框
        self.textBottom = Text(self.f1, width=54, height=3, font=fon, yscrollcommand="true", padx=5)
        self.textBottom.pack(side="left", fill='both')
        # 底部——确认按钮
        self.buttonR = Button(self.f1, width=18, text="发送", font=fon, command=self.sendThreadProcess)
        self.buttonR.pack(side='right', fill='y')

        # 设置聊天窗口顶部标签
        self.titleTop['text'] = "五邑聊天群(" + str(myName) + ")"

        self.thead()

    # 向服务器端发送消息的处理逻辑
    def sendThreadProcess(self):
        try:
            inputText = self.textBottom.get(1.0, END)
            sock.send(('000'+inputText).encode("utf-8"))  # 000表示用户发送输入框的信息
            self.textBottom.delete(0.0, END)
        except ConnectionAbortedError:
            print('服务端已经关闭这个连接!')
        except ConnectionResetError:
            print('服务器已关闭!')

    # 向服务器端接收消息的处理逻辑
    def recvThreadProcess(self):
        global usersList
        while True:
            try:
                self.otherMsg = sock.recv(1024).decode("utf-8")
                otherMsg_prefix = self.otherMsg[0:3]  # 前缀 信息标识符
                self.otherMsg = self.otherMsg[3:]  # 信息
                print('prefix:', otherMsg_prefix)
                if otherMsg_prefix == '000':
                    if len(self.otherMsg.rstrip().lstrip()) > len(myName) + 1:  # 空格不发送 .rstrip()去除尾空格 .lstrip()去除首空格
                        # 消息放入显示面板
                        self.listboxLeft.insert(END, self.otherMsg)
                elif otherMsg_prefix == '100': # 100表示用户列表
                    usersList = eval(self.otherMsg.rstrip().lstrip())  # 添加用户到元组列表
                    self.f_listboxBottom.delete(0, END)                  # 清除用户列表
                    for i in range(len(usersList)):                      # 显示在线人员
                        self.f_listboxBottom.insert(END, usersList[i])
                elif otherMsg_prefix == '200':
                    self.listboxLeft.insert(END, self.otherMsg)
                else:
                    pass
            except ConnectionAbortedError:
                print('服务端已经关闭这个连接!')
                break
            except ConnectionResetError:
                print('服务器已关闭!')
                break

    # 关闭窗口 并更新聊天界面的“用户列表”
    def closeWin(self):
        print("110"+myName)
        sock.send(("110"+myName).encode("utf-8"))
        root.destroy()

    # 创建发送和接收消息的子线程
    def thead(self):
        recvThread = threading.Thread(target=self.recvThreadProcess, daemon=True)
        recvThread.start()

"""登录窗口400x300"""
class loginWin(Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
        self.pack()
        self.createWidget()

    def createWidget(self):
        self.frame01 = Frame(login, width=400, height=100)
        self.frame02 = Frame(login, width=400, height=100)
        self.frame03 = Frame(login, width=400, height=100)
        self.frame04 = Frame(login, width=400, height=100)

        self.frame01.pack()
        self.frame02.pack()
        self.frame03.pack()
        self.frame04.pack()

        self.tittle01 = Label(self.frame01, text="欢迎登录五邑", font=fon)
        self.label01 = Label(self.frame02, text="用户名:", font=fon)
        self.label02 = Label(self.frame03, text="密  码:", font=fon)
        self.entry01 = Entry(self.frame02, width=16, font=fon, bg='white', xscrollcommand='true')
        self.entry02 = Entry(self.frame03, width=16, font=fon, bg='white', xscrollcommand='true', show="*")
        self.B_login = Button(self.frame04, width=10, height=1, text="登录", font=fon, command=self.logins)
        self.B_regist = Button(self.frame04, width=10, height=1, text="注册", font=fon, command=regist)

        self.tittle01.pack(pady=20)
        self.label01.pack(side='left', fill='both', padx=20, pady=20)
        self.label02.pack(side='left', fill='both', padx=20, pady=20)
        self.entry01.pack(side='right', fill='x', pady=20)
        self.entry02.pack(side='right', fill='x', pady=20)
        self.B_login.pack(side='left', fill='both', padx=20, pady=20)
        self.B_regist.pack(side='right', fill='both', padx=20, pady=20)

    def logins(self):
        global myName, usersDict
        myName = self.entry01.get()  # 输入用户名
        usersDict[str(myName)] = self.entry02.get()  # dict存放用户名和密码
        # 向服务器发送 登录窗口 用户名 和 密码
        sock.send(('500'+str([myName, usersDict])).encode("utf-8"))
        info = sock.recv(1024).decode("utf-8")
        if info == '300':
            messagebox.showinfo("错误", "用户名或密码错误")
        else:
            login.destroy()

    def closeWin(self):
        login.destroy()
        sys.exit()  # 程序关闭

"""注册窗口400x300"""
class registers(Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
        self.place()
        self.createWidget()

    def createWidget(self):
        self.lable01 = Label(reg, text="用 户 名:", font=fon)
        self.lable02 = Label(reg, text="密   码:", font=fon)
        self.lable03 = Label(reg, text="确认密码:", font=fon)
        self.entry01 = Entry(reg, font=fon)
        self.entry02 = Entry(reg, font=fon, show="·")
        self.entry03 = Entry(reg, font=fon, show="·")
        self.b1 = Button(reg, text='注册', font=fon, command=self.regButton)
        self.b2 = Button(reg, text='取消', font=fon, command=self.cancel)

        self.lable01.place(x=10, y=25)
        self.lable02.place(x=10, y=85)
        self.lable03.place(x=10, y=145)
        self.entry01.place(x=130, y=25)
        self.entry02.place(x=130, y=85)
        self.entry03.place(x=130, y=145)
        self.b1.place(x=80, y=210)
        self.b2.place(x=280, y=210)

    def regButton(self):
        global tempUandP
        # 获取注册信息
        try:
            if len(self.entry01.get()) >= 1:
                tempUandP.append(self.entry01.get())  # 用户名
            if len(self.entry02.get()) >= 6:
                tempUandP.append(self.entry02.get())  # 密码
            if len(self.entry03.get()) >= 6:
                tempUandP.append(self.entry03.get())  # 重复密码
            if tempUandP[1] == tempUandP[2]:
                # 向服务器发送 注册窗口 用户名、密码和重复密码
                sock.send(('400'+str(tempUandP)).encode("utf-8"))
                info = sock.recv(1024).decode("utf-8")
                info_prefix = info[0:3]
                info = info[3:]
                if info_prefix == '401':  # 成功
                    messagebox.showinfo("注册", "注册成功")
                    reg.destroy()
                elif info_prefix == '402':  # 失败
                    messagebox.showinfo("错误", info)
                    raise_above_all(reg)
            else:
                messagebox.showinfo("错误", "密码不相同")
        except:
            pass
    def cancel(self):
        reg.destroy()

"""创建Socket"""
def createSocket():
    global sock
    # 创建TCP Socket, 类型为服务器之间网络通信,流式Socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 通过IP和端口号连接服务器端Socket, 类型为服务器之间网络通信,流式Socket
    sock.connect((socket.gethostbyname('localhost'), 10000))
    # 向服务器发送连接请求
    sock.send(b'1')
    # 从服务器接收到的消息
    print(sock.recv(1024).decode("utf-8"))

# 窗口显示在最前
def raise_above_all(win):
    win.attributes('-topmost', 1)
    win.attributes('-topmost', 0)

# 注册窗口
def regist():
    global reg
    reg = Tk()
    reg.title("用户注册")
    reg.geometry("400x300+700+300")
    reg.resizable(0, 0)
    app3 = registers(master=reg)
    reg.mainloop()

# 登录窗口
def loginStart():
    global login
    login = Tk()
    login.title("用户登录")
    login.geometry("400x300+700+300")
    login.resizable(0, 0)    # 禁止窗口大小改变
    app2 = loginWin(master=login)
    login.protocol("WM_DELETE_WINDOW", app2.closeWin)
    raise_above_all(login)
    login.mainloop()

# 聊天窗口
def rootStart():
    global root
    root = Tk()
    root.title("聊天窗口")
    root.geometry("800x600+500+100")
    root.resizable(0, 0)    # 禁止窗口大小改变
    app1 = Application()
    root.protocol("WM_DELETE_WINDOW", app1.closeWin)
    raise_above_all(root)
    root.mainloop()

if __name__ == '__main__':
    createSocket()
    loginStart()
    rootStart()

总结:

有待改进……

> 引用文本《Python网络编程-从入门到精通》 苟英 张小华 高博·编著
  • 7
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值