简介
本次程序设计均使用python实现,使用sql server对聊天室用户的数据进行存储。通过python socket套接字编程,实现了在线聊天室的功能,并使用python tkinter进行UI界面的设计。
思路
由计算机网络的基础知识易知,两个主机之间通信的协议可以是TCP,也可以是UDP。其中TCP需要通过三次握手在两台主机之间建立连接,然后两台主机之间才可以通信;而UDP则是无连接的。显然,对于这样一个简单的程序,使用TCP和UDP作为通信协议均可,本次程序设计选择的是TCP。
- 首先面对的问题是,我们想要编写的程序,是一个在线聊天室,用户可以在聊天室内广播消息(即群聊),也可以向聊天室中的某人私发消息(即好友小窗聊天)。
- 那么显然,只通过单纯的TCP连接是无法满足我们的需求的:因为我们是想让客户端与服务器之间构建TCP连接,主机发出请求,服务器接到请求后和它建立连接,客户明确知道这个服务器是在线聊天室,而服务器却无法得知用户是哪位用户,只能通过服务器
socket.accept()
接受连接请求后,得知客户端的套接字信息和IP地址及端口号。
- 因此,为了解决这个问题,我们因此了本程序在设计时最关键的思路:以服务器作为中继端,它不仅能够接受信息,还能够转发信息。当客户端向服务器发送信息时,把这条信息编码,为它加上我们人为自定义的前缀。我设计的前缀是目标前缀,即“在服务器中广播/向服务器中另一个用户私发信息”( 私发时,前缀即为能唯一标识目标用户的信息;群发时则是BROADCASTING )
这样,服务器变成了一个后台,在用户视图下(ClientUI),用户可以看到其他用户群发的信息,也能够看到谁在给它私发信息。而对于服务器,它的任务是向用户转发信息 (显然,服务器知道每个连接到它的用户的IP和端口,由于用户在发送信息时,其信息被编码,目标地址作为前缀被编码进入了发给服务器的信息,而服务器端解码,将地址和信息拆包,服务器拿到地址后,如果这个地址是群发地址,则将信息再次编码<将发送方的地址编码进这条信息,以让接收方得知是谁在群发消息>,发送给所有用户;否则,服务器将地址编码<将发送方的地址编码进这条信息,以让接收方得知是谁在向它私发消息>,将信息转发给目标用户) ,并在服务器端显示聊天日志(方便维护与程序测试)。
👆客户端广播信息“嗡嗡嗡”;
👆客户端广播“嗡嗡嗡”,在服务器端也能看见这条广播信息,并得知是谁在发送广播信息。如果两个客户进行私聊,服务器端仍然可以对私发信息进行监听。
👆现在在两个客户端之间私发消息,由“猪头”向“电棍”发送“喂喂喂”。
👆“电棍”收到消息。
👆由“猪头”发送给“狗狗”,“电棍”收不到消息。同时由于“狗狗”没上线,服务器会反馈。
👆服务器会监听每条聊天记录。
以上便是本次程序设计中对于服务器ServerUI的设计思路。
ServerUI及其相关源码
socket.py
程序清单中的socket.py,定义了两个类及若干程序中必要的编码解码函数。其中包括客户端的Client以及服务器的Server。
import socket
"""
①Server类中初始化了套接字socket;
②开设序列socs,用于存放(append)主动连接至服务器端的客户端套接字信息;
③ip和port为服务器的ip地址和端口,由于本次程序是在本机上进行仿真操作的,因此ip
默认设置为自检环回地址127.0.0.1,端口可以自定义;
④bind指将ip和端口与套接字s绑定;
⑤listen设置为128,即最多接入128个用户。
"""
class Server():
def __init__(self,ip,port):
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socs = []
self.ip = ip
self.port = port
# self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.s.bind((self.ip, self.port))
self.s.listen(128)
class Client():
def __init__(self,ip,port):
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.ip = ip
self.port = port
self.s.bind((self.ip, self.port))
"""
👇三个编码解码函数
①wencoding是将目标地址信息编码进发送信息的编码器函数;
②w_oriencoding (Short for "w Original encoding") 是用于“当发送方向用户私发信息,
而该用户不在线”时的编码器。
③wdecoding是将wencoding编码信息解码的解码器。
"""
def wencoding(addr,data):
res = addr[0]
res += '|'
res += str(addr[1])
res += '|'
res += data
return res
def w_oriencoding(data):
res = "1|2|"
res += data
return res
def wdecoding(data):
data = tuple(data.split('|'))
addr = (data[0], int(data[1]))
data = data[2]
return addr, data
ServerUI
调用库
from tkinter import *
from tkinter import messagebox
from Socketer import *
from threading import Thread
from tkinter import scrolledtext
import datetime
"""
inspect和ctypes用于终止进程。
"""
import inspect
import ctypes
终止进程函数_async_raise(tid, exctype) && stop_thread(thread)
当服务器下线时,由于程序仍然没有结束,因为我们可以让服务器重新上线,因此在程序中我们需要终止服务器相关进程(显然需要终止的至少有连接监听进程)。
def _async_raise(tid, exctype):
"""raises the exception, performs cleanup if needed"""
tid = ctypes.c_long(tid)
if not inspect.isclass(exctype):
exctype = type(exctype)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
if res == 0:
raise ValueError("invalid thread id")
elif res != 1:
# """if it returns a number greater than one, you're in trouble,
# and you should call it again with exc=NULL to revert the effect"""
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
def stop_thread(thread):
_async_raise(thread.ident, SystemExit)
界面设计ServerUI类
没什么好说的,ServerUI类使用tkinter进行了UI界面的设计,可以调整UI界面中按钮、输入框、文本显示框及文本的位置与格式。
class ServerUI(object):
GUI = None
server_soc = None
port = None
text = None
isOn = False
host_ip = "127.0.0.1"
def __init__(self):
self.GUI = Tk()
self.GUI.title("Server")
self.GUI.geometry('500x460')
self.GUI.wm_resizable(False,False)
Label(self.GUI, text='服务端IP地址:' + self.host_ip,font=(20)).place(relx=.5, y=15, anchor="center")
Label(self.GUI, text='服务端端口号:' ,font=(20)).place(relx=.3, y=50, anchor="center")
self.port = Entry(self.GUI, width=10)
self.port.place(relx=.5, y=50, anchor="center")
Button(self.GUI,width=10,text='上线/下线',command=self.on_or_off).place(relx=.7, y=50, anchor="center")
self.text = scrolledtext.ScrolledText(self.GUI, width=78, height=22, font=('Times New Roman', 10))
self.text.place(relx=.5, y=240, anchor="center")
btn = Button(self.GUI, text='清空', font=('黑体', 14), height=1, command=lambda: self.text.delete("1.0", "end"))
btn.place(relx=.5, y=440, anchor="center")
self.GUI.mainloop()
实际上,以上程序控制的是这个👆UI界面的布局。
重头戏:Application_ServerUI类的实现
先看下这个类的定义以及初始化函数👇。
class Application_ServerUI(ServerUI):
def __init__(self):
ServerUI.__init__(self)
实际上,ServerUI是Application_ServerUI类的子类👇。
在ServerUI中给出了使用tkinter设计UI布局的定义,而在Application_ServerUI中,我则给出了每个定义下的实现(如,某个按钮应该与哪些行为绑定。可以在ServerUI中预设接口,然后在Application_ServerUI中定义函数来实现)。
完整实现如下:
class Application_ServerUI(ServerUI):
def __init__(self):
ServerUI.__init__(self)
def connection_accepted(self):
while True:
client_soc, addr = self.server_soc.s.accept