cayley图数据库

郭柱明


图数据库背景

在此之前已经简单记录了一下图数据的背景和发展现状 图数据引擎那部分现在还是知之甚少 还没深入去了解
图数据库背景


cayley优点

  • go语言实现
  • 运行简单(三四条命令)
  • 支持http接口以及REPL交互式
  • 支持多种语言进行查询
    • Gizmo
    • MQL
    • GraphQL
  • 支持多种后端存储
    • KVs: Bolt, LevelDB
    • NoSQL: MongoDB, ElasticSearch, CouchDB/PouchDB
    • SQL: PostgreSQL, CockroachDB, MySQL
    • n-memory, ephemeral
  • 模块化编程 容易拓展
  • 良好的测试覆盖
  • 速度快
  • 免费

运行和应用

这里真的要非常注意 因为cayley提供的官方文档相当有限 会导致很多新手搞不清到底怎么使用这个图数据

cayley使用两种方式

  • 作为应用运行使用
  • 作为第三方库运行使用

作为应用运行使用

下面是官方文档中作为应用进行运行使用的部分
参考链接

  1. 下载二进制文件解压或者下载源码进行编译
  2. 配置
  3. 导入数据
  4. 运行

运行使用一下命令

./cayley http --config=cayley.yml --host=0.0.0.0:64210

或者在Linux系统下后台运行

./cayley http --config=cayley.yml --host=0.0.0.0:64210 &

可以看到已经在后台运行 监听64210端口
image_1ci3f3d0g1hg21bt411nh4f04r09.png-26.4kB

上面这种运行方式是使用一个go原生net/http架起一个web服务器 以方便开放人员作为管理员可以在web界面上使用UI进行数据库的增删查找 如下
image_1ci24pfveosv1hudifc10ivmcl9.png-149.3kB

但是 对于这个http接口 官方并没有提供进一步的封装 意味着如果你要使用这种方式运行cayley 并且在你的项目中连接这个数据库 那么你是需要自己拼凑我们用来增删查找的Gizmo语句(如果选择使用Gizmo) 类似以下

1
2
3
g.V().Has("<name>","Casablanca")
.Out("</film/film/starring>").Out("</film/performance/actor>")
.Out("<name>").All()

然后post给服务端上的cayley

这就意味着你需要进一步封装这个http接口 就跟写一个爬虫一样 虽然在Python上已经有人做好了这部分工作
pyley一个封装了cayley http接口的客户端

但是go语言我目前还没发现有类似的 官方亦然


作为第三方库运行使用

我个人推荐将cayley作为第三方库来进行运行 一个是go get安装cayley第三方库的时候就顺带安装了cayley 因为cayley本身就是go编写的 但是这方面的使用官方文档上简直坑得不行 仅仅提供了几个hello world 对刚接触的开发者来说真的非常不友好

官方提供的几个example

hello_worldtransaction都是使用

cayley.NewMemoryGraph()

这个api使用内存作为临时的存储位置进行存储的

hello_bolthello_schema都是使用一个叫bolt的键值对数据库进行存储的 这个需要一个路径来存存储着数据的文件

除此之外 官方文档就没提供太多 实际在go语言中链接cayley的方式了

但是我们小组的领导决定使用mongodb作为backend进行存储 上面的例子都没有太大的实际应用意义

我参照了bolt作为backend的例子修改了一下 获得了使用mongodb作为backend的方法

注释已经将每一部分讲的很清楚了 所以就不说其他什么了

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
56
57
58
59
package main

import (
"context"
"fmt"
"log"
"github.com/cayleygraph/cayley"
"github.com/cayleygraph/cayley/graph"
//下面这个import必不可少 因为在这个import这个库的时候 它的init函数会注册这个类型的数据库 允许使用这个数据库进行存储
_ "github.com/cayleygraph/cayley/graph/nosql/mongo"
"github.com/cayleygraph/cayley/quad"
)

func main() {

// 初始化数据库
err := graph.InitQuadStore("mongo", "mongodb://120.92.100.60:27017", nil)
if err != nil {
log.Fatal(err)
}

//打开和使用数据库
store, err := cayley.NewGraph("mongo", "mongodb://120.92.100.60:27017", nil)
if err != nil {
log.Fatalln(err)
}

//给数据库添加一个quad实例 phrase of the day is of course Hello BoltDB! demo graph
//主语 phrase of the day
//谓语 is of course
//宾语 Hello BoltDB!
//标签 demo graph
store.AddQuad(quad.Make("phrase of the day", "is of course", "Hello BoltDB!", "demo graph"))

// 创建一个用于查找的path
p := cayley.StartPath(store, quad.String("phrase of the day")).Out(quad.String("is of course"))

//下面的查询是跟hello world里面是差不多的 但是更先进一点
//获取这个path的迭代器 并且优化它
it, _ := p.BuildIterator().Optimize()

//在quad层面进行优化 之后这个迭代器就被指定了backend的迭代器取代了
it, _ = store.OptimizeIterator(it)

// 关闭迭代器
defer it.Close()

ctx := context.TODO()
// 当path上还有下一个
for it.Next(ctx) {
token := it.Result() // 获取节点的一个引用
value := store.NameOf(token) // 获取节点的值
nativeValue := quad.NativeOf(value) // 将上面获取的节点的值转化为正常的数
fmt.Println(nativeValue)
}
if err := it.Err(); err != nil {
log.Fatalln(err)
}
}

输出
image_1ci3gj9n4178dooh1a7g166fsbbm.png-24.1kB


事务

  1. 初始化数据库获取一个handle
  2. 获取一个事务
  3. 添加多个四元组到事务中
  4. 将事务添加到handle中
  5. 查询跟之前没什么区别

