分类 软件开发 下的文章

这是用 Python 从头开始构建游戏的系列文章的第三部分。

这个系列的第一篇文章 中,我解释了如何使用 Python 创建一个简单的基于文本的骰子游戏。在第二部分中,我向你们展示了如何从头开始构建游戏,即从 创建游戏的环境 开始。但是每个游戏都需要一名玩家,并且每个玩家都需要一个可操控的角色,这也就是我们接下来要在这个系列的第三部分中需要做的。

在 Pygame 中,玩家操控的图标或者化身被称作 妖精 sprite 。如果你现在还没有任何可用于玩家妖精的图像,你可以使用 KritaInkscape 来自己创建一些图像。如果你对自己的艺术细胞缺乏自信,你也可以在 OpenClipArt.orgOpenGameArt.org 搜索一些现成的图像。如果你还未按照上一篇文章所说的单独创建一个 images 文件夹,那么你需要在你的 Python 项目目录中创建它。将你想要在游戏中使用的图片都放 images 文件夹中。

为了使你的游戏真正的刺激,你应该为你的英雄使用一张动态的妖精图片。这意味着你需要绘制更多的素材,并且它们要大不相同。最常见的动画就是走路循环,通过一系列的图像让你的妖精看起来像是在走路。走路循环最快捷粗糙的版本需要四张图像。

注意:这篇文章中的代码示例同时兼容静止的和动态的玩家妖精。

将你的玩家妖精命名为 hero.png。如果你正在创建一个动态的妖精,则需要在名字后面加上一个数字,从 hero1.png 开始。

创建一个 Python 类

在 Python 中,当你在创建一个你想要显示在屏幕上的对象时,你需要创建一个类。

在你的 Python 脚本靠近顶端的位置,加入如下代码来创建一个玩家。在以下的代码示例中,前三行已经在你正在处理的 Python 脚本中:

import pygame
import sys
import os # 以下是新代码

class Player(pygame.sprite.Sprite):
    '''
    生成一个玩家
    '''
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.images = []
    img = pygame.image.load(os.path.join('images','hero.png')).convert()
    self.images.append(img)
    self.image = self.images[0]
    self.rect  = self.image.get_rect()

如果你的可操控角色拥有一个走路循环,在 images 文件夹中将对应图片保存为 hero1.pnghero4.png 的独立文件。

使用一个循环来告诉 Python 遍历每个文件。

'''
对象
'''

class Player(pygame.sprite.Sprite):
    '''
    生成一个玩家
    '''
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.images = []
        for i in range(1,5):
            img = pygame.image.load(os.path.join('images','hero' + str(i) + '.png')).convert()
            self.images.append(img)
            self.image = self.images[0]
            self.rect  = self.image.get_rect()

将玩家带入游戏世界

现在已经创建好了一个 Player 类,你需要使用它在你的游戏世界中生成一个玩家妖精。如果你不调用 Player 类,那它永远不会起作用,(游戏世界中)也就不会有玩家。你可以通过立马运行你的游戏来验证一下。游戏会像上一篇文章末尾看到的那样运行,并得到明确的结果:一个空荡荡的游戏世界。

为了将一个玩家妖精带到你的游戏世界,你必须通过调用 Player 类来生成一个妖精,并将它加入到 Pygame 的妖精组中。在如下的代码示例中,前三行是已经存在的代码,你需要在其后添加代码:

world       = pygame.display.set_mode([worldx,worldy])
backdrop    = pygame.image.load(os.path.join('images','stage.png')).convert()
backdropbox = screen.get_rect()

# 以下是新代码

player = Player()   # 生成玩家
player.rect.x = 0   # 移动 x 坐标
player.rect.y = 0   # 移动 y 坐标
player_list = pygame.sprite.Group()
player_list.add(player)

尝试启动你的游戏来看看发生了什么。高能预警:它不会像你预期的那样工作,当你启动你的项目,玩家妖精没有出现。事实上它生成了,只不过只出现了一毫秒。你要如何修复一个只出现了一毫秒的东西呢?你可能回想起上一篇文章中,你需要在主循环中添加一些东西。为了使玩家的存在时间超过一毫秒,你需要告诉 Python 在每次循环中都绘制一次。

将你的循环底部的语句更改如下:

    world.blit(backdrop, backdropbox)
    player_list.draw(screen) # 绘制玩家
    pygame.display.flip()
    clock.tick(fps)

现在启动你的游戏,你的玩家出现了!

设置 alpha 通道

根据你如何创建你的玩家妖精,在它周围可能会有一个色块。你所看到的是 alpha 通道应该占据的空间。它本来是不可见的“颜色”,但 Python 现在还不知道要使它不可见。那么你所看到的,是围绕在妖精周围的边界区(或现代游戏术语中的“ 命中区 hit box ”)内的空间。

你可以通过设置一个 alpha 通道和 RGB 值来告诉 Python 使哪种颜色不可见。如果你不知道你使用 alpha 通道的图像的 RGB 值,你可以使用 Krita 或 Inkscape 打开它,并使用一种独特的颜色,比如 #00ff00(差不多是“绿屏绿”)来填充图像周围的空白区域。记下颜色对应的十六进制值(此处为 #00ff00,绿屏绿)并将其作为 alpha 通道用于你的 Python 脚本。

使用 alpha 通道需要在你的妖精生成相关代码中添加如下两行。类似第一行的代码已经存在于你的脚本中,你只需要添加另外两行:

            img = pygame.image.load(os.path.join('images','hero' + str(i) + '.png')).convert()
            img.convert_alpha()     # 优化 alpha
            img.set_colorkey(ALPHA) # 设置 alpha

除非你告诉它,否则 Python 不知道将哪种颜色作为 alpha 通道。在你代码的设置相关区域,添加一些颜色定义。将如下的变量定义添加于你的设置相关区域的任意位置:

ALPHA = (0, 255, 0)

在以上示例代码中,0,255,0 被我们使用,它在 RGB 中所代表的值与 #00ff00 在十六进制中所代表的值相同。你可以通过一个优秀的图像应用程序,如 GIMP、Krita 或 Inkscape,来获取所有这些颜色值。或者,你可以使用一个优秀的系统级颜色选择器,如 KColorChooser,来检测颜色。

如果你的图像应用程序将你的妖精背景渲染成了其他的值,你可以按需调整 ALPHA 变量的值。不论你将 alpha 设为多少,最后它都将“不可见”。RGB 颜色值是非常严格的,因此如果你需要将 alpha 设为 000,但你又想将 000 用于你图像中的黑线,你只需要将图像中线的颜色设为 111。这样一来,(图像中的黑线)就足够接近黑色,但除了电脑以外没有人能看出区别。

运行你的游戏查看结果。

这个系列的第四篇文章 中,我会向你们展示如何使你的妖精动起来。多么的激动人心啊!


via: https://opensource.com/article/17/12/game-python-add-a-player

作者:Seth Kenlon 选题:lujun9972 译者:cycoe 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

介绍

Python 与 PHP 都是广泛使用的语言,各有所长,让人期待两者结合可以实现更丰富的效果。

在 PHP 中调用 Python 实现某些处理,这种需求虽然比较小众,还是实用的。目前网上可以查到很多资料仍在探讨 exec()(也包括 system()shell_exec()passthru() 等)执行外部的 Python 文件,但这只是一种通用的方式,调用成本比较高,在每次调用时,需要装载整个 Python 解释环境。

有此类需求的开发者非常适合看一下 PPython,这是一种从根本上将 PHP 与 Python 有效结合的技术。

