大家一起玩libuv系列中间加入protobuf的基础教程是为了给我即将写的TcpServer做一个基本工作。TcpServer通信协议我打算使用protobuf来做,所以先讲讲protobuf怎样实现进程间通信的service吧。

1.安装

偷懒了,在mac上使用brew安装protobuf,版本是2.6.1.

brew install protobuf

根据官网的C++教程一步一步地使用protobuf。

2.编写proto文件

package communication;

option cc_generic_services = true;

message Void {
}

message Message {
    required bytes content = 1;
}

service CommunicationService {
    rpc send_message(Message) returns(Void);
}

3.生成C++文件

protoc --cpp_out=./ message_communication.proto

在本地生成了message_communication.ph.h和message_communication.pb.cc两个源文件。

4.使用

我们现在实现一个简单的应用。

Stub发送端尝试去发送一个简单的字符串,接收端接收这个数据。其实发送端和接收端可以位于两个进程上,甚至两个不同的物理机上都可以,但这次用例展示主要适用于展示protobuf的使用,因此我们使用一个公共的内存用于两端的数据通信。所以上图中虚线才是真正的数据通路。

#include <iostream>
#include <sstream>
#include <string>
#include "proto/message_communication.pb.h"

struct StoreStruct {
    int index;
    std::string content;
};

static StoreStruct global_memory;

class ServiceChannel : public google::protobuf::RpcChannel {
public:
    void CallMethod(const google::protobuf::MethodDescriptor *method, google::protobuf::RpcController *controller, const google::protobuf::Message *request, google::protobuf::Message *response, google::protobuf::Closure *done) {
        global_memory.index = method->index();
        std::ostringstream os;
        if(request->SerializeToOstream(&os)) {
            global_memory.content = os.str();
        }
    }
};

class MyCommunicationService : public communication::CommunicationService {
public:
    void send_message(::google::protobuf::RpcController* controller,
                                            const ::communication::Message* msg,
                                            ::communication::Void* rep,
                                            ::google::protobuf::Closure* done) {
        std::cout << "receive data is " << msg->content() << std::endl;
    }

    void receive_data() {
        const google::protobuf::MethodDescriptor *method = descriptor()->method(global_memory.index);
        google::protobuf::Message* msg = GetRequestPrototype(method).New();
        std::istringstream is(global_memory.content);
        msg->ParseFromIstream(&is);
        CallMethod(method, nullptr, msg, nullptr, nullptr);
    }
};

int main()
{
    communication::Message msg;
    msg.set_content("I will send a message");

    ServiceChannel service_channel;
    communication::CommunicationService_Stub stub(&service_channel);
    stub.send_message(nullptr, &msg, nullptr, nullptr);

    MyCommunicationService receiver;
    receiver.receive_data();
    return 0;
}

首先创建一个Message,并且将Message的content变量赋值。之后利用Stub将数据传输出去。MyCommunicationService接收数据,并调用CallMethod方法,CallMethod会根据method的index调用对应的函数,这里就是send_message了。

5.后记

过大年我写博客也是蛮拼的了。

这次我们利用uv_tcp_t来实现一个简单的echo server

1. libuv为TCP通信提供了什么

libuv中使用TCP通信和基本的socket编程相差无几:

  1. 使用uv_tcp_init初始化一个uv_tcp_t
  2. 使用uv_tcp_bind绑定ip和port
  3. 使用uv_listen监听客户端连接
  4. 使用uv_accept获取成功的连接

对libuv来说,TCP也是流数据的一种,所以读入和写出使用uv_writeuv_read_start两个接口。

2. 具体实现

具体的实现并不复杂:

  1. 创建第一个负责监听客户端连接的uv_tcp_t(server),使用uv_listen注册回调函数
  2. 在connect_cb的回调函数中使用uv_accept获取一个已经完成的连接,并赋值给一个新的uv_tcp_t(client)
  3. 关于向客户端写入:创建一个Request(uv_write_t)和一个Buffer(uv_buf_t),Buffer里面存储着要传送的信息,Request可以理解为一个向Stream的写入请求
  4. 关于读客户端信息:uv_read_start接口用于向libuv申请需要监听哪个uv_tcp_t的读信息,并在回调中处理读信息,回调函数nread参数说明信息长度,buf参数有信息内容

代码如下:

#include <stdio.h>
#include <uv.h>

#define DEFAULT_PORT 7000
#define DEFAULT_BACKLOG 5
#define PS ">>> "

struct sockaddr_in addr;

void alloc_buffer(uv_handle_t* handle,
                    size_t suggested_size,
                    uv_buf_t* buf) {
    buf->base = (char*)malloc(suggested_size);
    buf->len = suggested_size;
}

void write_to_remote(uv_stream_t* client, char* content, int size);

void read_cb(uv_stream_t* client,
             ssize_t nread,
             const uv_buf_t* buf) {
    if(nread < 0) {
        if(nread != UV_EOF)
            fprintf(stderr, "Read error %s\n", uv_strerror(nread));
        uv_close(client, NULL);
    } else if(nread > 0){
        write_to_remote(client, buf->base, nread);
    }
}

void write_cb(uv_write_t* req, int status) {
    if(status) {
        fprintf(stderr, "write cb error %s\n", uv_strerror(status));
    }
    free(req);
}

