好库文摘 http://doc.okbase.net/ MongoDB 4.X 用户和角色权限管理总结 http://doc.okbase.net/doclist/archive/265782.html doclist 2019-5-5 10:44:00

关于MongoDB的用户和角色权限的梳理一直不太清晰,仔细阅读了下官方文档,并对此做个总结。

默认情况下,MongoDB实例启动运行时是没有启用用户访问权限控制的,也就是说,在实例本机服务器上都可以随意登录实例进行各种操作,MongoDB不会对连接客户端进行用户验证,可以想象这是非常危险的。为了强制开启用户访问控制(用户验证),则需要在MongoDB实例启动时使用选项--auth或在指定启动配置文件中添加选项auth=true

本文就MongoDB用户的权限和角色管理进行测试,主要参考的是官方文档说明。

版本说明

操作系统:CentOS Linux release 7.5.1804 (Core)
数据库版本:MongoDB v4.0.9

启用访问控制

MongoDB使用的是基于角色的访问控制(Role-Based Access Control,RBAC)来管理用户对实例的访问。通过对用户授予一个或多个角色来控制用户访问数据库资源的权限和数据库操作的权限,在对用户分配角色之前,用户无法访问实例。

在实例启动时添加选项--auth或指定启动配置文件中添加选项auth=true

角色

在MongoDB中通过角色对用户授予相应数据库资源的操作权限,每个角色当中的权限可以显式指定,也可以通过继承其他角色的权限,或者两都都存在的权限。

权限

权限由指定的数据库资源(resource)以及允许在指定资源上进行的操作(action)组成。

  1. 资源(resource)包括:数据库、集合、部分集合和集群;
  2. 操作(action)包括:对资源进行的增、删、改、查(CRUD)操作。

在角色定义时可以包含一个或多个已存在的角色,新创建的角色会继承包含的角色所有的权限。在同一个数据库中,新创建角色可以继承其他角色的权限,在admin数据库中创建的角色可以继承在其它任意数据库中角色的权限。

关于角色权限的查看,可以通过如下命令查询:

// 查询当前数据库中的角色权限
> db.runCommand({ rolesInfo: "<rolename>" })