PPython 最初见于 https://code.google.com/p/ppython,该作者将 lajp(一种 PHP 结合 Java 的技术)移植到了 Python 上。

该项目最初建立于 2012 年,而且似乎已经停止维护多年,不过目前来看其思路及效果还是值得肯定的,因此将此项目从停止运营的 Google Code 上迁移到了 GitHub,并遵循原 Apache 许可证重新发布和维护。

日前笔者对此作了一番尝试,对 PPython 的方便易用有所体会。

原理与架构

PHP 与 Python 通讯有两种不同的套接字机制:TCP 套接字和 UNIX 套接字。UNIX 套接字是 Unix/Linux 本地套接字,相对于 TCP 套接字,具有以下特点:

  • 只能在同一台主机中通讯(IPC),不能跨主机;
  • 传输速度大于 TCP 套接字;
  • 服务端只向本机提供服务(没有对外侦听端口),相对安全,易于管理。

PHP 和 Python 各有其语言内部定义的数据类型,通常 PHP 进程与 Python 进程进行数据交互时,需要进行转码处理。此类转换如由应用自行实现,从开发效率到运行性能都会增加不少额外负担。

PPython 对 PHP 和 Python 间的通讯方式的处理支持 TCP 套接字和 UNIX 套接字两种机制,兼顾通讯效率和分布式,转码由服务统一处理,Python 为 PHP 的数据类型提供格式兼容,使 PHP 端开发无须为底层通讯担心。

Python 因其语言 GIL 特性,同一进程内多线程效率不高。PPython 可根据项目需要部署服务,多进程运行 Python,提高应用综合性能。

使用方法

PPython 的代码可从上述项目仓库中下载。

下载得到的文件中,以下三个是 PPython 的核心代码,作用如下:

  • php_python.py,Python 进程主文件,完成 Python 端监听请求并运行返回
  • process.py,Python 端核心类,实现 Python 内部进程调用及 PHP 与 Python 数据结构转化等关键处理
  • php_python.php,PPython 客户端,PHP 端引用此文件,可直接使用 PPython 函数实现调用。

将以上文件放置到任意目录。先修改准备运行 PPython 的端口,监听端口不限,只要 php_python.pyphp_python.php 两端修改一致。笔者统一改为 10240。

在当前目录下运行 php_python.py,只要 Python 环境正常,便将运行起一个 PPython 的服务。

-------------------------------------------
- PPython Service
- Time: 2019-05-13 22:24:09
-------------------------------------------
Listen port: 10240
charset: utf-8
Server startup...

PHP 端引入 php_python.php,就可以用 ppython 函数与之前启动的 PPython 服务通讯,传入请求由 PPython 服务调用 Python 处理后返回结果,如 $res = ppython('test::go') 是调用test.py 中的 go 函数,也可加上更多参数,第二个参数起将为被调的函数传递更多参数。

php_python.py 是 PPython 启动后直接运行的全局代码,有全局配置或进程启动后的通用处理都写在这里,如原生代码中建立了数据库连接等,项目中应视情况作优化。

但 Python 令人感兴趣的主要方面不只是像 PHP 那样描述业务功能,它可以在人工智能等领域所需要的计算型任务提供对更复杂的数据结构的处理,因此二者的结合可以给 PHP 带来更多应用场景。

改进

此外,原生的 php_python.py 还有些不足。笔者用 ppython 调用自定义代码中遇到了三个问题,并相应做了解决:

  1. 不支持 complex(复数类),复数是数学上的一种数据类型,主要包括 real(实部)和 imag(虚部)数据,虽然日常生活中遇到较少,但 AI 和各种专业研究领域或并不罕见。Python 里有 complex 类,对复数可以直接进行各种计算,但 PPython 序列化和反序列化对 complex 没有处理。为了能让 complex 包括的数据能正常返回,只要在 process.pyz_encode() 方法中加上符合 PHP 要求的序列化处理,代码如下:
elif isinstance(p, numpy.complex128):
    t1 = str(p.real)
    t2 = str(p.imag)
    return 'O:7:"complex":2:{s:4:"real";d:%s;s:4:"imag";d:%s;}' % (t1,t2)
  1. 不支持 ndarray(多维数组)。相比 complexndarray 要普通得多,相信凡使用到 Python 的各种计算功能,ndarray 是无法回避的,甚至 ndarray 在一定程度上成就了 Python。但原 php_python.py 不能识别 ndarray。不过解决起来并不难,在 process.py 里找到z_encode() 方法,加上下面这段,可以直接将 ndarray 转化为符合 PHP 要求的数组(数字索引)。
    elif isinstance(p, numpy.ndarray):
        s = ''
        i = 0
        for d in p:
            s += 'i:%d;%s' % (i,z_encode(d))
            i += 1
        return "a:%d:{%s}" % (len(p),s)
  1. 原代码不太稳健,如数据为 ndarray 类型,if p == None:报错ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all(),因为p == None的结果也是ndarray,不返回false ,将判断方法改为if p is None:可避免出错。

相应地 PHP 端也要注意一下序列化和反序列化的处理。

处理回复中类似 complex 这样的对象数据时,如系统中没有定义相应的类,PHP 是可以反序列化的,但将显示为 “incomplete object”,vardump 看得到 realimag 数据,但不能直接操作,自行定义 complex 类后,则按指定的类进行解析,与 PHP 内的一般对象无异,可以轻松进行所有操作。

至此,PHP 与 Python 的功能调讯已无问题。

补充:注册为服务

命令行下启动 php_python.py 主要是方便调试,可以看到观察反馈信息等,生产环境中手工启动 PPython 毕竟不太方便。可以将 PPython 配置成服务,修改端口也可以为不同的应用配置不同的 PPython 端。

Linux 下将一个进程注册为服务很简单,只要建立 /usr/lib/systemd/system/ppython.service,内容如下:

[Unit]
Description=PHP-Python Service
After=network.target remote-fs.target nss-lookup.target
 
[Service]
ExecStart={PPYTHON_PATH}/php_python.py

[Install]
WantedBy=multi-user.target

其中 {PPYTHON_PATH} 要改成实际路径。

总结

有了 PPython,可以摒弃 exec() 这种 shell 调用,使开发回归到逻辑本身。

个人认为该方案值得所有对 PHP 和 Python 都感兴趣的开发人员了解,也欢迎大家参与和贡献这个项目

学习自然语言处理的基础知识并探索两个有用的 Python 包。

自然语言处理(NLP)是机器学习的一种,它解决了口语或书面语言和计算机辅助分析这些语言之间的相关性。日常生活中我们经历了无数的 NLP 创新,从写作帮助和建议到实时语音翻译,还有口译。

本文研究了 NLP 的一个特定领域:情感分析。重点是确定输入语言的积极、消极或中性性质。本部分将解释 NLP 和情感分析的背景,并探讨两个开源的 Python 包。第 2 部分将演示如何开始构建自己的可扩展情感分析服务。

在学习情感分析时,对 NLP 有一个大体了解是有帮助的。本文不会深入研究数学本质。相反,我们的目标是阐明 NLP 中的关键概念,这些概念对于将这些方法实际结合到你的解决方案中至关重要。

自然语言和文本数据

合理的起点是从定义开始:“什么是自然语言?”它是我们人类相互交流的方式,沟通的主要方式是口语和文字。我们可以更进一步,只关注文本交流。毕竟,生活在 Siri、Alexa 等无处不在的时代,我们知道语音是一组与文本无关的计算。

