Thumbnail Server设计(uvicorn)

Thumbnail Server

最近有个任务,是独自开发一个缩略图服务器,用于专门处理:图片、PDF、PSD、HEIC、XMIND…的文件缩略图

框架 Uvicorn
实现方式:运用任务队列 Queue,前面的文章有讲过,这里不再过多讲解
特点:运用必会的知识点异步操作(协程),async、await
注:详细的缩略图生成功能就不讲解了,主要讲解大体的实现思路和难点
话不多说,开始!

Uvicorn

An ASGI web server, for Python.
详细请见:Uvicorn

Thumbnail Server 设计

准备任务

1. HTTPRequest创建
class HTTPRequest(object):
    def __init__(self, **scope):
        self.__dict__.update(scope)
        self.parse()

    def parse(self):
        self.parse_headers()
        self.parse_cookies()
        self.parse_url()
        self.parse_query_dict()

    def parse_headers(self):
        raw_headers = self.headers
        headers = {}
        for item in raw_headers:
            k = item[0].decode().lower()
            v = item[1].decode()
            if k in headers:
                headers[k].append(v)
            else:
                headers[k] = [v]
        self.headers = headers

    def parse_cookies(self):
        cookies = ()
        if self.headers.get('cookie'):
            cookie_string = self.headers.get('cookie')[0]
            s = SimpleCookie(cookie_string)
            cookies = {v.key: v.value for k, v in s.items()}
        self.cookies = cookies

    def parse_url(self):
        self.url = self.path[len(settings.URL_PREFIX):]

    def parse_query_dict(self):
        query_string = self.query_string.decode()
        self.query_dict = urllib.parse.parse_qs(query_string)

创建一个HTTPRequest类,用来解析其他服务端传来的请求(请求头、Cookie、Url和请求参数)

2. 创建响应头、响应体(Response)

def gen_response_header(status, content_type):
    return {
        'type': 'http.response.start',
        'status': status,
        'headers': [
            [b'Content-Type', content_type],
            [b'Cache-Control', b'max-age=604800, public']
        ]
    }


def gen_response_body(body):
    return {
        'type': 'http.response.body',
        'body': body
    }


def gen_error_response(status, error_msg):
    response_header = gen_response_header(status, TEXT_CONTENT_TYPE)
    response_body = gen_response_body(error_msg.encode('utf-8'))

    return response_header, response_body


def gen_text_response(text):
    response_header = gen_response_header(200, TEXT_CONTENT_TYPE)
    response_body = gen_response_body(text.encode('utf-8'))

    return response_header, response_body

def gen_cache_response():
    response_start = gen_response_start(304, THUMBNAIL_CONTENT_TYPE)
    response_body = gen_response_body(EMPTY_BYTES)

    return response_start, response_body

async def gen_thumbnail_response(request, thumbnail_info):
	"""缩略图创建"""
    content_type = 'application/json; charset=utf-8'
    result = {}
    etag = thumbnail_info['etag']
    .....
    task_id, status = generate_thumbnail(request, thumbnail_info)
    if status == 400:
        err_msg = 'Failed to create thumbnail.'
        return gen_error_response(status, err_msg)
    if not isinstance(task_id, bool):
        while True:
            if thumbnail_task_manager.query_status(task_id)[0]:
                break
    src = get_thumbnail_src(repo_id, size, path)
    result['encoded_thumbnail_src'] = quote(src)
    result = json.dumps(result)
    result_b = str(result).encode('utf-8')

    response_header = gen_response_header(200, content_type)
    response_header['headers'].append([b'Last-Modified', last_modified.encode('utf-8')])
    response_header['headers'].append([b'ETag', etag.encode('utf-8')])

    response_body = gen_response_body(result_b)
    return response_header, response_body


