git flow

分類 devops, git

Git Flow

Git Flow 是一种基于 Git 的分支模型,旨在帮助团队更好地管理和发布软件。

Git Flow 由 Vincent Driessen 在 2010 年提出,并通过一套标准的分支命名和工作流程,使开发、测试和发布过程更加有序和高效。

Git Flow 主要由以下几类分支组成:masterdevelopfeaturereleasehotfix

Git Flow 安装

Linux

Debian/Ubuntu:

1
sudo apt-get install git-flow

Fedora:

1
2
sudo dnf install gitflow
sudo apt-get install git-flow

macOS

在 macOS 上,你可以使用 Homebrew 来安装 Git Flow:

1
brew install git-flow

源码安装

如果你的发行版的包管理器中没有 Git Flow,你也可以从源代码进行安装:

1
2
3
git clone https://github.com/nvie/gitflow.git
cd gitflow
sudo make install

安装完成后,你可以通过以下命令验证 Git Flow 是否成功安装:

1
git flow version

Windows

在 Windows 上,你可以通过以下方式安装 Git Flow:

  • 使用 Git for Windows: Git for Windows 包含了 Git Flow。你可以从 Git for Windows 安装 Git,然后使用 Git Bash 来使用 Git Flow。

  • 使用 Scoop: 如果你使用 Scoop 包管理工具,可以通过以下命令安装 Git Flow:

    1
    scoop install git-flow
  • 使用 Chocolatey: 如果你使用 Chocolatey 包管理工具,可以通过以下命令安装 Git Flow:

    1
    choco install gitflow

Git Flow 分支模型

master 分支

  • 永远保持稳定和可发布的状态。
  • 每次发布一个新的版本时,都会从 develop 分支合并到 master 分支。

develop 分支

  • 用于集成所有的开发分支。
  • 代表了最新的开发进度。
  • 功能分支、发布分支和修复分支都从这里分支出去,最终合并回这里。

feature 分支

  • 用于开发新功能。
  • develop 分支创建,开发完成后合并回 develop 分支。
  • 命名规范:feature/feature-name

release 分支

  • 用于准备新版本的发布。
  • develop 分支创建,进行最后的测试和修复,然后合并回 developmaster 分支,并打上版本标签。
  • 命名规范:release/release-name

hotfix 分支

  • 用于修复紧急问题。
  • master 分支创建,修复完成后合并回 masterdevelop 分支,并打上版本标签。
  • 命名规范:hotfix/hotfix-name

分支操作原理

  • Master 分支上的每个 Commit 应打上 Tag,Develop 分支基于 Master 创建。
  • Feature 分支完成后合并回 Develop 分支,并通常删除该分支。
  • Release 分支基于 Develop 创建,用于测试和修复 Bug,发布后合并回 Master 和 Develop,并打 Tag 标记版本号。
  • Hotfix 分支基于 Master 创建,完成后合并回 Master 和 Develop,并打 Tag 1。

Git Flow 命令示例

  • 开始 Feature 分支:git flow feature start MYFEATURE
  • 完成 Feature 分支:git flow feature finish MYFEATURE
  • 开始 Release 分支:git flow release start RELEASE [BASE]
  • 完成 Release 分支:合并到 Master 和 Develop,打 Tag,删除 Release 分支。
  • 开始 Hotfix 分支:git flow hotfix start HOTFIX [BASE]
  • 完成 Hotfix 分支:合并到 Master 和 Develop,打 Tag,删除 Hotfix 分支。

Git Flow 工作流程

1. 初始化 Git Flow

首先,在项目中初始化 Git Flow。可以使用 Git Flow 插件(例如 git-flow)来简化操作。

1
git flow init

初始化时,你需要设置分支命名规则和默认分支。

2. 创建功能分支

当开始开发一个新功能时,从 develop 分支创建一个功能分支。

1
git flow feature start feature-name

完成开发后,将功能分支合并回 develop 分支,并删除功能分支。

1
git flow feature finish feature-name

3. 创建发布分支

当准备发布一个新版本时,从 develop 分支创建一个发布分支。

1
git flow release start release-name

在发布分支上进行最后的测试和修复,准备好发布后,将发布分支合并回 developmaster 分支,并打上版本标签。

1
git flow release finish release-name

4. 创建修复分支

当发现需要紧急修复的问题时,从 master 分支创建一个修复分支。

1
git flow hotfix start hotfix-name

修复完成后,将修复分支合并回 masterdevelop 分支,并打上版本标签。

1
git flow hotfix finish hotfix-name

实例操作

以下是一个实际使用 Git Flow 的综合实例。

初始化 Git Flow

1
git flow init

创建和完成功能分支

1
2
git flow feature start new-feature # 开发新功能
git flow feature finish new-feature

创建和完成发布分支

1
2
git flow release start v1.0.0 # 测试和修复
git flow release finish v1.0.0

创建和完成修复分支

1
2
git flow hotfix start hotfix-1.0.1. # 修复紧急问题
git flow hotfix finish hotfix-1.0.1

优点和缺点

优点

  • 明确的分支模型:清晰的分支命名和使用规则,使得开发过程井然有序。
  • 隔离开发和发布:开发和发布过程分离,减少了开发中的不确定性对发布的影响。
  • 版本管理:每次发布和修复都会打上版本标签,方便回溯和管理。

缺点

  • 复杂性:对于小型团队或简单项目,Git Flow 的分支模型可能显得过于复杂。
  • 频繁的合并:在大型团队中,频繁的分支合并可能导致合并冲突增加。

Git Flow 是一种结构化的分支管理模型,通过定义明确的分支和工作流程,帮助团队更好地管理软件开发和发布过程。虽然它增加了一定的复杂性,但对于大型项目和团队协作,Git Flow 提供了强大的支持和管理能力。

留言與分享

查看是否支持 SSL

首先在 MySQL 上执行如下命令, 查询是否 MySQL 支持 SSL:

1
2
3
4
5
6
7
mysql> SHOW VARIABLES LIKE 'have_ssl';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| have_ssl | YES |
+---------------+-------+
1 row in set (0.02 sec)

have_sslYES 时, 表示此时 MySQL 服务已经支持 SSL 了. 如果是 DESABLE, 则需要在启动 MySQL 服务时, 使能 SSL 功能.

使用 OpenSSL 创建 SSL 证书和私钥

首先我们需要使用 openssl 来创建服务器端的证书和私钥. 我使用的 openssl 版本为:

1
2
>>> /usr/local/Cellar/openssl/1.0.2j/bin/openssl version
OpenSSL 1.0.2j 26 Sep 2016

新建一个 ~/temp/cert 目录, 用于存放生成的证书和私钥

1
2
mkdir ~/temp/cert
cd ~/temp/cert

创建 CA 私钥和 CA 证书

然后, 我们先来生成一个 CA 私钥:

1
openssl genrsa 2048 > ca-key.pem

当有了一个 CA 私钥, 我们接下来就可以使用这个私钥生成一个新的数字证书:

1
openssl req -sha1 -new -x509 -nodes -days 3650 -key ca-key.pem > ca-cert.pem

执行这个命令时, 会需要填写一些问题, 随便填写就可以了. 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> openssl req -sha1 -new -x509 -nodes -days 3650 -key ca-key.pem > ca-cert.pem

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Beijing
Locality Name (eg, city) []:Beijing
Organization Name (eg, company) [Internet Widgits Pty Ltd]:xys
Organizational Unit Name (eg, section) []:xys
Common Name (e.g. server FQDN or YOUR name) []:xys
Email Address []:yongshun1228@gmail.com

执行上述命令后, 我们就有了一个 CA 私钥和一个 CA 证书.

创建服务器端的 RSA 私钥和数字证书

接着, 我们需要创建服务器端的私钥和一个证书请求文件, 命令如下:

1
openssl req -sha1 -newkey rsa:2048 -days 3650 -nodes -keyout server-key.pem > server-req.pem

上面这个命令会生成一个新的私钥(server-key.pem), 同时会使用这个新私钥来生成一个证书请求文件(server-req.pem).
上面这个命令同样需要回答几个问题, 随便填写即可. 不过需要注意的是, A challenge password 这一项需要为空.
即:

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
>>> openssl req -sha1 -newkey rsa:2048 -days 3650 -nodes -keyout server-key.pem > server-req.pem

Generating a 2048 bit RSA private key
.................+++
..+++
writing new private key to 'server-key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Beijing
Locality Name (eg, city) []:Beijing
Organization Name (eg, company) [Internet Widgits Pty Ltd]:xys
Organizational Unit Name (eg, section) []:xys
Common Name (e.g. server FQDN or YOUR name) []:xys
Email Address []:yongshun1228@gmail.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

下一步, 我们需要将生成的私钥转换为 RSA 私钥文件格式:

1
openssl rsa -in server-key.pem -out server-key.pem

最后一步, 我们需要使用原先生成的 CA 证书来生成一个服务器端的数字证书:

1
openssl x509 -sha1 -req -in server-req.pem -days 3650 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 > server-cert.pem

上面的命令会创建以服务器端的数字证书文件.

创建客户端的 RSA 私钥和数字证书

和服务器端所执行的命令类似, 我们也需要为客户端生成一个私钥和证书请求文件, 命令如下:

1
openssl req -sha1 -newkey rsa:2048 -days 3650 -nodes -keyout client-key.pem > client-req.pem

同样地, 我们需要将生成的私钥转换为 RSA 私钥文件格式:

1
openssl rsa -in client-key.pem -out client-key.pem

最后, 我们也需要为客户端创建一个数字证书:

1
openssl x509 -sha1 -req -in client-req.pem -days 3650 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 > client-cert.pem

使用工具创建证书与私钥

前面我们介绍了如何使用 OpenSSL 来创建 SSL 连接的私钥和证书文件, 现在我们来看一个更简单的方法.
在 MySQL 5.7 中, 提供了一个名为 mysql_ssl_rsa_setup 的工具, 通过它, 我们可以很方便地创建 SSL 连接所需要的各种文件:

1
2
3
mkdir ~/temp/cert
cd ~/temp/cert
mysql_ssl_rsa_setup --datadir ./

上面的命令中, --datadir 表示生成的文件的目录.

当执行了上述命令后, 也会生成八个文件:

1
2
3
4
5
6
7
8
ca-key.pem
ca.pem
client-cert.pem
client-key.pem
private_key.pem
public_key.pem
server-cert.pem
server-key.pem

这些文件和我们使用 OpenSSL 所创建的那八个文件的作用是一样的, 这里就不再详述了.

SSL 配置

在前面的步骤中, 我们已经生成了8个文件, 分别是:

  • ca-cert.pem: CA 证书, 用于生成服务器端/客户端的数字证书.

  • ca-key.pem: CA 私钥, 用于生成服务器端/客户端的数字证书.

  • server-key.pem: 服务器端的 RSA 私钥

  • server-req.pem: 服务器端的证书请求文件, 用于生成服务器端的数字证书.

  • server-cert.pem: 服务器端的数字证书.

  • client-key.pem: 客户端的 RSA 私钥

  • client-req.pem: 客户端的证书请求文件, 用于生成客户端的数字证书.

  • client-cert.pem: 客户端的数字证书.

接下来我们就需要分别配置服务器端和客户端.

服务器端配置

服务器端需要用到三个文件, 分别是: CA 证书, 服务器端的 RSA 私钥, 服务器端的数字证书, 我们需要在 [mysqld] 配置域下添加如下内容:

1
2
3
4
[mysqld]
ssl-ca=/etc/mysql/ca-cert.pem
ssl-cert=/etc/mysql/server-cert.pem
ssl-key=/etc/mysql/server-key.pem

接着我们还可以更改 bind-address, 使 MySQL 服务可以接收来自所有 ip 地址的客户端, 即:

1
bind-address = *

当配置好后, 我们需要重启 MySQL 服务, 使能配置.

最后一步, 我们添加一个需要使用 SSL 才可以登录的帐号, 来验证一下我们所配置的 SSL 是否生效:

1
2
GRANT ALL PRIVILEGES ON *.* TO 'ssl_test'@'%' IDENTIFIED BY 'ssl_test' REQUIRE SSL;
FLUSH PRIVILEGES;