数据前景和挑战

我们只考虑使用文本数据,我们可以对语言和文本做什么呢?首先是语言,特别是英语,除了规则还有很多例外,含义的多样性和语境差异,这些都可能使人类口译员感到困惑,更不用说计算机翻译了。在小学,我们学习文章和标点符号,通过讲母语,我们获得了寻找直觉上表示唯一意义的词的能力。比如,出现诸如 “a”、“the” 和 “or” 之类的文章,它们在 NLP 中被称为停止词,因为传统上 NLP 算法是在一个序列中找到这些词时意味着搜索停止。

由于我们的目标是自动将文本分类为情感类,因此我们需要一种以计算方式处理文本数据的方法。因此,我们必须考虑如何向机器表示文本数据。众所周知,利用和解释语言的规则很复杂,输入文本的大小和结构可能会有很大差异。我们需要将文本数据转换为数字数据,这是机器和数学的首选方式。这种转变属于特征提取的范畴。

在提取输入文本数据的数字表示形式后,一个改进可能是:给定一个文本输入体,为上面列出的文章确定一组向量统计数据,并根据这些数据对文档进行分类。例如,过多的副词可能会使撰稿人感到愤怒,或者过度使用停止词可能有助于识别带有内容填充的学期论文。诚然,这可能与我们情感分析的目标没有太大关系。

词袋

当你评估一个文本陈述是积极还是消极的时候,你使用哪些上下文来评估它的极性?(例如,文本中是否具有积极的、消极的或中性的情感)一种方式是隐含形容词:被称为 “disgusting”(恶心) 的东西被认为是消极的,但如果同样的东西被称为 “beautiful”(漂亮),你会认为它是积极的。从定义上讲,俗语给人一种熟悉感,通常是积极的,而脏话可能是敌意的表现。文本数据也可以包括表情符号,它带有固定的情感。

理解单个单词的极性影响为文本的 词袋 bag-of-words (BoW)模型提供了基础。它分析一组单词或词汇表,并提取关于这些单词在输入文本中是否存在的度量。词汇表是通过处理已知极性的文本形成称为标记的训练数据。从这组标记数据中提取特征,然后分析特征之间的关系,并将标记与数据关联起来。

“词袋”这个名称说明了它的用途:即不考虑空间位置或上下文的的单个词。词汇表通常是由训练集中出现的所有单词构建的,训练后往往会被修剪。如果在训练之前没有清理停止词,那么停止词会因为其高频率和低语境而被移除。很少使用的单词也可以删除,因为缺乏为一般输入实例提供的信息。

但是,重要的是要注意,你可以(并且应该)进一步考虑单词在单个训练数据实例之外的情形,这称为 词频 term frequency (TF)。你还应该考虑输入数据在所有训练实例中的单词计数,通常,出现在所有文档中的低频词更重要,这被称为 逆文本频率指数 inverse document frequency (IDF)。这些指标一定会在本主题系列的其他文章和软件包中提及,因此了解它们会有所帮助。

词袋在许多文档分类应用程序中很有用。然而,在情感分析中,当缺乏情境意识的问题被利用时,事情就可以解决。考虑以下句子:

  • 我们不喜欢这场战争。
  • 我讨厌下雨天,好事是今天是晴天。
  • 这不是生死攸关的问题。

这些短语的情感对于人类口译员来说是有难度的,而且通过严格关注单个词汇的实例,对于机器翻译来说也是困难的。

在 NLP 中也可以使用称为 “n-grams” 的单词分组。一个二元组考虑两个相邻单词组成的组而不是(或除了)单个词袋。这应该可以缓解诸如上述“不喜欢”之类的情况,但由于缺乏语境意思,它仍然是个问题。此外,在上面的第二句中,下半句的情感语境可以被理解为否定前半部分。因此,这种方法中也会丢失上下文线索的空间局部性。从实用角度来看,使问题复杂化的是从给定输入文本中提取的特征的稀疏性。对于一个完整的大型词汇表,每个单词都有一个计数,可以将其视为一个整数向量。大多数文档的向量中都有大量的零计数向量,这给操作增加了不必要的空间和时间复杂度。虽然已经提出了许多用于降低这种复杂性的简便方法,但它仍然是一个问题。

词嵌入

词嵌入 Word embedding 是一种分布式表示,它允许具有相似含义的单词具有相似的表示。这是基于使用实值向量来与它们周围相关联。重点在于使用单词的方式,而不仅仅是它们的存在与否。此外,词嵌入的一个巨大实用优势是它们关注于密集向量。通过摆脱具有相应数量的零值向量元素的单词计数模型,词嵌入在时间和存储方面提供了一个更有效的计算范例。

以下是两个优秀的词嵌入方法。

Word2vec

第一个是 Word2vec,它是由 Google 开发的。随着你对 NLP 和情绪分析研究的深入,你可能会看到这种嵌入方法。它要么使用一个 连续的词袋 continuous bag of words (CBOW),要么使用一个连续 skip-gram 模型。在 CBOW 中,一个单词的上下文是在训练中根据围绕它的单词来学习的。连续 skip-gram 学习倾向于围绕给定的单词学习单词。虽然这可能超出了你需要解决的问题,但是如果你曾经面对必须生成自己的词嵌入情况,那么 Word2vec 的作者就提倡使用 CBOW 方法来提高速度并评估频繁的单词,而 skip-gram 方法更适合嵌入稀有单词更重要的嵌入。

GloVe

第二个是 用于词表示的全局向量 Global Vectors for Word Representation (GloVe),它是斯坦福大学开发的。它是 Word2vec 方法的扩展,试图通过将经典的全局文本统计特征提取获得的信息与 Word2vec 确定的本地上下文信息相结合。实际上,在一些应用程序中,GloVe 性能优于 Word2vec,而在另一些应用程序中则不如 Word2vec。最终,用于词嵌入的目标数据集将决定哪种方法最优。因此,最好了解它们的存在性和高级机制,因为你很可能会遇到它们。

创建和使用词嵌入

最后,知道如何获得词嵌入是有用的。在第 2 部分中,你将看到我们通过利用社区中其他人的实质性工作,站到了巨人的肩膀上。这是获取词嵌入的一种方法:即使用现有的经过训练和验证的模型。实际上,有无数的模型适用于英语和其他语言,一定会有一种模型可以满足你的应用程序,让你开箱即用!

如果没有的话,就开发工作而言,另一个极端是培训你自己的独立模型,而不考虑你的应用程序。实质上,你将获得大量标记的训练数据,并可能使用上述方法之一来训练模型。即使这样,你仍然只是在理解你输入文本数据。然后,你需要为你应用程序开发一个特定的模型(例如,分析软件版本控制消息中的情感价值),这反过来又需要自己的时间和精力。

你还可以对针对你的应用程序的数据训练一个词嵌入,虽然这可以减少时间和精力,但这个词嵌入将是特定于应用程序的,这将会降低它的可重用性。

可用的工具选项

考虑到所需的大量时间和计算能力,你可能想知道如何才能找到解决问题的方法。的确,开发可靠模型的复杂性可能令人望而生畏。但是,有一个好消息:已经有许多经过验证的模型、工具和软件库可以为我们提供所需的大部分内容。我们将重点关注 Python,因为它为这些应用程序提供了大量方便的工具。

SpaCy

SpaCy 提供了许多用于解析输入文本数据和提取特征的语言模型。它经过了高度优化,并被誉为同类中最快的库。最棒的是,它是开源的!SpaCy 会执行标识化、词性分类和依赖项注释。它包含了用于执行此功能的词嵌入模型,还有用于为超过 46 种语言的其他特征提取操作。在本系列的第二篇文章中,你将看到它如何用于文本分析和特征提取。