image_1ci8s50oq9jqrrt82c1hqk19nq19.png-107.8kB


数据结构与api

1.添加四元组的方式
image_1ci99nqqkm61l077bd12fj1ng1m.png-113.8kB

2.更新或者删除四元组
cayley的设计中是没有更新四元组这个api 如果想要更新一个四元组只能先删除这个四元组 然后再重新添加一条新的四元组

image_1ci9habhi1iob1b5094s14k518t52j.png-105kB


数据集

image_1ci3jdcegdal17q8tga180t8at13.png-135.6kB

cayley api

四元组 含有主谓宾标签

1
2
3
func Quad(subject, predicate, object, label interface{}) quad.Quad {
return quad.Make(subject, predicate, object, label)
}

三元组 含有主谓宾 没有标签(标签为nil)

1
2
3
func Triple(subject, predicate, object interface{}) quad.Quad {
return Quad(subject, predicate, object, nil)
}

处理器handle

1
type Handle = graph.Handle

一个handle的结构是

1
2
3
4
type Handle struct {
QuadStore
QuadWriter
}

包含一个匿名的四元组存储成员 一个匿名的四元组写入成员

新建一个处理器

1
func NewGraph(name, dbpath string, opts graph.Options) (*Handle, error)

返回一个handle

新建一个内存存储的处理器

1
func NewMemoryGraph() (*Handle, error)

从它的具体实现 可以知道这基于memstore内存进行存储的处理器

1
2
3
func NewMemoryGraph() (*Handle, error) {
return NewGraph("memstore", "", nil)
}

迭代器

1
type Iterator = graph.Iterator

query path

1
type Path = path.Path


Gizmo

因为我们上面都是使用Gizmo作为查询语言进行查询的 所以很有必要记录一下Gizmo的基本语法

基本语句太多了 所以只简要记录一下常用的语句

1.
graph.M()

graph.Morphism()

的缩写 Morphism创建一条路径 这样允许Gizmo的路径可以被复用
类似如下

var shorterPath = graph.Morphism().Out("foo").Out("bar")

2.

graph.V(*)

graph.Vertex([nodeId],[nodeId]...)

的缩写 这个语句返回一个根据给定的点作为始点的query path 没有nodeID的话以为着所有的点


3.
Path object .Morphism()和.Vertex()和这个函数都可以创建一个query path对象


4.

path.And(path)

path.Intersect(path)

的缩写 就是逻辑与的意思
例子

1
g.V("<charlie>").Out("<follows>").And(g.V("<dani>").Out("<follows>")).All()


5.

path.As(tags)

path.Tag(tags)

的缩写
Tag保存一个list的nodes存到给定的tag 主要是为了保存我们的工作或者为了了解一个path是怎样到达终点的 我们需要这个tag来进行标志一下


6.

path.Back(tag)

跟tag来后退到tag上面去


7.

path.Both([predicatePath], [tags])

获取同时进来和出去的节点
例子

1
g.V("<fred>").Both("<follows>").All()


8.

path.Count()

返回结果的数量


9.

path.Difference(path)

path.Except(path)

的别名
Except移除当前path上匹配了这个query path删的所有paths


10.

path.Follow(path)

是一种使用.Morphism()准备好的query path的方式


11.

path.ForEach(callback) or (limit, callback)

遍历query path


12.

path.Has(predicate, object)

一般应用于所有节点或者一个堆节点按照某个谓语进行查询 例子

1
g.V().Has("<follows>", "<bob>").All()


13.

path.In([predicatePath], [tags])

in就比较直观了 就是按照这个谓语进来到这个节点

path.Out([predicatePath], [tags])

类似 就是按照这个谓语出来这个节点


14.

path.Or(path)

path.Union(path)

别名
这个也比较直观 就是逻辑或


15.

path.Save(predicate, tag)

保存所有使用这个谓语进入这个tag的节点 并且不遍历

图数据库

背景

互联网发展带来的问题

  1. 用户 系统和传感器的数据量呈指数增长
  2. 数据内部的依赖和复杂度迅速增加

数据库分类

  • sql
  • nosql

nosql是过去几年快速发展的
可以分类为

  • 键值存储库
  • BigTable实现
  • 文档库
  • 图形数据库

备注

BigTable是一种压缩的、高性能的、高可扩展性的,基于Google文件系统(Google File System,GFS)的数据存储系统,用于存储大规模结构化数据,适用于云端计算。优势在于扩展性和性能


图数据库表现方式

G = (V, E)

  • V表示定点
  • E表示边

表示图的两种常用方式

  • 邻接矩阵
  • 邻接表

image_1ci23g3nmnui1g6e13aeg4e1vih9.png-149.9kB

底层存储问题

  • 原生图存储
  • 非图后端

原生图村存储好处是专门为性能和拓展性建造的 占用内存巨大 而且在一些非遍历类查询上比较困难

相反 序列化之后再我们一些非常成熟的非图后端上可以非常有利于运维

beego学习2

beego执行逻辑

image_1cjtbnflsj961esq4ujqi66fm9.png-144.5kB


控制器

beego是严格遵循MVC的模式的 所以它的控制器是非常重要的 他的路由注册是有点特别的 它是一个URL前缀对应一个控制器 然后控制器里面定义不同的http methods对应的处理函数

一般声明一个控制器的方法就是自创建一个结构体 包含一个beego.Controller原生的控制器