当配置好后, 使用 root 登录 MySQL, 执行 show variables like '%ssl%' 语句会有如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> show variables like '%ssl%';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| have_openssl | YES |
| have_ssl | YES |
| ssl_ca | ca.pem |
| ssl_capath | |
| ssl_cert | server-cert.pem |
| ssl_cipher | |
| ssl_crl | |
| ssl_crlpath | |
| ssl_key | server-key.pem |
+---------------+-----------------+
9 rows in set (0.01 sec)

客户端配置

客户端配置相对简单一些. 首先我们需要拷贝 ca-cert.pem, client-cert.pemclient-key.pem 这三个文件到客户端主机中, 然后我们可以执行如下命令来使用 SSL 连接 MySQL 服务:

1
mysql --ssl-ca=/path/to/ca-cert.pem --ssl-cert=/path/to/client-cert.pem --ssl-key=/path/to/client-key.pem -h host_name -u ssl_test -p

除了上述的使用命令行方式配置 SSL 外, 我们也可以使用配置文件的方式. 即在 ~/.my.cnf 文件中添加如下内容即可:

1
2
3
4
[client]
ssl-ca=/path/to/ca-cert.pem
ssl-cert=/path/to/client-cert.pem
ssl-key=/path/to/client-key.pem

当连接成功后, 我们执行如下指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> \s
--------------
mysql Ver 14.14 Distrib 5.7.17, for Linux (x86_64) using EditLine wrapper

Connection id: 14
Current database:
Current user: ssl_test@172.17.0.4
SSL: Cipher in use is DHE-RSA-AES256-SHA
Current pager: stdout
Using outfile: ''
Using delimiter: ;
Server version: 5.7.17 MySQL Community Server (GPL)
Protocol version: 10
Connection: test_db via TCP/IP
Server characterset: latin1
Db characterset: latin1
Client characterset: latin1
Conn. characterset: latin1
TCP port: 3306
Uptime: 1 hour 2 min 9 sec

Threads: 1 Questions: 23 Slow queries: 0 Opens: 126 Flush tables: 3 Open tables: 0 Queries per second avg: 0.006
--------------

如果输出中有 SSL: Cipher in use is DHE-RSA-AES256-SHA 之类的信息, 则表示已经使用 SSL 来连接了.

在 Docker 中使能 MySQL SSL 连接

上面我们简单介绍了一下如果使能 MySQL SSL 连接, 那么现在我们使用 Docker 来具体的实战一把吧!

首先拉取最新的 MySQL 镜像:

1
docker pull mysql

然后需要准备一下挂载到 Docker 容器的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> cd ~/temp
>>> tree
.
├── cert
│ ├── ca-key.pem
│ ├── ca.pem
│ ├── client-cert.pem
│ ├── client-key.pem
│ ├── private_key.pem
│ ├── public_key.pem
│ ├── server-cert.pem
│ └── server-key.pem
├── config
│ └── my.cnf
└── db

3 directories, 9 files

在 temp 目录下有三个子目录:

  • cert 目录用于存放我们先前生成的证书和私钥信息;

  • config 目录用于存放 MySQL 服务的配置文件

  • db 目录是用于存放 MySQL 的数据.

下一步我们需要使用如下命令启动 MySQL 容器:

1
docker run --rm --name test_db -p 10000:3306 -e MYSQL_ROOT_PASSWORD=root -v /Users/xiongyongshun/temp/db:/var/lib/mysql -v /Users/xiongyongshun/temp/config:/etc/mysql/conf.d -v /Users/xiongyongshun/temp/cert:/etc/mysql/cert mysql:latest

我们在上面的命令中, 我们分别挂载了 cert, config, db 这三个宿主机上的目录到 MySQL 容器中.

启动了 MySQL 服务后, 可以先使用 root 帐号登录 MySQL, 来检查 MySQL 服务此时是否已经开启了 SSL 功能:

1
docker run -it --link test_db:test_db --rm  mysql sh -c 'exec mysql -u root -p -h test_db'

登录成功后, 我们在 MySQL 中执行如下指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> show variables like '%ssl%';
+---------------+---------------------------------+
| Variable_name | Value |
+---------------+---------------------------------+
| have_openssl | YES |
| have_ssl | YES |
| ssl_ca | /etc/mysql/cert/ca-cert.pem |
| ssl_capath | |
| ssl_cert | /etc/mysql/cert/server-cert.pem |
| ssl_cipher | |
| ssl_crl | |
| ssl_crlpath | |
| ssl_key | /etc/mysql/cert/server-key.pem |
+---------------+---------------------------------+
9 rows in set (0.01 sec)

有上面的输出后, 表明此时 MySQL 服务已经使用 SSL 功能了.

接着下一步, 我们按照前面所提到的, 创建一个仅仅可以使用 SSL 登录的帐号, 来检验我们的配置是否有效:

1
2
GRANT ALL PRIVILEGES ON *.* TO 'ssl_test'@'%' IDENTIFIED BY 'ssl_test' REQUIRE SSL;
FLUSH PRIVILEGES;

上面的命令创建了一个帐号名为 ssl_test, 密码为 ssl_test, 并且不限制登录主机 ip 的帐号.

这些都配置成功后, 我们再启动一个 MySQL 客户端容器:

1
docker run -it --link test_db:test_db --rm -v /Users/xiongyongshun/temp/cert:/etc/mysql/cert mysql sh -c 'exec mysql --ssl-ca=/etc/mysql/cert/ca-cert.pem --ssl-cert=/etc/mysql/cert/client-cert.pem --ssl-key=/etc/mysql/cert/client-key.pem -h test_db -u ssl_test -p'

从上面的这个命令中我们可以看到, 启动 MySQL 客户端容器时, 我们挂载了宿主机的 cert 目录到容器内的 /etc/mysql/cert 目录, 这样在容器中就可以访问到 SSL 私钥和证书文件了. 接着我们在 MySQL 客户端命令行中, 使用 --ssl-ca, --ssl-cert, --ssl-key 这三个参数来指定 SSL 连接所需要的 CA 证书, RSA 私钥和客户端证书.

登录成功后, 我们执行 s 命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> \s
--------------
mysql Ver 14.14 Distrib 5.7.17, for Linux (x86_64) using EditLine wrapper

Connection id: 5
Current database:
Current user: ssl_test@172.17.0.5
SSL: Cipher in use is DHE-RSA-AES256-SHA
Current pager: stdout
Using outfile: ''
Using delimiter: ;
Server version: 5.7.17 MySQL Community Server (GPL)
Protocol version: 10
Connection: test_db via TCP/IP
Server characterset: latin1
Db characterset: latin1
Client characterset: latin1
Conn. characterset: latin1
TCP port: 3306
Uptime: 6 min 8 sec

Threads: 2 Questions: 10 Slow queries: 0 Opens: 113 Flush tables: 1 Open tables: 106 Queries per second avg: 0.027
--------------

输出中有 SSL: Cipher in use is DHE-RSA-AES256-SHA 信息则说明我们确实是使用了 SSL 连接的 MySQL 服务器.

留言與分享

CentOs7.3 安装 MySQL 5.7.19 二进制版本

参考官网 - 使用通用二进制文件在Unix / Linux上安装MySQL

MySQL社区版 下载地址

采用二进制方式免编译安装MySQL,适合各类MySQL产品系列,不需要复杂的编译设置和编译时间等待,直接解压下载的软件包,初始化即可完成MySQL的安装和启动.

1.准备工作

依赖环境

关闭防火墙

1
$ systemctl stop firewalld.service

MySQL依赖于libaio 库

1
2
$ yum search libaio
$ yum install libaio

下载,解压,重命名

通常解压在 /usr/local/mysql

mysql-5.7.19-linux-glibc2.12-x86_64 文件夹,重命名成mysql,这样就凑成/usr/local/mysql目录了

1
2
3
4
$ cd /opt/
$ wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.19-linux-glibc2.12-x86_64.tar.gz
$ tar -zxvf /opt/mysql-5.7.19-linux-glibc2.12-x86_64.tar.gz -C /usr/local/
$ mv /usr/local/mysql-5.7.19-linux-glibc2.12-x86_64/ /usr/local/mysql

解压目录内容

bin mysqld服务器,客户端和实用程序
data 日志文件,数据库
docs MySQL手册信息格式
man Unix手册页
include 包含(标题)文件
lib
share 其他支持文件,包括错误消息,示例配置文件,用于数据库安装的SQL

2.安装MySQL

1. 新建用户组和用户

1
2
3
$ cd /usr/local/mysql/ 
$ groupadd mysql
$ useradd mysql -g mysql

2. 创建目录并授权

1
2
3
4
$ mkdir data mysql-files
$ chmod 750 mysql-files
$ chown -R mysql .
$ chgrp -R mysql .

3. 初始化MySQL

1
$ bin/mysqld --initialize --user=mysql # MySQL 5.7.6 and up

注意密码

4. mysql 临时密码

[注意]root@localhost生成临时密码:;b;s;)/rn6A3,也就是root@localhost:后的字符串

1
2
3
4
5
6
2017-08-26T03:23:35.368366Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2017-08-26T03:23:35.748679Z 0 [Warning] InnoDB: New log files created, LSN=45790
2017-08-26T03:23:35.793190Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.
2017-08-26T03:23:35.848286Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: f210c54b-8a0d-11e7-abbd-000c29129bb0.
2017-08-26T03:23:35.848889Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.
2017-08-26T03:23:35.849421Z 1 [Note] A temporary password is generated for root@localhost: ;b;s;)/rn6A3

5. 生成RSA私钥,可以跳过此步骤

mysql_ssl_rsa_setup需要openssl支持,用于启用数据量ssl连接,需要进一步配置。

参考-MySQL 使用 SSL 连接

1
$ bin/mysql_ssl_rsa_setup 

6. 授予读写权限

1
2
$ chown -R root .
$ chown -R mysql data mysql-files

7. 添加到MySQL 启动脚本到系统服务

1
$ cp support-files/mysql.server /etc/init.d/mysql.server

3.启动MySQL服务

启动脚本有两个分别是:

