0%

鱼和熊掌不能兼得

——中国谚语

一、介绍#

 Entity Framework作为一个优秀的ORM框架,它使得操作数据库就像操作内存中的数据一样,但是这种抽象是有性能代价的,故鱼和熊掌不能兼得。但是,通过对EF的学习,可以避免不必要的性能损失。本篇只介绍关联实体的加载的相关知识,这在我之前的文章中都有介绍。

我们已经了解到EF的关联实体加载有三种方式:Lazy Loading,Eager Loading,_Explicit Loading_,其中_Lazy Loading_和_Explicit Loading_都是延迟加载。

(一)Lazy Loading_使用的是动态代理,默认情况下,如果POCO类满足以下两个条件,EF就使用_Lazy Loading:

  1. POCO类是Public且不为Sealed。
  2. 导航属性标记为Virtual。

关闭Lazy Loading,可以将_LazyLoadingEnabled_设为false,如果导航属性没有标记为virtual,_Lazy Loading_也是不起作用的。

(二)_Eager Loading_使用Include方法关联预先加载的实体。

(三)_Explicit Loading_使用Entry方法,对于集合使用Collection,单个实体则使用Reference。

二、实例#

下面通过实例来理解这几种加载方式。

有下面三个实体:Province,City,Governor,一个Province有多个City,并且只有一个Governor。

1: public class Province

2: {

3: public int Id { get; set; }

4: public string Name { get; set; }

5:  

6: public virtual Governor Governor { get; set; }

7:  

8: public virtual List Cities { get; set; }

9: }

10:  

11: public class City

12: {

13: public int Id { get; set; }

14: public string Name { get; set; }

15: }

16:  

17:  

18: public class Governor

19: {

20: public int Id { get; set; }

21: public string Name { get; set; }

22: }

Lazy Loading

1: private static void LazyLoading(EFLoadingContext ctx)

2: {

3: //发送一条查询到数据库,查询所有的province

4: var list = ctx.Provines.ToList();

5: foreach (var province in list)

6: {

7: //每次遍历(用到导航属性时)都发送2条查询,一条查询当前province包含的city和另一条查询当前province的governor

8: //如果ctx.Configuration.LazyLoadingEnabled为false或者前者为true,但是导航属性没有标注为virtual,下面的操作都会抛出异常

9: Print(province);

10: }

11: }

Eager Loading

1: private static void EagerLoading(EFLoadingContext ctx)

2: {

3: //发送一条查询到数据库库,查询所有的province并关联city和governor

4: var list = ctx.Provines.Include(t => t.Cities).Include(t => t.Governor);

5: foreach (var province in list)

6: {

7: //不管ctx.Configuration.LazyLoadingEnabled为false,还是没有标注导航属性virtual,都不会抛出异常

8: Print(province);

9: }

10: }

Explicti Loading

1: private static void ExplicitLoading(EFLoadingContext ctx)

2: {

3: //发送一条查询到数据库,查询所有的province

4: var list = ctx.Provines.ToList();

5: foreach (var province in list)

6: {

7: var p = ctx.Entry(province);

8: //发送一条查询,查询所有当前province的city

9: p.Collection(t => t.Cities).Load();

10: //发送一条查询,查询当前province的governor

11: p.Reference(t => t.Governor).Load();

12: //不管ctx.Configuration.LazyLoadingEnabled为false,还是没有标注导航属性virtual,都不会抛出异常

13: Print(province);

14: }

15: }

Print方法

1: private static void Print(Province province)

2: {

3: Console.WriteLine(“省:【{0}】,市:【{1}】,省长:【{2}】”, province.Name, string.Join(“,”, province.Cities.Select(t => t.Name)), province.Governor.Name);

4: }

三、总结#

关于关联加载实体基本上就是这些内容吧,如果想看这部分详细的介绍,可以参考我这篇文章的后半部分。总的来说,这部分比较简单,一个LazyLoadingEnabled设置,三种加载方式。Lazy Loading会生成大量的sql,Eager Loading生成的关联查询比较负责,Explicit Loading同Lazy Loading一样生成很多的sql,但是有一些其他优点,比如:导航属性可以不用标注为virtual。如果这几种关联都不能解决实际问题,可以直接使用sql查询。

最后附上本文的demo,下载地址:http://pan.baidu.com/s/1i3IAiNF

EFK(Elasticsearch+Filebeat+Kibana)日志收集系统-腾讯云开发者社区-腾讯云

Excerpt

Elasticsearch 是一个实时的、分布式的可扩展的搜索引擎,允许进行全文、结构化搜索,它通常用于索引和搜索大量日志数据,也可用于搜索许多不同类型的文档。


EFK简介

Elasticsearch 是一个实时的、分布式的可扩展的搜索引擎,允许进行全文、结构化搜索,它通常用于索引和搜索大量日志数据,也可用于搜索许多不同类型的文档。

Beats 是数据采集的得力工具。将 Beats 和您的容器一起置于服务器上,或者将 Beats 作为函数加以部署,然后便可在 Elastisearch 中集中处理数据。如果需要更加强大的处理性能,Beats 还能将数据输送到 Logstash 进行转换和解析。

Kibana 核心产品搭载了一批经典功能:柱状图、线状图、饼图、旭日图,等等。不仅如此,您还可以使用 Vega 语法来设计独属于您自己的可视化图形。所有这些都利用 Elasticsearch 的完整聚合功能。

Elasticsearch 通常与 Kibana 一起部署,Kibana 是 Elasticsearch 的一个功能强大的数据可视化 Dashboard,Kibana 允许你通过 web 界面来浏览 Elasticsearch 日志数据。

EFK架构图

在这里插入图片描述

在这里插入图片描述

ELK和EFK的区别

ELK 是现阶段众多企业单位都在使用的一种日志分析系统,它能够方便的为我们收集你想要的日志并且展示出来

ELK是Elasticsearch、Logstash、Kibana的简称,这三者都是开源软件,通常配合使用。

1. Elasticsearch –>存储数据

是一个实时的分布式搜索和分析引擎,它可以用于全文搜索,结构化搜索以及分析。它是一个建立在全文搜索引擎 Apache Lucene 基础上的搜索引擎,使用 Java 语言编写,能对大容量的数据进行接近实时的存储、搜索和分析操作。

2. Logstash –> 收集数据

数据收集引擎。它支持动态的从各种数据源搜集数据,并对数据进行过滤、分析、丰富、统一格式等操作,然后存储到用户指定的位置。

3. Kibana –> 展示数据

数据分析和可视化平台。通常与 Elasticsearch 配合使用,对其中数据进行搜索、分析和以统计图表的方式展示。

EFK是ELK日志分析系统的一个变种,加入了filebeat 可以更好的收集到资源日志 来为我们的日志分析做好准备工作。

优缺点

Filebeat 相对 Logstash 的优点:

  • 侵入低,无需修改 elasticsearch 和 kibana 的配置;
  • 性能高,IO 占用率比 logstash 小太多;

当然 Logstash 相比于 FileBeat 也有一定的优势,比如 Logstash 对于日志的格式化处理能力,FileBeat 只是将日志从日志文件中读取出来,当然如果收集的日志本身是有一定格式的,FileBeat 也可以格式化,但是相对于Logstash 来说,效果差很多。

安装环境

此次实验用的一台服务器进行操作 准备安装包,EFK的三个安装包保持相同版本

这里是引用

这里是引用

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

安装es的节点一定安装java环境

1
通过yum安装java [root@localhost ~]# yum search jdk 已加载插件:fastestmirror Loading mirror speeds from cached hostfile * base: mirrors.tuna.tsinghua.edu.cn * extras: mirrors.tuna.tsinghua.edu.cn * updates: mirrors.tuna.tsinghua.edu.cn ================================================================= N/S matched: jdk ================================================================= copy-jdk-configs.noarch : JDKs configuration files copier java-1.6.0-openjdk.x86_64 : OpenJDK Runtime Environment java-1.6.0-openjdk-demo.x86_64 : OpenJDK Demos java-1.6.0-openjdk-devel.x86_64 : OpenJDK Development Environment java-1.6.0-openjdk-javadoc.x86_64 : OpenJDK API Documentation java-1.6.0-openjdk-src.x86_64 : OpenJDK Source Bundle java-1.7.0-openjdk.x86_64 : OpenJDK Runtime Environment java-1.7.0-openjdk-accessibility.x86_64 : OpenJDK accessibility connector java-1.7.0-openjdk-demo.x86_64 : OpenJDK Demos java-1.7.0-openjdk-devel.x86_64 : OpenJDK Development Environment java-1.7.0-openjdk-headless.x86_64 : The OpenJDK runtime environment without audio and video support java-1.7.0-openjdk-javadoc.noarch : OpenJDK API Documentation java-1.7.0-openjdk-src.x86_64 : OpenJDK Source Bundle java-1.8.0-openjdk.i686 : OpenJDK Runtime Environment 8 java-1.8.0-openjdk.x86_64 : OpenJDK Runtime Environment 8 java-1.8.0-openjdk-accessibility.i686 : OpenJDK accessibility connector java-1.8.0-openjdk-accessibility.x86_64 : OpenJDK accessibility connector java-1.8.0-openjdk-demo.i686 : OpenJDK Demos 8 java-1.8.0-openjdk-demo.x86_64 : OpenJDK Demos 8 java-1.8.0-openjdk-devel.i686 : OpenJDK Development Environment 8 java-1.8.0-openjdk-devel.x86_64 : OpenJDK Development Environment 8 java-1.8.0-openjdk-headless.i686 : OpenJDK Headless Runtime Environment 8 java-1.8.0-openjdk-headless.x86_64 : OpenJDK Headless Runtime Environment 8 java-1.8.0-openjdk-javadoc.noarch : OpenJDK 8 API documentation java-1.8.0-openjdk-javadoc-zip.noarch : OpenJDK 8 API documentation compressed in a single archive java-1.8.0-openjdk-src.i686 : OpenJDK Source Bundle 8 java-1.8.0-openjdk-src.x86_64 : OpenJDK Source Bundle 8 java-11-openjdk.i686 : OpenJDK Runtime Environment 11 java-11-openjdk.x86_64 : OpenJDK Runtime Environment 11 java-11-openjdk-demo.i686 : OpenJDK Demos 11 java-11-openjdk-demo.x86_64 : OpenJDK Demos 11 java-11-openjdk-devel.i686 : OpenJDK Development Environment 11 java-11-openjdk-devel.x86_64 : OpenJDK Development Environment 11 java-11-openjdk-headless.i686 : OpenJDK Headless Runtime Environment 11 java-11-openjdk-headless.x86_64 : OpenJDK Headless Runtime Environment 11 java-11-openjdk-javadoc.i686 : OpenJDK 11 API documentation java-11-openjdk-javadoc.x86_64 : OpenJDK 11 API documentation java-11-openjdk-javadoc-zip.i686 : OpenJDK 11 API documentation compressed in a single archive java-11-openjdk-javadoc-zip.x86_64 : OpenJDK 11 API documentation compressed in a single archive java-11-openjdk-jmods.i686 : JMods for OpenJDK 11 java-11-openjdk-jmods.x86_64 : JMods for OpenJDK 11 java-11-openjdk-src.i686 : OpenJDK Source Bundle 11 java-11-openjdk-src.x86_64 : OpenJDK Source Bundle 11 java-11-openjdk-static-libs.i686 : OpenJDK libraries for static linking 11 java-11-openjdk-static-libs.x86_64 : OpenJDK libraries for static linking 11 ldapjdk-javadoc.noarch : Javadoc for ldapjdk icedtea-web.x86_64 : Additional Java components for OpenJDK - Java browser plug-in and Web Start implementation ldapjdk.noarch : The Mozilla LDAP Java SDK 名称和简介匹配 only,使用“search all”试试。 查询到版本然后进行安装 [root@localhost ~]# yum -y install java-1.8.0-openjdk [root@localhost ~]# java -version openjdk version "1.8.0_275" OpenJDK Runtime Environment (build 1.8.0_275-b01) OpenJDK 64-Bit Server VM (build 25.275-b01, mixed mode)