// 查询其它数据库中指定的角色权限
> db.runCommand({ rolesInfo: { role: "<rolename>", db: "<database>" } }

// 查询多个角色权限
> db.runCommand(
    {
        rolesInfo: [
            "<rolename>",
            { role: "<rolename>", db: "<database>" },
            ...
        ]     
    }
)

// 查询所有角色权限(仅用户自定义角色)
> db.runCommand({ rolesInfo: 1 })

// 查询所有角色权限(包含内置角色)
> db.runCommand({ rolesInfo: 1, showBuiltinRoles: true })

系统内置角色

MongoDB内部提供了一系列内置的角色,这些角色是为了完成一些基本的数据库操作。每个内置角色提供了用户在角色数据库内数据库级别所有非系统类集合的访问权限,也提供了对集合级别所有系统集合的访问权限。MongoDB在每个数据库上都提供内置的数据库用户角色数据库管理角色,但只在admin数据库中提供其它的内置角色。

内置角色主要包括以下几个类别:

  1. 数据库用户角色(Database User Roles)
  2. 数据库管理角色(Database Administration Roles)
  3. 集群管理角色(Cluster Administration Roles)
  4. 备份和恢复角色(Backup and Restoration Roles)
  5. 全数据库级角色(All-Database Roles)
  6. 超级用户角色(Superuser Roles)
  7. 内部角色(Internal Role)

数据库用户角色

  • read

read角色包含读取所有非系统集合数据和订阅部分系统集合(system.indexes、system.js、system.namespaces)的权限。

该角色权限包含命令操作:changeStream、collStats、dbHash、dbStats、find、killCursors、listIndexes、listCollections。

  • readWrite

readWrite角色包含read角色的权限同时增加了对非系统集合数据的修改权限,但只对系统集合system.js有修改权限。

该角色权限包含命令操作:collStats、convertToCapped、createCollection、dbHash、dbStats、dropCollection、createIndex、dropIndex、find、insert、killCursors、listIndexes、listCollections、remove、renameCollectionSameDB、update。

数据库管理角色

  • dbAdmin

dbAdmin角色包含执行某些管理任务(与schema相关、索引、收集统计信息)的权限,该角色不包含用户和角色管理的权限。

对于系统集合(system.indexes、system.namespaces、system.profile)包含命令操作:collStats、dbHash、dbStats、find、killCursors、listIndexes、listCollections、dropCollection and createCollection(仅适用system.profile)

对于非系统集合包含命令操作:bypassDocumentValidation、collMod、collStats、compact、convertToCapped、createCollection、createIndex、dbStats、dropCollection、dropDatabase、dropIndex、enableProfiler、reIndex、renameCollectionSameDB、repairDatabase、storageDetails、validate

  • dbOwner

dbOwner角色包含对数据所有的管理操作权限。即包含角色readWrite、dbAdmin和userAdmin的权限。

  • userAdmin

userAdmin角色包含对当前数据库创建和修改角色和用户的权限。该角色允许向其它任何用户(包括自身)授予任何权限,所以这个角色也提供间接对超级用户(root)的访问权限,如果限定在admin数据中,也包括集群管理的权限。

该角色权限包含命令操作:changeCustomData、changePassword、createRole、createUser、dropRole、dropUser、grantRole、revokeRole、setAuthenticationRestriction、viewRole、viewUser。

集群管理角色

  • clusterManager

clusterManager角色包含对集群监控和管理操作的权限。拥有此角色的用户能够访问集群中的config数据库和local数据库。

对于整个集群该角色包含命令操作:addShard、appendOplogNote、applicationMessage、cleanupOrphaned、flushRouterConfig、listSessions (3.6新增)、listShards、removeShard、replSetConfigure、replSetGetConfig、replSetGetStatus、replSetStateChange、resync。

对于集群中所有的数据库包含命令操作:enableSharding、moveChunk、splitChunk、splitVector。

对于集群中config数据库和local数据库包含的命令操作可以参考官方文档:https://docs.mongodb.com/manual/reference/built-in-roles/#clusterManager

  • clusterMonitor

clusterMonitor角色包含针对监控工具具有只读操作的权限。如工具MongoDB Cloud Manager和工具Ops Manager

对于整个集群该角色包含命令操作:checkFreeMonitoringStatus(4.0新增)、connPoolStats、getCmdLineOpts、getLog、getParameter、getShardMap、hostInfo、inprog、listDatabases、listSessions (3.6新增)、listShards、netstat、replSetGetConfig、replSetGetStatus、serverStatus、setFreeMonitoring (4.0新增)、shardingState、top。

对于集群中所有的数据为包含命令操作:collStats、dbStats、getShardVersion、indexStats、useUUID(3.6新增)。

对于集群中config数据库和local数据库包含的命令操作可以参考官方文档:https://docs.mongodb.com/manual/reference/built-in-roles/#clusterMonitor

  • hostManager

hostManager角色包含针对数据库服务器的监控和管理操作权限。

对于整个集群该角色包含命令操作:applicationMessage、closeAllDatabases、connPoolSync、cpuProfiler、flushRouterConfig、fsync、invalidateUserCache、killAnyCursor (4.0新增)、killAnySession (3.6新增)、killop、logRotate、resync、setParameter、shutdown、touch、unlock。

对于集群中所有的数据库包含命令操作:killCursors、repairDatabase。

  • clusterAdmin

clusterAdmin角色包含MongoDB集群管理最高的操作权限。该角色包含clusterManagerclusterMonitorhostManager三个角色的所有权限,并且还拥有dropDatabase操作命令的权限。

备份和恢复角色

  • backup

backup角色包含备份MongoDB数据最小的权限。

对于MongoDB中所有的数据库资源包含命令操作:listDatabases、listCollections、listIndexes。

对于整个集群包含命令操作:appendOplogNote、getParameter、listDatabases。

对于以下数据库资源提供find操作权限:

  1. 对于集群中的所有非系统集合,包括自身的config数据库和local数据库;
  2. 对于集群中的系统集合:system.indexes、system.namespaces、system.js和system.profile;
  3. admin数据库中的集合:admin.system.users和admin.system.roles;
  4. config.settings集合;
  5. 2.6版本之前的system.users集合。

对于config.setting集合还有insert和update操作权限。

  • restore

restore角色包含从备份文件中还原恢复MongoDB数据(除了system.profile集合)的权限。

restore角色有以下注意事项:

  1. 如果备份中包含system.profile集合而恢复目标数据库没有system.profile集合,mongorestore会尝试重建该集合。因此执行用户需要有额外针对system.profile集合的createCollection和convertToCapped操作权限;
  2. 如果执行mongorestore命令时指定选项--oplogReplay,则restore角色包含的权限无法进行重放oplog。如果需要进行重放oplog,则需要只对执行mongorestore的用户授予包含对实例中任何资源具有任何权限的自定义角色。

对于整个集群包含命令操作:getParameter。

对于所有非系统集合包含命令操作:bypassDocumentValidation、changeCustomData、changePassword、collMod、convertToCapped、createCollection、createIndex、createRole、createUser、dropCollection、dropRole、dropUser、grantRole、insert、revokeRole、viewRole、viewUser。

关于restore角色包含其它的命令操作可以参考官方文档:https://docs.mongodb.com/manual/reference/built-in-roles/#restore

全数据库级角色

以下角色只存在于admin数据库,并且适用于除了config和local之外所有的数据库。

  • readAnyDatabase

readAnyDatabase角色包含对除了config和local之外所有数据库的只读权限。同时对于整个集群包含listDatabases命令操作。

在MongoDB3.4版本之前,该角色包含对config和local数据库的读取权限。当前版本如果需要对这两个数据库进行读取,则需要在admin数据库授予用户对这两个数据库的read角色。

  • readWriteAnyDatabase

readWriteAnyDatabase角色包含对除了config和local之外所有数据库的读写权限。同时对于整个集群包含listDatabases命令操作。

在MongoDB3.4版本之前,该角色包含对config和local数据库的读写权限。当前版本如果需要对这两个数据库进行读写,则需要在admin数据库授予用户对这两个数据库的readWrite角色。

  • userAdminAnyDatabase

userAdminAnyDatabase角色包含类似于userAdmin角色对于所有数据库的用户管理权限,除了config数据库和local数据库。

对于集群包含命令操作:authSchemaUpgrade、invalidateUserCache、listDatabases。

对于系统集合admin.system.users和admin.system.roles包含命令操作:collStats、dbHash、dbStats、find、killCursors、planCacheRead、createIndex、dropIndex。

该角色不会限制用户授予权限的操作,因此,拥有角色的用户也有可能授予超过角色范围内的权限给自己或其它用户,也可以使自己成为超级用户,userAdminAnyDatabase角色也可以认为是MongoDB中的超级用户角色。

  • dbAdminAnyDatabase

dbAdminAnyDatabase角色包含类似于dbAdmin角色对于所有数据库管理权限,除了config数据库和local数据库。同时对于整个集群包含listDatabases命令操作。

在MongoDB3.4版本之前,该角色包含对config和local数据库的管理权限。当前版本如果需要对这两个数据库进行管理,则需要在admin数据库授予用户对这两个数据库的dbAdmin角色。

超级用户角色

以下角色包含在任何数据库授予任何用户任何权限的权限。这意味着用户如果有以下角色之一可以为自己在任何数据库授予任何权限。

  • dbOwner角色(作用范围为admin数据库)
  • userAdmin角色(作用范围为admin数据库)
  • userAdminAnyDatabase角色

以下角色包含数据库所有资源的所有操作权限。

  • root

root角色包含角色readWriteAnyDatabase、dbAdminAnyDatabase、userAdminAnyDatabase、clusterAdmin、restore和backup联合之后所有的权限。

内部角色

  • **__system**

MongoDB将此角色授予代表集群成员的用户对象,如副本集(replica set)成员或mongos实例。该角色允许用户对于需要的数据库操作都具有相应的权限,不要将该角色授予应用程序用户或其它管理员用户。

总结

通过以上对内置角色的说明,总结一下较为常用的内置角色,如下表:

角色 权限描述
read 可以读取指定数据库中任何数据。
readWrite 可以读写指定数据库中任何数据,包括创建、重命名、删除集合。
readAnyDatabase 可以读取所有数据库中任何数据(除了数据库config和local之外)。
readWriteAnyDatabase 可以读写所有数据库中任何数据(除了数据库config和local之外)。
dbAdmin 可以读取指定数据库以及对数据库进行清理、修改、压缩、获取统计信息、执行检查等操作。
dbAdminAnyDatabase 可以读取任何数据库以及对数据库进行清理、修改、压缩、获取统计信息、执行检查等操作(除了数据库config和local之外)。
clusterAdmin 可以对整个集群或数据库系统进行管理操作。
userAdmin 可以在指定数据库创建和修改用户。
userAdminAnyDatabase 可以在指定数据库创建和修改用户(除了数据库config和local之外)。

用户自定义角色

虽然MongoDB提供了一系列内置角色,但有时内置角色所包含的权限并不满足所有需求,所以MongoDB也提供了创建自定义角色的方法。当创建一个自定义角色时需要进入指定数据库进行操作,因为MongoDB通过数据库和角色名称对角色进行唯一标识。

除了在admin数据库中创建的角色之外,在其它数据库中创建的自定义角色包含的权限只适用于角色所在的数据库,并且只能继承同数据库其它角色的权限。在admin数据库中创建的自定义角色则不受此限制。

MongoDB将所有的角色信息存储在admin数据库的system.roles集合中,不建议直接访问此集合内容,而是通过角色管理命令来查看和编辑自定义角色。

创建自定义角色

测试环境说明:

> show dbs;
admin   0.000GB
config  0.000GB
dbabd   0.001GB
local   0.000GB

> use dbabd;
switched to db dbabd

> show collections;
city
user_operation

> db.city.count()
600

> db.user_operation.count()
22068

在admin数据库中创建自定义用户dbabd,对集合city有find,update权限,对集合user_operation只有find权限。

> db.createRole(
    {
        role: "dbabd",
        privileges: [
            { resource: { db: "dbabd", collection: "city" }, actions: ["find", "update"] },
            { resource: { db: "dbabd", collection: "user_operation" }, actions: ["find"] },
        ],
        roles: []
    }
)

或

> db.adminCommand(
    {
        createRole: "dbabd",
        privileges: [
            { resource: { db: "dbabd", collection: "city" }, actions: ["find", "update"] },
            { resource: { db: "dbabd", collection: "user_operation" }, actions: ["find"] }
        ],
        roles: []
    }
)

查看自定义角色

> db.getRole("dbabd", { showPrivileges: true })

或

> db.getRoles({ rolesInfo: 1, showPrivileges: true })

或

> use admin
> db.runCommand(
    {
        rolesInfo: { role: "dbabd", db: "admin" },
        showPrivileges: true
    }
)

更新自定义角色

为自定义角色dbabd更新集合dbabd.user_operation的insert权限。

> db.updateRole(
    "dbabd",
    {
        privileges: [
            { resource: { db: "dbabd", collection: "city" }, actions: ["find", "update"] },
            { resource: { db: "dbabd", collection: "user_operation" }, actions: ["find", "insert"] }
        ],
        roles: []
    }
)

或

> db.adminCommand(
    {
        updateRole: "dbabd",
        privileges: [
            { resource: { db: "dbabd", collection: "city" }, actions: ["find", "update"] },
            { resource: { db: "dbabd", collection: "user_operation" }, actions: ["find", "insert"] }
        ],
        roles: []
    }
)

添加角色权限

为自定义角色dbabd添加集合dbabd.user_operation的remove权限。

db.grantPrivilegesToRole(
    "dbabd",
    [
        { resource: { db: "dbabd", collection: "user_operation" }, actions: ["remove"] }
    ]
)

或

> use admin
> db.runCommand(
    {
        grantPrivilegesToRole: "dbabd",
        privileges: [
            { resource: { db: "dbabd", collection: "user_operation" }, actions: ["remove"] }
        ]
    }
)

删除角色权限

为自定义角色dbabd收回集合dbabd.city的update权限。

> db.revokePrivilegesFromRole(
    "dbabd", 
    [
        { resource: { db: "dbabd", collection: "city" }, actions: ["update"] }
    ]
)

或

> use admin
> db.runCommand(
    { 
        revokePrivilegesFromRole: "dbabd", 
        privileges: [
            { resource: { db: "dbabd", collection: "city" }, actions: ["update"] }
        ] 
    }
)

添加角色继承的角色

为自定义角色dbabd添加dbabd数据库的read角色,继承其角色权限。

> use dbabd
> db.grantRolesToRole("dbabd", [{ role: "read", db: "dbabd" }])

或

> use dbabd
> db.runCommand({ grantRolesToRole: "dbabd", roles: [{ role: "read", db: "dbabd" }] })

// 查询角色信息验证
> db.getRole("dbabd")

{
    "role" : "dbabd",
    "db" : "admin",
    "isBuiltin" : false,
    "roles" : [
        {
            "role" : "read",
            "db" : "dbabd"
        }
    ],
    "inheritedRoles" : [
        {
            "role" : "read",
            "db" : "dbabd"
        }
    ]
}

删除角色继承的角色

为自定义角色dbabd收回dbabd数据库的read角色及其角色权限。

> use dbabd
> db.revokeRolesFromRole("dbabd", [{ role: "read", db: "dbabd" }])

或

> use dbabd
> db.runCommand({ revokeRolesFromRole: "dbabd", roles: [{ role: "read", db: "dbabd" }] })

//查询角色信息验证
> db.getRole("dbabd")

{
    "role" : "dbabd",
    "db" : "admin",
    "isBuiltin" : false,
    "roles" : [ ],
    "inheritedRoles" : [ ]
}

删除自定义角色

删除自定义角色dbabd。

> use admin
> db.dropRole("dbabd")

或

> use admin
> db.runCommand({ dropRole: "dbabd" })

用户

MongoDB是基于角色的访问控制,所以创建用户需要指定用户的角色,在创建用户之前需要满足:

  1. 先在admin数据库中创建角色为userAdmin或userAdminAnyDatabase的用户作为管理用户的用户;
  2. 启用访问控制,进行登录用户验证,这样创建用户才有意义。

创建用户管理的用户

启用访问控制登录之前,首先需要在admin数据库中创建角色为userAdmin或userAdminAnyDatabase作为用户管理的用户,之后才能通过这个用户创建其它角色的用户,这个用户作为其它所有用户的管理者。

// 创建管理用户用户名为user_admin,密码admin
db.createUser(
    {
        user: "user_admin",
        pwd: "admin",
        roles: [{ role: "userAdminAnyDatabase", db: "admin" }]
    }
)

Successfully added user: {
    "user" : "user_admin",
    "roles" : [
        {
            "role" : "userAdminAnyDatabase",
            "db" : "admin"
        }
    ]
}

开启访问控制

要开启访问控制,则需要在mongod进程启动时加上选项--auth或在启动配置文件加入选项auth=true,并重启mongodb实例。

## mongod配置文件如下
# cat mongodb.cnf 
journal=true
dbpath=/data/mongodb/4.0/data
directoryperdb=true
fork=true
port=27017
logpath=/data/mongodb/4.0/logs/mongodb.log
quiet=true
bind_ip_all=true
auth=true

## 启动mongodb实例
# mongod -f mongodb.cnf 
about to fork child process, waiting until server is ready for connections.
forked process: 44347
child process started successfully, parent exiting

使用mongo shell登录mongodb实例:

# mongo 192.168.58.2:27017
MongoDB shell version v4.0.9
connecting to: mongodb://192.168.58.2:27017/test?gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("428c215c-2927-49ee-8507-573efc4a1185") }
MongoDB server version: 4.0.9
>

// 如果没有开启访问控制,则在登录时会提示如下警告信息
** WARNING: Access control is not enabled for the database.
**          Read and write access to data and configuration is unrestricted.

用户管理用户验证

可以在使用mongo shell登录时添加选项--authenticationDatabase或登录完后在admin数据库下进行验证。

在mongo shell登录时同时进行验证:

# mongo 192.168.58.2:27017 -uuser_admin -p --authenticationDatabase admin
MongoDB shell version v4.0.9
Enter password:  # 输入密码admin

connecting to: mongodb://192.168.58.2:27017/test?authSource=admin&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("94663b8d-7d88-4c97-ad1c-c3c24262ad39") }
MongoDB server version: 4.0.9
> 

mongo shell登录完成之后进行验证:

# mongo 192.168.58.2:27017
MongoDB shell version v4.0.9
connecting to: mongodb://192.168.58.2:27017/test?gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("531e75df-3a5d-4f35-9e18-d7a6e090df63") }
MongoDB server version: 4.0.9

> use admin
switched to db admin

> db.auth('user_admin','admin')
1

用户管理操作

创建普通用户

使用user_admin用户在admin数据库中创建基于角色dbabd的用户dbabd_user,密码为dbabd。

> use admin
> db.createUser(
    {
        user: "dbabd_user",
        pwd: "dbabd",
        roles: ["dbabd"],
        customData: { info: "user for dbabd" }
    }
)
Successfully added user: {
    "user" : "dbabd_user",
    "roles" : [
        "dbabd"
    ],
    "customData" : {
        "info" : "user for dbabd"
    }


或

> db.getSiblingDB("admin").runCommand(
    {
        createUser: "dbabd_user",
        pwd: "dbabd",
        customData: { info: "user for dbabd" },
        roles: ["dbabd"]
    }
)
{ "ok" : 1 }

查看用户信息

> use admin
> db.getUser("dbabd_user", { showPrivileges: true })

或

> db.getSiblingDB("admin").runCommand(
    {
        usersInfo: "dbabd_user",
        showPrivileges: true

    }
)

为用户添加角色

用户dbabd_user添加admin数据库的readWrite角色。

> use admin
> db.grantRolesToUser(
    "dbabd_user",
    [
        { role: "readWrite", db: "admin" },
        { role: "dbabd", db: "admin" }
    ]
)

或

> use admin
> db.runCommand(
    {
        grantRolesToUser: "dbabd_user",
        roles:
            [
                { role: "readWrite", db: "admin" },
                { role: "dbabd", db: "admin" }
            ]
    }
)

更新用户信息

更新用户dbabd_user具有admin数据库readWrite角色为read角色。

> use admin
> db.updateUser(
    "dbabd_user",
    {
        customData: { info: "user for dbabd" },
        roles: [
            { role: "dbabd", db: "admin" },
            { role: "read", db: "admin" }
        ]
    }
)

或

> use admin
> db.runCommand(
    {
        updateUser: "dbabd_user",
        customData: { info: "user for dbabd" },
        roles: [
            { role: "dbabd", db: "admin" },
            { role: "read", db: "admin" }
        ]
    }
)

为用户回收角色

用户dbabd_user回收admin数据库的read角色。

> use admin
> db.revokeRolesFromUser(
    "dbabd_user",
    [
        { role: "read", db: "admin" }
    ]
)

或

> use admin
> db.runCommand(
    {
        revokeRolesFromUser: "dbabd_user",
        roles:
            [
                { role: "read", db: "admin" }
            ]
    }
)

更改用户密码

更改用户dbabd_user密码为dbabdnew。

> use admin
> db.changeUserPassword("dbabd_user", "dbabdnew")

删除用户

删除用户dbabd_user。

> use admin
> db.dropUser("dbabd_user")

或

> use admin
> db.runCommand({ dropUser: "dbabd_user" })

关于更多用户管理操作信息可以参考官方文档说明:https://docs.mongodb.com/manual/reference/method/js-user-management/

总结

一直以为MongoDB对于安全性能不是特别重视,在重新阅读了最新版本的官方文档之后有了很大的改观,对于基于角色的用户访问控制能够更为精细地控制访问用户的权限。在新的版本当中对于密码加密机制以及加密算法都做了很大的改进与提升,通过这次总结梳理可以给本文总结如下:

  1. MongoDB中角色和用户是建立在数据库中的;
  2. 在哪个数据库中创建用户就需要在哪个数据库中进行验证;
  3. 为了更好对用户权限进行控制,最好为每个用户创建一个自定义角色。

参考

https://docs.mongodb.com/manual/tutorial/enable-authentication/
https://docs.mongodb.com/manual/reference/command/nav-role-management/
https://docs.mongodb.com/manual/reference/command/nav-user-management/

☆〖本人水平有限,文中如有错误还请留言批评指正!〗☆

]]>
Flutter 你需要知道的那些事 01 http://doc.okbase.net/doclist/archive/265781.html doclist 2019-5-5 10:25:00