/usr/local/mysql/bin/mysqld_safe
/usr/local/mysql/support-files/mysql.server(即/etc/init.d/mysqld

当启动mysqld时,mysqld_safe同时启动

mysqld_safe监控mysqld服务,记录错误日志,并在mysqld因故障停止时将其重启

启动方式一

1
$ bin/mysqld_safe --user=mysql &

启动方式二

1
$ service mysql.server start

或者

1
/usr/local/mysql/support-files/mysql.server start

如若出现报错

1
2
Starting MySQL.2017-08-26T07:31:24.312411Z mysqld_safe error: log-error set to '/var/log/mariadb/mariadb.log', however file don't exists. Create writable for user 'mysql'.
ERROR! The server quit without updating PID file (/var/lib/mysql/node1.pid).

给日志目录授予读写权限

1
2
3
$ mkdir /var/log/mariadb
$ touch /var/log/mariadb/mariadb.log
$ chown -R mysql:mysql /var/log/mariadb

4.登录MySQL

1
2
$ /usr/local/mysql/bin/mysql -uroot -p
Enter password:

如果不知道密码
密码在,安装MySQL步骤 4 ,有提到,怎么找初始化临时密码

如若出现报错

1
2
Enter password: 
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

故障分析

查看mysql实例的状态

1
2
$ netstat -ntlp  | grep 3306
tcp6 0 0 :::3306 :::* LISTEN 10794/mysqld

查看my.cnf关于socket的配置

1
2
$ more /etc/my.cnf |grep sock
socket=/var/lib/mysql/mysql.sock

解决方法,修改/etc/my.cnf

1
$ vi /etc/my.cnf

修改 [mysqld]组下的 socket 路径,我是选择注释掉,加一行为tmp/mysql.soc

1
2
3
4
[mysqld]
datadir=/var/lib/mysql
#socket=/var/lib/mysql/mysql.sock
socket=/tmp/mysql.sock

重启MySQL 服务

1
2
$ service mysql.server start
Shutting down MySQL.. SUCCESS!

再次登录

1
$ /usr/local/mysql/bin/mysql -uroot -p

如果不知道密码
密码在,安装MySQL步骤 4 ,有提到,怎么找初始化临时密码

设置MySQL密码

登陆成功后,设置MySQL密码

1
mysql> ALTER USER   'root'@'localhost' identified by 'mima';  

或者

1
mysql> set password=password("mima");

刷新权限

1
2
mysql> flush privileges;
mysql> exit;

查看mysql.user表中存在哪些帐户 以及它们的密码是否为空:

MySQL 5.7.6起,使用这个语句:

1
2
3
4
5
6
7
8
mysql> SELECT User, Host, HEX(authentication_string) FROM mysql.user;
+---------------+-----------+------------------------------------------------------------------------------------+
| User | Host | HEX(authentication_string) |
+---------------+-----------+------------------------------------------------------------------------------------+
| root | localhost | 2A39383730334637413534333934344644333831383037373636394637344436303631364442324338 |
| mysql.session | localhost | 2A5448495349534E4F544156414C494450415353574F52445448415443414E42455553454448455245 |
| mysql.sys | localhost | 2A5448495349534E4F544156414C494450415353574F52445448415443414E42455553454448455245 |
+---------------+-----------+------------------------------------------------------------------------------------+

开启远程登录

关闭防火墙

1
$ systemctl stop firewalld.service

以权限用户root登录

1
$ /usr/local/mysql/bin/mysql -uroot -p
1
2
3
mysql> use mysql;
mysql> update user set host = '%' where user ='root';
mysql> flush privileges;

第1行:选择mysql库
第2行:修改host值(以通配符%的内容增加主机/IP地址),当然也可以直接增加IP地址
第3行:刷新MySQL的系统权限相关表

或者

1
2
mysql> grant all privileges on *.*  to  'root'@'%'  identified by 'mima'  with grant option;
mysql> flush privileges;

推荐阅读

CentOs7.3 搭建 MySQL 5.7.19 主从复制,以及复制实现细节分析

留言與分享

git教程

分類 devops, git
初始化仓库:
1
git init

如果当前目录下有几个文件想要纳入版本控制,需要先用 git add 命令告诉 Git 开始对这些文件进行跟踪,然后提交:

1
2
3
$ git add *.c
$ git add README
$ git commit -m 'initial project version'

image

忽略某些文件

我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件模式。
文件 .gitignore 的格式规范如下:

  • 所有空行或者以注释符号 # 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配。
  • 匹配模式最后跟反斜杠(/)说明要忽略的是目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。

所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。星号(*)匹配零个或多个任意字符;[abc] 匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。

我们再看一个 .gitignore 文件的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 此为注释 – 将被 Git 忽略
# 忽略所有 .a 结尾的文件
*.a
# 但 lib.a 除外
!lib.a
# 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
/TODO
# 忽略 build/ 目录下的所有文件
build/
# 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
doc/*.txt
# 忽略 doc/ 目录下所有扩展名为 txt 的文件
doc/**/*.txt
跟踪新文件

使用命令 git add 开始跟踪一个新文件。所以,要跟踪 README 文件,运行:

1
$ git add README
提交更新

每次准备提交前,先用 git status 看下,是不是都已暂存起来了,然后再运行提交命令 git commit:

1
$ git commit

这种方式会启动文本编辑器以便输入本次提交的说明。(默认会启用 shell 的环境变量 $EDITOR 所指定的软件,一般都是 vim 或 emacs。当然也可以按照第一章介绍的方式,使用 ++git config --global core.editor++ 命令设定你喜欢的编辑软件。)

默认的提交消息包含最后一次运行 git status 的输出,放在注释行里,另外开头还有一空行,供你输入提交说明。你完全可以去掉这些注释行,不过留着也没关系,多少能帮你回想起这次更新的内容有哪些。(如果觉得这还不够,可以用 -v 选项将修改差异的每一行都包含到注释中来。)退出编辑器时,Git 会丢掉注释行,将说明内容和本次更新提交到仓库。

另外也可以用 +±m++ 参数后跟提交说明的方式,在一行命令中提交更新:

跳过使用暂存区域
Git 提供了一个跳过使用暂存区域的方式,只要在提交的时候,给 git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤

移除文件

要从 Git 中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。可以用 git rm 命令完成此项工作,并连带从工作目录中删除指定的文件,这样以后就不会出现在未跟踪文件清单中了。

移动文件

要在 Git 中对文件改名,可以这么做:

1
$ git mv file_from file_to

运行 git mv 就相当于运行了下面三条命令:

1
2
3
$ mv README.txt README
$ git rm README.txt
$ git add README
查看提交历史

默认不用任何参数的话,git log 会按提交时间列出所有的更新,最近的更新排在最上面。看到了吗,每次更新都有一个 SHA-1 校验和、作者的名字和电子邮件地址、提交时间,最后缩进一个段落显示提交说明。

git log 有许多选项可以帮助你搜寻感兴趣的提交,接下来我们介绍些最常用的。

我们常用 -p 选项展开显示每次提交的内容差异,用 -2 则仅显示最近的两次更新

某些时候,单词层面的对比,比行层面的对比,更加容易观察。Git 提供了 +±-word-diff++ 选项。我们可以将其添加到 ++git log -p++ 命令的后面,从而获取单词层面上的对比。在程序代码中进行单词层面的对比常常是没什么用的。不过当你需要在书籍、论文这种很大的文本文件上进行对比的时候,这个功能就显出用武之地了。

另外,git log 还提供了许多摘要选项可以用,比如 --stat,仅显示简要的增改行数统计

用 oneline 或 format 时结合 --graph 选项,可以看到开头多出一些 ASCII 字符串表示的简单图形,形象地展示了每个提交所在的分支及其分化衍合情况。

另外还有按照时间作限制的选项,比如 --since 和 --until。

撤消操作

有时候我们提交完了才发现漏掉了几个文件没有加,或者提交信息写错了。想要撤消刚才的提交操作,可以使用 --amend 选项重新提交:

1
$ git commit --amend

撤消操作

远程仓库的使用

查看当前的远程库
要查看当前配置有哪些远程仓库,可以用 git remote 命令,它会列出每个远程库的简短名字。
也可以加上 -v 选项(译注:此为 --verbose 的简写,取首字母),显示对应的克隆地址

添加远程仓库

要添加一个新的远程仓库,可以指定一个简单的名字,以便将来引用,运行

1
git remote add [shortname] [url]:

现在可以用字符串 pb 指代对应的仓库地址了。比如说,要抓取所有 Paul 有的,但本地仓库没有的信息,可以运行 git fetch pb

从远程仓库抓取数据

正如之前所看到的,可以用下面的命令从远程仓库抓取数据到本地:

1
$ git fetch [remote-name]

推送数据到远程仓库

目进行到一个阶段,要同别人分享目前的成果,可以将本地仓库中的数据推送到远程仓库。实现这个任务的命令很简单:

1
git push [remote-name] [branch-name]。

如果要把本地的 master 分支推送到 origin 服务器上(再次说明下,克隆操作会自动使用默认的 master 和 origin 名字),可以运行下面的命令:

1
$ git push origin master

查看远程仓库信息
我们可以通过命令

1
git remote show [remote-name]

查看某个远程仓库的详细信息

远程仓库的删除和重命名
在新版 Git 中可以用 git remote rename 命令修改某个远程仓库在本地的简称,比如想把 pb 改成 paul,可以这么运行:

1
2
3
4
$ git remote rename pb paul
$ git remote
origin
paul

碰到远端仓库服务器迁移,或者原来的克隆镜像不再使用,又或者某个参与者不再贡献代码,那么需要移除对应的远端仓库,可以运行 git remote rm 命令:

1
2
3
$ git remote rm paul
$ git remote
origin
打标签

打标签

Git 命令别名

Git 并不会推断你输入的几个字符将会是哪条命令,不过如果想偷懒,少敲几个命令的字符,可以用 git config 为命令设置别名。来看看下面的例子:

1
2
3
4
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status

取消暂存文件时的输入比较繁琐,可以自己设置一下:

1
$ git config --global alias.unstage 'reset HEAD --'

这样一来,下面的两条命令完全等同:

1
2
$ git unstage fileA
$ git reset HEAD fileA

显然,使用别名的方式看起来更清楚。另外,我们还经常设置 last 命令:

1
$ git config --global alias.last 'log -1 HEAD'

然后要看最后一次的提交信息,就变得简单多了

分支的新建与切换

要新建并切换到该分支,运行 git checkout 并加上 -b 参数:

1
2
$ git checkout -b iss53
Switched to a new branch 'iss53'

回到 master 分支并把它合并进来,然后发布到生产服务器。用 git merge 命令来进行合并:

1
2
3
4
5
6
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
README | 1 -
1 file changed, 1 deletion(-)

这相当于执行下面这两条命令:

1
2
$ git branch iss53
$ git checkout iss53

如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。

合并之后,master 分支和 hotfix 分支指向同一位置。
由于当前 hotfix 分支和 master 都指向相同的提交对象,所以 hotfix 已经完成了历史使命,可以删掉了。使用 git branch 的 -d 选项执行删除操作:

1
2
$ git branch -d hotfix
Deleted branch hotfix (was 3a0874c).
遇到冲突时的分支合并

有时候合并操作并不会如此顺利。如果在不同的分支中都修改了同一个文件的同一部分,Git 就无法干净地把两者合到一起(译注:逻辑上说,这种问题只能由人来裁决。)。
Git 作了合并,但没有提交,它会停下来等你解决冲突。要看看哪些文件在合并时发生冲突,可以用 git status 查阅

任何包含未解决冲突的文件都会以未合并(unmerged)的状态列出。Git 会在有冲突的文件里加入标准的冲突解决标记,可以通过它们来手工定位并解决这些冲突。可以看到此文件包含类似下面这样的部分:

1
2
3
4
5
6
7
<<<<<<< HEAD
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53

可以看到 ======= 隔开的上半部分,是 HEAD(即 master 分支,在运行 merge 命令时所切换到的分支)中的内容,下半部分是在 iss53 分支中的内容。解决冲突的办法无非是二者选其一或者由你亲自整合到一起。比如你可以通过把这段内容替换为下面这样来解决:

1
2
3
<div id="footer">
please contact us at email.support@github.com
</div>

这个解决方案各采纳了两个分支中的一部分内容,而且我还删除了 <<<<<<<,======= 和 >>>>>>> 这些行。在解决了所有文件里的所有冲突后,运行 git add 将把它们标记为已解决状态(译注:实际上就是来一次快照保存到暂存区域。)。

再运行一次 git status 来确认所有冲突都已解决:

1
2
3
4
5
6
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: index.html

如果觉得满意了,并且确认所有冲突都已解决,也就是进入了暂存区,就可以用 git commit 来完成这次合并提交。提交的记录差不多是这样:

1
2
3
4
5
6
7
8
9
10
Merge branch 'iss53'

Conflicts:
index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
# .git/MERGE_HEAD
# and try again.
#
查看当前的远程库

要查看当前配置有哪些远程仓库,可以用 git remote 命令,它会列出每个远程库的简短名字。
可以加上 -v 选项(译注:此为 --verbose 的简写,取首字母),显示对应的克隆地址

添加远程仓库

要添加一个新的远程仓库,可以指定一个简单的名字,以便将来引用,运行

1
git remote add [shortname] [url]:
从远程仓库抓取数据

正如之前所看到的,可以用下面的命令从远程仓库抓取数据到本地:

1
$ git fetch [remote-name]

推送数据到远程仓库
项目进行到一个阶段,要同别人分享目前的成果,可以将本地仓库中的数据推送到远程仓库。实现这个任务的命令很简单: git push [remote-name] [branch-name]。如果要把本地的 master 分支推送到 origin 服务器上(再次说明下,克隆操作会自动使用默认的 master 和 origin 名字),可以运行下面的命令:

1
$ git push origin master

只有在所克隆的服务器上有写权限,或者同一时刻没有其他人在推数据,这条命令才会如期完成任务。如果在你推数据前,已经有其他人推送了若干更新,那你的推送操作就会被驳回。你必须先把他们的更新抓取到本地,合并到自己的项目中,然后才可以再次推送。

查看远程仓库信息
我们可以通过命令

1
git remote show [remote-name]

查看某个远程仓库的详细信息

远程仓库的删除和重命名
在新版 Git 中可以用 git remote rename 命令修改某个远程仓库在本地的简称,比如想把 pb 改成 paul,可以这么运行:

1
2
3
4
$ git remote rename pb paul
$ git remote
origin
paul

注意,对远程仓库的重命名,也会使对应的分支名称发生变化,原来的 pb/master 分支现在成了 paul/master。

碰到远端仓库服务器迁移,或者原来的克隆镜像不再使用,又或者某个参与者不再贡献代码,那么需要移除对应的远端仓库,可以运行 git remote rm 命令:

1
2
3
$ git remote rm paul
$ git remote
origin
远程分支

我们用 ==(远程仓库名)/(分支名)== 这样的形式表示远程分支。比如我们想看看上次同 origin 仓库通讯时 master 分支的样子,就应该查看 origin/master 分支。如果你和同伴一起修复某个问题,但他们先推送了一个 iss53 分支到远程仓库,虽然你可能也有一个本地的 iss53 分支,但指向服务器上最新更新的却应该是 origin/iss53 分支。

可以运行 git fetch origin 来同步远程服务器上的数据到本地。该命令首先找到 origin 是哪个服务器(本例为 git.ourcompany.com),从上面获取你尚未拥有的数据,更新你本地的数据库,然后把 origin/master 的指针移到它最新的位置上
image

为了演示拥有多个远程分支(在不同的远程服务器上)的项目是如何工作的,我们假设你还有另一个仅供你的敏捷开发小组使用的内部服务器 git.team1.ourcompany.com。可以用第二章中提到的 git remote add 命令把它加为当前项目的远程分支之一。我们把它命名为 teamone,以便代替完整的 Git URL 以方便使用
image
现在你可以用 git fetch teamone 来获取小组服务器上你还没有的数据了。由于当前该服务器上的内容是你 origin 服务器上的子集,Git 不会下载任何数据,而只是简单地创建一个名为 teamone/master 的远程分支,指向 teamone 服务器上 master 分支所在的提交对象 31b8e(见图 3-26)。

image

推送本地分支

要想和其他人分享某个本地分支,你需要把它推送到一个你拥有写权限的远程仓库。你创建的本地分支不会因为你的写入操作而被自动同步到你引入的远程服务器上,你需要明确地执行推送分支的操作。

如果你有个叫 serverfix 的分支需要和他人一起开发,可以运行 git push (远程仓库名) (分支名):

1
2
3
4
5
6
7
$ git push origin serverfix
Counting objects: 20, done.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (15/15), 1.74 KiB, done.
Total 15 (delta 5), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
* [new branch] serverfix -> serverfix

这里其实走了一点捷径。Git 自动把 serverfix 分支名扩展为 refs/heads/serverfix:refs/heads/serverfix,意为“取出我在本地的 serverfix 分支,推送到远程仓库的 serverfix 分支中去”。

也可以运行 ==git push origin serverfix:serverfix== 来实现相同的效果,它的意思是“上传我本地的 serverfix 分支到远程仓库中去,仍旧称它为 serverfix 分支”。通过此语法,你可以把本地分支推送到某个命名不同的远程分支:若想把远程分支叫作 awesomebranch,可以用 git push origin serverfix:awesomebranch 来推送数据。

跟踪远程分支

从远程分支 checkout 出来的本地分支,称为 跟踪分支 (tracking branch)。跟踪分支是一种和某个远程分支有直接联系的本地分支。在跟踪分支里输入 git push,Git 会自行推断应该向哪个服务器的哪个分支推送数据。同样,在这些分支里运行 git pull 会获取所有远程索引,并把它们的数据都合并到本地分支中来。

在克隆仓库时,Git 通常会自动创建一个名为 master 的分支来跟踪 origin/master。这正是 git push 和 git pull 一开始就能正常工作的原因。当然,你可以随心所欲地设定为其它跟踪分支,比如 origin 上除了 master 之外的其它分支。刚才我们已经看到了这样的一个例子:git checkout -b [分支名] [远程名]/[分支名]。如果你有 1.6.2 以上版本的 Git,还可以用 --track 选项简化:

1
2
3
$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

要为本地分支设定不同于远程分支的名字,只需在第一个版本的命令里换个名字:

1
2
3
$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch serverfix from origin.
Switched to a new branch 'sf'

现在你的本地分支 sf 会自动将推送和抓取数据的位置定位到 origin/serverfix 了

删除远程分支

如果不再需要某个远程分支了,比如搞定了某个特性并把它合并进了远程的 master 分支(或任何其他存放稳定代码的分支),可以用这个非常无厘头的语法来删除它:git push [远程名] :[分支名]。如果想在服务器上删除 serverfix 分支,运行下面的命令:

1
2
3
$ git push origin :serverfix
To git@github.com:schacon/simplegit.git
- [deleted] serverfix

咚!服务器上的分支没了。你最好特别留心这一页,因为你一定会用到那个命令,而且你很可能会忘掉它的语法。有种方便记忆这条命令的方法:==记住我们不久前见过的 git push [远程名] [本地分支]:[远程分支] 语法,如果省略 [本地分支],那就等于是在说“在这里提取空白然后把它变成[远程分支]”==。

解决远程分支和本地冲突

1.先将本地修改储存起来

1
git stash

2.pull内容

1
git pull

3.还原暂存内容

1
git stash pop stash@{0}

4.解决冲突

5.提交

1
git commit -m ''
  1. 打标签
1
git tag v1.1 -m ''

7.列标签

1
git tag -l

8.获取标签代码

1
git checkout 

9.提交标签到远程端

1
git push tag 
拉取本地没有的远程分支
1
git checkout -b 本地分支名 origin/远程分支名

后续:变基rebase

留言與分享

Scala-2.13.0 安装及配置

分類 编程语言, scala

Scala 简介

Scala 是一门多范式(multi-paradigm)的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。

Scala 运行在Java虚拟机上,并兼容现有的Java程序。

Scala 源代码被编译成Java字节码,所以它可以运行于JVM之上,并可以调用现有的Java类库。

准备工作

环境

1
2
JDK:1.8  
Scala:Scala-2.13.0

依赖环境

Scala 语言可以运行在Window、Linux、Unix、 Mac OS X等系统上。

Scala是基于java之上,大量使用java的类库和变量,必须使用Scala之前必须先安装 Java

安装 JDK

CentOs7.3 安装 JDK1.8

安装

确保你本地以及安装了 JDK 1.8 以上版本,并且设置了 JAVA_HOME 环境变量及 JDK 的bin目录。

下载解压

解压缩文件包,可将其移动至/lib/下:

1
2
3
4
5
su hadoop
cd /home/hadoop/
wget https://downloads.lightbend.com/scala/2.13.0-M2/scala-2.13.0-M2.tgz
sudo tar -zxvf scala-2.13.0-M2.tgz -C /lib/
sudo mv /lib/scala-2.13.0-M2/ /lib/scala

环境变量

如果是对所有的用户都生效就修改vi /etc/profile 文件
如果只针对当前用户生效就修改 vi ~/.bahsrc 文件

1
sudo vi /etc/profile
1
2
3
#scala
export SCALA_HOME=/lib/scala
export PATH=${SCALA_HOME}/bin:$PATH

使环境变量生效,运行 source /etc/profile使/etc/profile文件生效

验证

:wq!保存退出,重启终端,执行 scala 命令,输出以下信息,表示安装成功:

1
scala
1
2
3
4
5
6
Welcome to Scala 2.13.0-M2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_144).
Type in expressions for evaluation. Or try :help.