安装es

1
tar zxf elasticsearch-6.3.2.tar.gz mv elasticsearch-6.3.2 /usr/local/es

调整系统文件描述符的软硬限制

1
vim /etc/security/limits.conf 末尾添加 打开文件的软限制,ES要求系统文件描述符大于65535 * soft nofile 655360 打开文件的硬限制 * hard nofile 655360 用户可用进程数软限制 * soft nproc 2048 用户可用进程数硬限制 * hard nproc 4096 JVM能够使用最大线程数 echo "vm.max_map_count=655360" >> /etc/sysctl.conf sysctl -p

配置Elasticsearch服务环境

1
useradd es mkdir -p /es/{ data,logs} # 日志及数据存放目录 chown -R es:es /usr/local/es /es # 使用es用户启动时,权限不对也会报错

网络对时

重启服务器

编辑elasticsearch.yml配置文件,ES默认就是集群模式的,所以只有一个节点也是集群模式

1
vim /usr/local/es/config/elasticsearch.yml 取消注释 cluster.name: my-application node.name: node-1 添加 node.master: true node.data: true 取消注释并修改 path.data: /es/data path.logs: /es/logs network.host: 192.168.1.10 # 改为本机ip discovery.zen.minimum_master_nodes: 1 # master的最少节点数 取消注释 http.port: 9200

安装Kibana

1
tar zxf kibana-6.3.2-linux-x86_64.tar.gz mv kibana-6.3.2-linux-x86_64 /usr/local/kibana

修改Kibana配置文件

1
vim /usr/local/kibana/config/kibana.yml 取消注释 server.port: 5601 server.host: "192.168.1.10" 用来连接es服务 elasticsearch.url: "http://192.168.1.10:9200"

安装Filebeat

1
tar zxf filebeat-6.3.2-linux-x86_64.tar.gz mv filebeat-6.3.2-linux-x86_64 /usr/local/filebeat

举例收集nginx的日志 安装nginx

