帐前卒专栏

code, software architect, articles and novels.
代码,软件架构,博客和小说

今天终于申请成功了google adsense.以前申请不成功是:
  • 是二级域名
  • <li>内容太少</li>
    
    <li>没有加上adsense.txt.</li>
    

    今天终于申请成功了。不过今天为了调整这个东东的格式(见右侧)。也是花费了很长功夫。而且貌似chrome是不支持看到自己的广告的。不让我看到,你说我咋调整?于是只得换成firefox.看了看效果还是可以的。而且推荐的广告还是很符合内容的。不过我自己就能点了。另外google adsense的要求蛮多的。开始放入网页中的时候我下载了word press的插件awesome google adsense.结果后来还是自己手工改php好看些。再也不相信那无聊的plugin了…能自己写还是自己动手吧。害我现在的网页加载速度越来越慢。有空改改…没空…咳咳…就这样吧。

    这个东东制作RPG还是不错的。不过我用不了那么多功能。此外层数太少了,还要学习Ruby。其实Ruby和python差不多。那帮家伙为啥不用python呢? 算了。学点语言也没有坏处。然后我就做起了RPG. 昨天终于做好了。至少可以在我的电脑上运行了。 结果放在宝的电脑上就杯具了。 提示应该是缺少RGSS102J.dll的错误,其实RGSS103J.dll的错误是一样的。就是安装过RPG maker的电脑上安装了RGSS102J.dll,而没有装过的缺少呗。其实根本不用到网上下载那些dll。只需要在系统盘的windows文件夹寻找就可以找到。之后把这个dll拷贝到游戏目录中即可。 但即使这样还是会有提示说:RGSS-RTP Standard 找不到咯~所以嘛,在RPG Maker XP中的游戏->选择RTP中不要选Standard。 解决方案:
  • 首先就在RPG maker XP的安装目录\RGSS\Standard中将Audio和Graphics两个目录拷贝出来。然后将你制作的游戏目录中的Audio和Graphics覆盖到这两个目录。然后再将这两个目录覆盖到自己的游戏目录。注意这里谁先覆盖谁。
  • <li>第二步将游戏->选择RTP中全部改为Standard.如果你第一步做对了,那么现在你的游戏画面还是存在的,否则你就会发现全部都是空白。</li>
    
    <li>然后将RGSS102J.dll或者RGSS103J.dll放入到你的游戏制作目录。然后将这个dll文件的属性改为隐藏。不隐藏的话,RPG maker XP是不给打到安装包里的。</li>
    

    现在制作打包,就可以在没有安装过RPG maker XP的系统中运行了。当然需要是windows的系统。

    first we write simplest client and server.

    in client.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import socket
    class Client:
    def __init__(self, host , port, bufsize = 1024, timeout = 10):
    self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.host = host
    self.port = port
    self.bufsize = bufsize
    self.timeout = timeout

    def connect(self):
    self.client.connect((self.host,self.port))
    def close(self):
    self.client.close()
    def send(self, string):
    self.client.send(bytes(string,"utf8"))
    def recv(self):
    return str(self.client.recv(self.bufsize),encoding="utf8")

    in server.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import socket
    class Server:
    def __init__(self,port,listen = 5,timeout = 10, buf = 4096, queueSize = 10):
    self.host = 'localhost'
    self.port = port
    self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.listen = listen
    self.timeout = timeout
    def send(self, conn, string):
    conn.send(bytes(string,encoding="utf8"))
    def recv(self, conn):
    return str(conn.recv(self.bufsize),encoding="utf8")
    def run(self):
    print("server start, port:",self.port,"listen num:",self.listen)
    self.sock.bind((self.host, self.port))
    self.sock.listen(self.listen)
    while True:
    connection, address = self.sock.accept()
    message = self.recv(connection)
    print('message',message)
    self.send(connection,'OK')
    connection.close()

    next I will write some code to run client and server. Here is :

    in client_start.py

    1
    2
    3
    4
    5
    6
    7
    from client import Client
    if __name__ == "__main__":
    c = Client('localhost',7777)
    c.connect()
    c.send('hello')
    message = c.recv()
    print('message',message)

    in server_start.py

    1
    2
    3
    4
    5

    from server import Server
    if __name__ == "__main__":
    s = Server(7777, listen = 1000)
    s.run()

    Run server_start.py firstly and then run client_start.py. You will find both client and server print messages. Then client is shut down, but server is still running. Yup. Server should not shut down in order to supply services. And shut client down to close socket connection and save resources of server. These code runs perfectly. But we still should make some improvement. Server can server many clients. When it servers one client connection, it should server other connections and not be held by the former connection. Now multi-thread is considered.

    in “run” function of Server in server.py

    change run function and add a new function called “serviceWaiting”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    def serviceWaiting(self, connection, address):
    print("connection:",id(connection),"address:",str(address)," connect...")
    message = self.recv(connection)
    print('message',message)
    self.send(connection,'OK')
    connection.close()

    def run(self):
    print("server start, port:",self.port,"listen num:",self.listen)
    self.sock.bind((self.host, self.port))
    self.sock.listen(self.listen)
    while True:
    connection, address = self.sock.accept()
    serverThread = Thread(target = self.serviceWaiting, args=(connection, address))
    serverThread.start()

    Ok, done! Now server can server multi-clients in the same time. But only sending and receiving string is not enough for our applications. Sending and receiving objects should be considered.
    Here we add two functions in client.py and server.py.

    we should change a function in our former code:

    1
    2
    3
    4
    5
    6
    def recv(self, n= -1):
    if n == -1:
    return str(self.client.recv(self.bufsize),encoding="utf8")
    else:
    return str(self.client.recv(n),encoding="utf8")

    then we should add two function for sending and receive objects

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
        def recvObj(self):
    OK = 'Y'
    obj = None
    try:
    buffer = []
    totalsize = self.recv()
    # print('client:totalsize',totalsize,'len:',len(totalsize))
    totalsize = int(totalsize)
    self.send(OK)
    # print('client:send length ok')
    while totalsize !=0:
    # print('client: receiving')
    tBuff = self.client.recv(self.bufsize)
    totalsize -= len(tBuff)
    # print('client:tBuff len:',len(tBuff))
    buffer.extend(tBuff)
    buffer = bytes(buffer)
    # print('client:buff len',len(buffer))
    obj = pickle.loads(buffer)
    # print('client:server receiving done')
    # print('client:obj',obj)
    self.send(OK)
    # print('client:send ok')
    except socket.timeout:
    print ("client:time out")
    raise socket.timeout
    except EOFError:
    print('client:buff',buffer)
    raise EOFError
    return obj

    def sendObj(self, obj):
    OK = 'Y'
    try:
    totalbytes = pickle.dumps(obj)
    totalsize = len(totalbytes)
    # print('client:will send totalsize')
    self.send(str(totalsize))
    # print('client:send totalsize done')
    if self.recv()== OK:
    # print('client:send totalsize success and recv ok')
    self.client.sendall(totalbytes)
    # print('client:send object done')
    string = self.recv(1)
    # print('client: recv string',string)
    if string == OK:
    print('client:send object success, and recv ok')
    else:
    print('client:send object error')
    else:
    print('client:send object error')
    except:
    print('client:send object Exception')


    Here I will explain these code.

    • In sendObj(), first transform object to bytes and send the length of object. Then wait to receive “OK” message(string ‘Y’). If get OK message, then use sendall() function to send all bytes out. Then wait to receive another OK message. Here we use recv(1) to only get one char in receiving queue. We should not use recv to get more information which may not be used by sendObj() function.
    • In recvObj(), first receive the length of object. Then send OK message back. Receive bytes by fixed buffer each time. And receiv many times until we received all bytes of object. Then transform bytes to objects and send OK message back.
      Ok. Now we should consider if the we can not connect server at first time. How do we solve this problem? The simplest idea is try many times until we connected server successfully.We should change connection() function.

    in connection function of Client in client.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    def connect(self, count = -1):
    flag = True
    success = False
    while flag:
    try:
    print('connect...',self.host, self.port)

    self.client.connect((self.host,self.port))
    self.client.settimeout(self.timeout)
    success = True
    break
    except:
    count -= 1
    if count == 0:
    flag = False
    else:
    time.sleep(3)
    return success


    OK. Here we solved many problems, such as multi-clients, sending and receiving objects, and connection failed.

    长久以来一直被这个问题困扰:

    所谓引用就是看起来像赋值的东西,其实并没有复制出另外一份实例,而且共享同一个实例对象。

    python中对于基本类型一般是复制,例如int,float,boolean三种类型

    a = 3 b = a b = 4 print(a) // a == 3 a = 3.4 b = a b = 4.3 print(a) // a ==
    3.4 a = True b = a b = False print(a) // a == True

    另外文件、列表、字典三种类型都是

    f = open(‘a.txt’,‘w’) g = f g.close() f.write(‘afdsaf’) // I/O write error f =
    [1,2,3,4] g = f g[1] = 444 print(f) // [1,444,3,4] >>> f = {1:3} >>> g = f >>>
    g[1] = 4 >>> f {1: 4}

    另外值得注意的是传入的参数是值传入还是引用传入也是根据基本类型。

    def x(l): l = 3 a = 4 x(a) print(a) // a == 4 a = [1,3,4,5] x(a) print(a) // a
    == [1,3,4,5] def x(l): l[2] = 3 a = [1,3,4,5] x(a) print(a) // a == [1,3,3,5]

    在python出现的每一个东东都是由基本类型构成,而不是c/c++中的值。

    例如:

    a = [1,2,3,5] // 这里不是赋值,这里a是基本类型[1,2,3,5]的引用 b = a // 这里b是[1,2,3,5]的另外一个引用 b =
    [2,3,4] // b的引用变为[2,3,4],而a的还是[1,2,3,5]

    前几天,小李子一直在纠缠3D,伪3D游戏等。虽说小李子效力于大公司,但是该公司毕竟不是专业的游戏公司。美工XX等一概不全,要使用个3D引擎做个ipad,An
    droid的游戏。这年头是怎么了。

    游戏最主要的是什么?是画面?还是互动的画面?还是互动的立体画面?接触过这么多游戏,还没有见过只以最精致的画面获奖的作品。相反游戏界面粗糙主题出彩的作品倒是蛮
    多。画面仅仅是表现形式,何必拘泥于此。2D也好,3D也好,能表现出来该有的效果就可以了。45度角的2D的仙剑,没有啥角度可言的雨血,经典的俄罗斯方块和超级玛
    丽,2D方块的坦克大战。玩家理解接受当年的游戏可能因为条件限制开发不出来3D效果。现在条件有了,玩家就不再认可2D游戏?显然这并不是主要原因。我没有听到大富
    翁和仙剑被改版成3D效果后赞誉有所增加。现在的3D游戏的配置要求越来越高。一种游戏的观点是游戏需要成为现实生活的再现,而另外一种观点是游戏需要天马行空的思维
    ,构造出梦幻的世界。这些观点都是ok,但是没有说现实生活的再现,开发游戏就必须使用3D引擎,就必须全部都是3D的。

    虽然我不是游戏开发者,但是怎么说也是资深的游戏玩家。当然我也不能代表所有的游戏玩家说话。我个人只是觉得游戏题材和编剧是最为关键的因素。画面和交互性满足展现形
    式即可。所有2D还是3D…我还是觉得能用2D就用2D。因为2D相对于3D开发周期要快。

    socket connect fail will be many reasons. First you should check the firewall, does it block your socket port which you want to connect, python.exe or pythonw.exe Second, your server listens host and ip are the same your client socket. for example. Your server socket listens host='localhost', port=7777, and your client want to connect host='192.168.12.XXX', port=7777. They are not same. 'localhost' is '127.0.0.1', and your client want to connect host='192.168.12.XXX'. Even there is only one net-adapter and one network configure. So change host which your server socket listened to '192.168.12.XXX'. This error will occur in :
    1
    2
    3
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('192.168.12.XXX',7777)) # here you can not connect server

    this error seems you did not return the same number of results for example:
    1
    2
    3
    def fun():
    return 1
    a,b = fun()

    this will throw Exception : ValueError: need more than 2 values to unpack
    change code:

    1
    2
    3
    def fun():
    return 1,2
    a,b = fun()

    then ok!

    copy from .........??? 计算机专业面试笔试题目 一,选择题(皆为单选): 1,以下谁是二进制思想的最早提出者? a,伏羲;b,姬昌;c,莱布尼茨;d,柏拉图。 2,以下哪个概念和公孙龙的《指物论》中的“指”字含义相近? a,变量;b,数组;c,对象;d,指针。 3,蔺相如,司马相如;魏无忌,长孙无忌。下列哪一组对应关系与此类似? a,PHP,Python;b,JSP,servlet;c,java,javascript;d,C,C++。 4,秦始皇吞并六国采用了以下哪种算法思想? a,递归;b,分治;c,迭代;d,模拟。 5,雅典王子忒修斯勇闯克里特岛斩杀米诺牛的时候采用了以下哪种算法? a,动态规划;b,穷举;c,记忆化搜索;d,Dijkstra算法。 6,印度电影《宝莱坞机器人之恋》中的机器人七弟采用的智能算法最有可能是以下哪一种 ? a,神经网络;b,遗传算法;c,模拟退火;d,穷举算法。 7,《公孙龙子》记载:“齐王之谓尹文曰:‘寡人甚好士,以齐国无士,何也?’尹文曰 :‘愿闻大王之所谓士者。’齐王无以应。”这说明了齐王: a,昏庸无道;b,是个结巴;c,不会下定义;d,不会定义自己的需求。 8,惠施曾提出过“卵有毛”的命题,以下哪一项是导致这个错误命题的原因: a,混淆了命名空间;b,引入了错误的包;c,衍生类未重载;d,调用了危险的指针。 9,下面哪种面向对象的方法可以让你变得富有? a,继承;b,封装;c,多态;d,抽象。 10,明朝时期张居正改革的一条鞭法的主要思想是: a,面向过程;b,万物皆数;c,统一接口;d,泛型编程。 二,匹配题(分析A中的句子所体现的算法,和B中的算法一一匹配): A: 1,江南可采莲,莲叶何田田,鱼戏莲叶间。鱼戏莲叶东,鱼戏莲叶西,鱼戏莲叶南,鱼戏 莲叶北。——汉乐府《江南》 2,众里寻他千百度,蓦然回首,那人却在灯火阑珊处。——辛弃疾《青玉案》 3,从前有座山,山里有座庙,庙里有个老和尚,再给小和尚讲故事,故事内容是:从前有 座山,山里有座庙,庙里有个老和尚,再给小和尚讲故事,故事内容是:从前有座山,山 里有座庙,庙里有个老和尚,再给小和尚讲故事,故事内容是…… 4,只劝楼台追后主,不愁弓矢下残唐。——孔尚任《桃花扇》 5,飞鸟之影,未尝动也。——《庄子》 B: 1,贪心; 2,回溯; 3,穷举; 4,分治; 5,递归; 三,阅读理解(阅读下文,回答后面的问题): 美国的贝尔实验室设计了最初的C语言 刻在UNIX操作系统距今已有三四十年 你在屏幕前凝视数据的缱绻 我却在旁轻轻敲打键盘把你的梦想展现 循环 递归 贪心 动规 是谁的从前 喜欢在匈牙利算法中你我牵手的画面 经过MSRA门前我以大牛之名许愿 思念像斐波那契数列般漫延 当软工沦落在设计的文档间 算法依旧是永垂不朽的诗篇 我给你的爱写在程序间 深藏在最长不下降子序列里面 几万组数据流过后发现 我的心依然不变 我给你的爱写在程序间 深藏在最长不下降子序列里面 用无尽的代码刻下了永远 那已保存千年的誓言 一切又重演 我算了很多遍 时间复杂度还是趋于无限 我只想要这样永远链接在你的身边 1,题目中的MSRA是什么的缩写? 2,试赏析“思念像斐波那契数列般漫延”一句。 3,请结合时代背景,谈谈你对“当软工沦落在设计的文档间,算法依旧是永垂不朽的诗篇 ”一句的理解。 4,“几万组数据流过后发现,我的心依然不变”一句体现了算法的什么特性? 5,就“喜欢在匈牙利算法中你我牵手的画面”一句,谈谈你对匈牙利算法的理解。

    I found this funny clock in Fool's Day. You will like it.
    [swfobj src="http://chabudai.sakura.ne.jp/blogparts/honehoneclock/honehone_clock_tr.swf"]

    copy from :http://rdc.taobao.com/blog/cs/?p=671

    用消息队列和消息应用状态表来消除分布式事务

    由于数据量的巨大,大部分Web应用都需要部署很多个数据库实例。这样,有些用户操作就可能需要去修改多个数据库实例中的数据。传统的解决方法是使用分布式事务保证数据的全局一致性,经典的方法是使用两阶段提交协议。

    长期以来,分布式事务提供的优雅的全局ACID保证麻醉了应用开发者的心灵,很多人都不敢越雷池一步,想像没有分布式事务的世界会是怎样。如今就如MySQL和PostgreSQL这类面向低端用户的开源数据库都支持分布式事务了,开发者更是沉醉其中,不去考虑分布式事务是否给系统带来了伤害。

    事实上,有所得必有所失,分布式事务提供的ACID保证是以损害系统的可用性、性能与可伸缩性为代价的。只有在参与分布式事务的各个数据库实例都能够正常工作的前提下,分布式事务才能够顺利完成,只要有一个工作不正常,整个事务就不能完成。这样,系统的可用性就相当于参加分布式事务的各实例的可用性之积,实例越多,可用性下降越明显。从性能和可伸缩性角度看,首先是事务的总持续时间通常是各实例操作时间之和,因为一个事务中的各个操作通常是顺序执行的,这样事务的响应时间就会增加很多;其次是一般Web应用的事务都不大,单机操作时间也就几毫秒甚至不到1毫秒,一但涉及到分布式事务,提交时节点间的网络通信往返过程也为毫秒级别,对事务响应时间的影响也不可忽视。由于事务持续时间延长,事务对相关资源的锁定时间也相应增加,从而可能严重增加了并发冲突,影响到系统吞吐率和可伸缩性。

    正是由于分布式事务有以上问题,eBay在设计上就不采用分布式事务,而是通过其它途径来解决数据一致性问题。其中使用的最重要的技术就是消息队列和消息应用状态表。

    举个例子。假设系统中有以下两个表
    user(id, name, amt_sold, amt_bought)
    transaction(xid, seller_id, buyer_id, amount)
    其中user表记录用户交易汇总信息,transaction表记录每个交易的详细信息。

    这样,在进行一笔交易时,若使用事务,就需要对数据库进行以下操作:

    1
    2
    3
    4
    5
    begin;
    INSERT INTO transaction VALUES(xid, $seller_id, $buyer_id, $amount);
    UPDATE user SET amt_sold = amt_sold + $amount WHERE id = $seller_id;
    UPDATE user SET amt_bought = amt_bought + $amount WHERE id = $buyer_id;
    commit;

    即在transaction表中记录交易信息,然后更新卖家和买家的状态。

    假设transaction表和user表存储在不同的节点上,那么上述事务就是一个分布式事务。要消除这一分布式事务,将它拆分成两个子事务,一个更新transaction表,一个更新user表是不行的,因为有可能transaction表更新成功后,更新user失败,系统将不能恢复到一致状态。

    解决方案是使用消息队列。如下所示,先启动一个事务,更新transaction表后,并不直接去更新user表,而是将要对user表进行的更新插入到消息队列中。另外有一个异步任务轮询队列内容进行处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    begin;
    INSERT INTO transaction VALUES(xid, $seller_id, $buyer_id, $amount);
    put_to_queue “update user(“seller”, $seller_id, amount);
    put_to_queue “update user(“buyer”, $buyer_id, amount);
    commit;
    for each message in queue
    begin;
    dequeue message;
    if message.type = “seller” then
    UPDATE user SET amt_sold = amt_sold + message.amount WHERE id = message.user_id;
    else
    UPDATE user SET amt_bought = amt_bought + message.amount WHERE id = message.user_id;
    end
    commit;
    end

    上述解决方案看似完美,实际上还没有解决分布式问题。为了使第一个事务不涉及分布式操作,消息队列必须与transaction表使用同一套存储资源,但为了使第二个事务是本地的,消息队列存储又必须与user表在一起。这两者是不可能同时满足的。

    如果消息具有操作幂等性,也就是一个消息被应用多次与应用一次产生的效果是一样的话,上述问题是很好解决的,只要将消息队列放到transaction表一起,然后在第二个事务中,先应用消息,再从消息队列中删除。由于消息队列存储与user表不在一起,应用消息后,可能还没来得及将应用过的消息从队列中删除时系统就出故障了。这时系统恢复后会重新应用一次这一消息,由于幂等性,应用多次也能产生正确的结果。

    但实际情况下,消息很难具有幂等性,比如上述的UPDATE操作,执行一次和执行多次的结束显然是不一样的。解决这一问题的方法是使用另一个表记录已经被成功应用的消息,并且这个表使用与user表相同的存储。假设增加以下表 message_applied(msg_id)记录被成功应用的消息,则产生最终的解决方案如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    begin;
    INSERT INTO transaction VALUES(xid, $seller_id, $buyer_id, $amount);
    put_to_queue “update user(“seller”, $seller_id, amount);
    put_to_queue “update user(“buyer”, $buyer_id, amount);
    commit;
    for each message in queue
    begin;
    SELECT count(*) as cnt FROM message_applied WHERE msg_id = message.id;
    if cnt = 0 then
    if message.type = “seller” then
    UPDATE user SET amt_sold = amt_sold + message.amount WHERE id = message.user_id;
    else
    UPDATE user SET amt_bought = amt_bought + message.amount WHERE id = message.user_id;
    end
    INSERT INTO message_applied VALUES(message.id);
    end
    commit;
    if 上述事务成功
    dequeue message
    DELETE FROM message_applied WHERE msg_id = message.id;
    end
    end

    我们来仔细分析一下:
    1、消息队列与transaction使用同一实例,因此第一个事务不涉及分布式操作;
    2、message_applied与user表在同一个实例中,也能保证一致性;
    3、第二个事务结束后,dequeue message之前系统可能出故障,出故障后系统会重新从消息队列中取出这一消息,但通过message_applied表可以检查出来这一消息已经被应用过,跳过这一消息实现正确的行为;
    4、最后将已经成功应用,且已经从消息队列中删除的消息从message_applied表中删除,可以将message_applied表保证在很小的状态(不清除也是可以的,不影响系统正确性)。由于消息队列与message_applied在不同实例上,dequeue message之后,将对应message_applied记录删除之前可能出故障。一但这时出现故障,message_applied表中会留下一些垃圾内容,但不影响系统正确性,另外这些垃圾内容也是可以正确清理的。

    虽然由于没有分布式事务的强一致性保证,使用上述方案在系统发生故障时,系统将短时间内处于不一致状态。但基于消息队列和消息应用状态表,最终可以将系统恢复到一致。使用消息队列方案,解除了两个数据库实例之间的紧密耦合,其性能和可伸缩性是分布式事务不可比拟的。

    当然,使用分布式事务有助于简化应用开发,使用消息队列明显需要更多的工作量,两者各有优缺点。个人观点是,对于时间紧迫或者对性能要求不高的系统,应采用分布式事务加快开发效率,对于时间需求不是很紧,对性能要求很高的系统,应考虑使用消息队列方案。对于原使用分布式事务,且系统已趋于稳定,性能要求高的系统,则可以使用消息队列方案进行重构来优化性能。

    注: 本文主要取材于eBay的工程师Dan Pritchet写的这篇文章。

    0%