scala> scala> println("Hello World!")
Hello World!
scala>

留言與分享

Hadoop简介

2003-2004年,Google公开了部分GFS和Mapreduce思想的细节,以此为基础Doug Cutting等人用了2年业余时间实现了DFS和Mapreduce机制,一个微缩版:Nutch

Hadoop 于 2005 年秋天作为 Lucene的子项目 Nutch的一部分正式引入Apache基金会。2006 年 3 月份,Map-Reduce 和 Nutch Distributed File System (NDFS) 分别被纳入称为 Hadoop 的项目中

Hadoop

分布式存储系统HDFS (Hadoop Distributed File System )

  • 分布式存储系统
  • 提供了 高可靠性、高扩展性和高吞吐率的数据存储服务

分布式计算框架MapReduce

  • 分布式计算框架
  • 具有 易于编程、高容错性和高扩展性等优点。

HDFS优点

高容错性

  • 数据自动保存多个副本
  • 副本丢失后,自动恢复

适合批处理

  • 移动计算而非数据
  • 数据位置暴露给计算框架

适合大数据处理

  • GB 、TB 、甚至PB 级数据
  • 百万规模以上的文件数量
  • 10K+ 节点

可构建在廉价机器上

  • 通过多副本提高可靠性
  • 提供了容错和恢复 机制

HDFS缺点

低延迟数据访问

  • 比如毫秒级
  • 低延迟与高吞吐率

小文件存取

  • 占用NameNode 大量内存
  • 寻道时间超过读取时间

并发写入、文件随机修改

  • 一个文件只能有一个写者
  • 仅支持append

准备工作

环境

1
2
3
4
5
6
7
JDK:1.8  
Hadoop Release:2.7.4
centos:7.3

node1(master) 主机: 192.168.252.121
node2(slave1) 从机: 192.168.252.122
node3(slave2) 从机: 192.168.252.123

安装 JDK

CentOs7.3 安装 JDK1.8

SSH 免秘钥

CentOs7.3 Hadoop 用户 ssh 免密登录

创建用户

建议创建一个单独的用户Hadoop以从Unix文件系统隔离Hadoop文件系统

1
2
3
4
$ useradd hadoop
$ passwd hadoop
New password:
Retype new password:

授权 root 权限,在root下面加一条hadoophadoop ALL=(ALL) ALL

1
2
3
4
5
$ chmod 777 /etc/sudoers
$ vi /etc/sudoers
root ALL=(ALL) ALL
hadoop ALL=(ALL) NOPASSWD:ALL
$ pkexec chmod 0440 /etc/sudoers

免秘钥登录

安装

下载解压

在 ndoe1 上操作

1
2
3
4
su hadoop
cd /home/hadoop/
wget https://mirrors.tuna.tsinghua.edu.cn/apache/hadoop/common/hadoop-2.7.4/hadoop-2.7.4.tar.gz
tar -zxvf hadoop-2.7.4.tar.gz

环境变量

如果是对所有的用户都生效就修改vi /etc/profile 文件
如果只针对当前用户生效就修改 vi ~/.bahsrc 文件

1
sudo vi /etc/profile
1
2
3
#hadoop
export PATH=${HADOOP_HOME}/bin:$PATH
export HADOOP_HOME=/home/hadoop/hadoop-2.7.4/

使环境变量生效,运行 source /etc/profile使/etc/profile文件生效

配置Hadoop

进入hadoop 配置文件目录

1
cd /home/hadoop/hadoop-2.7.4/etc/hadoop/

编辑 hadoop-env.sh 文件,找到 JAVA_HOME 改为 JDK 的安装目录

1
sudo vi hadoop-env.sh
1
export JAVA_HOME=/lib/jvm

修改 core-site.xml

打开 core-site.xml文件并对其进行编辑,如下图所示。

1
vi core-site.xml
1
2
3
4
5
6
7
8
9
10
<configuration>
<property>
<name>fs.defaultFS</name>
<value>hdfs://node1:9000</value>
</property>
<property>
<name>hadoop.tmp.dir</name>
<value>file:/home/hadoop/hadoop-2.7.4/tmp</value>
</property>
</configuration>

修改 hdfs-site.xml

打开hdfs-site.xml文件并对其进行编辑,如下图所示。

1
vi hdfs-site.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<configuration>
<property>
<name>dfs.namenode.secondary.http-address</name>
<value>node1:50090</value>
</property>
<property>
<name>dfs.replication</name>
<value>2</value>
</property>
<property>
<name>dfs.namenode.name.dir</name>
<value>file:/home/hadoop/hadoop-2.7.4/tmp/dfs/name</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>file:/home/hadoop/hadoop-2.7.4/tmp/dfs/data</value>
</property>
</configuration>

修改 mapred-site.xml

目录下么没有这个文件,这有一个模板,我们需要先拷贝一份