void write_to_remote(uv_stream_t* client, char* content, int size) {
    if(size < 0)
        size = strlen(content);
    uv_write_t* w_req = (uv_write_t*)malloc(sizeof(uv_write_t));
    char * reply = (char*)malloc(size + 5);
    strcat(reply, content);
    strcat(reply, PS);
    uv_buf_t buf = uv_buf_init(reply, strlen(reply));
    uv_write(w_req, client, &buf, 1, write_cb);
}

void connect_cb(uv_stream_t* server, int status) {
    if(status < 0) {
        fprintf(stderr, "new connect error %s", uv_strerror(status));
    } else {
        uv_tcp_t *client = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
        uv_tcp_init(uv_default_loop(), client);
        if(uv_accept(server, client) == 0) {
            write_to_remote(client, "Hi, I will repeat you.\n", -1);
            uv_read_start(client, alloc_buffer, read_cb);

        } else {
            fprintf(stderr, "accept error.\n");
        }
    }
}

int main()
{
    uv_tcp_t server;
    uv_tcp_init(uv_default_loop(), &server);

    uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);

    uv_tcp_bind(&server, (const struct sockaddr_in*)&addr, 0);

    int r = uv_listen(&server, DEFAULT_BACKLOG, connect_cb);
    if(r) {
        fprintf(stderr, "uv listen error.\n");
        return 1;
    }

    return uv_run(uv_default_loop(), UV_RUN_DEFAULT);
}

3. 验证

使用telnet连接Server,连接成功后,将会首先收到客户端的一条回复,之后你的每一次输入Server都会返回给客户端。

whosemario:~ whosemario$ telnet 127.0.0.1 7000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hi, I will repeat you.
>>> Hi, I am at home
Hi, I am at home
>>> I am Happy.
I am Happy.
>>> OK
OK
>>> Play
Play
>>>

Handle和Request是libuv最基本的数据结构,或者说对象。libuv的各种功能都是通过它们来实现。本文利用libuv的Timer Handle实现了一个周期性gc的实例程序;利用filesystem Request实现了一个异步读取文件的实例。

1.Handle 与 Request

Handles represent long-lived objects capable of performing certain operations while active. Some examples: a prepare handle gets its callback called once every loop iteration when active, and a TCP server handle get its connection callback called every time there is a new connection.
Requests represent (typically) short-lived operations. These operations can be performed over a handle: write requests are used to write data on a handle; or standalone: getaddrinfo requests don’t need a handle they run directly on the loop.

上面是我摘录了官网上对Handle和Request的解释,目前来说,本人对Handle和Request的理解是:Handle是一种长期的对象,我们可以在Handle上挂一个Timer,周期性的调用某个Callback;Request是一个短期的对象,比如从文件里面读一段数据。
下面就是libuv中Handle和Request的声明。

/* Handle types. */
typedef struct uv_loop_s uv_loop_t;
typedef struct uv_handle_s uv_handle_t;
typedef struct uv_stream_s uv_stream_t;
typedef struct uv_tcp_s uv_tcp_t;
typedef struct uv_udp_s uv_udp_t;
typedef struct uv_pipe_s uv_pipe_t;
typedef struct uv_tty_s uv_tty_t;
typedef struct uv_poll_s uv_poll_t;
typedef struct uv_timer_s uv_timer_t;
typedef struct uv_prepare_s uv_prepare_t;
typedef struct uv_check_s uv_check_t;
typedef struct uv_idle_s uv_idle_t;
typedef struct uv_async_s uv_async_t;
typedef struct uv_process_s uv_process_t;
typedef struct uv_fs_event_s uv_fs_event_t;
typedef struct uv_fs_poll_s uv_fs_poll_t;
typedef struct uv_signal_s uv_signal_t;

/* Request types. */
typedef struct uv_req_s uv_req_t;
typedef struct uv_getaddrinfo_s uv_getaddrinfo_t;
typedef struct uv_getnameinfo_s uv_getnameinfo_t;
typedef struct uv_shutdown_s uv_shutdown_t;
typedef struct uv_write_s uv_write_t;
typedef struct uv_connect_s uv_connect_t;
typedef struct uv_udp_send_s uv_udp_send_t;
typedef struct uv_fs_s uv_fs_t;
typedef struct uv_work_s uv_work_t;

/* None of the above. */
typedef struct uv_cpu_info_s uv_cpu_info_t;
typedef struct uv_interface_address_s uv_interface_address_t;
typedef struct uv_dirent_s uv_dirent_t;

2.timer

每一个异步IO库都少不了Timer,下面是利用Timer实现一个gc的周期性处理程序,但我们希望当loop中没有其他的handle的时候,loop会结束。因此我们要使用uv_unref接口,将Timer的Handle的引用在loop之前先删除。

#include <stdio.h>
#include <stdlib.h>
#include <uv.h>

uv_loop_t* loop;
uv_timer_t gc_timer;
uv_timer_t fake_job_timer;

void gc_cb(uv_timer_t* handle) {
    printf("GC unused objects.\n");
}

void fake_job_cb(uv_timer_t* handle) {
    printf("Fake job done.\n");
}

