首先说一下聊天室需要的技术:
客户端-服务器模式的搭建
tcp/udp协议的选择
设计思路:
服务器接收客户端的登录(连接),然后接收客户端的消息,并根据消息内容选择回发对象。
服务器端先输入姓名作为登录的姓名,然后连接到服务器之后就可以发送给服务器,服务器进行处理。
选择通讯协议udp,因为这里是不需要三次握手四次挥手的过程,用udp就完全可以进行的,实现起来也比较简单。
问题点:
先启动服务器,然后多个客户端连接到服务器,此时假设客户端c1发送消息到服务器,c1希望能够收到消息,但是其他客户端还是在等待输入的地方,这样就会造成了c1与 c2...等其他客户端状态不一致的情况,此时服务器收到消息,服务器是不能阻塞的,所以只能讲将收到的消息回发,此时其他客户端因为阻塞在了输入函数,所以无法收到消息和显示。
解决这个问题的思路是在客户端建立2个进程,分别处理输入事件和接收事件,在哪个事件状态就绪,就可以获得到系统时间片,就能够得到执行,这样就可以保证c1发送了消息,服务器收到消息的时候,转发该条消息到连接到该服务器的每个客户端,实现了群聊天的功能。
聊天室具有如下功能
1,新客户端连接时,相当于有人加入群聊,系统通知到所有玩家该客户端的加入
2,客户端发送的消息,服务器转发给所有玩家
3,客户端断开连接,代表退出了群聊,系统发送该玩家退出消息给所有人(或者管理员)
其他说明:enrc="^^^^"是选择的一种加密方式,在这里类似于一个宏定义的功能定义一个全局变量,对发送的内容进行加密。因为是udp协议,所以其他人写的客户端程序也能连接到我的服务器,但是通过加密,就能识别到自己的客户端的内容,然后进行处理,其他程序的客户端不做处理来实现将其他客户端的连接屏蔽的目的。
具体代码如下:
服务器端代码---
# myserver.py
import socket
import select
import sys
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind(("127.0.0.1",9527))
enrc = "^^^^"
addrlist = []
namelist = []
# data = "welcome %s to join us"
while True:
# print(addrlist)
# print(namelist)
#recvfrom接收UDP消息,参数是每次接收消息的大小,返回接收到的内容
data,address = s.recvfrom(1024)
if enrc in data.decode():
name = data.decode().split(enrc)[0]
# print("name:",name)
if data.decode().endswith("end"):
addrlist.remove(address)
namelist.remove(name)
for i in range(len(addrlist)):
s.sendto((name+" quit").encode(),addrlist[i])
else:
if address in addrlist:
pass
else:
addrlist.append(address)
namelist.append(name)
# print("received string :",data.decode("utf-8"))
#sendto 发送UDP消息,,参数分别为消息和发送方的地址,返回发送的字节数
for x in addrlist:
s.sendto(data,x)
s.close()
客户端代码---
import socket
import os
import time
HOST = "127.0.0.1"
PORT = 9527
enrc = "^^^^"
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# s.bind((HOST,PORT))
# hello = "hello"
name = input("please input your name:\t")
data = name + enrc
# +hello
s.sendto(data.encode("utf-8"),(HOST,PORT))
#login successful
pid = os.fork()
if pid < 0:
print("create process failed")
#子进程执行
elif pid == 0:
while True:
data = s.recv(1024).decode('utf-8')
if data.endswith(enrc):
datalist = data.split(enrc)
print(("system msg:"+datalist[0]+" join group").center(80))
# print("said: ".join(datalist))
elif data.endswith("quit"):
print(data.center(80))
else:
datalist = data.split(enrc)
if name == datalist[0]:
print("%80s"%(datalist[0]+" "+time.ctime()))
print("%80s"%datalist[1])
else:
print("%-80s"%(datalist[0]+" "+time.ctime()))
print("%-80s"%datalist[1])
# print("%-80s"%(datalist[0]+" said: "+datalist[1]))
# print("receive from server\n",data.decode('utf-8'))
#父进程执行
else:
while True:
info = input()
if not info:
s.sendto((name+ enrc+"end").encode("utf-8"),(HOST,PORT))
break
else:
data = name + enrc + info
s.sendto(data.encode("utf-8"),(HOST,PORT))
"""
zhang3^^^^hello
zhang3^^^^end
zhang3 + "^^^^"
"""
在客户端需要说明一下:
父进程执行的是输入事件,因为2个进程都是死循环,但是都会阻塞,所以不会出现真正意义上的死循环造成系统资源过度浪费,客户端只有在自己主动退出群聊的情况下才会得到退出,所以肯定是客户端主动输入退出消息,这里是回车键,这样父进程就会结束,然后发送了一条退出消息给到服务器,然后服务器回发消息到客户端,只是本客户端子进程此时收到的消息是退出消息,所以选择不打印出来,然后结束了。这样做的目的是父进程先退出,子进程被系统进程收养,不会出现僵尸进程对进程资源的浪费。
总结:
首先,选择合理的加密方式和解析方式,给程序带来了很大的方便,也可以避免其他人的恶意攻击。
其次,在选择协议上采用的是更方便的udp协议,对于聊天室来说更易于实现。
再则分清楚客户端和服务器分别需要干嘛,将各自的功能确认清楚,这样逻辑就不会混乱。
最后当然是针对问题的根本点进行解决,解决了客户端状态不一致的问题,那么整个程序也就可以顺利的运行起来了。
特殊说明:该程序肯定还是有一些潜在的bug,难免考虑不到的地方,如果读者有看到的,请指出改正,本人后续也会时常翻看并完善的。