公众号「AndroidTraveler」首发。

1. width 属性

对于设置控件宽度填充父控件这件事情,在 Android 里面,只需要设置 MATCH_PARENT 即可。

但是在 Flutter 里面却不是这样,因为 Flutter 要具体的数值。

所以我们可以这样考虑,假设我这个值非常大,比所有市面上的设备宽度还要大,那么是不是表现出来就是充满父控件了。

所以这边的做法是设置为无限,即 double.infinite

我们以一个常用场景来说明。

比如设置图片填充屏幕宽度。

刚开始没有设置的代码如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text('My Flutter'),
          ),
          body: Center(
            child: Image.asset('assets/images/example.jpeg'),
          ),
        )
    );
  }
}

效果:

可以看到没有设置的情况下,显示会根据图片自身的宽高显示。

这个时候如果设置 width 为无穷大,修改代码如下:

child: Image.asset('assets/images/example.jpeg', width: double.infinity,),

效果

什么情况,没起作用?

这个时候不要慌,我们来给大家分析分析。

以后大家遇到类似问题也可以这样分析。

我们通过给 Image 外面套上一层 Container,然后设置背景颜色来对比一下。

代码如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      appBar: AppBar(
        title: Text('My Flutter'),
      ),
      body: Center(
        child: Container(
          color: Colors.blue,
          //left
//          child: Image.asset('assets/images/example.jpeg',),
          //right
          child: Image.asset('assets/images/example.jpeg', width: double.infinity,),
        ),
      ),
    ));
  }
}

效果如下:

可以看到,设置宽度之后,Image 确实是填充了宽度,只不过由于图片本身没有那么宽,因此看起来就以为是没有起作用。

那么如何让图片可以填充宽度呢?

这个就涉及到图片的填充模式了。

2. fit 属性

点击 Image 的 fit 属性进入源码可以看到如下:

/// How to inscribe the image into the space allocated during layout.
///
/// The default varies based on the other fields. See the discussion at
/// [paintImage].
final BoxFit fit;

我们再点一下 BoxFit,可以看到如下:

/// How a box should be inscribed into another box.
///
/// See also [applyBoxFit], which applies the sizing semantics of these values
/// (though not the alignment semantics).
enum BoxFit {
  /// Fill the target box by distorting the source's aspect ratio.
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/box_fit_fill.png)
  fill,

  /// As large as possible while still containing the source entirely within the
  /// target box.
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/box_fit_contain.png)
  contain,

  /// As small as possible while still covering the entire target box.
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/box_fit_cover.png)
  cover,

  /// Make sure the full width of the source is shown, regardless of
  /// whether this means the source overflows the target box vertically.
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/box_fit_fitWidth.png)
  fitWidth,

  /// Make sure the full height of the source is shown, regardless of
  /// whether this means the source overflows the target box horizontally.
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/box_fit_fitHeight.png)
  fitHeight,

  /// Align the source within the target box (by default, centering) and discard
  /// any portions of the source that lie outside the box.
  ///
  /// The source image is not resized.
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/box_fit_none.png)
  none,

  /// Align the source within the target box (by default, centering) and, if
  /// necessary, scale the source down to ensure that the source fits within the
  /// box.
  ///
  /// This is the same as `contain` if that would shrink the image, otherwise it
  /// is the same as `none`.
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/painting/box_fit_scaleDown.png)
  scaleDown,
}

相信大家看到源码的注释应该很清楚每个值的意义了。

如果你还不清楚,可以点击注释里面对应的链接去查看示意图。

比如以我们这个实际应用场景填充宽度为例,那么我们可以看到 fitWidth 应该是符合我们要求的,我们点击注释的链接,跳转可以看到图片如下:

很形象的做了几种情况的示意。我们设置了 Image 的 fit 属性如下:

child: Image.asset('assets/images/example.jpeg', width: double.infinity, fit: BoxFit.fitWidth,),

效果:

可以看到已经满足我们的需求了。

温馨提示:测试完之后不要忘记去掉测试的 Container 以及对应颜色哦~

3. print

我们知道在 Android 里面,当我们 try catch 之后,我们打印异常基本会写出类似下面代码:

Log.e(TAG, "exception="+e);

在 Flutter 也有异常捕获。

你可能会习惯的写出如下代码:

print('exception='+e);

但是切记,不要使用上面的写法。

因为当 e 为 null 时,上面的 print 不会执行打印。

这可能会误导你。因为你在成功的时候加上打印语句,异常捕获也加上打印语句。但是程序就是没有打印。你就会觉得很奇怪。

实际上当 e 为 null 时,print 语句会报错,+ 号连接的左右不能是 null,所以不会正常打印。因此请避免上面的写法。可以用下面的替换写法:

//替换写法一
print('exception=');
print(e);
//替换写法二
print('exception='+(e ?? ''));
//替换写法三
var printContent = e ?? '';
print('exception='+printContent);

4. GestureDetector

我们知道如果要给一个 Widget 增加点击事件,最简单的方法就是套一层 GestureDetector。

但是有时候你这样做了,却发现有些“隐患”,或者说,有些你意料不到的事情。

这里用一个场景来告诉你,你平时可能没有发现的细节。

微博里面有点赞这个小组件,我们写下如下代码:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      appBar: AppBar(
        title: Text('My Flutter'),
      ),
      body: Row(
        children: <Widget>[
          Image.asset('assets/images/2.0x/like.png', width: 20, height: 20,),
          SizedBox(width: 5,),
          Text('30')
        ],
      ),
    ));
  }
}

效果如下:

假设我们要求给这个点赞组件加上点击事件,那么我们直接给 Row 套上 GestureDetector Widget。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      appBar: AppBar(
        title: Text('My Flutter'),
      ),
      body: GestureDetector(
        onTap: (){
          print('onTap');
        },
        child: Row(
          children: <Widget>[
            Image.asset('assets/images/2.0x/like.png', width: 20, height: 20,),
            SizedBox(width: 5,),
            Text('30')
          ],
        ),
      ),
    ));
  }
}

点击点赞组件确实会打印 onTap,但是如果你点击了点赞图标和数字中间的白色区域,你会发现点击事件没有回调,没有打印。

这个时候有两种解决方法:

1. 给空白组件设置 color 属性,颜色值设置透明

对于 Container 设置的 padding 可以直接设置,对于我们这里例子的 SizeBox 需要改为如下:

SizedBox(width: 15, child: Container(color: Colors.transparent,),),

为了方便测试,这边将宽度改为 15。

所以对于设置 GestureDetector 的 Container,如果没有设置 color 属性,那么点击空白不会回调。

2. 设置 GestureDetector 的 behavior 属性(推荐方式)

其实如果你需要空白区域也响应点击,只需要设置一下 GestureDetector 的 behavior 属性即可。

behavior 默认值为 HitTestBehavior.deferToChild,我们这里将其设置为 HitTestBehavior.translucent

代码如下:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      appBar: AppBar(
        title: Text('My Flutter'),
      ),
      body: GestureDetector(
        behavior: HitTestBehavior.translucent,
        onTap: (){
          print('onTap');
        },
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Image.asset('assets/images/2.0x/like.png', width: 20, height: 20,),
            SizedBox(width: 15),
            Text('30')
          ],
        ),
      ),
    ));
  }
}

这里的点赞图片我直接从网上获取的,你测试可以用随便一张图片代替验证。或者用两个文本来验证也是可以的。

]]>
快速排序及其优化 http://doc.okbase.net/doclist/archive/265780.html doclist 2019-5-5 10:05:00

一、引言

顾名思义,快速排序是实践中的一种快速排序算法,在C++或对Java基础类型的排序中特别有用。它的平均运行时间是O(NlogN);但最坏情形性能为O(N2)。我会先介绍快速排序过程,再讨论如何优化。

二、快速排序(quicksort)

  • #### 算法思想