int main()
{
    loop = uv_default_loop();

    uv_timer_init(loop, &gc_timer);
    uv_unref((uv_handle_t*)&gc_timer);

    uv_timer_start(&gc_timer, gc_cb, 0, 2000);

    uv_timer_init(loop, &fake_job_timer);
    uv_timer_start(&fake_job_timer, fake_job_cb, 9000, 0);

    return uv_run(loop, UV_RUN_DEFAULT);
}

输出如下:

output:
GC unused objects.
GC unused objects.
GC unused objects.
GC unused objects.
GC unused objects.    
Fake job done.
Program ended with exit code: 0

3. filesystem

Linux系统提供的文件数据读是阻塞模式的,libuv为了将所有的IO操作都搞成异步的,在文件操作上加入了线程池。对文件操作是一种Request,向loop请求一种Request(open, read, close),libuv通过callback返回最终结果。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <uv.h>

uv_fs_t open_fs_r;
uv_fs_t read_fs_r;

static char buffer[1024];
static uv_buf_t iov;
static read_off = 0;

void read_cb(uv_fs_t* request) {
    if(request->result < 0) {
        fprintf(stderr, "error read file; %s\n", uv_strerror(request->result));
    } else if(request->result == 0) {
        uv_fs_t close_fs_r;
        uv_fs_close(uv_default_loop(), &close_fs_r, open_fs_r.result, NULL);
    } else {
        iov.base[request->result] = 0;
        fprintf(stdout, "%s", iov.base);
        read_off += request->result;
        uv_fs_read(uv_default_loop(), &read_fs_r, open_fs_r.result, &iov, 1, read_off, read_cb);
    }
}

void open_cb(uv_fs_t* request) {
    if(request->result > 0) {   
        iov = uv_buf_init(buffer, sizeof(buffer) - 1);
        uv_fs_read(uv_default_loop(), &read_fs_r, request->result, &iov, 1, read_off, read_cb);
    } else {
        fprintf(stderr, "error open file: %s\n", uv_strerror(request->result));
    }
}

int main(int argc, char **argv)
{
    uv_fs_open(uv_default_loop(), &open_fs_r, argv[1], O_RDONLY, S_IRWXU, open_cb);
    uv_run(uv_default_loop(), UV_RUN_DEFAULT);
    uv_fs_req_cleanup(&open_fs_r);
    uv_fs_req_cleanup(&read_fs_r);
}

4.后记

libuv将所有的IO在上层抽象为异步IO,但在内部实现做了区分——网络IO是走kqueue和epoll,文件IO是利用线程池。

libuv是一个轻量级的异步IO库,学习它这件事情已经打算好久了,那么从今天开始,我尝试使用libuv写一些教程性质的代码

1.下载与编译

github上clone下源码,编译lib库。

> cd libuv/
> ./autogen.sh
> ./configure
> make

编译之后会在.libs文件夹存在libuv.a的库文件,当前根目录下有include头文件夹。

2.Hello World

#include <stdio.h>
#include <stdlib.h>
#include <uv.h>

int main() {
    uv_loop_t *loop = malloc(sizeof(uv_loop_t));
    uv_loop_init(loop);

    printf("Now quitting.\n");
    uv_run(loop, UV_RUN_DEFAULT);

    uv_loop_close(loop);
    free(loop);
    return 0;
}

loop需要手动的分配空间,并在结束的时候释放空间。
uv_run用于运行主循环,但在主循环里面其实什么都没做就又跳出主循环了,简单讲一下为什么。

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
      int timeout;
      int r;
      int ran_pending;

      r = uv__loop_alive(loop);
      if (!r)
        uv__update_time(loop);

      while (r != 0 && loop->stop_flag == 0) {
          ....
      }
      ....
      return r;
  }

此时loop里面都是空的,在uv__loop_alive函数中有如下判断:

static int uv__loop_alive(const uv_loop_t* loop) {
      return uv__has_active_handles(loop) ||
             uv__has_active_reqs(loop) ||
             loop->closing_handles != NULL;
}

此时上面三个判断条件都不满足,因此r为0,不会进入while循环,直接跳出uv_run函数。

3.Reference

  1. An Introduction to libuv - http://nikhilm.github.io/uvbook/

pytracemalloc用于跟踪Python内存的分配情况,在我们的项目中用于在后期优化性能使用。

1.安装

pytracemalloc的安装需要修改Python的源码,我们创建一个名为pytracemalloc的目录,进入目录,运行如下命令:

> wget http://www.python.org/ftp/python/2.7.8/Python-2.7.8.tgz
> wget https://pypi.python.org/packages/source/p/pytracemalloc/pytracemalloc-1.2.tar.gz
> tar -xf Python-2.7.8.tgz
> tar -xf pytracemalloc-1.2.tar.gz
> cd Python-2.7.8
> patch -p1 < ../pytracemalloc-1.2/patches/2.7/pep445.patch
> ./configure --prefix=../build/
> make && make install
> cd ../pytracemalloc-1.2
> ../bin/python setup.py install

2.简单的实例

显示最占用内存的10行代码

1 # -*- coding:utf-8 -*-
  2
  3 import os
  4 import sys
  5 import tracemalloc
  6
  7 tracemalloc.start()
  8
  9 li = []