vaderSentiment

vaderSentiment 包提供了积极、消极和中性情绪的衡量标准。正如 原论文 的标题(《VADER:一个基于规则的社交媒体文本情感分析模型》)所示,这些模型是专门为社交媒体文本数据开发和调整的。VADER 接受了一组完整的人类标记过的数据的训练,包括常见的表情符号、UTF-8 编码的表情符号以及口语术语和缩写(例如 meh、lol、sux)。

对于给定的输入文本数据,vaderSentiment 返回一个极性分数百分比的三元组。它还提供了一个单个的评分标准,称为 vaderSentiment 复合指标。这是一个在 [-1, 1] 范围内的实值,其中对于分值大于 0.05 的情绪被认为是积极的,对于分值小于 -0.05 的被认为是消极的,否则为中性。

第 2 部分中,你将学习如何使用这些工具为你的设计添加情感分析功能。


via: https://opensource.com/article/19/4/social-media-sentiment-analysis-python

作者:Michael McCune 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这系列的第一篇通过创建一个简单的骰子游戏来探究 Python。现在是来从零制作你自己的游戏的时间。

在我的这系列的第一篇文章 中, 我已经讲解如何使用 Python 创建一个简单的、基于文本的骰子游戏。这次,我将展示如何使用 Python 模块 Pygame 来创建一个图形化游戏。它将需要几篇文章才能来得到一个确实做成一些东西的游戏,但是到这系列的结尾,你将更好地理解如何查找和学习新的 Python 模块和如何从其基础上构建一个应用程序。

在开始前,你必须安装 Pygame

安装新的 Python 模块

有几种方法来安装 Python 模块,但是最通用的两个是:

  • 从你的发行版的软件存储库
  • 使用 Python 的软件包管理器 pip

两个方法都工作的很好,并且每一个都有它自己的一套优势。如果你是在 Linux 或 BSD 上开发,可以利用你的发行版的软件存储库来自动和及时地更新。

然而,使用 Python 的内置软件包管理器可以给予你控制更新模块时间的能力。而且,它不是特定于操作系统的,这意味着,即使当你不是在你常用的开发机器上时,你也可以使用它。pip 的其它的优势是允许本地安装模块,如果你没有正在使用的计算机的管理权限,这是有用的。

使用 pip

如果 Python 和 Python3 都安装在你的系统上,你想使用的命令很可能是 pip3,它用来区分 Python 2.x 的 pip 的命令。如果你不确定,先尝试 pip3

pip 命令有些像大多数 Linux 软件包管理器一样工作。你可以使用 search 搜索 Python 模块,然后使用 install 安装它们。如果你没有你正在使用的计算机的管理权限来安装软件,你可以使用 --user 选项来仅仅安装模块到你的家目录。

$ pip3 search pygame
[...]
Pygame (1.9.3)                 - Python Game Development
sge-pygame (1.5)               - A 2-D game engine for Python
pygame_camera (0.1.1)          - A Camera lib for PyGame
pygame_cffi (0.2.1)            - A cffi-based SDL wrapper that copies the pygame API.
[...]
$ pip3 install Pygame --user

Pygame 是一个 Python 模块,这意味着它仅仅是一套可以使用在你的 Python 程序中的库。换句话说,它不是一个像 IDLENinja-IDE 一样可以让你启动的程序。

Pygame 新手入门

一个电子游戏需要一个背景设定:故事发生的地点。在 Python 中,有两种不同的方法来创建你的故事背景:

  • 设置一种背景颜色
  • 设置一张背景图片

你的背景仅是一张图片或一种颜色。你的电子游戏人物不能与在背景中的东西相互作用,因此,不要在后面放置一些太重要的东西。它仅仅是设置装饰。

设置你的 Pygame 脚本

要开始一个新的 Pygame 工程,先在计算机上创建一个文件夹。游戏的全部文件被放在这个目录中。在你的工程文件夹内部保持所需要的所有的文件来运行游戏是极其重要的。

一个 Python 脚本以文件类型、你的姓名,和你想使用的许可证开始。使用一个开放源码许可证,以便你的朋友可以改善你的游戏并与你一起分享他们的更改:

#!/usr/bin/env python3
# by Seth Kenlon

## GPLv3
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

然后,你告诉 Python 你想使用的模块。一些模块是常见的 Python 库,当然,你想包括一个你刚刚安装的 Pygame 模块。

import pygame  # 加载 pygame 关键字
import sys     # 让 python 使用你的文件系统
import os      # 帮助 python 识别你的操作系统

由于你将用这个脚本文件做很多工作,在文件中分成段落是有帮助的,以便你知道在哪里放代码。你可以使用块注释来做这些,这些注释仅在看你的源文件代码时是可见的。在你的代码中创建三个块。

'''
Objects
'''

# 在这里放置 Python 类和函数

'''
Setup
'''

# 在这里放置一次性的运行代码

'''
Main Loop
'''

# 在这里放置游戏的循环代码指令

接下来,为你的游戏设置窗口大小。注意,不是每一个人都有大计算机屏幕,所以,最好使用一个适合大多数人的计算机的屏幕大小。

这里有一个方法来切换全屏模式,很多现代电子游戏都会这样做,但是,由于你刚刚开始,简单起见仅设置一个大小即可。

'''
Setup
'''
worldx = 960
worldy = 720

在脚本中使用 Pygame 引擎前,你需要一些基本的设置。你必须设置帧频,启动它的内部时钟,然后开始 (init)Pygame 。

fps   = 40  # 帧频
ani   = 4   # 动画循环
clock = pygame.time.Clock()
pygame.init()

现在你可以设置你的背景。

设置背景

在你继续前,打开一个图形应用程序,为你的游戏世界创建一个背景。在你的工程目录中的 images 文件夹内部保存它为 stage.png

这里有一些你可以使用的自由图形应用程序。

  • Krita 是一个专业级绘图素材模拟器,它可以被用于创建漂亮的图片。如果你对创建电子游戏艺术作品非常感兴趣,你甚至可以购买一系列的游戏艺术作品教程
  • Pinta 是一个基本的,易于学习的绘图应用程序。
  • Inkscape 是一个矢量图形应用程序。使用它来绘制形状、线、样条曲线和贝塞尔曲线。

你的图像不必很复杂,你可以以后回去更改它。一旦有了它,在你文件的 Setup 部分添加这些代码:

world    = pygame.display.set_mode([worldx,worldy])
backdrop = pygame.image.load(os.path.join('images','stage.png').convert())
backdropbox = world.get_rect()

如果你仅仅用一种颜色来填充你的游戏的背景,你需要做的就是:

world = pygame.display.set_mode([worldx,worldy])

你也必须定义颜色以使用。在你的 Setup 部分,使用红、绿、蓝 (RGB) 的值来创建一些颜色的定义。

'''
Setup
'''

BLUE  = (25,25,200)
BLACK = (23,23,23 )
WHITE = (254,254,254)

至此,你理论上可以启动你的游戏了。问题是,它可能仅持续了一毫秒。

为证明这一点,保存你的文件为 your-name_game.py(用你真实的名称替换 your-name)。然后启动你的游戏。

如果你正在使用 IDLE,通过选择来自 “Run” 菜单的 “Run Module” 来运行你的游戏。

如果你正在使用 Ninja,在左侧按钮条中单击 “Run file” 按钮。

