开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Redis的安装及基本使用

发表于 2018-01-12

#1.Redis
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。
Redis 与其他 key - value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  • Redis支持数据的备份,即master-slave模式的数据备份。
  • 官方文档
  • 中文文档
    #2.Redis安装
    ##1.windows安装
    下载官方文件之后,安装即可。
    在命令窗口输入命令
1
复制代码redis-cli

启动成功

  • 设置远程访问
    找到redis的配置文件

注释bind 127.0.0.1即可实现远程连接访问

##2.linux安装

  • 下载安装包解压,解压完毕在文件夹内打开命令窗口
1
2
3
4
5
复制代码#输入命令
make

#完毕之后在当前窗口输入新的命令
sudo make install
  • 设置redis在后台运行

把daemonize 改为yes即可实现在后台运行

  • 启动和关闭redis服务
1
2
3
4
5
6
7
8
9
10
11
12
复制代码#启动redis服务
./redis-server redis.conf

#查看redis服务进程 是否启动成功
ps -ef | grep redis
ps -A | grep redis

#redis客户端启动
redis-cli

#关闭redis服务
ps -A | grep redis

#3.redis常见配置

  • 配置文件redis.conf
  • 常见配置项
    • bind 127.0.0.1 [绑定ip地址,远程访问请注释]
    • port 6379 [默认访问地址 6379]
    • daemonize yes [是否以后台进程<守护进程>运行]
    • dbfilename dump.rdb [存储数据的文件]
    • dir ./. [存储数据的文件所在路径]
  • redis中的数据类型
  • **redis的数据存储:**key=value 键值对
  • **key<键>的数据类型:**字符串
  • value<值的类型>:
    • string字符串
    • hash哈希
    • list列表
    • set集合
    • zset有序集合

#4.redis数据操作

  • string:字符串操作

**set key value :**给一个key赋值value
**setex key seconds value:**给一个key设置值value,过期时间seconds
**mset key value [key value]:**设置多个键值对

**get key:**根据key获取一个值
**mget key [key]:**根据多个key获取多个值

**incr key :**将key对应的值+1
**incrby key increment:**将key对应的值+increment
**decr key:**将key对应的值-1
**decrby key increment:**将key对应的值-increment

**append key value:**将value的值拼接到x后面
**strlen key:**获取key对应的值的长度

  • key操作

**keys pattern:**查找键,支持正则
**exists key:**查找键是否存在,存在返回1,否则返回0
**type key:**查看键对应的值的类型
**del key:**根据key删除键值对
**expire key seconds:**给key设置过期时间
**ttl key:**查看键的有效时间(显示结果为-2 的话表示过期,-1表示永不过期)

  • hash:用于存储对象,对象的格式为键值对

**hset key field value:**设置单个属性
**hmset key field value [field value]:**设置多个属性

**hget key field :**获取key对应的值
**hmget key field [field]:**获取多个key对应的value值
**hgetall key:**获取所有属性和值
**hkeys key:**获取所有的属性
**hlen key:**获取包含属性的个数
**hvals key:**获取所有的值
**hexists key field:**判断属性是否存在
**hdel key field [field]:**根据属性名称删除属性及值
**hstrlen key field:**返回值的字符串长度

  • list列表:有序存储多个数据

**lpush key value [value]:**列表头部增加多个数据
**rpush key value [value]:**列表尾部增加多个数据
**linsert key before | after privot value:**在一个元素钱/后插入数据
**lset key index value:**设置指定索引的元素的值

**lpop key:**删除并且获取key对应的list第一个元素
**rpop key:**删除并且获取key对应的list最后一个元素
**lrange key start stop:**返回存在在key的list中指定范围的数据

**llen key:**获取列表的长度
**lindex key index:**获取列表中索引对应的元素
**ltrim key start stop:**获取列表中start~stop组成的新的列表

  • set集合:无序存储多个数据

**sadd key value [value]:**添加多个数据到key集合中
**smembers key:**获取key集合中所有的数据
**sismember key value:**判断value是否在key集合中存在
**scard key:**获取key集合中元素的个数

**sinter key [key]:**获取多个集合 交集
**sdiff key [key]:**获取多个集合的差集
**sunion key [key]:**获取多个集合的并集

  • zset集合:有序存储多个数据
  • sorted set,有序集合
  • 元素为string类型
  • 元素具有唯一性,不重复
  • 每个元素都会关联一个double类型的score,表示权重,通过权重将元素从小到大排序
  • 元素的score可以相同

**zadd key score value [ score value]:**添加多个带权重的数据到key集合中
**zrange key start stop:**获取指定范围中所有的元素
**zcard key:**返回元素的个数
**zcount key min max :**返回score值在min和max之间的数据
**zscore key member:**返回集合中member元素的score值

#5.redis发布订阅

  • 发布者不是计划发送消息给特定的接收者(订阅者),而是发布的消息分到不同的频道,不需要知道什么样的订阅者订阅
  • 订阅者对一个或多个频道感兴趣,只需接收感兴趣的消息,不需要知道什么样的发布者发布的
  • 发布者和订阅者的解耦合可以带来更大的扩展性和更加动态的网络拓扑
  • 客户端发到频道的消息,将会被推送到所有订阅此频道的客户端
  • 客户端不需要主动去获取消息,只需要订阅频道,这个频道的内容就会被推送过来
  • 消息的格式

推送消息的格式包含三部分

  • part1:消息类型,包含三种类型
+ **subscribe**,表示订阅成功
+ **unsubscribe**,表示取消订阅成功
+ **message**,表示其它终端发布消息
  • 如果第一部分的值为subscribe,则第二部分是频道,第三部分是现在订阅的频道的数量
  • 如果第一部分的值为unsubscribe,则第二部分是频道,第三部分是现在订阅的频道的数量,如果为0则表示当前没有订阅任何频道,当在Pub/Sub以外状态,客户端可以发出任何redis命令
  • 如果第一部分的值为message,则第二部分是来源频道的名称,第三部分是消息的内容
1
2
3
4
5
复制代码 subscribe 频道名称 [频道名称]:订阅多个频道

unsubscribe 频道名称 [频道名称]:取消多个频道的订阅

publish 频道 消息:向指定的频道推送消息

###打开多个命令窗口:

  • 第一个窗口当做订阅者

输入命令:

1
2
3
4
5
6
7
8
9
10
> 复制代码#启动redis
> redis-cli
>
> #选择数据库
> select 0
>
> #订阅频道
> subscribe zhiji
>
>
  • 第二个窗口当做客户端

输入命令

1
2
3
4
5
6
7
8
9
10
> 复制代码#启动redis
> redis-cli
>
> #选择数据库
> select 1
>
> #发布消息
> publish zhiji 'hellow'
>
>

效果如图所示

#6.主从双备

  • 主从配置
    一个master可以拥有多个slave,一个slave又可以拥有多个slave,如此下去,形成了强大的多级服务器集群架构
    比如,将ip为192.168.1.10的机器作为主服务器,将ip为192.168.1.11的机器作为从服务器
  • 设置主服务器的配置
    bind 192.168.1.10 设置从服务器的配置
    注意:在slaveof后面写主机ip,再写端口,而且端口必须写
1
2
3
4
5
6
7
8
9
10
复制代码通过redis.**.conf配置完成主从双备

bind配置主数据库服务器

slaveof配置从数据库服务器

bind 192.168.1.11 slaveof 192.168.1.10 6379 在master和slave分别执行info命令,查看输出信息
在master上写数据
set hello world 在slave上读数据
get hello

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Django教程(一) Django视图与网址

发表于 2018-01-12

###目录:

  • Django教程(一)- Django视图与网址
  • Django教程(二)- Django视图与网址进阶
  • Django教程(三)- Django表单Form
  • Django教程(四)- Django模板及进阶
  • Django模型(数据库)及Django Query常用方法
  • Django教程(五)- 上传及显示
  • Django实战(一)- 搭建简单的博客系统
  • Django实战(二)- 创建一个课程选择系统

#1.简介
###MVC

  • 大部分开发语言中都有MVC框架
  • MVC框架的核心思想是:解耦
  • 降低各功能模块之间的耦合性,方便变更,更容易重构代码,最大程度上实现代码的重用
  • m表示model,主要用于对数据库层的封装
  • v表示view,用于向用户展示结果
  • c表示controller,是核心,用于处理请求、获取数据、返回结果

###MVT

  • Django是一款python的web开发框架
  • 与MVC有所不同,属于MVT框架
  • m表示model,负责与数据库交互
  • v表示view,是核心,负责接收请求、获取数据、返回结果
  • t表示template,负责呈现内容到浏览器

Django 是用Python开发的一个免费开源的Web框架,可以用于快速搭建高性能,优雅的网站!

Django官方网站
Django官方文档
安装Django官方文档介绍

Django是一个基于MVC构造的框架。但是在Django中,控制器接受用户输入的部分由框架自行处理,所以 Django 里更关注的是模型(Model)、模板(Template)和视图(Views),称为 MTV模式。

BSD:BSD许可证是随着加州大学伯克利分校发布BSD UNIX发展起来的,修改版本被Apple、Apache所采用。BSD协议是“宽容自由软件许可证”中的一员,在软件复用上给予了最小限度的限制。

BSD协议允许作者使用该协议下的资源,将其并入私人版本的软件,该软件可使用闭源软件协议发布。

#2.环境搭建

  1. 下载Ubuntu 镜像文件
    地址一
    地址二
    地址三
  2. 安装ubuntu
  3. 安装pip,使用以下合适的代码安装
1
2
3
复制代码sudo apt-get update
sudo apt-get upgrade
sudo apt-get install python-pip

对于Python开发用户来讲,PIP安装软件包是家常便饭。但国外的源下载速度实在太慢,浪费时间,而且好多软件总是被墙,所以把PIP安装源替换成国内镜像,可以大幅提升下载速度,还可以解决被墙导致的装不上库的烦恼,提高安装成功率。网上有很多可用的源,这里推荐的是清华大学的pip源,它是官网pypi的镜像,每隔5分钟同步一次。

Linux下,修改 ~/.pip/pip.conf (没有就创建一个),按下Ctrl + H 可以看到隐藏文件,修改 index-url至tuna,内容如下:

1
2
复制代码[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple

4.利用pip安装 Django(推荐使用1.11版本)

1
2
3
复制代码(sudo)pip install Django
或者 (sudo) pip install Django==1.8.16
或者 pip install Django==1.11

检查是否安装成功

1
2
3
4
5
6
复制代码>>> import django
>>> django.VERSION
(1, 11, 'final', 0)
>>>
>>> django.get_version()
'1.11'

#3.安装pycharm

  1. 下载JDK


2. 解压
输入命令:tar zvxf jdk-8u131-linux-x64.tar.gz
3. 创建jvm文件
输入命令:sudo mkdir /usr/lib/jvm
4. 移动到/usr/lib/jvm下
输入命令:sudo mv jdk1.8.0_131/ /usr/lib/jvm/
**注意:**如果没有jvm文件,执行该语句虽然会自动创建jvm文件,但只会把jdk1.8.0_25里面的文件都放到jvm中,而不是把jdk1.8.0_25及其里面的文件放到jvm文件中,两者是有区别的
5. 设置JDK环境变量
(也有在/.bashrc修改的,区别是:/etc/profile的设置方法对所有登陆用户都有效/.bashrc只对当前用户有效)
输入命令:sudo vim ~/.profile
编辑:

1
2
3
4
复制代码export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_131
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH
  1. 使修改立刻生效source ~/.profile
  2. 验证JDK
    输入命令:java -version

#4.Ubuntu下 正确安装VMware Tools
为了实现可以从windows拖拽文件到ubuntu,可以安装VMware Tools。

#5.Django主要模块

  • urls.py
    网址入口,关联到对应的views.py中的一个函数(或者generic类),访问网址就对应一个函数。
  • views.py
    处理用户发出的请求,从urls.py中对应过来, 通过渲染templates中的网页可以将显示内容,比如登陆后的用户名,用户请求的数据,输出到网页。
  • models.py
    与数据库操作相关,存入或读取数据时用到这个,当然用不到数据库的时候 你可以不使用。
  • forms.py
    表单,用户在浏览器上输入数据提交,对数据的验证工作以及输入框的生成等工作,当然你也可以不使用。
  • templates 文件夹
    views.py 中的函数渲染templates中的Html模板,得到动态内容的网页,当然可以用缓存来提高速度。
  • admin.py
    后台,可以用很少量的代码就拥有一个强大的后台。
  • settings.py
    Django 的设置,配置文件,比如 DEBUG 的开关,静态文件的位置等。

#6.Django基本命令

  • 新建一个 django project
1
2
复制代码django-admin.py startproject project_name
特别是在 windows 上,如果报错,尝试用 django-admin 代替 django-admin.py 试试

注意 project_name 是自己的项目名称,需要为合法的 Python 包名,如不能为 1a 或 a-b。

  • 新建 app
    要先进入项目目录下,cd project_name 然后执行下面的命令(下同,已经在项目目录下则不需要 cd project_name)
1
2
复制代码python manage.py startapp app_name
或 django-admin.py startapp app_name

一般一个项目有多个app, 当然通用的app也可以在多个项目中使用。
与项目名类似 app name 也需要为合法的 Python 包名,如 blog,news, aboutus 等都是合法的 app 名称。

  • 创建数据库表 或 更改数据库表或字段
1
2
3
4
5
6
7
8
9
复制代码Django 1.7.1及以上 用以下命令
# 1. 创建更改的文件
python manage.py makemigrations
# 2. 将生成的py文件应用到数据库
python manage.py migrate


旧版本的Django 1.6及以下用
python manage.py syncdb

这种方法可以在SQL等数据库中创建与models.py代码对应的表,不需要自己手动执行SQL。
备注:对已有的 models 进行修改,Django 1.7之前的版本的Django都是无法自动更改表结构的,不过有第三方工具 south

  • 使用开发服务器
    开发服务器,即开发时使用,一般修改代码后会自动重启,方便调试和开发,但是由于性能问题,建议只用来测试,不要用在生产环境。
1
2
3
4
5
6
7
8
9
10
11
复制代码python manage.py runserver

# 当提示端口被占用的时候,可以用其它端口:
python manage.py runserver 8001
python manage.py runserver 9999
(当然也可以kill掉占用端口的进程)

# 监听机器所有可用 ip (电脑可能有多个内网ip或多个外网ip)
python manage.py runserver 0.0.0.0:8000
# 如果是外网或者局域网电脑上可以用其它电脑查看开发服务器
# 访问对应的 ip加端口,比如 http://172.16.20.2:8000
  • 清空数据库
1
复制代码python manage.py flush

此命令会询问是 yes 还是 no, 选择 yes 会把数据全部清空掉,只留下空表。

  • 创建超级管理员
1
2
3
4
5
6
复制代码python manage.py createsuperuser

# 按照提示输入用户名和对应的密码就好了邮箱可以留空,用户名和密码必填

# 修改 用户密码可以用:
python manage.py changepassword username
  • Django 项目环境终端
1
复制代码python manage.py shell

#7. Django视图与网址
###1.Django中网址是写在 urls.py 文件中,用正则表达式对应 views.py 中的一个函数(或者generic类)。

  1. 新建一个项目(project), 名称为 zebk
1
复制代码django-admin startproject zebk

备注: 如果 django-admin 不行,请用 django-admin.py
2. 新建一个应用(app), 名称叫 zhong

1
复制代码python manage.py startapp zhong  # zhong 是一个app的名称
  1. 注:Django 1.8.x 以上的,还有一个 migrations 文件夹。Django 1.9.x 还会在 Django 1.8 的基础上多出一个 apps.py 文件。

把我们新定义的app加到settings.py中的INSTALL_APPS中
修改 mysite/mysite/settings.py

1
2
3
4
5
6
7
8
9
10
复制代码INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',

'zhong',
)

