目录
一、主要设计思路: 2
1、客户端主要设计思路: 2
2、服务器主要设计思路 3
3、中间协议包制造和收到的数据处理设计思路 4
二、遇到的问题和解决方法 4
1、问题描述: 4
2、问题描述: 5
3、问题描述: 5
4、问题描述: 5
5、问题描述: 5
服务器端: 5
1.问题描述: 5
2.问题描述: 5
3.问题描述: 6
4.问题描述: 6
协议包制造和数据处理: 6
1、问题描述: 6
2、问题描述: 6
3、问题描述: 6
4、问题描述: 6
三、成果展示 7
客户端: 7
服务器: 9
四、实习收获和心得体会 11
2、服务器主要设计思路
服务器主要分为两类 Server 类和 Processor 类,Server 类用来接收客户端的连接,这是主线程,服务器为每一个客户端开辟一个线程 tcplink,每个线程中有一个用来处理线程的对象-processor,用来处理每一个客户端传来的包。主要分为以下几个部分。
接受请求:
服务器接受客户端传来的数据流,然后将数据流解析成一个 packet 对象, 分为两种情况来处理。
一:数据流不合法,就是传递过来的数据的格式,并没有按照标准的格式,或者中间的数据是不合法的。这些数据将会被打包成一个 None,然后判断后,断开与服务器的连接,避免是恶意客户端的攻击。
二:数据合法,将 packet 传给 processor,使用 doMessage 函数,将 packet 进行分发, 发给同的处理包的函数。如 doLogin()
请求处理:
处理对象:
处理请求的工作由 processor 来做。Processor 有 curPlayer,由 curPlay 来记录玩家的所有信心,并可以用来判断(curPlay 是否为 None)玩家是否登录,还有两个集合用来记录当前登录的人和注册过的人。另外有 doSpeak,doLogin 等函数,用来处理客户端发来的包。
处理方法:
Login:当用户登录的时候,先判断客户端的登录状态(当前客户端已登录),玩家名字是否已登录,如果合法,进行登录处理,并向所有已登录玩家发送 LoginReply,
MoveNoTify 包,,同时,有已登录的玩家,向当前客户端发送 MoveNotify 包,表明自己的位置。
Move,Speak,Attack:这三者大同小异,都是处理用户发来的包中的信息,然后进行 Notify。
Loginout:当玩家退出登录的时候,首先要在服务器的 users 中记录玩家的信息, 并保存,然后断开与客户端的连接,并广发 LogoutNotify 包,通知其他玩家,当前玩家已下线。
Notify 处理:几乎所有的回复都需要进行广播,所以需要使用 broadCast(packet),这个函数是用来遍历所有的已登录玩家,并发送相应的 Notify 包
HP 按时递增:为 HP 开辟一个单独线程,按时调用,实现 HP 的按时增加。
异常处理:
异常分为两种:发来的数据错误,客户端的异常中断。
一:数据错误
将与客户端的连接断开。
二:客户端的异常中断
当客户端中断后,客户端会不断发来空字符,所以当收到空字符的时候,就将客户端的连接断开。
3、中间协议包制造和收到的数据处理设计思路
协议包指按照协议制造的包,协议包方面的设计属于对协议的理解部分,需要正确的理解协议并且按照协议制造这些包。数据处理处理指客户端或者服务器收到一个消息时,需要对这个消息发送过来的数据进行处理,将其解析成需要的信息,而且需要对消息进行判断,以防有错误的信息。
具体设计:
协议包:
1.设计两个枚举类,在 Constants.py 文件当中。一个叫 messages,里面的枚举的消息类型,一个叫 direction,枚举移动的方向(可以参照给的 Constants.h)
2.设计一个基类 Package,里面有 type(包的类型)、ver(版本号)、len(消息的长度)、
message(用于套接字发送的字节数据,可能这个不好理解,意思是 sock 发送时,发送的是
sock.send(Package.message))
3.各种类型协议包继承于 Package,然后里面要添加自己独特的属性,以及 message 的计算。Eg: LOGIN_REQUEST 包,继承于 Package。是客户端登录时发送的请求包,所以按照协议,里面添加了属性 name,并且实现了具体的 message 的制造。
数据处理:
1.设计 operation 类,里面是各种静态函数用来进行数据处理。
2. operation 里面的 make_packet(),接收一个 type(包的类型)和*args,进行包的制造,会返回一个调用者想要的 package,如果给定的数据有问题(比如:长度问题、类型不匹配等),就会返回 None,方便使用这个函数的人进行判断。
3. operation 里面的 unpack_msg (),接收一个 msg(是套接字接收的信息,即msg=sock.recv()),然后对 msg 进行解析,然后返回相应的 packages(列表,因为 msg 中可能不止一个包)。如:接收的 msg 是关于 login_reply 包,packages 中就包含 login_reply 包,使用时只用 packages[i].相应属性即可使用。如果接收的 msg 出现错误(如:长度不对,或者版本号错误),就会 None,方便使用者来处理错误信息。
from string import *
from Player.player import *
from socket import *
from Package.Constants import *
from Package.Operation import *
from time import sleep
import threading
import sys
import os
import getopt
lock=threading.RLock()
class client:
def __init__(self,IP,port):
self.IP=IP
self.port=port
try:
#如果服务器未开启,则抛出异常
self.client_socket=socket(AF_INET,SOCK_STREAM)
self.client_socket.connect((IP,port))
except:
sys.stdout.write('The gate to the tiny world of warcraft is not ready.\n')
self.show_input()
os._exit(0)
self.vision_range=5 #视野范围
self.player=None #玩家
self.direction_dictionary={'north':direction.NORTH.value,'south':direction.SOUTH.value,
'west':direction.WEST.value,'east':direction.EAST.value}
#方向集合
self.players_in_vision=set()
#在视野范围内的玩家,包括这台客户端的玩家
def do_login(self,name):
#处理login指令
#参数为登录名
if " " in name:
#名字中不能有空格
sys.stdout.write("! Invalid name: %s"%(name))
self.show_input()
else:
requestType = messages.LOGIN_REQUEST #message type
package=operation.make_packet(requestType,name)
self.player=player(name)
self.client_socket.send(package.message)
def do_move(self,direction):
#处理move指令
#参数为(north,south,west,east)
requestType = messages.MOVE
move_directon=self.direction_dictionary.get(direction)
if move_directon==None:
#错误的方向参数
sys.stdout.write('! Invalid direction: %s.\n'%(direction))
self.show_input()
else:
#方向参数正确
package=operation.make_packet(requestType,move_directon)
self.client_socket.send(package.message)
def do_attack(self,name):
#处理attack指令
if name in self.players_in_vision and name!=self.player.name:
#只有攻击对象在攻击者视野范围内且攻击对象不时攻击者的时候才能攻击
requestType = messages.ATTACK
package=operation.make_packet(requestType,name)
self.client_socket.send(package.message)
else:
#攻击对象不在视野范围内
sys.stdout.write('The target is not visible.\n')
self.show_input()
def do_speak(self,msg):
#处理speak指令
#参数msg为speak内容
requestType = messages.SPEAK
package=operation.make_packet(requestType,msg)
self.client_socket.send(package.message)
def do_logout(self):
#处理登出
requestType = messages.LOGOUT
package=operation.make_packet(requestType)
self.client_socket.send(package.message)
def do_login_reply(self,package,name):
#处理LOGIN_REPLY
#参数package:解析服务器数据所得的包,Packages
#参数name是玩家登录时的登录名
if package.Error_code==0: #login success
#登录成功,创建player对象,同步属性
sys.stdout.write("Welcome to the tiny world of warcraft.\n")
self.player=player(name)
self.player.setAttribute(package.HP,package.EXP,package.x,package.y) #get attributes
self.players_in_vision.add(name)
elif package.Error_code==1:
#登录的帐号已经被登录
self.player=None
sys.stdout.write('A player with the same name is already in the game.\n')
self.show_input()
def do_move_notify(self,package):
#处理MOVE_NOTIFY消息
#参数是解析服务器数据所得的包,Packages
if package.player_name == self.player.name:
#如果玩家名是这台客户端上登录的玩家,则更新玩家属性
self.player.setAttribute(package.HP, package.EXP, package.x, package.y) # change player local
if self.point_in_vision(package.x,package.y):
#当玩家处于视野范围内时才会输出位置信息
sys.stdout.write("%s: location=(%d,%d), HP=%d, EXP=%d.\n"%(package.player_name,package.x,package.y,package.HP,package.EXP))
self.show_input()
self.players_in_vision.add(package.player_name)
else:
self.players_in_vision.discard(package.player_name)
def point_in_vision(self,x,y):
#判断一个点(x,y)是否在视野范围内
if x>=self.player.x-self.vision_range and x<=self.player.x+self.vision_range:
if y>=self.player.y-self.vision_range and y<=self.player.y+self.vision_range:
return True
return False
def do_attack_notify(self,package):
#处理ATTACK_NOTIFY消息
#参数是从服务器接受的数据解析成的包
Attacker=package.Attacker_name
Victim=package.Victim_name
if Attacker in self.players_in_vision and Victim in self.players_in_vision:
#判断攻击者和被攻击者是否在事业范围内
#只有两者都在事业范围内时才会打印消息
if not package.HP==0:
#血量非0时打印造成伤害
sys.stdout.write("%s damaged %s by %d. %s\'s HP is now %d.\n"%(Attacker,Victim,package.Damage,Victim,package.HP))
else:
#血量为0时打印击杀消息
sys.stdout.write("%s killed %s.\n"%(Attacker,Victim))
self.show_input()
def do_speak_notify(self,package):
#处理SPEAK_NOTIFY消息
#参数时Packages的对象
sys.stdout.write("%s: %s.\n"%(package.Broadcaster_name,package.Msg))
self.show_input()
def do_logout_notify(self,package):
#处理LOGOUT_NOTIFY数据
#参数是Packages的对象
sys.stdout.write("Player %s has left the tiny world of warcraft.\n"%(package.player_name))
self.show_input()
def do_invalid_state(self,package):
#处理INVALID-STATE数据
#参数是一个Packages的对象
if package.Error_code==0:
sys.stdout.write("You must log in first.\n")
#登录之前不能做其他指令
elif package.Error_code==1:
sys.stdout.write("You already logged in.\n")
#不能重复登录
self.show_input()
def invalidcommand(self,list):
#参数是指令分割后得到的数组
#判断指令是否符合规范
if len(list)==0 or len(list)>2:
return False
if list[0]=="logout" and len(list)>1:
return False
return True
def receiv_msg(self):
msg=self.client_socket.recv(1024)
if len(msg)==0:
#接受到一个空字符,说明连接已经断开
sys.stdout.write("The gate to the tiny world of warcraft has disappeared.\n")
self.show_input()
os._exit(0)
packages = operation.unpack_msg(msg)
if packages==None:
#打包失败,说明接受到一个畸形包
sys.stdout.write("Meteor is striking the world.\n")
self.show_input()
os._exit(0)
#返回值为一个Package的数组
return packages
def run_send(self):
self.show_input()
while True:
#输入指令
command=input()
self.show_input()
command_list=command.split(" ",1)
#处理指令及参数
Type=""
if self.invalidcommand(command_list):
Type=command_list[0]
#指令格式
else:
sys.stdout.write("! Invalid command: %s.Available commands = login, move, attack, speak, logout.\n"%(Type))
self.show_input()
if Type=="login":
name=command_list[1]
self.do_login(name)
elif Type=="move":
direction=command_list[1]
self.do_move(direction)
elif Type=="attack":
name=command_list[1]
self.do_attack(name)
elif Type=="speak":
msg=command_list[1]
self.do_speak(msg)
elif Type=="logout":
self.do_logout()
sys.stdout.write("The gate to the tiny world of warcraft has disappeared.\n")
self.show_input()
os._exit(0)
else:
sys.stdout.write("! Invalid command: %s.Available commands = login, move, attack, speak, logout.\n"%(Type))
self.show_input()
sleep(0.8)
def run_receiv(self):
#从服务器接受消息
while True:
packages=self.receiv_msg()
lock.acquire()
for package in packages:
type=package.type
#判断是那一种消息类型
if type is messages.LOGIN_REPLY:
self.invalidlocation(package.x,package.y)
self.do_login_reply(package,self.player.name)
elif type is messages.MOVE_NOTIFY:
self.invalidname(package.player_name)
self.invalidlocation(package.x,package.y)
self.do_move_notify(package)
elif type is messages.ATTACK_NOTIFY:
self.invalidname(package.Attacker_name)
self.invalidname(package.Victim_name)
self.do_attack_notify(package)
elif type is messages.SPEAK_NOTIFY:
self.invalidname(package.Broadcaster_name)
self.invalidspeak(package.Msg)
self.do_speak_notify(package)
elif type is messages.LOGOUT_NOTIFY:
self.invalidname(package.player_name)
self.do_logout_notify(package)
elif type is messages.INVALID_STATE:
self.do_invalid_state(package)
lock.release()
def show_input(self):
#每次输出结束后在新的一行打印command>,清除缓冲区
sys.stdout.write("command> ")
sys.stdout.flush()
def invalidname(self,name):
#判断收到的包中的名字是否符合规范
if name==None or name=="" or " " in name:
sys.stdout.write("Meteor is striking the world.\n")
self.show_input()
os._exit(0)
def invalidspeak(self,msg):
#判断收到的speak内容是否符合规范
if len(msg)==0:
sys.stdout.write("Meteor is striking the world.\n")
self.show_input()
os._exit(0)
def invalidlocation(self,x,y):
#判断位置信息是否有效
if x<0 or x>99 or y<0 or y>99:
sys.stdout.write("Meteor is striking the world.\n")
self.show_input()
os._exit(0)
def main(argv):
try:
opts,args=getopt.getopt(argv,'s:p:',[])
ip=None
port=None
for opt,arg in opts:
if opt=='-s':
ip=arg
elif opt=='-p':
port=arg
#ip='192.168.43.22'
#port=8080
oneclient=client(ip,int(port))
try:
t1=threading.Thread(target=oneclient.run_receiv)
t2=threading.Thread(target=oneclient.run_send)
t1.setDaemon(True)
t2.setDaemon(True)
t1.start()
t2.start()
t1.join()
t2.join()
except KeyboardInterrupt:
pass
except:
sys.stdout.write("Error: unable to start thread.\n")
except:
sys.stdout.write("! ./client -s -p \n")
if __name__=='__main__':
main(sys.argv[1:])