现在我们来看一下控制器的内部

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
type Controller struct {
// 上下文数据
Ctx *context.Context
// 用来存储一些配置等信息非常方便
Data map[interface{}]interface{}

// 控制器的一些基本介绍信息 名字之类的
controllerName string
actionName string
methodMapping map[string]func() //method:routertree
gotofunc string
AppController interface{}

// 渲染模板信息 模板名字等 这个现在而言不是很重要
TplName string
ViewPath string
Layout string
LayoutSections map[string]string // the key is the section name and the value is the template name
TplPrefix string
TplExt string
EnableRender bool

// 跨域数据
_xsrfToken string
XSRFExpire int
EnableXSRF bool

// session数据
CruSession session.Store
}

所以一个beego控制器中含有以下几部分内容

  • 上下文数据
  • 控制器基本信息
  • 渲染模板信息
  • 跨域信息
  • session信息

json对象转struct

当路由的方法是post的时候 旺旺需要post一些json对象到后端 那么将他们转为json对象就尤为必要 但是当这个struct结构含有匿名函数的时候需要注意一下

1.当struct没有匿名成员的时候

一切跟平时将json转struct没什么区别 只注意要讲结构体内的成员首字母为大写来声明 不然没有导出的字段是无法被赋值上去的

2.当时结构体含有匿名成员的时候

我们要记住 其实匿名成员里面的所有字段都是直接属于最外面的结构体的 所以我们post表单的时候要注意 不需要给匿名成员提供key 直接将匿名成员里面的字段作为一对key-value对就行了

例如
结构体设计如下

1
2
3
4
5
6
7
8
9
type User struct {
Desc
Name string `json:"name"`
Id int `json:"id"`
}

type Desc struct {
Num int `json:"num"`
}

那么在postman这边需要这样提交post json对象
image_1ck1fk2v9194l1vln1s2mlbl1u349.png-20.5kB

结果
image_1ck1fkq7n1h2319mn1c2vdp6hmpm.png-9.6kB


copybody bug

这里真的是神坑 浪费了我很多时间 在beego的post路由函数中 我们一般是这样接受发送过来的json对象的

1
2
3
if err := json.Unmarshal(this.Ctx.Input.RequestBody, &f); err == nil {
//
}

但是有可能一直报错unexpected end of json data json对象的结构不对?但是postman上并没有显示报错啊这就很迷了

实质上是这是因为没有设置copyrequestbody = true 所以post过去的数据一直都是空的 但是我的配置文件上一直有这句话啊 比较坑的是 其实配置文件并没有被加载进去 beego没有加载了你的配置文件 而是加载了默认的配置文件 比较坑的是beego并没有报错说配置文件找不到 所以copyrequestbody一直为false

解决方法是在运行配置那里修改working directory修改为beego项目的根目录 这样beego就可以找到你的配置文件的正确目录了

真的是神坑 beego找不到配置文件目录竟然不会报错。。。


beego入门

入门部分

beego跟一般Python框架不怎样一样 它有controler控制器的概念

  • 每个url对应一个controller
  • controller可以从beego.Controller中继承过来
  • beego.Controller已经具有get post delete put等方法
  • 我们要注册视图函数的一般做法就是重写这些方法 来定义我们自己的视图函数

hello world

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "github.com/astaxie/beego"

//定义一个controller 继承beego.Controller
type MainController struct {
beego.Controller
}
//重写get函数
func (c *MainController) Get() {
c.Ctx.WriteString("lalalala")
}

func main() {
//注册路由
beego.Router("/", &MainController{})
//运行web app
beego.Run()
}


beego安装工具bee

跟其他web 框架一样 beego也是可以初始化一个后端应用的文件目录结构来方便开发者进行开发的

创建一个新的beego项目

bee new newProjectName

目录结构如下
image_1chuh0ikb124h12l81s1fum41o5pp.png-29.7kB

可以看到是包含static和views目录的 意味着这包含着前端部分的内容

创建一个api应用

bee api apiProjectName

目录结构如下 可以看到是没有静态文件和模板文件目录了
image_1chuh6345r681h5r1i261bl0hi7p.png-22.3kB

进入项目的根目录下运行

bee run

进行热编译 我也不知道热变异是什么

session和cookies关系和原理

前言

前几天思考了一下session跟cookies的关系和原理 发现以前对session跟cookies的理解一直都有误 以前我一直以为session是存在服务端 cookies存在客户端 他们的键值对是一一对应关系的 后来想了想 这其实是不完全对的


session和cookies的位置

之前我们说到我们需要一种跟踪和认证客户端的机制 这种机制在客户端处表现为cookies

cookies可以保存在浏览器缓存 磁盘中等等

这种机制在服务端表现为session

session可以保存在内存 缓存 memcache redis或者磁盘中等等

这些都是没问题的


cookies和session的对应关系

cookies和session上的键值对其实不是一一对应关系的 甚至可以说关系不大
我们使用java的servlet来举例子 就可以比较清楚的说明他们的关系

在我们服务端这边cookies和session不是一回事 而且它们都可以被我们服务端分别进行操作


设置cookies

在java servlet中 给http response增加一个键值对 {“login”; “mylogin”}

1
2
3
4
5
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// add cookies
Cookie login = new Cookie("login", "mylogin");
resp.addCookie(login);
}

客户端那边的效果 成功设置这个键值对到cookies中
image_1cg8recrtgbg1m201gvdvu29q39.png-3.3kB

cookies原理
image_1cg8s7r6mdbe7mf1pd51kqu2vv1g.png-18.1kB


设置session

然后我给session对象设置一个键值对 在java servlet中给session设置键值对叫做setAttribute 设置键值对{“login”: “this is my login session”}

1
2
3
4
5
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// add session attribute
HttpSession session = req.getSession();
session.setAttribute("login", "this is my login session");
}

然后我们在客户端的http response的cookies中发现新增了以下这个键值对
image_1cg8rn4b81fcg80l1epc1h181corm.png-4.8kB