async def thumbnail_get(request, thumbnail_info):
    """
    缩略图获取
    """
    thumbnail_file = thumbnail_info['thumbnail_path']
    last_modified = thumbnail_info['last_modified']
    etag = thumbnail_info['etag']
    if not os.path.exists(thumbnail_file):
        task_id, status = generate_thumbnail(request, thumbnail_info)
        if status == 400:
            err_msg = 'Failed to create thumbnail.'
            return gen_error_response(status, err_msg)
        while True:
            if thumbnail_task_manager.query_status(task_id)[0]:
                break
    try:
        with open(thumbnail_file, 'rb') as f:
            thumbnail = f.read()
            response_header = gen_response_header(200, 'image/' + THUMBNAIL_EXTENSION)
            response_body = gen_response_body(thumbnail)
            if thumbnail:
                response_start['headers'].append([b'Last-Modified', last_modified.encode('utf-8')])
                response_start['headers'].append([b'ETag', etag.encode('utf-8')])

    
            return response_header, response_body
    except:
        err_msg = 'Failed to create thumbnail.'
        return gen_error_response(400, err_msg)

创建响应头和响应体,返回给前端,前端获取到对应的信息进行渲染。
代码逻辑:

  • 前端发起请求
  • 服务端根据请求的URL 去找到指定的函数
  • 当发起创建请求时,将执行 gen_thumbnail_response 函数,去返回请求头和请求体,函数逻辑如下:
    • 根据函数返回的任务ID和状态去判断该缩略图文件是否被创建
    • 若未被创建,则循环等待任务创建完成后将缩略图文件位置进行返回
    • 若已经被创建,则直接返回其位置(前端要求,也可返回二进制数据)
  • 当发起获取请求时,将执行thumbnail_get函数,返回相应的请求头和请求体,函数逻辑如下:
    • 前端发起对应的请求URL
    • 后端判断其缩略图文件是否存在
    • 未创建,则将创建任务添加至任务队列,返回任务ID和状态
    • 循环等待该任务的完成,完成后直接读取并返回二进制
    • 若创建直接打开对应的文件返回其二进制

返回头中包含ETag和Last-Modified和Cache-Control,其中ETag和Last-Modified用来做304处理,Cache-Control用来做缓存处理

ETag和Last-Modified详解
ETag和Last-Modified都是HTTP头部字段,用于缓存控制,它们提供了服务器上资源的状态信息,以便浏览器可以决定是否需要从服务器重新加载资源。
我们用大白话解释:

  1. 客户端向服务器请求资源 thumbnail
  2. 服务端返回数据并带上ETag或Last-Modified(他们有优先级)
  3. 客户端再次请求资源 thumbnail
  4. 由于上次服务器给他返回了一个 Etag或Last-Modified,这次请求的时候他会带上这个 If-None-Match(对应ETag的内容)或 If-Modified-Since(对应Last-Modified)
  5. 服务器发现请求中包含 Etag,判断是否过期,没过期则返回 304 Not Modified
  6. 客户端强制刷新(如chrome中ctrl+shift+R刷新页面),请求中剔除 ETag
  7. 服务器未发现请求中包含 Etag,返回资源 thumbnial,并带上一个 ETag

Cache相关代码如下:

def cache_check(request, info):
    etag = info.get('etag')
    if_none_match_headers = request.headers.get('if-none-match')
    if_none_match = if_none_match_headers[0] if if_none_match_headers else ''

    last_modified = info.get('last_modified')
    if_modified_since_headers = request.headers.get('if-modified-since')
    if_modified_since = if_modified_since_headers[0] if if_modified_since_headers else ''
    if (if_none_match and if_none_match == etag) \
            or (if_modified_since and if_modified_since == last_modified):
        return True
    else:
        return False

在返回响应头和响应体之前,我们需要先对缓存进行检查,增效:

   try:
        if cache_check(request, thumbnail_info):
            response_header, response_body = gen_cache_response()
            await send(response_header)
            await send(response_body)
            return
    except Exception as e:
        logger.exception(e)