作用:新建的 app 如果不加到 INSTALL_APPS 中的话, django 就不能自动找到app中的模板文件(app-name/templates/下的文件)和静态文件(app-name/static/中的文件)

###2.定义视图函数(即访问页面时显示的内容)
打开/zebk下的views.py文件 增加以下内容

1
2
3
4
5
复制代码# -*- coding: utf-8 -*- 
from django.http import HttpResponse

def index(request):
return HttpResponse(u"hellow 中二病控丶!")
  1. 第一行是声明编码为utf-8, 因为我们在代码中用到了中文,如果不声明就报错.
  2. 第二行引入HttpResponse,它是用来向网页返回内容的,就像Python中的 print 一样,只不过 HttpResponse 是把内容显示到网页上。
  3. 我们定义了一个index()函数,第一个参数必须是 request,与网页发来的请求有关,request 变量里面包含get或post的内容,用户浏览器,系统等信息在里面(后面会讲,先了解一下就可以)。
  4. 函数返回了一个 HttpResponse 对象,可以经过一些处理,最终显示几个字到网页上。

###3. 定义视图函数函数相关的URL

  1. 定义视图函数相关的URL(网址) (即规定 访问什么网址对应什么内容)
    打开 mysite/mysite/urls.py 这个文件, 修改其中的代码:
    在mysite/urls.py,导入django.conf.urls.include模块,并且添加到urlpatterns列表,所以mysite/urls.py如下:
1
2
3
4
5
6
7
8
复制代码# mysite/urls.py
from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
url(r'^zhong/', include('zhong.urls')),
url(r'^admin/', admin.site.urls),
]

2.在zhong中创建urls.py,编写如下:

1
2
3
4
5
6
7
复制代码from django.conf.urls import url

from . import views

urlpatterns = [
url(r'^$', views.index, name='index'),
]

然后在终端上运行 python manage.py runserver 我们会看到类似下面的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码 python manage.py runserver

Performing system checks...

System check identified no issues (0 silenced).

You have unapplied migrations; your app may not work properly until they are applied.
Run 'python manage.py migrate' to apply them.

December 22, 2015 - 11:57:33
Django version 1.9, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

打开网页,输入127.0.0.1:8000/zhong/

#8.管理操作

  • 站点分为“内容发布”和“公共访问”两部分
  • “内容发布”的部分负责添加、修改、删除内容,开发这些重复的功能是一件单调乏味、缺乏创造力的工作。为此,Django会根据定义的模型类完全自动地生成管理模块
    ###使用django的管理
    创建一个管理员用户
1
复制代码python manage.py createsuperuser,按提示输入用户名、邮箱、密码
  • 启动服务器,通过“127.0.0.1:8000/admin”访问,输入上面创建的用户名、密码完成登录
  • 进入管理站点,默认可以对groups、users进行管理

###管理界面本地化

  • 编辑settings.py文件,设置编码、时区
1
2
复制代码LANGUAGE_CODE = 'zh-Hans'
TIME_ZONE = 'Asia/Shanghai'

###向admin注册booktest的模型

  • 打开booktest/admin.py文件,注册模型
1
2
3
复制代码from django.contrib import admin
from models import BookInfo
admin.site.register(BookInfo)
  • 刷新管理页面,可以对BookInfo的数据进行增删改查操作
  • 问题:如果在str方法中返回中文,在修改和添加时会报ascii的错误
  • 解决:在str()方法中,将字符串末尾添加“.encode(‘utf-8’)”
    ###自定义管理页面
  • Django提供了admin.ModelAdmin类
  • 通过定义ModelAdmin的子类,来定义模型在Admin界面的显示方式
1
2
3
复制代码class QuestionAdmin(admin.ModelAdmin):
...
admin.site.register(Question, QuestionAdmin)

#####列表页属性

  • list_display:显示字段,可以点击列头进行排序
1
复制代码list_display = ['pk', 'btitle', 'bpub_date']
  • list_filter:过滤字段,过滤框会出现在右侧
1
复制代码list_filter = ['btitle']
  • search_fields:搜索字段,搜索框会出现在上侧
1
复制代码search_fields = ['btitle']
  • list_per_page:分页,分页框会出现在下侧
1
复制代码list_per_page = 10

#####添加、修改页属性

  • fields:属性的先后顺序
1
复制代码fields = ['bpub_date', 'btitle']
  • fieldsets:属性分组
1
2
3
4
复制代码fieldsets = [
('basic',{'fields': ['btitle']}),
('more', {'fields': ['bpub_date']}),
]

#####关联对象

  • 对于HeroInfo模型类,有两种注册方式
+ 方式一:与BookInfo模型类相同
+ 方式二:关联注册
  • 按照BookInfor的注册方式完成HeroInfo的注册
  • 接下来实现关联注册
1
2
3
4
5
6
7
8
9
10
11
复制代码from django.contrib import admin
from models import BookInfo,HeroInfo

class HeroInfoInline(admin.StackedInline):
model = HeroInfo
extra = 2

class BookInfoAdmin(admin.ModelAdmin):
inlines = [HeroInfoInline]

admin.site.register(BookInfo, BookInfoAdmin)
  • 可以将内嵌的方式改为表格
1
复制代码class HeroInfoInline(admin.TabularInline)

#####布尔值的显示

  • 发布性别的显示不是一个直观的结果,可以使用方法进行封装
1
2
3
4
5
6
7
8
9
复制代码def gender(self):
if self.hgender:
return '男'
else:
return '女'
gender.short_description = '性别'
在admin注册中使用gender代替hgender
class HeroInfoAdmin(admin.ModelAdmin):
list_display = ['id', 'hname', 'gender', 'hcontent']

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

C++ STL容器总结

发表于 2018-01-11

Hints:在本文中,重点总结的是各个容器中增删查的算法时间复杂度。

在c++中,容器指的是能够容纳各种数据类型的通用数据数据结构,是类模板。
比如set类模板:

1
2
3
复制代码template <class _Key, class _Compare = less<_Key>,
class _Allocator = allocator<_Key> >
class _LIBCPP_TYPE_VIS_ONLY set

C++的容器主要分为三种:

  • 顺序容器
  • 关联容器
  • 容器适配器

而访问容器元素我们需要迭代器(引入容器头文件就可以了,不需要单独因为头文件),迭代器有以下几个属性:

  • 用于指向顺序容器和关联容器的元素
  • 用法和指针类似
  • 有const(容器类名::iterator 变量名)和非const两种(容器类名::const_iterator 变量名)
  • 通过迭代器可以读取它指向的元素
  • 非const迭代器和可以修改其指向的元素

顺序容器

何为顺序容器,也就是说元素的位置是顺序插入的,插入位置与元素的值无关,核心是容器没有排序。
顺序容器都具有以下10个成员函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码#返回迭代器
begin
end
rbegin //返回指向最后一个元素的迭代器
rend
#返回元素引用
front
back
#其他
erase(删除区间时返回被删除元素后面的迭代器;也可删除一个或几个元素)
clear
push_back
pop_back

顺序容器有以下三种:

1.vector动态数组 头文件<vetor>

元素在内存中是连续存放的。

随机存取时间:常数时间(因为可以通过下标直接访问到地址)。

在尾部增删元素通常是常数时间(正常是常数时间,如果超出了默认分配的元素个数,会重新分配存储空间,此时会消耗更多时间)。

在中间或者头部增删元素:o(n)(会移动其他元素的位置)。

迭代器类型:随机访问(支持下标访问、随机移动,例:a[i])。

查询时间:o(n)(因为没有排序,只能现行查找,效率较低)。

可见vector在中间或者头部增删元素性能较低。

优点:内存和C完全兼容、高效随机访问、节省空间

缺点:内部插入删除元素代价巨大、动态大小查过自身容量需要申请大量内存做大量拷贝。

构造函数:

1
2
3
4
复制代码 vector();
vector(int n);
vector(int n, const T &a); //把n个元素初始化为a
vector(iterator first,iterator last); //初始化为其他容器上区间[first,last)一致的内容

常用成员函数

1
2
3
4
5
复制代码void pop_back();
void push_back(const T &val);
int size();
T & font();
T & back();

deque[读:dek,之前经常读dkju:]双向队列 头文件<deque>

元素在内存中连续存放(连续内存的容器有个明显的缺点,就是有新元素插入或老元素删除的时候,为了给新元素腾出位置或者填充老元素的空缺,同一块内存中的其他数据需要进行整体的移位,这种移位的拷贝代价有时是非常巨大的。deque实际上是分配到不同内存块,通过链表把内存块连在一起,再进行连续存放,是list与vector的折中)。

随机存取时间:常数时间(仅次于vector,因为有可能存在尾部的内存位置在头部之前的场景)。

在两端增删元素通常是常数时间。(deque不像vector没有容量,不需要重新分配内存空间。这是因为deque由动态分配的连续空间,即缓冲区,组合而成,随时可以增加一段新的空间链接起来。它没有必要像vector那样“因旧空间不足而重新分配2倍的空间,然后复制元素,再释放旧空间”。当重新分配缓冲区时,耗时增加)。

在中间插入:时间复杂度较高。

迭代器类型:随机访问(效率低于vector)。

查询时间:o(n)(原因同上)。

**优点:高效随机访问、内部插入删除元素效率方便、两端push、pop效率很高
**

**缺点:内存占用比较高
**

list双向链表 头文件<list>

元素在内存中不连续分配(因为指针可以获取前后元素的地址),所以不支持随机存取。

在任何位置增删元素时间:常数时间。

查询时间:o(n)(原因同上)。

迭代器类型:双向(不支持下标访问,不支持迭代器的比较运算符,和+-运算符)

优点:任意位置插入删除元素常量时间复杂度、两个容器融合是常量时间复杂度

缺点:不支持随机访问、比vector占用更多的存储空间

常用成员函数:(注意这些在顺序容器中都是list独有的)

1
2
3
4
5
6
7
8
复制代码push_front
pop_front
sort //不支持STLsort算法
remove
unique
merge
reverse
splice

特别说明,list的sort函数有无参和compare两个版本

1
2
3
复制代码list<T> classname
classname.sort(compare); //compare自定义
classname.sort();

关联容器(迭代器类型:双向)

关联的意思就是元素是排序的。

插入与检索元素时间: o(log(N))(因为通过红黑二叉树实现,时间与平衡二叉树一致)。

需要注意的是,STL中有些算法比如sort,binary_search需要通过随机访问迭代器访问容器中的元素,因此list以及关联容器就不能支持该算法!

主要包括以下4种:

  • set
  • multiset
  • map
  • multimap

除了个容器都有的成员函数外,它们都有以下成员函数

1
2
3
4
5
6
复制代码find
lower_bound
upper_bound
equal_range
count
insert

map与set的不同点在于,map中存放的元素有且仅有两个成员变量,first(类似于key),second(类似于value),map可以根据first对元素排序和检索。

set与multiset的不同在于,后者允许存在相同值的元素。
map与multimap的不同在于,后者允许存在相同的first值的元素。

容器适配器(不支持迭代器)

这个概念听起来有点抽象,其实就是STL帮我们抽象了实际工作中所需要的数据操作方式。举个栗子,交流电可以有任意的传输电压,但是实际生活中我们只需要220v就够了,变压器帮我们实现了这个基本的功能。同理,容器的操作方式多种多样,但是其实我们只需要他们按照实际的一些规范来操作即可。

他们都有以下3个成员函数:

1
2
3
复制代码push
top
pop

STL中的排序,查找,变序算法都不适合容器适配器(因为容器适配器的元素位置不可随意改变,并且访问有一定的访问规则,不支持随机访问)。

stack 栈 头文件<stack>

是一个有限序列,并满足序列中被删除、检索和修改的项只能是最近插入序列的项(栈顶项)。也即,先进先出LIFO。

可以用vector,deque,list(性能最差)实现,编译器缺省是用deque实现的。

比如,在程序调试中,错误堆栈就是一种应用。

queue 队列 头文件 <queue>

插入只可以在尾部进行,删除、检索和修改只允许从头部进行。先进先出FIFO。

queue由于pop,push发生在对头,所以不能用vector实现,可以用list,deque实现,编译器缺省用deque实现。

比如,应用于需要保证消息顺序性的场景,如消息队列。

priority_queue 优先级队列 头文件<queue>

最高优先级元素总是第一个出列。

它需采用堆排序来保证元素总是在最前面,因此要求随机访问,所以不能使用list实现,可以用vector和deque实现,编译器缺省情况下用vector实现。

比如,操作系统的线程的调度算法,有的是按照优先级来调度的。

总结

  • 如果需要随机访问,用vector
  • 如果存储元素的数目已知,用vector
  • 需要任意位置随机插入删除,用list
  • 经常在容器的首部尾部插入删除元素,用deque
  • 元素结构复杂用list,也可以用vector存储指针(需要额外的精力去维护内存),看需求
  • 如果操作是基于键值,用set/map
  • 如果需要经常的搜索,用map/set

参考:

C++ STL容器时间复杂度下的最佳选择

深入理解deque容器

优先队列及最小堆最大堆

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据结构与算法 排序与搜索

发表于 2018-01-10

文章来源:数据结构与算法(Python)

#排序与搜索

排序算法(英语:Sorting algorithm)是一种能将一串数据依照特定顺序进行排列的一种算法。

#1.冒泡排序

**冒泡排序(英语:Bubble Sort)**是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

冒泡排序算法的运作如下:

  • 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
    ##冒泡排序的分析

交换过程图示(第一次):