只是新建了一个key为jsessionid的键值对 跟我们设置的session attribute毫无关系

其实事实是
服务端这边是很节省资源的 它不是建立TCP连接之后就直接给客户端简历一个session 而是执行到如下语句

1
HttpSession session = req.getSession();

这时候才给客户端新建一个session对象(如果没有session则新建一个session 如果有则返回) 这个session对象含有一个attribute map一个映射表来存储我们给session设置的一些键值对 当我们set attribute之后 这些键值对存在attribute map之中 并且会http response新建一个特别的键值对

这个键值对就是我们上面看到的那个跟我们设置的session attribute毫无关系的cookies键值对

image_1cg8rn4b81fcg80l1epc1h181corm.png-4.8kB
这个jsessionid的cookies值顾名思义就是在cookies中用来标记这个客户端属于哪个session的唯一属性

所以说我们设置的所有session attribute都是存在服务端的本地的 给客户端那边传送过去关于session那部分的内容仅仅只是一个用来标识客户端所属session的session id 而session的所有内容都是存在服务端本地 之后对于每次客户端的请求 服务端都可以根据它请求头的session id这个cookie值(名字因所用框架有别 上面使用servlet 是jsessionid)来证明它是那个session的 然后根据这个session id在内存缓存memcache或者redis中找到这个session 继而找到这个session用来存键值对(就是attributes)的attribute map 从而找到我们在session中设置的内容

所以说cookies和session的内容 本质上除了需要在cookies上设置一个session id的cookies值来标记客户端所属session 其他基本毫无关系的 更不存在以前理解的一一对应关系

session原理
image_1cg8u36631o2ft241indpi019qj2a.png-39.6kB

综上所述

  • cookies是一种存储在客户端的 对客户端进行跟踪和认证的机制
  • session是一种存储在服务端的 对客户端进行跟踪和认证的机制 session的设置需要给cookies设置一个特殊的sessionid字段值
  • 对于服务端开发者来说 这两种机制都是可选的 并且是可以结合使用的 但是他们之间不是一回事

servlet登录demo

前言

源代码地址
因为最近这段时间在看java web开发的一些东西 因为spring mvc框架是封装了servlet而来的 所以觉得很有必要学习一下servlet 以便后面如果学习spring mvc等框架的时候可以有助于理解 进而写了一个包含mysql连接 cookies设置 session attribute设置和检验的简单demo

servlet架构
servlet架构

servlet生命周期
image_1cg3glqm518pqbbo11jn190i145p4r.png-20.8kB

  1. 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。
  2. Servlet 容器在调用 service() 方法之前加载 Servlet。
  3. 然后 Servlet 容器处理由多个线程产生的多个请求,每个线程执行一个单一的 Servlet 实例的 service() 方法。

架构

使用了tomcat作为servlet容器

tomcat实现了对servlet和jsp的支持 并提供了作为web服务器的一些特有功能
如Tomcat管理和控制平台、安全域管理和Tomcat阀等 由于tomcat内置了一个http服务器 它也可以被单独视为一个web服务器
但是不能将tomcat和apache http web服务器混淆 这两个http web服务器不是捆绑在一起的
tomcat的可以通过xml文件格式的配置文件进行配置


在intellij idea进行tomcat server运行配置

这个在网上找到一个博客写的很详尽 这里就不重复了
tomcat server运行配置


login demo

文件结构
image_1cg3ea5e41vtr11mj1po41hps12e313.png-27kB

数据库
在mysql中设置一个如下结构的表(因为仅仅是写一个简单的登录demo 所以没有考虑密码哈希之类的问题)
image_1cg3e73p61b2dtiafpiejlbd99.png-93.1kB
添加一个实例
image_1cg3e861fmdd692ca71oup149pm.png-68.7kB

数据库连接

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
package servletPackage;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.ResultSet;

public class FindPsw {
// 加载JDBC引擎的路径
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
// 数据库链接的URI
static final String DB_URL = "jdbc:mysql://localhost:3306/javaTest";
// 连接数据库的用户名
static final String USER = "root";
// 数据库密码
static final String PSW = "Gzm20125";
public String getPsw(String name) {
// 数据库连接对象
Connection conn;
// 执行语句的statement对象 类似于python-connector的cursor
Statement stmt;
String psw;
try {
// 加载JDBC引擎
Class.forName(JDBC_DRIVER);
// 建立数据库链接
conn = DriverManager.getConnection(DB_URL, USER, PSW);
// 获取statement
stmt = conn.createStatement();
String stmtToExec = String.format("select psw from testUser where name = '%s';", name);
System.out.println("exec string is " + stmtToExec);
// 执行查询语句 返回结果
ResultSet rs = stmt.executeQuery(stmtToExec);
if (rs.next()) {
psw = rs.getString("psw");
}
else {
psw = "";
}
rs.close();
stmt.close();
conn.close();
return psw;
}catch (Exception e) {
e.printStackTrace();
return "";
}
}
}

登录页面

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
package servletPackage;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/* 声明/login资源 使用Login这个类作作为视图类 */
@WebServlet("/login")
public class Login extends HttpServlet {
// 重写doGet方法 处理get请求
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 在response中写入一个登录form
PrintWriter out = resp.getWriter();
out.println("<!DOCTYPE html>\n" +
"<html lang='en-US'>\n" +
"<head>\n" +
" <meta charset='utf-8'>\n" +
"</head>\n" +
"<body>\n" +
"<h1>login</h1>\n" +
"<form action='/postDeal' method='post'>\n" +
" <input type='text' name='user'>\n" +
"<br>" +
" <input type='text' name='password'>\n" +
"<br>" +
" <input type='submit'>\n" +
"</form>\n" +
"</body>\n" +
"</html>"
);
out.close();
}
}