1
yum -y install epel-release yum -y install nginx 启动 nginx 查看配置文件 cat /etc/nginx/nginx.conf 得知配置文件存放在/var/log/nginx/*.log 访问测试

在这里插入图片描述

在这里插入图片描述

整合环境 修改filebeat配置文件,将本机的nginx日志文件打标签为nginx,方便elasticsearch来创建索引

1
vim /usr/local/filebeat/filebeat.yml 添加注释 filebeat.inputs: - type: log paths: - /var/log/*.log 添加以下内容为inputs配置 filebeat: prospectors: - type: log paths: - /var/log/nginx/*.log tags: ["nginx"] 修改 enabled: true # 表示以上配置是否生效 修改 setup.kibana: host: "192.168.1.10:5601" #kibana的主机ip output.elasticsearch: hosts: ["192.168.1.10:9200"] #es主机的ip

进行启动

  • es
1
su es /usr/local/es/bin/elasticsearch
  • filebeat
1
/usr/local/filebeat/filebeat -c /usr/local/filebeat/filebeat.yml
  • kibana
1
/usr/local/kibana/bin/kibana

各个服务启动之后阻塞信息都是INFO就没问题,遇到WARN是no route的就关闭防火墙或者放行端口

在访问kibana的ip:5601,http://192.168.1.10:5601

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。

Docker配置加速器 - 晓晨Master - 博客园

Excerpt

我们国内使用官方Docker Hub仓库实在是太慢了,很影响效率 使用命令编辑文件: 加入下面的数据: docker cn镜像: https://www.docker cn.com/registry mirror 如果你是腾讯云的服务器那么请加入: 阿里云的服务器请查看:https://yq.ali


我们国内使用官方Docker Hub仓库实在是太慢了,很影响效率

使用命令编辑文件:

1
vim /etc/docker/daemon.json

加入下面的数据:

docker-cn镜像:

1
{ "registry-mirrors": ["https://registry.docker-cn.com"] }

https://www.docker-cn.com/registry-mirror

如果你是腾讯云的服务器那么请加入:

1
{ "registry-mirrors": ["https://mirror.ccs.tencentyun.com"] }

阿里云的服务器请查看:https://yq.aliyun.com/articles/29941

wq保存退出:

执行命令生效:

1
systemctl daemon-reload systemctl restart docker

以上两个源,我都测试过,如果你是腾讯云那么肯定用腾讯云的源最好,阿里同样,速度飞快 image 秒 pull。

Drone CI使用docker插件构建和推送镜像 - wosperry - 博客园

Excerpt

使用docker插件进行构建和发布镜像 首先,在项目中安装 drone 构建工具,可以通过以下命令进行安装: $ curl https://downloads.drone.io/cli/latest/drone_linux_amd64.tar.gz 


使用docker插件进行构建和发布镜像

首先,在项目中安装 drone 构建工具,可以通过以下命令进行安装:

1
$ curl https://downloads.drone.io/cli/latest/drone_linux_amd64.tar.gz \ | tar zx $ sudo install -t /usr/local/bin drone

接着,我们需要在项目根目录中创建一个 .drone.yml 文件,并添加以下代码:

1
kind: pipeline name: default steps: - name: build image: plugins/docker settings: repo: myrepo/myimage tags: [ latest, v1.0 ] context: ./app dockerfile: ./app/Dockerfile username: from_secret: docker_username password: from_secret: docker_password secrets: - name: docker_username value: your_dockerhub_username - name: docker_password value: your_dockerhub_password

解释一下以上代码:

kind: 定义的 Pipeline 类型。
name: Pipeline 名称,这里设置为默认值 default
steps: 定义步骤列表。
settings/repo: 指定要构建和发布到哪个 Dcoker 仓库和镜像名称。
settings/tags: 相应的镜像标签,用于版本管理。在这里使用了最新版和 v1.0 版本号作为例子。
settings/context: 指定项目所在目录的路径,即包含 Dockerfile 所处的目录。
settings/dockerfile: Dockerfile 文件所处的位置。
settings/username 和 settings/password: Docker hub 的用户名和密码,使用 from_secret 表示从 Drone Secrets 中获取。
secrets: Secrets 存储了私密数据(如远程服务器 SSH 密钥、数据库连接字符串等),它们不会明文地出现在 .drone.yml 中。

最后,启动构建过程,执行以下命令:

1
$ drone exec

在 Drone 控制台上查看构建状态,如果构建没有出现问题,则说明你已经成功使用 Drone 和 plugins/docker 构建并发布 Docker 镜像了喵~

1、EF Code First一对一关联关系

   项目结构图:

  实体类:

  Account.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Northwind.App.Entities
{ public class Account
{ ///


/// 账户ID ///

public int AccountID { get; set; } ///
/// 账户名 ///

public string AccountName { get; set; } ///
/// 密码 ///

public string Password { get; set; } ///
/// 用户信息 ///

public virtual User User { get; set; }
}
}

复制代码

  User.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Northwind.App.Entities
{ public class Account
{ ///


/// 账户ID ///

public int AccountID { get; set; } ///
/// 账户名 ///

public string AccountName { get; set; } ///
/// 密码 ///

public string Password { get; set; } ///
/// 用户信息 ///

public virtual User User { get; set; }
}
}

复制代码

  实体映射类:

  AccountMap.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Entity.ModelConfiguration; using System.ComponentModel.DataAnnotations.Schema; using Northwind.App.Entities; namespace Northwind.App.Mapping
{ public class AccountMap : EntityTypeConfiguration { public AccountMap()
{ // Primary Key
this.HasKey(t => t.AccountID); // Properties
this.Property(t => t.AccountName).HasMaxLength(50); this.Property(t => t.Password).HasMaxLength(100); // Table & Column Mappings
this.ToTable(“Account”); this.Property(t => t.AccountID).HasColumnName(“AccountID”); this.Property(t => t.AccountName).HasColumnName(“AccountName”); this.Property(t => t.Password).HasColumnName(“Password”);
}
}
}

复制代码

  UserMap.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Entity.ModelConfiguration; using System.ComponentModel.DataAnnotations.Schema; using Northwind.App.Entities; namespace Northwind.App.Mapping
{ public class UserMap : EntityTypeConfiguration { public UserMap()
{ // Primary Key
this.HasKey(t => t.AccountID); // Properties
this.Property(t => t.AccountID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); this.Property(t => t.UserName).HasMaxLength(50); this.Property(t => t.Email).HasMaxLength(100); // Table & Column Mappings
this.ToTable(“User”); this.Property(t => t.AccountID).HasColumnName(“AccountID”); this.Property(t => t.UserName).HasColumnName(“UserName”); this.Property(t => t.Email).HasColumnName(“Email”); this.Property(t => t.RegisterDate).HasColumnName(“RegisterDate”); // Relationships
this.HasRequired(t => t.Account)
.WithRequiredDependent(t => t.User);
}
}
}

复制代码

  NorthwindContext.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Entity; using Northwind.App.Entities; using Northwind.App.Mapping; namespace Northwind.App
{ public class NorthwindContext : DbContext
{ static NorthwindContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges());
} public DbSet Accounts { get; set; } public DbSet Users { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new AccountMap());
modelBuilder.Configurations.Add(new UserMap());
}
}
}

复制代码

  Program.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; using Northwind.App.Entities; namespace Northwind.App
{ class Program
{ static void Main(string[] args)
{ using (NorthwindContext db = new NorthwindContext())
{
Account account = new Account { AccountName = “Test”, Password = “1” };
db.Accounts.Add(account);

            User user \= new User { AccountID = account.AccountID, UserName = "测试", Email = "test@126.com", RegisterDate = DateTime.Now };
            db.Users.Add(user);

            db.SaveChanges();
        }
    }
}

}

复制代码

  代码运行后生成的数据库结构图:

2、EF Code First一对多关联关系

  关联表:Product 产品表、Category分类表

  关联关系:一个产品属于一个分类,一个分类可以有多个产品

  实体代码:

  Category.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Northwind.App.Entities
{ public class Category
{ ///


/// 分类ID ///

public Guid CategoryID { get; set; } ///
/// 分类名称 ///

public string CategoryName { get; set; } ///
/// 产品 ///

public virtual ICollection Products { get; set; }
}
}

复制代码

  Product.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Northwind.App.Entities
{ public class Product
{ ///


/// 产品ID ///

public Guid ProductID { get; set; } ///
/// 产品名称 ///

public string ProductName { get; set; } ///
/// 单价 ///

public decimal UnitPrice { get; set; } ///
/// 数量 ///

public Nullable<int> Quantity { get; set; } ///
/// 库存 ///

public Nullable<int> UnitsInStock { get; set; } ///
/// 产品类别ID ///

public Guid CategoryID { get; set; } ///
/// 产品类别 ///

public virtual Category Category { get; set; }
}
}

复制代码

  实体映射类:

  CategoryMap.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Entity.ModelConfiguration; using System.ComponentModel.DataAnnotations.Schema; using Northwind.App.Entities; namespace Northwind.App.Mapping
{ public class CategoryMap : EntityTypeConfiguration { public CategoryMap()
{ // Primary Key
this.HasKey(t => t.CategoryID); // Properties
this.Property(t => t.CategoryID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); this.Property(t => t.CategoryName).IsRequired()
.HasMaxLength(100); // Table & Column Mappings
this.ToTable(“Category”); this.Property(t => t.CategoryID).HasColumnName(“CategoryID”); this.Property(t => t.CategoryName).HasColumnName(“CategoryName”);
}
}
}

复制代码

  ProductMap.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Entity.ModelConfiguration; using System.ComponentModel.DataAnnotations.Schema; using Northwind.App.Entities; namespace Northwind.App.Mapping
{ public class ProductMap : EntityTypeConfiguration { public ProductMap()
{ // Primary Key
this.HasKey(t => t.ProductID); // Properties
this.Property(t => t.ProductID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); this.Property(t => t.ProductName).IsRequired()
.HasMaxLength(100); this.Property(t => t.UnitPrice).HasPrecision(10, 2); // Table & Column Mappings
this.ToTable(“Product”); this.Property(t => t.ProductID).HasColumnName(“ProductID”); this.Property(t => t.ProductName).HasColumnName(“ProductName”); this.Property(t => t.UnitPrice).HasColumnName(“UnitPrice”); this.Property(t => t.Quantity).HasColumnName(“Quantity”); this.Property(t => t.UnitsInStock).HasColumnName(“UnitsInStock”); this.Property(t => t.CategoryID).HasColumnName(“CategoryID”); // Relationships
this.HasRequired(t => t.Category)
.WithMany(t => t.Products)
.HasForeignKey(t => t.CategoryID)
.WillCascadeOnDelete(false);
}
}
}

复制代码

  NorthwindContext.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Entity; using Northwind.App.Entities; using Northwind.App.Mapping; namespace Northwind.App
{ public class NorthwindContext : DbContext
{ static NorthwindContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges());
} public DbSet Accounts { get; set; } public DbSet Users { get; set; } public DbSet Categories { get; set; } public DbSet Products { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new AccountMap());
modelBuilder.Configurations.Add(new UserMap());
modelBuilder.Configurations.Add(new CategoryMap());
modelBuilder.Configurations.Add(new ProductMap());
}
}
}

复制代码

  Program.cs

复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; using Northwind.App.Entities; namespace Northwind.App
{ class Program
{ static void Main(string[] args)
{ using (NorthwindContext db = new NorthwindContext())
{
Category category = new Category { CategoryName = “手机数码” };
db.Categories.Add(category);

            Product product \= new Product { CategoryID = category.CategoryID, ProductName = "IPhone5", UnitPrice = 5000m, Quantity = 100, UnitsInStock = 60 };
            db.Products.Add(product);

            db.SaveChanges();
        }
    }
}

}

复制代码

  运行代码后生成的数据表:

上一篇:《DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(2)

这篇文章主要是对 DDD.Sample 框架增加 Transaction 事务操作,以及增加了一些必要项目。

虽然现在的 IUnitOfWork 实现中有 Commit 的实现,但也就是使用的 EF SaveChanges,满足一些简单操作可以,但一些稍微复杂点的实体操作就不行了,并且 Rollback 也没有实现。

现在的 UnitOfWork 实现代码:

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
public class UnitOfWork : IUnitOfWork
{
private IDbContext _dbContext;

public UnitOfWork(IDbContext dbContext)
{
_dbContext = dbContext;
}

public void RegisterNew<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Set<TEntity>().Add(entity);
}

public void RegisterDirty<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Entry<TEntity>(entity).State = EntityState.Modified;
}

public void RegisterClean<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Entry<TEntity>(entity).State = EntityState.Unchanged;
}

public void RegisterDeleted<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Set<TEntity>().Remove(entity);
}

public async Task<bool> CommitAsync()
{
return await _dbContext.SaveChangesAsync() > 0;
}

public void Rollback()
{
throw new NotImplementedException();
}
}

基于上面的实现,比如要处理这样的一个操作:先添加一个 Teacher,然后再添加一个 Student,Student 实体中有一个 TeacherId,一般实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public async Task<bool> Add(string name)
{
var teacher = new Teacher { Name = "teacher one" };
_unitOfWork.RegisterNew(teacher);
await _unitOfWork.CommitAsync();



var student = new Student { Name = name, TeacherId = teacher.Id };
_unitOfWork.RegisterNew(student);
await _unitOfWork.CommitAsync();

return true;
}

上面的实现可能会出现一些问题,比如添加 Teacher 出现了异常,web 请求出现了异常,添加 Student 出现了异常等,该如何进行处理?所以你可能会增加很多判断,还有就是异常出现后的修复操作,当需求很复杂的时候,我们基于上面的处理也就会更加复杂,根本原因是并没有真正的实现 Transaction 事务操作。

如果单独在 EF 中实现 Transaction 操作,可以使用 TransactionScope,参考文章:在 Entity Framework 中使用事务

TransactionScope 的实现比较简单,如何在 DDD.Sample 框架中,结合 IUnitOfWork 和 IDbContext 进行使用呢?可能实现方式有很多中,现在我的实现是这样:

首先,IDbContext 中增加 Database 属性定义:

1
2
3
4
5
6
7
8
9
10
11
12
public interface IDbContext
{
Database Database { get; }

DbSet<TEntity> Set<TEntity>()
where TEntity : class;

DbEntityEntry<TEntity> Entry<TEntity>(TEntity entity)
where TEntity : class;

Task<int> SaveChangesAsync();
}

增加 Database 属性的目的是,便于我们在 UnitOfWork 中访问到 Transaction,其实还可以定义这样的接口:DbContextTransaction BeginTransaction();,但 EF 中的 DbContext 并没有进行实现,而是需要通过 Database 属性,所以还需要在 IDbContext 实现中额外实现,另外增加 Database 属性的好处,还有就是可以在 UnitOfWork 中访问执行很多的操作,比如执行 SQL 语句等等。

这里需要说下 IDbContext 定义,我原先的设计初衷是,让它脱离 EF,作为所有数据操作上下文的定义,但其实实现的时候,还是脱离不了 EF,因为接口返回的类型都在 EF 下,最后 IDbContext 就变成了 EF DbContext 的部分接口定义,所以这部分是需要再进行设计的,但好在有了 IDbContext,可以让 EF 和 UnitOfWork 隔离开来。

SchoolDbContext 中的实现没有任何变换,因为继承的 EF DbContext 已经有了实现,UnitOfWork 改动比较大,代码如下:

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
public class UnitOfWork : IUnitOfWork
{
private IDbContext _dbContext;
private DbContextTransaction _dbTransaction;

public UnitOfWork(IDbContext dbContext)
{
_dbContext = dbContext;
_dbTransaction = _dbContext.Database.BeginTransaction();
}

public async Task<bool> RegisterNew<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Set<TEntity>().Add(entity);
return await _dbContext.SaveChangesAsync() > 0;
}

public async Task<bool> RegisterDirty<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Entry<TEntity>(entity).State = EntityState.Modified;
return await _dbContext.SaveChangesAsync() > 0;
}

public async Task<bool> RegisterClean<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Entry<TEntity>(entity).State = EntityState.Unchanged;
return await _dbContext.SaveChangesAsync() > 0;
}

public async Task<bool> RegisterDeleted<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Set<TEntity>().Remove(entity);
return await _dbContext.SaveChangesAsync() > 0;
}

public void Commit()
{
_dbTransaction.Commit();
}

public void Rollback()
{
_dbTransaction.Rollback();
}
}

UnitOfWork 构造函数中,根据 DbContext 创建 DbContextTransaction 对象,然后在实体每个操作中,都添加了 SaveChanges,因为我们用了 Transaction,所以在执行 SaveChanges 的时候,并没有应用到数据库,但可以获取到新添加实体的 Id,比如上面示例 Student 中的 TeacherId,并且用 Sql Profiler 可以检测到执行的 SQL 代码,当执行 Commit 的时候,数据对应进行更新。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public async Task<bool> AddWithTransaction(string name)
{
var teacher = new Teacher { Name = "teacher one" };
await _unitOfWork.RegisterNew(teacher);



var student = new Student { Name = name, TeacherId = teacher.Id };
await _unitOfWork.RegisterNew(student);

_unitOfWork.Commit();
return true;
}

在上面代码中,首先在没执行到 Commit 之前,是可以获取到新添加 Teacher 的 Id,并且如果出现了任何异常,都是可以进行回滚的,当然也可以手动进行 catch 异常,并执行_unitOfWork.Rollback()

不过上面的实现有一个问题,就是每次实体操作,都用了 Transaction,性能我没测试,但肯定会有影响,好处就是 IUnitOfWork 基本没有改动,还是按照官方的定义,只不过部分接口改成了异步接口。

除了上面的实现,还有一种解决方式,就是在 IUnitOfWork 中增加一个类似 BeginTransaction 的接口,大致实现代码:

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
public class UnitOfWork : IUnitOfWork
{
private IDbContext _dbContext;
private DbContextTransaction _dbTransaction;

public UnitOfWork(IDbContext dbContext)
{
_dbContext = dbContext;
}


public void BeginTransaction()
{
_dbTransaction = _dbContext.Database.BeginTransaction();
}

public async Task<bool> RegisterNew<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Set<TEntity>().Add(entity);
if (_dbTransaction != null)
return await _dbContext.SaveChangesAsync() > 0;
return true;
}

public async Task<bool> RegisterDirty<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Entry<TEntity>(entity).State = EntityState.Modified;
if (_dbTransaction != null)
return await _dbContext.SaveChangesAsync() > 0;
return true;
}

public async Task<bool> RegisterClean<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Entry<TEntity>(entity).State = EntityState.Unchanged;
if (_dbTransaction != null)
return await _dbContext.SaveChangesAsync() > 0;
return true;
}

public async Task<bool> RegisterDeleted<TEntity>(TEntity entity)
where TEntity : class
{
_dbContext.Set<TEntity>().Remove(entity);
if (_dbTransaction != null)
return await _dbContext.SaveChangesAsync() > 0;
return true;
}

public async Task<bool> Commit()
{
if (_dbTransaction == null)
return await _dbContext.SaveChangesAsync() > 0;
else
_dbTransaction.Commit();
return true;
}

public void Rollback()
{
if (_dbTransaction != null)
_dbTransaction.Rollback();
}
}

上面这种实现方式就解决了第一种方式的问题,需要使用 Transaction 的时候,直接在操作之前调用 BeginTransaction 就行了,但不好的地方就是改动了 IUnitOfWork 的接口定义。

除了上面两种实现方式,大家如果有更好的解决方案,欢迎提出。

另外,DDD.Sample 增加和改动了一些东西:

  • 增加 DDD.Sample.Domain.DomainEvents、DDD.Sample.Domain.DomainServices 和 DDD.Sample.Domain.ValueObjects。
  • 从 DDD.Sample.Domain 分离出 DDD.Sample.Domain.Repository.Interfaces。
  • 增加 DDD.Sample.BootStrapper,执行 Startup.Configure 用于系统启动的配置。
  • 去除 IEntity,在 IAggregateRoot 中添加 Id 属性定义。

上一篇:《DDD 领域驱动设计-如何控制业务流程?

开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新,并增加了应用层代码)

在 JsPermissionApply 领域模型中,User 被设计为值对象,也就是 JsPermissionApply 实体中的 UserId 属性,这个没啥问题,但后来再实现代码的时候,就出现了一些问题,在 JS 权限申请和审核系统中,用户的一些操作如下:

  1. 申请:根据当前 LoginName 获取 UserId,UserId 存储在 JsPermissionApply 实体。
  2. 验证:根据 UserId 判断此用户是否拥有博客。
  3. 权限:根据当前 LoginName,判断此用户是否拥有审核权限。
  4. 审核:循环遍历每个申请,根据其 UserId 获取其他的用户信息。

对于上面的四个用户操作,因为每个请求都会耗费时间,所以我们需要尽量简化其操作,尤其是第四个操作,如果管理员要审核 10 个申请,那么就得请求用户服务 10 次,那怎么省掉这个操作呢?就是用户在申请 JS 权限的时候,我们先获取用户信息,然后存在 JsPermissionApply 实体中,如何这样设计,那么第二个用户验证操作,也可以省掉。

代码如何实现?我之前想在 JsPermissionApply 实体中,直接增加如下值对象:

1
2
3
4
5
6
7
8
9
public int UserId { get; set; }

public string UserLoginName { get; set; }

public string UserDisplayName { get; set; }

public string UserEmail { get; set; }

public string UserAlias { get; set; }

这样实现也没什么问题,但 JsPermissionApply 实体的构造函数参数赋值,就变的很麻烦,UserId 标识一个 User,那一个 User 也是标识一个 User,所以我们可以直接把 User 设计为值对象,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace CNBlogs.Apply.Domain.ValueObjects
{
public class User
{
public string LoginName { get; set; }

public string DisplayName { get; set; }

public string Email { get; set; }

public string Alias { get; set; }

[JsonProperty("SpaceUserID")]
public int Id { get; set; }
}
}

JsonProperty 的作用是在 UserService 获取用户信息的时候,映射源属性名称,GetUserByLoginName 示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace CNBlogs.Apply.ServiceAgent
{
public class UserService
{
private static string userHost = "";

public static async Task<User> GetUserByLoginName(string loginName)
{
using (var httpCilent = new HttpClient())
{
httpCilent.BaseAddress = new System.Uri(userHost);
var response = await httpCilent.GetAsync($"/users?loginName={Uri.EscapeDataString(loginName)}");
if (response.StatusCode == HttpStatusCode.OK)
{
return await response.Content.ReadAsAsync<CNBlogs.Apply.Domain.ValueObjects.User>();
}
return null;
}
}
}
}

JsPermissionApply 实体代码:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
namespace CNBlogs.Apply.Domain
{
public class JsPermissionApply : IAggregateRoot
{
private IEventBus eventBus;

public JsPermissionApply()
{ }

public JsPermissionApply(string reason, User user, string ip)
{
if (string.IsNullOrEmpty(reason))
{
throw new ArgumentException("申请内容不能为空");
}
if (reason.Length > 3000)
{
throw new ArgumentException("申请内容超出最大长度");
}
if (user == null)
{
throw new ArgumentException("用户为null");
}
if (user.Id == 0)
{
throw new ArgumentException("用户Id为0");
}
this.Reason = HttpUtility.HtmlEncode(reason);
this.User = user;
this.Ip = ip;
this.Status = Status.Wait;
}

public int Id { get; private set; }

public string Reason { get; private set; }

public virtual User User { get; private set; }

public Status Status { get; private set; } = Status.Wait;

public string Ip { get; private set; }

public DateTime ApplyTime { get; private set; } = DateTime.Now;

public string ReplyContent { get; private set; }

public DateTime? ApprovedTime { get; private set; }

public bool IsActive { get; private set; } = true;

public async Task<bool> Pass()
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Pass;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = "恭喜您!您的JS权限申请已通过审批。";
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new JsPermissionOpenedEvent() { UserId = this.User.Id });
return true;
}

public bool Deny(string replyContent)
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Deny;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = replyContent;
return true;
}

public bool Lock()
{
if (this.Status != Status.Wait)
{
return false;
}
this.Status = Status.Lock;
this.ApprovedTime = DateTime.Now;
this.ReplyContent = "抱歉!您的JS权限申请没有被批准,并且申请已被锁定,具体请联系contact@cnblogs.com。";
return true;
}

public async Task Passed()
{
if (this.Status != Status.Pass)
{
return;
}
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请已批准", Content = this.ReplyContent, RecipientId = this.User.Id });
}

public async Task Denied()
{
if (this.Status != Status.Deny)
{
return;
}
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请未通过审批", Content = this.ReplyContent, RecipientId = this.User.Id });
}

public async Task Locked()
{
if (this.Status != Status.Lock)
{
return;
}
eventBus = IocContainer.Default.Resolve<IEventBus>();
await eventBus.Publish(new MessageSentEvent() { Title = "您的JS权限申请未通过审批", Content = this.ReplyContent, RecipientId = this.User.Id });
}
}
}

JsPermissionApply 实体去除了 UserId 属性,并增加了 User 值对象,构造函数也相应进行了更新,如果实体进行这样设计,那数据库存储该如何设计呢?EF 不需要添加任何的映射代码,直接用 EF Migration 应用更新就可以了,生成 JsPermissionApplys 表结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT TOP 1000 [Id]
,[Reason]
,[Status]
,[Ip]
,[ApplyTime]
,[ReplyContent]
,[ApprovedTime]
,[IsActive]
,[User_LoginName]
,[User_DisplayName]
,[User_Email]
,[User_Alias]
,[User_Id]
FROM [cnblogs_apply].[dbo].[JsPermissionApplys]

JsPermissionApplyDTO 示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace CNBlogs.Apply.Application.DTOs
{
public class JsPermissionApplyDTO
{
public int Id { get; set; }

public string Reason { get; set; }

public string Ip { get; set; }

public DateTime ApplyTime { get; set; }

public int UserId { get; set; }

public string UserLoginName { get; set; }

public string UserDisplayName { get; set; }

public string UserEmail { get; set; }

public string UserAlias { get; set; }
}
}

使用.ProjectTo<JsPermissionApplyDTO>().ToListAsync()获取申请列表的时候,AutoMapper 也不需要添加任何对 JsPermissionApply 和 JsPermissionApplyDTO 的映射代码。

另外领域服务、应用服务和单元测试代码,也对应进行了更新,详细查看上面的开源地址。

UserId 换为 User 设计,大致有两个好处:

  • 用户信息在申请的时候获取并存储,审核直接展示,减少不必要的请求开销。
  • 有利于 User 的扩展,JsPermissionApply 领域模型会更加健壮。

技术是设计的实现,不能用技术来影响设计。

无意中看到了**dqzhangp**的一篇博客,分析了DSS的核心架构,读完顿时感觉豁然开朗,茅塞顿开,写得非常的鞭辟入里,言简意赅,我想没有相当的功力是写不出这样的文章的,情不自禁转到自己空间来,生怕弄丢了。

基本概念

首先,我针对的代码是Darwin StreamingServer 6.0.3未经任何改动的版本。

DarwinStreaming Server从设计模式上看,采用了Reactor的并发服务器设计模式,如果对Reactor有一定的了解会有助于对DarwinStreaming Server核心代码的理解。

Reactor模式是典型的事件触发模式,当有事件发生时则完成相应的Task,Task的完成是通过调用相应的handle来实现的,对于handle的调用是由有限个数的Thread来完成的。

DarwinStreaming Server中定义了一个Task类。Task类有两个典型的方法,一个是Signal,一个是Run。调用Signal把一个Task加入到TaskThread的Task队列中,等待完成,Run就是完成一个任务的handle。基于Task类,定义了三种类型的Task,分别是IdleTask,TimeoutTask,以及普通的Task。

在Darwin StreamingServer中,除了主线程以外,有三种类型的线程,分别是TaskThread,EventThread以及IdleTaskThread:

1.    TaskThread,TaskThread通过运行Task类型对象的Run方法来完成相应Task的处理。典型的Task类型是RTSPSession和RTPSession。TaskThread的个数是可配置的,缺省情况下TaskThread的个数与处理器的个数一致。等待被TaskThread调用并运行的Task放在队列或者堆中。

2.    EventThread,EventThread负责侦听套接口事件,在DarwinStreaming Server中,有两种被侦听的事件,分别是建立RTSP连接请求的到达和RTSP请求的到达。对于RTSP连接请求的事件,EventThread建立一个RTSPSession,并启动针对相应的socket的侦听。对于RTSP请求的事件,EventThread把对应的RTSPSession类型的Task加入到TaskThread的队列中,等待RTSP请求被处理。

3.    IdleTaskThread,IdleTaskThread管理IdleTask类型对象的队列,根据预先设定的定时器触发IdleTask的调度。TCPListenerSocket就是一个IdleTask的派生类,当并发连接数达到设定的最大值时,会把派生自TCPListenerSocket的RTSPListenerSocket加入到IdleTaskThread管理的IdleTask队列中,暂时停止对RTSP端口的侦听,直到被设定好的定时器触发。

核心架构

下图是DarwinStreaming Server核心架构的示意图。在这个示意图中有三种类型的要素,分别是线程,Task队列或者堆,被侦听的事件。图中的文字都是从源代码中copy出来的,便于读者通过查找与源代码对应起来。

 【图片】

图中给出了三个线程,分别是TaskThread::Entry,EventThread::Entry以及IdleTaskThread::Entry。前文已经对这三种线程进行了概要描述。

除了三个线程,图中还有另外五个矩形块。与TaskThread::Entry线程相关联的有两个,分别是TaskThread::fTaskQueue队列和TaskThread::fHeap堆,通过调用Signal被调度等待完成的Task就放在队列或者堆中。与IdleTaskThread::Entry线程相关联的有一个,是IdleTaskThread::IdleHeap堆。与EventThread::Entry相关联的是EventContext::fEventReq,是被侦听的端口。还有一个是TimeoutTaskThread::fQueue队列,它事实上是通过TimeoutTask与TaskThread::Entry相关联。

图中指向线程的连接线表明从队列或者堆中取出Task,而对于EventThread::Entry线程来说,则是被侦听事件的发生。指向被侦听的端口的连接线表明把端口加入侦听,指向Task的队列或堆的连接线,表明把Task加入到队列或者堆中。连接线的文字给出的是相应的函数调用,可以直接在源代码中搜索到。

系统启动的时候调用QTSServer::StartTasks()把RTSP服务端口加入到侦听队列中。此时便开始接收客户端的RTSP连接请求了。

在EventThread::Entry中调用select_waitevent函数等待事件的发生,当有事件发生的时候,就通过调用ProcessEvent方法对事件进行相应的处理。注意ProcessEvent是一个虚函数,共有两个实现。EventContext类中实现了ProcessEvent方法,EventContext的派生类TCPListenerSocket中也实现了ProcessEvent方法。

对于建立RTSP连接的请求,调用TCPListenerSocket::ProcessEvent方法来处理,此方法调用RTSPListenerSocket的GetSessionTask方法建立一个RTSPSession,并把相应的套接口加入侦听队列,等待RTSP请求。然后还需调用this->RequestEvent(EV_RE)把建立RTSP连接的请求加入到侦听队列。

对于RTSP连接上的RTSP请求事件,调用的是EventContext::ProcessEvent方法,通过Task的Signal把对应的RTSPSession类型的Task加入到TaskThread::fTaskQueue中等待TaskThread处理。

TaskThread与Task

TaskThread::Entry调用TaskThread::WaitForTask()方法获得下一个需要处理的Task。TaskThread::WaitForTask()首先从TaskThread::fHeap中获得Task,如果TaskThread::fHeap中没有满足条件的Task,则从TaskThread::fTaskQueue中获得Task。

TaskThread::Entry调用Task::Run方法来完成对应的Task,Task::Run方法的返回值类型是SInt64,也即signedlong long int类型。TaskThread::Entry根据Task::Run方法的返回值进行不同的处理。对于小于0的返回值,需delete这个Task;对于大于0的返回值,返回值代表了下次处理这个Task需等待的时间,TaskThread::Entry调用fHeap.Insert(&theTask->fTimerHeapElem)把Task插入到堆里,并设定等待时间。对于等于0的返回值,TaskThread::Entry不再理会该Task。

TimeoutTask

从代码中看,TimeoutTaskThread是IdleTask的派生类,分析后发现从TimeoutTaskThread与IdleTask没有任何关系,完全可以从Task派生,修改代码后验证了这个想法。因此TimeoutTaskThread就是一个普通的Task,TimeoutTaskThread通过其Run方法监控一组超时任务,具体的比如RTSP协议或者RTP协议超时。

在系统启动的时候TimeoutTaskThread被加入到TaskThread的队列中,这是通过在StartServer函数中调用TimeoutTask::Initialize()来实现的。TimeoutTaskThread::Run函数的返回值是intervalMilli= kIntervalSeconds * 1000,也就是一个正数,于是TimeoutTaskThread这个Task会加入到TaskThread::fHeap中被周期性的调用。

TimeoutTaskThread::Run方法发现有超时的任务,则通过Signal方法调度这个Task,event为Task::kTimeoutEvent。被管理的这组任务,要有RefreshTimeout的机制。

一次点播请求的处理

为了更好的理解DarwinStreaming Server的架构,我们从客户端发起点播,触发服务器的建立RTSP连接事件的发生开始,看看DSS的工作流程是什么样的。

针对RTSP协议,DarwingStreaming Server在554端口上侦听,当有连接请求到达时,通过accept调用返回一个socket,对应的后续RTSP请求都是通过这个socket来传送的。我们把RTSP相关的事件分成两类,一类是RTSP连接请求,一类是RTSP请求。先来看RTSP连接请求的过程:

1.    RTSP连接请求到达后,被select_waitevent函数捕获,代码在EventContext.cpp的EventThread::Entry中232行。

2.    查找EventThread::fRefTable,获取对应的EventContext。得到的是EventContext类型的派生类RTSPListenerSocket。相应的代码在EventContext.cpp中的249到253行。

3.    调用ProcessEvent,处理事件。相应的代码在EventContext.cpp中的257行。注意,由于对应的EventContext类其实是RTSPListenerSocket,因此调用的应该是TCPListenerSocket::ProcessEvent。

4.    在TCPListenerSocket.cpp的106行TCPListenerSocket::ProcessEvent方法中,调用accept得到socket,在160行调用了GetSessionTask方法,对应的是RTSPListenerSocket::GetSessionTask,在QTSServer.cpp中定义。

5.    在RTSPListenerSocket::GetSessionTask方法中,QTSServer.cpp的1077行,调用NEWRTSPSession建立了一个新的RTSPSession。

6.    回到TCPListenerSocket.cpp文件中的TCPListenerSocket::ProcessEvent方法,注意189行,把刚刚建立好的RTSP连接加入到侦听队列,等待RTSP请求的到来。

RTSP请求的处理流程步骤如下,注意前面第一步是一样的:

1.    RTSP连接请求到达后,被select_waitevent函数捕获,代码在EventContext.cpp的EventThread::Entry中232行。

2.    查找EventThread::fRefTable,获取对应的EventContext。得到的是EventContext类。相应的代码在EventContext.cpp中的249到253行。

3.    调用ProcessEvent,处理事件。相应的代码在EventContext.cpp中的257行。注意,此时调用的是EventContext::ProcessEvent。

4.    EventContext::ProcessEvent方法在EventContext.h中实现,在127行。在138行调用了fTask->Signal(Task::kReadEvent),fTask就是RTSPSession类。把RTSPSession加入到TaskThread的队列等待RTSPSession::Run()被调用。

5.    后续就是RTSPSession::Run()对RTSP请求的具体的处理。

(全文完)

本项目是一个典型的电商项目,采用Python语言编写,Django框架搭建。

github中创建远程仓库

github上创建仓库meiduo,填写描述,选择.gitignore文件的语言为python,软件许可协议为MIT

修改.gitignore的内容为:.idea/*.pyc*.log

新建分支dev

在本地克隆项目

1
2
3
4
5
git clone https://github.com/junsircoding/meiduo.git # 克隆项目
cd meiduo # 进入项目目录
git branch # 查看当前分支
git branch dev origin/dev # 克隆远程仓库中的dev分支
git checkout dev # 切换到dev分支

在虚拟环境中创建项目

1
2
3
4
workon django_py3_1.11 # 进入虚拟环境
django-admin startproject shop # 创建项目,项目名称为shop
cd shop # 进入项目目录
pwd # 查看当前地址并拷贝,在pycharm中打开

在Pycharm中搭建项目

重设settings文件路径

开发环境和上线环境用不同的配置文件比较容易部署和维护,故而最好重新设置settings的路径。

django自动创建的项目中,根目录下有一个同名目录,在此做一个约定:根目录的shop一级shop,根目录下的同名目录为二级shop

新建名为settingspython package于二级shop中,将原二级shop中的settings.py更名为dev.py,并将其移动到新建的settings目录中。

修改一级shop下的manage.py中的环境变量:

1
2
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings") 
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings.dev")

配置jinja2模板

dev.py(位于二级shop)中配置jinja2的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TEMPLATES = [
{
'BACKEND': 'django.template.backends.jinja2.Jinja2',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],

'environment': 'meiduo_mall.utils.jinja2_env.jinja2_environment',
},
},
]

新建名为utilspython package二级shop中。

新建名为jinja2_env.pypython fileutils目录中,并在其中写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse
from jinja2 import Environment


def jinja2_environment(**options):
env = Environment(**options)
env.globals.update({
'static': staticfiles_storage.url,
'url': reverse,
})
return env


"""
确保可以使用Django模板引擎中的{% url('') %} {% static('') %}这类的语句
"""

配置mysql数据库

新建mysql数据库

1
mysql -uroot -p111
1
2
3
4
create database shop charset=utf8; 
create user shoproot identified by '111';
grant all on shop.* to 'shop'@'%';
flush privileges;

配置mysql数据库于dev.py

1
2
3
4
5
6
7
8
9
10
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': '127.0.0.1',
'PORT': 3306,
'USER': 'shoproot',
'PASSWORD': '111',
'NAME': 'shop'
},
}

修改__init__.py(二级shop目录),配置pymysql连接

1
2
3
from pymysql import install_as_MySQLdb
install_as_MySQLdb()

配置Redis中的缓存Session

dev.py(二级shop/settings)中添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
"session": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
}
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "session"

配置工程日志

新建名为logspython package一级shop

新建名为shop.logfile于目录logs

dev.py(二级shop/settings)中添加日志的配置信息:

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
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s'
},
'simple': {
'format': '%(levelname)s %(module)s %(lineno)d %(message)s'
},
},
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'console': {
'level': 'INFO',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'file': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(os.path.dirname(BASE_DIR), 'logs/shop.log'),
'maxBytes': 300 * 1024 * 1024,
'backupCount': 10,
'formatter': 'verbose'
},
},
'loggers': {
'django': {
'handlers': ['console', 'file'],
'propagate': True,
'level': 'INFO',
},
}
}

配置静态页面

将静态页面文件拷贝到二级shop目录下

dev.py中配置静态文件路径

1
2
3
4
5
STATIC_URL = '/static/'


STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

配置模板文件

新建名为templatespython package二级shop中,将其标记为Template Folder

项目环境搭建完毕,开启服务器,在浏览器中查看效果

在浏览器中输入地址:127.0.0.1:8000/static/index.html

编写用户模块代码

新建名为appspython package二级shop

进入apps目录,用Django命令创建子应用:

1
2
3
cd apps
python ../../manage.py startapp users

dev.py中注册app,在二级shop/apps/users/apps.py中,右击app名称sConfig,选择Copy Reference,拷贝引用,在dev.py中粘贴

1
2
‘shop.apps.users.apps.UsersConfig’,

dev.py中追加导包路径

1
2
3
import sys
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))

改写注册内容:

1
2
‘users.apps.UsersConfig’,

新建名为urls.pypython file二级shop/apps/users中,在其中填写如下内容:

1
2
3
4
5
6
7
from django.conf.urls import url
from . import views

urlpatterns = [
url(r'^register/$', views.RegisterView.as_view()),
]

将此子路由添加至总路由(二级shop/urls.py):

1
2
3
4
5
6
7
8
from django.conf.urls import url, include
from django.contrib import admin

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

编写视图函数RegisterView二级shop/apps/users/views.py中:

1
2
3
4
5
6
7
8
9
from django.shortcuts import render
from django.views import View


class RegisterView(View):
def get(self, request):
return render(request, 'register.html')


编写用户模型类

Django自带了用户模型类,如要添加别的字段,只需继承Django自带的模型类,再添加自己的特有字段即可

二级shop/apps/users/models.py中添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.db import models
from django.contrib.auth.models import AbstractUser




class User(AbstractUser):
"""自定义用户模型类"""
mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')

class Meta:
db_table = 'tb_users'
verbose_name = '用户'
verbose_name_plural = verbose_name

def __str__(self):
return self.username

dev.py中指定用户模型类:

1
2
AUTH_USER_MODEL = 'users.User' 

迁移用户模型类

创建迁移文件

1
2
python manage.py makemigrations

执行迁移文件

1
2
python manage.py migrate

编码

注册功能

填写完注册页面表单后,后台要处理POST请求

二级shop/apps/users/views.py/RegisterView中添加如下代码:

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
def post(self, request):

user_name = request.POST.get('user_name')
pwd = request.POST.get('pwd')
cpwd = request.POST.get('cpwd')
phone = request.POST.get('phone')
allow = request.POST.get('allow')


if not all([user_name, pwd, cpwd, phone, allow, sms_code_request]):
return http.HttpResponseBadRequest('参数不完整')

if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name):
return http.HttpResponseBadRequest('请输入5-20个字符')
if User.object.filter(username=user_name).count() > 0:
return http.HttpResponseBadRequest('用户名已存在')

if not re.match(r'[0-9A-Za-z]{8,20}$', pwd):
return http.HttpResponseBadRequest('请输入8-20的密码')
if pwd != cpwd:
return http.HttpResponseBadRequest('两次密码输入不一致')

if not re.match(r'^1[345789]\d{9}', phone):
return http.HttpResponseBadRequest('手机号格式不正确')
if User.objects.filter(mobile=phone).count() > 0:
return http.HttpResponseBadRequest('手机号已存在')

user = User.objects.create_user(username=user_name, password=pwd, mobile=phone)

login(request, user)

return redirect('/')

用Ajax异步校验的方式验证用户名是否存在

在子路由中添加ajax的路由:

1
2
url(r'^usernames/(?P<username>[a-zA-Z0-9_-]{5,20})/count/$', views.UsernameCountView.as_view()),

二级shop/apps/users/views.py中添加视图类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class UsernameCountView(View):
def get(self, request, username):



count = User.objects.filter(username=username).count()

return http.JsonResponse({
'code':RETCODE.OK,
'errmsg':'OK',
'count':count
})

用Ajax异步校验的方式验证手机号是否存在

在子路由中添加ajax的路由:

1
2
url('^mobiles/(?P<mobile>1[3-9]\d{9})/count/$', views.MobileCountView.as_view()),

二级shop/apps/users/views.py中添加视图类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MobileCountView(View):
def get(self, request, mobile):



count = User.objects.filter(mobile=mobile).count()

return http.JsonResponse({
'code':RETCODE.OK,
'errmsg':'OK',
'count':count
})

新建名为response_codepython fileutils目录中,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class RETCODE:
OK = "0"
IMAGECODEERR = "4001"
THROTTLINGERR = "4002"
NECESSARYPARAMERR = "4003"
USERERR = "4004"
PWDERR = "4005"
CPWDERR = "4006"
MOBILEERR = "4007"
SMSCODERR = "4008"
ALLOWERR = "4009"
SESSIONERR = "4101"
DBERR = "5000"
EMAILERR = "5001"
TELERR = "5002"
NODATAERR = "5003"
NEWPWDERR = "5004"
OPENIDERR = "5005"
PARAMERR = "5006"
STOCKERR = "5007"

图片验证码

安装pillow

1
2
pip install Pillow

新建名为libspython package目录于二级shop中,将第三方图片验证码工具captcha拷贝至这里。

新建名为verificationsappapps中:

1
2
python ../../manage.py startapp verifications

注册appdev.py中:

1
2
'verifications.apps.VerificationsConfig',

新建路由表urls,在子路由中添加路由:

1
2
3
4
urlpatterns = [
url(r'^image_codes/(?P<uuid>[\w-]+)/$', views.ImagecodeView.as_view()),
]

在总路由中包含子路由:

1
2
3
4
urlpatterns = [
url(r'^', include('verifications.urls')),
]

编写视图函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ImagecodeView(View):
def get(self, request, uuid):




text, code, image = captcha.generate_captcha()



redis_cli = get_redis_connection('verify_code')

redis_cli.setex(uuid, constants.IMAGE_CODE_EXPIRES, code)


return http.HttpResponse(image, content_type='image/png')

新建名为constantspy文件于verifications目录下,编写其内容如下:

1
2
3
4
5
6
7

IMAGE_CODE_EXPIRES = 60 * 5

SMS_CODE_EXPIRES = 60 * 5

SMS_CODE_FLAG_EXPIRES = 60

验证码字符在redis缓存中存储,在dev.py中新建缓存字段verify_code

1
2
3
4
5
6
7
8
"verify_code":{
"BACKEND":"django_redis.cache.RedisCache",
"LOCATION":"redis://127.0.0.1:6379/2",
"OPTIONS":{
"CLIENT_CLASS":"django_redis.client.DefaultClient",
}
},

注意:chrome有个大坑,它会缓存之前请求过的地址。验证码的请求地址是和host.js关联的。当更改了host.js时,重新访问发现地址并没有更改,这是chrome缓存的缘故,记得定期清理缓存。

短信验证码

拷贝工具代码yuntongxun(短信)子shop/libs目录中

verifications/url中添加路由:

1
2
3
4
5
6
7
8
from django.conf.urls import url
from . import views

urlpatterns = [
url(r'^image_codes/(?P<uuid>[\w-]+)/$', views.ImagecodeViews.as_view()),
url('^sms_codes/(?P<mobile>1[3-9]\d{9})/$', views.SmscodeView.as_view()),
]

verifications/views.py中添加名为SmscodeView的视图函数:

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
class SmscodeView(View):
def get(self, request):

image_code_request = request.GET.get('image_code')
uuid = request.GET.get('image_code_id')

redis_cli = get_redis_connection('verify_code')
image_code_redis = redis_cli.get(uuid)
if not image_code_redis:
return http.JsonResponse({
'code':RETCODE.PARAMERR,
'errmsg':'图形验证码已过期'
})

redis_cli.delete(uuid)


if image_code_redis.decode() != image_code_request.upper():
return http.JsonResponse({
'code':RETCODE.PARAMERR,
'errmsg':'图形验证码错误'
})


sms_code = '%6d' % random.randint(0, 999999)

redis_cli.setex('sms_'+mobile, 300, sms_code)



print(sms_code)

return http.JsonResponse({
'code':RETCODE.OK,
'errmsg':'OK'
})

在注册视图中验证短信验证码,改写二级shop/apps/users/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def post(self, request):

user_name = request.POST.get('user_name')
pwd = request.POST.get('pwd')
cpwd = request.POST.get('cpwd')
phone = request.POST.get('phone')
allow = request.POST.get('allow')
sms_code_request = request.POST.get('msg_code')


if not all([user_name, pwd, cpwd, phone, allow, sms_code_request]):
return http.HttpResponseBadRequest('参数不完整')

if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name):
return http.HttpResponseBadRequest('请输入5-20个字符')
if User.objects.filter(username=user_name).count() > 0:
return http.HttpResponseBadRequest('用户名已存在')

if not re.match(r'[0-9A-Za-z]{8,20}$', pwd):
return http.HttpResponseBadRequest('请输入8-20的密码')
if pwd != cpwd:
return http.HttpResponseBadRequest('两次密码输入不一致')

if not re.match(r'^1[345789]\d{9}', phone):
return http.HttpResponseBadRequest('手机号格式不正确')
if User.objects.filter(mobile=phone).count() > 0:
return http.HttpResponseBadRequest('手机号已存在')


redis_cli = get_redis_connection('verify_code')

sms_code_redis = redis_cli.get('sms_' + phone)

if not sms_code_redis:
return http.HttpResponseBadRequest('短信验证已过期')

redis_cli.delete('sms_' + phone)

if sms_code_redis != sms_code_request:
return http.HttpResponseBadRequest('短信验证码错误')


user = User.objects.create_user(username=user_name, password=pwd, mobile=phone)

login(request, user)

return redirect('/')

避免频繁发送短信,添加如下代码于verificatons/views.py中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25


if image_code_redis.decode() != image_code_request.upper():
return http.JsonRequest({
'code':RETCODE.PARAMERR,
'errmsg':'图形验证码错误'
})

if redis_cli.get('sms_flag_' + mobile):
return http.JsonResponse({
'code':RETCODE.PARAMERR,
'errmsg':'已经向次手机号发过短信,请查看手机'
})


sms_code = '%6d' % random.randint(0, 999999)

redis_cli.setex('sms_'+mobile, constants.SMS_CODE_EXPIRES, sms_code)

redis_cli.setex('sms_flag_' + mobile, constants.SMS_CODE_FLAG_EXPIRES, 1)



print(sms_code)

使用pipeline优化与redis交互,只与redis服务器交互一次,执行多条命令

1
2
3
4
5
6
7
8
9
10
11
12







redis_pl = redis_cli.pipeline()
redis_pl.setex('sms_'+mobile, constants.SMS_CODE_EXPIRES, sms_code)
redis_pl.setex('sms_flag_' + mobile, constants.SMS_CODE_FLAG_EXPIRES, 1)
redis_pl.execute()

使用celery框架实现异步

新建名为celery_taskspython package一级shop

新建名为mainpython filecelery_tasks中,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from celery import Celery
import os

os.environ["DJANGO_SETTINGS_MODULE"] = "shop.settings.dev"

app = Celery('shop')

app.config_from_object('celery_tasks.config')

app.autodiscover_tasks([
'celery_tasks.sms',
])

新建名为configpython filecelery_tasks中,内容如下:

1
2
broker_url = 'redis://127.0.0.1:6379/15'

新建名为smspython packagecelery_tasks

新建名为taskspython filesms中,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
from shop.libs.yuntongxun.sms import CCP
from celery_tasks.main import app

@app.task(bind=True, name='send_sms', retry_backoff = 3)
def send_sms(self, to, datas, tempid):
try:


print(datas[0])
except Exception as e:
self.retry(exc = e, max_retries = 3)

shell中运行如下命令,开启celery服务:

1
2
celery -A celery_tasks.main worker -l info

调用任务,于二级shop/apps/users/views.py中:

1
2
3
4
5
6
7
from celery_tasks.sms.tasks import send_sms




send_sms.delay(mobile, [sms_code, contansts.SMS_CODE_EXPIRES / 60], 1)

登录功能

二级shop/apps/users/urls.py中添加登录的路由:

1
2
url('^login/$', views.LoginView.as_view()),

二级shop/apps/users/views.py中添加视图类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class LoginView(View):
def get(self, request):
return render(request, 'login.html')
def post(self, request):

username = request.POST.get('username')
pwd = request.POST.get('pwd')

if not all([user_name, pwd]):
return http.HttpResponseBadRequest('参数不完整')

if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name):
return http.HttpResponseBadRequest('请输入5-20个字符')
if not re.match(r'[0-9A-Za-z]{8,20}$', pwd):
return http.HttpResponseBadRequest('请输入8-20的密码')

user = authenticate(username = username, pwd = pwd)
if user is None:

return render(request, 'login.html', {
'loginerror':'用户名或密码错误'
})
else:

login(request, user)
return redirect('/')


多账号登录

新建名为shop_backendspython fileutils中,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from django.contrib.auth.backends import ModelBackend
class ShopModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):

try:
if re.match('^1[3-9]\d{9}$', username):

user = User.objects.get(mobile=username)
else:

user = User.objects.get(username=username)
except:
return None
else:

if user.check_password(password):
return user
else:
return None

dev.py中添加自定义认证类型:

1
2
AUTHENTICATION_BACKENDS = ['shop.utils.auth_backends.ShopModelBackend']

首页用户名显示

新建应用,cd shop/shop/appspython ../../manage.py startapp contents

注册应用,新建urls.py

1
2
3
4
5
6
7
from django.conf.urls import url
from . import views

urlpatterns = [
url('^$', views.IndexView.as_view()),
]

dev.py中注册应用:

1
2
'contents.apps.ContentsConfig',

在总路由中添加子路由:

1
2
url('^', include('content.urls')),

添加视图类:

1
2
3
4
5
6
from django.shortcuts import render
from django.views import View
class IndexView(View):
def get(self, request):
return render(request, 'index.html')

更改users/views.py.LoginView

1
2
3
4
5
6
7
8
else:

login(request, user)

response = redirect('/')
response.set_cookie('username', user.username, max_age=60*60*24*14)
return response

更改users/views.py.RegisterView

1
2
3
4
5

response = redirect('/')
response.set_cookie('username', user.username, max_age=60*60*24*14)
return response

退出

users/views.py中添加新的视图类:

1
2
3
4
5
6
7
8
9
class LogoutView(View):
def get(self, request):

logout(request)

response = redirect('/')
response.delete_cookie('usename')
return response

users.urls.py中添加路由:

1
2
url('^logout/$', views.LogoutView.as_view()),

用户个人信息

添加路由在users/urls.py中:

1
2
url('^info/$', views.InfoView.as_view()),

添加视图类在users/views.py中:

1
2
3
4
5
6
7
8
9
10
class InfoView(View):
def get(self, request):

if request.user.is_authenticated:

return render(request, 'user_center_info.html')
else:

return redirect('/login/')

拷贝页面至templates中。

判断是否登录

dev.py中指定登录页视图:

1
2
LOGIN_URL = '/login/'

改写views.py中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
from django.contrib.auth.mixins import LoginRequiredMixin
class InfoView(LoginRequiredMixin, View):
def get(self, request):







return render(request, 'user_center_info.html')

完善登录视图代码:

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
class LoginView(View):
def get(self, request):
return render(request, 'login.html')
def post(self, request):

username = request.POST.get('username')
pwd = request.POST.get('pwd')

next_url = request.GET.get('next', '/')

if not all([user_name, pwd]):
return http.HttpResponseBadRequest('参数不完整')

if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name):
return http.HttpResponseBadRequest('请输入5-20个字符')
if not re.match(r'[0-9A-Za-z]{8,20}$', pwd):
return http.HttpResponseBadRequest('请输入8-20的密码')

user = authenticate(username = username, pwd = pwd)
if user is None:

return render(request, 'login.html', {
'loginerror':'用户名或密码错误'
})
else:

login(request, user)

response = redirect(next_url)
response.set_cookie('username', user.username, max_age=60*60*24*14)
return response

QQ授权登录

在虚拟环境中安装QQLoginTool

1
2
pip install QQLoginTool

新建名为oauth的应用,python ../../manage.py startapp oauth

新建urls.py,内容如下:

1
2
3
4
5
6
7
from django.conf.urls import url
from . import views
urlpatterns = [
url('^qq/login/$', views.QQurlView.as_view()),
url('^oauth_callback$', views.QQopenidView.as_view()),
]

在总路由中添加子路由:

1
2
url('^', include('oauth.urls')),

dev.py中注册app

1
2
'oauth.apps.OauthConfig',

views.py中创建类视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from django.shortcuts import render
from django.views import View
from django import http
from QQLoginTool.QQtool import OAuthQQ
from django.conf import settings

class QQurlView(View):
def get(self, request):

next_url = request.GET.get('next', '/')

oauthqq_tool = OAuthQQ(
settings.QQ_CLIENT_ID,
settings.QQ_CLIENT_SECRET,
settings.QQ_CLIENT_URI,
next_url
)
login_url = oauthqq_tool.get_qq_url()
return http.JsonResponse({
'code':RETCODE.OK,
'errmsg':'OK',
'login_url':login_url
})
class QQopenidView(View):
def get(self, request):


code = request.GET.get('code')
next_url = request.GET.get('state', '/')

oauthqq_tool = OAuthQQ(
settings.QQ_CLIENT_ID,
settings.QQ_CLIENT_SECRET,
settings.QQ_CLIENT_URI,
next_url
)
try:

token = oauthqq_tool.get_access_token(code)

openid = oauthqq_tool.get_openid(token)
except:
openid = '0'
return http.HttpResponse(openid)

dev.py中添加QQ授权信息:

1
2
3
4
QQ_CLIENT_ID = '101518219'
QQ_CLIENT_SECRET = '418d84ebdc7241efb79536886ae95224'
QQ_REDIRECT_URI = 'http://www.meiduo.site:8000/oauth_callback'

/etc/hosts中添加127.0.0.1 www.meiduo.site

dev.py中添加:ALLOWED_HOSTS = ['www.meiduo.site',]

host.js中的var host = 'http://www.meiduo.site:8000'的注释打开,其余均注释

QQ账号信息与本网站绑定

新建视图类于oauth/model.py中:

1
2
3
4
5
6
7
8
9
10
from users.models import User
from shop.utils.models import BaseModel

class OAuthQQUser(models.Model):
user = models.ForeignKey(User)
openid = models.CharField(max_length = 50)

class Meta:
db_table = 'tb_oauth_qq'

新建名为models.pypython file文件于utils目录中,内容为:

1
2
3
4
5
6
7
8
9
from django.db import models
class BaseModel(models.Model):
create_time = models.DateField(auto_now_add = True)
update_time = models.DateField(auto_now = True)
class Meta:

abstract = True


执行迁移:

1
2
3
python manage.py makemigrations
python manage.py migrate

改写oauth/views.py文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
from django.shortcuts import render
from django.views import View
from django import http
from QQLoginTool.QQtool import OAuthQQ
from django.conf import settings
from .models import OAuthQQUser

class QQurlView(View):
def get(self, request):

next_url = request.GET.get('next', '/')

oauthqq_tool = OAuthQQ(
settings.QQ_CLIENT_ID,
settings.QQ_CLIENT_SECRET,
settings.QQ_CLIENT_URI,
next_url
)
login_url = oauthqq_tool.get_qq_url()
return http.JsonResponse({
'code':RETCODE.OK,
'errmsg':'OK',
'login_url':login_url
})
class QQopenidView(View):
def get(self, request):


code = request.GET.get('code')
next_url = request.GET.get('state', '/')

oauthqq_tool = OAuthQQ(
settings.QQ_CLIENT_ID,
settings.QQ_CLIENT_SECRET,
settings.QQ_CLIENT_URI,
next_url
)
try:

token = oauthqq_tool.get_access_token(code)

openid = oauthqq_tool.get_openid(token)

try:

qquser = OAuthQQUser.objects.get(openid=openid)
except:

return render(request, 'oauth_callback.html')

except:
openid = '0'
return http.HttpResponse(openid)

views.py中添加post方法:

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
class QQopenidView(View):
def get(self, request):


code = request.GET.get('code')
next_url = request.GET.get('state', '/')

oauthqq_tool = OAuthQQ(
settings.QQ_CLIENT_ID,
settings.QQ_CLIENT_SECRET,
settings.QQ_CLIENT_URI,
next_url
)
try:

token = oauthqq_tool.get_access_token(code)

openid = oauthqq_tool.get_openid(token)

try:

qquser = OAuthQQUser.objects.get(openid=openid)
except:

context = {'token':openid}
return render(request, 'oauth_callback.html', context)

except:
openid = '0'
return http.HttpResponse(openid)
def post(self, request):


安装加密的包:itsdangerous

1
2
pip install itsdangerous

新建名为shop_signature.pypython file文件于utils.py中,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings

def dumps(json, expires):
'''
:param json:字典
:param expires:加密数据的过期时间
:return:字符串
'''


serializer = Serializer(settings.SECRET_KEY, expires)

serializer.dumps(json)

return s1.decode()

def loadds(s1, expires):
'''
:param s1:字符串
:param expires:加密数据的过期时间
:return:字典
'''


serializer = Serializer(settings.SECRET_KEY, expires)

try:
json = serializer.loads(s1)
except:
return None

return json


更改views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class QQopenidView(View):
def get(self, request):


code = request.GET.get('code')
next_url = request.GET.get('state', '/')

oauthqq_tool = OAuthQQ(
settings.QQ_CLIENT_ID,
settings.QQ_CLIENT_SECRET,
settings.QQ_CLIENT_URI,
next_url
)
try:

token = oauthqq_tool.get_access_token(code)

openid = oauthqq_tool.get_openid(token)

try:

qquser = OAuthQQUser.objects.get(openid=openid)
except:

token = shop_signature.dumps({'openid':openid}, contants.OPENID_EXPIRES)
context = {'token':openid}
return render(request, 'oauth_callback.html', context)

except:
openid = '0'
return http.HttpResponse(openid)
def post(self, request):


新建名为contantspython file文件于oauth目录中,内容如下:

1
2
3

OPENID_EXPIRES = 60 * 10

写post方法:

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
def post(self, request):


mobile = request.POST.get('mobile')
pwd = request.POST.get('pwd')
sms_code_request = request.POST.get('sms_code')
access_token = request.POST.get('access_token')
next_url = request.GET.get('state')

json = shop_signature.loadds(access_token, contants.OPENID_EXPIRES)
if json is None:
return http.HttpResponseBadRequest('授权信息无效,请重新授权')
openid = json.get('openid')


try:
user = User.objects.get(mobile = mobile)
except:

user = User.objects.create_user(username=mobile, password=pwd, mobile=mobile)
else:

if not user.check_password(pwd):
return http.HttpResponseBadRequest('密码错误')

OAuthQQUSer.objects.create(user=user, openid=openid)

login(request, user)

response = redirect(next_url)
response.set_cookie('username', user.username, max_age = 60 * 60 * 24 * 14)
return response

非初次授权的情况。补充get方法:

1
2
3
4
5
6

login(request, qquser.user)
response = redirect(next_url)
response.set_cookie('username', qquser.user.username, max_age = 60 * 60 * 24 * 14)
return response

显示用户的个人信息

改写users/views.py/InfoView

1
2
3
4
5
6
7
8
9
10
11
12
def InfoView(LoginRequiredMixin, View):
def get(self, request):

user = request.user
context = {
'username':user.username,
'mobile':user.mobile,
'email':user.email,
'email_active':user.email_active
}


models.py/User中添加邮箱激活属性:

1
2
3
4
5
class User(AbstractUser):
mobile = model.CharField(max_length=11)

email_active = models.BooleanField(default=False)

迁移:

1
2
3
python manage.py makemigratons
python manage.py migrate

邮箱

users/urls.py中添加路由:

1
2
url('^emails$', views.EmailView.as_view()),

users.views.py中添加视类图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class EmailView(LoginRequiredMixin, View):
def put(self, request):

dict1 = json.loads(request.body.decode())
email = dict1.get('email')

if not all([email]):
return http.JsonResponse({
'code':RETCODE.PARAMERR,
'errmsg':'没有邮箱参数'
})
if not re.match('^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
return http.JsonResponse({
'code':RETCODE.PARAMERR,
'errmsg':'邮箱格式错误'
})


user = request.user
user.email = email
user.save()


return http.JsonResponse({
'code':RETCODE.OK,
'errmsg':'OK'
})


发邮件

dev.py中添加邮件服务器配置:

1
2
3
4
5
6
7
8
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 25
EMAIL_HOST_USER = 'hmmeiduo@163.com'
EMAIL_HOST_PASSWORD = 'hmmeiduo123'
EMAIL_FROM = '美多商城<hmmeiduo@163.com>'
EMAIL_VERIFY_URL = 'http://www.meiduo.site:8000/emails/verification/'

新建名为mailpython packagecelery_tasks目录中,在其中新加文件tasks.py

在其中定义方法:

1
2
3
4
5
6
7
8
9
10
11
from django.core.mail import send_mail
from django.conf import settings
from celery_tasks.main import app
@app.task(name='send_user_email', bind=True)
def send_user_email(to_mail, verify_url):
html_message = '您的邮箱为:%s,激活链接为%s' % (to_mail, verify_url)
try:
send_mail('美多商城-邮箱激活','', settings.EMAIL_FROM, [to_mail], html_message=html_message)
except Exception as e:
self.retry(exc=e, max_retries=2)

celery_tasks/sms/main.py中添加任务:

1
2
3
4
5
app.autodiscover_tasks([
'celery_tasks.sms',
'celery_tasks.mail',
])

启动celery:celery -A celery tasks.main worker -l info

views.py中调用任务

1
2
3
4
5
6
7
from celery_tasks.mail.tasks import send_user_email
...

token = shop_signature.dumps({'user_id':user_id}, contants.EMAIL_EXPIRES)
verify_url = settings.EMAIL_VERIFY_URL + '?token=%s' % token
send_user_email.delay(email, verify_url)

users,目录总新建contants.py,内容为:

1
2
3

EMAIL_EXPIRES = 60 * 60 * 2

激活邮箱

新建视图类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class EmailVerifyView(View):
def get(self, request):

token = request.GET.get('token')


dict1 = meiduo_signature.loadds(token, contants.EMAIL_EXPIRES)
if dict1 is None:
return http.HttpResponseBadRequest('激活信息无效,请重新发邮件')
user_id = dict1.get('user_id')


try:
user = User.objects.get(pk=user_id)
except:
return http.HttpResponseBadRequest('激活信息无效')
else:
user.email_active = True
user.save()


return redirect('/info/')

添加路由:

1
2
url('^emails/verifyication$', views.EmailVerifyView.as_view()),

收货地址

users/urls.py中添加路由:

1
2
url('^addresses$', views.AddressesView.as_view()),

users/views.py中定义视图类:

1
2
3
4
class AddressesView(LoginRequiredMixin, View):
def get(self, request):
return render(request, 'user_center_site.html')

新建应用

1
2
3
cd shop/shop/apps/
python ../../manage.py startapp areas

新建路由urls.py

1
2
3
4
5
6
7
from django.conf.urls import url
from . import views

urlpatterns = [

]

在总路由中添加子路由

1
2
url('^', include('areas.urls')),

注册app

1
2
'areas.apps.AreasConfig',

Docker 常用命令 - Phuker’s Blog

Excerpt

这篇文章是洒家刚接触 Docker 时写的,部分操作即使在当时也不太恰当。推荐使用 Docker Compose 等容器编排工具,尽量不要直接使用 docker 命令。 由于 Docker 版本变化很快(笑),这篇文章的内容已经过时,可能不会再更新。建议查阅 Docker Documentation。 …


这篇文章是洒家刚接触 Docker 时写的,部分操作即使在当时也不太恰当。推荐使用 Docker Compose 等容器编排工具,尽量不要直接使用 docker 命令。

这篇文章最初发布于旧博客。2017 年 4 月 14 日搭建新博客时,对内容做了一些增改,作为测试文章发出来。

这篇文章针对 Ubuntu Server 16.04 和 Docker 1.12.6,主要参考《Docker 技术入门与实战》(杨保华等编著),内容都是洒家用过的命令。

Table of Contents

安装

现在 Docker 又分成了 Community Edition 和 Enterprise Edition,以前安装 docker.io 包的命令已经过时,请参考官方文档的安装方法。

搜索镜像

一般我们只需要拉取官方镜像,基于官方镜像写 Dockerfile。常用的镜像也就那几个,因此搜索功能并不常用,一般直接在 Docker Hub 网站搜索即可。

使用命令搜索

我们也可以使用 docker 命令搜索:

输出:

NAME DESCRIPTION STARS OFFICIAL AUTOMATED
debian Debian is a Linux distribution that’s comp… 1519 [OK]
neurodebian NeuroDebian provides neuroscience research… 25 [OK]
jesselang/debian-vagrant Stock Debian Images made Vagrant-friendly … 8 [OK]
armbuild/debian ARMHF port of debian 8 [OK]

常用镜像

下载镜像

指定镜像的 tag:

1
2
<span></span><code><span># </span>docker<span> </span>pull<span> </span>php:5.6.24-apache
</code>

如果不指定 tag,则视为使用默认 latest tag:

显示本地镜像

输出:

REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
ubuntu latest 4ef6a5ece191 3 days ago 120.1 MB
reinblau/lamp latest e9df29833f32 9 days ago 703.8 MB

创建并启动容器

临时运行 ubuntu:latest 镜像(退出后自动删除容器),把工作目录挂载到容器中,把宿主机 8080 端口映射到容器 80 端口

1
2
3
4
<span></span><code><span># </span>docker<span> </span>run<span> </span>-it<span> </span>--rm<span> </span>--mount<span> </span><span>"type=bind,src=</span><span>$(</span><span>pwd</span><span>)</span><span>,dst=/opt"</span><span> </span>-w<span> </span>/opt<span> </span>-p<span> </span><span>8080</span>:80<span> </span>ubuntu:latest<span> </span>bash
<span>root@0123456789ab:/opt# </span><span>pwd</span>
<span>/opt</span>
</code>

运行某 LAMP 镜像,把宿主机 8080 端口映射到容器 80 端口,e9d 是 镜像 ID 或镜像名

1
2
3
4
5
6
7
<span></span><code><span># </span>docker<span> </span>run<span> </span>-it<span> </span>-p<span> </span><span>8080</span>:80<span> </span>e9d<span> </span>apache2
<span>AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.1. Set the 'ServerName' directive globally to suppress this message</span>

<span># </span>docker<span> </span>run<span> </span>-it<span> </span>-p<span> </span><span>8080</span>:80<span> </span>e9d<span> </span>/bin/bash
<span>root@0123456789ab:/var/www/html# </span>apache2
<span>AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message</span>
</code>

在后台运行,启动后容器内自动运行 /root/run.sh

1
2
<span></span><code><span># </span>docker<span> </span>run<span> </span>-itd<span> </span>-p<span> </span><span>8080</span>:80<span> </span>e9d<span> </span>/root/run.sh
</code>

参数

加上 -i-t 可以先按 Ctrl + P,再按 Ctrl + Q unattach(退出并保持运行)。

1
2
3
4
5
6
7
<span></span><code>-d, --detach=false         Run container in background and print container ID
-i, --interactive=false Keep STDIN open even if not attached
-P, --publish-all=false Publish all exposed ports to random ports
-p, --publish=[] Publish a container's port(s) to the host
-t, --tty=false Allocate a pseudo-TTY
--name string Assign a name to the container
</code>

列出容器

1
2
3
4
5
<span></span><code><span># </span>docker<span> </span>ps<span> </span>-a
<span>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES</span>
<span>ac4c74c9ac8a e9d:latest "/bin/bash" 7 minutes ago Up 7 minutes 0.0.0.0:8080-&gt;80/tcp insane_mayer</span>
<span>3a4b37b41ea7 e9d:latest "apache2" 7 minutes ago Exited (0) 7 minutes ago suspicious_darwin</span>
</code>

参数

1
2
3
<span></span><code>-a, --all=false       Show all containers (default shows just running)
-q, --quiet=false Only display numeric IDs
</code>

依附到运行中的容器

ac4c 是容器号

操作完毕退出时,不要按 Ctrl + C,否则会停止 Docker 容器。

要先按 Ctrl + P,再按 Ctrl + Q

在容器内增加进程

使用 docker exec 命令:

1
2
<span></span><code><span># </span>docker<span> </span><span>exec</span><span> </span>-it<span> </span>88c<span> </span>/bin/bash
</code>

退出:输入 exit 命令(不必先按 Ctrl + P 再按 Ctrl + Q

容器内外复制文件

如果有这种需求,建议使用 volume。

c9f 是容器 ID

容器外向容器内

1
2
3
<span></span><code><span># </span>docker<span> </span><span>exec</span><span> </span>-i<span> </span>c9f<span> </span>/bin/sh<span> </span>-c<span> </span><span>'cat &gt; /var/static/original/img/xxx.jpg'</span><span> </span>&lt;<span> </span>./xxx.jpg
<span># </span>docker<span> </span><span>exec</span><span> </span>-i<span> </span>7d0<span> </span>/bin/sh<span> </span>-c<span> </span><span>'cat &gt; /home/ctf/pwn1'</span><span> </span>&lt;<span> </span>./pwn1
</code>

容器内向容器外

1
2
<span></span><code><span># </span>docker<span> </span>cp<span> </span>c9f:/opt/CTFd/CTFd/static/original/img/xxx.jpg<span> </span>./
</code>

目录自动递归复制

查看端口映射

ac4c 是容器 ID

1
2
3
<span></span><code><span># </span>docker<span> </span>port<span> </span>ac4c
<span>80/tcp -&gt; 0.0.0.0:8080</span>
</code>

Dockerfile

ENTRYPOINT 和 CMD 的区别

The main purpose of a CMD is to provide defaults for an executing container. These defaults can include an executable, or they can omit the executable, in which case you must specify an ENTRYPOINT instruction as well.

  • CMD [“executable”,”param1”,”param2”] (exec form, this is the preferred form)
  • CMD [“param1”,”param2”] (as default parameters to ENTRYPOINT)
  • CMD command param1 param2 (shell form)

从 Dockerfile 创建镜像

1
2
<span></span><code><span># </span>docker<span> </span>build<span> </span>-t<span> </span>mylamp/test<span> </span>~/Docker/mylamp_test/
</code>

上例中,Dockerfile 文件位于 ~/Docker/mylamp_test/,镜像 tag 为 mylamp/test

参数

1
2
<span></span><code>-t, --tag=            Repository name (and optionally a tag) for the image
</code>

删除镜像

先删除所有依赖容器,再删除镜像。

后面跟上标签或 ID,跟标签会先删除标签(untag),如果没有标签指向镜像,就删除(delete)镜像。

跟 ID,删除所有相关标签(untag),再删除(delete)镜像。

1
2
3
<span></span><code><span># </span>docker<span> </span>rmi<span> </span><span>2318</span>
<span># </span>docker<span> </span>rmi<span> </span>ubuntu
</code>

删除容器

停止容器

docker stop

命令用法:

1
2
3
4
5
<span></span><code>Usage:  docker stop [OPTIONS] CONTAINER [CONTAINER...]

Options:
-t, --time int Seconds to wait for stop before killing it (default 10)
</code>

使用 docker stop 命令,Docker 首先会向容器发送一个 SIGTERM 信号。如果默认 10 秒之后没有停止,会发送 SIGKILL 信号强行停止。可以用 -t 参数修改等待时间。

Docker 1.12.6 存在的 bug:如果用一个 Shell Script 脚本作为 Docker 的 ENTRYPOINT,最后一条命令是 sleep infinity 或者 exec sleep infinity 等都会出现可以收到 SIGTERM 但是不能立即停止的问题。暂时的解决方法是 Shell Script 中最后执行 exec bash 才对 SIGTERM 有反应。

docker kill

如果想发送 SIGKILL 信号直接杀死容器,可以使用:

命令用法:

1
2
3
4
5
<span></span><code>Usage:  docker kill [OPTIONS] CONTAINER [CONTAINER...]

Options:
-s, --signal string Signal to send to the container (default "KILL")
</code>

批量操作容器

停止所有正在运行的容器:

1
2
<span></span><code><span># </span>docker<span> </span>stop<span> </span><span>$(</span>docker<span> </span>ps<span> </span>-q<span>)</span>
</code>

删除所有已经停止的容器:

删除所有已经停止的容器(旧方法,利用正在运行的容器无法删除,会报错并跳过的特性):

1
2
<span></span><code><span># </span>docker<span> </span>rm<span> </span><span>$(</span>docker<span> </span>ps<span> </span>-a<span> </span>-q<span>)</span>
</code>

更改容器端口

建议使用 Docker Compose 和 volume,不要在 container、iptables 上瞎搞。

以 CTFd 为例。老版本的 CTFd 没有使用 volume,可以直接这么搞

1
2
3
4
5
6
7
8
9
10
11
<span></span><code><span># </span>docker<span> </span>ps<span> </span>-a
<span>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES</span>
<span>e669c2bddb74 ctfd "gunicorn --bind 0.0." 16 minutes ago Up 16 minutes 0.0.0.0:8086-&gt;8000/tcp ctfd</span>
<span># </span>docker<span> </span>commit<span> </span>e669<span> </span>ctfdrunning
<span>sha256:bae813fdc553022c9a6fdb2bb7bcddb182cb2c7ab9ef396ac9941ab3ef17a8e2</span>
<span># </span>docker<span> </span>images
<span>REPOSITORY TAG IMAGE ID CREATED SIZE</span>
<span>ctfdrunning latest bae813fdc553 4 seconds ago 507.2 MB</span>
<span># </span>docker<span> </span>stop<span> </span>e669
<span># </span>docker<span> </span>run<span> </span>-itd<span> </span>-p<span> </span><span>8000</span>:8000<span> </span>--name<span> </span>ctfd_newport<span> </span>ctfdrunning
</code>

CTFd 的新版本有 volume,docker commit 不包括 volume。可以运行一个新的容器,直接加上参数

1
2
<span></span><code><span># </span>docker<span> </span>run<span> </span>-itd<span> </span>-p<span> </span><span>8912</span>:8000<span> </span>--name<span> </span>ctfd_newport<span> </span>--volumes-from<span> </span>&lt;老的<span> </span>container<span> </span>的<span> </span>name&gt;<span> </span>&lt;ctfd<span> </span>的<span> </span>image<span> </span>名&gt;
</code>

查看容器详细信息

使用 docker inspect 命令:

1
2
3
4
5
6
7
8
9
<span></span><code>Usage:  docker inspect [OPTIONS] CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...]

Return low-level information on a container, image or task

-f, --format Format the output using the given go template
--help Print usage
-s, --size Display total file sizes if the type is container
--type Return JSON for specified type, (e.g image, container or task)
</code>

Volume

查看所有 volume

1
2
<span></span><code><span># </span>docker<span> </span>volume<span> </span>inspect<span> </span><span>$(</span>docker<span> </span>volume<span> </span>ls<span> </span>-q<span>)</span>
</code>