采用分治法,将数组分为两部分,并递归调用。将数组S排序的快排过程

  1. 如果S中元素个数是0或1,则直接返回;
  2. 取S中任一元素v,称之为枢纽元(pivot);【枢纽元的选取策略很重要,下面会详述
  3. 将S-{v}(S中除了枢纽元中的其余元素)划分为两个不相交的集合S1和S2,S1集合中的所有元素小于等于枢纽元v,S2中的所有元素大于等于枢纽元;
  4. 返回quicksort(S1),枢纽元v,quicksort(S1</sub2)。
  • #### 枢纽元的选取策略
  1. 取第一个或者最后一个:简单但很傻的选择(啊,9龙,上面这图???)。当输入序列是升序或者降序时,这时候就会导致S1集合为空,除枢纽元外所有元素在S2集合,这种做法,最坏时间复杂度为O(N2)。
  2. 随机选择:这是比较安全的做法。除非随机数发生器出现错误,并且连续产生劣质分割的概率比较低。但随机数生成开销较大,这样就增加了运行时间。
  3. 三数中值分割法:一组序列的中值(中位数)是枢纽元最好的选择(因为可以将序列均分为两个子序列,归并排序告诉我们,这时候是O(NlogN);但要计算一组数组的中位数就比较耗时,会减慢快排的效率。但可以通过计算数组的第一个,中间位置,最后一个元素的中值来代替。比如序列:[8,1,4,9,6,3,5,2,7,0]。第一个元素是8,中间(left+right)/2(向下取整)元素为6,最后一个元素为0。所以中位数是6,即枢纽元是6。显然使用三数分割法消除了预排序输入的坏情形,并且实际减少了14%的比较。
  • #### 快排过程
  1. 将枢纽元与数组最后一个元素调换,使枢纽元离开要被分割的数据段;

  2. 初始化两个索引left和right,分别指向数组第一个与倒数第二个元素;

  3. 如果left索引指向的元素小于枢纽元,则left++;否则,left停止。right索引指向的元素大于枢纽元,right--;否则,right停止。

  4. 如果left<right,则交换两个元素,循环继续3,4步骤;否则跳出循环,将left对应的元素与枢纽元交换(这时候完成了分割)。递归调用这两个子序列。

    假设所有元素互异(即都不相等)。下面会说重复元素怎么处理。

    接下来要做的就是将小于枢纽元的元素移到数组左边,大于枢纽元的元素移到数组右边。

    当left在right的左边时,我们将left右移,移过那些小于枢纽元的元素,并将right左移,移过那些大于枢纽元的元素。当left和right停止时,left指向一个大于枢纽元的元素,right指向一个小于枢纽元的元素,如果left<right,则将这两个元素交换。这样是将一个大于枢纽元的元素推向右边而把小于枢纽元的元素推向左边。我们来图示过程:left不动,而right左移一个位置,如下图:

    我们交换left与right指向的元素,重复这个过程,直到left>right。

    至此,我们可以看到,left左边的元素都小于枢纽元,右边的元素都大于枢纽元。我们继续递归左右序列,最终可完成排序。

    上面我们假设的是元素互异,下面我们讨论重复元素的处理情况。

  • 重复元素的处理:简单说是遇到与枢纽元相等的元素时,左右索引需要停止吗?
  1. 如果只有其中一个停止:这包含两种,如果只停止左、或者右索引,这将导致等于枢纽元的元素都移动到一个集合中。考虑序列所有元素都是重复元素,会是最坏情形O(N2)。
  2. 如果都不停止:这需要防范左右索引越界,并且不用交换元素。但正如上面图示的正确过程是,枢纽元需要与left索引指向的元素进行交换。还是考虑所有元素相同的情况,这会导致序列全分到左边,这样还是最坏情形O(N2)
  3. 都停止:还是考虑元素全都相等的情况,这样看似会进行很多次“无意义”的交换;但正面的效果却是,left与right交错是发生在中间位置,这时刚好将序列均分为两个子序列,还是归并排序的原理,这是O(NlogN)。我们分析指出,只有这种情况可以避免二次方。

在大规模输入量中,重复元素还是挺多的。考虑能将这些重复元素进行有效排序,还是很重要。

快速排序真的快吗?其实也不一定,对于小数组(N<=20)的输入序列,快速排序不如插入排序并且在我们上面的优化中,采用三数中值分割时,递归得到的结果可以是只有一个,或者两个元素,这时会有错误。所以,继续优化是将小的序列用插入排序代替,这会减少大约15%的运行时间。较好的截止范围是10(其实5-20产生的效果差不多)。

对于三数中值分割还可以进行优化:假设输入序列为a,则选择a[left],a[center],a[right],选择出枢纽值,并将最小,与最大值分别放到a[left],a[right],将枢纽值放到a[right-1]处,这样放置也是正确的位置,并且可以防止right向右进行比较时不会越界;这样左右起始位置就是left+1,right-2。

三、优化汇总的java实现快速排序:

public class Quicksort {
    /**
     * 截止范围
     */

    private static final int CUTOFF = 10;

    public static void main(String[] args) {
        Integer[] a = {8149635270};
        System.out.println("快速排序前:" + Arrays.toString(a));
        quicksort(a);
        System.out.println("快速排序后:" + Arrays.toString(a));
    }

    public static <T extends Comparable<? super T>> void quicksort(T[] a) {
        quicksort(a, 0, a.length - 1);
    }

    private static <T extends Comparable<? super T>> void quicksort(T[] a, int left, int right) {
        if (left + CUTOFF <= right) {
            //三数中值分割法获取枢纽元
            T pivot = median3(a, left, right);

            // 开始分割序列
            int i = left, j = right - 1;
            for (; ; ) {
                while (a[++i].compareTo(pivot) < 0) {
                }
                while (a[--j].compareTo(pivot) > 0) {
                }
                if (i < j) {
                    swapReferences(a, i, j);
                } else {
                    break;
                }
            }
            //将枢纽元与位置i的元素交换位置
            swapReferences(a, i, right - 1);
            //排序小于枢纽元的序列
            quicksort(a, left, i - 1);
            //排序大于枢纽元的序列
            quicksort(a, i + 1, right);
        } else {
            //插入排序
            insertionSort(a, left, right);
        }
    }

    private static <T extends Comparable<? super T>> median3(T[] a, int left, int right) {
        int center = (left + right) / 2;
        if (a[center].compareTo(a[left]) < 0) {

            swapReferences(a, left, center);
        }
        if (a[right].compareTo(a[left]) < 0) {
            swapReferences(a, left, right);
        }
        if (a[right].compareTo(a[center]) < 0) {
            swapReferences(a, center, right);
        }
        // 将枢纽元放置到right-1位置
        swapReferences(a, center, right - 1);
        return a[right - 1];
    }

    public static <T> void swapReferences(T[] a, int index1, int index2) {
        T tmp = a[index1];
        a[index1] = a[index2];
        a[index2] = tmp;
    }

    private static <T extends Comparable<? super T>> void insertionSort(T[] a, int left, int right) {
        for (int p = left + 1; p <= right; p++) {
            T tmp = a[p];
            int j;

            for (j = p; j > left && tmp.compareTo(a[j - 1]) < 0; j--) {
                a[j] = a[j - 1];
            }

            a[j] = tmp;
        }
    }

}
//输出结果
//快速排序前:[8, 1, 4, 9, 6, 3, 5, 2, 7, 0]
//快速排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

四、快速排序分析

  1. 最坏时间复杂度:即元素都分到一个子序列,另一个子序列为空的情况,时间复杂度为O(N2)。
  2. 最好时间复杂度:即序列是均分为两个子序列,时间复杂度是O(NlogN),分析与归并排序差不多。
  3. 平均时间复杂度:O(NlogN)
  4. 空间复杂度:O(logN)

五、总结

本篇从如何较好选择枢纽元,分析重复元素的处理及递归分成小数组时更换为插入排序三个方面进行快速排序的优化,系统全面详述了快速排序原理、过程及其优化。快速排序以平均时间O(NlogN)进行,是java中基础类型使用的排序算法。可以去看一下Arrays.sort方法。到这里,我就要回过头去完善求解topK问题了,可以利用快速排序的思想,达到平均O(N)求解topK。

觉得可以的小伙伴们点个推荐或小赞支持啊。

]]>
沉默,并不代表我们无话可说 http://doc.okbase.net/doclist/archive/265779.html doclist 2019-5-5 10:05:00

 

01、

说到让我受益匪浅的一本书,如果不选择技术书的话,我会毫不犹豫地选择《沉默的大多数》——如果你留意观察的话,我的笔名“沉默王二”就佐证了这本书对我的影响深远。

有人说,我们都是沉默的大多数,或在内心挣扎,或在脸上写满愤怒,然而结局终究是平庸和顺从

平庸和顺从这两个词乍看起来,多少显得突兀,令人难以难受,就好像不小心咬到舌头一样,很疼,但又不能生气地把牙齿打掉。平庸和顺从这两个词体现出来了大多数人的无奈:接受现实,做好自己,或者改变自己。总不能做一个破坏大环境的人吧。

王小波曾说:

我对自己的要求很低:我活在世上,无非想要明白一些道理,遇见一些有趣的事。倘若如我所愿,我的一生就算成功。为此也要去论是非,否则道理不给你明白,有趣的事也不让你遇到。我开始得太晚了,很可能做不成什么,但我总得申明我的态度,所以就有了这本书——为我自己,也代表沉默的大多数。

我对自己的要求也谈不上高雅:活在这个世界上,明白一些道理,遇见一些有趣的人或事,如果可能的话,再挣一些钱、发一些声。假如能够如我所愿,我就很心满意足。

当然了,沉默,并不代表我们无话可说,只是代表了我们的一种处事方式,不管这种方式和勇敢、自由沾不沾边。这种处事方式就是:在某些公共场合什么都不说,但到了私底下却妙语连珠,也就是说,我们对信得过的人什么都说,对信不过的人什么都不说。

02、

最近,我尝试在 V2EX 上发表一些话题,以此来吐露我的心声。这些主题的评论里富有一些建设性的提议,和一些美好的祝福,但也充斥着大量的讥讽(戾气很重)。

我是个纯洁的人,对于提议和祝福,我会毫不保留地收下,对于那些负面的评论,我会伤心,但也不会大动肝火。

因为我知道,与沉默的大多数相反,任何年代都会有喜欢喋喋不休的人。况且,假如没有这些负面评论的话,话题的热度也就会很快减退,那些真正善意的评论可能就不会出现,因为他们可能没有机会看到这些话题——我这种想法很坏,但也只是事后诸葛亮的一种自我安慰。

我相信你也看过不少篇文章,这些文章总有惊人的相似性:评论多的阅读量无一例外都很高,评论少的阅读量就惨不忍睹。换句话说,人们不喜欢看那些没人评论的文章。

作者当然希望他的文章有更多的人阅读、更多的人评论。但我就看不惯那些耍小聪明的、喜欢炒作的作者,他们为了文章的阅读量,故意挑起某些争端,引起更多的人进行评论,从而达到他们不纯洁的目的。

对于那些动机不够纯洁的文章来说,我奉劝各位读者,做一个沉默的大多数,尽量不要评论,免得中了那些心怀不轨的作者的圈套。

03、

1997 年,王小波就永远地离开这个美好的世界。我那时候还年幼,也没有阅读他作品的机会。

2017 年,我从《一个人的书房》的讲书栏目里听到了他的作品——《沉默的多数》。哇,我那时候才知道,原来文字可以这样写,写得这么幽默风趣。

大多数的文学作品,还有编程类书籍,读起来就好像是在喝白开水。虽然说水是生命之源,生活的必需品,但如果少了酸甜苦辣咸,就不免令人厌倦。

王小波的书有着很大的不同,读起来能够让我笑,在笑的时候去思考,感受思维的乐趣。假如我的生命中没有遇到王小波,那就好像人没有遇到爱情或者知音一样。

父亲后来也读了这本书,说里面的文字太生硬,太拗口,不够优美,并且埋怨王小波的着眼点不够宽泛,没有通过思想内涵唤起整个社会思考的宏观气质。

我想来想去,也没搞清楚父亲所说的宏观气质是什么。但我总觉得,人没有必要活得很崇高,总要操心着整个社会,把自己建设好也是一件极其重要的事。

04、

后来我知道,王小波不仅是个作家,也是一个程序员,并且编程能力比我厉害得多——好吧,终于找到了我们之间的相似性,但似乎也找到了我们之间巨大的差距。

当然了,不仅王小波是个程序员,更有趣的是他的外甥也是个程序员,参与开发过“QQ炫舞”,后来还加入了一支乐队,就是为人熟知的水木年华,他叫姚勇。

现在的生活比王小波生活的年代好多了,但不得不承认的是,大家活得依然疲惫,许多人,尤其是程序员,因此也就不怎么想说话,埋头挣钱最要紧,大多数还是在沉默着

这让我想起来《沉默的大多数》这本书里的一句话:

傍晚你坐在屋檐下,看着天慢慢地黑下去,寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看來,这是比死亡更可怕的事。

05、

很多年前,在我沉迷 QQ 网恋的岁月里,曾有一个性别为女的加我好友,我们聊得还算是情投意合。我不记得我们曾聊过什么了,但她的昵称却深深地烙印在了我的脑海里。这个昵称很特别,叫做“特立独行的猪”。

那时候,我想不通为什么一个女生会叫“特立独行的猪”,我也不好意思问她原因。直到我读了《沉默的大多数》,里面就有这样一篇杂文,题目叫做“一只特立独行的猪”。这篇杂文里有一句意味深长的话:

“除了这只猪,我还没见过谁敢于如此无视对生活的设置。相反,我倒见过很多想要设置别人生活的人,还有对被设置的生活安之若素的人。因为这个缘故,我一直怀念这只特立独行的猪。”

在现实生活中,总有一些人喜欢设置别人的生活,比如马某人那句伟大的“996 是前世修来的福分。”、刘某人那句嚣张的“不能拼的都不是好兄弟。”

套用王小波的话叫做:人是经不起恭维的,越是天真、质朴的人(马某人自称为乡村教师,刘某人出身于农民),听到一种于己有利的说法,证明自己身上有种种优越的素质,是人类中最优秀的部分,就越会不知东西南北,撒起癔症来。

我想马某人和刘某人多半是得了癔症。

当然了,还有更多更多的人,愿意全身心地投入到这份伟大的福分当中,并且毫不怀疑地认为这是最好的成长机会——这令我感到更为可怕。

明天就是五四青年节了,我希望所有的青年人能够过自己想过的生活,而不是过别人要我们过的(艰苦)生活。人生在世会有种种不如意,但我希望所有人可以在幸福与不幸中选择前者。

06、

最后,我劝你读一读这本书——《沉默的大多数》。在这本书里,王小波用他轻松有趣的文字,不经意间说出了很多道理,更可贵的是,这本书可以启发读者自觉地去思考。



]]>
维诺图之平面扫描法 http://doc.okbase.net/doclist/archive/265778.html doclist 2019-5-5 10:05:00

维诺图(Voronoi Diagram),简单来说,是一种平面区域的划分方式。假设平面上有 n 个点:P1 ~ Pn,那么对应维诺图则划分成 n 个区域:S1 ~ Sn,并且 Si 内所有点到 Pi 的距离小于等于到其他任意点的距离。维诺图还经常和德洛内三角(Delaunay 三角网)扯上关系,德洛内三角是一系列相连不重叠的三角形集合,特点有两个:1、任意三角形的外接圆不包含面内其他三角形顶点,2、相邻两个三角形构成的凸四边形,交换对角线,六个内角的最小角不会增大。如下图,实线构成德洛内三角,虚线构成维诺图,德洛内三角每一个三角形的顶点便是维诺图的初始点集。