表单 cookies session的处理

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
package servletPackage;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;


@WebServlet("/postDeal")
public class PostDeal extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 获取表单中的用户和密码
String user = req.getParameter("user");
String psw = req.getParameter("password");
// 使用刚刚数据库连接的类进行数据库查询 获取正确密码
FindPsw f = new FindPsw();
String rightPsw = f.getPsw(user);
System.out.println("right psw is " + rightPsw);
PrintWriter out = resp.getWriter();
// 如果密码正确
if (psw.equals(rightPsw)) {
// add cookies
Cookie login = new Cookie("login", "mylogin");
resp.addCookie(login);

// add session attribute
HttpSession session = req.getSession();
session.setAttribute("login", "this is my login session");

// add response text
out.println(
"<p>login successfully user " + user + " psw is " + psw + "</p>" +
"<a href='/checkSession'>this is a page to check whether set session attribute</a>"
);
}
else {
out.println("<p>psw is error</p>");
}
}

// 登录表单提交过来这个书图类之后 doPost函数首先接受到请求 随之转发给doGet函数 由doGet函数进行处理
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
doGet(req, resp);
}
}

检查session attribute是否被成功设置

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
package servletPackage;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

//这个视图用来测试客户端是否被成功设置了session attribute
@WebServlet("/checkSession")
public class checkSession extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 获取客户端的session
HttpSession session = req.getSession();
// 获取session attribute
String login = (String) session.getAttribute("login");
System.out.println("login session is " + login);
PrintWriter out = resp.getWriter();
out.println("login session is " + login);
out.close();
}
}


效果

登录

image_1cg3fmvge1e441h2c16ep19pvgb51g.png-19.2kB


登录成功

image_1cg3frqlm1r3bnr315g2oc8gdl1t.png-71.7kB


登录失败

image_1cg3fsovg1h0r6c8nj41rft1k8q2a.png-62.4kB


检测session attribute

image_1cg3ftt2f1jm9eo3phe12vvh0r2n.png-67.6kB

aiohttp session

cookie和session

服务端和客户端之间的联系一般是根据http协议来进行的 但是http协议是无状态的 无状态会有状态的概念大概如下

有状态

  1. 甲:你吃午饭了吗
  2. 乙:吃了
  3. 甲:吃了什么菜
  4. 乙:吃了鸡蛋

无状态

  1. 甲:你吃午饭了吗
  2. 乙:吃了
  3. 甲:吃了什么菜
  4. 乙:你说什么时候吃什么菜 早饭?午饭?还是晚饭

因此服务端和客户端之间需要一种机制来记录用户的状态 这种机制在客户端那边是cookies 在服务端那边是session
cookies可以存储在磁盘 内存里面 session可以存储在内存 缓存 redis甚至磁盘中


aiohttp session

flask框架具有内置的session机制 我以前用起来觉得非常方便以至于我觉得这个不重要 aiohttp不具有内置的session机制 但是具有第三方模块支持 aiohttp_session

安装这个第三方模块真的挺费事 因为依赖模块实在太多了 而且aiohttp_session是可以依赖redis作为缓存的 还需要安装redis和一些相关模块 下面的开始的实例我没有使用redis作为存储位置 而是使用了EncryptedCookieStorage 后面附加了使用redis存储session的例子

dependencies

  • python3.5.3+
  • cryptography
  • aioredis

安装模块

  • pip3 install cryptography(必要时进行upgrade 可能版本太低)
  • pip3 install aiohttp_session[secure]
  • pip3 install aiohttp_session[aioredis] (需要安装redis libhiredis-dev hiredis libssl-dev)
  • pip3 install aiohttp_session[aiomcache]
  • pip3 install aiohttp_sessionsss

    上面的安装问题真的太多 谷歌好几次才解决


存储位置选择

所有仓库是使用名为AIOHTTP_COOKIE_SESSION的对象来进行存储数据的

可以使用的仓库有

  • SimpleCookieStorage 把session数据以明文json字符串的格式存在cookies body中 只用来测试 非常不安全
  • EncryptedCookieStorage 存储数据在cookie中的格式跟SimpleCookieStorage一样 但是使用了cryptography的Fernet cipher加密算法进行加密
  • RedisStorage 存json化的数据到redis中 格式是name+reids key
  • MemcachedStorage 跟RedisStorage类似 但是使用memcache数据库进行存储

使用

使用上跟flask的session大同小异 但是需要在init app阶段将session安装到app上 设定一个32位bytes作为加密密钥 跟flask session的secret key类似

session是一个dict like的对象

设定session

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
from aiohttp import web
import datetime
from aiohttp_session import get_session

async def login(request):
engine = await aio_engine.init_engine()
data = await request.json()
print("data", data)
if "name" not in data or "psw" not in data:
return web.json_response({
"status": False
})
name = data["name"]
psw = data["psw"]
verify = await keeper.verify(engine, name = name, psw = psw)
if verify:
session = await get_session(request)
session["ooad"] = name
session["login"] = psw
session["time"] = str(datetime.datetime.now())
return web.json_response({
"status": True
})
return web.json_response({
"status": False
})

检查是否含有具体session字段值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from aiohttp import web
import datetime
from aiohttp_session import get_session

