9、匹配系统-thrift实现

源码地址https://gitee.com/pxlsdz/hyld

演示地址http://121.199.59.80/hyld/

可以先参考我以前写的:thrift实现多服务多线程的匹配系统

thrift官方教程

image-20211226150140663

server每一个都是单线程的,所以按照之前的逻辑实现按照战斗力进行匹配会发生阻塞,所以需要一个额外的模块来实现匹配。

thrift 开另外一个进程或者另外一个服务器开一进程来执行任务两个进程的通信。

  • 通信方式ip:port

  • 匹配系统(每秒钟匹配一次,匹配期间,数组不发生变化,多线程(消息队列:缓存信息,匹配)。

匹配系统

thrift生成代码

编写~/project/hyld/match_system/thrift/match.thrift

namespace py match_service

service Match {
    i32 add_palyer(1: i32 score, 2: string uuid, 3: string username, 4: string photo, 5: string channel_name),
}

使用thrift工具来创建,客户端和服务端需要的代码

# ~/project/hyld/match_system/src/
thrift --gen py ../thrift/match.thrift
mv gen-py/ match_server
touch __init__.py
vim main.py

如果没有安装包,执行pip install thrift

服务端

暴露出api,实现在函数外面依旧可以访问channel功能,即可以在匹配进程调用websocket服务,让thrift进程调用asgi进程里的函数

~/project/hyld/asgi.py

import os

# 设置环境变量,越靠前越好
import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hyld.settings')
django.setup()

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from game.routing import websocket_urlpatterns

from channels.layers import get_channel_layer

# 实现在函数外面依旧可以访问channel功能,即可以在匹配进程调用websocket服务
channel_layer = get_channel_layer()

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
})

匹配系统,将官方教程python例子Server代码复制过来

~/project/hyld/match_system/src/main.py

#! /usr/bin/env python3

import glob
import sys

# sys.path.append('gen-py')
# 将django的家目录加到import包里
sys.path.insert(0, glob.glob('../../')[0])

from match_server.match_service import Match

from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from thrift.server import TServer

#  线程安全(同步,读写不会发生冲突)的队列
from queue import Queue

from time import sleep
from threading import Thread
from hyld.asgi import channel_layer
# 并行变为串行
from asgiref.sync import async_to_sync
from django.core.cache import cache

# 消息队列异步接收玩家信息,匹配池在这里面取消息
queue = Queue()


class Player:
    def __init__(self, score, uuid, username, photo, channel_name):
        self.score = score
        self.uuid = uuid
        self.username = username
        self.photo = photo
        self.channel_name = channel_name
        self.waiting_time = 0  # 等待时间,时间越长,条件越低


# 匹配池
class Pool:
    def __init__(self):
        self.players = []

    def add_player(self, player):
        print("add player:%s %d" % (player.username, player.score))
        self.players.append(player)

    def check_match(self, a, b):
        dt = abs(a.score - b.score)
        a_max_dif = a.waiting_time * 50
        b_max_dif = b.waiting_time * 50
        return dt <= a_max_dif and dt <= b_max_dif

    def match_success(self, ps):
        print("Match Success: %s %s %s" % (ps[0].username, ps[1].username, ps[2].username))
        room_name = "room-%s-%s-%s" % (ps[0].uuid, ps[1].uuid, ps[2].uuid)
        players = []
        for p in ps:
            async_to_sync(channel_layer.group_add)(room_name, p.channel_name)
            players.append({
                'uuid': p.uuid,
                'username': p.username,
                'photo': p.photo,
                'hp': 100,
            })
        cache.set(room_name, players, 3600)
        for p in ps:
            async_to_sync(channel_layer.group_send)(
                room_name,
                {
                    'type': "group_send_event",
                    'event': "create_player",
                    'uuid': p.uuid,
                    'username': p.username,
                    'photo': p.photo,
                }
            )

    def increase_waiting_time(self):
        for player in self.players:
            player.waiting_time += 1

    def match(self):
        while len(self.players) >= 3:
            self.players = sorted(self.players, key=lambda p: p.score)
            flag = False
            for i in range(len(self.players) - 2):
                a, b, c = self.players[i], self.players[i + 1], self.players[i + 2]
                if self.check_match(a, b) and self.check_match(a, c) and self.check_match(b, c):
                    self.match_success([a, b, c])
                    self.players = self.players[:i] + self.players[i + 3:]
                    flag = True
                    break
            if not flag:
                break

        self.increase_waiting_time()


# 接收客户端发来的消息
class MatchHandler:
    def add_player(self, score, uuid, username, photo, channel_name):
        player = Player(score, uuid, username, photo, channel_name)
        queue.put(player)
        return 0


# get_nowait有就返回,没有就抛出异常
def get_player_from_queue():
    try:
        return queue.get_nowait()
    except:
        return None


# 1. 匹配池从消息队列取玩家消息
# 2. 进行匹配服务
def worker():
    pool = Pool()
    while True:
        player = get_player_from_queue()
        if player:
            pool.add_player(player)
        else:
            pool.match()
            sleep(1)