那么我们需要进行n-1次冒泡过程,每次对应的比较次数如下图所示:

1
2
3
4
5
6
7
8
9
10
复制代码def bubble_sort(alist):
for j in range(len(alist)-1,0,-1):
# j表示每次遍历需要比较的次数,是逐渐减小的
for i in range(j):
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i]

li = [54,26,93,17,77,31,44,55,20]
bubble_sort(li)
print(li)

##时间复杂度

  • 最优时间复杂度:O(n) (表示遍历一次发现没有任何可以交换的元素,排序结束。)
  • 最坏时间复杂度:O(n2)
  • 稳定性:稳定

##冒泡排序的演示
效果:

#2.选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

##选择排序分析
排序过程:

红色表示当前最小值,黄色表示已排序序列,蓝色表示当前位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码def selection_sort(alist):
n = len(alist)
# 需要进行n-1次选择操作
for i in range(n-1):
# 记录最小位置
min_index = i
# 从i+1位置到末尾选择出最小数据
for j in range(i+1, n):
if alist[j] < alist[min_index]:
min_index = j
# 如果选择出的数据不在正确位置,进行交换
if min_index != i:
alist[i], alist[min_index] = alist[min_index], alist[i]

alist = [54,226,93,17,77,31,44,55,20]
selection_sort(alist)
print(alist)

##时间复杂度

  • 最优时间复杂度:O(n2)
  • 最坏时间复杂度:O(n2)
  • 稳定性:不稳定(考虑升序每次选择最大的情况)
    ##选择排序演示

#3.插入排序

插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

##插入排序分析

1
2
3
4
5
6
7
8
9
10
11
复制代码def insert_sort(alist):
# 从第二个位置,即下标为1的元素开始向前插入
for i in range(1, len(alist)):
# 从第i个元素开始向前比较,如果小于前一个元素,交换位置
for j in range(i, 0, -1):
if alist[j] < alist[j-1]:
alist[j], alist[j-1] = alist[j-1], alist[j]

alist = [54,26,93,17,77,31,44,55,20]
insert_sort(alist)
print(alist)

##时间复杂度

  • 最优时间复杂度:O(n) (升序排列,序列已经处于升序状态)
  • 最坏时间复杂度:O(n2)
  • 稳定性:稳定
    ##插入排序演示

#4.快速排序

快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

步骤为:

1.从数列中挑出一个元素,称为”基准”(pivot),
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
3.递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

##快速排序的分析

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
复制代码def quick_sort(alist, start, end):
"""快速排序"""

# 递归的退出条件
if start >= end:
return

# 设定起始元素为要寻找位置的基准元素
mid = alist[start]

# low为序列左边的由左向右移动的游标
low = start

# high为序列右边的由右向左移动的游标
high = end

while low < high:
# 如果low与high未重合,high指向的元素不比基准元素小,则high向左移动
while low < high and alist[high] >= mid:
high -= 1
# 将high指向的元素放到low的位置上
alist[low] = alist[high]

# 如果low与high未重合,low指向的元素比基准元素小,则low向右移动
while low < high and alist[low] < mid:
low += 1
# 将low指向的元素放到high的位置上
alist[high] = alist[low]

# 退出循环后,low与high重合,此时所指位置为基准元素的正确位置
# 将基准元素放到该位置
alist[low] = mid

# 对基准元素左边的子序列进行快速排序
quick_sort(alist, start, low-1)

# 对基准元素右边的子序列进行快速排序
quick_sort(alist, low+1, end)


alist = [54,26,93,17,77,31,44,55,20]
quick_sort(alist,0,len(alist)-1)
print(alist)

##时间复杂度

  • 最优时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(n2)
  • 稳定性:不稳定
    从一开始快速排序平均需要花费O(n log n)时间的描述并不明显。但是不难观察到的是分区运算,数组的元素都会在每次循环中走访过一次,使用O(n)的时间。在使用结合(concatenation)的版本中,这项运算也是O(n)。

在最好的情况,每次我们运行一次分区,我们会把一个数列分为两个几近相等的片段。这个意思就是每次递归调用处理一半大小的数列。因此,在到达大小为一的数列前,我们只要作log n次嵌套的调用。这个意思就是调用树的深度是O(log n)。但是在同一层次结构的两个程序调用中,不会处理到原来数列的相同部分;因此,程序调用的每一层次结构总共全部仅需要O(n)的时间(每个调用有某些共同的额外耗费,但是因为在每一层次结构仅仅只有O(n)个调用,这些被归纳在O(n)系数中)。结果是这个算法仅需使用O(n log n)时间。

##快速排序演示

#5.希尔排序
希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因DL.Shell于1959年提出而得名。 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

##希尔排序过程

希尔排序的基本思想是:将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身还是使用数组进行排序。

例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样(竖着的元素是步长组成):

1
2
3
4
复制代码13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10

然后我们对每列进行排序:

1
2
3
4
复制代码10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45

将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ]。这时10已经移至正确位置了,然后再以3为步长进行排序:

1
2
3
4
5
6
复制代码10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45

排序之后变为:

1
2
3
4
5
6
复制代码10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94

最后以1步长进行排序(此时就是简单的插入排序了)

##希尔排序的分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码def shell_sort(alist):
n = len(alist)
# 初始步长
gap = n / 2
while gap > 0:
# 按步长进行插入排序
for i in range(gap, n):
j = i
# 插入排序
while j>=gap and alist[j-gap] > alist[j]:
alist[j-gap], alist[j] = alist[j], alist[j-gap]
j -= gap
# 得到新的步长
gap = gap / 2

alist = [54,26,93,17,77,31,44,55,20]
shell_sort(alist)
print(alist)

##时间复杂度

最优时间复杂度:根据步长序列的不同而不同
最坏时间复杂度:O(n2)
稳定想:不稳定
##希尔排序演示

#6.归并排序
归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。

将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。

##归并排序的分析

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
复制代码def merge_sort(alist):
if len(alist) <= 1:
return alist
# 二分分解
num = len(alist)/2
left = merge_sort(alist[:num])
right = merge_sort(alist[num:])
# 合并
return merge(left,right)

def merge(left, right):
'''合并操作,将两个有序数组left[]和right[]合并成一个大的有序数组'''
#left与right的下标指针
l, r = 0, 0
result = []
while l<len(left) and r<len(right):
if left[l] < right[r]:
result.append(left[l])
l += 1
else:
result.append(right[r])
r += 1
result += left[l:]
result += right[r:]
return result

alist = [54,26,93,17,77,31,44,55,20]
sorted_alist = mergeSort(alist)
print(sorted_alist)

##时间复杂度

  • 最优时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(nlogn)
  • 稳定性:稳定

#7.常见排序算法效率比较

#8.搜索

搜索是在一个项目集合中找到一个特定项目的算法过程。搜索通常的答案是真的或假的,因为该项目是否存在。 搜索的几种常见方法:顺序查找、二分法查找、二叉树查找、哈希查找

二分法查找

二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

##二分法查找实现

##(非递归实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码def binary_search(alist, item):
first = 0
last = len(alist)-1
while first<=last:
midpoint = (first + last)/2
if alist[midpoint] == item:
return True
elif item < alist[midpoint]:
last = midpoint-1
else:
first = midpoint+1
return False
testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,]
print(binary_search(testlist, 3))
print(binary_search(testlist, 13))

##(递归实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码def binary_search(alist, item):
if len(alist) == 0:
return False
else:
midpoint = len(alist)//2
if alist[midpoint]==item:
return True
else:
if item<alist[midpoint]:
return binary_search(alist[:midpoint],item)
else:
return binary_search(alist[midpoint+1:],item)

testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,]
print(binary_search(testlist, 3))
print(binary_search(testlist, 13))

##时间复杂度

  • 最优时间复杂度:O(1)
  • 最坏时间复杂度:O(logn)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

探索支付宝账单的技术实现

发表于 2018-01-09

2017年度的支付宝账单果然不负众望,再一次刷屏了。

回顾一下这个年关,现象级的刷屏活动就有三起:

秀“18岁”;秀网易音乐歌单;秀支付宝账单。

一位网友调侃道:2018年大型“相亲”节目就此拉开帷幕……

秀“18岁”,看颜值;

秀网易音乐歌单,晒品味;

秀支付宝账单,炫财富。

“相亲”的三个重要指标不就都齐了吗?!

玩笑归玩笑,身为一名程序员的我,“职业病”又犯了。就像微信出品的“跳一跳”小程序,各种外挂,刷分攻略络绎不绝,占领各大技术媒体头条。

于是乎,我决定探究一下支付宝账单背后的技术实现。但这并不意味着我会再造一个支付宝账单出来,毕竟这份账单核心部分是背后的海量消费数据,这并不是我等刁民能获取得到的。

由于个人经验水平有限,本文所作猜想如有不足之处,敬请指正。如果有蚂蚁金服的同学愿意分享账单背后的技术架构,想必也是非常受欢迎的。

本文是对支付宝账单技术实现的个人猜想,我将此简单粗暴的分成了两部分:前端,后端。由表及里,从看得见的前端展示来推测看不见的后端逻辑。

一、寻找突破口

整个探索过程的第一步就是找入口,我认为这是一个非常关键的突破口。我将账单通过钉钉分享出来,然后进入钉钉的PC版,右键那条分享记录,即可复制整个URL。

dingding

为了方便讲解,这里把整个URL放出来给大家看看:

1
复制代码https://render.alipay.com/p/s/i/?scheme=alipays://platformapi/startapp?appId=68687017&showOptionMenu=NO&allowsBounceVertical=NO&transparentTitle=auto&bizScenario=Share&url=https://render.alipay.com/p/f/fd-jbg7if4k/index.html

整个scheme参数的作用就是会去尝试打开手机上的支付宝应用,这对于做移动开发的同学来说是很容易看懂的。当然这种尝试不一定都是成功的,这要看浏览器的安全策略了,以下是在Safari(iPhone)和Chrome(PC)中打开的结果:

scheme

scheme自带了6个参数,这都将会传入到APP中,执行相应的操作。本以为每个用户分享出来的scheme参数会有所不同,但是经过我一番对比后,发现都是同样的参数。经过后面对源码的一番研究后,发现「bizScenario」参数会不同,因为我是从钉钉分享找到突破口的,所以bizScenario的值都是Share,但如果是从其他渠道打开的,取值就不一定是一样了。

至于用户信息,应该是在页面中通过js与native进行交互获取的。如果我们想看最终的账单页面的话,直接把scheme当中的url参数copy出来,在浏览器中打开即可。

至此,我们就打开了一扇探索支付宝账单的大门了。

二、前端页面实现

首先需要明白的是支付宝账单是一个Single Page Application,换言之,就是一个由html+js实现的单页应用,我了解到的是静态背景图片+过场动画+数据图层。这样做不仅有效解决了跨平台问题,而且更有利于传播。

值得一提的是整个应用的css和js文件,包括资源请求url等,都做了一定的加密和混淆,要想读懂,还是有一定困难的,尤其是js代码。

静态背景图片总共有9张,我这里放三张,大家可以感受一下。

这里写图片描述

过场动画其实是一系列的mp4文件组成的,放在video标签里播放,篇幅有限,这里只放一段视频,让大家感受一下。
至于数据来源及其图层都是通过js来完成的,下面的截图来自chrome控制台,展现了页面的主体DOM元素。

DOM

大致分为了三个部分:加载,账单主体内容,错误提示。

加载过程中主要有两个元素,一个是加载动画,其实就是一个翻日历的gif动画;另一个就是进度百分比,这个不用细想,肯定做的是假的进度指示。通过**「AlipayJSBridge」**,委托Native APP发送账单数据请求,在这个过程中,进度指示按照一定的速率增长,大概是到了97%的时候会停下来,直到数据获取完毕了再正式进入账单页面。

账单主体内容由三部分组成:第一个是swiper,滑动屏幕切换场景,其下有9个子元素,9张静态图片,分别对应了9个场景;第二个和第三个部分则是两个video标签,分别播放下一个场景和上一个场景的mp4文件。

错误提示的部分没什么好解释的,一行提示文字,一个重试按钮,一目了然。

有读者可能会问,这些图片、视频之类的资源怎么得到的?这是我接下来要讲解的内容。

先来看一段数据结构的定义:

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
复制代码// 静态资源。
window.resource = [
{
"scene": "https://gw.alipayobjects.com/zos/rmsportal/epRpbpBcCZIKasROmxcL.jpg",
"video": {
"forward": "",
"back": ""
},
"__key": 9
},
{
"scene": "https://gw.alipayobjects.com/zos/rmsportal/epRpbpBcCZIKasROmxcL.jpg",
"video": {
"forward": "https://gw.alipayobjects.com/os/rmsportal/KwIPQNAHVfCMQSToOqxX.mp4",
"back": "https://gw.alipayobjects.com/os/rmsportal/zRAHVrBufLRAlmOMwXgA.mp4"
},
"__key": 1
},
{
"scene": "https://gw.alipayobjects.com/zos/rmsportal/ALzmXZZYrnFDqYFFrGjY.jpg",
"video": {
"forward": "https://gw.alipayobjects.com/os/rmsportal/QuZvhHxfIyRqgNxGQRqq.mp4",
"back": "https://gw.alipayobjects.com/os/rmsportal/zRAHVrBufLRAlmOMwXgA.mp4"
},
"__key": 2
},
{
"scene": "https://gw.alipayobjects.com/zos/rmsportal/ZcmJnyzRQNuFspDzfoxX.jpg",
"video": {
"forward": "https://gw.alipayobjects.com/os/rmsportal/CLryDglMNEQLfDxmYnUW.mp4",
"back": "https://gw.alipayobjects.com/os/rmsportal/nmqWCcwURUxRdUqgdJje.mp4"
},
"__key": 3
},
{
"scene": "https://gw.alipayobjects.com/zos/rmsportal/qQLBJCGEDtXCoCiOtPzc.jpg",
"video": {
"forward": "https://gw.alipayobjects.com/os/rmsportal/TUKbyXyonamHXwQifpDQ.mp4",
"back": "https://gw.alipayobjects.com/os/rmsportal/rwcPCPShQgxvVYfobSeU.mp4"
},
"__key": 4
},
{
"scene": "https://gw.alipayobjects.com/zos/rmsportal/zQzcNkGFJJqHinrABCDa.jpg",
"video": {
"forward": "https://gw.alipayobjects.com/os/rmsportal/OBocUmcqGaHuZJJkNQvV.mp4",
"back": "https://gw.alipayobjects.com/os/rmsportal/HbyqRtKZMlKFQZcxwCJy.mp4"
},
"__key": 5
},
{
"scene": "https://gw.alipayobjects.com/zos/rmsportal/JzzNsINqFRVOCAMNJonO.jpg",
"video": {
"forward": "https://gw.alipayobjects.com/os/rmsportal/kBYTpmZElHykGKYytIIG.mp4",
"back": "https://gw.alipayobjects.com/os/rmsportal/zvFLfvTTgXdUUCFHuIlv.mp4"
},
"__key": 6
},
{
"scene": "https://gw.alipayobjects.com/zos/rmsportal/vOpPBFXzjbvlSAxZBcSB.jpg",
"video": {
"forward": "https://gw.alipayobjects.com/os/rmsportal/dIPIDFjkwlJkxwjbtmRO.mp4",
"back": "https://gw.alipayobjects.com/os/rmsportal/VIbUIjvACyUlBSVULXbB.mp4"
},
"__key": 7
},
{
"scene": "https://gw.alipayobjects.com/zos/rmsportal/HJUaMfRgdvNwrxJgibsJ.jpg",
"video": {
"forward": "https://gw.alipayobjects.com/os/rmsportal/egQUtbPUbknxskiWkGgU.mp4",
"back": "https://gw.alipayobjects.com/os/rmsportal/JlBuZvsTbtOGIiUDNTuW.mp4"
},
"__key": 8,
"poster": "https://gw.alipayobjects.com/zos/rmsportal/guBrTMglMdSRsWcRnaXB.jpg"
}
];