理论上,两种图形可以互相转化。1、生成维诺图后,连接有公共边的初始点集,便可构成德洛内三角。2、生成德洛内三角后,针对每一个三角形边,生成垂直平分线,并将垂直平分线的端点设置为三角形边所在外接圆的圆心即可(内部三角形边的垂直平分线为线段,边界三角形边的垂直平分线为射线)。

下面先推荐几篇我看过写得比较好的学习资料,可以用于参考。(吐槽:这几篇是我从海量博客中筛选出来的,网上能搜出一堆相关博客,可真正有内容的却没有几篇)

1、http://www.cnblogs.com/zhiyishou/p/4430017.html 讲解如何生成Delaunay三角网的博客。这篇文章是讲解地比较细,比较全,比较容易理解的,不过同样有很多坑,阅读时不要遗漏了博客评论区,那里指出了很多坑点。最坑的一点,即使你排除万难,写出了和作者一模一样的代码,仍然有很多BUG,比如点数少的情况下有可能没有任何生成,比如最终生成的德洛内三角不是凸包等。当然,文章确实好,值得一看,用来理解德洛内三角是很有帮助的。

2、https://en.wikipedia.org/wiki/Fortune%27s_algorithm 讲解如何生成维诺图的维基百科,有个gif 图可以帮助理解,中间那段英文的算法描述写得很好,读下来大致就有了生成维诺图的思路(英文不好的可以百度翻译一下)。下面还附了个伪代码,不过这伪代码我是完全看不懂了,百度翻译也不管用了。文章末尾还附加了几个算法源码,不过不推荐阅读,代码可读性太差了,反正我是啃不下来,后面会推荐一个写得比较清楚的源码。

3、https://www.cnblogs.com/Seiyagoo/p/3339886.html 讲解如何生成维诺图的博客,内容比较少,不过比较清晰,附加的伪代码也比较容易理解,建议有了大致思路后根据这篇博客来完善代码。

4、https://www.cs.hmc.edu/~mbrubeck/voronoi.html 提供源码的博客,其他很多博客都只是简单介绍方法,而像codeproject 或Wikipedia 上的源码可读性太差(不是我吐槽,是真的太差,完全看不懂),而这篇博客提供的代码很适合用来学习,定义清晰明了,虽然代码效率达不到logn,但这仅仅是存放海岸线的数据结构差异,其余内容与平面扫描法一致,可以参照该源码去解读推荐的第三篇博客。不过该源码也有些BUG,有一些特殊情况会返回不正确的维诺图。

下面就介绍通过平面扫描法来生成维诺图,首先介绍平面扫描法的几个基础定义:

1、扫描线:扫描线将从 y = 0 一直扫描到 y = maxY,扫描完成后,维诺图也将生成完毕。(上图的黑色线)

2、海岸线:由多段抛物线组成,抛物线的焦点是初始点集,准线为扫描线。(上图的蓝色线)

3、站点事件:扫描线扫描到了某个初始点。

4、圆事件:扫描线扫描到了某个圆(三个站点共圆)的最低点。

算法思路:

我们需要维护一条扫描线和一条海岸线,这两条线都随着程序的运行,通过整个平面。扫描线是一条直线,我们可以假定它是水平的,在平面上从上到下地移动。在算法运行期间,扫描线上方的初始点已经被纳入Voronoi图,而扫描线下方的点暂未考虑。海滩线不是一条直线,而是一条复杂的多段曲线,位于扫描线的上方,它将已经确定的区域和未确定的区域分割开,即不管后续还有多少个点,海岸线上方的维诺图都已经是确定的了。对于扫描线上方的初始点,我们可以定义距离该点和扫描线等距的点的曲线(即以该点为焦点,以扫描线为准线的抛物线),海岸线就是这些抛物线并集的边界。随着扫描线的推进,海岸线中相邻抛物线交点(相交的点)将勾勒出维诺图的边。海岸线也随着扫描线的推进而推进,始终保持其上的点到焦点的距离和到扫描线的距离相等。

该算法使用了排序二叉树来维护海岸线,使用优先队列来维护可能引起海岸线变化的事件。这些事件包括新增抛物线到海岸线(扫描过一个初始点,称之为站点事件)和从海岸线中删除某一条抛物线(这条抛物线缩小成一个点时,即海岸线中相邻的三个抛物线焦点生成的圆与扫描线相切,称之为圆事件)。每一个事件可以用发生该事件时的扫描线 y 坐标来确定优先级。然后,我们要做的就是反复从优先队列中取出事件,进行处理,可能会影响到海岸线结构,可能会新增圆事件。

所以,该算法的重点便是如何处理站点事件和圆事件。首先看站点事件,当扫描线遇到 P4 时,过 P4 做扫描线的垂线,垂线和海岸线相交点到 P4 和 P2 距离相等,当扫描线越过 P4 时,将生成一条以 P4 为焦点,扫描线为准线的抛物线,该抛物线和 P2 对应的海岸线相交于两点,这两点会随着扫描线的移动而分离,事实上,这两点将勾勒出同一条维诺图边(可以确定该边上点到 P4 和 P2 距离相等)。P4 抛物线与 P2 抛物线交于两点,会将 P2 抛物线分割成两段抛物线,命名为S1、S2,假设 P4 下方没有新的站点,随着扫描线继续移动,P4 对应抛物线将越变越宽,可能会将原先 S1、S2 挤兑没,即 S1 可能由一段抛物线缩小成一个点,而 P1 抛物线和 S1 的交点,与 P4 抛物线和 S1 的交点重合,这便产生了一个圆事件,即缩小成的那一点到 P1、P2、P4 距离相等。当然程序不可能做到一点一点的移动扫描线,所以生成圆事件,是在遇到 P4 的一瞬间决定的,即遇到 P4 时,判断 P1、P2、P4 是否共圆。接着我们来看圆事件,当发生了圆事件后,就说明有某一段抛物线缩小成一个点,所以我们就需要将该段抛物线删除,并生成已经确定下来的维诺图边。 

维诺图的目标是找出所有站点对应区域的边,而边是随着扫描线移动,由相邻抛物线交点勾勒出来的,所以我们把边记在弧上,一条弧左右各有一个交点,所以我们每条弧记两条边S0、S1。当发生站点事件时,先找到其正上方的弧,然后这条弧中间部分将被新的抛物线取代,即由一段弧变成两个交点Inter1、Inter2 和 三段弧Arc1、Arc2、Arc3,其中Arc2 是新增的弧,Arc1、Arc3 是原弧分裂出来的,所以Arc1 的S0 继承自原弧的S0,Arc3 的S1 继承自原弧的S1,Arc1 的S1 与 Arc2 的S0 将指向根据左交点创建的一条新边,Arc2 的S1 和Arc3 的S0 将指向根据右交点创建的一条新边。当发生圆事件时,弧Arc 消失,所以 Arc 的S0、S1 将完成构造,即S0、S1 的终点设置在圆事件的圆心,而 Arc 消失后,其前置弧和后置弧相交在一起,所以前置弧的S1 和后置弧的S0 将指向根据圆心创建的一条新边。当所有事件处理完毕后,我们需要假设一条扫描线,使剩余的所有相邻弧交点位于维诺图边界外,计算此时这些交点的位置,完成剩余的维诺图边。

数据结构:

1、我们需要按 y 顺序遍历站点和圆事件,所以引入优先队列这个数据结构(Y 越小越靠前,Y 相同则 X 越小越靠前)。

2、我们需要快速获取某点正上方的抛物线,所以引入排序二叉树,排序二叉树每一个叶子结点代表一段抛物线,每一个内部结点代表相邻抛物线的交点,排序依据就是交点的 x 坐标,从而可以用logn 的时间,快速获取某点正上方的抛物线。(排序二叉树可能会退化成链表,真正使用的时候可以考虑是否可以替换成平衡二叉树)

3、我们需要快速检查相邻的三段抛物线对应焦点是否共圆,所以引入双向链表,用于管理排序二叉树的叶子结点,即每个叶子结点记录上一片叶子和下一片叶子指针。

源码链接:伪代码可以看上面的第三篇博客,或者直接阅读下面的源代码,注释应该是比较全了。该源码仅用于交流学习,内部有挺多细节没有考虑最优的算法。

https://github.com/hchlqlz/VoronoiDiagram

欢迎大家指出源码 BUG。

]]>
Docker for Java Developers http://doc.okbase.net/doclist/archive/265777.html doclist 2019-5-5 9:46:00

1.  基本概念

1.1.  主要组件

Docker有三个主要组件:

  • 镜像是Docker的构建组件,而且是定义应用程序操作系统的只读模板
  • 容器是Docker的运行组件,它是从镜像创建的。容器可以运行、启动、停止、移动和删除
  • 镜像在注册中心中存储、共享和管理,并且是Docker的分发组件。Docker Store 是一个公开可用的注册中心。https://hub.docker.com/

为了上这三个组件协同工作,Docker守护进程(或者叫Docker容器)运行在一个主机上,并负责构建、运行和分发Docker容器。此外,客户端是一个Docker二进制文件,它接受来自用户的命令并与引擎来回通信。

1.2.  Docker Image

Docker镜像是一个可以从其中启动Docker容器的只读模板。每个镜像又一系列的层组成。(PS:现在发现,把“Image”翻译成专业术语“镜像”的话这里就感觉跟别扭。原文是“Each image consists of a series of layers”,如果按“Image”本来的意思“图像”去理解就很好理解了,对PhotoShop有点儿了解的人都能理解这句话,“图像由一系列图层组成”,真是太形象了。)

Docker如此轻量级的原因之一就是这些层(图层)。当你修改镜像(例如,将应用程序更新到新版本)时,将构建一个新的层。因此,只添加或更新该层,而不是像使用虚拟机那样替换整个映像或完全重建。现在,您不需要发布整个新图像,只需要更新即可,从而使分发Docker镜像更快、更简单。(PS:越发觉得此处用“图像”更好理解,加个新图层或者在原先的图层上做修改即可)

每个镜像都是从一个基本镜像开始的。你也可以使用自己的镜像作为新镜像的基础。如果你有一个基本的Apache镜像,那么你可以使用它作为所有web应用程序镜像的基础。

Docker使用一组称为指令的简单描述性步骤来构建镜像。每条指令在镜像中创建一个新层。

  1. 运行一条命令
  2. 添加一个文件或目录
  3. 创建一个环境变量
  4. 当启动一个容器时运行一个进程

这些指令被存储在一个叫“Dockerfile”的文件中。当你请求构建镜像时,Docker读取这个Dockerfile文件,然后执行这些指令,并返回最终的镜像。