1
2
cp mapred-site.xml.template mapred-site.xml
vi mapred-site.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
<property>
<name>mapreduce.jobhistory.address</name>
<value>node1:10020</value>
</property>
<property>
<name>mapreduce.jobhistory.webapp.address</name>
<value>node1:19888</value>
</property>
</configuration>

修改 yarn-site.xml

1
vi yarn-site.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<configuration>

<!-- Site specific YARN configuration properties -->

<property>
<name>yarn.resourcemanager.hostname</name>
<value>node1</value>
</property>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
</configuration>

配置集群

复制节点

将 hadoop-2.7.4 文件夹重打包后复制到其他子节点

1
2
3
4
5
cd /home/hadoop/

tar zcvf hadoop.tar.gz hadoop-2.7.4
scp hadoop.tar.gz hadoop@node2:/home/hadoop/
scp hadoop.tar.gz hadoop@node3:/home/hadoop/

在其他子节点 解压

1
tar -zxvf hadoop.tar.gz

配置slaves文件

修改(Master主机)node1/etc/hadoop/slaves该文件指定哪些服务器节点是datanode节点。删除locahost,添加所有datanode节点的主机名

1
2
3
4
5
cd /home/hadoop/hadoop-2.7.4/etc/hadoop/

[hadoop@node1 hadoop]$ cat slaves
node2
node3

集群操作

Format

格式化namenode和datanode并启动,(在master上执行就可以了 不需要在slave上执行)

1
2
3
4
cd /home/hadoop/hadoop-2.7.4/bin

./hadoop namenode -format
./hadoop datanode -format

启动 hadoop

关闭防火墙

1
systemctl stop firewalld.service
1
2
3
4
5
6
cd /home/hadoop/hadoop-2.7.4/sbin


./start-dfs.sh
./start-yarn.sh
./mr-jobhistory-daemon.sh start historyserver

或者

1
2
3
./start-all.sh

./mr-jobhistory-daemon.sh start historyserver

查看进程服务

查看启动进程,缺少以下任一进程都表示出错

1
2
3
4
5
$ jps
2528 NameNode
2720 SecondaryNameNode
2872 ResourceManager
3151 JobHistoryServer

查看端口占用情况

1
netstat -tnlp | grep java

访问node1

1
2
3
http://192.168.252.121:50070

http://192.168.252.121:8088

图片描述

图片描述

图片描述

停止 hadoop

1
cd /home/hadoop/hadoop-2.7.4/sbin
1
./stop-all.sh

或者 jps 查看进程kill

留言與分享

iptables

分類 服务端, linux

基本操作


查看iptable版本

1
rpm -q iptables

查看iptables 规则

1
2
3
service iptables status
# 或
service -L --line-numbers

iptables的4个表分别是:

  • filter(过滤):数据包过滤/拦截,可以包含INPUT、FORWARD、OUTPUT这3个内置chain。
  • nat(地址转换):IP地址或端口号转换,可以包含PREROUTING、OUTPUT、POSTROUTING 3个内置chain,nat table在会话建立时会记录转换的对应关系,同一会话的回包和后续报文会自动地址转换,这是因为nat使用了ip_conntrack模块。
  • mangle(包管理):用来修改IP报文,可以包含PREROUTING、OUTPUT、INPUT、FORWARD、POSTROUTING 5个内置chain。
  • raw:此表的优先级高于ip_conntrack模块和其它的table,主要用于将有会话状态的连接(比如tcp)的数据包排除在会话外。可以包含POSTROUTING、OUTPUT两个内置chain。

看到这里肯定会有这样的疑问,为什么table只能包含一些而不是全部的chain呢?我想这个构架是按需设计而不是按功能设计的,尽管table不是包含所有的chain,但是每种功能的table都包含了实现这种功能所需的chain,即使包含更多的chain也是累赘或者无用的,而且实际上用起来也的确如此,够用了。

还有就是不同table生效优先顺序问题,先后优先级是这样的:

raw > mangle > nat > filter

所以,如果有filter禁止ping目的地址2.2.2.2,而nat又有策略将目的地址1.1.1.1转换成2.2.2.2,那么ping 1.1.1.1是ping不通的。

不过一般情况下filter是不会和nat的策略打起架来,比如INPUT chain能做filter,却不能做nat,PREROUTING能做nat却不能做filter,而且PREROUTING只能做目的地址转换,不会对源地址过滤的需求造成麻烦,所以通常是不会相互干扰的。

规则链(chain)

iptables内置的5个chain:PREROUTING、INPUT、OUPUT、FORWARD、POSTROUGING,这5个chain分别与netfilter中数据转发路径上的5个不同的位置挂钩,以匹配筛选不同类型的数据流,如下图所示:

其中:

  • PREROUTING链:应用于所有进入机器的ip包,包括目的地址是本机和目的地址非本机的包。
  • INPUT链:应用于所有目的是本机的包,也就是目的IP是本机接口地址,所有发给本地socket的数据都经过它。
  • OUPUT链:应用于所有由本机产生的包,所有应用程序发出的数据都经过它。
  • FORWARD链:应用于所有经过路由决策被转发的包,也就是目的地址不是本机的数据包。
  • POSTROUGING链:应用于所有发出机器的IP包,包括本机发出的和从本机转发的数据包。

策略匹配按照重上到下的顺序进行,当测试到某策略匹配时执行target并跳出,不再向下匹配,当测试到最后一条策略仍不匹配时,则采用policy指定的动作,如下图:

除了内置chain外,还可以自定义chain,自定义chain并不能利用netfilter的hook来捕捉数据包,但是可用于策略的分类,比如有3类不同的用户访问主机上的不同服务,如果所有策略都放在INPUT chain中策略会多而难以维护,这个时候就可以定义3个自定义chain,分别配置不同的策略,同时在INPUT chain中添加策略对来访者分类并将目标指向3个自定义chain。

自定义chain大显神威的地方在于动态生成策略,例如VPN服务器上,需要对不同分组的用户区别对待管理,但是用户IP是随机分配的,不能根据IP来区分用户组,这时候可以预先定义好各组chain,利用VPN服务端软件的一些钩子,当用户登陆时自动添加策略引导到自定义chain上来匹配。如果这时候没有自定义chain,那么策略的数量将是(用户数×所属组策略数),每增加一个用户,都要把所属组的全部策略添加一遍,这样大量的时间花费在策略匹配上,性能下降很快。


各指令的含义


-APPEND -A

1
iptables -A chain firewall-rule

将规则添加到末尾。

一般情况下,iptables 中最后的规则是丢弃所有数据包。

于是使用-A参数添加的规则在丢弃规则后,将不起作用。


-DELETE -D

1
2
3
4
5
iptables -D chain firewall-rule
# 删除对应规则direwall-rule的那一条
# 或
iptables -D chain rulenum
# 删除chain中编号为rulenum的那一条.1表示第一条。

-INSERT -I

1
iptables -I chain [rulenum] firewall-rule.

将firewall-rule添加为chain中的第rulenum条规则,原先的第rulenum条规则及以后的各条顺次+1。如未指定rulenum,则默认为1.


-REPLACE -R

1
iptables -R chain [rulenum] firewall-rule.

将firewall-rule添加为chain中的第rulenum条规则,原先的第rulenum条规则及以后的各条顺次+1。如未指定rulenum,则默认为1.


-LIST -L

1
iptables -L chain [rulenum] firewall-rule.

列出chain或所有chain的第rulenum条规则或所有规则,在-L后再加上–lube-numbers,则显示序号


-LIST-RULES -S

1
iptables -S chain [rulenum] 

打印全部规则


-FLUSH -F

1
iptables -F chain

情况chain(或全部chain)中的规则


-ZERO -Z

1
iptables -Z chain [rulenum] 

清空chain或所有chain的包和字节计数器


-NEW -N

1
iptables -N chain [rulenum] 

创建名称为chain的新链


-DELETE-CHAIN -X

1
iptables -X [chain] 

删除用户自定义chain或所有用户定义的chain,该指令不影响预设规则


-POLICY -P

1
iptables -P chain target 

改变chain的策略为target


-RENAME-CHAIN -E

1
iptables -E old-chain new-chain

将old-chain名更换为new-chain,使得old-chain的规则失效


各参数的含义


-P 代表协议(PROTOCOL)

  • 指明当前规则针对的传输协议(如 TCP、UDP、ICMP 等)
  • 可能的参数值有:tcp, udp, icmp, all
  • 使用 “all” 表示适用于所有协议。而如果在规则中不指定 -p 参数,则默认使用 “all” 参数。一般不使用 “all” 这个值,要么指定某个特定的协议,要么就指定 -p 参数。
  • -p 的参数值既可以用名称(如 tcp)也可以用协议对应的数值(如 6 代表 tcp 协议)
  • /etc/protocols 文件中包含了所有允许的协议名称和相应数值
  • 也可以用长的参数名 –protocol

-S 代表源地址(SOURCE)

  • 指定数据包的源地址
  • 可以是 ip 地址,或者网络地址,或者主机名(hostname)
  • 例如:-s 192.168.1.101 表示针对特定的 ip 地址
  • 对于网络掩码,使用 /mask。例如,“-s 192.168.1.0/24″ 表示网络掩码为 255.255.255.0 的所有 192.168.1.x 地址都匹配。
  • 如果不指定 -s 参数,默认匹配所有源地址
  • 也可以用长参数名 –src 或者 –source

-D 代表目的地址(DESTINATION)

  • 指定数据包的目的地址
  • 使用方式与上面的 “-s” 一样(不同之处仅在于 -s 指源,而 -d 表示目的地址)
  • 也可以用长参数名 –dst 或者 –destination

-J 代表跳转(TARGET)

  • j 的意思是 “jump”(跳转) 到目标
  • 指定当某个数据包满足该规则的时候的就跳转到的下一个处理规则,而不再顺序执行后面的规则判断
  • 可能的值有:ACCEPT, DROP, QUEUE, RETURN,分别表示接受、丢弃、进入队列,返回(跳出,通常是从某个 chain 中跳回到调用该 chain 的上层 chain)
  • 也可以跳转到某个自定义的 chain 中,使用该 chain 的名称做为跳转目标

-I 代表 IN INTERFACE(入站接口)

  • i 表示 “input interface”(输入接口,即,指定网络数据处理的网卡,一般 eth0 即表示第一块有线网卡的外网接口,lo 表示局域网接口)
  • 可以直接理解为 “-i” 表示接口。不过,-i 和 -o 都表示接口,-i 表示输入时的接口,而 -o 特指输出用的接口。
  • 指定数据包进入 INPUT、FORWARD 和 PREROUTING 链时经由的接口。
  • 例如:-i eth0 表示该规则应该针对从 eth0 接口进来的数据包。
  • 如果不指定 -i 参数,则经由系统中所有可用的接口进入的数据包都可以匹配该规则。
  • 也可以使用长参数 –in-interface

-O 代表 OUT INTERFACE(出站接口)

  • o 表示 “output interface”(出站经由接口)
  • 指定发送出去的数据包进入 INPUT、FORWARD 和 PREROUTING 链时经由的接口。
  • 如果不指定 -o 参数,则经由系统中所有可用的接口发出的数据包都可以匹配该规则。
  • 也可以使用长参数 –out-interface

其它防火墙参数选项

上面某些防火墙参数还有属于它自己的参数选项,可以与其配合使用。下面是一些常用的选项。

要使用这些参数选项,需要指定相应的参数,例如,要使用 “–sport” 选项,应该在规则中指定 “-p tcp” (或者 “-p udp”)等参数。

注意:所有这些选项前面都是 –(2个短横线 – )。

–SPORT 表示 SOURCE PORT (源端口,用于 -P TCP, 或者 -P UDP)

  • 默认匹配所有端口(未特别指定时)
  • 可以指定端口号(数字),也可以指定端口名称。例如,默认的 SSH 端口号码为 22,名称为 ssh,可以表示为 “–sport 22″ 或者 “–sport ssh”。
  • /etc/services 文件包含了所有允许的端口名称和对应的端口号码。
  • 在规则中使用号码比使用名称要好(效率高些)
  • 要匹配一个端口范围,使用英文半角冒号(:),如 22:100 匹配从 22 到 100 的所有端口号。
  • 也可以使用长名称 –source-port