你也可以直接从一个 Unix 终端或一个 Windows 命令提示符中运行一个 Python 脚本。

$ python3 ./your-name_game.py

如果你正在使用 Windows,使用这命令:

py.exe your-name_game.py

启动它,不过不要期望很多,因为你的游戏现在仅仅持续几毫秒。你可以在下一部分中修复它。

循环

除非另有说明,一个 Python 脚本运行一次并仅一次。近来计算机的运行速度是非常快的,所以你的 Python 脚本运行时间会少于 1 秒钟。

为强制你的游戏来处于足够长的打开和活跃状态来让人看到它(更不要说玩它),使用一个 while 循环。为使你的游戏保存打开,你可以设置一个变量为一些值,然后告诉一个 while 循环只要变量保持未更改则一直保存循环。

这经常被称为一个“主循环”,你可以使用术语 main 作为你的变量。在你的 Setup 部分的任意位置添加代码:

main = True

在主循环期间,使用 Pygame 关键字来检查键盘上的按键是否已经被按下或释放。添加这些代码到你的主循环部分:

'''
Main loop
'''
while main == True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit(); sys.exit()
            main = False

        if event.type == pygame.KEYDOWN:
            if event.key == ord('q'):
                pygame.quit()
                sys.exit()
                main = False

也是在你的循环中,刷新你世界的背景。

如果你使用一个图片作为背景:

world.blit(backdrop, backdropbox)

如果你使用一种颜色作为背景:

world.fill(BLUE)

最后,告诉 Pygame 来重新刷新屏幕上的所有内容,并推进游戏的内部时钟。

    pygame.display.flip()
    clock.tick(fps)

保存你的文件,再次运行它来查看你曾经创建的最无趣的游戏。

退出游戏,在你的键盘上按 q 键。

在这系列的 下一篇文章 中,我将向你演示,如何加强你当前空空如也的游戏世界,所以,继续学习并创建一些将要使用的图形!


via: https://opensource.com/article/17/12/game-framework-python

作者:Seth Kenlon 选题:lujun9972 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

学习在 Linux 中进程是如何与其他进程进行同步的。

本篇是 Linux 下进程间通信(IPC)系列的第二篇文章。第一篇文章 聚焦于通过共享文件和共享内存段这样的共享存储来进行 IPC。这篇文件的重点将转向管道,它是连接需要通信的进程之间的通道。管道拥有一个写端用于写入字节数据,还有一个读端用于按照先入先出的顺序读入这些字节数据。而这些字节数据可能代表任何东西:数字、员工记录、数字电影等等。

管道有两种类型,命名管道和无名管道,都可以交互式的在命令行或程序中使用它们;相关的例子在下面展示。这篇文章也将介绍内存队列,尽管它们有些过时了,但它们不应该受这样的待遇。

在本系列的第一篇文章中的示例代码承认了在 IPC 中可能受到竞争条件(不管是基于文件的还是基于内存的)的威胁。自然地我们也会考虑基于管道的 IPC 的安全并发问题,这个也将在本文中提及。针对管道和内存队列的例子将会使用 POSIX 推荐使用的 API,POSIX 的一个核心目标就是线程安全。

请查看一些 mq\_open 函数的 man 页,这个函数属于内存队列的 API。这个 man 页中有关 特性 的章节带有一个小表格:

接口特性
mq_open()线程安全MT-Safe

上面的 MT-Safe(MT 指的是 多线程 multi-threaded )意味着 mq_open 函数是线程安全的,进而暗示是进程安全的:一个进程的执行和它的一个线程执行的过程类似,假如竞争条件不会发生在处于相同进程的线程中,那么这样的条件也不会发生在处于不同进程的线程中。MT-Safe 特性保证了调用 mq_open 时不会出现竞争条件。一般来说,基于通道的 IPC 是并发安全的,尽管在下面例子中会出现一个有关警告的注意事项。

无名管道

首先让我们通过一个特意构造的命令行例子来展示无名管道是如何工作的。在所有的现代系统中,符号 | 在命令行中都代表一个无名管道。假设我们的命令行提示符为 %,接下来考虑下面的命令:

## 写入方在 | 左边,读取方在右边
% sleep 5 | echo "Hello, world!" 

sleepecho 程序以不同的进程执行,无名管道允许它们进行通信。但是上面的例子被特意设计为没有通信发生。问候语 “Hello, world!” 出现在屏幕中,然后过了 5 秒后,命令行返回,暗示 sleepecho 进程都已经结束了。这期间发生了什么呢?

在命令行中的竖线 | 的语法中,左边的进程(sleep)是写入方,右边的进程(echo)为读取方。默认情况下,读取方将会阻塞,直到从通道中能够读取到字节数据,而写入方在写完它的字节数据后,将发送 流已终止 end-of-stream 的标志。(即便写入方过早终止了,一个流已终止的标志还是会发给读取方。)无名管道将保持到写入方和读取方都停止的那个时刻。

在上面的例子中,sleep 进程并没有向通道写入任何的字节数据,但在 5 秒后就终止了,这时将向通道发送一个流已终止的标志。与此同时,echo 进程立即向标准输出(屏幕)写入问候语,因为这个进程并不从通道中读入任何字节,所以它并没有等待。一旦 sleepecho 进程都终止了,不会再用作通信的无名管道将会消失然后返回命令行提示符。

下面这个更加实用的示例将使用两个无名管道。我们假定文件 test.dat 的内容如下:

this
is
the
way
the
world
ends

下面的命令:

% cat test.dat | sort | uniq

会将 cat 连接 concatenate 的缩写)进程的输出通过管道传给 sort 进程以生成排序后的输出,然后将排序后的输出通过管道传给 uniq 进程以消除重复的记录(在本例中,会将两次出现的 “the” 缩减为一个):

ends
is
the
this
way
world

下面展示的情景展示的是一个带有两个进程的程序通过一个无名管道通信来进行通信。

示例 1. 两个进程通过一个无名管道来进行通信

#include <sys/wait.h> /* wait */
#include <stdio.h>
#include <stdlib.h>   /* exit functions */
#include <unistd.h>   /* read, write, pipe, _exit */
#include <string.h>

#define ReadEnd  0
#define WriteEnd 1

void report_and_exit(const char* msg) {
  perror(msg);
  exit(-1);    /** failure **/
}

int main() {
  int pipeFDs[2]; /* two file descriptors */
  char buf;       /* 1-byte buffer */
  const char* msg = "Nature's first green is gold\n"; /* bytes to write */

  if (pipe(pipeFDs) < 0) report_and_exit("pipeFD");
  pid_t cpid = fork();                                /* fork a child process */
  if (cpid < 0) report_and_exit("fork");              /* check for failure */

  if (0 == cpid) {    /*** child ***/                 /* child process */
    close(pipeFDs[WriteEnd]);                         /* child reads, doesn't write */

    while (read(pipeFDs[ReadEnd], &buf, 1) > 0)       /* read until end of byte stream */
      write(STDOUT_FILENO, &buf, sizeof(buf));        /* echo to the standard output */

    close(pipeFDs[ReadEnd]);                          /* close the ReadEnd: all done */
    _exit(0);                                         /* exit and notify parent at once  */
  }
  else {              /*** parent ***/
    close(pipeFDs[ReadEnd]);                          /* parent writes, doesn't read */

    write(pipeFDs[WriteEnd], msg, strlen(msg));       /* write the bytes to the pipe */
    close(pipeFDs[WriteEnd]);                         /* done writing: generate eof */

    wait(NULL);                                       /* wait for child to exit */
    exit(0);                                          /* exit normally */
  }
  return 0;
}