这段代码出自账单页面,从Chrome控制台里复制出来的。

scene 为静态图片,用作背景;forward 为前进动画,back 为后退动画;poster 似乎意义不大(从实现上考虑),可以认为是对scene的“备胎”;至于__key字段最值得斟酌,在以下给出的代码中,会有我对此字段的理解。

特别注意一下最后一组场景对象,forward 里面的视频内容在整个账单似乎都没有出现过,一个不小心就让我看到了动画制作的外包商名字了,这是程序猿哥哥加的“鸡腿”吗?

还需要注意的是 forward 和 back 的取值含义。forward 字段的取值含义还是比较好理解,就是当前场景滑向下一个场景所播放的过场动画,而 back 字段的取值含义稍微有点绕,我直接举个例子:__key=3的场景对象中,back字段记录的是回退到__key=2的场景的过场动画。

因为js源代码被混淆得实在是没法看懂了,只能根据交互的效果来猜想代码实现。以下是场景切换的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码//此处也可以直接赋值为0,猜想__key的作用,为了用上此字段
var index = window.resource[0].__key;
var len = window.resource.length;
function next() {
if(index == window.resource[len - 1].__key) return; //at the last page
index = (index + 1) % len;
var resource = window.resource[index];
//1. play video in resource.forward.
//2. video player listener binding.
//3. to display data with animations after forward video completed.
}
function previous() {
if(index == window.resource[0].__key) return; //at the first page
var resource = window.resource[index];
//1. play video in resource.back.
//2. video player listener binding.
//3. to display data with animations after back video completed.
//4. set value for index variable.
index = (index == window.resource[1].__key) ? window.resource[0].__key : index--;
}

主要是通过对len取模,对场景进行前后切换,这样做可以达到循环播放的效果。对于__key字段的猜想也在代码中体现了,值得一提的是,在所有的js源代码中搜索了一番,并没有发现有任何地方用到了这个字段,不知道是不是被加密混淆了。

至此,关于前端的页面展示部分的介绍就结束了。

三、后端数据整合

这部分内容将重点介绍支付宝账单数据的形成,纯属个人对支付宝技术架构的了解进行猜想的,并不代表是真实的运作情况。

架构图

以上是我认为比较合理的架构图,架构的视角放在了Data层面。

1、Database,这一个层次表示的是云数据库集群,整个集群中的数据库极有可能是异构的,如MySQL,Oracle,PostgreSQL,MongoDB等等,此外,这里所说的集群也涵盖了淘宝,天猫,支付宝等阿里体系中的产品所使用的数据库,所以这一部分承载了较多的数据输入输出的工作,至关重要。

2、DW,Data Warehouse,即数据仓库。其中重要的数据来源是云数据库集群,也会有一些直接来自文件。在数据仓库里面能实现的功能就非常多了,其中当属ETL工作,这也是BI的必经之路,配合Reporting System,可以实现数据可视化,日志分析,运维监控等功能。

3、MaxCompute,这个其实是属于阿里云的一个大规模分布式计算平台,其中以Hadoop、Spark为代表的分布式计算框架,Hadoop擅长离线计算,Spark则可以完成快速实时计算。

4、DRDS和REST APIs。DRDS同样也是阿里云出品的数据库中间件产品,上述提到过云数据库集群是异构的,必须有一个中间件参与数据的读写工作。至于REST APIs,主要是提供一些列的API,以便客户端进行数据操作。

解释完了整张架构图后,我再进一步将整个数据请求流程梳理一遍。

1、数据的产生。主要是用户2017年度的消费记录,来自天猫,淘宝,支付宝,蚂蚁信用等平台,这些数据大部分被结构化的存储在了数据库集群中;

2、年度账单数据的生成。将用户2017年度的消费数据导入到数据仓库中,经由分布式计算平台离线计算出每位用户的账单数据,将这份结构化的账单数据再放入数据库集群中。这里使用离线计算是比较明智的,毕竟数据都是PB级别的,实时计算也只能针对个别用户,不然的话,会对用户体验造成一定的影响。这部分的工作,简单地说,就是写若干个MapReduce任务,在分布式计算平台上跑2~3天应该就差不多了;

3、数据获取。到这个步骤,说明账单数据已经准备就绪了,客户端只需要调用API即可获取,也就能做出我们现在所看到的账单页面了。

四、一点小改进

基于对前端展示的研究,我才做出了上述架构的猜想,但这并不是我第一直觉的产物。我一直认为像支付宝账单,网易歌单这类年度盘点的营销活动,可以使用「页面静态化」技术。当然,这样的架构也是有利有弊,先看一张改进后的架构图。

页面静态化技术

可以看到,前端和后端可以说已经处于一个高度解耦的状态了,后端只负责账单数据的生成,并填充好用户的账单页面,而前端访问指定的静态HTML页面即可。针对这个架构,我们来讨论如下三个问题:

1、html文件命名方式。这个想象空间还是很大的,规则也各式各样,比如简单粗暴地将用户id,生成时间等元素进行Hash。当然,对于文件目录也是有要求的,这里就不再深究了。

2、页面静态化技术选型。理论上,最佳的选择就是CDN技术,这方面的技术在市面上已经比较成熟了,可以放心使用。如果不用CDN的话,那可以考虑利用squid,做一个缓存代理缓存服务,可以认为是精简版的CDN,如果只需要内容分发而不考虑其他更高级的功能,squid不失为一个好的解决方案。

3、适用场景分析。页面静态化最吸引我的地方就是减轻了大量后端数据访问的压力,将压力转移给了CDN,但是大可不必担心,因为这是CDN的长项,实现成本低,不易触及瓶颈,此外,没有额外的网络数据访问,不仅不会暴露API,有一定的安全保障,前端页面也可以做到秒开,给用户带来了绝佳的体验。因此,既然是页面静态化,那么肯定就不适合那些页面频繁改动,或者有强交互的场景。

本来这次支付宝账单页面完全可以静态化,后来发生的「授权协议门」事件让我打消了这个念头,这个小插曲的出现就意味着需要将之前生成好的页面全部失效并整改,又会引起一大波流量,也会引起存储空间的浪费,除非是替换之前的文件。不过细想一下,支付宝账单页面嵌入了动态授权,也就不好做页面静态化了。

五、总结

本文从前端到后端两个层面,对支付宝账单的技术实现做了一次非常浅显的剖析,对于一些无法得知的技术细节,也给了一部分自己的实现思路。如果读者看了这篇文章之后,对此也非常感兴趣,也可以针对这个话题发表自己的想法。

技术汇

每日干货分享,传递互联网世界有价值的讯息,微信公众号:技术汇

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

依赖注入不是Java的专利,Golang也有

发表于 2018-01-09

笔者在使用Golang的时候就发现构建系统依赖树非常繁琐,New了很多对象,又手工代码将它们拼接起来,写了一堆非常冗繁的代码。然后就开始想,要是Golang像Java一样有一个好用的依赖注入框架就好啦。

果不其然,Golang还真有,居然还是大厂facebook团队开源的。


Golang的很多用户都不是来自Java,依赖注入他们可能听过,可是从来没有玩过。为了说明依赖注入有多好用,我先用Java代码来解释一下。

先来看一下没有依赖注入的Java世界是怎样的

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
复制代码class DBEngine {
}

class CacheEngine {

}

class UserDB {

private DBEngine db;
private CacheEngine cache;

public UserDB(DBEngine db, CacheEngine cache) {
this.db = db;
this.cache = cache;
}

}

class ItemDB {

private DBEngine db;
private CacheEngine cache;

public ItemDB(DBEngine db, CacheEngine cache) {
this.db = db;
this.cache = cache;
}

}

class UserService {

private UserDB db;

public UserService(UserDB db) {
this.db = db;
}

}

class ItemService {

private ItemDB db;

public ItemService(ItemDB db) {
this.db = db;
}

}

class App {

private UserService user;
private ItemService item;

public App(UserService user, ItemService item) {
this.user = user;
this.item = item;
}
}

public class GuiceTest {

public static void main(String[] args) {
DBEngine db = new DBEngine();
CacheEngine cache = new CacheEngine();
UserDB userDB = new UserDB(db, cache);
ItemDB itemDB = new ItemDB(db, cache);
UserService userServ = new UserService(userDB);
ItemService itemServ = new ItemService(itemDB);
App app = new App(userServ, itemServ);
}

}

在main方法里面,我们new出来很多对象,然后用他们构造了一颗依赖树。我们还写了很多构造器,为了能方便的构造出每个节点。


然后我们用依赖注入框架来改造它。

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
60
61
62
63
64
65
66
67
68
69
70
71
复制代码class DBEngine {
}

class CacheEngine {
}

@Singleton
class UserDB {

@Inject
private DBEngine db;
@Inject
private CacheEngine cache;

}

@Singleton
class ItemDB {
@Inject
private DBEngine db;
@Inject
private CacheEngine cache;

}

@Singleton
class UserService {

@Inject
private UserDB db;

}

@Singleton
class ItemService {

@Inject
private ItemDB db;

}

@Singleton
class App {

@Inject
private UserService user;
@Inject
private ItemService item;

}

class AppModule extends AbstractModule {

@Override
protected void configure() {
DBEngine db = new DBEngine();
CacheEngine cache = new CacheEngine();
bind(DBEngine.class).toInstance(db);
bind(CacheEngine.class).toInstance(cache);
}

}

public class GuiceTest {

public static void main(String[] args) {
Injector injector = Guice.createInjector(new AppModule());
App app = injector.getInstance(App.class);
}

}

这里我们使用的是另一个开源大厂google的依赖注入框架Guice。我们发现main方法缩减了很多代码,所有的new操作都不见了,然后我们还发现每个对象的构造器也消失了,取而代之的是多了两个注解@Singleton和@Inject,Singleton表示类是单例的,Inject表示当前字段使用依赖注入自动设置。好处不用多说,一目了然,就是帮我们节省代码,省去了很多系统初始化时构建一系列对象的细节。另外Guice还需要定义一个Module,把依赖树的叶子节点手工实例化一下,叶子结点对象往往不是简单的依赖注入,而需要手动构造。如果把整个系统的状态比喻成现实的物理世界,那么Module里面干的事就是宇宙大爆炸,所有一切对象的输入源点。依赖注入仅仅帮我们省去了中间节点的构建工作。

在我们的例子中,这棵树还谈不上复杂,毕竟节点数很有限,节点之间的连接也很有限。在大型的复杂业务系统中,这样的对象那就是成百上千了,如果没有使用依赖注入的话,那就真是剪不断理还乱了。

好,接下来我们说说facebookgo团队开源的这个Inject框架如何使用。我们还使用上面的例子,用golang 改写一下。

首先,我们看一下没有依赖注入的Golang世界是怎样的。

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
60
61
62
63
64
复制代码type DBEngine struct{}

func NewDBEngine() *DBEngine {
return &DBEngine{}
}

type CacheEngine struct{}

func NewCacheEngine() *CacheEngine {
return &CacheEngine{}
}

type UserDB struct {
db *DBEngine
cache *CacheEngine
}

func NewUserDB(db *DBEngine, cache *CacheEngine) *UserDB {
return &UserDB{db, cache}
}

type ItemDB struct {
db *DBEngine
cache *CacheEngine
}

func NewItemDB(db *DBEngine, cache *CacheEngine) *ItemDB {
return &ItemDB{db, cache}
}

type UserService struct {
db *UserDB
}

func NewUserService(db *UserDB) *UserService {
return &UserService{db}
}

type ItemService struct {
db *ItemDB
}

func NewItemService(db *ItemDB) *ItemService {
return &ItemService{db}
}

type App struct {
user *UserService
item *ItemService
}

func NewApp(user *UserService, item *ItemService) *App {
return &App{user, item}
}

func main() {
db := NewDBEngine()
cache := NewCacheEngine()
userDB := NewUserDB(db, cache)
itemDB := NewItemDB(db, cache)
userServ := NewUserService(userDB)
itemServ := NewItemService(itemDB)
_ = NewApp(userServ, itemServ)
}

跟Java版本一样,定义了不少构造器,然后手工组装依赖树。

然后我们把这段代码改造成facebookgo依赖注入版本的

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
复制代码import "github.com/facebookgo/inject"

type DBEngine struct{}

func NewDBEngine() *DBEngine {
return &DBEngine{}
}

type CacheEngine struct{}

func NewCacheEngine() *CacheEngine {
return &CacheEngine{}
}

type UserDB struct {
db *DBEngine `inject:""`
cache *CacheEngine `inject:""`
}

type ItemDB struct {
db *DBEngine `inject:""`
cache *CacheEngine `inject:""`
}

type UserService struct {
db *UserDB `inject:""`
}

type ItemService struct {
db *ItemDB `inject:""`
}

type App struct {
user *UserService `inject:""`
item *ItemService `inject:""`
}

func main() {
db := NewDBEngine()
cache := NewCacheEngine()
var g inject.Graph
var app App
g.Provide(
&inject.Object{Value: &app},
&inject.Object{Value: db},
&inject.Object{Value: cache})
g.Populate()
// use app do something
}

这个跟Java版本也很类似,只是Module的定义直接放在了main方法里,也就是上面代码中的Provide方法调用,@Singleton不需要了,@Inject变成了inject:""。然后用Populate方法一次性将整个依赖树构建出来就可以使用了。