注意⚠️:Cache-Control:服务器的响应包含Cache-Control头的话,指示浏览器可以缓存资源多长时间。如果缓存时间足够长,浏览器可能会在缓存未过期时直接使用缓存,而不发送If-None-Match头。

3. 任务管理设计(TaskManager)

大体的请求和响应我们都已经设计好了,接下来设计一个任务队列,用来对大量的 缩略图创建任务进行管理,该任务管理设置了两个任务队列,一个图片处理队列,一个视频处理队列,分别用两个线程去维护

任务管理的设计前面文章已经讲过,这次就不做过多的解释,直接上伪代码:

class ThumbnailManager(object):

    def __init__(self):
        self.tasks_map = {}
        self.task_results_map = {}
        self.image_queue = queue.Queue()
        self.video_queue = queue.Queue()
        self.current_task_info = {}
        self.threads = []
        
    def add_image_creat_task(self, func, bb, file_id, path, size, thumbnail_file):
        task_id = str(uuid.uuid4())
        task = (func, (bb, file_id, path, size, thumbnail_file))
        self.image_queue.put(task_id)
        self.tasks_map[task_id] = task
        return task_id

    def add_video_task(self, func, aa, path, size, thumbnail_file):
        task_id = str(uuid.uuid4())
        task = (func, (aa, path, size, thumbnail_file))
        self.video_queue.put(task_id)
        self.tasks_map[task_id] = task
        return task_id

	.........

    def query_status(self, task_id):
        task_result = self.task_results_map.pop(task_id, None)
        if task_result == 'success':
            return True, None
        if isinstance(task_result, str) and task_result.startswith('error_'):
            return True, task_result[6:]
        return False, None

    def handle_image_task(self):
        while True:
            try:
                image_id = self.image_queue.get(timeout=2)
            except queue.Empty:
                continue
            except Exception as e:
                logger.error(e)
                continue
            task = self.tasks_map.get(image_id)
            if type(task) != tuple or len(task) < 1:
                continue
            task_info = image_id + ' ' + str(task[0])
            try:
                self.current_task_info[image_id] = task_info
                start_time = time.time()
                # run
                task[0](*task[1])
                self.task_results_map[image_id] = 'success'

                finish_time = time.time()
                self.current_task_info.pop(image_id, None)
            except Exception as e:
                self.task_results_map[image_id] = 'error_' + str(e.args[0])
                self.current_task_info.pop(image_id, None)
            finally:
                self.tasks_map.pop(image_id, None)

    def handle_video_task(self):
        while True:
            try:
                video_id = self.video_queue.get(timeout=2)
            except queue.Empty:
                continue
            except Exception as e:
                continue
            task = self.tasks_map.get(video_id)
            if type(task) != tuple or len(task) < 1:
                continue
            task_info = video_id + ' ' + str(task[0])
            try:
                self.current_task_info[video_id] = task_info
                start_time = time.time()
                # run
                task[0](*task[1])
                self.task_results_map[video_id] = 'success'                	
                self.current_task_info.pop(video_id, None)
            except Exception as e:
                self.task_results_map[video_id] = 'error_' + str(e.args[0])
                self.current_task_info.pop(video_id, None)
            finally:
                self.tasks_map.pop(video_id, None)

    def run(self):
        image_name = 'ImageManager Thread'
        video_name = 'VideoManager Thread'

        image_t = threading.Thread(target=self.handle_image_task, name=image_name)
        video_t = threading.Thread(target=self.handle_video_task, name=video_name)
        self.threads.append(image_t)
        self.threads.append(video_t)
        image_t.start()
        video_t.start()


thumbnail_task_manager = ThumbnailManager()

该服务器启动时,即启动该任务管理线程

  • 前端发起请求时,走对应处理,调用对应的任务添加函数即add_image_creat_taskadd_video_task
  • 将对应的创建缩略图任务添加到该任务队列中(imgae_queue和video_queue)
  • 俩线程通过一直循环的方式查看任务队列中是否存在任务,存在则进行执行,不存在则一直监听
  • 可以通过query_status查询任务的完成状态