上面名为 pipeUN 的程序使用系统函数 fork 来创建一个进程。尽管这个程序只有一个单一的源文件,在它正确执行的情况下将会发生多进程的情况。

下面的内容是对库函数 fork 如何工作的一个简要回顾:

  • fork 函数由进程调用,在失败时返回 -1 给父进程。在 pipeUN 这个例子中,相应的调用是:
pid_t cpid = fork(); /* called in parent */

函数调用后的返回值也被保存下来了。在这个例子中,保存在整数类型 pid_t 的变量 cpid 中。(每个进程有它自己的进程 ID,这是一个非负的整数,用来标记进程)。复刻一个新的进程可能会因为多种原因而失败,包括进程表满了的原因,这个结构由系统维持,以此来追踪进程状态。明确地说,僵尸进程假如没有被处理掉,将可能引起进程表被填满的错误。

  • 假如 fork 调用成功,则它将创建一个新的子进程,向父进程返回一个值,向子进程返回另外的一个值。在调用 fork 后父进程和子进程都将执行相同的代码。(子进程继承了到此为止父进程中声明的所有变量的拷贝),特别地,一次成功的 fork 调用将返回如下的东西:
+ 向子进程返回 `0`
+ 向父进程返回子进程的进程 ID
  • 在一次成功的 fork 调用后,一个 if/else 或等价的结构将会被用来隔离针对父进程和子进程的代码。在这个例子中,相应的声明为:
if (0 == cpid) { /*** child ***/
...
}
else { /*** parent ***/
...
} 

假如成功地复刻出了一个子进程,pipeUN 程序将像下面这样去执行。在一个整数的数列里:

int pipeFDs[2]; /* two file descriptors */

来保存两个文件描述符,一个用来向管道中写入,另一个从管道中写入。(数组元素 pipeFDs[0] 是读端的文件描述符,元素 pipeFDs[1] 是写端的文件描述符。)在调用 fork 之前,对系统 pipe 函数的成功调用,将立刻使得这个数组获得两个文件描述符:

if (pipe(pipeFDs) < 0) report_and_exit("pipeFD");

父进程和子进程现在都有了文件描述符的副本。但分离关注点模式意味着每个进程恰好只需要一个描述符。在这个例子中,父进程负责写入,而子进程负责读取,尽管这样的角色分配可以反过来。在 if 子句中的第一个语句将用于关闭管道的读端:

close(pipeFDs[WriteEnd]); /* called in child code */

在父进程中的 else 子句将会关闭管道的读端:

close(pipeFDs[ReadEnd]); /* called in parent code */

然后父进程将向无名管道中写入某些字节数据(ASCII 代码),子进程读取这些数据,然后向标准输出中回放它们。

在这个程序中还需要澄清的一点是在父进程代码中的 wait 函数。一旦被创建后,子进程很大程度上独立于它的父进程,正如简短的 pipeUN 程序所展示的那样。子进程可以执行任意的代码,而它们可能与父进程完全没有关系。但是,假如当子进程终止时,系统将会通过一个信号来通知父进程。

要是父进程在子进程之前终止又该如何呢?在这种情形下,除非采取了预防措施,子进程将会变成在进程表中的一个僵尸进程。预防措施有两大类型:第一种是让父进程去通知系统,告诉系统它对子进程的终止没有任何兴趣:

signal(SIGCHLD, SIG_IGN); /* in parent: ignore notification */

第二种方法是在子进程终止时,让父进程执行一个 wait。这样就确保了父进程可以独立于子进程而存在。在 pipeUN 程序中使用了第二种方法,其中父进程的代码使用的是下面的调用:

wait(NULL); /* called in parent */

这个对 wait 的调用意味着一直等待直到任意一个子进程的终止发生,因此在 pipeUN 程序中,只有一个子进程。(其中的 NULL 参数可以被替换为一个保存有子程序退出状态的整数变量的地址。)对于更细粒度的控制,还可以使用更灵活的 waitpid 函数,例如特别指定多个子进程中的某一个。

pipeUN 将会采取另一个预防措施。当父进程结束了等待,父进程将会调用常规的 exit 函数去退出。对应的,子进程将会调用 _exit 变种来退出,这类变种将快速跟踪终止相关的通知。在效果上,子进程会告诉系统立刻去通知父进程它的这个子进程已经终止了。

假如两个进程向相同的无名管道中写入内容,字节数据会交错吗?例如,假如进程 P1 向管道写入内容:

foo bar

同时进程 P2 并发地写入:

baz baz

到相同的管道,最后的结果似乎是管道中的内容将会是任意错乱的,例如像这样:

baz foo baz bar

只要没有写入超过 PIPE_BUF 字节,POSIX 标准就能确保写入不会交错。在 Linux 系统中, PIPE_BUF 的大小是 4096 字节。对于管道我更喜欢只有一个写入方和一个读取方,从而绕过这个问题。

命名管道

无名管道没有备份文件:系统将维持一个内存缓存来将字节数据从写方传给读方。一旦写方和读方终止,这个缓存将会被回收,进而无名管道消失。相反的,命名管道有备份文件和一个不同的 API。