10 for i in xrange(0, 100000):
11     li.append(i)
12
13 class A(object):
14     def __init__(self):
15         self.m = {}
16         for i in xrange(0, 10):
17             self.m[i] = i * i
18
19 obj_li = []
20 for i in xrange(0, 100000):
21     obj_li.append(A())
22
23 snapshot = tracemalloc.take_snapshot()
24 top_stats = snapshot.statistics("lineno")
25
26 for line in top_stats[:10]:
27     print line

运行结果如下:

test.py:17: size=73.0 MiB, count=100000, average=768 B
test.py:15: size=53.0 MiB, count=199999, average=280 B
test.py:21: size=7055 KiB, count=100004, average=72 B
test.py:10: size=2356 KiB, count=2432, average=992 B
test.py:11: size=805 KiB, count=1, average=805 KiB
test.py:13: size=1952 B, count=7, average=278 B
test.py:23: size=456 B, count=1, average=456 B
test.py:14: size=120 B, count=1, average=120 B
test.py:16: size=72 B, count=1, average=72 B

比较两个snapshot的不同

1 # -*- coding:utf-8 -*-
  2
  3 import os
  4 import sys
  5 import tracemalloc
  6
  7 tracemalloc.start()
  8
  9 li = []
10 for i in xrange(0, 100000):
11     li.append(i)
12
13 class A(object):
14     def __init__(self):
15         self.m = {}
16         for i in xrange(0, 10):
17             self.m[i] = i * i
18
19 obj_li = []
20 for i in xrange(0, 100000):
21     obj_li.append(A())
22
23 snapshot1 = tracemalloc.take_snapshot()
24
25 for i in xrange(0, 1000):
26     obj_li.append(A())
27
28 snapshot2 = tracemalloc.take_snapshot()
29
30 top_stats = snapshot2.compare_to(snapshot1, "lineno")
31
32 for line in top_stats[:10]:
33     print line

下面是输出结果:

test.py:17: size=73.0 MiB (+750 KiB), count=101000 (+1000), average=768 B
test.py:15: size=53.0 MiB (+546 KiB), count=201999 (+2000), average=280 B
test.py:26: size=62.0 KiB (+62.0 KiB), count=1000 (+1000), average=64 B
/Users/whosemario/MyApplications/pytracemalloc/build/lib/python2.7/site-packages/tracemalloc.py:380: size=1008 B (+1008 B), count=6 (+6), average=168 B
/Users/whosemario/MyApplications/pytracemalloc/build/lib/python2.7/site-packages/tracemalloc.py:518: size=672 B (+672 B), count=4 (+4), average=168 B
/Users/whosemario/MyApplications/pytracemalloc/build/lib/python2.7/site-packages/tracemalloc.py:308: size=280 B (+280 B), count=1 (+1), average=280 B
/Users/whosemario/MyApplications/pytracemalloc/build/lib/python2.7/site-packages/tracemalloc.py:306: size=64 B (+64 B), count=1 (+1), average=64 B
test.py:21: size=7055 KiB (+0 B), count=100004 (+0), average=72 B
test.py:10: size=2356 KiB (+0 B), count=2432 (+0), average=992 B
test.py:11: size=805 KiB (+0 B), count=1 (+0), average=805 KiB

果然python的dict是很耗内存的,这让我想到了原来的项目的优化。

3.Reference

  1. pytracemalloc官网 - http://pytracemalloc.readthedocs.org/index.html#

使用Python来实现单例模式是一件很有意思的事情,在这个过程中我们会重新审视Python在创建类(Build Class)和构建实例时都调用了那些magic method
同时我也想说的是Python的单例模块实现多种多样,但有些实现是存在问题的,在某些情况下是不可用或者影响系统的某些功能的。

1.利用Python的metaclass实现单例

先说我认为实现起来最地道的Python Singleton的实现方法。metaclass在Python中是一个很奇特的东西。Python在类的构建过程中会寻找metaclass,默认是type,如果我们在类中定义的metaclass,Python在类的构建中就会使用这个metaclass。
上面的描述主要体现在下面的代码逻辑中。