4. 前期准备工作(序列化设计ThumbnailSeralizer)

在这个模块中,我们将对其params(请求参数)、session及resource(资源) 进行检查,主要为之后的 thumbnail 生成做准备

代码如下:

class ThumbnailSerializer(object):
    def __init__(self, request):
        self.request = request
        self.check()
        self.gen_thumbnail_info()

    def check(self):
        self.params_check()
        self.session_check()
        self.resource_check()

    def gen_thumbnail_info(self):
        thumbnail_info = {}
        thumbnail_info.update(self.params)
        thumbnail_info.update(self.resource)
        self.thumbnail_info = thumbnail_info

    def resource_check(self):
        size = self.params['size']
        file_path = self.params['file_path']
        file_name = os.path.basename(file_path)
        filetype, fileext = get_file_type_and_ext(file_name)
        ......
        thumbnail_dir = os.path.join(settings.THUMBNAIL_DIR, str(size))
        thumbnail_file = os.path.join(thumbnail_dir, file_id)
        if not os.path.exists(thumbnail_dir):
            os.makedirs(thumbnail_dir)
        file_obj = seafile_api.get_dirent_by_path(repo_id, file_path)
        last_modified_time = file_obj.mtime
        last_modified = formatdate(int(last_modified_time), usegmt=True)
        etag = '"' + file_id + '"'
        self.resource = {
            'file_size': file_size,
            'file_id': file_id,
            'file_ext': fileext,
            'file_type': filetype,
            'file_name': file_name,
            'thumbnail_dir': thumbnail_dir,
            'thumbnail_path': thumbnail_file,
            'last_modified': last_modified,
            'etag': etag
        }

    def get_enable_file_type(self):
        enable_file_type = [IMAGE]
        if settings.ENABLE_VIDEO_THUMBNAIL:
            enable_file_type.append(VIDEO)
        if settings.ENABLE_XMIND_THUMBNAIL:
            enable_file_type.append(XMIND)
        if settings.ENABLE_PDF_THUMBNAIL:
            enable_file_type.append(PDF)
        self.enable_file_type = enable_file_type

    def params_check(self):
        token = None
        repo_id = None
        match = re.match('^thumbnail/(?P<repo_id>[-0-9a-f]{36})/create/$', self.request.url)
        query_dict = self.request.query_dict
        path = query_dict['path'][0]
        size = query_dict['size'][0]
        repo_id = match.group('repo_id')

        if not size:
            size = settings.THUMBNAIL_DEFAULT_SIZE
        if not path:
            err_msg = "Invalid arguments."
            raise AssertionError(400, err_msg)
        ...... # 其他URL对应的检查
        self.params = {
            'size': size,
            'token': token,
            'file_path': path,
        }

    def session_check(self):
        try:
            session_key = self.request.cookies[settings.SESSION_KEY]
        except:
            session_key = ''
        self.session_key = session_key
        self.permission_check()

    def permission_check(self):
        permission = jwt_permission_check(self.session_key, self.params['repo_id'], self.params['file_path'])
        if not permission:
            err_msg = "Permission denied."
            raise AssertionError(400, err_msg)

Params检查
主要针对前端传来的参数是否合规进行检查,并将其参数加入 self,params字典中,为之后的操作做准备
Resource检查
该项目中需要对指定文件夹是否存在等资源进行检查,主要保存其对应的缩略图路径及last-modified和etag,将这些参数都整合到一起,方便为之后的操作做准备
Session检查
对其权限进行检查,该项目主要从Cookies中获取session_key,将session_key返回给另一个服务器做jwt验证,判断是否有权,jwt验证代码如下