看这个开源库的源码发现,整个类库的实现才500多行代码。这是多么轻量级的一个类库,只不过代码这么短,功能也不会太多,相比Java的依赖注入而言,它的功能就单一太多了。不过没关系,相比Guice而言这些缺失的功能不是必须的,能帮我们省掉很多代码它已经做得很好了,这就足够了。要知道Java里面非常完善的依赖注入框架80%的代码那都是为了实现那20%的特殊功能,基本功能所需要的代码量是非常少的。

阅读更多相关文章,请关注知乎专栏【码洞】

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

NET Core+MySql+Nginx 容器化部署 1

发表于 2018-01-09

  1. .NET Core容器化@Docker
  2. .NET Core容器化之多容器应用部署@Docker-Compose
  3. .NET Core+MySql+Nginx 容器化部署
  4. GitHub-Demo:Docker.NetCore.MySql
  1. 引言

上两节我们通过简单的demo学习了docker的基本操作。这一节我们来一个进阶学习,完成ASP.NET Core + MySql + Nginx的容器化部署。

本文是基于CentOS 7.4环境进行演示,示例项目可以访问*\Docker.NetCore.MySql*进行下载。

  1. Hello MySQL

同样我们还是以循序渐进的方式来展开。首先来基于Docker来试玩一下MySQL。

2.1. 创建MySql实例

1
2
3
4
5
6
7
8
9
10
复制代码//拉取mysql镜像
docker pull mysql
$ docker images$
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/mysql latest 7d83a47ab2d2 13 days ago 408.2 MB
//创建一个mysql实例
$ docker run --name hello.mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e21bbd84e0b5 mysql "docker-entrypoint.sh" 3 minutes ago Up 3 minutes 3306/tcp hello.mysql

下面我们直接在容器中连接到我们刚刚创建的mysql数据库:

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
复制代码$ docker exec -it hello.mysql \
> mysql -uroot -p123456
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 5.7.20 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.00 sec)

2.2. 挂载数据卷

上面创建的mysql实例其数据都在容器内部存储,这样就暴露了一个问题,如果容器销毁,那么对应的数据库数据就会丢失。那如何持久化存储容器内数据呢?我们可以通过挂载数据卷的方式来解决这一问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码//创建数据卷
$ docker volume create --name hello.db
hello.db
//查看数据卷信息
$ docker volume inspect hello.db
[
{
"Name": "hello.db",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/hello.db/_data",
"Labels": {},
"Scope": "local"
}
]
// 挂载数据卷启动MySql实例
$ docker run --name hello.mysql \
> -v hello.db:/var/lib/mysql \
> -e MYSQL_ROOT_PASSWORD=123456 -d mysql

上面是使用使用了docker volume create命令创建了一个数据卷,当然我们也可以自行挂载某个目录作为数据卷。

  1. 准备.NET Core+EFCore+MySql项目

为了演示方便,我准备了一个ASP.NET Core+EFCore+MySql的示例项目。其结构如下所示:

示例项目结构

是基于.NET Core Mvc模板项目,其中定义了一个Product实体,并通过ProductsController暴露WebApi接口。核心代码如下:
Product实体类:

1
2
3
4
5
6
7
复制代码public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int StockQty { get; set; }
}

DbContext类:

1
2
3
4
5
6
7
8
9
复制代码public class MySqlDbContext : DbContext
{
public MySqlDbContext (DbContextOptions<MySqlDbContext> options)
: base(options)
{
}

public DbSet<Product> Products { get; set; }
}

数据库初始化类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码public class DbInitializer
{
public static void Initialize(MySqlDbContext context)
{
context.Database.EnsureCreated();

if (context.Products.Any())
{
return;
}

var products = new Product[]
{
new Product{Name="iphone 6",Price=5000,StockQty=10 },
new Product{Name="iphone 7",Price=6000,StockQty=10 },
new Product{Name="iphone 7 plus",Price=7000,StockQty=10 },
new Product{Name="iphone x",Price=8000,StockQty=10 }
};

context.Products.AddRange(products);

context.SaveChanges();
}
}

该数据库初始化类会在项目启动时运行。详细代码可参考**Docker.NetCore.MySql**。

  1. 基于示例项目进行实操演练

4.1 安装Git并Clone示例项目

1
2
3
4
5
6
7
8
9
10
11
复制代码$ yum install git
$ git --version
git version 1.8.3.1
$ cd ~/demo
$ git clone https://github.com/yanshengjie/Docker.NetCore.MySql.git
Cloning into 'Docker.NetCore.MySql'...
remote: Counting objects: 155, done.
remote: Compressing objects: 100% (125/125), done.
remote: Total 155 (delta 42), reused 123 (delta 25), pack-reused 0
Receiving objects: 100% (155/155), 534.30 KiB | 333.00 KiB/s, done.
Resolving deltas: 100% (42/42), done.

4.2. 构建镜像

细心的你会发现,项目中已经定义了Dockerfile,所以我们可以直接使用docker build构建镜像。

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
复制代码# cd Docker.NetCore.MySql
[root@iZ288a3qazlZ Docker.NetCore.MySql]# ls
appsettings.Development.json docker-compose.yml Program.cs Views
appsettings.json Dockerfile proxy.conf wwwroot
bundleconfig.json Docker.NetCore.MySql.csproj README.md
Controllers LICENSE ScaffoldingReadMe.txt
Data Models Startup.cs
//构建镜像
# docker build -t docker.netcore.mysql .
Sending build context to Docker daemon 3.045 MB
Step 1 : FROM microsoft/dotnet:latest
---> 7d4dc5c258eb
Step 2 : WORKDIR /app
---> Using cache
---> 98d48a4e278c
Step 3 : COPY . /app
---> 6b1bf8bb5261
Removing intermediate container b86460477977
Step 4 : RUN dotnet restore
---> Running in 4e0a46f762bb
Restoring packages for /app/Docker.NetCore.MySql.csproj...
Installing Microsoft.CodeAnalysis.Razor 2.0.0.
.....
Restore completed in 216.83 ms for /app/Docker.NetCore.MySql.csproj.
---> 4df70c77916e
Removing intermediate container 4e0a46f762bb
Step 5 : EXPOSE 5000
---> Running in 11b421b3bd3e
---> 3506253060fe
Removing intermediate container 11b421b3bd3e
Step 6 : ENV ASPNETCORE_URLS http://*:5000
---> Running in 201aabbab72c
---> 7f29963a8d96
Removing intermediate container 201aabbab72c
Step 7 : ENTRYPOINT dotnet run
---> Running in c79f73cba162
---> 9d1fb6ee46cb
Removing intermediate container c79f73cba162
Successfully built 9d1fb6ee46cb
[root@iZ288a3qazlZ Docker.NetCore.MySql]# docker images docker.netcore.mysql
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.netcore.mysql latest 9d1fb6ee46cb 13 seconds ago 1.756 GB

4.3. 启动镜像并连接到指定数据库

docker提供了--link参数用于在容器之间建立连接。下面我们实例化创建的镜像docker.netcore.mysql并命名容器名为hello.netcore.mysql,并使用–link参数与我们文章开头建立的hello.mysql容器建立连接。

1
2
复制代码# docker run --name hello.netcore.mysql --link hello.mysql:db -d -p 5000:5000 
docker.netcore.mysql

这里需要特别注意一下--link=hello.mysql:db,这个参数就是告诉Docker容器需要使用hello.mysql容器,并将其别名命名为db,这样在hello.netcore.mysql这个容器中就可以使用db来作为提供mysql数据库服务的服务器。这也就是为什么我们.NET Core项目中连接字符串设置为server=db;的原因。
"ConnectionStrings": { "MySql": "server=db;database=MySqlDbContext;uid=root;pwd=123456;" }

1
2
3
4
5
6
7
8
复制代码//查看运行中容器列表
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5cbfd27ebe2a docker.netcore.mysql "dotnet run" 2 minutes ago Up 2 minutes 0.0.0.0:5000->5000/tcp hello.netcore.mysql
4dfa4159b669 mysql "docker-entrypoint.sh" About an hour ago Up About an hour 3306/tcp hello.mysql
//访问api/products
[root@iZ288a3qazlZ Docker.NetCore.MySql]# curl http://localhost:5000/api/products
[{"productId":1,"name":"iphone 6","price":5000.0000000000000000000000000,"stockQty":10},{"productId":2,"name":"iphone 7","price":6000.0000000000000000000000000,"stockQty":10},{"productId":3,"name":"iphone 7 plus","price":7000.0000000000000000000000000,"stockQty":10},{"productId":4,"name":"iphone x","price":8000.000000000000000000000000,"stockQty":10}]

从上图可知,我们完成了.NET Core与MySql的连接。

  1. ASP.NET Core + MySql + Nginx

结合上一篇文章.NET Core容器化之多容器应用部署@Docker-Compose,我们来使用docker-compose完成asp.net core + mysql + nginx的多容器部署。

5.1. 定义 docker-compose.yml

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
复制代码version: '2'
services:
db:
container_name: hello.db
environment:
MYSQL_ROOT_PASSWORD: 123456
volumes:
- ./mysql:/var/lib/mysql

web:
container_name: hello.web
build: .
depends_on:
- db
links:
- db

reverse-proxy:
container_name: hello.proxy
image: nginx
depends_on:
- web
ports:
- "9090:8080"
volumes:
- ./proxy.conf:/etc/nginx/conf.d/default.conf