# ceval.c
static PyObject *  
build_class(PyObject *methods, PyObject *bases, PyObject *name) {
    PyObject *metaclass = NULL, *result, *base;

    // 0.先尝试从用户自定义的类中查询__metaclass__
    if (PyDict_Check(methods))
          metaclass = PyDict_GetItemString(methods, "__metaclass__");
    if (metaclass != NULL)
          Py_INCREF(metaclass);
    else if (PyTuple_Check(bases) && PyTuple_GET_SIZE(bases) > 0) {
          // 1.从基类获取__class__作为metaclass
          base = PyTuple_GET_ITEM(bases, 0);
          metaclass = PyObject_GetAttrString(base, "__class__");
          if (metaclass == NULL) {
              PyErr_Clear();
              // 2.使用ob_type
              metaclass = (PyObject *)base->ob_type;
              Py_INCREF(metaclass);
          }
     }
     else {
          // 3.如果这个类连基类都没有,使用全局global的__metaclass__
          PyObject *g = PyEval_GetGlobals();
          if (g != NULL && PyDict_Check(g))
              metaclass = PyDict_GetItemString(g, "__metaclass__");
          if (metaclass == NULL)
              // 4.使用PyClass_Type
              metaclass = (PyObject *) &PyClass_Type;
          Py_INCREF(metaclass);
      }
  ......

metaclass创建好之后,我们要使用metaclass创建对应的类,并初始化创建处理的类。
类的创建主要在type_call是实现,我们在这篇文章中大体提到了type_call里面的部分逻辑,主要涉及tp_newtp_init。之后,在初始化类中会调用PyObject_Call方法。PyObject_Call方法会调用metaclass的tp_call方法,并且返回此类对应的实例,因此为了实现单例模式,我们可以在tp_call这个方法上做手脚。

class Singleton(type):
      _instance = {}
      def __call__(cls, *args, **kwargs):
            if cls not in Singleton._instance:
                Singleton._instance[cls] = type.__call__(cls, *args, **kwargs)
            return Singleton._instance[cls]

 class A(object):
       __metaclass__ = Singleton

  a1 = A()
  a2 = A()
  print id(a1) == id(a2)

上面实例a1和实例a2的id是相同。例外call即使上面所只得tp_call。

2.利用__new__实现单例模式

另一种方法是我们在tp_new的时候去检查此类有没有被创建,创建了就直接返回对应的实例。

class Singleton(object):
    _instance = None
    def __new__(cls, *args, **kwargs):
          if Singleton._instance is None:
              Singleton._instance = object.__new__(cls, *args, **kwargs)
          return Singleton._instance

class A(Singleton):
    def __init__(self):
          print "init A"

 a1 = A()
 a2 = A()
 print id(a1) == id(a2)

上面这种方法也可以实现单例,同时也存在着风险。

  1. 如果子类重载了__new__方法,那么父类的__new__方法就不起作用了
  2. 如果运行上面的代码会发现打印出两次”init A”, 因为在这种情况下tp_new会被调用两次,第二次由于_instance不等于None,直接返回。相同tpinit也会被调用两次,每次都会调用A的\_init__方法。如果不小心在__init__中做了一些初始化的逻辑,那么每次构建一个实例都要进行初始化了(是不是感觉和单例模式的初衷相违背了呢)。

3.利用修饰器实现单例模式

_instance = {}
def Singleton(cls):
     def wrapper(*args, **kwargs):
          if cls not in _instance:
              _instance[cls] = cls(*args, **kwargs)
          return _instance[cls]
     return wrapper

 @Singleton
 class A(object):
       def __init__(self):
            print "init"

  a = A() 
  b = A() 
  print id(a)
  print id(b)
  print type(A)

运行上面的代码,一切看起来都合情合理,”init”也只打印了一次。但看最后一行的输出,A的类型为function。好的,显然没有错,我们用修饰器修饰了。但A的类型变化了会产生什么负面作用是很难把握的。随着工程的扩大,A是什么类型开发者可能已经不了解了,因此也会导致很多问题出来,并且很难去定位问题。

4.小结

上面三种方法其实都可以实现单例模式,就个人经验和喜好来说,第一种最好。

状态模式是在我们游戏开发中非常常用的一个设计模式。状态模式的特点就是将状态和行为分离,每一个状态下只执行当前状态的行为,这样就将复杂的行为逻辑分为一个个不同状态下的行为逻辑,代码可维护性大大提高。

1.游戏的应用场景

玩家可以控制角色在场景中移动,攻击,飞行,静止不动或者被敌人攻击。在每一个状态下,玩家的动画是不一样的,并且在飞行状态玩家不能进行攻击,在受击状态下,玩家不能移动。
这就是典型的状态模式的需求——在不同的状态下有不同的行为模式。

上图是各个状态之间的转化,有了状态转化图,我们就可以着手开始写状态机的代码了,但在这之前我们先复习一下状态模式的基础内容。

2.状态模式

  • StateM:用于维护和调度所有State
  • State:状态的基类
  • Idle/Run:这里画出了Idle和Run两个状态

这里我们只实现一个最基本的状态机,状态的转换会导致上一个状态结束(即调用leave),状态的进入会调用enter方法。pause方法被调用的情况是在当前状态进行切换,但状态并没有结束,只是产生了一个子状态,当子状态结束后,当前状态又会恢复(resume)。

class StateM(object):

    def __init__(self):
        self._states = {}
        self._curr_state = None

    def add_state(self, name, state):
        self._states[name] = state
        state.statem = self

    def get_curr_state(self):
        return self._curr_state

    def call_method(self, method, *args, **kwargs):
        curr_state = self.get_curr_state()
        if curr_state and hasattr(curr_state, methed):
            getattr(curr_state, method)(*args, **kwargs)

    def change_to(self, name, *args, **kwargs):
        if self._curr_state:
            self._curr_state.leave()
        self._curr_state = self._states[name]
        self._curr_state.enter(*args, **kwargs)

StateM类change_to接口用于切换状态,call_method用于调用当前状态的方法。

class State(object):
    def enter(self, *args, **kwargs):
        pass

    def leave(self):
        pass

    def pause(self):
        pass

    def resume(self):
        pass


class Idle(State):
    def enter(self):
        super(Idle, self).enter()
        # 播放待机动画

    def play_fly(self):
        self.statem.change_to("fly")

    ...

...

# main
self.statem = StateM()
self.statem.add_state("idle", Idle())
self.statem.add_state("fly",  Fly())
self.statem.add_state("atk",  Attack())
...

self.statem.change_to("idle")

手游的新手教学是再常见不过了,我们的手游新手教学也进行了三次比较大得迭代,每次迭代不仅包括策划案的迭代,也包括游戏逻辑代码的设计迭代。新手的实现中最主要使用了观察者模式,本文先大体讲一下新手模块的设计方案,之后终点介绍观察者模式在游戏中的使用。

1.新手教学的设计实现

我们希望将新手模块设计成一个尽量独立的模块,不去影响其他模块的开发。举个简单的例子,在装备界面我们会设计一个新手教学,但我们不希望由于新手教学的加入导致装备界面的逻辑还要进行过多的修改。基于这样的需求或者说最基本的初衷,我们可以将新手模块设计如下:

每一个新手步骤可以理解为用户随着新手指引的每一次点击操作(或者滑动,拖拽操作),从新手步骤1到新手步骤2的条件就是event_1这个事件被触发了,同时我们可以发现event_1事件的触发是通过用户操作装备界面的”操作”进行触发的。这样整个流程就很明白了,UI层接受用户的操作,根据不同的操作下放不同的事件,UI层是不需要关心是谁去接收或者说处理这些事件的,事件可以有监听者也可以没有,在这里,新手模块就是某些事件的监听者,它监听到某些事件后,根据策划的需求转换自己的当前状态,最明显的就是转换到下一个新手阶段。这种设计就是非常常见的观察者模式
游戏逻辑的各个角落都在使用这种模式,但接下来我们先回顾一下观察者模式到底长什么样。

2.观察者模式

上面是最基本的观察者模式的UML图

  • Dispatcher: 分发消息的对象,也成为目标对象。一个目标对象可以有多个观察者,目标可以添加或者删除观察者。
  • Listener:观察者,具有do_event方法,接收Dispatcher的调用。

但在我们的游戏中,并没有完全参照观察者模式的架构进行实现,我们的实现很简单:一个Dispatcher类,维护者所有的listener,并且listener其实只是PyFunction。

class Dispatcher(object):

    def __init__(self):
        super(Dispatcher, self).__init__()
        // event -> listener_list
        self.listeners = {}

    def add_listener(self, event, listener):
        if not self.listeners.get(event, None):
            self.listeners[event] = []
        self.listeners[event].append(listener)

    def remove_listener(self, event, listener):
        if listener in self.listeners.get(event, []):
            self.listeners[event].remove(listener)

    def dispatch_event(self, event, data):
        if event in self.listeners:
            for listener in self.listeners[event]:
                listener(data)

3.观察者模式在游戏中的使用

成就系统又是一个典型使用观察者模式的模块,比如获得十件金装,每一次获得一件金装,变会dispatch一个获得金装的事件,成就系统获取事件并记录,当达到十件金装的时候通知玩家达成此成就。
观察者模式的使用,降低了模块直接的耦合度,我们删除或者关闭一个模块的功能,其他模块并不需要关心,因为他们直接的”交流”都是通过事件传输的,而不是直接的方法调用。

4.Reference

  1. Game Programming Pattern —— Observer

这一篇接着上一个篇继续讨论Python Import的源码。

1.关于import_submodule

import_submodule函数做的事情就是将指定的module(或者package)加载进来。

static PyObject *
import_submodule(PyObject *mod, char *subname, char *fullname)
{
    // [1] 输入参数的简单解释
    // mod : 两种情况,一种是package,一种是None
    // subname : 将要import的module名称
    // fullname : import的全路径

    if ((m = PyDict_GetItemString(modules, fullname)) != NULL) {
        // [2] 查看在不在sys.modules里面
        Py_INCREF(m);
    }
    else {
        if (mod == Py_None)
            // path为NULL意味着import的搜索路径从sys.path的默认路径开始
            path = NULL;
        else {
            // 这里的mod一定是package
            path = PyObject_GetAttrString(mod, "__path__");
        }
        // [3] 找出对应的module,同时会返回文件描述符(fp),fdp会描述相关信息
        fdp = find_module(fullname, subname, path, buf, MAXPATHLEN+1,
                      &fp, &loader);
        // [4] import 对应的module
        m = load_module(fullname, fp, buf, fdp->type, loader);

    }
    return m;
}

2.find_module函数

static struct filedescr *
find_module(char *fullname, char *subname, PyObject *path, char *buf,
            size_t buflen, FILE **p_fp, PyObject **p_loader)
{

    /* sys.meta_path import hook */
    if (p_loader != NULL) {
        PyObject *meta_path;

        meta_path = PySys_GetObject("meta_path");  // sys.meta_path
        /* meta_path里面是用户hook的一些类,默认是空
           这些类实现了find_module方法,并返回对应的
           loader
         */
        npath = PyList_Size(meta_path);
        for (i = 0; i < npath; i++) {
            PyObject *loader;
            PyObject *hook = PyList_GetItem(meta_path, i);
            loader = PyObject_CallMethod(hook, "find_module",
                                         "sO", fullname,
                                         path != NULL ?
                                         path : Py_None);
            if (loader == NULL) {
                Py_DECREF(meta_path);
                goto error_exit;  /* true error */
            }
            if (loader != Py_None) {
                /* a loader was found */
                *p_loader = loader;
                Py_DECREF(meta_path);
                PyMem_FREE(name);
                return &importhookdescr;
            }
            Py_DECREF(loader);
        }
        Py_DECREF(meta_path);
    }

    if (path == NULL) {
        // path为NULL,从默认路径开始查找
        if (is_builtin(name)) {
            // 检查是不是内建模块
            strcpy(buf, name);
            PyMem_FREE(name);
            return &fd_builtin;
        }
        if ((find_frozen(name)) != NULL) {
            strcpy(buf, name);
            PyMem_FREE(name);
            return &fd_frozen;
        }

        // path = sys.path
        path = PySys_GetObject("path");
    }

    npath = PyList_Size(path);
    namelen = strlen(name);
    // 遍历所有的path
    for (i = 0; i < npath; i++) {
        PyObject *copy = NULL;
        PyObject *v = PyList_GetItem(path, i);
        strcpy(buf, PyString_AS_STRING(v));
          ....
        strcpy(buf+len, name);
        len += namelen;
        // 做完上面这一步后,此时buf存储是import的全路径

        /* Check for package import (buf holds a directory name,
           and there's an __init__ module in that directory */
        if (isdir(buf) &&         /* it's an existing directory */ // 是不是存在的目录
            case_ok(buf, len, namelen, name)) { /* case matches */
            if (find_init_module(buf)) { /* and has __init__.py */
                Py_XDECREF(copy);
                PyMem_FREE(name);
                return &fd_package;
            }
            else {
                char warnstr[MAXPATHLEN+80];
                sprintf(warnstr, "Not importing directory "
                    "'%.*s': missing __init__.py",
                    MAXPATHLEN, buf);
                if (PyErr_Warn(PyExc_ImportWarning,
                               warnstr)) {
                    Py_XDECREF(copy);
                    goto error_exit;
                }
            }
        }
        for (fdp = _PyImport_Filetab; fdp->suffix != NULL; fdp++) {
            ...
            fp = fopen(buf, filemode);
            if (fp != NULL) {
                if (case_ok(buf, len, namelen, name)) {
                    //printf("in find_module case_ok buf is %s\n", buf);
                    break;
                }
                else {                   /* continue search */
                    fclose(fp);
                    fp = NULL;
                }
            }
        }
    *p_fp = fp;
    PyMem_FREE(name);
    return fdp;
}

find_module的基本流程可以描述如下:

  1. 尝试在sys.meta_path里面找到hook class,并尝试获得loader。如果成功,函数返回
  2. 如果path是空:
    先看要import得module是不是builtin module,是,返回
    再看module是不是frozen module,是,返回
    然后将path = sys.path,从默认路径开始查找
  3. 开始遍历path了
    创建full为import的全路径
    分两种情况
    1) full是一个目录 package
    2) full是一个module
    