–DPORT 表示 DESTINATION PORT (目的端口,-P TCP, 或者 -P UDP)

  • 与 –sport 的用法相同,区别仅在于对象是目的端口
  • 也可以使用长名称 –destination-port

–TCP-FLAGS 表示 TCP FLAGS (用于 -P TCP)

  • 可以使用英文半角逗号(,)来指定多个 TCP 状态标识
  • 可能值有:SYN, ACK, FIN, RST, URG, PSH。可以全用,也可以不用。

–ICMP-TYPE 表示 ICMP TYPE (ICMP 类型,用于 -P ICMP)

  • 当使用 icmp 协议 “-p icmp” 的时候,可以使用 “–icmp-type” 特别指定 ICMP 类型
  • 例如,使用 “–icmp-type 0″ 表示 “Echo Reply”,“–icmp-type 8″ 表示 “Echo”。

添加VIP:

1
ifconfig eth0:1 192.168.1.22 broadcast 192.168.1.2 netmask 255.255.255.0 up

留言與分享

linux基础

分類 后端, linux

Linux 基础操作笔记

1. 文件操作

1.1 创建文件

1
2
3
4
5
# 创建单个文件
touch file

# 批量创建文件(创建love_1_linux.txt到love_10_linux.txt)
touch love_{1..10}_linux.txt

1.2 文件权限查看

1
ls -l  # 详细列表显示文件权限

输出示例:

1
-rw-r--r-- 1 user group 1024 Mar 1 10:00 example.txt

权限字段说明:

1
2
[文件类型][所有者权限][组权限][其他用户权限]
d rwx r-x r-x
  • 文件类型:-普通文件,d目录,l链接文件
  • 权限组成:r读(4) + w写(2) + x执行(1)

2. 帮助系统

1
2
man <command>  # 查看命令手册
man ls # 示例:查看ls命令手册

常用操作键:

  • 空格键:向下翻页
  • q:退出手册
  • /:搜索内容

3. 用户管理

3.1 用户查看命令

1
2
3
4
whoami        # 显示当前用户名
who am i # 显示登录信息
who -m # 同"who am i"
who mom likes # 显示特定用户信息(需实际用户存在)

3.2 用户操作

1
2
3
4
5
6
7
8
9
10
11
# 创建用户(自动创建home目录)
sudo adduser lilei

# 查看已创建用户
ls /home

# 切换用户
su -l lilei # "-l"模拟完整登录环境

# 删除用户及home目录
sudo deluser lilei --remove-home

4. 用户组管理

4.1 用户组查看

1
2
3
4
5
# 查看指定用户所属组
groups shiyanlou

# 查看所有组(按字母排序)
cat /etc/group | sort

/etc/group 文件格式:

1
group_name:password:GID:user_list

4.2 用户组操作

1
2
# 添加用户到组(需要root权限)
sudo usermod -aG sudo lilei # 示例:添加sudo组

5. 权限管理

5.1 文件权限修改

1
2
3
4
5
6
7
8
# 变更文件所有者
sudo chown shiyanlou iphone6

# 变更文件所属组
sudo chgrp group_name file

# 修改所有权限(所有者/组/其他用户)
chmod 755 filename # rwxr-xr-x

5.2 权限数字表示法

权限 数字
r– 4
-w- 2
–x 1
rwx 7 (4+2+1)

6. 文件类型说明

Linux 一切皆文件,主要类型包括:

  • -:普通文件
  • d:目录文件
  • l:符号链接(相当于快捷方式)
  • c:字符设备文件
  • b:块设备文件
  • s:套接字文件
  • p:管道文件

7. 重要补充说明

  1. 目录权限特殊含义:

    • 读权限:可查看目录内容
    • 写权限:可创建/删除文件
    • 执行权限:可进入目录
  2. 新建用户默认会:

    • 创建同名用户组
    • /home下创建用户目录
    • 基本配置文件从/etc/skel复制
  3. sudo权限配置路径:

    1
    2
    /etc/sudoers
    /etc/sudoers.d/

建议后续可补充:

  • 实际权限修改案例
  • 用户组管理实验
  • sudoers文件的配置方法
  • ACL高级权限控制

留言與分享

19.python异步IO

分類 编程语言, python

在IO编程一节中,我们已经知道,CPU的速度远远快于磁盘、网络等IO。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。

在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。

因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,所以我们必须使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。

多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。

由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。

另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。

可以想象如果按普通顺序写出的代码实际上是没法完成异步IO的:

1
2
3
4
5
do_some_code()
f = open('/path/to/file', 'r')
r = f.read() # <== 线程停在此处等待IO操作结果
# IO操作完成后线程才能继续执行:
do_some_code(r)

所以,同步IO模型的代码是无法实现异步IO模型的。

异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程:

1
2
3
4
loop = get_event_loop()
while True:
event = loop.get_event()
process_event(event)

消息模型其实早在应用在桌面应用程序中了。一个GUI程序的主线程就负责不停地读取消息并处理消息。所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中,然后由GUI程序的主线程处理。

由于GUI线程处理键盘、鼠标等消息的速度非常快,所以用户感觉不到延迟。某些时候,GUI线程在一个消息处理的过程中遇到问题导致一次消息处理时间过长,此时,用户会感觉到整个GUI程序停止响应了,敲键盘、点鼠标都没有反应。这种情况说明在消息模型中,处理一个消息必须非常迅速,否则,主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。

消息模型是如何解决同步IO必须等待IO操作这一问题的呢?当遇到IO操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果。

在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。

在学习异步IO模型前,我们先来了解协程。

协程,又称微线程,纤程。英文名Coroutine。

协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。

所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。

子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如子程序A、B:

1
2
3
4
5
6
7
8
9
def A():
print('1')
print('2')
print('3')

def B():
print('x')
print('y')
print('z')

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:

1
2
3
4
5
6
1
2
x
y
3
z

但是在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些。

看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

Python对协程的支持是通过generator实现的。

在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。

但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。

来看例子:

传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。

如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'

def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()

c = consumer()
produce(c)

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意到consumer函数是一个generator,把一个consumer传入produce后:

  1. 首先调用c.send(None)启动生成器;
  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produceconsumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

最后套用Donald Knuth的一句话总结协程的特点:

“子程序就是协程的一种特例。”

参考源码

coroutine.py

asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。

asyncio的编程模型就是一个消息循环。asyncio模块内部实现了EventLoop,把需要执行的协程扔到EventLoop中执行,就实现了异步IO。

asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。

为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法asyncawait,可以让coroutine的代码更简洁易读。

asyncio实现Hello world代码如下:

1
2
3
4
5
6
7
8
9
import asyncio

async def hello():
print("Hello world!")
# 异步调用asyncio.sleep(1):
await asyncio.sleep(1)
print("Hello again!")

asyncio.run(hello())

async把一个函数变成coroutine类型,然后,我们就把这个async函数扔到asyncio.run()中执行。执行结果如下:

1
2
3
Hello!
(等待约1秒)
Hello again!

hello()会首先打印出Hello world!,然后,await语法可以让我们方便地调用另一个async函数。由于asyncio.sleep()也是一个async函数,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,就接着执行下一行语句。

asyncio.sleep(1)看成是一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行EventLoop中其他可以执行的async函数了,因此可以实现并发执行。

上述hello()还没有看出并发执行的特点,我们改写一下,让两个hello()同时并发执行:

1
2
3
4
5
6
7
8
# 传入name参数:
async def hello(name):
# 打印name和当前线程:
print("Hello %s! (%s)" % (name, threading.current_thread))
# 异步调用asyncio.sleep(1):
await asyncio.sleep(1)
print("Hello %s again! (%s)" % (name, threading.current_thread))
return name

asyncio.gather()同时调度多个async函数:

1
2
3
4
5
async def main():
L = await asyncio.gather(hello("Bob"), hello("Alice"))
print(L)

asyncio.run(main())

执行结果如下:

1
2
3
4
5
6
Hello Bob! (<function current_thread at 0x10387d260>)
Hello Alice! (<function current_thread at 0x10387d260>)
(等待约1秒)
Hello Bob again! (<function current_thread at 0x10387d260>)
Hello Alice again! (<function current_thread at 0x10387d260>)
['Bob', 'Alice']

从结果可知,用asyncio.run()执行async函数,所有函数均由同一个线程执行。两个hello()是并发执行的,并且可以拿到async函数执行的结果(即return的返回值)。

如果把asyncio.sleep()换成真正的IO操作,则多个并发的IO操作实际上可以由一个线程并发执行。

我们用asyncio的异步网络连接来获取sina、sohu和163的网站首页:

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
import asyncio

async def wget(host):
print(f"wget {host}...")
# 连接80端口:
reader, writer = await asyncio.open_connection(host, 80)
# 发送HTTP请求:
header = f"GET / HTTP/1.0\r\nHost: {host}\r\n\r\n"
writer.write(header.encode("utf-8"))
await writer.drain()

# 读取HTTP响应:
while True:
line = await reader.readline()
if line == b"\r\n":
break
print("%s header > %s" % (host, line.decode("utf-8").rstrip()))
# Ignore the body, close the socket
writer.close()
await writer.wait_closed()
print(f"Done {host}.")

async def main():
await asyncio.gather(wget("www.sina.com.cn"), wget("www.sohu.com"), wget("www.163.com"))

asyncio.run(main())

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
wget www.sohu.com...
wget www.sina.com.cn...
wget www.163.com...
(等待一段时间)
(打印出sohu的header)
www.sohu.com header > HTTP/1.1 200 OK
www.sohu.com header > Content-Type: text/html
...
(打印出sina的header)
www.sina.com.cn header > HTTP/1.1 200 OK
www.sina.com.cn header > Date: Wed, 20 May 2015 04:56:33 GMT
...
(打印出163的header)
www.163.com header > HTTP/1.0 302 Moved Temporarily
www.163.com header > Server: Cdn Cache Server V2.0
...

可见3个连接由一个线程并发执行3个async函数完成。

小结

asyncio提供了完善的异步IO支持,用asyncio.run()调度一个coroutine

在一个async函数内部,通过await可以调用另一个async函数,这个调用看起来是串行执行的,但实际上是由asyncio内部的消息循环控制;

在一个async函数内部,通过await asyncio.gather()可以并发执行若干个async函数。

参考源码

hello.py

gather.py

wget.py

asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+async函数实现多用户的高并发支持。

asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现的HTTP框架。

我们先安装aiohttp

1
$ pip install aiohttp

然后编写一个HTTP服务器,分别处理以下URL:

  • / - 首页返回Index Page
  • /{name} - 根据URL参数返回文本Hello, {name}!

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# app.py
from aiohttp import web

async def index(request):
text = "<h1>Index Page</h1>"
return web.Response(text=text, content_type="text/html")

async def hello(request):
name = request.match_info.get("name", "World")
text = f"<h1>Hello, {name}</h1>"
return web.Response(text=text, content_type="text/html")

app = web.Application()

# 添加路由:
app.add_routes([web.get("/", index), web.get("/{name}", hello)])

if __name__ == "__main__":
web.run_app(app)

直接运行app.py,访问首页:

Index

访问http://localhost:8080/Bob

Hello

使用aiohttp时,定义处理不同URL的async函数,然后通过app.add_routes()添加映射,最后通过run_app()以asyncio的机制启动整个处理流程。

参考源码

app.py

留言與分享

18.python(Web开发)

分類 编程语言, python

Web开发

最早的软件都是运行在大型机上的,软件使用者通过“哑终端”登陆到大型机上去运行软件。后来随着PC机的兴起,软件开始主要运行在桌面上,而数据库这样的软件运行在服务器端,这种Client/Server模式简称CS架构。

随着互联网的兴起,人们发现,CS架构不适合Web,最大的原因是Web应用程序的修改和升级非常迅速,而CS架构需要每个客户端逐个升级桌面App,因此,Browser/Server模式开始流行,简称BS架构。

在BS架构下,客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取Web页面,并把Web页面展示给用户即可。

当然,Web页面也具有极强的交互性。由于Web页面是用HTML编写的,而HTML具备超强的表现力,并且,服务器端升级后,客户端无需任何部署就可以使用到新的版本,因此,BS架构迅速流行起来。

今天,除了重量级的软件如Office,Photoshop等,大部分软件都以Web形式提供。比如,新浪提供的新闻、博客、微博等服务,均是Web应用。