下面让我们通过另一个命令行示例来了解命名管道的要点。下面是具体的步骤:

  • 开启两个终端。这两个终端的工作目录应该相同。
  • 在其中一个终端中,键入下面的两个命令(命令行提示符仍然是 %,我的注释以 ## 打头。):
% mkfifo tester ## 创建一个备份文件,名为 tester
% cat tester    ## 将管道的内容输出到 stdout 

在最开始,没有任何东西会出现在终端中,因为到现在为止没有在命名管道中写入任何东西。

  • 在第二个终端中输入下面的命令:
% cat > tester ## redirect keyboard input to the pipe
hello, world!  ## then hit Return key
bye, bye       ## ditto
<Control-C>    ## terminate session with a Control-C

无论在这个终端中输入什么,它都会在另一个终端中显示出来。一旦键入 Ctrl+C,就会回到正常的命令行提示符,因为管道已经被关闭了。

  • 通过移除实现命名管道的文件来进行清理:
% unlink tester

正如 mkfifo 程序的名字所暗示的那样,命名管道也被叫做 FIFO,因为第一个进入的字节,就会第一个出,其他的类似。有一个名为 mkfifo 的库函数,用它可以在程序中创建一个命名管道,它将在下一个示例中被用到,该示例由两个进程组成:一个向命名管道写入,而另一个从该管道读取。

示例 2. fifoWriter 程序

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>

#define MaxLoops         12000   /* outer loop */
#define ChunkSize           16   /* how many written at a time */
#define IntsPerChunk         4   /* four 4-byte ints per chunk */
#define MaxZs              250   /* max microseconds to sleep */

int main() {
  const char* pipeName = "./fifoChannel";
  mkfifo(pipeName, 0666);                      /* read/write for user/group/others */
  int fd = open(pipeName, O_CREAT | O_WRONLY); /* open as write-only */
  if (fd < 0) return -1;                       /* can't go on */

  int i;
  for (i = 0; i < MaxLoops; i++) {          /* write MaxWrites times */
    int j;
    for (j = 0; j < ChunkSize; j++) {       /* each time, write ChunkSize bytes */
      int k;
      int chunk[IntsPerChunk];
      for (k = 0; k < IntsPerChunk; k++)
        chunk[k] = rand();
      write(fd, chunk, sizeof(chunk));
    }
    usleep((rand() % MaxZs) + 1);           /* pause a bit for realism */
  }

  close(fd);           /* close pipe: generates an end-of-stream marker */
  unlink(pipeName);    /* unlink from the implementing file */
  printf("%i ints sent to the pipe.\n", MaxLoops * ChunkSize * IntsPerChunk);

  return 0;
}

上面的 fifoWriter 程序可以被总结为如下:

  • 首先程序创建了一个命名管道用来写入数据:
mkfifo(pipeName, 0666); /* read/write perms for user/group/others */
int fd = open(pipeName, O_CREAT | O_WRONLY);

其中的 pipeName 是备份文件的名字,传递给 mkfifo 作为它的第一个参数。接着命名管道通过我们熟悉的 open 函数调用被打开,而这个函数将会返回一个文件描述符。

  • 在实现层面上,fifoWriter 不会一次性将所有的数据都写入,而是写入一个块,然后休息随机数目的微秒时间,接着再循环往复。总的来说,有 768000 个 4 字节整数值被写入到命名管道中。
  • 在关闭命名管道后,fifoWriter 也将使用 unlink 取消对该文件的连接。
close(fd); /* close pipe: generates end-of-stream marker */
unlink(pipeName); /* unlink from the implementing file */

一旦连接到管道的每个进程都执行了 unlink 操作后,系统将回收这些备份文件。在这个例子中,只有两个这样的进程 fifoWriterfifoReader,它们都做了 unlink 操作。

这个两个程序应该在不同终端的相同工作目录中执行。但是 fifoWriter 应该在 fifoReader 之前被启动,因为需要 fifoWriter 去创建管道。然后 fifoReader 才能够获取到刚被创建的命名管道。

示例 3. fifoReader 程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

unsigned is_prime(unsigned n) { /* not pretty, but efficient */
  if (n <= 3) return n > 1;
  if (0 == (n % 2) || 0 == (n % 3)) return 0;

  unsigned i;
  for (i = 5; (i * i) <= n; i += 6)
    if (0 == (n % i) || 0 == (n % (i + 2))) return 0;

  return 1; /* found a prime! */
}

int main() {
  const char* file = "./fifoChannel";
  int fd = open(file, O_RDONLY);
  if (fd < 0) return -1; /* no point in continuing */
  unsigned count = 0, total = 0, primes_count = 0;

  while (1) {
    int next;
    int i;

    ssize_t count = read(fd, &next, sizeof(int));
    if (0 == count) break;                  /* end of stream */
    else if (count == sizeof(int)) {        /* read a 4-byte int value */
      total++;
      if (is_prime(next)) primes_count++;
    }
  }

  close(fd);       /* close pipe from read end */
  unlink(file);    /* unlink from the underlying file */
  printf("Received ints: %u, primes: %u\n", total, primes_count);

  return 0;
}

上面的 fifoReader 的内容可以总结为如下:

  • 因为 fifoWriter 已经创建了命名管道,所以 fifoReader 只需要利用标准的 open 调用来通过备份文件来获取到管道中的内容:
const char* file = "./fifoChannel";
int fd = open(file, O_RDONLY);

这个文件的是以只读打开的。

  • 然后这个程序进入一个潜在的无限循环,在每次循环时,尝试读取 4 字节的块。read 调用:
ssize_t count = read(fd, &next, sizeof(int));

返回 0 来暗示该流的结束。在这种情况下,fifoReader 跳出循环,关闭命名管道,并在终止前 unlink 备份文件。

  • 在读入 4 字节整数后,fifoReader 检查这个数是否为质数。这个操作代表了一个生产级别的读取器可能在接收到的字节数据上执行的逻辑操作。在示例运行中,在接收到的 768000 个整数中有 37682 个质数。

重复运行示例, fifoReader 将成功地读取 fifoWriter 写入的所有字节。这不是很让人惊讶的。这两个进程在相同的机器上执行,从而可以不用考虑网络相关的问题。命名管道是一个可信且高效的 IPC 机制,因而被广泛使用。

下面是这两个程序的输出,它们在不同的终端中启动,但处于相同的工作目录:

% ./fifoWriter
768000 ints sent to the pipe.
###
% ./fifoReader
Received ints: 768000, primes: 37682

消息队列

管道有着严格的先入先出行为:第一个被写入的字节将会第一个被读,第二个写入的字节将第二个被读,以此类推。消息队列可以做出相同的表现,但它又足够灵活,可以使得字节块可以不以先入先出的次序来接收。

正如它的名字所提示的那样,消息队列是一系列的消息,每个消息包含两部分:

  • 荷载,一个字节序列(在 C 中是 char)
  • 类型,以一个正整数值的形式给定,类型用来分类消息,为了更灵活的回收

看一下下面对一个消息队列的描述,每个消息由一个整数类型标记:

          +-+    +-+    +-+    +-+
sender--->|3|--->|2|--->|2|--->|1|--->receiver
          +-+    +-+    +-+    +-+

在上面展示的 4 个消息中,标记为 1 的是开头,即最接近接收端,然后另个标记为 2 的消息,最后接着一个标记为 3 的消息。假如按照严格的 FIFO 行为执行,消息将会以 1-2-2-3 这样的次序被接收。但是消息队列允许其他收取次序。例如,消息可以被接收方以 3-2-1-2 的次序接收。

mqueue 示例包含两个程序,sender 将向消息队列中写入数据,而 receiver 将从这个队列中读取数据。这两个程序都包含的头文件 queue.h 如下所示:

示例 4. 头文件 queue.h

#define ProjectId 123
#define PathName  "queue.h" /* any existing, accessible file would do */
#define MsgLen    4
#define MsgCount  6

typedef struct { 
  long type;                 /* must be of type long */ 
  char payload[MsgLen + 1];  /* bytes in the message */  
} queuedMessage;

上面的头文件定义了一个名为 queuedMessage 的结构类型,它带有 payload(字节数组)和 type(整数)这两个域。该文件也定义了一些符号常数(使用 #define 语句),前两个常数被用来生成一个 key,而这个 key 反过来被用来获取一个消息队列的 ID。ProjectId 可以是任何正整数值,而 PathName 必须是一个存在的、可访问的文件,在这个示例中,指的是文件 queue.h。在 senderreceiver 中,它们都有的设定语句为:

key_t key = ftok(PathName, ProjectId); /* generate key */
int qid = msgget(key, 0666 | IPC_CREAT); /* use key to get queue id */

ID qid 在效果上是消息队列文件描述符的对应物。

示例 5. sender 程序

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include "queue.h"

void report_and_exit(const char* msg) {
  perror(msg);
  exit(-1); /* EXIT_FAILURE */
}

int main() {
  key_t key = ftok(PathName, ProjectId);
  if (key < 0) report_and_exit("couldn't get key...");

  int qid = msgget(key, 0666 | IPC_CREAT);
  if (qid < 0) report_and_exit("couldn't get queue id...");

  char* payloads[] = {"msg1", "msg2", "msg3", "msg4", "msg5", "msg6"};
  int types[] = {1, 1, 2, 2, 3, 3}; /* each must be > 0 */
  int i;
  for (i = 0; i < MsgCount; i++) {
    /* build the message */
    queuedMessage msg;
    msg.type = types[i];
    strcpy(msg.payload, payloads[i]);

    /* send the message */
    msgsnd(qid, &msg, sizeof(msg), IPC_NOWAIT); /* don't block */
    printf("%s sent as type %i\n", msg.payload, (int) msg.type);
  }
  return 0;
}

上面的 sender 程序将发送出 6 个消息,每两个为一个类型:前两个是类型 1,接着的连个是类型 2,最后的两个为类型 3。发送的语句:

msgsnd(qid, &msg, sizeof(msg), IPC_NOWAIT);

被配置为非阻塞的(IPC_NOWAIT 标志),是因为这里的消息体量上都很小。唯一的危险在于一个完整的序列将可能导致发送失败,而这个例子不会。下面的 receiver 程序也将使用 IPC_NOWAIT 标志来接收消息。

示例 6. receiver 程序

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include "queue.h"

void report_and_exit(const char* msg) {
  perror(msg);
  exit(-1); /* EXIT_FAILURE */
}

int main() {
  key_t key= ftok(PathName, ProjectId); /* key to identify the queue */
  if (key < 0) report_and_exit("key not gotten...");

  int qid = msgget(key, 0666 | IPC_CREAT); /* access if created already */
  if (qid < 0) report_and_exit("no access to queue...");

  int types[] = {3, 1, 2, 1, 3, 2}; /* different than in sender */
  int i;
  for (i = 0; i < MsgCount; i++) {
    queuedMessage msg; /* defined in queue.h */
    if (msgrcv(qid, &msg, sizeof(msg), types[i], MSG_NOERROR | IPC_NOWAIT) < 0)
      puts("msgrcv trouble...");
    printf("%s received as type %i\n", msg.payload, (int) msg.type);
  }

  /** remove the queue **/
  if (msgctl(qid, IPC_RMID, NULL) < 0)  /* NULL = 'no flags' */
    report_and_exit("trouble removing queue...");

  return 0;
}

这个 receiver 程序不会创建消息队列,尽管 API 尽管建议那样。在 receiver 中,对

int qid = msgget(key, 0666 | IPC_CREAT);

的调用可能因为带有 IPC_CREAT 标志而具有误导性,但是这个标志的真实意义是如果需要就创建,否则直接获取sender 程序调用 msgsnd 来发送消息,而 receiver 调用 msgrcv 来接收它们。在这个例子中,sender 以 1-1-2-2-3-3 的次序发送消息,但 receiver 接收它们的次序为 3-1-2-1-3-2,这显示消息队列没有被严格的 FIFO 行为所拘泥:

% ./sender
msg1 sent as type 1
msg2 sent as type 1
msg3 sent as type 2
msg4 sent as type 2
msg5 sent as type 3
msg6 sent as type 3

% ./receiver
msg5 received as type 3
msg1 received as type 1
msg3 received as type 2
msg2 received as type 1
msg6 received as type 3
msg4 received as type 2

上面的输出显示 senderreceiver 可以在同一个终端中启动。输出也显示消息队列是持久的,即便 sender 进程在完成创建队列、向队列写数据、然后退出的整个过程后,该队列仍然存在。只有在 receiver 进程显式地调用 msgctl 来移除该队列,这个队列才会消失:

if (msgctl(qid, IPC_RMID, NULL) < 0) /* remove queue */

总结

管道和消息队列的 API 在根本上来说都是单向的:一个进程写,然后另一个进程读。当然还存在双向命名管道的实现,但我认为这个 IPC 机制在它最为简单的时候反而是最佳的。正如前面提到的那样,消息队列已经不大受欢迎了,尽管没有找到什么特别好的原因来解释这个现象;而队列仍然是 IPC 工具箱中的一个工具。这个快速的 IPC 工具箱之旅将以第 3 部分(通过套接字和信号来示例 IPC)来终结。


via: https://opensource.com/article/19/4/interprocess-communication-linux-channels

作者:Marty Kalin 选题:lujun9972 译者:FSSlc 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

加密你的数据并使其免受攻击者的攻击。

密码学俱乐部的第一条规则是:永远不要自己发明密码系统。密码学俱乐部的第二条规则是:永远不要自己实现密码系统:在现实世界中,在实现以及设计密码系统阶段都找到过许多漏洞。

Python 中的一个有用的基本加密库就叫做 cryptography。它既是一个“安全”方面的基础库,也是一个“危险”层。“危险”层需要更加小心和相关的知识,并且使用它很容易出现安全漏洞。在这篇介绍性文章中,我们不会涵盖“危险”层中的任何内容!

cryptography 库中最有用的高级安全功能是一种 Fernet 实现。Fernet 是一种遵循最佳实践的加密缓冲区的标准。它不适用于非常大的文件,如千兆字节以上的文件,因为它要求你一次加载要加密或解密的内容到内存缓冲区中。

Fernet 支持 对称 symmetric (即 密钥 secret key )加密方式*:加密和解密使用相同的密钥,因此必须保持安全。

生成密钥很简单:

>>> k = fernet.Fernet.generate_key()
>>> type(k)
<class 'bytes'>

这些字节可以写入有适当权限的文件,最好是在安全的机器上。

有了密钥后,加密也很容易:

>>> frn = fernet.Fernet(k)
>>> encrypted = frn.encrypt(b"x marks the spot")
>>> encrypted[:10]
b'gAAAAABb1'

如果在你的机器上加密,你会看到略微不同的值。不仅因为(我希望)你生成了和我不同的密钥,而且因为 Fernet 将要加密的值与一些随机生成的缓冲区连接起来。这是我之前提到的“最佳实践”之一:它将阻止对手分辨哪些加密值是相同的,这有时是攻击的重要部分。

解密同样简单:

>>> frn = fernet.Fernet(k)
>>> frn.decrypt(encrypted)
b'x marks the spot'

请注意,这仅加密和解密字节串。为了加密和解密文本串,通常需要对它们使用 UTF-8 进行编码和解码。

20 世纪中期密码学最有趣的进展之一是 公钥 public key 加密。它可以在发布加密密钥的同时而让解密密钥保持保密。例如,它可用于保存服务器使用的 API 密钥:服务器是唯一可以访问解密密钥的一方,但是任何人都可以保存公共加密密钥。

虽然 cryptography 没有任何支持公钥加密的安全功能,但 PyNaCl 库有。PyNaCl 封装并提供了一些很好的方法来使用 Daniel J. Bernstein 发明的 NaCl 加密系统。

NaCl 始终同时 加密 encrypt 签名 sign 或者同时 解密 decrypt 验证签名 verify signature 。这是一种防止 基于可伸缩性 malleability-based 的攻击的方法,其中攻击者会修改加密值。

加密是使用公钥完成的,而签名是使用密钥完成的:

>>> from nacl.public import PrivateKey, PublicKey, Box
>>> source = PrivateKey.generate()
>>> with open("target.pubkey", "rb") as fpin:
... target_public_key = PublicKey(fpin.read())
>>> enc_box = Box(source, target_public_key)
>>> result = enc_box.encrypt(b"x marks the spot")
>>> result[:4]
b'\xe2\x1c0\xa4'

解密颠倒了角色:它需要私钥进行解密,需要公钥验证签名:

>>> from nacl.public import PrivateKey, PublicKey, Box
>>> with open("source.pubkey", "rb") as fpin:
... source_public_key = PublicKey(fpin.read())
>>> with open("target.private_key", "rb") as fpin:
... target = PrivateKey(fpin.read())
>>> dec_box = Box(target, source_public_key)
>>> dec_box.decrypt(result)
b'x marks the spot'

最后,PocketProtector 库构建在 PyNaCl 之上,包含完整的密钥管理方案。


via: https://opensource.com/article/19/4/cryptography-python

作者:Moshe Zadka 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出