其中定义了三个服务:

  1. db:使用mysql镜像,并挂载当前项目下的mysql文件夹来持久化存储。
  2. web:基于当前项目构建的容器服务,依赖于db服务。
  3. reverse-proxy:使用nginx定义反向代理服务,其中挂载了当前项目下的proxy.conf文件作为反向代理配置文件。其中proxy.conf的配置如下(注意proxy_pass指定的url为http://web:5000):
1
2
3
4
5
6
7
复制代码server {
listen 8080;

location / {
proxy_pass http://web:5000;
}
}

5.2. 启动Compose

在启动Compose之前,建议清空上面创建的容器。也可以使用docker rm $(docker ps -qa)清除所有容器。

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
复制代码//启动compose
[root@iZ288a3qazlZ Docker.NetCore.MySql]# docker-compose up -d
Creating network "dockernetcoremysql_default" with the default driver
Building web
Step 1 : FROM microsoft/dotnet:latest
---> 7d4dc5c258eb
Step 2 : WORKDIR /app
---> Using cache
---> 98d48a4e278c
Step 3 : COPY . /app
---> d41b32323c0f
Removing intermediate container 1259f5fb82bc
Step 4 : RUN dotnet restore
---> Running in d482e355de77
Restoring packages for /app/Docker.NetCore.MySql.csproj...
Installing Microsoft.CodeAnalysis.Razor 2.0.0.
.....
Restore completed in 216.83 ms for /app/Docker.NetCore.MySql.csproj.
---> a0658008f161
Removing intermediate container d482e355de77
Step 5 : EXPOSE 5000
---> Running in dc6eeb29fd5e
---> a419314ece08
Removing intermediate container dc6eeb29fd5e
Step 6 : ENV ASPNETCORE_URLS http://*:5000
---> Running in c1d1474b14a0
---> 9cc13c549042
Removing intermediate container c1d1474b14a0
Step 7 : ENTRYPOINT dotnet run
---> Running in efdf0e857a84
---> 830ac11428cf
Removing intermediate container efdf0e857a84
Successfully built 830ac11428cf
Creating hello.db ... done
Creating hello.web ... done
Creating hello.proxy ... done
Creating hello.web ...
Creating hello.proxy ...
[root@iZ288a3qazlZ Docker.NetCore.MySql]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6253bf85682e nginx "nginx -g 'daemon off" 33 seconds ago Up 28 seconds 80/tcp, 0.0.0.0:9090->8080/tcp hello.proxy
ea553a9e22f2 dockernetcoremysql_web "dotnet run" 37 seconds ago Up 32 seconds 5000/tcp hello.web
a1f5aa981bfb mysql "docker-entrypoint.sh" 38 seconds ago Up 36 seconds 3306/tcp hello.db
[root@iZ288a3qazlZ Docker.NetCore.MySql]# docker-compose ps
Name Command State Ports
----------------------------------------------------------------------------------
hello.db docker-entrypoint.sh mysqld Up 3306/tcp
hello.proxy nginx -g daemon off; Up 80/tcp, 0.0.0.0:9090->8080/tcp
hello.web dotnet run Up 5000/tcp
[root@iZ288a3qazlZ Docker.NetCore.MySql]# curl http://localhost:9090/api/products
[{"productId":1,"name":"iphone 6","price":5000.0000000000000000000000000,"stockQty":10},{"productId":2,"name":"iphone 7","price":6000.0000000000000000000000000,"stockQty":10},{"productId":3,"name":"iphone 7 plus","price":7000.0000000000000000000000000,"stockQty":10},{"productId":4,"name":"iphone x","price":8000.000000000000000000000000,"stockQty":10}]

上面的运行结果显示,我们已经成功完成了ASP.NET Core+MySql+Nginx的多容器应用部署。通过浏览器访问http:<ipaddress>:9090/api/products即可访问我们暴露的api。

5.3. 数据库验证

我们来验证一下数据库是否成功创建:

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
复制代码[root@iZ288a3qazlZ Docker.NetCore.MySql]# ls mysql
auto.cnf client-key.pem ib_logfile0 performance_schema server-key.pem
ca-key.pem MySqlDbContext ib_logfile1 private_key.pem sys
ca.pem ib_buffer_pool ibtmp1 public_key.pem
client-cert.pem ibdata1 mysql server-cert.pem
[root@iZ288a3qazlZ Docker.NetCore.MySql]# docker exec -it hello.db mysql -uroot -p123456
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 5.7.20 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases;
+-----------------------+
| Database |
+-----------------------+
| information_schema |
| MySqlDbContext |
| mysql |
| performance_schema |
| sys |
+-----------------------+
5 rows in set (0.00 sec)

mysql> use MySqlDbContext;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+---------------------------------+
| Tables_in_MySqlDbContext |
+---------------------------------+
| Products |
+---------------------------------+
1 row in set (0.00 sec)

mysql> select * from Products;
+-----------+---------------+-------------------------------------+----------+
| ProductId | Name | Price | StockQty |
+-----------+---------------+-------------------------------------+----------+
| 1 | iphone 6 | 5000.000000000000000000000000000000 | 10 |
| 2 | iphone 7 | 6000.000000000000000000000000000000 | 10 |
| 3 | iphone 7 plus | 7000.000000000000000000000000000000 | 10 |
| 4 | iphone x | 8000.000000000000000000000000000000 | 10 |
+-----------+---------------+-------------------------------------+----------+
4 rows in set (0.00 sec)

从上面的运行结果可知,我们成功将项目文件夹下的mysql文件夹挂载到容器内部进行数据持久化。

  1. 最后

本文通过先介绍如何基于Docker实例化MySQL容器,再介绍如何通过挂载数据卷来持久化MySQL数据,以及如何使用–Link参数进行容器之间的连接,完成了.NET Core连接MySQL数据库。
最后,使用Docker-Compose综合ASP.NET Core+MySQL+Nginx完成了容器化部署。

下一节我们来介绍下如何使用Docker-Swarm进行集群部署。

  1. 参考资料

  1. mysql -Docker Documentation
  2. Hello Docker
  3. .NET Core容器化@Docker
  4. .NET Core容器化之多容器应用部署@Docker-Compose

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

代码审计工具 Cobra 源码分析(一)

发表于 2018-01-05

0x00 前言

@0r3ak 师傅向我推荐了一款代码审计工具Cobra(wufeifei/cobra),该工具基于Python开发,可以针对多种语言的源代码安全性评估。

在测试的过程中,总是不得要领,有一些问题不知道怎么解决,都是又很想了解下这款项目的实现原理。

在这一系列的笔记中,将会记录下对 Cobra 的使用体验,以及源码级的分析。

0x01 基础环境

Ubuntu 16.04.3 LTS

Python 2.7.12

Cobra 2.0.0-alpha.5

pdb

0x02 执行流程&关键代码分析

关于 Cobra 的安装,与基本的使用方法在 Cobra 的文档中说的很详细了。我这里就不在赘述。不了解的同学可以在本文的参考链接中获取 Cobra 的文档。

这篇笔记主要记录下,CLI模式下,对某一个文件中代码做安全审计的过程中,相关的函数调用栈,及关键函数的原理分析。其间会通过pdb动态调试结合静态代码分析以及项目的文档,辅助对整个项目的理解。(关于pdb的使用方法可以参考:Flask debug 模式 PIN 码生成机制安全性研究笔记)

第1步:在cobra.py文件中sys.exit(main())语句前下断点


第2步:执行如下代码(具体的功能为:审计XXX目录下的代码)

1
复制代码python cobra.py -t /home/tonghua/XXX

步入到main()函数中


main()中的代码主要功能是,获取当前命令行敲的命令,并做相应的处理。


通过第18行可知,main()函数在\cobra\\init__.py文件中定义,我们步入到main()函数的定义位置。

第55行args = parser.parse_args(),args变量包含着需要用到的各个变量的值。


由代码可知,debug模式将会执行57行的if逻辑,通过logger记录debug日志,61行的if判断是打印帮助信息,65行的if逻辑是启动RESTful服务。

当然由于我们只执行了-t参数,所以以上if判断均不会进入,直接logger.debug(‘[INIT] start scanning…’)后,继续向下执行。


第68-84行,即为该程序进行代码审计的关键代码。


get_sid()函数(\cobra-master\cobra[cli.py](https://link.zhihu.com/?target=http%3A//cli.py/) 第28-38行),拼接字符串’a+项目文件夹md5的前5位+一个随机字符串’,其功能想必应该是区分不同的扫描项目。

第78行,Running(a_sid).status(data),实例化Running类(调用其__init__()构造方法),然后调用该对象的status()方法。其会向指定的临时文件中写入当前的审计状态。

第82行,调用cli.start()函数,开始扫描,步入到该函数的具体实现代码(\cobra-master\cobra[cli.py](https://link.zhihu.com/?target=http%3A//cli.py/) 第41-122行)。

来到cli.start()函数中,第52-67行,实现了对变量初始化赋值、logger记录扫描报告的URL、确认目标模式(文件夹、还是Git文件等),输出方式。


第70-75行,明确文件夹路径,确认每一个需要扫描的文件位置,统计文件数量


其中对文件夹下的内容进行遍历和统计的功能,主要由\cobra-master\cobra[pickup.py](https://link.zhihu.com/?target=http%3A//pickup.py/)中的Directory()类实现。为了先大体了解下整体的执行流程,这块具体实现细节先不分析。

第78-88行,通过Detection()类中的功能实现对开发语言、开发框架的检测(检测规则后续还可以自行增加)。这里用到了Python的@property语法糖,可以将类中的方法已变量的形式进行调用。


步入scan()函数,顾名思义这里实现了扫描功能。


第153行创建了Rule()实例,接下来几行,对漏洞类型、开发语言、扫描规则等进行定义


第160行,通过scan_cve()函数进行CVE漏洞扫描,步入到该函数(cobra-master\cobra[cve.py](https://link.zhihu.com/?target=http%3A//cve.py/) 第332-361行)中,查看其实现方式。


第348-350行的for循环中,会对【漏洞规则】目录下的,漏洞文件进行遍历,其中【CVI-999】开头的文件为CVE漏洞检测文件,遍历出所有的CVE漏洞文件。

第354行,pool = multiprocessing.Pool(),搞了一个进程池,之后循环遍历所有的CVE漏洞文件,在第358行进行漏洞扫描,pool.apply_async(scan_single, args=(target_directory, cve_path), callback=store),调用scan_single()漏扫函数,扫描完成后回调到store()函数中。

第372行,漏扫函数scan_single()的调用链:scan_single()->cve.scan_cve(),cve.get_scan_result()


这里有一个坑点就是,文件中的scan_cve()函数和CveParse类中的scan_cve()方法同名,导致刚刚跟丢了,2333

分析下cve.scan_cve()方法(第214-225行)


今天就先到这里,剩下的内容明天再更新。

0x03 程序执行链

今天的分析先到这里,脑阔疼。

梳理下截至目前的函数调用链:

[cobra.py](https://link.zhihu.com/?target=http%3A//cobra.py/) line 22 main()

–>\cobra\\init__.py line 82 cli.start() and line 78 Running()

—->\cobra[cli.py](https://link.zhihu.com/?target=http%3A//cli.py/) line 91 scan()

——>\cobra[engine.py](https://link.zhihu.com/?target=http%3A//engine.py/) line 160 scan_cve()

——–>\cobra[cve.py](https://link.zhihu.com/?target=http%3A//cve.py/) line 358 pool.apply_async(scan_single, args=(target_directory, cve_path), callback=store)

———> \cobra[cve.py](https://link.zhihu.com/?target=http%3A//cve.py/) line 371 cve.scan_cve()

pause!!!

另外,Cobra好多文件、类中都采用相同的方法名,翻起来好费劲啊!!!

0x04 后记

希望我可以尽快熟悉这套项目的代码,做一些二次开发、增加漏洞规则的事。同时也期待有一天我可以参与到这个开源项目中来,贡献力量。

暂且不论该工具的漏报、误报情况,因为自动化的代码审计,文件与文件之间的关系、函数与函数之间的调用关系、Web框架提供的操作接口、如何确定URL路由以及不同漏洞类型的检测规则,本来就是一件难度很大的事。漏报、误报在所难免,光是这种分享精神就让我很敬佩了,再次要感谢下该开源项目的作者,让像我一样的菜鸟有一个学习的方向。也要感谢那些帮助我的师傅们,是你们不断的指导着我成长。

0x05 参考链接

Introduction(介绍)

27.3. pdb - The Python Debugger - Python 3.6.4 documentation

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

怎么让不可靠的UDP可靠?

发表于 2018-01-05

最近和很多实时音视频领域的朋友交流中都有谈论到 RUDP(Reliable UDP),这其实是个老生常谈的问题,RUDP 在很多著名的项目上都有使用,例如 Google 的 QUIC 和 webRTC。在 UDP 之上做一层可靠,很多朋友认为这是很不靠谱的事情,也有朋友认为这是一个大杀器,可以解决实时领域里大部分问题。

作为教育公司,学霸君在很多实时场景下确实使用 RUDP 技术来解决我们的问题,不同场景我们采用的 RUDP 方式也不一样。先来看看学霸君哪些场景使用了 RUDP:

  • 全局 250 毫秒延迟的实时 1V1 答疑,采用的是 RUDP + 多点 relay 智能路由方案。
  • 500 毫秒 1080P 视频连麦互动系统,采用的是 RUDP + PROXY 调度传输方案。
  • 6 方实时同步书写系统,采用的是 RUDP+redo log 的可靠传输技术。
  • 弱网 Wi-Fi 下 Pad 的 720P 同屏传输系统,采用的是 RUDP +GCC 实时流控技术。
  • 大型直播的 P2P 分发系统,通过 RUDP + 多点并联 relay 技术节省了 75% 以上的分发带宽。

涉及到实时传输我们都会先考虑 RUDP,RUDP 应用在学霸君核心传输体系的各个方面,但不同的系统场景我们设计了不同的 RUDP 方式,所以基于那些激烈的讨论和我们使用的经验我扒一扒 RUDP。

三角制约

其实在实时通信领域存在一个三角平衡关系:成本、质量和时延三者的制约关系:

图 1

也就是说投入的成本、获得的质量和通信的时延之间是一个三角制约 (LEQ) 关系,所以实时通信系统的设计者会在这三个制约条件下找到一个平衡点,TCP 属于通过增大延迟和传输成本来保证质量的通信方式,UDP 是通过牺牲质量来保证时延和成本的通信方式,所以在一些特定场景下 RUDP 更容易找到这样的平衡点。RUDP 是怎么去找这个平衡点的,就要先从 RUDP 的可靠概念和使用场景来分析。

可靠的概念

在实时通信过程中,不同的需求场景对可靠的需求是不一样的,我们在这里总体归纳为三类定义:

  • 尽力可靠:通信的接收方要求发送方的数据尽量完整到达,但业务本身的数据是可以允许缺失的。例如:音视频数据、幂等性状态数据。
  • 无序可靠:通信的接收方要求发送方的数据必须完整到达,但可以不管到达先后顺序。例如:文件传输、白板书写、图形实时绘制数据、日志型追加数据等。
  • 有序可靠:通信接收方要求发送方的数据必须按顺序完整到达。

RUDP 是根据这三类需求和图 1 的三角制约关系来确定自己的通信模型和机制的,也就是找通信的平衡点。

UDP 为什么要可靠

说到这里可能很多人会说:干嘛那么麻烦,直接用 TCP 好了! 确实很多人也都是这样做的,TCP 是个基于公平性的可靠通信协议,但是在一些苛刻的网络条件下 TCP 要么不能提供正常的通信质量保证,要么成本过高。为什么要在 UDP 之上做可靠保证,究其原因就是在保证通信的时延和质量的条件下尽量降低成本,RUDP 主要解决以下相关问题:

  • 端对端连通性问题:一般终端直接和终端通信都会涉及到 NAT 穿越,TCP 在 NAT 穿越实现非常困难,相对来说 UDP 穿越 NAT 却简单很多,如果是端到端的可靠通信一般用 RUDP 方式来解决,场景有:端到端的文件传输、音视频传输、交互指令传输等等。
  • 弱网环境传输问题:在一些 Wi-Fi 或者 3G/4G 移动网下,需要做低延迟可靠通信,如果用 TCP 通信延迟可能会非常大,这会影响用户体验。例如:实时的操作类网游通信、语音对话、多方白板书写等,这些场景可以采用特殊的 RUDP 方式来解决这类问题。
  • 带宽竞争问题:有时候客户端数据上传需要突破本身 TCP 公平性的限制来达到高速低延时和稳定,也就是说要用特殊的流控算法来压榨客户端上传带宽,例如:直播音视频推流,这类场景用 RUDP 来实现不仅能压榨带宽,也能更好地增加通信的稳定性,避免类似 TCP 的频繁断开重连。
  • 传输路径优化问题:在一些对延时要求很高的场景下,会用应用层 relay 的方式来做传输路由优化,也就是动态智能选路,这时双方采用 RUDP 方式来传输,中间的延迟进行 relay 选路优化延时。还有一类基于传输吞吐量的场景,例如:服务与服务之间数据分发、数据备份等,这类场景一般会采用多点并联 relay 来提高传输的速度,也是要建立在 RUDP 上的(这两点在后面着重来描述)。
  • 资源优化问题:某些场景为了避免 TCP 的三次握手和四次挥手的过程,会采用 RUDP 来优化资源的占用率和响应时间,提高系统的并发能力,例如 QUIC。

不管哪类场景,都是要保证可靠性,也就是质量,那么在 UDP 之上怎么实现可靠呢?答案就是重传。

重传模式

IP 协议在设计的时候就不是为了数据可靠到达而设计的,所以 UDP 要保证可靠,就依赖于重传,这也就是我们通常意义上的 RUDP 行为,在描述 RUDP 重传之前先来了解下 RUDP 基本框架,如图:

图 2

RUDP 分为发送端和接收端,每一种 RUDP 在设计的时候会做不一样的选择和精简,概括起来就是图中的单元。RUDP 的重传是发送端通过接收端 ACK 的丢包信息反馈来进行数据重传,发送端会根据场景来设计自己的重传方式,重传方式分为三类:定时重传、请求重传和 FEC 选择重传。

定时重传

定时重传很好理解,就是发送端如果在发出数据包(T1)时刻一个 RTO 之后还未收到这个数据包的 ACK 消息,那么发送端就重传这个数据包。这种方式依赖于接收端的 ACK 和 RTO,容易产生误判,主要有两种情况:

  • 对方收到了数据包,但是 ACK 发送途中丢失。
  • ACK 在途中,但是发送端的时间已经超过了一个 RTO。

所以超时重传的方式主要集中在 RTO 的计算上,如果你的场景是一个对延迟敏感但对流量成本要求不高的场景,就可以将 RTO 的计算设计得比较小,这样能尽最大可能保证你的延时足够小。

例如:实时操作类网游、教育领域的书写同步,是典型的用 expense 换 latency 和 quality 的场景,适合用于小带宽低延迟传输。如果是大带宽实时传输,定时重传对带宽的消耗是很大的,极端情况会有 20% 的重传率,所以在大带宽模式下一般会采用请求重传模式。

请求重传

请求重传就是接收端在发送 ACK 的时候携带自己丢失报文的信息反馈,发送端接收到 ACK 信息时根据丢包反馈进行报文重传。如下图:

图 3

这个反馈过程最关键的步骤就是回送 ACK 的时候应该携带哪些丢失报文的信息,因为 UDP 在网络传输过程中会乱序会抖动,接收端在通信的过程中要评估网络的 jitter time,也就是 rtt_var(RTT 方差值),当发现丢包的时候记录一个时刻 t1,当 t1 + rtt_var < curr_t(当前时刻),我们就认为它丢失了。

这个时候后续的 ACK 就需要携带这个丢包信息并更新丢包时刻 t2,后续持续扫描丢包队列,如果 t2 + RTO <curr_t,则再次在 ACK 携带这个丢包信息,以此类推,直到收到报文为止。

这种方式是由丢包请求引起的重发,如果网络很不好,接收端会不断发起重传请求,造成发送端不停的重传,引起网络风暴,通信质量会下降,所以我们在发送端设计一个拥塞控制模块来限流,这个后面我们重点分析。

整个请求重传机制依赖于 jitter time 和 RTO 这个两个时间参数,评估和调整这两个参数和对应的传输场景也息息相关。请求重传这种方式比定时重传方式的延迟会大,一般适合于带宽较大的传输场景,例如:视频、文件传输、数据同步等。

FEC 选择重传

除了定时重传和请求重传模式以外,还有一种方式就是以 FEC 分组方式选择重传,FEC(Forward Error Correction)是一种前向纠错技术,一般通过 XOR 类似的算法来实现,也有多层的 EC 算法和 raptor 涌泉码技术,其实是一个解方程的过程。应用到 RUDP 上示意图如下:

图 4

在发送方发送报文的时候,会根据 FEC 方式把几个报文进行 FEC 分组,通过 XOR 的方式得到若干个冗余包,然后一起发往接收端,如果接收端发现丢包但能通过 FEC 分组算法还原,就不向发送端请求重传,如果分组内包是不能进行 FEC 恢复的,就向发送端请求原始的数据包。

FEC 分组方式适合解决要求延时敏感且随机丢包的传输场景,在一个带宽不是很充裕的传输条件下,FEC 会增加多余的包,可能会使得网络更加不好。FEC 方式不仅可以配合请求重传模式,也可以配合定时重传模式。

RTT 与 RTO 的计算

在上面介绍重传模式时多次提到 RTT、RTO 等时间度量,RTT(Round Trip Time)即网络环路延时,环路延迟是通过发送的数据包和接收到的 ACK 包计算的,示意图如下:

图 5

RTT = T2 - T1

这个计算方式只是计算了某一个报文时刻的 RTT,但网络是会波动的,这难免会有噪声现象,所以在计算的过程中引入了加权平均收敛的方法(具体可以参考 RFC793)

SRTT = (α * SRTT) + (1-α)RTT

这样可以求得逼近的 SRTT,在公式中一般α=0.8,确定了 SRTT,下一步就是计算 RTT_VAR(方差),我们设 RTT_VAR = |SRTT – RTT|,那么 SRTT_VAR =(α * SRTT_VAR) + (1-α) RTT_VAR,这样可以得到 RTT_VAR 的值。

但最终我们是需要 RTO,因为涉及到报文重传,RTO 就是一个报文的重传周期,从网络的通信流程我们很容易知道,重传一个包以后,如果一个 RTT+RTT_VAR 之后的时间还没收到确定,那我们就可以再次重传,则可知:RTO = SRTT + SRTT_VAR。

但一般网络在严重抖动的情况下还是会有较大的重复率问题,所以:RTO = β*(SRTT + RTT_VAR),1.2 <β<2.0,可以根据不同的传输场景来选择β的值。

窗口与拥塞控制

RUDP 是通过重传来保证可靠的,重传在三角平衡关系中其实是用 expense 和 latency 来换取 quality 的行为,所以重传会引来两个问题,一个是延时,一个是重传的带宽,尤其是后者,如果控制不好会引来网络风暴,所以在发送端会设计一个窗口拥塞机制了避免并发带宽占用过高的问题。

窗口

RUDP 需要一个收发的滑动窗口系统来配合对应的拥塞算法做流量控制,有些 RUDP 需要发送端和接收端的窗口严格地对应,有些 RUDP 不要求收发窗口严格对应。如果涉及到可靠有序的 RUDP,接收端就要做窗口排序和缓冲,如果是无序可靠或者尽力可靠的场景,接收端一般就不做窗口缓冲,只做位置滑动。先来看收发窗口关系图:

图 6

上图描述的是发送端从发送窗口中发了 6 个数据报文给接收端,接收端收到 101,102,103,106 时会先判断报文的连续性并滑动窗口开始位置到 103,接着每个包都回应 ACK,发送端在接收到 ACK 的时候,会确认报文的连续性,并滑动窗口到 103,发送端会再判断窗口的空余,然后填补新的发送数据,这就是整个窗口滑动的流程。

这里值的一提的是在接收端收到 106 时的处理,如果是有序可靠,那么 106 不会通知上层业务进行处理,而是等待 104、105。如果是尽力可靠和无序可靠场景,会将 106 通知给上层业务先进行处理。在收到 ACK 后,发送端的窗口要滑动多少是由自己的拥塞机制定的,也就是说窗口的滑动速度受拥塞机制控制,拥塞控制实现要么基于丢包率来实现,要么基于双方的通信时延来实现,下面来看几种典型的拥塞控制。

经典拥塞算法

TCP 经典拥塞算法分为四个部分:慢启动、拥塞避免、拥塞处理和快速恢复,这四个部分都是为了决定发送窗口和发送速度而设计的,其实就是为了在当前网络条件下通过网络丢包来判断网络拥塞状态,从而确定比较适合的发送传输窗口。

经典算法是建立在定时重传的基础上的,如果 RUDP 采用这种算法来做拥塞控制,一般的场景是为了保证有序可靠传输的同时又兼顾网络传输的公平性原则。先逐个来解释下这几部分。

慢启动(slow start)

当连接链路刚刚建立后,不可能一开始将 cwnd 设置得很大,这样容易造成大量重传,经典拥塞里面会在开始将 cwnd = 1,然后根据通信过程的丢包情况来逐步扩大 cwnd 来适应当前的网络状态,直到达到慢启动的门限阈值 (ssthresh),步骤如下:

  1. 初始化设置 cwnd = 1,并开始传输数据。
  2. 收到回馈的 ACK,会将 cwnd 加 1。
  3. 当发送端一个 RTT 后且未发现有丢包重传,就会将 cwnd = cwnd * 2。
  4. 当 cwnd >= ssthresh 或发生丢包重传时慢启动结束,进入拥塞避免状态。

拥塞避免

当通信连接结束慢启动后,有可能还未到网络传输速度的上线,这个时候需要进一步通过一个缓慢的调节过程来进行适配。一般是一个 RTT 后如果未发现丢包,就将 cwnd = cwnd + 1。一但发现丢包和超时重传,就进入拥塞处理状态。

拥塞处理

拥塞处理在 TCP 里面实现很暴力,如果发生丢包重传,直接将 cwnd = cwnd / 2,然后进入快速恢复状态。

快速恢复

通过确认丢包只发生在窗口一个位置的包来确定是否进行快速恢复,如图 6 中描述,如果只是 104 发生了丢失,而 105 和 106 是收到了的,那么 ACK 总是会将 ACK 的 base = 103,如果连续 3 次收到 base 为 103 的 ACK,就进行快速恢复,也就是立即重传 104,而后如果收到新的 ACK 且 base > 103,将 cwnd = cwnd + 1,并进入拥塞避免状态。

经典拥塞控制是基于丢包检测和定时重传模式来设计的,在三角平衡关系中是一个典型的以 latency 换取 quality 的案例,但由于其公平性设计避免了过高的 expense,也就会让这种传输方式很难压榨网络带宽,很难保证网络的大吞吐量和小时延。

BBR 拥塞算法

对于经典拥塞算法的延迟和带宽压榨问题 Google 设计了基于发送端延迟和带宽评估的 BBR 拥塞控制算法。这种拥塞算法致力于解决两个问题:

  • 在一定丢包率网络传输链路上充分利用带宽
  • 降低网络传输中的 buffer 延迟

BBR 的主要策略是周期性通过 ACK 和 NACK 返回来评估链路的 min_rtt 和 max_bandwidth。最大吞吐量(cwnd)的大小就是:cwnd = max_bandwidth / min_rtt。

传输模型如下:

图 7

BBR 整个拥塞控制是一个探测带宽和 Pacing rate 的状态,有 4 个状态:

  • Startup:启动状态(相当于慢启动),增益参数为 max_gain = 2.85
  • DRAIN:满负荷传输状态
  • PROBE_BW:带宽评估状态,通过一个较小的 BBR 增益参数来递增(1.25)或者递减 (0.75)
  • PROBE_RTT:延迟评估状态,通过维持一个最小发送窗口(4 个 MSS)进行的 RTT 采样

那么这几种状态是怎么来回切换的呢?以下是 QUIC 中 BBR 大致的步骤如下:

  1. 初始化连接时会设置一个初始的 cwnd = 8,并将状态设置 Startup。
  2. 在 Startup 下发送数据,根据 ACK 数据的采样周期性判断是否可以增加带宽,如果可以,将 cwnd = cwnd *max_gain。如果时间周期数超过了预设的启动周期时间或者发生了丢包,进行 DRAIN 状态。
  3. 在 DRAIN 状态下,如果 flight_size(发送出去但还未确认的数据大小) >cwnd, 继续保证 DRAIN 状态,如果 flight_size<cwd,进入 PROBE_BW 状态。
  4. 在PROBE_BW状态下,如果未发生丢包且flight_size<cwnd * 1.25,将维持原来的cwnd,并进入StartUp,如果发生丢包或者flight_size > cwnd,将cwnd = cwnd * 1.25,如果发生丢包,cwnd = cwnd * 0.75。
  5. 在 Startup/DRAIN/PROBE_BW 三个状态中,如果持续 10 秒钟的通信中没有出现 RTT <= min_rtt,就会进入到 PROBE_RTT 状态,并将 cwnd = 4 *MSS。
  6. 在 PROBE_RTT 状态,会在收到 ACK 返回的时候持续判断 flight_size >= cwnd 并且无丢包,将本次统计的最小 RTT 作为 min_rtt,进入 Startup 状态。

BBR 通过以上几个步骤来周期性计算 cwnd,也就是网络最大吞吐量和最小延迟,然后通过 pacing rate 来确定这一时刻发送端的码率,最终达到拥塞控制的目的。

BBR 适合在随机丢包且网络稳定的情况下做拥塞,如果在网络信号极不稳定的 Wi-Fi 或者 4G 上,容易出现网络泛洪和预测不准的问题,BBR 在多连接公平性上也存在小 RTT 的连接比大 RTT 的连接更吃带宽的情况,容易造成大 RTT 的连接速度过慢的情况。BBR 拥塞算法在三角平衡关系中是采用 expense 换取 latency 和 quality 的案例。

webRTC GCC

说到音视频传输就必然会想到 webRTC 系统,在 webRTC 中对于视频传输也实现了一个拥塞控制算法 (GCC),webRTC 的 GCC 是一个基于发送端丢包率和接收端延迟带宽统计的拥塞控制,而且是一个尽力可靠的传输算法,在传输的过程中如果一个报文重发太多次后会直接丢弃,这符合视频传输的场景,借用 weizhenwei 同学一张图来看个究竟:

图 8,来源 www.jianshu.com/u/102fafe8c…

GCC 的发送端会根据丢包率和一个对照表来 pacing rate,当 loss < 2% 时,会加大传输带宽,当 loss >=2% &&loss <10%,会保持当前码率,当 loss>=10%,会认为传输过载,进行调小传输带宽。

GCC 的接收端是根据数据到达的延迟方差和大小进行 KalmanFilter 进行带宽逼近收敛,具体的细节不介绍了,请查看 http://www.jianshu.com/p/bb34995c549a。

这里值得一说的是 GCC 引入接收端对带宽进行 KalmanFilter 评估是一个非常新颖的拥塞控制思路,如果实现一个尽力可靠的 RUDP 传输系统不失为是一个很好的参考。

但这种算法也有个缺陷,就是在网络间歇性丢包情况下,GCC 可能收敛的速度比较慢,在一定程度上有可能会造成 REMB 很难反馈给发送端,容易出现发送端流控失效。GCC 在三角平衡关系算一个以 quality 和 expense 换取 latency 的案例。

弱窗口拥塞机制

其实在很多场景是不用拥塞控制或者只要很弱的拥塞控制即可,例如:师生双方书写同步、实时游戏,因为本身的传输的数据量不大,只要保证足够小的延时和可靠性就行,一般会采用固定窗口大小来进行流控,我们在系统中一般采用一个 cwnd =32 这样的窗口来做流控,ACK 确认也是通过整个接收窗口数据状态反馈给发送方,简单直接,也很容易适应弱网环境。

传输路径

RUDP 除了优化连接、压榨带宽、适应弱网环境等以外,它也继承了 UDP 天然的动态性,可以在中间应用层链路上做传输优化,一般分为多点串联优化和多点并联优化。我们具体来说一说。

多点串联 relay

在实时通信中一些业务场景对延迟非常敏感,例如:实时语音、同步书写、实时互动、直播连麦等,如果单纯的服务中转或者 P2P 通信,很难满足其需求,尤其是在物理距离很大的情况下。

在解决这个问题上 SKYPE 率先提出全球 RTN(实时多点传输网络),其实就是在通信双方之间通过几个 relay 节点来动态智能选路,这种传输方式很适合 RUDP,我们只要在通信双方构建一个 RUDP 通道,中间链路只是一个无状态的 relay cache 集合,relay 与 relay 之间进行路由探测和选路,以此来做到链路的高可用和实时性。如下图:

图 9

通过多点 relay 来保证 RUDP 进行传输优化,这类场景在三角平衡关系里是典型的用 expense 来换取 latency 的案例。

多点并联 relay

在服务与服务进行媒体数据传输或者分发过程中,需要保证传输路径高可用和带宽并发,这类使用场景也会使用传输双方构建一个 RUDP 通道,中间通过多 relay 节点的并联来解决,如下图所示:

图 10

这种模型需要在发送端设计一个多点路由表探测机制,以此来判断各个路径同时发送数据的比例和可用性,这个模型除了链路备份和增大传输并发带宽外,还有个辅助的功能,如果是流媒体分发系统,我们一般会用 BGP 来做中转,如果节点与节点之间可以直连,这样还可以减少对 BGP 带宽的占用,以此来减少成本。

后 记

到这里 RUDP 的介绍也就结束了,说了些细节和场景相关的事,也算是个入门级的科普文章。RUDP 的概念从提出到现在也差不多有 20 年了,很多从业人员希望通过一套完善的方案来设计一个通用的 RUDP,我个人觉得这不太可能,就算设计出来了,估计和现在 TCP 差不多,这样做的意义不大。

RUDP 的价值在于根据不同的传输场景进行不同的技术选型,可能选择宽松的拥塞方式、也可能选择特定的重传模式,但不管怎么选,都是基于 expense(成本)、latency(时延)、quality(质量)三者之间来权衡,通过结合场景和权衡三角平衡关系 RUDP 或许能帮助开发者找到一个比较好的方案。

作者介绍

袁荣喜,学霸君资深架构师,16 年的 C 程序员,擅长 P2P 通信网络、TCP/IP 通信协议栈和鉴权加密技术,2015 年加入学霸君,负责构建学霸君的智能路由实时音视频传输系统和网络,解决音视频通信的实时性的问题。

感谢雨多田光对本文的审校。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

WebSocket:5分钟从入门到精通

发表于 2018-01-05

阿里前端实习春招# 阿里CBU技术部招前端实习生啦!2023届的同学看过来,专业过关、前端基础扎实即可,低代码/跨端/直播/VR/electron/nodejs ,总有一款适合你! 点击投递简历)

一、内容概览

WebSocket的出现,使得浏览器具备了实时双向通信的能力。本文由浅入深,介绍了WebSocket如何建立连接、交换数据的细节,以及数据帧的格式。此外,还简要介绍了针对WebSocket的安全攻击,以及协议是如何抵御类似攻击的。

二、什么是WebSocket

HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。

对大部分web开发者来说,上面这段描述有点枯燥,其实只要记住几点:

  1. WebSocket可以在浏览器里使用
  2. 支持双向通信
  3. 使用很简单

1、有哪些优点

说到优点,这里的对比参照物是HTTP协议,概括地说就是:支持双向通信,更灵活,更高效,可扩展性更好。

  1. 支持双向通信,实时性更强。
  2. 更好的二进制支持。
  3. 较少的控制开销。连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
  4. 支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)

对于后面两点,没有研究过WebSocket协议规范的同学可能理解起来不够直观,但不影响对WebSocket的学习和使用。

2、需要学习哪些东西

对网络应用层协议的学习来说,最重要的往往就是连接建立过程、数据交换教程。当然,数据的格式是逃不掉的,因为它直接决定了协议本身的能力。好的数据格式能让协议更高效、扩展性更好。

下文主要围绕下面几点展开:

  1. 如何建立连接
  2. 如何交换数据
  3. 数据帧格式
  4. 如何维持连接

三、入门例子

在正式介绍协议细节前,先来看一个简单的例子,有个直观感受。例子包括了WebSocket服务端、WebSocket客户端(网页端)。完整代码可以在 这里 找到。

这里服务端用了ws这个库。相比大家熟悉的socket.io,ws实现更轻量,更适合学习的目的。

1、服务端

代码如下,监听8080端口。当有新的连接请求到达时,打印日志,同时向客户端发送消息。当收到到来自客户端的消息时,同样打印日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
javascript复制代码var app = require('express')();
var server = require('http').Server(app);
var WebSocket = require('ws');

var wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
console.log('server: receive connection.');

ws.on('message', function incoming(message) {
console.log('server: received: %s', message);
});

ws.send('world');
});

app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});

app.listen(3000);

2、客户端

代码如下,向8080端口发起WebSocket连接。连接建立后,打印日志,同时向服务端发送消息。接收到来自服务端的消息后,同样打印日志。

1
2
3
4
5
6
7
8
9
10
11
htmlbars复制代码<script>
var ws = new WebSocket('ws://localhost:8080');
ws.onopen = function () {
console.log('ws onopen');
ws.send('from client: hello');
};
ws.onmessage = function (e) {
console.log('ws onmessage');
console.log('from server: ' + e.data);
};
</script>

3、运行结果

可分别查看服务端、客户端的日志,这里不展开。

服务端输出:

1
2
bash复制代码server: receive connection.
server: received hello

客户端输出:

1
2
bash复制代码client: ws connection is open
client: received world

四、如何建立连接

前面提到,WebSocket复用了HTTP的握手通道。具体指的是,客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。

1、客户端:申请协议升级

首先,客户端发起协议升级请求。可以看到,采用的是标准的HTTP报文格式,且只支持GET方法。

1
2
3
4
5
6
7
http复制代码GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==

重点请求首部意义如下:

  • Connection: Upgrade:表示要升级协议
  • Upgrade: websocket:表示要升级到websocket协议。
  • Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
  • Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。

注意,上面请求省略了部分非重点请求首部。由于是标准的HTTP请求,类似Host、Origin、Cookie等请求首部会照常发送。在握手阶段,可以通过相关请求首部进行 安全限制、权限校验等。

2、服务端:响应协议升级

服务端返回内容如下,状态代码101表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。

1
2
3
4
http复制代码HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=

备注:每个header都以\r\n结尾,并且最后一行加上一个额外的空行\r\n。此外,服务端回应的HTTP状态码只能在握手阶段使用。过了握手阶段后,就只能采用特定的错误码。

3、Sec-WebSocket-Accept的计算

Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。

计算公式为:

  1. 将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。
  2. 通过SHA1计算出摘要,并转成base64字符串。

伪代码如下:

1
javascript复制代码>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 )  )

验证下前面的返回结果:

1
2
3
4
5
6
7
8
9
10
javascript复制代码const crypto = require('crypto');
const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const secWebSocketKey = 'w4v7O6xFTi36lq3RNcgctw==';

let secWebSocketAccept = crypto.createHash('sha1')
.update(secWebSocketKey + magic)
.digest('base64');

console.log(secWebSocketAccept);
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=

五、数据帧格式

客户端、服务端数据的交换,离不开数据帧格式的定义。因此,在实际讲解数据交换之前,我们先来看下WebSocket的数据帧格式。

WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。

  1. 发送端:将消息切割成多个帧,并发送给服务端;
  2. 接收端:接收消息帧,并将关联的帧重新组装成完整的消息;

本节的重点,就是讲解数据帧的格式。详细定义可参考 RFC6455 5.2节 。

1、数据帧格式概览

下面给出了WebSocket数据帧的统一格式。熟悉TCP/IP协议的同学对这样的图应该不陌生。

  1. 从左到右,单位是比特。比如FIN、RSV1各占据1比特,opcode占据4比特。
  2. 内容包括了标识、操作代码、掩码、数据、数据长度等。(下一小节会展开)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lua复制代码  0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+

2、数据帧格式详解

针对前面的格式概览图,这里逐个字段进行讲解,如有不清楚之处,可参考协议规范,或留言交流。

FIN:1个比特。

如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是是消息(message)的最后一个分片(fragment)。

RSV1, RSV2, RSV3:各占1个比特。

一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用WebSocket扩展,连接出错。

Opcode: 4个比特。

操作代码,Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection)。可选的操作代码如下:

  • %x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
  • %x1:表示这是一个文本帧(frame)
  • %x2:表示这是一个二进制帧(frame)
  • %x3-7:保留的操作代码,用于后续定义的非控制帧。
  • %x8:表示连接断开。
  • %x9:表示这是一个ping操作。
  • %xA:表示这是一个pong操作。
  • %xB-F:保留的操作代码,用于后续定义的控制帧。

Mask: 1个比特。

表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。

如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。

如果Mask是1,那么在Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1。

掩码的算法、用途在下一小节讲解。

Payload length:数据载荷的长度,单位是字节。为7位,或7+16位,或1+64位。

假设数Payload length === x,如果

  • x为0~126:数据的长度为x字节。
  • x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。
  • x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。

此外,如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian,重要的位在前)。

Masking-key:0或4字节(32位)

所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。

备注:载荷数据的长度,不包括mask key的长度。

Payload data:(x+y) 字节

载荷数据:包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。

扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。

应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。

3、掩码算法

掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:

首先,假设:

  • original-octet-i:为原始数据的第i字节。
  • transformed-octet-i:为转换后的数据的第i字节。
  • j:为i mod 4的结果。
  • masking-key-octet-j:为mask key第j字节。

算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。

j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j

六、数据传递

一旦WebSocket客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。

WebSocket根据opcode来区分操作的类型。比如0x8表示断开连接,0x0-0x2表示数据交互。

1、数据分片

WebSocket的每条消息可能被切分成多个数据帧。当WebSocket的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧。

FIN=1表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。FIN=0,则接收方还需要继续监听接收其余的数据帧。

此外,opcode在数据交换的场景下,表示的是数据的类型。0x01表示文本,0x02表示二进制。而0x00比较特殊,表示延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完。

2、数据分片例子

直接看例子更形象些。下面例子来自MDN,可以很好地演示数据的分片。客户端向服务端两次发送消息,服务端收到消息后回应客户端,这里主要看客户端往服务端发送的消息。

第一条消息

FIN=1, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。

第二条消息

  1. FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成,还有后续的数据帧。
  2. FIN=0,opcode=0x0,表示消息还没发送完成,还有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。
  3. FIN=1,opcode=0x0,表示消息已经发送完成,没有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。服务端可以将关联的数据帧组装成完整的消息。
1
2
3
4
5
6
7
8
arduino复制代码Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!

七、连接保持+心跳

WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。

但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。

  • 发送方->接收方:ping
  • 接收方->发送方:pong

ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。

举例,WebSocket服务端向客户端发送ping,只需要如下代码(采用ws模块)

1
javascript复制代码ws.ping('', false, true);

八、Sec-WebSocket-Key/Accept的作用

前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在于提供基础的防护,减少恶意连接、意外连接。

作用大致归纳如下:

  1. 避免服务端收到非法的websocket连接(比如http客户端不小心请求连接websocket服务,此时服务端可以直接拒绝连接)
  2. 确保服务端理解websocket连接。因为ws握手阶段采用的是http协议,因此可能ws连接是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议。(并非百分百保险,比如总是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key,但并没有实现ws协议。。。)
  3. 用浏览器里发起ajax请求,设置header时,Sec-WebSocket-Key以及其他相关的header是被禁止的。这样可以避免客户端发送ajax请求时,意外请求协议升级(websocket upgrade)
  4. 可以防止反向代理(不理解ws协议)返回错误的数据。比如反向代理前后收到两次ws连接的升级请求,反向代理把第一次请求的返回给cache住,然后第二次请求到来时直接把cache住的请求给返回(无意义的返回)。
  5. Sec-WebSocket-Key主要目的并不是确保数据的安全性,因为Sec-WebSocket-Key、Sec-WebSocket-Accept的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)。

强调:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端/服务端是否合法的 ws客户端、ws服务端,其实并没有实际性的保证。

九、数据掩码的作用

WebSocket协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法。

那么为什么还要引入掩码计算呢,除了增加计算机器的运算量外似乎并没有太多的收益(这也是不少同学疑惑的点)。

答案还是两个字:安全。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。

1、代理缓存污染攻击

下面摘自2010年关于安全的一段讲话。其中提到了代理服务器在协议实现上的缺陷可能导致的安全问题。猛击出处。

“We show, empirically, that the current version of the WebSocket consent mechanism is vulnerable to proxy cache poisoning attacks. Even though the WebSocket handshake is based on HTTP, which should be understood by most network intermediaries, the handshake uses the esoteric “Upgrade” mechanism of HTTP [5]. In our experiment, we find that many proxies do not implement the Upgrade mechanism properly, which causes the handshake to succeed even though subsequent traffic over the socket will be misinterpreted by the proxy.”

[TALKING] Huang, L-S., Chen, E., Barth, A., Rescorla, E., and C.
Jackson, “Talking to Yourself for Fun and Profit”, 2010,

在正式描述攻击步骤之前,我们假设有如下参与者:

  • 攻击者、攻击者自己控制的服务器(简称“邪恶服务器”)、攻击者伪造的资源(简称“邪恶资源”)
  • 受害者、受害者想要访问的资源(简称“正义资源”)
  • 受害者实际想要访问的服务器(简称“正义服务器”)
  • 中间代理服务器

攻击步骤一:

  1. 攻击者浏览器 向 邪恶服务器 发起WebSocket连接。根据前文,首先是一个协议升级请求。
  2. 协议升级请求 实际到达 代理服务器。
  3. 代理服务器 将协议升级请求转发到 邪恶服务器。
  4. 邪恶服务器 同意连接,代理服务器 将响应转发给 攻击者。

由于 upgrade 的实现上有缺陷,代理服务器 以为之前转发的是普通的HTTP消息。因此,当协议服务器 同意连接,代理服务器 以为本次会话已经结束。

攻击步骤二:

  1. 攻击者 在之前建立的连接上,通过WebSocket的接口向 邪恶服务器 发送数据,且数据是精心构造的HTTP格式的文本。其中包含了 正义资源 的地址,以及一个伪造的host(指向正义服务器)。(见后面报文)
  2. 请求到达 代理服务器 。虽然复用了之前的TCP连接,但 代理服务器 以为是新的HTTP请求。
  3. 代理服务器 向 邪恶服务器 请求 邪恶资源。
  4. 邪恶服务器 返回 邪恶资源。代理服务器 缓存住 邪恶资源(url是对的,但host是 正义服务器 的地址)。

到这里,受害者可以登场了:

  1. 受害者 通过 代理服务器 访问 正义服务器 的 正义资源。
  2. 代理服务器 检查该资源的url、host,发现本地有一份缓存(伪造的)。
  3. 代理服务器 将 邪恶资源 返回给 受害者。
  4. 受害者 卒。

附:前面提到的精心构造的“HTTP请求报文”。

1
2
3
4
5
arduino复制代码Client → Server:
POST /path/of/attackers/choice HTTP/1.1 Host: host-of-attackers-choice.com Sec-WebSocket-Key: <connection-key>
Server → Client:
HTTP/1.1 200 OK
Sec-WebSocket-Accept: <connection-key>

2、当前解决方案

最初的提案是对数据进行加密处理。基于安全、效率的考虑,最终采用了折中的方案:对数据载荷进行掩码处理。

需要注意的是,这里只是限制了浏览器对数据载荷进行掩码处理,但是坏人完全可以实现自己的WebSocket客户端、服务端,不按规则来,攻击可以照常进行。

但是对浏览器加上这个限制后,可以大大增加攻击的难度,以及攻击的影响范围。如果没有这个限制,只需要在网上放个钓鱼网站骗人去访问,一下子就可以在短时间内展开大范围的攻击。

十、写在后面

WebSocket可写的东西还挺多,比如WebSocket扩展。客户端、服务端之间是如何协商、使用扩展的。WebSocket扩展可以给协议本身增加很多能力和想象空间,比如数据的压缩、加密,以及多路复用等。

篇幅所限,这里先不展开,感兴趣的同学可以留言交流。文章如有错漏,敬请指出。

十一、相关链接

RFC6455:websocket规范

规范:数据帧掩码细节

规范:数据帧格式

server-example

编写websocket服务器

对网络基础设施的攻击(数据掩码操作所要预防的事情)

Talking to Yourself for Fun and Profit(含有攻击描述)

What is Sec-WebSocket-Key for?

10.3. Attacks On Infrastructure (Masking)

Talking to Yourself for Fun and Profit

Why are WebSockets masked?

How does websocket frame masking protect against cache poisoning?

What is the mask in a WebSocket frame?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…901902903…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%