Web应用开发可以说是目前软件开发中最重要的部分。Web开发也经历了好几个阶段:

  1. 静态Web页面:由文本编辑器直接编辑并生成静态的HTML页面,如果要修改Web页面的内容,就需要再次编辑HTML源文件,早期的互联网Web页面就是静态的;
  2. CGI:由于静态Web页面无法与用户交互,比如用户填写了一个注册表单,静态Web页面就无法处理。要处理用户发送的动态数据,出现了Common Gateway Interface,简称CGI,用C/C++编写。
  3. ASP/JSP/PHP:由于Web应用特点是修改频繁,用C/C++这样的低级语言非常不适合Web开发,而脚本语言由于开发效率高,与HTML结合紧密,因此,迅速取代了CGI模式。ASP是微软推出的用VBScript脚本编程的Web开发技术,而JSP用Java来编写脚本,PHP本身则是开源的脚本语言。
  4. MVC:为了解决直接用脚本语言嵌入HTML导致的可维护性差的问题,Web应用也引入了Model-View-Controller的模式,来简化Web开发。ASP发展为ASP.Net,JSP和PHP也有一大堆MVC框架。

目前,Web开发技术仍在快速发展中,异步开发、新的MVVM前端技术层出不穷。

Python的诞生历史比Web还要早,由于Python是一种解释型的脚本语言,开发效率高,所以非常适合用来做Web开发。

Python有上百种Web开发框架,有很多成熟的模板技术,选择Python开发Web应用,不但开发效率高,而且运行速度快。

本章我们会详细讨论Python Web开发技术。


在Web应用中,服务器把网页传给浏览器,实际上就是把网页的HTML代码发送给浏览器,让浏览器显示出来。而浏览器和服务器之间的传输协议是HTTP,所以:

  • HTML是一种用来定义网页的文本,会HTML,就可以编写网页;
  • HTTP是在网络上传输HTML的协议,用于浏览器和服务器的通信。

在举例子之前,我们需要安装Google的Chrome浏览器

为什么要使用Chrome浏览器而不是IE呢?因为我们需要在浏览器很方便地调试我们的Web应用,而Chrome提供了一套完整地调试工具,非常适合Web开发。

安装好Chrome浏览器后,打开Chrome,在菜单中选择“视图”,“开发者”,“开发者工具”,就可以显示开发者工具:

chrome-dev-tools

Elements显示网页的结构,Network显示浏览器和服务器的通信。我们点Network,确保第一个小红灯亮着,Chrome就会记录所有浏览器和服务器之间的通信:

chrome-devtools-network

当我们在地址栏输入www.sina.com.cn时,浏览器将显示新浪的首页。在这个过程中,浏览器都干了哪些事情呢?通过Network的记录,我们就可以知道。在Network中,定位到第一条记录,点击,右侧将显示Request Headers,点击右侧的view source,我们就可以看到浏览器发给新浪服务器的请求:

sina-http-request

最主要的头两行分析如下,第一行:

1
GET / HTTP/1.1

GET表示一个读取请求,将从服务器获得网页数据,/表示URL的路径,URL总是以/开头,/就表示首页,最后的HTTP/1.1指示采用的HTTP协议版本是1.1。目前HTTP协议的版本就是1.1,但是大部分服务器也支持1.0版本,主要区别在于1.1版本允许多个HTTP请求复用一个TCP连接,以加快传输速度。

从第二行开始,每一行都类似于Xxx: abcdefg

1
Host: www.sina.com.cn

表示请求的域名是www.sina.com.cn。如果一台服务器有多个网站,服务器就需要通过Host来区分浏览器请求的是哪个网站。

继续往下找到Response Headers,点击view source,显示服务器返回的原始响应数据:

sina-http-response

HTTP响应分为Header和Body两部分(Body是可选项),我们在Network中看到的Header最重要的几行如下:

1
200 OK

200表示一个成功的响应,后面的OK是说明。失败的响应有404 Not Found:网页不存在,500 Internal Server Error:服务器内部出错,等等。

1
Content-Type: text/html

Content-Type指示响应的内容,这里是text/html表示HTML网页。请注意,浏览器就是依靠Content-Type来判断响应的内容是网页还是图片,是视频还是音乐。浏览器并不靠URL来判断响应的内容,所以,即使URL是http://example.com/abc.jpg,它也不一定就是图片。

HTTP响应的Body就是HTML源码,我们在菜单栏选择“视图”,“开发者”,“查看网页源码”就可以在浏览器中直接查看HTML源码:

sina-http-source

当浏览器读取到新浪首页的HTML源码后,它会解析HTML,显示页面,然后,根据HTML里面的各种链接,再发送HTTP请求给新浪服务器,拿到相应的图片、视频、Flash、JavaScript脚本、CSS等各种资源,最终显示出一个完整的页面。所以我们在Network下面能看到很多额外的HTTP请求。

HTTP请求

跟踪了新浪的首页,我们来总结一下HTTP请求的流程:

步骤1:浏览器首先向服务器发送HTTP请求,请求包括:

方法:GET还是POSTGET仅请求资源,POST会附带用户数据;

路径:/full/url/path

域名:由Host头指定:Host: www.sina.com.cn

以及其他相关的Header;

如果是POST,那么请求还包括一个Body,包含用户数据。

步骤2:服务器向浏览器返回HTTP响应,响应包括:

响应代码:200表示成功,3xx表示重定向,4xx表示客户端发送的请求有错误,5xx表示服务器端处理时发生了错误;

响应类型:由Content-Type指定,例如:Content-Type: text/html;charset=utf-8表示响应类型是HTML文本,并且编码是UTF-8Content-Type: image/jpeg表示响应类型是JPEG格式的图片;

以及其他相关的Header;

通常服务器的HTTP响应会携带内容,也就是有一个Body,包含响应的内容,网页的HTML源码就在Body中。

步骤3:如果浏览器还需要继续向服务器请求其他资源,比如图片,就再次发出HTTP请求,重复步骤1、2。

Web采用的HTTP协议采用了非常简单的请求-响应模式,从而大大简化了开发。当我们编写一个页面时,我们只需要在HTTP响应中把HTML发送出去,不需要考虑如何附带图片、视频等,浏览器如果需要请求图片和视频,它会发送另一个HTTP请求,因此,一个HTTP请求只处理一个资源。

HTTP协议同时具备极强的扩展性,虽然浏览器请求的是http://www.sina.com.cn/的首页,但是新浪在HTML中可以链入其他服务器的资源,比如<img src="http://i1.sinaimg.cn/home/2013/1008/U8455P30DT20131008135420.png">,从而将请求压力分散到各个服务器上,并且,一个站点可以链接到其他站点,无数个站点互相链接起来,就形成了World Wide Web,简称“三达不溜”(WWW)。

HTTP格式

每个HTTP请求和响应都遵循相同的格式,一个HTTP包含Header和Body两部分,其中Body是可选的。

HTTP协议是一种文本协议,所以,它的格式也非常简单。HTTP GET请求的格式:

1
2
3
4
GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3

每个Header一行一个,换行符是\r\n

HTTP POST请求的格式:

1
2
3
4
5
6
POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3

body data goes here...

当遇到连续两个\r\n时,Header部分结束,后面的数据全部是Body。

HTTP响应的格式:

1
2
3
4
5
6
200 OK
Header1: Value1
Header2: Value2
Header3: Value3

body data goes here...

HTTP响应如果包含body,也是通过\r\n\r\n来分隔的。请再次注意,Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。

当存在Content-Encoding时,Body数据是被压缩的,最常见的压缩方式是gzip,所以,看到Content-Encoding: gzip时,需要将Body数据先解压缩,才能得到真正的数据。压缩的目的在于减少Body的大小,加快网络传输。

要详细了解HTTP协议,推荐“HTTP: The Definitive Guide”一书,非常不错,有中文译本《HTTP权威指南》。

HTML简介

网页就是HTML?这么理解大概没错。因为网页中不但包含文字,还有图片、视频、HTML5小游戏,有复杂的排版、动画效果,所以,HTML定义了一套语法规则,来告诉浏览器如何把一个丰富多彩的页面显示出来。

HTML长什么样?上次我们看了新浪首页的HTML源码,如果仔细数数,竟然有6000多行!

所以,学HTML,就不要指望从新浪入手了。我们来看看最简单的HTML长什么样:

1
2
3
4
5
6
7
8
<html>
<head>
<title>Hello</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>

可以用文本编辑器编写HTML,然后保存为hello.html,双击或者把文件拖到浏览器中,就可以看到效果:

hello.html

HTML文档就是一系列的Tag组成,最外层的Tag是<html>。规范的HTML也包含<head>...</head><body>...</body>(注意不要和HTTP的Header、Body搞混了),由于HTML是富文档模型,所以,还有一系列的Tag用来表示链接、图片、表格、表单等等。

CSS简介

CSS是Cascading Style Sheets(层叠样式表)的简称,CSS用来控制HTML里的所有元素如何展现,比如,给标题元素<h1>加一个样式,变成48号字体,灰色,带阴影:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<title>Hello</title>
<style>
h1 {
color: #333333;
font-size: 48px;
text-shadow: 3px 3px 3px #666666;
}
</style>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>

效果如下:

hello-css

JavaScript简介

JavaScript虽然名称有个Java,但它和Java真的一点关系没有。JavaScript是为了让HTML具有交互性而作为脚本语言添加的,JavaScript既可以内嵌到HTML中,也可以从外部链接到HTML中。如果我们希望当用户点击标题时把标题变成红色,就必须通过JavaScript来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<html>
<head>
<title>Hello</title>
<style>
h1 {
color: #333333;
font-size: 48px;
text-shadow: 3px 3px 3px #666666;
}
</style>
<script>
function change() {
document.getElementsByTagName('h1')[0].style.color = '#ff0000';
}
</script>
</head>
<body>
<h1 onclick="change()">Hello, world!</h1>
</body>
</html>

点击标题后效果如下:

hello-js-change-color

小结

如果要学习Web开发,首先要对HTML、CSS和JavaScript作一定的了解。HTML定义了页面的内容,CSS来控制页面元素的样式,而JavaScript负责页面的交互逻辑。

讲解HTML、CSS和JavaScript就可以写3本书,对于优秀的Web开发人员来说,精通HTML、CSS和JavaScript是必须的,这里推荐一个在线学习网站w3schools,以及一个对应的中文版本

当我们用Python或者其他语言开发Web应用时,我们就是要在服务器端动态创建出HTML,这样,浏览器就会向不同的用户显示出不同的Web页面。


了解了HTTP协议和HTML文档,我们其实就明白了一个Web应用的本质就是:

  1. 浏览器发送一个HTTP请求;
  2. 服务器收到请求,生成一个HTML文档;
  3. 服务器把HTML文档作为HTTP响应的Body发送给浏览器;
  4. 浏览器收到HTTP响应,从HTTP Body取出HTML文档并显示。

所以,最简单的Web应用就是先把HTML用文件保存好,用一个现成的HTTP服务器软件,接收用户请求,从文件中读取HTML,返回。Apache、Nginx、Lighttpd等这些常见的静态服务器就是干这件事情的。

如果要动态生成HTML,就需要把上述步骤自己来实现。不过,接受HTTP请求、解析HTTP请求、发送HTTP响应都是苦力活,如果我们自己来写这些底层代码,还没开始写动态HTML呢,就得花个把月去读HTTP规范。

正确的做法是底层代码由专门的服务器软件实现,我们用Python专注于生成HTML文档。因为我们不希望接触到TCP连接、HTTP原始请求和响应格式,所以,需要一个统一的接口,让我们专心用Python编写Web业务。

这个接口就是WSGI:Web Server Gateway Interface。

WSGI接口定义非常简单,它只要求Web开发者实现一个函数,就可以响应HTTP请求。我们来看一个最简单的Web版本的“Hello, web!”:

1
2
3
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return [b'<h1>Hello, web!</h1>']

上面的application()函数就是符合WSGI标准的一个HTTP处理函数,它接收两个参数:

  • environ:一个包含所有HTTP请求信息的dict对象;
  • start_response:一个发送HTTP响应的函数。

application()函数中,调用:

1
start_response('200 OK', [('Content-Type', 'text/html')])