3.load_module函数

load_module函数并不复杂,其根据不同的类型选择不同的加载module的方式。

static PyObject *
load_module(char *name, FILE *fp, char *pathname, int type, PyObject *loader)
{
    switch (type) {
    // .py文件
    case PY_SOURCE:
        m = load_source_module(name, pathname, fp);
        break;
    // .pyc文件
    case PY_COMPILED:
        m = load_compiled_module(name, pathname, fp);
        break;
    // .so or .dll or .pyd 文件
    case C_EXTENSION:
        m = _PyImport_LoadDynamicModule(name, pathname, fp);
        break;
    // package
    case PKG_DIRECTORY:
        m = load_package(name, pathname);
        break;
        ....
    }

    return m;
} 

理解Python Import机制可以对Python的Package结构进行更深刻的理解。

1.__import__函数

__import__函数是import关键字的内部实现。

static PyObject *
builtin___import__(PyObject *self, PyObject *args, PyObject *kwds) {
    static char *kwlist[] = {"name", "globals", "locals", "fromlist",
                         "level", 0};
    char *name;
    PyObject *globals = NULL;
    PyObject *locals = NULL;
    PyObject *fromlist = NULL;
    int level = -1;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|OOOi:__import__",
                kwlist, &name, &globals, &locals, &fromlist, &level))
        return NULL;

    return PyImport_ImportModuleLevel(name, globals, locals,
                                  fromlist, level);
}