def jwt_permission_check(session_key, repo_id, path):
    jwt_url = get_jwt_url(repo_id)
    payload = {
        'is_internal': True
    }
    jwt_token = jwt.encode(payload, JWT_PRIVATE_KEY, algorithm='HS256')
    headers = {
        'Authorization': f'token {jwt_token}',
        'Cookie': "sessionid=%s" % session_key
    }
    try:
        response = requests.post(jwt_url, data={'path': path}, headers=headers)
        if response.status_code != 200:
            error_msg = 'Internal Server Error'
            logger.error(error_msg)
            return False

        res = json.loads(response.text)
        if res["success"]:
            return True
        else:
            return False
    except Exception as e:
        logger.error(e)
        return False

请求对应的路径,将其jwt_token进行加密,发送到另一服务端,另一服务端解密该token进行比对,并且判断其权限,并将需要的参数传回到该服务器中,为之后的操作做其准备

这里有一点需要注意:因jwt请求的服务器是Django框架,所以将其sessionid发送到Django所建立的服务器时,其会通过中间件处理session信息,在Django服务端将能够直接根据sessionid获取其主要的信息

总结
做完以上认证的同时,在后面数据处理过程中的参数也已经被保存,为后面的数据处理做准备

4. App设计(管理请求路径)

在这个模块中,我们将规整其大体的流程

class App:
    async def __call__(self, scope, receive, send):
        # request
        request = HTTPRequest(**scope)
        if request.method != 'GET':
            response_stat, response_body = gen_error_response(
                405, 'Method %s not allowed' % request.method
            )
            await send(response_stat)
            await send(response_body)
            return

        # serialize check
        try:
            serializer = ThumbnailSerializer(request)
            thumbnail_info = serializer.thumbnail_info
        except Exception as e:
            logger.warning(e)
            thumbnail_info = None

        # ========= router=======
        # ------ping
        if request.url in ('ping', 'ping/'):
            response_stat, response_body = gen_text_response('pong')
            await send(response_stat)
            await send(response_body)
            return
        # ------thumbnail
        elif re.match('^thumbnail/(?P<token>[a-f0-9]+)/create/$', request.url):
            # cache
            try:
                if cache_check(request, thumbnail_info):
                    response_start, response_body = gen_cache_response()
                    await send(response_start)
                    await send(response_body)
                    return
            except Exception as e:
                logger.exception(e)
            response_start, response_body = await share_link_thumbnail_create(request, thumbnail_info)
            await send(response_start)
            await send(response_body)
            return
        elif re.match('^thumbnail/(?P<token>[a-f0-9]+)/(?P<size>[0-9]+)/(?P<path>.*)$', request.url):
            # cache
            try:
                if cache_check(request, thumbnail_info):
                    response_start, response_body = gen_cache_response()
                    await send(response_start)
                    await send(response_body)
                    return
            except Exception as e:
                logger.exception(e)
            response_start, response_body = await share_link_thumbnail_get(request, thumbnail_info)
            await send(response_start)
            await send(response_body)
            return
        else:
            response_stat, response_body = gen_error_response(
                404, 'Not Found'
            )
            await send(response_stat)
            await send(response_body)
            return


app = App()

代码流程详解:

  1. 前端发起请求,服务端创建HTTPRequest对象
  2. 判断其请求方法是否属于GET请求,不是则返回错误
  3. 对该请求做序列化处理,返回序列化对象(ThumbnailSeralizer)
  4. 在序列化中上面已经讲过,对其参数、session、资源进行检查并准备后面数据处理所需的参数
  5. 用正则表达式判断URL的路径,不同的路径走不同的逻辑
  6. 判断其cache,若比对成功,则直接返回304,避免资源的浪费
  7. 若无cache,则走对应的函数返回其对应的响应头和响应体返回给前端

async、await:
这里是异步协程部分,大白话说就是 await 要等待其函数内部的执行完成才能执行以下的函数,否则不等待其 thumbnail 生成就返回给前端将会出现错误,若需要详解的话可以在评论区打出,小白根据情况专门出一篇对应的async和await的见解

以上便是这次开发的整个过程,有啥见解可以评论区交流
声明:我是小白,大神勿喷,可以交流

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值