(PS:关于镜像,记住下面两句话

  • Each image consists of a series of layers.
  • Each instruction creates a new layer in our image. 

1.3.  Docker Container

容器由操作系统、用户添加的文件和元数据组成。正如我们所看到的,每个容器都是由一个镜像构建的。镜像告诉Docker容器持有什么、启动容器时运行什么进程以及各种其他配置数据。镜像是只读的。当Docker从映像运行容器时,它会在镜像之上添加一个读写层,然后你的应用程序就可以在其中运行了。

1.4.  Docker Engine

Docker Host是在安装Docker的时候创建的。一旦Docker Host被创建了,那么你就可以管理镜像和容器了。例如,你可以下载镜像、启动或停止容器。

1.5.  Docker Client

Docker Client与Docker Host通信,进而你就可以操作镜像和容器了。

 

2.  构建一个镜像

2.1.  Dockerfile

Docker通过从Dockerfile文件中读取指令来构建镜像。Dockerfile是一个文本文档,它包含用户可以在命令行上调用的所有命令来组装一个镜像。docker image build命令会使用这个文件,并执行其中的所有命令。

build命令还传递一个在创建映像期间使用的上下文。这个上下文可以是本地文件系统上的路径,也可以是Git存储库的URL。

关于Dockerfile中可以使用的命令,详见 https://docs.docker.com/engine/reference/builder/

下面是一些常用的命令:

2.2.  创建你的第一个镜像

首先,创建一个目录hellodocker,然后在此目录下创建一个名为Dockerfile的文本文件,编辑该文件,内容如下:

从以上两行命令我们可以看到,该镜像是以ubuntu作为基础基础,CMD命令定义了需要运行的命令。它提供了一个不同的入口/bin/echo,并给出了一个参数“hello world”。

2.3.  用Java创建你的第一个镜像

补充:OpenJDK是Java平台标准版的一个开源实现,是Docker官方提供的镜像

首先,让我们创建一个java工程,然后打个jar包,接着创建并编辑Dockerfile

使用docker image build构建镜像

使用docker container run启动容器

其实,跟我们平常那一套没多大区别,不过是把打好的jar包做成镜像而已

2.4.  使用Docker Maven Plugin构建镜像

利用Docker Maven Plugin插件我们可以使用Maven来管理Docker镜像和容器。下面是一些预定义的目标:

详见 https://github.com/fabric8io/docker-maven-plugin

补充:Maven中的生命周期、阶段、目标

  1. 生命周期有三套:clean、default、site
  2. 生命周期由多个阶段组成的,比如default生命周期的阶段包括:clean、validate、compile、
  3. 每个阶段由多个目标组成,也就是说目标才是定义具体行为的
  4. 插件是目标的具体实现

稍微留一下IDEA里面的Maven区域就不难理解了

言归正传,利用docker-maven-plugin来构建镜像的方式有很多,比如,可以配置插件或属性文件,还可以结合Dockerfile,都在这里:

https://github.com/fabric8io/docker-maven-plugin/tree/master/samples

此处,我们演示用属性文件的方式,首先,定义一段profile配置,比如这样:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4     <modelVersion>4.0.0</modelVersion>
 5     <parent>
 6         <groupId>org.springframework.boot</groupId>
 7         <artifactId>spring-boot-starter-parent</artifactId>
 8         <version>2.1.4.RELEASE</version>
 9         <relativePath/> <!-- lookup parent from repository -->
10     </parent>
11     <groupId>com.cjs.example</groupId>
12     <artifactId>hello-demo</artifactId>
13     <version>0.0.1-SNAPSHOT</version>
14     <name>hello-demo</name>
15     <description>Demo project for Spring Boot</description>
16 
17     <properties>
18         <java.version>1.8</java.version>
19     </properties>
20 
21     <dependencies>
22         <dependency>
23             <groupId>org.springframework.boot</groupId>
24             <artifactId>spring-boot-starter-web</artifactId>
25         </dependency>
26 
27         <dependency>
28             <groupId>org.springframework.boot</groupId>
29             <artifactId>spring-boot-starter-test</artifactId>
30             <scope>test</scope>
31         </dependency>
32     </dependencies>
33 
34     <build>
35         <plugins>
36             <plugin>
37                 <groupId>org.springframework.boot</groupId>
38                 <artifactId>spring-boot-maven-plugin</artifactId>
39             </plugin>
40         </plugins>
41     </build>
42 
43     <profiles>
44         <profile>
45             <id>docker</id>
46             <build>
47                 <plugins>
48                     <plugin>
49                         <groupId>io.fabric8</groupId>
50                         <artifactId>docker-maven-plugin</artifactId>
51                         <version>0.30.0</version>
52                         <configuration>
53                             <images>
54                                 <image>
55                                     <name>hellodemo</name>
56                                     <build>
57                                         <from>openjdk:latest</from>
58                                         <assembly>
59                                             <descriptorRef>artifact</descriptorRef>
60                                         </assembly>
61                                         <cmd>java -jar maven/${project.name}-${project.version}.jar</cmd>
62                                     </build>
63                                 </image>
64                             </images>
65                         </configuration>
66                         <executions>
67                             <execution>
68                                 <id>docker:build</id>
69                                 <phase>package</phase>
70                                 <goals>
71                                     <goal>build</goal>
72                                 </goals>
73                             </execution>
74                             <execution>
75                                 <id>docker:start</id>
76                                 <phase>install</phase>
77                                 <goals>
78                                     <goal>run</goal>
79                                     <goal>logs</goal>
80                                 </goals>
81                             </execution>
82                         </executions>
83                     </plugin>
84                 </plugins>
85             </build>
86         </profile>
87     </profiles>
88 </project> 

 然后,在构建的时候指定使用docker这个profile即可

1 mvn clean package -Pdocker

 

 

2.5.  Dockerfile命令(番外篇)

CMD 与 ENTRYPOINT 的区别

容器默认的入口点是 /bin/sh,这是默认的shell。

当你运行 docker container run -it ubuntu 的时候,启动的是默认shell。

ENTRYPOINT 允许你覆盖默认的入口点。例如:

 

这里默认的入口点被换成了/bin/cat

ADD 与 COPY 的区别

ADD有COPY所有的能力,而且还有一些额外的特性:

  1. 允许在镜像中自动提取tar文件
  2. 允许从远程URL下载文件 

 

3.  运行一个Docker容器

3.1.  交互

以交互模式运行WildFly容器,如下:

1 docker container run -it jboss/wildfly

默认情况下,Docker在前台运行。-i允许与STDIN交互,-t将TTY附加到进程上。它们可以一起用作 -it

按Ctrl+C停止容器

3.2.  分离容器

1 docker container run -d jboss/wildfly

 用-d选项代替-it,这样容器就以分离模式运行

(PS:-it前台运行,-d后台运行)

3.3.  用默认端口

如果你想要容器接受输入连接,则需要在调用docker run时提供特殊选项。 

1 $ docker container ls
2 CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
3 93712e8e5233        jboss/wildfly       "/opt/jboss/wildfly/…"   4 minutes ago       Up 4 minutes        8080/tcp            serene_margulis
4 02aa2ed22725        ubuntu              "/bin/bash"              2 hours ago         Up 2 hours                              frosty_bhabha 

重启容器

1 docker container stop `docker container ps | grep wildfly | awk '{print $1}'`
2 docker container run -d -P --name wildfly jboss/wildfly

-P选项将镜像中的任何公开端口映射到Docker主机上的随机端口。--name选项给这个容器起个名字。

1 $ docker container ls
2 CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                     NAMES
3 3f2babcc1df7        jboss/wildfly       "/opt/jboss/wildfly/…"   47 seconds ago      Up 47 seconds       0.0.0.0:32768->8080/tcp   wildfly

3.4.  用指定端口

1 docker container stop wildfly
2 docker container rm wildfly 

或者你还可以用 docker container rm -f wildfly 来停止并删除容器

1 docker container run -d -p 8080:8080 --name wildfly jboss/wildfly

格式是: -p hostPort:containerPort 

此选项将主机上的端口映射到容器中的端口。这样就使得我们可以通过主机上的特定的端口来访问容器。

现在我们访问http://localhost:8080/跟刚才http://localhost:32768/是一样的 

3.5.  停止容器

1 # 按id或name停止指定的容器
2 docker container stop <CONTAINER ID>
3 docker container stop <NAME>
4 
5 # 停止所有容器
6 docker container stop $(docker container ps -q)
7 
8 # 停止已经退出的容器
9 docker container ps -a -f "exited=-1"

3.6.  删除容器

1 # 按id或name删除指定的容器
2 docker container rm <CONTAINER ID>
3 docker container rm <NAME>
4 
5 # 用正则表达式删除匹配到的容器
6 docker container ps -a | grep wildfly | awk '{print $1}' | xargs docker container rm
7 
8 # 删除所有容器
9 docker container rm $(docker container ps -aq)

3.7.  查看端口映射

1 docker container port <CONTAINER ID> or <NAME>

4.  参考

https://github.com/docker/labs/tree/master/developer-tools/java/ 

https://github.com/fabric8io/docker-maven-plugin 

  

]]>
程序员之家五一出行攻略(上) http://doc.okbase.net/doclist/archive/265776.html doclist 2019-5-5 9:46:00

4月30日

因为今天很多同事都请假了,公司里也没有会议。工作进度本来就提前了,加上牙疼还没有好的原因,今天请假一天在家办公。

上午一边处理公司问题一边看书,听到小区里传来小学的广播声,喜欢听这个广播。下午在小区里做了减肥SPA,又洗了牙。

SPA的时候美容师说她们这边的减肥早饭吃一个鸡蛋和麦片粥,中午吃菜不吃主食,下午饿了可以吃个水果,晚上吃主食也可以吃点素菜和鱼虾。我说我都三年不吃晚饭了。美容师说这里减肥必须吃晚饭不然影响新陈代谢率。我听着很有道理,决定开始吃晚饭。

牙医告诉我:”我的牙很干净,每个健康状况也很好。就是最里面有颗没长出来的智齿需要拔掉。“这次也是这颗牙在疼。牙我是不拔的,《周易》里说每颗牙都连接身体的器官,是身体器官的健康状况检查器,多出来的牙是「窍」(心智)多,所以叫智齿。上学的时候系花的好朋友就是因为拔了智齿吃饭牙都打颤。在口腔医院拿了一管抑菌牙膏,决定今天的请假走病假😂

5月1日

老公说公司忙,需要在家办公不出去。因为五一之后小鲜肉要考试,所以让小鲜肉在家做功课。因为小鲜肉的功课做得不符合他老爸预期,他老爸很生气的说不带他去游泳了。

我一边看自己的电脑,一边站在「上帝视角」教育爸爸说真没有方法论。应该定一个时间点,在这个时间点做到什么程度可以去游泳,没达到就不能去。爸爸说有道理。看,所谓「领导」就是具体工作做的不多,关键时刻产生更大的价值。

下午他们做完功能去游泳,我在游泳中心旁边做了一个皮肤管理+艾灸的SPA。美容师说我虽然现在皮肤很好,但是像我这样疏于打理,再过十年就肯定不行了,需要每天一二三四五……总之就是抹的跟鬼似的。十年前我做SPA时,美容师也是这么说,我想到就笑出来了。好东西还是贵在天然✌️

 

 

费用:两日花费500人民币,在美团搜索过,想要节约点的话几十元就能下来。

 

总结:对于自己关心的、或者有心研究的东西,有时间就去听听专业的意见,实现「跃迁」。

5月2日

很早就想新配一副眼镜了。今日行程计划:潘家园眼镜城+附近龙潭公园。早上我们一家三口都出发了。

提前在美团查过价格。下了地铁直接奔眼镜城二楼。因为一楼门面费很贵,价格会加在顾客身上。在二楼随便进了一家。试了镜框之后,店家推荐了一款相对还可以的镜片。算了价格,打六折之后是480元。我说要最好的。店家拿出另一本镜片价格册子。说这是日本的。1.67非球面镜片原价720元。整套(镜架120元)店家说不找我多要,给360元。我想了一个肯定能下来且店家会比较客气的价格。我说260元,final offer不要跟我聊。成交。我不怎么爱讲价,宁愿多花点钱买别人客气点。美团上183元可以下来这么一套配置的眼镜。店里人说是镜片质量不同,这实际上怎么回事我就不知道了。

本来想到他俩吃的东西会比较油腻,所以中午带了水果,我就打算不吃了。果然也不饿。潘家园附近吃饭挺贵的。要了两碗冷盘+一份锅包肉,近100元。我说你俩能吃得了嘛。老公说不够吃吧,就我俩这实力。我心想你俩这实力又得需要我善后影响我减肥。果然,等我吃了一肚子水果之后,小鲜肉放下刚扒了两口的冷面说饱了。老公拨了一点冷面过去也宣布饱了。我一边接过小鲜肉的冷面一边戏谑他俩的实力。讨厌浪费食物是我减肥的第一大敌。

路过潘家园旧货市场,老公想进去看,平时小鲜肉也喜欢看鉴宝节目我们就进去了。这个地方他俩很喜欢。一个秦国刀币25元,多半是假的,就玩玩也挺不错。

人家说:「世人只知玉渊潭,不见龙潭也枉然。」因为龙潭不怎么出名,所以在五一这个季节里,人算是不多的,风景不错,可以划船。票价2元。老公觉得热去洗手间换衣服的时候丢了公交卡。路上我用自己带NFC功能的手机开了一张卡给他用。回家下车刷了卡,再想打开手机看一眼,手机没电关机了。还好我数学能力过关,从决定用自己手机给他当公交卡就计算着电量。因为虽然老公带了充电宝,但我没带数据线。