builtin___import__函数内部很简单,主要是提取kwlist列出的几个参数,然后就进入了PyImport_ImportModuleLevel方法内。PyImport_ImportModuleLevel函数只是对即将进行的操作进行加锁,然后调用方法import_module_level

static PyObject *
import_module_level(char *name, PyObject *globals, PyObject *locals,
                    PyObject *fromlist, int level) {

    // [1] 获取import发生时的package
    parent = get_parent(globals, buf, &buflen, level);

    // [2] 解析module的路径结构,依次加载每一个package/module
    head = load_next(parent, level < 0 ? Py_None : parent, &name, buf,
                    &buflen);
    if (head == NULL)
        goto error_exit;

    tail = head;
    Py_INCREF(tail);
    while (name) {
        next = load_next(tail, tail, &name, buf, &buflen);
        Py_DECREF(tail);
        if (next == NULL) {
            Py_DECREF(head);
            goto error_exit;
        }
        tail = next;
    }

    // import xx.xx.xx 形式
    if (fromlist == NULL) {
        Py_DECREF(tail);
        PyMem_FREE(buf);
        return head;
    }

    Py_DECREF(head);
    // from xx.xx import xx 形式
    if (!ensure_fromlist(tail, fromlist, buf, buflen, 0)) {
        Py_DECREF(tail);
        goto error_exit;
    }

    PyMem_FREE(buf);
    return tail;
}