async def need_cookies_page(request):
engine = await aio_engine.init_engine()
session = await get_session(request)
if "ooad" not in session or "login" not in session or "time" not in session:
return web.json_response({
"status": False
})
name = session["ooad"]
psw = session["login"]
r = await keeper.verify(engine, name = name, psw = psw)
if r:
return web.json_response({
"status": True
})
return web.json_response({
"status": False
})
`

setup app

1
setup(app, EncryptedCookieStorage(b'Thirty  two  length  bytes  key.'))


效果

login result
image_1cfrgldqfjrht9n1gva149ocj423.png-72.7kB
login cookies
image_1cfrgm0d8nov172b2mj10ta4g62g.png-46.1kB
cookies check
image_1cfrgn8mh1vf717qu1elcqdv8ur2t.png-43.3kB


使用redis存储session

这里有个问题 就是aiohttp本事内置一个asyncio event loop执行所有跟app相关的协程了 但是当我们使用redis作为仓库进行存储session的时候需要使用到aioredis进行支持 但是这个也是异步io连接模块 因此我们初始化redis pool的时候也需要一个event loop进行运行这个初始化的协程 一旦这个event loop跟web.run_app混在一起就会报错 event loop is already run

正确的做法是分开 并且在执行redis pool连接的event loop run_until_complete处获取返回值 得到需要的storage

1
2
3
4
5
6
7
8
9
10
11
12
from aiohttp_session import setup, redis_storage
import aioredis
import asyncio

async def get_storage():
redis = await aioredis.create_pool(("localhost", 6379))
return redis_storage.RedisStorage(redis)

def setup_session_support(app):
storage = asyncio.get_event_loop().run_until_complete(get_storage())
setup(app, storage)
return app
1
app = session_redis.setup_session_support(app)

效果
可以看到生成的cookies值确实存在了redis中
postman中获取的cookies
image_1cfs5ia7pm0mke1hcn13edld9m.png-18.6kB
redis中也存有一样的cookies
image_1cfs5h8s6st01coa4491gh0bic9.png-20kB

aiohttp的跨域问题

跨域

以前使用flask框架的时候一直都是没有前后端分离 现在使用aiohttp框架 并且前端使用vue 前后端分离 因此出现了跨域的问题

不同浏览器对跨域的支持也是不一样的 支持跨域的浏览器主要有

  • Chrome 3+
  • Firefox 3.5+
  • Opera 12+
  • Safari 4+
  • Internet Explorer 8+

标准的跨域请求默认是不发送任何的cookies的 如果需要把cookies作为请求的一部分 需要将请求 比如js底层的XMLHttpRequest请求的withCredentials属性设置为true

xhr.withCredentials = true;

相应的服务端那边需要设置Access-Control-Allow-Credentials为true来允许cookies的传送

Access-Control-Allow-Credentials: true

withCredentials属性将让request包含来自远程域的任何cookies 同时它也设置任何来自远程域的cookies 要注意这些cookies还遵循同域标准的 因此你的javascrpt代码不能访问来自document.cookie或者responset header的cookies 这些cookies只可以被远程域控制

除了javascrip代码需要根据浏览器对cors支持程度不一样需要恰当的作出跨域请求 例如如下代码

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
// Create the XHR object.
function createCORSRequest(method, url) {
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr) {
// XHR for Chrome/Firefox/Opera/Safari.
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined") {
// XDomainRequest for IE.
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
// CORS not supported.
xhr = null;
}
return xhr;
}

// Helper method to parse the title tag from the response.
function getTitle(text) {
return text.match('<title>(.*)?</title>')[1];
}

// Make the actual CORS request.
function makeCorsRequest() {
// This is a sample server that supports CORS.
var url = 'http://html5rocks-cors.s3-website-us-east-1.amazonaws.com/index.html';

var xhr = createCORSRequest('GET', url);
if (!xhr) {
alert('CORS not supported');
return;
}

// Response handlers.
xhr.onload = function() {
var text = xhr.responseText;
var title = getTitle(text);
alert('Response from CORS request to ' + url + ': ' + title);
};

xhr.onerror = function() {
alert('Woops, there was an error making the request.');
};

xhr.send();
}

服务端也需要一定的cors支持
在客户端和服务端之间的传输 一般客户端需要作出一些附加的headers和附加的请求 这些请求一般是隐藏的 但是可以抓包发现
image_1cfk1f1dvn7bpjdc6k3q61gc3m.png-29.4kB

跨域请求分类

  1. 简单跨域请求
  2. 不简单跨域请求

处理简单跨域请求

  • HTTP方法是下面其中一种
    • HEAD
    • GET
    • POST
  • HTTP头部是
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type, 但是仅仅当值为
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

简单跨域请求这样命名的原因是因为他们可以让浏览器不做处跨域请求也可以访问到 比如JOPN请求或者html的表单form 这些情况不存在跨域的问题

JSONP是什么

一个有效的服务器response

1
2
3
4
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

其中以下是跨域请求必须的

  • Access-Control-Allow-Origin

Access-Control-Allow-Origin这个属性必须在server response的headers中 这个属性用来说明哪个域可以访问你的数据 如果缺少会导致跨域的失败 如果像让所有的域可以访问你的数据 那么可以使用*

Access-Control-Allow-Credentials默认来说 cookies是不可以包含在跨域请求中 使用这个属性说明cookies可以包含在跨域请求头部中 这个属性跟上面说到的withCredentials协同工作的(withCredentials是js客户端这边设置的 Access-Control-Allow-Credentials是服务端这边设置的)

Access-Control-Expose-Headers 在js的XMLHttpRequest 2对象中可以使用getResponseHeader()的方法来获取一个response的header 在跨域请求中 getResponseHeader()方法只可以获取简单的response header 简单的response header被定义为如下

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

如果像客户端可以获取其他的headers就需要使用Access-Control-Expose-Headers这个属性了

处理不简单跨域请求
不简单跨域请求在客户端看起来跟简单跨域请求差不多 但是它在有运行两个附加的请求

  1. 首先客户端会发送一个preflight request 这个请求用于获取服务端的允许来发送真正的请求
  2. 获取允许之后 客户端发送真正的请求
  3. 因为preflight request是可以被缓存的 所以不需要每次请求都发送一次preflight request

跨域例子

有以下api

http://localhost:8080/api/product/1

以上api可以获取以下json个格式对象

1
2
3
4
5
6
7
8
9
10
11
12
{
"id": 1,
"name": "pork",
"picture": "https://demo.pork.png",
"price": 53,
"description": "this is a pork",
"rating": 0.8,
"amount": 100,
"likes": 0,
"tag_id": 1,
"sales_permonth": 382
}

在一个html的script中使用jquery的$.get访问上面的api 因为需要从不同域名或者不同端口下加载资源 所以浏览器出于安全考虑会阻止这个api的访问 值得一提的是postman并不是严格按照这个标准的

测试跨域的html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<title>cors_test</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$("#submit").click(function() {
$.get("http://localhost:8080/api/product/1", function(data, status) {
alert("data:" + data + "\nStatus:" + status);
});
});
});
</script>
</head>
<body>
<button id="submit">submit</button>
</body>
</html>

结果 出现跨域请求被禁止的报错
image_1cfjvf6ui2ohpq1fbh19kjftm9.png-78.9kB


aiohttp的跨域问题解决

因为aiohttp框架本身是不支持跨域的 所以我们需要使用一个拓展包aiohtto_cors

pip安装

pip install aiohttp_cors

具体使用方法请看aiohttp_cors

我系统分析与设计项目选择解决详情
image_1cfk4pnuuoi31aan1rmn1nr58s91g.png-85.8kB

结果 跨域请求成功
image_1cfk4ucj71ao11suj1r13j0taer1t.png-82.1kB

最后附加flask跨域问题解决方法flask_cors

nginx+supervisor部署aiohttp

前言

服务器上的部署问题一直很让我头疼 因为我一直是一个在windows平台上开发的 但是vps一直都是选择ubuntu16.04居多 因为在windos系统上 权限是一个总是不是被注意的事情 但是在ubuntu系统上部署web后端的时候 权限问题尤为重要


部署

为什么选择nginx+supervisor这样的组合来部署 因为

优点

  • nginx是一个完美的前端服务器 可以防止基于畸形的http协议的攻击等等
  • 使用nginx运行几个aiohttp实例可以使用CPU多核
  • nginx对static静态文件的支持比aiohttp的内置的静态文件支持更快
  • supervisor主要是为了系统崩溃或者重启之后重新运行web app实例 怎么kill也kill不掉

缺点

  • 配置比其他组合方式麻烦一点

参考链接

由于选择使用nginx+superviosr的组合方式部署aiohttp 所以要先下载nginx和supervisor 这个没什么问题 自行谷歌即可 但是要注意点

因为aiohttp是只可以支持python3.5.3+的 但是好巧不巧ubuntu16.04的自带python版本是python3.5.2 这下有点蛋疼 我给的建议是保留python3.5 另外安装python3.6

因为ubuntu系统以及很多像sublime等软件都是依赖py2或者py3.5的 一旦卸载或者修改了/use/bin/python的链接
会导致一系列版本不兼容的问题 我试过最严重的是卸载了python3.5 并且修改默认python版本为3.6 系统崩溃
折腾了很久最后只能重装系统

为什么我要说这个呢 因为supervisor依赖于ubuntu的py2的
所以最好不要卸载任何版本python和修改系统默认的python版本 最好就另行安装python3.6 终端中使用python3.6调用即可

项目代码放置于/var/www/aio_ooad下
image_1cff1giub10gmo07k6b16gjv339.png-71.6kB


nginx配置

  • nginx配置文件 /etc/nginx/nginx.conf
  • nginx日志文件
    • /var/log/nginx/access.log
    • /var/log/nginx/error.log

/etc/nginx/nginx.conf

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
http {
server {
listen 80;
client_max_body_size 4G;

server_name localhost;

location / {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_buffering off;
proxy_pass http://aiohttp;
}

location /static {
# path for static files
root /var/www/aio_ooad/static;
}

}
upstream aiohttp {
# fail_timeout=0 means we always retry an upstream even if it failed
# to return a good HTTP response

# Unix domain servers
server unix:/var/www/aio_ooad/example_1.sock fail_timeout=0;
server unix:/var/www/aio_ooad/example_2.sock fail_timeout=0;
server unix:/var/www/aio_ooad/example_3.sock fail_timeout=0;
server unix:/var/www/aio_ooad/example_4.sock fail_timeout=0;

# Unix domain sockets are used in this example due to their high performance,
# but TCP/IP sockets could be used instead:
# server 127.0.0.1:8081 fail_timeout=0;
# server 127.0.0.1:8082 fail_timeout=0;
# server 127.0.0.1:8083 fail_timeout=0;
# server 127.0.0.1:8084 fail_timeout=0;
}
}

上面大部分内容都是常规的

指明静态文件路径 nginx提供静态文件的支持是很快的 这是选择nginx部署的原因之一

1
2
3
4
location /static {
# path for static files
root /var/www/aio_ooad/static;
}

选择使用unix domian sockets文件的方式设置upstream 下面的注释也说到也可以使用TCP/IP sockets的方式 但是unix domain socket效果更好 一共配置了四个sockets文件 意味着后台一共有四个web app实例可以供nginx轮询选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
upstream aiohttp {
# fail_timeout=0 means we always retry an upstream even if it failed
# to return a good HTTP response

# Unix domain servers
server unix:/var/www/aio_ooad/example_1.sock fail_timeout=0;
server unix:/var/www/aio_ooad/example_2.sock fail_timeout=0;
server unix:/var/www/aio_ooad/example_3.sock fail_timeout=0;
server unix:/var/www/aio_ooad/example_4.sock fail_timeout=0;

# Unix domain sockets are used in this example due to their high performance,
# but TCP/IP sockets could be used instead:
# server 127.0.0.1:8081 fail_timeout=0;
# server 127.0.0.1:8082 fail_timeout=0;
# server 127.0.0.1:8083 fail_timeout=0;
# server 127.0.0.1:8084 fail_timeout=0;
}


supervisor配置

  • supervisor默认配置文件 /etc/supervisor/supervisord.conf
  • 部署项目的配置文件 /etc/supervisor/conf.d/example.ini

修改/etc/supervisor/supervisord.conf 最后一行处为

1
2
[include]
files = conf.d/*.ini ;

/etc/supervisor/conf.d/example.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[program:aiohttp]
numprocs = 4
numprocs_start = 1
process_name = example_%(process_num)s

; Unix socket paths are specified by command line.
command=python3.6 /var/www/aio_ooad/test_main.py --path=/var/www/aio_ooad/example_%(process_num)s.sock --port=808%(process_num)s

; We can just as easily pass TCP port numbers:
; command=/path/to/aiohttp_example.py --port=808%(process_num)s

user=www-data
autostart=true
autorestart=true

numprocs = 4
numprocs_start = 1
process_name = example_%(process_num)s

一共四个进程 进程号从1到4 进程名字由进程号决定 则为example_1 example_2 example_3 example_4

command=python3.6 /var/www/aio_ooad/test_main.py --path=/var/www/aio_ooad/example_%(process_num)s.sock --port=808%(process_num)s
user=www-data

后台运行此命令 也即是运行web app所在的py文件 执行用户为www-data

这里坑就比较多了

1.python模块问题
因为supervisor是由root用户运行的 所以在supervisor里面后台执行python命令也是在root用户下执行的

正如我开始所说 我一直依赖都是windows系统下进行开发的 对权限的意识不是很足

下面我们来看看一个模块安装问题
因为我是使用普通用户gzm进行安装aiohttp模块的 但是在部署之后日志显示没有安装aiohttp模块

让我们在普通用户gzm下打开python3终端 查看模块安装目录
image_1cff2k7ie1nqkpra1a7t1pp81e0q2m.png-40kB
我们会发现里面有个路径为

/home/gzm/.local/lib/python3.6/site-packages

进入之后发现aiohttp正是安装在这个路径下面
image_1cff2pkee1quh1kbssklugl167p33.png-66kB

然后让我们切换为root用户 查看模块安装路径 发现没有/home/gzm/.local/lib/python3.6/site-packages 这个路径 这就解释了为什么即使我真的在gzm这个用户下安装了aiohttp等模块 但是运行supervisor还是会在日志中报错没有此模块 所以正确的方式是切换到root安装需要的模块
image_1cff2sipucq2gtm1qid5141fvd4g.png-40.1kB

还有一个问题是

--path=/var/www/aio_ooad/example_%(process_num)s.sock

意味着www-data用户需要在/var/www/aio_ooad目录下创建四个.socket文件 以支持nginx 所以/var/www/aio_ooad这个文件需要把所有权改为www-data 不然会在日志中报错permission denied

sudo chown -R www-data:www-data /var/www/aio_ooad/

web app所在py

test_main.py

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
from aiohttp_polls import main
from aiohttp import web
import argparse

parser = argparse.ArgumentParser(description="aiohttp server example")
parser.add_argument('--path')
parser.add_argument('--port')

app = main.app
args = parser.parse_args()
web.run_app(app, host="0.0.0.0", port=args.port, path=args.path)


nginx supervisor重启命令

1
2
sudo /etc/init.d/nginx restart
sudo supervisorctl reload

java socket编程

java的socket编程跟python的很像 而且也很方便 简单记录一下 同时server那边的accept也是阻塞的

connect函数可以在初始化一个socket对象之后 一般客户端可以用来链接服务端 但是如果在socket构造函数那里传入serverName和port 也是可以在初始化的时候进行连接的

服务端

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
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class JServer extends Thread {
private ServerSocket server;
public JServer(int port) throws IOException {
server = new ServerSocket(port);
server.setSoTimeout(10000);
}
public void run() {
while (true) {
try {
System.out.println("等待客户端连接");
Socket s = server.accept();
System.out.println("远程连接地址 " + s.getRemoteSocketAddress());
DataInputStream in = new DataInputStream(s.getInputStream());
System.out.println("接受数据" + in.readUTF());
DataOutputStream out = new DataOutputStream(s.getOutputStream());
out.writeUTF("我接收到了 拜拜");
s.close();
} catch (IOException i) {
i.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
JServer JS = new JServer(5000);
JS.run();
}
}

客户端

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
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

public class JClient extends Thread {
private Socket client;
public JClient(String serverName, int port) throws IOException {
client = new Socket(serverName, port);
}
public void run() {
try {
System.out.println("现在开始连接服务端");
DataOutputStream out = new DataOutputStream(client.getOutputStream());
out.writeUTF("我是客户端 服务端你那边接收到我发的信息了没");
DataInputStream in = new DataInputStream(client.getInputStream());
System.out.println("从服务端接受到 " + in.readUTF());
client.close();
} catch (IOException i) {
i.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
JClient client = new JClient("127.0.0.1", 5000);
client.run();
}
}

结果
image_1cf7fbt5l1flia2gpmn1bp1328p.png-192.9kB
image_1cf7fcgan62o1ict1n4lvbbda16.png-160.3kB