在公园里,小鲜肉翻着跟头过来找我。一群人路过。其中一个说着小孩长得真好看。另一个就说他妈长得也好看。我回到老公那里汇报了这个事情。爸爸说这是因为没有看到我。我说是的,看到你就知道养出我这朵鲜花的肥料有多丰厚了😂。

前几天几年没见的原公司同事(现在同一个公司不同部门,所以不常见面)在会议室外遇到了,回来就发消息说:”我说怎么还有个大美女在答辩。“我也是马上向老公汇报说:”也就是你「不识妻美」“。偶尔转述别人的话让他知道自己多幸运。

但是有次老公不干了。我看了一篇文章里面说家里因为各种小事鸡犬不宁。我就跟老公说你多幸运啊,我从来都不会因为柴米油盐跟你吵架,从来不关心钱。与别人相比,你清净好多。老公说:「是的。这些毛病你都没有,你只不过是精神病。」他这么说是因为我是喜怒不藏的人,所以上一秒还挺开心的,下一秒……但是就是这种性格,所以我们家里从没有积怨,也不会爆发什么情感危机。

但是听到老公这么评价自己,很自责啊。打算改正一下。周末老公在旁边办公,我打开两个酸奶,放了勺子。自己默默的在旁边吃了一个。看他没有动,我干别的去了。回来看到酸奶还没有动,都快不凉了,我就自己吃了。吃完老公问还有酸奶没,我说刚才是最后一个了,特地给你打开,看你没吃我就吃了。他说想吃的,以为我要吃所以没吃。额,我在旁边吃了一个的,整个画面他都没看到?竟然被无视了,顿时觉得没有装下去的必要了。

我回到床上看电脑,老公说我放到洗衣机里的衣服已经好了。我头也不抬说:”你是逼我犯精神病吗?“老公立即从椅子上跳起来收衣服去了。不要隐藏劣势,要利用劣势🤗

 

费用:260元眼镜费+40元来回路费+100元饭费+25元刀币+6元门票+18元冰品(天热,12元两个肯德基甜筒+公园内6元两个北京老冰砖,我就是那个颠颠的跑去买回来看着你们吃的那个人)+丢一张公交卡工本费plus余额100元(让我充的时候我反复提醒过他不要充这么多,没办法谁叫我家他是老大😂)

 

总结:带着目标发现途中的风景

]]>
【面试】迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章(快快珍藏) http://doc.okbase.net/doclist/archive/265775.html doclist 2019-5-5 9:46:00
网上有很多讲同步/异步/阻塞/非阻塞/BIO/NIO/AIO的文章,但是都没有达到我的心里预期,于是自己写一篇出来。



常规的误区


假设有一个展示用户详情的需求,分两步,先调用一个HTTP接口拿到详情数据,然后使用适合的视图展示详情数据。

如果网速很慢,代码发起一个HTTP请求后,就卡住不动了,直到十几秒后才拿到HTTP响应,然后继续往下执行。

这个时候你问别人,刚刚代码发起的这个请求是不是一个同步请求,对方一定回答是。这是对的,它确实是。

但你要问它为什么是呢?对方一定是这样回答的,“
因为发起请求后,代码就卡住不动了,直到拿到响应后才可以继续往下执行”。

我相信很多人也都是这样认为的,其实这是不对的,是把因果关系搞反了:

不是因为代码卡住不动了才叫同步请求,而是因为它是同步请求所以代码才卡住不动了。

至于为什么能卡住不动,这是由操作系统和CPU决定的:

因为内核空间里的对应函数会卡住不动,造成用户空间发起的系统调用卡住不动,继而使程序里的用户代码卡住不动了。

因此卡住不动了只是同步请求的一个副作用,并不能用它来定义同步请求,那该如何定义呢?


同步和异步


所谓同步,指的是协同步调。既然叫协同,所以至少要有2个以上的事物存在。协同的结果就是:

多个事物不能同时进行,必须一个一个的来,上一个事物结束后,下一个事物才开始。

那当一个事物正在进行时,其它事物都在干嘛呢?

严格来讲这个并没有要求,但一般都是处于一种“等待”的状态,因为通常后面事物的正常进行都需要依赖前面事物的结果或前面事物正在使用的资源。

因此,可以认为,同步更希望关注的是从宏观整体来看,多个事物是一种逐个逐个的串行化关系,绝对不会出现交叉的情况。

所以,自然也不太会去关注某个瞬间某个具体事物是处于一个什么状态。

把这个理论应用的出神入化的非“排队”莫属。凡是在资源少需求多的场景下都会用到排队。

比如排队买火车票这件事:

其实售票大厅更在意的是旅客一个一个的到窗口去买票,因为一次只能卖一张票。

即使大家一窝蜂的都围上去,还是一次只能卖一张票,何必呢?挤在一起又不安全。

只是有些人素质太差,非要往上挤,售票大厅迫不得已,采用排队这种形式来达到自己的目的,即一个一个的买票。

至于每个旅客排队时的状态,是看手机呀还是说话呀,根本不用去在意。

除了这种由于资源导致的同步外,还存在一种由于逻辑上的先后顺序导致的同步。

比如,先更新代码,然后再编译,接着再打包。这些操作由于后一步要使用上一步的结果,所以只能按照这种顺序一个一个的执行。

关于同步还需知道两个小的点:

一是范围,并不需要在全局范围内都去同步,只需要在某些关键的点执行同步即可。

比如食堂只有一个卖饭窗口,肯定是同步的,一个人买完,下一个人再买。但吃饭的时候也是一个人吃完,下一个人才开始吃吗?当然不是啦。

二是粒度,并不是只有大粒度的事物才有同步,小粒度的事物也有同步。


只不过小粒度的事物同步通常是天然支持的,而大粒度的事物同步往往需要手工处理。

比如两个线程的同步就需要手工处理,但一个线程里的两个语句天然就是同步的。

所谓异步,就是步调各异。既然是各异,那就是都不相同。所以结果就是:

多个事物可以你进行你的、我进行我的,谁都不用管谁,所有的事物都在同时进行中。


一言以蔽之,同步就是多个事物不能同时开工,异步就是多个事物可以同时开工。


注:一定要去体会“多个事物”,多个线程是多个事物,多个方法是多个事物,多个语句是多个事物,多个CPU指令是多个事物。等等等等。



阻塞和非阻塞



所谓阻塞,指的是阻碍堵塞。它的本意可以理解为由于遇到了障碍而造成的动弹不得。


所谓非阻塞,自然是和阻塞相对,可以理解为由于没有遇到障碍而继续畅通无阻。

对这两个词最好的诠释就是,当今中国一大交通难题,堵车:

汽车可以正常通行时,就是非阻塞。一旦堵上了,全部趴窝,一动不动,就是阻塞。


因此阻塞关注的是不能动,非阻塞关注的是可以动。


不能动的结果就是只能等待,可以动的结果就是继续前行。

因此和阻塞搭配的词一定是等待,和非阻塞搭配的词一定是进行。

回到程序里,阻塞同样意味着停下来等待,非阻塞表明可以继续向下执行。


阻塞和等待



等待只是阻塞的一个副作用而已,表明随着时间的流逝,没有任何有意义的事物发生或进行。


阻塞的真正含义是你关心的事物由于某些原因无法继续进行,因此让你等待。但没必要干等,你可以做一些其它无关的事物,因为这并不影响你对相关事物的等待。

在堵车时,你可以干等。也可以玩手机、和别人聊天,或者打牌、甚至先去吃饭都行。因为这些事物并不影响你对堵车的等待。不过你的车必须呆在原地。

在计算机里,是没有人这么灵活的,一般在阻塞时,选在干等,因为这最容易实现,只需要挂起线程,让出CPU即可。在条件满足时,会重新调度该线程。


两两组合


所谓同步/异步,关注的是
能不能同时开工

所谓阻塞/非阻塞,关注的是
能不能动

通过推理进行组合:

同步阻塞,不能同时开工,也不能动。只有一条小道,一次只能过一辆车,可悲的是还TMD的堵上了。

同步非阻塞,不能同时开工,但可以动。只有一条小道,一次只能过一辆车,幸运的是可以正常通行。

异步阻塞,可以同时开工,但不可以动。有多条路,每条路都可以跑车,可气的是全都TMD的堵上了。

异步非阻塞,可以工时开工,也可以动。有多条路,每条路都可以跑车,很爽的是全都可以正常通行。


是不是很容易理解啊。
其实它们的关注点是不同的,只要搞明白了这点,组合起来也不是事儿。

回到程序里,把它们和线程关联起来:

同步阻塞,相当于一个线程在等待。

同步非阻塞,相当于一个线程在正常运行。

异步阻塞,相当于多个线程都在等待。

异步非阻塞,相当于多个线程都在正常运行。



I/O



IO指的就是读入/写出数据的过程,和等待读入/写出数据的过程。一旦拿到数据后就变成了数据操作了,就不是IO了。

拿网络IO来说,等待的过程就是数据从网络到网卡再到内核空间。读写的过程就是内核空间和用户空间的相互拷贝。


所以IO就包括两个过程,一个是等待数据的过程,一个是读写(拷贝)数据的过程。而且还要明白,一定能包括操作数据的过程。



阻塞IO和非阻塞IO



应用程序都是运行在用户空间的,所以它们能操作的数据也都在用户空间。按照这样子来理解,只要数据没有到达用户空间,用户线程就操作不了。


如果此时用户线程已经参与,那它一定会被阻塞在IO上。这就是常说的阻塞IO。用户线程被阻塞在等待数据上或拷贝数据上。


非阻塞IO就是用户线程不参与以上两个过程,即数据已经拷贝到用户空间后,才去通知用户线程,一上来就可以直接操作数据了。


用户线程没有因为IO的事情出现阻塞,这就是常说的非阻塞IO。



同步IO和同步阻塞IO



按照上文中对同步的理解,同步IO是指发起IO请求后,必须拿到IO的数据才可以继续执行。

按照程序的表现形式又分为两种:

在等待数据的过程中,和拷贝数据的过程中,线程都在阻塞,这就是同步阻塞IO。

在等待数据的过程中,线程采用死循环式轮询,在拷贝数据的过程中,线程在阻塞,这其实还是同步阻塞IO。


网上很多文章把第二种归为同步非阻塞IO,这肯定是错误的,它一定是阻塞IO,因为拷贝数据的过程,线程是阻塞的。

严格来讲,在IO的概念上,同步和非阻塞是
不可能搭配的,因为它们是一对相悖的概念。

同步IO意味着必须拿到IO的数据,才可以继续执行。因为后续操作依赖IO数据,所以它必须是阻塞的。

非阻塞IO意味着发起IO请求后,可以继续往下执行。说明后续执行不依赖于IO数据,所以它肯定不是同步的。


因此,在IO上,
同步非阻塞互斥的,所以不存在同步非阻塞IO但同步非阻塞是存在的,那不叫IO,叫操作数据了。


所以,同步IO一定是阻塞IO,同步IO也就是同步阻塞IO。



异步IO和异步阻塞/非阻塞IO



按照上文中对异步的理解,异步IO是指发起IO请求后,不用拿到IO的数据就可以继续执行。


用户线程的继续执行,和操作系统准备IO数据的过程是同时进行的,因此才叫做异步IO。

按照IO数据的两个过程,又可以分为两种:

在等待数据的过程中,用户线程继续执行,在拷贝数据的过程中,线程在阻塞,这就是异步阻塞IO。

在等待数据的过程中,和拷贝数据的过程中,用户线程都在继续执行,这就是异步非阻塞IO。


第一种情况是,用户线程没有参与数据等待的过程,所以它是异步的。但用户线程参与了数据拷贝的过程,所以它又是阻塞的。合起来就是异步阻塞IO。

第二种情况是,用户线程既没有参与等待过程也没有参与拷贝过程,所以它是异步的。当它接到通知时,数据已经准备好了,它没有因为IO数据而阻塞过,所以它又是非阻塞的。合起来就是异步非阻塞IO。