get_parent方法用于获取当前import动作所在的package。load_next可以认为是在导入name中声明的每一个模块,每次调用load_next都会返回本次导入的模块的PyObject,并且修改name的信息,将上次导入的模块的名称删除。

2.get_parent函数

static PyObject *
get_parent(PyObject *globals, char *buf, Py_ssize_t *p_buflen, int level)
{

    ......
    // 获取__package__信息,就是当前import的环境下,其所在的package叫什么名字
    pkgname = PyDict_GetItem(globals, pkgstr);

    if ((pkgname != NULL) && (pkgname != Py_None)) {
        ......
        // 如果package是存在的,那么我们直接使用它
        // 将package的内容直接设置到buf里面
        strcpy(buf, PyString_AS_STRING(pkgname));
    } else {
        // __package__并没有设置,我们通过其他方法找出来
        // 先获得我们import的环境的__name__变量
        modname = PyDict_GetItem(globals, namestr);
        if (modname == NULL || !PyString_Check(modname))
            return Py_None;

        // 再获得__path__变量,需要知道的一点是在__init__.py
        // 内部__path__变量是存在的,但其他module内部__path__
        // 变量是为None的
        modpath = PyDict_GetItem(globals, pathstr);
        if (modpath != NULL) {
            ......
            // __path__已经被设置了,那么__name__的值就是package的名称
            // 将package的内容直接设置到buf里面
            strcpy(buf, PyString_AS_STRING(modname));
            ......
        } else {
            // __package__名称为空,那么import的动作是在某个module内部的
            char *start = PyString_AS_STRING(modname);
            // lastdot就是当前module所在的package的名称
            char *lastdot = strrchr(start, '.');
            size_t len;
            int error;
            if (lastdot == NULL && level > 0) {
                PyErr_SetString(PyExc_ValueError,
                    "Attempted relative import in non-package");
                return NULL;
            }
            if (lastdot == NULL) {
                // 我们import的环境是最上层的环境
                // 比如说得命令行环境
                // 程序最初的运行文件等
                error = PyDict_SetItem(globals, pkgstr, Py_None);
                if (error) {
                    PyErr_SetString(PyExc_ValueError,
                        "Could not set __package__");
                    return NULL;
                }
                return Py_None;
            }
            len = lastdot - start;
            // 将__package__的信息设置到buf里面去
            strncpy(buf, start, len);
            ......
        }
    }
    // level > 0 的情况很多时候都是
    // from ... import a.b
    // 上面的level=3
    while (--level > 0) {
        char *dot = strrchr(buf, '.');
        if (dot == NULL) {
            PyErr_SetString(PyExc_ValueError,
                "Attempted relative import beyond "
                "toplevel package");
            return NULL;
        }
        *dot = '\0';
    }
    *p_buflen = strlen(buf);

    modules = PyImport_GetModuleDict();
    // 从sys.modules中获取对应的package
    parent = PyDict_GetItemString(modules, buf);
    return parent;
}    

3.load_next函数

static PyObject *
load_next(PyObject *mod, PyObject *altmod, char **p_name, char *buf,
          Py_ssize_t *p_buflen)
{
    ... ...
    // p_name指向被截取后的路径名称
    if (dot == NULL) {
        *p_name = NULL;
        len = strlen(name);
    }
    else {
        *p_name = dot+1;
        len = dot-name;
    }
    ... ...
    // 修改buf的内容,将截取的模块内容拷贝到buf后
    p = buf + *p_buflen;
    if (p != buf)
        *p++ = '.';
    if (p+len-buf >= MAXPATHLEN) {
        PyErr_SetString(PyExc_ValueError,
                        "Module name too long");
        return NULL;
    }
    strncpy(p, name, len);
    p[len] = '\0';
    *p_buflen = p+len-buf;

    // import模块
    result = import_submodule(mod, p, buf);
    if (result == Py_None && altmod != mod) {
        //如果没有import成功,那么我们从默认路径开始import
        Py_DECREF(result);
        /* Here, altmod must be None and mod must not be None */
        result = import_submodule(altmod, p, p);
        if (result != NULL && result != Py_None) {
            if (mark_miss(buf) != 0) {
                Py_DECREF(result);
                return NULL;
            }
            strncpy(buf, name, len);
            buf[len] = '\0';
            *p_buflen = len;
        }
    }
    ... ...

    return result;
}

4.领悟

  1. import是不能import进module内部的类的,比如说module A中实现了类A,那么import A_p.A.A会有问题,需要改为from A_p.A import A
  2. 对应from A_p.A import a形式,我们可以看成__import__(name = “A_p.A”, from_list = [“a”]),函数返回的是module A