文章目录
前言
本内容为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网络编程-从入门到精通》 苟英 张小华 高博·编著