PS:聪明的你或许发现了我没有提多路复用IO,因为它值得专门撰文一篇。


(END)


作者是工作超过10年的码农,现在任架构师。喜欢研究技术,崇尚简单快乐。追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。下面是公众号和知识星球的二维码,欢迎关注!

       

]]>
Vue 进阶之路(十一) http://doc.okbase.net/doclist/archive/265774.html doclist 2019-5-5 9:46:00

之前的文章我们说了一下 vue 中组件的原生事件绑定,本章我们来所以下 vue 中的插槽使用。

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>vue</title>
 6     <script src="https://cdn.jsdelivr.net/npm/vue"></script>
 7 </head>
 8 <body>
 9 <div id="app">
10     <child title="<p>你好 世界</p>"></child>
11 </div>
12 <script>
13     Vue.component("child", {
14         props: ['title'],
15         template: `
16             <div>
17                 {{title}}
18                 <p>hello world</p>
19             </div>
20        `
21     });
22     var app = new Vue({
23         el: '#app',
24     })
25 </script>
26 </body>
27 </html>

上面的代码中,我们通过 title="" 形式通过父组件向子组件 child 传递了一个 "<p>你好世界</p>" 的带标签的内容,然后我们在子组件中输出,结果如下:

显示结果是按字符串展示的,但我们想要的是不带标签的输出结果,在之前的文章中我们说过可以通过 v-html 来进行转义,代码如下:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>vue</title>
 6     <script src="https://cdn.jsdelivr.net/npm/vue"></script>
 7 </head>
 8 <body>
 9 <div id="app">
10     <child title="<p>你好 世界</p>"></child>
11 </div>
12 <script>
13     Vue.component("child", {
14         props: ['title'],
15         template: `
16             <div>
17                 <div v-html="title"></div>
18                 <p>hello world</p>
19             </div>
20        `
21     });
22     var app = new Vue({
23         el: '#app',
24     })
25 </script>
26 </body>
27 </html>

我们把 template 中的 {{title}} 改成了 v-html 的形式输出,结果如下:

输出结果没问题,但是当我们看控制台的 HTML 代码时发现外层多加了一个 <div> 标签,这显然不友好,,这时可能有人会想到模板标签 <template v-html="title"> 这样写,但是这样的话在页面上是不会输出内容的。而且如果 <child> 标签内的 title 属性里面的内容并不只是一个 <p> 标签,还有很多其他的内容,例如 "<p>你好 世界<p><p>你好 世界<p><p>你好 世界<p><p>你好 世界<p><p>你好 世界<p><p>你好 世界<p>" 这么长的内容,在代码里也不好看。

如何解决上面的问题,Vue 官方为我们提供插槽 slot,我们可以将代码改成如下:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>vue</title>
 6     <script src="https://cdn.jsdelivr.net/npm/vue"></script>
 7 </head>
 8 <body>
 9 <div id="app">
10     <child>
11         <p>你好 世界</p>
12     </child>
13 </div>
14 <script>
15     Vue.component("child", {
16         template: `
17             <div>
18                 <slot></slot>
19                 <p>hello world</p>
20             </div>
21        `
22     });
23     var app = new Vue({
24         el: '#app',
25     })
26 </script>
27 </body>
28 </html>

我们将 p 标签想要输出的内容直接放在了 <child> 标签内,然后在 template 中添加标签 <slot>,意思就是将 <child> 内的内容通过 slot 插槽插入子组件,结果如下:

完美解决了我们的问题,而且 <slot> 标签内还可以自定义我们想要输出的内容,如果 <child> 标签内没有内容的话以自定义的内容输出,如下:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>vue</title>
 6     <script src="https://cdn.jsdelivr.net/npm/vue"></script>
 7 </head>
 8 <body>
 9 <div id="app">
10     <child></child>
11 </div>
12 <script>
13     Vue.component("child", {
14         template: `
15             <div>
16                 <slot>插槽自定义内容</slot>
17                 <p>hello world</p>
18             </div>
19        `
20     });
21     var app = new Vue({
22         el: '#app',
23     })
24 </script>
25 </body>
26 </html>

我们在 <child> 标签内没有内容,在 slot 标签内插入了一些内容,在页面显示如下:

上面的 "插槽自定义内容" 在 <child> 内没有内容时输出,如果有内容则输出 <child> 标签内的内容。

上面的插槽形式我们可以称之为无名插槽,还有一种插槽叫具名插槽,看以下代码:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>vue</title>
 6     <script src="https://cdn.jsdelivr.net/npm/vue"></script>
 7 </head>
 8 <body>
 9 <div id="app">
10     <child>
11         <header>我是 header</header>
12         <footer>我是 footer</footer>
13     </child>
14 </div>
15 <script>
16     Vue.component("child", {
17         template: `
18             <div>
19                 <slot></slot>
20                 <p>hello world</p>
21                 <slot></slot>
22             </div>
23        `
24     });
25     var app = new Vue({
26         el: '#app',
27     })
28 </script>
29 </body>
30 </html>

我们想要一种效果,就是自定义插槽的内容位置,假设上的代码中 <header> 标签内的内容为头部信息,<footer> 标签内的内容为底部信息,我们想让它们分别输出在 template 模板中 p 标签的上下,结果如下:

输出内容显然不是我们想要的结果,我们每用一次 <slot> 标签就会在页面输出一次,那该如何解决这个问题呢,我们可以使用具名插槽来为我们的插槽定义名称,如下:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>vue</title>
 6     <script src="https://cdn.jsdelivr.net/npm/vue"></script>
 7 </head>
 8 <body>
 9 <div id="app">
10     <child>
11         <header slot="header">我是 header</header>
12         <footer slot="footer">我是 footer</footer>
13     </child>
14 </div>
15 <script>
16     Vue.component("child", {
17         template: `
18             <div>
19                 <slot name="header"></slot>
20                 <p>hello world</p>
21                 <slot name="footer"></slot>
22             </div>
23        `
24     });
25     var app = new Vue({
26         el: '#app',
27     })
28 </script>
29 </body>
30 </html>

在上面的代码中,我们分别为 <header> <footer> 标签添加 slot 属性,然后在 template 中的 <slot> 标签内以 name 属性来分别对应标签 <header> <footer> 内的 slot 属性值,这样就将指定的内容输出,结果如下:

完美解决我们的问题。

 

]]>
百亿级企业级 RPC 框架开源了! http://doc.okbase.net/doclist/archive/265773.html doclist 2019-5-5 9:46:00

今天给大家介绍给一款性能卓越的 RPC 开源框架,其作者就是我推荐每个 Java 程序员都应该看的《Java 生态核心知识点整理》的原作者张玉龙。

说实话我第一次看到这个资料的时候,就感觉作者是一位真正的技术爱好者,后来通过朋友介绍终于认识了他。交谈之中得知他在美团工作,最初和朋友一起整理这份资料的初衷是为了面试,估计每天需要面试太多的应聘者,这份资料成了助手。强烈建议没有看这份资料的同学学习下,作为 Java 生态知识体系构建也是一份不错的资源。

后来得知业余时间他在研发一款开源的 RPC 开源框架,并且经过测试可支持百亿级别的调用,并且于近期终于完成推出 1.0 版本。这款开源软件名字叫做 Koalas,源代码地址:koalas-rpc,下面对这款开源软件做详细介绍,内容来源于 Koalas 。

Koalas 介绍

企业生产级百亿日 PV 高可用可拓展的 RPC 框架。理论上并发数量接近服务器带宽,客户端采用 thrift 协议,服务端支持 netty 和 thrift 的 TThreadedSelectorServer 半同步半异步线程模型,支持动态扩容,服务上下线,权重动态,可用性配置,页面流量统计,支持 trace 跟踪等,天然接入 cat 支持数据大盘展示等,持续为个人以及中小型公司提供可靠的 RPC 框架技术方案。

Thrift 是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。它被当作一个远程过程调用(RPC)框架来使用,是由 Facebook 为“大规模跨语言服务开发”而开发的。

为什么叫 koalas

树袋熊英文翻译,希望考拉 RPC 给那些不太喜欢动手自己去造轮子的人提供可靠的 RPC 使用环境。

为什么要写这个 RPC

市面上常见的 RPC 框架很多,grpc,motan,dubbo 等,但是随着越来越多的元素加入,复杂的架构设计等因素似使得这些框架和 spring 一样,虽然号称是轻量级,但是用起来却是让我们很蹩脚,大量的配置,繁杂的 API 设计,其实,我们根本用不上这些东西!!!

我也算得上是在很多个互联网企业厮杀过,见过很多很多的内部 RPC 框架,有些优秀的设计让我非常赞赏,有一天我突然想着,为什么不对这些设计原型进行聚合归类,于是自己搞一套【轻量级】 RPC 框架呢,于是利用业余时间开发此项目,希望源码对大家对认识 RPC 框架起到推进的作用。

技术栈

  • thrift 0.8.0
  • spring-core-4.2.5,spring-context-4.2.5,spring-beans-4.2.5
  • log4j,slf4j
  • org.apache.commons(v2.0+)
  • io.netty4
  • fastJson
  • zookeeper
  • 点评cat(V3.0.0+ 做数据大盘统计上报等使用,可不配置)
  • AOP,反射代理等

技术架构

Koalas 架构图

序列化

考察了很多个序列化组件,其中包括jdk原生,kryo、hessian、protoStuff,thrift,json等,最终选择了Thrift,原因如下:原生JDK序列化反序列化效率堪忧,其序列化内容太过全面kryo和hessian,json相对来说比原生JDK强一些,但是对跨语言支持一般,所以舍弃了,最终想在protoBuf和Thrift协议里面选择一套框架,这俩框架很相通,支持跨语言,需要静态编译等等。但是protoBuf不带RPC服务,本着提供多套服务端模式(thrift rpc,netty)的情况下,最终选择了Thrift协议。

IO线程模型

原生socket可以模拟出简单的RPC框架,但是对于大规模并发,要求吞吐量的系统来说,也就算得上是一个demo级别的,所以BIO肯定是不考虑了,NIO的模型在序列化技术选型的时候已经说了,Thrift本身支持很多个io线程模型,同步,异步,半同步异步等(SimpleServer,TNonblockingServer,THsHaServer,TThreadedSelectorServer,TThreadPoolServer),其中吞吐量最高的肯定是半同步半异步的IO模TThreadedSelectorServer了,具体原因大家可自行google,这次不做多的阐述,选择好了模型之后,发现thrift简直就是神器一样的存在,再一想,对于服务端来说,IO模型怎么能少得了Netty啊,所以下决心也要支持Netty,但是很遗憾Netty目前没有对Thrift的序列化解析,拆包粘包的处理,但是有protoBuf,和http协议的封装,怎么办,自己在netty上写对thrift的支持呗,虽然工作量大了一些,但是一想netty不就是干这个事儿的嘛- -!

服务发现

支持集群的RPC框架里面,像dubbo,或者是其他三方框架,对服务发现都进行的封装,那么自研RPC的话,服务发现就要自己来写了,那么简单小巧容易上手的zookeeper肯定是首选了。

内容展示

实际性能压测

8C 16G mac 开发本,单机 10000 次请求耗时截图

10w 次请求,大约耗时 12s,平均 qps 在8000左右,在集群环境下会有不错的性能表现

数据大盘展示

koalas2.0 已经接入了 cat 服务,cat 服务支持 qps 统计,可用率,tp90line,tp99line,丰富自定义监控报警等,接入效果图

丰富的可视参数,流量统计,日,周,月报表展示等。

链路跟踪

对 RPC 服务来说,系统间的调用和排查异常接口,确定耗时代码是非常重要的,只要接入了 cat,koalsa-rpc 天然的支持链路跟踪,一切尽在眼前!

最后

作者非常具有技术情怀,在聊天中说就剩这点爱好了,要坚持下去。听了这句话啥都不说了,点击下方链接,先 Star 为敬。

https://gitee.com/a1234567891/koalas-rpc

]]>