就发送了HTTP响应的Header,注意Header只能发送一次,也就是只能调用一次start_response()函数。start_response()函数接收两个参数,一个是HTTP响应码,一个是一组list表示的HTTP Header,每个Header用一个包含两个strtuple表示。

通常情况下,都应该把Content-Type头发送给浏览器。其他很多常用的HTTP Header也应该发送。

然后,函数的返回值b'<h1>Hello, web!</h1>'将作为HTTP响应的Body发送给浏览器。

有了WSGI,我们关心的就是如何从environ这个dict对象拿到HTTP请求信息,然后构造HTML,通过start_response()发送Header,最后返回Body。

整个application()函数本身没有涉及到任何解析HTTP的部分,也就是说,底层代码不需要我们自己编写,我们只负责在更高层次上考虑如何响应请求就可以了。

不过,等等,这个application()函数怎么调用?如果我们自己调用,两个参数environstart_response我们没法提供,返回的bytes也没法发给浏览器。

所以application()函数必须由WSGI服务器来调用。有很多符合WSGI规范的服务器,我们可以挑选一个来用。但是现在,我们只想尽快测试一下我们编写的application()函数真的可以把HTML输出到浏览器,所以,要赶紧找一个最简单的WSGI服务器,把我们的Web应用程序跑起来。

好消息是Python内置了一个WSGI服务器,这个模块叫wsgiref,它是用纯Python编写的WSGI服务器的参考实现。所谓“参考实现”是指该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用。

运行WSGI服务

我们先编写hello.py,实现Web应用程序的WSGI处理函数:

1
2
3
4
5
# hello.py

def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return [b'<h1>Hello, web!</h1>']

然后,再编写一个server.py,负责启动WSGI服务器,加载application()函数:

1
2
3
4
5
6
7
8
9
10
11
# server.py
# 从wsgiref模块导入:
from wsgiref.simple_server import make_server
# 导入我们自己编写的application函数:
from hello import application

# 创建一个服务器,IP地址为空,端口是8000,处理函数是application:
httpd = make_server('', 8000, application)
print('Serving HTTP on port 8000...')
# 开始监听HTTP请求:
httpd.serve_forever()

确保以上两个文件在同一个目录下,然后在命令行输入python server.py来启动WSGI服务器:

wsgiref-start

注意:如果8000端口已被其他程序占用,启动将失败,请修改成其他端口。

启动成功后,打开浏览器,输入http://localhost:8000/,就可以看到结果了:

hello-web

在命令行可以看到wsgiref打印的log信息:

wsgiref-log

Ctrl+C终止服务器。

如果你觉得这个Web应用太简单了,可以稍微改造一下,从environ里读取PATH_INFO,这样可以显示更加动态的内容:

1
2
3
4
5
6
# hello.py

def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
body = '<h1>Hello, %s!</h1>' % (environ['PATH_INFO'][1:] or 'web')
return [body.encode('utf-8')]

你可以在地址栏输入用户名作为URL的一部分,将返回Hello, xxx!

hello-michael

是不是有点Web App的感觉了?

小结

无论多么复杂的Web应用程序,入口都是一个WSGI处理函数。HTTP请求的所有输入信息都可以通过environ获得,HTTP响应的输出都可以通过start_response()加上函数返回值作为Body。

复杂的Web应用程序,光靠一个WSGI函数来处理还是太底层了,我们需要在WSGI之上再抽象出Web框架,进一步简化Web开发。

参考源码

hello.py

server.py

了解了WSGI框架,我们发现:其实一个Web App,就是写一个WSGI的处理函数,针对每个HTTP请求进行响应。

但是如何处理HTTP请求不是问题,问题是如何处理100个不同的URL。

每一个URL可以对应GET和POST请求,当然还有PUT、DELETE等请求,但是我们通常只考虑最常见的GET和POST请求。

一个最简单的想法是从environ变量里取出HTTP请求的信息,然后逐个判断:

1
2
3
4
5
6
7
8
def application(environ, start_response):
method = environ['REQUEST_METHOD']
path = environ['PATH_INFO']
if method=='GET' and path=='/':
return handle_home(environ, start_response)
if method=='POST' and path='/signin':
return handle_signin(environ, start_response)
...

只是这么写下去代码是肯定没法维护了。

代码这么写没法维护的原因是因为WSGI提供的接口虽然比HTTP接口高级了不少,但和Web App的处理逻辑比,还是比较低级,我们需要在WSGI接口之上能进一步抽象,让我们专注于用一个函数处理一个URL,至于URL到函数的映射,就交给Web框架来做。

由于用Python开发一个Web框架十分容易,所以Python有上百个开源的Web框架。这里我们先不讨论各种Web框架的优缺点,直接选择一个比较流行的Web框架——Flask来使用。

用Flask编写Web App比WSGI接口简单(这不是废话么,要是比WSGI还复杂,用框架干嘛?),我们先用pip安装Flask:

1
$ pip install flask

然后写一个app.py,处理3个URL,分别是:

  • GET /:首页,返回Home
  • GET /signin:登录页,显示登录表单;
  • POST /signin:处理登录表单,显示登录结果。

注意噢,同一个URL/signin分别有GET和POST两种请求,映射到两个处理函数中。

Flask通过Python的装饰器在内部自动地把URL和函数给关联起来,所以,我们写出来的代码就像这样:

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

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def home():
return '<h1>Home</h1>'

@app.route('/signin', methods=['GET'])
def signin_form():
return '''<form action="/signin" method="post">
<p><input name="username"></p>
<p><input name="password" type="password"></p>
<p><button type="submit">Sign In</button></p>
</form>'''

@app.route('/signin', methods=['POST'])
def signin():
# 需要从request对象读取表单内容:
if request.form['username']=='admin' and request.form['password']=='password':
return '<h3>Hello, admin!</h3>'
return '<h3>Bad username or password.</h3>'

if __name__ == '__main__':
app.run()

运行python app.py,Flask自带的Server在端口5000上监听:

1
2
$ python app.py 
* Running on http://127.0.0.1:5000/

打开浏览器,输入首页地址http://localhost:5000/

flask-home

首页显示正确!

再在浏览器地址栏输入http://localhost:5000/signin,会显示登录表单:

flask-signin-form

输入预设的用户名admin和口令password,登录成功:

flask-signin-ok

输入其他错误的用户名和口令,登录失败:

flask-signin-failed

实际的Web App应该拿到用户名和口令后,去数据库查询再比对,来判断用户是否能登录成功。

除了Flask,常见的Python Web框架还有:

  • Django:全能型Web框架;
  • web.py:一个小巧的Web框架;
  • Bottle:和Flask类似的Web框架;
  • Tornado:Facebook的开源异步Web框架。

当然了,因为开发Python的Web框架也不是什么难事,我们后面也会讲到开发Web框架的内容。

小结

有了Web框架,我们在编写Web应用时,注意力就从WSGI处理函数转移到URL+对应的处理函数,这样,编写Web App就更加简单了。

在编写URL处理函数时,除了配置URL外,从HTTP请求拿到用户数据也是非常重要的。Web框架都提供了自己的API来实现这些功能。Flask通过request.form['name']来获取表单的内容。

参考源码

app.py

Web框架把我们从WSGI中拯救出来了。现在,我们只需要不断地编写函数,带上URL,就可以继续Web App的开发了。

但是,Web App不仅仅是处理逻辑,展示给用户的页面也非常重要。在函数中返回一个包含HTML的字符串,简单的页面还可以,但是,想想新浪首页的6000多行的HTML,你确信能在Python的字符串中正确地写出来么?反正我是做不到。

俗话说得好,不懂前端的Python工程师不是好的产品经理。有Web开发经验的同学都明白,Web App最复杂的部分就在HTML页面。HTML不仅要正确,还要通过CSS美化,再加上复杂的JavaScript脚本来实现各种交互和动画效果。总之,生成HTML页面的难度很大。

由于在Python代码里拼字符串是不现实的,所以,模板技术出现了。

使用模板,我们需要预先准备一个HTML文档,这个HTML文档不是普通的HTML,而是嵌入了一些变量和指令,然后,根据我们传入的数据,替换后,得到最终的HTML,发送给用户:

mvc-seq

这就是传说中的MVC:Model-View-Controller,中文名“模型-视图-控制器”。

Python处理URL的函数就是C:Controller,Controller负责业务逻辑,比如检查用户名是否存在,取出用户信息等等;

包含变量{{ name }}的模板就是V:View,View负责显示逻辑,通过简单地替换一些变量,View最终输出的就是用户看到的HTML。

MVC中的Model在哪?Model是用来传给View的,这样View在替换变量的时候,就可以从Model中取出相应的数据。

上面的例子中,Model就是一个dict

1
{ 'name': 'Michael' }

只是因为Python支持关键字参数,很多Web框架允许传入关键字参数,然后,在框架内部组装出一个dict作为Model。

现在,我们把上次直接输出字符串作为HTML的例子用高端大气上档次的MVC模式改写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask, request, render_template

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def home():
return render_template('home.html')

@app.route('/signin', methods=['GET'])
def signin_form():
return render_template('form.html')

@app.route('/signin', methods=['POST'])
def signin():
username = request.form['username']
password = request.form['password']
if username=='admin' and password=='password':
return render_template('signin-ok.html', username=username)
return render_template('form.html', message='Bad username or password', username=username)

if __name__ == '__main__':
app.run()

Flask通过render_template()函数来实现模板的渲染。和Web框架类似,Python的模板也有很多种。Flask默认支持的模板是jinja2,所以我们先直接安装jinja2:

1
$ pip install jinja2

然后,开始编写jinja2模板:

home.html

用来显示首页的模板:

1
2
3
4
5
6
7
8
<html>
<head>
<title>Home</title>
</head>
<body>
<h1 style="font-style:italic">Home</h1>
</body>
</html>

form.html

用来显示登录表单的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<title>Please Sign In</title>
</head>
<body>
{% if message %}
<p style="color:red">{{ message }}</p>
{% endif %}
<form action="/signin" method="post">
<legend>Please sign in:</legend>
<p><input name="username" placeholder="Username" value="{{ username }}"></p>
<p><input name="password" placeholder="Password" type="password"></p>
<p><button type="submit">Sign In</button></p>
</form>
</body>
</html>

signin-ok.html

登录成功的模板:

1
2
3
4
5
6
7
8
<html>
<head>
<title>Welcome, {{ username }}</title>
</head>
<body>
<p>Welcome, {{ username }}!</p>
</body>
</html>

登录失败的模板呢?我们在form.html中加了一点条件判断,把form.html重用为登录失败的模板。

最后,一定要把模板放到正确的templates目录下,templatesapp.py在同级目录下:

mvc-dir

启动python app.py,看看使用模板的页面效果:

mvc-form

通过MVC,我们在Python代码中处理M:Model和C:Controller,而V:View是通过模板处理的,这样,我们就成功地把Python代码和HTML代码最大限度地分离了。

使用模板的另一大好处是,模板改起来很方便,而且,改完保存后,刷新浏览器就能看到最新的效果,这对于调试HTML、CSS和JavaScript的前端工程师来说实在是太重要了。

在Jinja2模板中,我们用{{ name }}表示一个需要替换的变量。很多时候,还需要循环、条件判断等指令语句,在Jinja2中,用{% ... %}表示指令。

比如循环输出页码:

1
2
3
{% for i in page_list %}
<a href="/page/{{ i }}">{{ i }}</a>
{% endfor %}

如果page_list是一个list:[1, 2, 3, 4, 5],上面的模板将输出5个超链接。

除了Jinja2,常见的模板还有:

  • Mako:用<% ... %>${xxx}的一个模板;
  • Cheetah:也是用<% ... %>${xxx}的一个模板;
  • Django:Django是一站式框架,内置一个用{% ... %}{{ xxx }}的模板。

小结

有了MVC,我们就分离了Python代码和HTML代码。HTML代码全部放到模板里,写起来更有效率。

参考源码

app.zip

留言與分享

作者的圖片

Kein Chan

這是獨立全棧工程師Kein Chan的技術博客
分享一些技術教程,命令備忘(cheat-sheet)等


全棧工程師
資深技術顧問
數據科學家
Hit廣島觀光大使


Tokyo/Macau