if __name__ == '__main__':
    handler = MatchHandler()
    processor = Match.Processor(handler)
    transport = TSocket.TServerSocket(host='127.0.0.1', port=9090)
    tfactory = TTransport.TBufferedTransportFactory()
    pfactory = TBinaryProtocol.TBinaryProtocolFactory()

    # 匹配线程
    # server.serve()是阻塞进程,所以匹配线程写在其上方
    # daemon为守护进程,false的意思主进程关闭,该进程继续执行
    Thread(target=worker, daemon=True).start()

    # 接收消息线程
    # 单线程版本
    # server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)

    # You could do one of these for a multithreaded server
    server = TServer.TThreadedServer(
        processor, transport, tfactory, pfactory)
    # 多线程限制版本,可能最多只有十个线程
    # server = TServer.TThreadPoolServer(
    #     processor, transport, tfactory, pfactory)

    print('Starting the server...')
    server.serve()
    print('done.')

执行函数

chomd 764 main.py
./main.py

如果"/usr/bin/env: ‘python3\r’: No such file or directory"错误,,则vi main.py, 按下:输入set ff=unix

客户端

~/project/hyld/game/consumers/multiplayer/index.py

重写 create_player 函数,参考地址thrift实现多服务多线程的匹配系统代码

from channels.generic.websocket import AsyncWebsocketConsumer
import json
from django.conf import settings
from django.core.cache import cache

from thrift import Thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from match_system.src.match_server.match_service import Match
from game.models.player.player import Player
# 数据库是串行操作,但这里都是并行函数,需要将数据库改为并行操作
from channels.db import database_sync_to_async

class MultiPlayer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.accept()

    async def disconnect(self, close_code):  # 大概率断开连接时调用
        if self.room_name:
            await self.channel_layer.group_discard(self.room_name, self.channel_name)

    async def create_player(self, data):
        self.room_name = None

        # Make socket
        transport = TSocket.TSocket('localhost', 9090)

        # Buffering is critical. Raw sockets are very slow
        transport = TTransport.TBufferedTransport(transport)

        # Wrap in a protocol
        protocol = TBinaryProtocol.TBinaryProtocol(transport)

        # Create a client to use the protocol encoder
        client = Match.Client(protocol)

        def db_get_player():
            return Player.objects.get(user__username=data['username'])
        player = await database_sync_to_async(db_get_player)()

        # Connect!
        transport.open()

        # 具体业务代码
        client.add_player(player.score, data['uuid'], data['username'], data['photo'], self.channel_name)

        # Close!
        transport.close()

    async def move_to(self, data):
        await self.channel_layer.group_send(
            self.room_name,
            {
                'type': "group_send_event",
                'event': "move_to",
                'uuid': data['uuid'],
                'tx': data['tx'],
                'ty': data['ty'],
            }
        )

    async def shoot_fireball(self, data):
        await self.channel_layer.group_send(
            self.room_name,
            {
                'type': "group_send_event",
                'event': "shoot_fireball",
                'uuid': data["uuid"],
                'tx': data['tx'],
                'ty': data['ty'],
                'ball_uuid': data['ball_uuid'],
            }
        )

    async def attack(self, data):
        await self.channel_layer.group_send(
            self.room_name,
            {
                'type': "group_send_event",
                'event': "attack",
                'attackee_uuid': data["attackee_uuid"],
                'uuid': data['uuid'],
                'x': data['x'],
                'y': data['y'],
                'angle': data['angle'],
                'damage': data['damage'],
                'ball_uuid': data['ball_uuid'],
            }
        )

    async def blink(self, data):
        await self.channel_layer.group_send(
            self.room_name,
            {
                'type': "group_send_event",
                'event': "blink",
                'uuid': data['uuid'],
                'tx': data['tx'],
                'ty': data['ty'],
            }
        )

    async def message(self, data):
        await self.channel_layer.group_send(
            self.room_name,
            {
                'type': "group_send_event",
                'event': "message",
                'uuid': data['uuid'],
                'username': data['username'],
                'text': data['text'],
            }
        )

    async def group_send_event(self, data):
        if not self.room_name:
            keys = cache.keys('*%s*' % (data['uuid']))
            if keys:
                self.room_name = keys[0]
        await self.send(text_data=json.dumps(data))

    # 接收前端信息
    async def receive(self, text_data):
        data = json.loads(text_data)
        event = data['event']
        if event == "create_player":
            await self.create_player(data)
        elif event == "move_to":
            await self.move_to(data)
        elif event == "shoot_fireball":
            await self.shoot_fireball(data)
        elif event == "attack":
            await self.attack(data)
        elif event == "blink":
            await self.blink(data)
        elif event == "message":
            await self.message(data)

~/project/hyld/game/models/player/player.py

数据库添加战斗力

score = models.IntegerField(default=1500)

更新数据库

python3 manage.py makemigrations
python3 manage.py migrate

重启服务器

posted @ 2022-01-08 20:15  pxlsdz  阅读(251)  评论(0编辑  收藏  举报