0%

“五色令人目盲,五音令人耳聋,五味令人口爽,驰骋畋猎令人心发狂,难得之货令人行妨。
是以圣人为腹不为目,故去彼取此。”1

numpy

NumPy数组通常是由相同种类的元素组成的,既数组中的数据项的类型必须一致。
通常先要引入numpy包:

1
import numpy as np

创建一个简单的一维数据:

1
arr1 = np.arange(5)
2
arr1

输出:

1
dtype('int64')

以上数组的数据类型为int64,除了知道数据类型之外,还要注意其形状,这一点非常重要。

1
arr1

输出:

1
array([0, 1, 2, 3, 4])
1
arr1.shape

输出:

1
(5,)

如你所见,该向量有5个元素,他们的值分别是从0到4,该数组的shape属性是一个元组,存放的是数组在每一个维度的长度。

创建多维数据

数组大于一维以后,我们习惯称之为矩阵,下面创建一个2行2列的矩阵:

1
metr = np.array([1,2],[3,4])

上面的矩阵是通过向array()函数传递一个由列表组成的列表得到的,接下来,我们要逐个选择矩阵的各个元素,代码如下所示。下标从0开始,如下取出第1行第1列、第2行第2列的元素:

1
metr[0][0]
2
metr[1][1]

输出:

1
1
2
4

可见,选择数组元素是一件非常简单的事情,对于数组metr,只要通过metr[m,n]的形态,就能访问数组内的元素,其中m和n为数组元素的下标。

一维数组的切片与索引

一维NumPy数组的切片操作与Python列表的切片一样。下面先来定义包括0、1、2,直到8的一个数组,然后通过指定下标2到5来选择该数组的部分元素,这实际上就是提取数组中值为2到5的那些元素。

1
arr2 = np.arange(9)
2
arr2[2:6]

输出:

1
array([2, 3, 4, 5])

可以用下标选择元素,下标范围从0到7,并且下标每次递增2,如:

1
arr2[:7:2]

输出:

1
array([0, 2, 4, 6])

恰如使用Python那样,也可用负值下标来反转数组:

1
arr2[::-1]

输出:

1
array([8, 7, 6, 5, 4, 3, 2, 1, 0])

处理数组形态

常用的处理数组形态的方法,先记录如下几个:

  • reshape()
  • ravel()
  • flatten()
  • transpose()
  • resize()
    reshape()
    将一个由0到11构成的一维数组,改变为3行4列的二维数组:
    1
    arr3 = np.arange(12).reshape(3,4)
    2
    arr3
    输出:
    1
    array([[ 0,  1,  2,  3],
    2
           [ 4,  5,  6,  7],
    3
           [ 8,  9, 10, 11]])
    ravel()
    将多维数组变成一维数组
    将上面的二维数据再改变为一维数组:
    1
    arr3.ravel()
    输出:
    1
    array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
    flatten()
    其功能与ravel()相同。可是,flatten()返回的是真实的数组,需要分配新的内存空间;而ravel()函数返回的只是数组的视图。两者的区别在于返回拷贝(copy)还是返回视图(view),numpy.flatten()返回一份拷贝,对拷贝所做的修改不会影响(reflects)原始矩阵,而numpy.ravel()返回的是视图(view,也颇有几分C/C++引用reference的意味),会影响(reflects)原始矩阵。
    1
    arr3.flatten()
    输出:
    1
    array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
    transpose()
    转置:在线性代数中,矩阵的转置操作非常常见。转置是一种数据变换方法,对于二维表而言,转置就意味着行变成列,同时列变成行。
    如:创建一个0-11数字组成的一维数组,先reshape为3行4列的数组:
    1
    arr4 = np.arange(12).reshape(3,4)
    2
    arr4
    输出:
    1
    array([[ 0,  1,  2,  3],
    2
           [ 4,  5,  6,  7],
    3
           [ 8,  9, 10, 11]])
    然后对其进行转置:
    1
    arr4.transpose()
    输出:
    1
    array([[ 0,  4,  8],
    2
           [ 1,  5,  9],
    3
           [ 2,  6, 10],
    4
           [ 3,  7, 11]])
    resize()
    调整大小:函数resize()的作用类似于reshape(),但是会改变所作用的数组:
    如:将arr4调整为2行6列的二维数组
    1
    arr4.resize(2,6)
    2
    arr4
    输出:
    1
    array([[ 0,  1,  2,  3,  4,  5],
    2
           [ 6,  7,  8,  9, 10, 11]])

    堆叠数组

    从深度看,数组既可以横向叠放,也可以竖向叠放。涉及许下几个方法:
  • vstack()
  • dstack()
  • hstack()
  • column_stack()
  • row_stack()
  • concatenate()

先创建两个数组a 和 b,均为3行3列的二维数据:
数组a

1
a = np.arange(9).reshape(3,3)
2
a

输出a:

1
array([[0, 1, 2],
2
       [3, 4, 5],
3
       [6, 7, 8]])

数组b

1
b = a*2
2
b

输出b:

1
array([[ 0,  2,  4],
2
       [ 6,  8, 10],
3
       [12, 14, 16]])
hstack()

水平叠加:先介绍水平叠加方式,即用元组确定ndarrays数组的形状,然后交由hstack()函数来码放这些数组。

1
np.hstack((a,b))

输出:

1
array([[ 0,  1,  2,  0,  2,  4],
2
       [ 3,  4,  5,  6,  8, 10],
3
       [ 6,  7,  8, 12, 14, 16]])

将调用hstack时a和b的入参顺序换一下再看看结果:

1
np.hstack((b,a))

输出:

1
array([[ 0,  2,  4,  0,  1,  2],
2
       [ 6,  8, 10,  3,  4,  5],
3
       [12, 14, 16,  6,  7,  8]])
vstack()

垂直叠加:使用垂直叠加方法时,先要构建一个元祖,然后将元祖交给vstack()函数来码放数组。

1
np.vstack((a,b))

输出:

1
array([[ 0,  1,  2],
2
       [ 3,  4,  5],
3
       [ 6,  7,  8],
4
       [ 0,  2,  4],
5
       [ 6,  8, 10],
6
       [12, 14, 16]])

当参数axis置0时,concatenate()函数也会得到相同的效果。实际上,这是该参数的缺省值:

1
np.concatenate((a,b),axis=0)

输出:

1
array([[ 0,  1,  2],
2
       [ 3,  4,  5],
3
       [ 6,  7,  8],
4
       [ 0,  2,  4],
5
       [ 6,  8, 10],
6
       [12, 14, 16]])
dstack()

深度叠加:除此之外,还有一种深度叠加方法,这要用到dstack()函数和一个元组。这种方法是沿着第三个坐标轴(纵向)的方向来叠加一摞数组。举例来说,可以在一个图像数据的二维数组上叠加另一幅图像的数据。

1
np.dstack((a,b))

输出:

1
array([[[ 0,  0],
2
        [ 1,  2],
3
        [ 2,  4]],
4
5
       [[ 3,  6],
6
        [ 4,  8],
7
        [ 5, 10]],
8
9
       [[ 6, 12],
10
        [ 7, 14],
11
        [ 8, 16]]])

####拆分numpy数组
可以从纵向,横向和深度方向来拆分数组,相关函数有hsplit(),vsplit(),dsplit()和split()。我们即可以把数组分成相同的形状的数组,也可以从规定的位置开始切取数组。
横向拆分:对于一个3行3列数组,可以沿着横轴方向将其分解为3部分,并且各部分的大小和形状完全一致。

1
np.hsplit(a,3)

输出:

1
[array([[0],
2
        [3],
3
        [6]]), array([[1],
4
        [4],
5
        [7]]), array([[2],
6
        [5],
7
        [8]])]

这个相当于调用了参数axis = 1的split()函数:

1
np.split(a,3,1)

输出:

1
[array([[0],
2
        [3],
3
        [6]]), array([[1],
4
        [4],
5
        [7]]), array([[2],
6
        [5],
7
        [8]])]

纵向拆分:同理

1
np.vsplit(a,3)

输出:

1
[array([[0, 1, 2]]), array([[3, 4, 5]]), array([[6, 7, 8]])]

深向拆分:dsplit()函数会沿着深度方向分解数组。

后记:关于本文数据的由来

玩python,最好在本地安装Anaconda,里面有notebook的运行环境,交互式的笔记notebook是最好的工具,没有之一。

  • 下载安装anaconda,运行,如下图为主界面
    anaconda.png

  • 点击”jupyter notebook”,可以启动notebook运行环境,新建一个notebook python的文档,形如界面:
    notebook.png

  • 目前网上支持notebook最好的免费平台就是github了,将自己的notebook文档上传到github上可以进行分享。

1:老子《道德经》第十二章,老子故里,中国鹿邑。

“孔德之容,惟道是从。
道之为物,惟恍惟惚。
惚兮恍兮,其中有象;恍兮惚兮,其中有物;窈兮冥兮,其中有精。
其精甚真,其中有信。
自古及今,其名不去,以阅众甫。
吾何以知众甫之状哉?
以此。”1

Python 在处理数据、分析数据以及数据可视化方面拥有很多功能强大的工具,这也是 Python 在科学领域中能够迅速发展的一个主要原因。今天记录一下matplotlib,在数据可视化方面,你必须熟练shying它。

Matplotlib

Matplotlib 是 Python 的一个绘图库。它包含了大量的工具,你可以使用这些工具创建各种图形,包括简单的散点图,正弦曲线,甚至是三维图形。Python 科学计算社区经常使用它完成数据可视化的工作。
首先引入模块包:

1
import matplotlib.pyplot as plt

画一段正弦曲线

1
import matplotlib.pyplot as plt
2
import numpy as np
3
4
x = np.linspace(1,10,50)  #在指定的间隔内返回均匀间隔的数字。
5
plt.plot(x,np.sin(x)) #定义显示x轴,y轴数据
6
plt.show() #图显示

输出图形如下:
plt_output_sin.png

叠加绘制两个数据集

试试用上面的50个数字绘制正弦曲线和余弦曲线的叠加。

1
plt.plot(x,np.sin(x),x,np.cos(x))
2
plt.show()

输出图形如下:
plt_2layer_sinXcos.png

自定义图形的外观

当在同一个图形上展示多个数据集时,通过改变线条的外观来区分不同的数据集变得非常必要。

1
plt.plot(x,np.sin(x),'r-o',x,np.cos(x),'g--')
2
plt.show()

输出图形如下:
set_linetype.png
代码展示了两种不同的曲线样式:’r-o’ 和 ‘g–’

‘-o’ 代表包含实心点标记的实线,’–’ 代表虚线
颜色:蓝色 - ‘b’ 绿色 - ‘g’ 红色 - ‘r’ 青色 - ‘c’ 品红 - ‘m’ 黄色 - ‘y’ 黑色 - ‘k’ 白色 - ‘w’
线:直线 - ‘-‘ 虚线 - ‘–’ 点线 - ‘:’ 点划线 - ‘-.’
常用点标记:点 - ‘.’ 像素 - ‘,’ 圆 - ‘o’ 方形 - ‘s’ 三角形 - ‘^’ 更多点标记样式

处理文字

text()命令可以在任意的位置添加文字,xlabel(),ylabel(),title()分别是添加x轴,y轴标签和标题。

1
import matplotlib.pyplot as plt
2
import numpy as np
3
# Fixing random state for reproducibility
4
np.random.seed(19680801)
5
mu, sigma = 100, 15
6
x = mu + sigma * np.random.randn(10000)
7
# the histogram of the data
8
n, bins, patches = plt.hist(x, 50, normed=1, facecolor='b', alpha=0.75)
9
plt.xlabel('Smarts')
10
plt.ylabel('Probability')
11
plt.title('Histogram of IQ')
12
plt.text(60, .025, r'$\mu=100,\ \sigma=15$')
13
plt.axis([40, 160, 0, 0.03])
14
plt.grid(True)
15
plt.show()

输出图形:
plot_text_demo.png
所有的text()命令会返回一个matplotlib.text.Text实例,通过属性或者setp()来改变他们。
t = plt.xlabel(‘my data’, fontsize=14, color=’red’)
使用数学表达式

plt.title(r’$\sigma_i=15$’)

前置的r是指定它后面的字符串是原始的字符串,然后用$包裹表示中间的是数学表达式,\表示转译具体的数学符号。

注释

annotate()方法提供了一个注释的方法。
annotate(string,xy,xytest,arrowprops)

1
ax = plt.subplot(111)
2
t = np.arange(0.0, 5.0, 0.01)
3
s = np.cos(2*np.pi*t)
4
line, = plt.plot(t, s, lw=2)
5
plt.annotate('local max', xy=(2, 1), xytext=(3, 1.5),
6
arrowprops=dict(facecolor='red', shrink=0.05),
7
)
8
plt.ylim(-2,2)
9
plt.show()

输出图形:
plot_annotate.png
默认的情况下,xy和xytext使用的坐标是数据的坐标,你也可以指定其他的坐标系统。

使用子图

在一个窗口绘制多张图,将上述的叠加的用不同曲线样式的例子用子图的方式改造:

1
plt.subplot(2,1,1)
2
plt.plot(x,np.sin(x),'r-o')
3
plt.subplot(2,1,2)
4
plt.plot(x,np.cos(x),'g--')
5
plt.show()

输出图形如下:
2layer_subgraph.png
使用子图只需要一个额外的步骤,就可以像前面的例子一样绘制数据集。即在调用 plot() 函数之前需要先调用 subplot() 函数。该函数的第一个参数代表子图的总行数,第二个参数代表子图的总列数,第三个参数代表活跃区域。

活跃区域代表当前子图所在绘图区域,绘图区域是按从左至右,从上至下的顺序编号。例如在 3×3 的方格上,活跃区域 4 在方格上的坐标为 (1, 1)。

散点图

散点图是一堆离散点的集合。用 Matplotlib 画散点图也同样非常简单。
还以画正弦曲线为例:

1
plt.scatter(x,np.sin(x))
2
plt.show()

输出图形:
plt_scatter.png
只需要调用 scatter() 函数并传入两个分别代表 x 坐标和 y 坐标的数组。注意,我们通过 plot 命令并将线的样式设置为 ‘bo’ 也可以实现同样的效果。

彩色映射散点图

另一种你可能用到的图形是彩色映射散点图。这里我们会根据数据的大小给每个点赋予不同的颜色和大小,并在图中添加一个颜色栏。

1
x = np.random.rand(1000)
2
y = np.random.rand(1000)
3
size = np.random.rand(1000)*50
4
colors = np.random.rand(1000)
5
plt.scatter(x,y,size,colors)
6
plt.colorbar()
7
plt.show()

输出图形:
plt_scatter_colors.png
上面的代码大量的用到了 np.random.rand(1000),原因是我们绘图的数据都是随机产生的。

同前面一样我们用到了 scatter() 函数,但是这次我们传入了另外的两个参数,分别为所绘点的大小和颜色。通过这种方式使得图上点的大小和颜色根据数据的大小产生变化。

然后我们用 colorbar() 函数添加了一个颜色栏。

直方图

直方图是 Matplotlib 中最简单的图形之一。你只需要给 hist() 函数传入一个包含数据的数组。第二个参数代表数据容器的个数。数据容器代表不同的值的间隔,并用来包含我们的数据。数据容器越多,图形上的数据条就越多。

1
z = np.random.rand(1000)
2
plt.hist(z,50)
3
plt.show()

输出图形:
plt_column.png

添加标题、坐标轴标记和图例

数据仍然使用上述正弦曲线和余弦曲线叠加图的那个,现在把标签和纵横坐标标签以及图例加上:

1
plt.plot(x,np.sin(x),'r-o',label='Sin(x)') # 要显示图例,需定义lable参数
2
plt.plot(x,np.cos(x),'g-^',label='Cos(x)')
3
plt.legend() #图例
4
plt.xlabel('Xlabel') #x轴标签
5
plt.ylabel('Ylabel') #y轴标签
6
plt.title('my title') # 标题
7
plt.show()

输出图形:
plt_complete.png

网上这篇帖子,对matplotlib的使用整理的也很出彩,可以移步去看看:
Python–matplotlib绘图可视化知识点整理

1:老子《道德经》第二十一章,老子故里,中国鹿邑。

“天下皆知美之为美,斯恶已,皆知善之为善,斯不善已。
故有无相生,难易相成,长短相形,高下相倾,音声相和,前后相随。
是以圣人处无为之事,行不言之教,万物作焉而不辞,生而不有,为而不恃,功成而弗居。
夫惟弗居,是以不去。” ^1

本篇目录:

  • 何谓红黑树?
  • 操作分析
  • 应用场景
  • Java实现
何谓红黑树?

红黑树(red-black tree R-B Tree),是一种特殊的(自平衡)二叉树查找树,查找的时间复杂度最坏情况O(logn)。
它满足二叉查找树的特征:任意一个节点包含的键值,大于等于左孩子的键值,小于等于右孩子的键值。
RBTree.png

同时又具有如下特有属性:

  1. 每个节点或者是黑色,或者是红色。
  2. 根节点是黑色。
  3. 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
  4. 如果一个节点是红色的,则它的子节点必须是黑色的。
  5. 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

注意:

  • 特性3,中的叶子节点,是只为空(NIL或null)的节点。
  • 特性5,确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。
操作分析
基本定义
1
public class RBTree<T extends Comparable<T>> {
2
3
    private RBTNode<T> mRoot;// 根结点
4
5
    private static final boolean RED   = false;
6
    private static final boolean BLACK = true;
7
8
    public class RBTNode<T extends Comparable<T>> {
9
        boolean color; // 颜色
10
        T key;  // 关键字(键值)
11
        RBTNode<T> left; // 左孩子
12
        RBTNode<T> right; // 右孩子
13
        RBTNode<T> parent; // 父结点
14
15
        public RBTNode(T key, boolean color, RBTNode<T> parent, RBTNode<T> left, RBTNode<T> right) {
16
            this.key = key;
17
            this.color = color;
18
            this.parent = parent;
19
            this.left = left;
20
            this.right = right;
21
        }
22
23
    }
24
25
    ...
26
}

红黑树的基本操作是添加、删除和旋转。
在对红黑树进行添加或删除后,会用到旋转方法:添加或删除红黑树中的节点之后,红黑树就发生了变化,可能不满足红黑树的5条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。
简单点说,旋转的目的是让树保持红黑树的特性。
旋转包括两种:左旋 和 右旋。下面分别对红黑树的基本操作进行介绍。

左旋

对x进行左旋,意味着”将x变成一个左节点”。

1
/**
2
     * 对红黑树的节点(x)进行左旋转
3
     *
4
     * 左旋示意图(对节点x进行左旋):
5
     *      px                              px
6
     *     /                               /
7
     *    x                               y
8
     *   /  \      --(左旋)-.             / \                #
9
     *  lx   y                          x  ry
10
     *     /  \                       /    \
11
     *    ly  ry                     lx    ly
12
     *
13
     *
14
     */
右旋

对y进行左旋,意味着”将y变成一个右节点”。

1
/**
2
     * 对红黑树的节点(y)进行右旋转
3
     *
4
     * 右旋示意图(对节点y进行左旋):
5
     *            py                               py
6
     *           /                                /
7
     *          y                                x
8
     *         /  \      --(右旋)-.             /  \                     #
9
     *        x   ry                           lx   y
10
     *       / \                                   / \                   #
11
     *      lx  rx                                rx  ry
12
     *
13
     */
添加

将一个节点插入到红黑树中,需要执行哪些步骤呢?首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过”旋转和重新着色”等一系列操作来修正该树,使之重新成为一颗红黑树。详细描述如下:

  • 第一步: 将红黑树当作一颗二叉查找树,将节点插入。

    红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。

    好吧?那接下来,我们就来想方设法的旋转以及重新着色,使这颗树重新成为红黑树!

  • 第二步:将插入的节点着色为”红色”。

    为什么着色成红色,而不是黑色呢?为什么呢?在回答之前,我们需要重新温习一下红黑树的特性:

    (1) 每个节点或者是黑色,或者是红色。
    (2) 根节点是黑色。
    (3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
    (4) 如果一个节点是红色的,则它的子节点必须是黑色的。
    (5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

    将插入的节点着色为红色,不会违背"特性(5)"!少违背一条特性,就意味着我们需要处理的情况越少。接下来,就要努力的让这棵树满足其它性质即可;满足了的话,它就又是一颗红黑树了。o(∩∩)o...哈哈
  • 第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。

    第二步中,将插入节点着色为"红色"之后,不会违背"特性(5)"。那它到底会违背哪些特性呢?
    对于"特性(1)",显然不会违背了。因为我们已经将它涂成红色了。
    对于"特性(2)",显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根节点仍然是黑色。
    对于"特性(3)",显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。
    对于"特性(4)",是有可能违背的!
    那接下来,想办法使之"满足特性(4)",就可以将树重新构造成红黑树了。
    删除

    将红黑树内的某一个节点删除。需要执行的操作依次是:首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;然后,通过”旋转和重新着色”等一系列来修正该树,使之重新成为一棵红黑树。详细描述如下:

  • 第一步:将红黑树当作一颗二叉查找树,将节点删除。

    这和"删除常规二叉查找树中删除节点的方法是一样的"。分3种情况:

    ① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就OK了。
    ② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
    ③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给”被删除节点”之后,再将后继节点删除。这样就巧妙的将问题转换为”删除后继节点”的情况了,下面就考虑后继节点。 在”被删除节点”有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然”的后继节点”不可能双子都非空,就意味着”该节点的后继节点”要么没有儿子,要么只有一个儿子。若没有儿子,则按”情况① “进行处理;若只有一个儿子,则按”情况② “进行处理。

  • 第二步:通过”旋转和重新着色”等一系列来修正该树,使之重新成为一棵红黑树。

    因为"第一步"中删除节点之后,可能会违背红黑树的特性。所以需要通过"旋转和重新着色"来修正该树,使之重新成为一棵红黑树。
    应用场景

    在Jdk1.8中,HashMap最大的优化就是用红黑树替代原来的链表来解决hash键值冲突。

    Java实现

    RBTree.java

“曲则全,枉则直;洼则盈,弊则新;少则得,多则惑,是以圣人抱一为天下式。
不自见,故明;不自是,故彰;不自伐,故有功;不自矜,故长。
夫唯不争,故天下莫能与之争。
古之所谓“曲则全”者,岂虚言哉?
诚全而归之。” 1

本篇文章记录了用scikit-learn框架下以KNN对数据集进行分类预测时的一个完整步骤。

下文涉及下面内容:

  • 实例介绍
  • 数据准备
  • 选择模型
  • 训练模型
  • 测试模型
  • 保存模型
  • 用模型进行预测

实例介绍

本文从sklearn自带的dataset iris,对鸢尾花数据集进行分类。Iris数据中data存储花瓣长宽(column0,1)和花萼长宽(column2,3),target存储花的分类,Iris-setosa , Iris-versicolor , and Iris-virginica ,分别存储为数字 0,1,2

数据准备

打开sklearn官网api,sklearn.datasets里有两大模块:

  • loaders
    提供非常丰富的用于实践分类、回归、聚类的采集数据,你完全可以这些数据进行各种算法的训练。本文使用的数据就是iris。
    sklearn-datasets-loaders.png

  • Samples generator
    当你不想用sklearn自提供的采集数据,你可以用数据生成器根据场景为您生成个性的模拟数据进行算法训练。
    sklearn-datasets-generator.png

可视化数据

我们首先看看iris数据长什么样子?

1
from sklearn import datasets
2
import numpy as np
3
4
iris = datasets.load_iris()
5
X = iris.data
6
y = iris.target
7
X[:10]  # 显示前10条记录
8
y  # 类别分类

输出:

1
array([[ 5.1,  3.5,  1.4,  0.2], # 花萼长、宽 花瓣长、宽
2
       [ 4.9,  3. ,  1.4,  0.2],
3
       [ 4.7,  3.2,  1.3,  0.2],
4
       [ 4.6,  3.1,  1.5,  0.2],
5
       [ 5. ,  3.6,  1.4,  0.2],
6
       [ 5.4,  3.9,  1.7,  0.4],
7
       [ 4.6,  3.4,  1.4,  0.3],
8
       [ 5. ,  3.4,  1.5,  0.2],
9
       [ 4.4,  2.9,  1.4,  0.2],
10
       [ 4.9,  3.1,  1.5,  0.1]])
11
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
12
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
13
       0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
14
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
15
       1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
16
       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
17
       2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]) # 可见共有三个分类

首先要对数据集进行随机划分,分为训练集和测试集

1
from sklearn.model_selection import train_test_split
2
'''
3
train_data:所要划分的样本特征集
4
train_target:所要划分的样本结果
5
test_size:样本占比,如果是整数的话就是样本的数量
6
random_state:是随机数的种子。
7
随机数种子:其实就是该组随机数的编号,在需要重复试验的时候,保证得到一组一样的随机数。
8
比如你每次都填1,其他参数一样的情况下你得到的随机数组是一样的。但填0或不填,每次都会不一样。
9
'''
10
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, random_state=0)

选择模型

1
from sklearn.neighbors import KNeighborsClassifier
2
knn = KNeighborsClassifier(n_neighbors=7)  # K最近邻(k-Nearest Neighbor,KNN)

训练模型

1
knn.fit(X_train, y_train)
2
print(knn.score(X_test, y_test))  # 对模型进行验证

输出结果:

1
0.966666666667

通常在训练模型的时候,采用交叉验证会大大提高模型的准确性:

1
from sklearn.model_selection import cross_val_score
2
scores = cross_val_score(knn, X, y, cv=5, scoring='accuracy')
3
print(scores) # 对模型进行交叉验证后结果
4
print(scores.mean()) # 求平均值

输出结果:

1
[ 0.96666667  1.          0.96666667  0.96666667  1.        ]
2
0.98 # 很明显结果比没有进行交叉验证的准确性有了显著提高

保存和使用模型

1
from sklearn.externals import joblib
2
joblib.dump(knn, "/Users/song/work/ai/sklearn_practice/model/knn_test_001.pkl") # 保存
3
knn3 = joblib.load("/Users/song/work/ai/sklearn_practice/model/knn_test_001.pkl") # 使用
4
print(knn3.score(X_test, y_test))

保存模型还有另一种方式:(建议使用上一种方式)

1
import pickle
2
# 保存
3
with open('/Users/song/work/ai/sklearn_practice/model/knn_test_001.pickle', 'wb') as f:
4
    pickle.dump(knn, f)
5
# 使用
6
with open('/Users/song/work/ai/sklearn_practice/model/knn_test_001.pickle', 'rb') as f:
7
   knn3 = pickle.load(f)
8
   print(knn3.score(X_test, y_test))

1:老子《道德经》第二十二章,老子故里,中国鹿邑。

“致虚极,守静笃,万物并作,吾以观其复。
夫物芸芸,各复归其根。
归根曰静,是谓复命。
复命曰常,知常曰明,不知常,妄作,凶。
知常容,容乃公,公乃全,全乃天,天乃道,道乃久,没身不殆。” 1

Scikit-learn

官网 (scikit-learn.org)

之前在python易筋经系列中我有写过scipy的笔记2,scipy是一个开源的基于python的科学计算工具包。基于scipy,目前开发者们针对不同的应用领域已经发展出了为数众多的分支版本,它们被统一称为Scikits,即scipy工具包的意思。而在这些分支版本中,最有名,也是专门面向机器学习的一个就是Scikit-learn。

为什么还要scikit-learn?

tensorflow、pytouch所代表的深度学习框架的兴起,使初学者趋之若鹜,如果使用python作为机器学习语言首选语言的话,其实都应该静下心来好好研究一下scikit-learn。Scikit-learn针对每个算法和模块都提供了丰富的参考样例和详细的说明文档。

做了一张脑图来鸟瞰scikit-learn一下:
scikits-learn.png

六大功能

Scikit-learn的基本功能主要被分为六大部分:分类,回归,聚类,数据降维,模型选择和数据预处理。

分类

识别给定对象的所属类别,属于监督学习的范畴,最常见的应用场景包括垃圾邮件检测和图像识别等。目前Scikit-learn已经实现的算法包括:支持向量机(SVM),最近邻,逻辑回归,随机森林,决策树以及多层感知器(MLP)神经网络等等。

回归

预测与给定对象相关联的连续值属性,最常见的应用场景包括预测药物反应和预测股票价格等。目前Scikit-learn已经实现的算法包括:支持向量回归(SVR),脊回归,Lasso回归,弹性网络(Elastic Net),最小角回归(LARS ),贝叶斯回归,以及各种不同的鲁棒回归算法等。可以看到,这里实现的回归算法几乎涵盖了所有开发者的需求范围,而且更重要的是,Scikit-learn还针对每种算法都提供了简单明了的用例参考。

聚类

自动识别具有相似属性的给定对象,并将其分组为集合,属于无监督学习的范畴,最常见的应用场景包括顾客细分和试验结果分组。目前Scikit-learn已经实现的算法包括:K-均值聚类,谱聚类,均值偏移,分层聚类,DBSCAN聚类等。

数据降维

使用主成分分析(PCA)、非负矩阵分解(NMF)或特征选择等降维技术来减少要考虑的随机变量的个数,其主要应用场景包括可视化处理和效率提升。

模型选择

对于给定参数和模型的比较、验证和选择,其主要目的是通过参数调整来提升精度。目前Scikit-learn实现的模块包括:格点搜索,交叉验证和各种针对预测误差评估的度量函数。

数据预处理

数据的特征提取和归一化,是机器学习过程中的第一个也是最重要的一个环节。这里归一化是指将输入数据转换为具有零均值和单位权方差的新变量,但因为大多数时候都做不到精确等于零,因此会设置一个可接受的范围,一般都要求落在0-1之间。而特征提取是指将文本或图像数据转换为可用于机器学习的数字变量。
需要特别注意的是,这里的特征提取与上文在数据降维中提到的特征选择非常不同。特征选择是指通过去除不变、协变或其他统计上不重要的特征量来改进机器学习的一种方法。

环境安装

再次推荐Anaconda

  • 方便安装各种包,numpy、scipy、scikit-learn等,有点类似maven。
  • Jupyter Notebook 集成在anaconda中,可以方便调试程序和熟悉交互式笔记。

1 : 老子《道德经》第十六章,老子故里,中国鹿邑。
2 : 《python易筋经-scipy》

“不尚贤,使民不争;不贵难得之货,使民不为盗;不见可欲,使民心不乱。
是以圣人之治,虚其心,实其腹,弱其志,强其骨,常使民无知无欲。
使夫知者不敢为也。
为无为,则无不治。”^ddj

JVM为我们提供了很多工具,我们可以用它来监测运行中的程序,对于程序优化和问题分析很有帮助,今天就聊聊这方面的操作。

大致涉及如下内容:

  • jps
  • jstack
  • jstat
  • jmap
  • 实战

jps

jps命令可以显示当前运行java进程以及相关参数。

1
man jps  //可以命令行jps的用法说明
1
jps -q //只显示pid,不显示class名称,jar文件名和传递给main 方法的参数
1
jps -m //输出传递给main 方法的参数
1
jps -l //输出应用程序main class的完整package名 或者 应用程序的jar文件完整路径名
1
jps -v 输出传递给JVM的参数

jstack

jstack是虚拟机堆栈追踪工具,用于生成虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。

线程状态:

1
NEW 未启动的
2
RUNNABLE 在虚拟中运行的
3
BLOCKED 受阻塞并等待监视器锁
4
WAITTING 无限期等待另一个线程执行特定操作
5
TIMED_WAITTING 有限期的等待另一线程执行特定操作
6
TERMINATED 已退出的
1
jstack [options] <pid> //pid可以通过jps或者top获取,后面给出的具体分析实例会写到
1
jstack -l 长列表. 打印关于锁的附加信息,例如属于java.util.concurrent的ownable synchronizers列表.
1
jstack -m 打印java和native c/c++框架的所有栈信息.
1
jstack -h 打印帮助信息

jstat

jstat(JVM Statistics Monitoring Tool)是用于监控虚拟机各种运行状态信息的命令行工具。他可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

1
jstat [options] <pdi>
1
jstat -class <pid> 显示加载class的数量,及所占空间等信息。
2
3
结果说明:
4
Loaded 装载的类的数量
5
Bytes 装载类所占用的字节数
6
Unloaded 卸载类的数量
7
Bytes 卸载类的字节数
8
Time 装载和卸载类所花费的时间
1
jstat -gc <pid>: 可以显示gc的信息,查看gc的次数,及时间。
2
3
结果说明:
4
S0C 年轻代中第一个survivor(幸存区)的容量 (字节)
5
S1C 年轻代中第二个survivor(幸存区)的容量 (字节)
6
S0U 年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
7
S1U 年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
8
EC 年轻代中Eden(伊甸园)的容量 (字节)
9
EU 年轻代中Eden(伊甸园)目前已使用空间 (字节)
10
OC Old代的容量 (字节)
11
OU Old代目前已使用空间 (字节)
12
PC Perm(持久代)的容量 (字节)
13
PU Perm(持久代)目前已使用空间 (字节)
14
YGC 从应用程序启动到采样时年轻代中gc次数
15
YGCT 从应用程序启动到采样时年轻代中gc所用时间(s)
16
FGC 从应用程序启动到采样时old代(全gc)gc次数
17
FGCT 从应用程序启动到采样时old代(全gc)gc所用时间(s)
18
GCT 从应用程序启动到采样时gc用的总时间(s)

jmap

jmap是JDK自带的工具软件,主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。堆Dump是反应Java堆使用情况的内存镜像,其中主要包括系统信息、虚拟机属性、完整的线程Dump、所有类和对象的状态等。
常见内存错误:

1
OutOfMemoryError 年老代内存不足。
2
OutOfMemoryError:PermGen Space 永久代内存不足。
3
OutOfMemoryError:GC overhead limit exceed 垃圾回收时间占用系统运行时间的98%或以上。
1
jmap [optioins] <pid>

options

1
<no option> 如果使用不带选项参数的jmap打印共享对象映射,将会打印目标虚拟机中加载的每个共享对象的起始地址、映射大小以及共享对象文件的路径全称。这与Solaris的pmap工具比较相似。
2
-dump:[live,]format=b,file=<filename> 以hprof二进制格式转储Java堆到指定filename的文件中。live子选项是可选的。如果指定了live子选项,堆中只有活动的对象会被转储。想要浏览heap dump,你可以使用jhat(Java堆分析工具)读取生成的文件。
3
-finalizerinfo 打印等待终结的对象信息。
4
-heap 打印一个堆的摘要信息,包括使用的GC算法、堆配置信息和generation wise heap usage。
5
-histo[:live] 打印堆的柱状图。其中包括每个Java类、对象数量、内存大小(单位:字节)、完全限定的类名。打印的虚拟机内部的类名称将会带有一个’*’前缀。如果指定了live子选项,则只计算活动的对象。
6
-permstat 打印Java堆内存的永久保存区域的类加载器的智能统计信息。对于每个类加载器而言,它的名称、活跃度、地址、父类加载器、它所加载的类的数量和大小都会被打印。此外,包含的字符串数量和大小也会被打印。
7
-F 强制模式。如果指定的pid没有响应,请使用jmap -dump或jmap -histo选项。此模式下,不支持live子选项。
8
-h 打印帮助信息。
9
-help 打印帮助信息。
10
-J<flag> 指定传递给运行jmap的JVM的参数。

实战

下面以一个简单的例子,分析出运行项目中最消耗cpu的线程并定位代码。

  • 得到进程id
    jps or ps,以ps为例:
    1
    ps -ef | grep Kernel | grep -v grep
    得到进程id 即pid
    然后查找该进程id下各个线程中最消耗的那个线程tid,用top指令:
    1
    top -Hp <pid>
    得到线程id 即tid,由于jstack中tid和nid都是十六进制的,因此需要将得到的tid转换为十六进制:
    1
    printf '%x\n' <tid>
    将得到的十六进制tid,用jstack即可定位具体线程:
    1
    jstack <pid> | grep <tid十六进制>

“载营魄抱一,能无离乎?
专气致柔,能如婴儿乎?
涤除玄览,能无疵乎?
爱民治国,能无以智乎?
天门开阖,能为雌乎?
明白四达,能无为乎?
生之畜之,生而不有,为而不恃,长而不宰,是谓玄德。”1

Spring从诞生至今,ioc和aop一直都是其灵魂所在,至今我们使用spring依然依赖这两大特性:依赖注入(ioc)、面向切面编程(aop)。从设计模式上来讲,却极其简单,无外乎工厂模式和代理模式。
今天我们从dubbo的内核源码入手,来看看ioc和aop是如何影响着dubbo的框架设计。

###ioc在dubbo中的应用
dubbo的ioc主要体现在dubbo扩展点加载的环节,T injectExtension(T instance)方法中。该方法只在三个地方被使用:

1
createAdaptiveExtension()
2
 --injectExtension((T) getAdaptiveExtensionClass().newInstance()) //为创建好的AdaptiveExtensionClass实例进行属性注入
3
4
createExtension(String name)
5
 --injectExtension(instance) //为创建好的Extension实例进行属性注入
6
 --injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)) //为创建好的wrapperClass实例进行属性注入

ExtensionLoader:

1
private T injectExtension(T instance) {
2
    try {
3
        if (objectFactory != null) {
4
            for (Method method : instance.getClass().getMethods()) {
5
                if (method.getName().startsWith("set") && method.getParameterTypes().length == 1
6
                    && Modifier.isPublic(method.getModifiers())) {//一个参数的public的setXXX(T param)方法.例如,setName(String name)
7
                    Class<?> pt = method.getParameterTypes()[0];//参数param的类型T,eg.String
8
                    try {
9
                        String property = method.getName().length() > 3
10
                            ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";//获取属性名XXX, eg.name
11
                        Object object = objectFactory.getExtension(pt, property);//实例化参数
12
                        if (object != null) {
13
                            //执行instance.method(object)方法,这里就是执行instance的setter方法,进行setter注入
14
                            method.invoke(instance, object);
15
                        }
16
                    } catch (Exception e) {
17
                        logger.error("fail to inject via method " + method.getName() + " of interface "
18
                                     + type.getName() + ": " + e.getMessage(),
19
                            e);
20
                    }
21
                }
22
            }
23
        }
24
    } catch (Exception e) {
25
        logger.error(e.getMessage(), e);
26
    }
27
    return instance;
28
}

整个方法的作用就是通过instance对象实例的setter方法为instance的属性赋值,完成setter注入,即IOC的最经典的注入方式。

详细步骤:

  • 获取instance的setter方法,通过setter方法获取属性名称property和属性类型pt(即paramType的简写)
  • 使用objectFactory创建一个property名称(类型为pt)的对象实例
  • 执行instance的setter方法,注入property实例

其中,比较重要的就是:Object object = objectFactory.getExtension(pt, property);这个方法。其中的objectFactory=AdaptiveExtensionFactory实例,其属性factories = [SpringExtensionFactory实例, SpiExtensionFactory实例]。

看一下源码:

1
private final List<ExtensionFactory> factories;
2
3
    public <T> T getExtension(Class<T> type, String name) {
4
        /**
5
         * 先调用SpiExtensionFactory来实例化;
6
         * 如果不行,再使用SpringExtensionFactory来实例化
7
         */
8
        for (ExtensionFactory factory : factories) {
9
            T extension = factory.getExtension(type, name);
10
            if (extension != null) {
11
                return extension;
12
            }
13
        }
14
        return null;
15
    }

SpiExtensionFactory:

1
public class SpiExtensionFactory implements ExtensionFactory {
2
    public <T> T getExtension(Class<T> type, String name) {
3
        if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {//type是接口且必须具有@SPI注解
4
            ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
5
            if (loader.getSupportedExtensions().size() > 0) {//获取type的所有ExtensionClasses实现的key
6
                return loader.getAdaptiveExtension();//获取type的装饰类,如果有@Adaptive注解的类,则返回该类的实例,否则返回一个动态代理类的实例(例如Protocol$Adpative的实例)
7
            }
8
        }
9
        return null;
10
    }
11
}

从这里我们可以看出dubbo-SPI的另外一个好处:可以为SPI实现类注入SPI的装饰类或动态代理类。

SpringExtensionFactory:

1
public class SpringExtensionFactory implements ExtensionFactory {
2
    private static final Set<ApplicationContext> contexts = new ConcurrentHashSet<ApplicationContext>();
3
4
    public static void addApplicationContext(ApplicationContext context) {
5
        contexts.add(context);
6
    }
7
8
    public static void removeApplicationContext(ApplicationContext context) {
9
        contexts.remove(context);
10
    }
11
12
    @SuppressWarnings("unchecked")
13
    public <T> T getExtension(Class<T> type, String name) {
14
        for (ApplicationContext context : contexts) {
15
            if (context.containsBean(name)) {//该context是否包含name的bean
16
                Object bean = context.getBean(name);//获取name的bean,如果是懒加载或多例的bean,此时会实例化name的bean
17
                if (type.isInstance(bean)) {//如果obj的类型是type或其子类,与instanceof相同
18
                    return (T) bean;
19
                }
20
            }
21
        }
22
        return null;
23
    }
24
}

###aop在dubbo中的应用
dubbo在获取扩展点时,会有如下调用:

final Protocol dubboProtocol = loader.getExtension(“dubbo”);
调用层级:

1
ExtensionLoader<T>.getExtension()
2
--createExtension(String name)
3
----getExtensionClasses().get(name)//获取扩展类
4
------loadExtensionClasses()
5
--------loadFile(Map<String, Class<?>> extensionClasses, String dir)
6
----injectExtension(instance);//ioc
7
----wrapper包装;//aop

createExtension(String name),该方法源码如下:

1
private T createExtension(String name) {
2
        /** 从cachedClasses缓存中获取所有的实现类map,之后通过name获取到对应的实现类的Class对象 */
3
        Class<?> clazz = getExtensionClasses().get(name);
4
        if (clazz == null) {
5
            throw findException(name);
6
        }
7
        try {
8
            /** 从EXTENSION_INSTANCES缓存中获取对应的实现类的Class对象,如果没有,直接创建,之后放入缓存 */
9
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
10
            if (instance == null) {
11
                EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance());
12
                instance = (T) EXTENSION_INSTANCES.get(clazz);
13
            }
14
            injectExtension(instance);
15
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
16
            if (wrapperClasses != null && wrapperClasses.size() > 0) {
17
                for (Class<?> wrapperClass : wrapperClasses) {
18
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
19
                }
20
            }
21
            return instance;
22
        } catch (Throwable t) {
23
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " + type
24
                                            + ")  could not be instantiated: " + t.getMessage(),
25
                t);
26
        }
27
    }

这里,先给出META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol内容:

1
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
2
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
3
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
4
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
5
mock=com.alibaba.dubbo.rpc.support.MockProtocol
6
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
7
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
8
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
9
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
10
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
11
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
12
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
13
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol

com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper和com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper,这两个类不含有@Adaptive注解且具有含有Protocol的单参构造器,符合这样条件的会被列入AOP增强类。放置在loader的私有属性cachedWrapperClasses中。

此时的loader:

  • Class<?> type = interface com.alibaba.dubbo.rpc.Protocol
  • ExtensionFactory objectFactory = AdaptiveExtensionFactory(适配类)
    factories = [SpringExtensionFactory实例, SpiExtensionFactory实例]
  • cachedWrapperClasses = [class ProtocolListenerWrapper, class ProtocolFilterWrapper]

再来看createExtension(String name)中的红色部分,就是今天的重点AOP。如上所讲,我在cachedWrapperClasses中缓存了两个AOP增强类:class ProtocolListenerWrapper和class ProtocolFilterWrapper。

首先是获取ProtocolListenerWrapper的单参构造器,然后创建ProtocolListenerWrapper实例,最后完成对ProtocolListenerWrapper实例进行属性注入,注意此时的instance=ProtocolListenerWrapper实例,而不再是之前的DubboProtocol实例了。之后使用ProtocolFilterWrapper以同样的方式进行包装,只是此时ProtocolFilterWrapper包装的是ProtocolListenerWrapper实例。

1:老子《道德经》第十章,老子故里,中国鹿邑。

“谷神不死,是谓玄牝。
玄牝之门,是谓天地根。
绵绵若存,用之不勤。”1

dubbo默认采用netty进行网络传输,协议中对字节流的处理在com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec类中,包含了对request请求的编码和解码,response响应的编码和解码。
dubbo协议采用固定长度的消息头(16字节)和不定长度的消息体来进行数据传输,消息头定义了netty在IO线程处理时需要的信息,协议的报文格式如下:
dubbo_message_protocol_header

消息头详解

协议头是16字节的定长数据:

1
2byte magic:类似java字节码文件里的魔数,用来判断是不是dubbo协议的数据包。魔数是常量0xdabb
2
1byte 的消息标志位:16-20序列id,21 event,22 two way,23请求或响应标识
3
1byte 状态,当消息类型为响应时,设置响应状态。24-31位。状态位, 设置请求响应状态,dubbo定义了一些响应的类型。具体类型见com.alibaba.dubbo.remoting.exchange.Response
4
8byte 消息ID,long类型,32-95位。每一个请求的唯一识别id(由于采用异步通讯的方式,用来把请求request和返回的response对应上)
5
4byte 消息长度,96-127位。消息体 body 长度, int 类型,即记录Body Content有多少个字节。

解码过程 ExchangeCodec->encode-> encodeRequest

1
public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
2
        if (msg instanceof Request) {
3
            encodeRequest(channel, buffer, (Request) msg);
4
        } else if (msg instanceof Response) {
5
            encodeResponse(channel, buffer, (Response) msg);
6
        } else {
7
            super.encode(channel, buffer, msg);
8
        }
9
}
1
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
2
     Serialization serialization = getSerialization(channel);
3
     // header.
4
     byte[] header = new byte[HEADER_LENGTH];
5
     // set magic number.
6
     Bytes.short2bytes(MAGIC, header);
7
8
     // set request and serialization flag.
9
     header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
10
11
     if (req.isTwoWay()) header[2] |= FLAG_TWOWAY;
12
     if (req.isEvent()) header[2] |= FLAG_EVENT;
13
14
     // set request id.
15
     Bytes.long2bytes(req.getId(), header, 4);
16
17
     // encode request data.
18
     int savedWriteIndex = buffer.writerIndex();
19
     buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
20
     ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
21
     ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
22
     if (req.isEvent()) {
23
         encodeEventData(channel, out, req.getData());
24
     } else {
25
         encodeRequestData(channel, out, req.getData());
26
     }
27
     out.flushBuffer();
28
     if (out instanceof Cleanable) {
29
         ((Cleanable) out).cleanup();
30
     }
31
     bos.flush();
32
     bos.close();
33
     int len = bos.writtenBytes();
34
     checkPayload(channel, len);
35
     Bytes.int2bytes(len, header, 12);
36
37
     // write
38
     buffer.writerIndex(savedWriteIndex);
39
     buffer.writeBytes(header); // write header.
40
     buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
41
 }

消息体详解

实现源码在DubboCodec.encodeRequestData(Channel channel, ObjectOutput out, Object data):

1
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data) throws IOException {
2
    RpcInvocation inv = (RpcInvocation) data;
3
4
    out.writeUTF(inv.getAttachment(Constants.DUBBO_VERSION_KEY, DUBBO_VERSION));
5
    out.writeUTF(inv.getAttachment(Constants.PATH_KEY));
6
    out.writeUTF(inv.getAttachment(Constants.VERSION_KEY));
7
8
    out.writeUTF(inv.getMethodName());
9
    out.writeUTF(ReflectUtils.getDesc(inv.getParameterTypes()));
10
    Object[] args = inv.getArguments();
11
    if (args != null)
12
        for (int i = 0; i < args.length; i++) {
13
            out.writeObject(encodeInvocationArgument(channel, inv, i));
14
        }
15
    out.writeObject(inv.getAttachments());
16
}

消息体的内容如下:
1、dubbo版本号
2、invoke的路径
3、invoke的provider端暴露的服务的版本号
4、调用的方法名称
5、参数类型描述符
6、遍历请求参数值并编码
7、dubbo请求的attachments

1:老子《道德经》第六章,老子故里,中国鹿邑。

“持而盈之,不如其已。
揣而锐之,不可常保。
金玉满堂,莫之能守;富贵而骄,自遗其咎。
功遂身退,天之道也。”1

上一篇中我们简单分析了netty在dubbo中的应用,本篇从dubbo发布服务的角度从源码中详细分析netty的具体使用。
开启netty服务,代码的调用链如下:

1
ServiceBean
2
      -->onApplicationEvent()
3
      -->ServiceConfig.export()
4
          -->doExport()
5
              -->doExportUrls()
6
                  -->doExportUrlsFor1Protocol()

发布服务的入口代码:

1
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
2
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
3
Exporter<?> exporter = protocol.export(wrapperInvoker);

这里proxyFactory对象默认当做JdkProxyFactory,进入到getInvoker方法。第一个参数是接口的实现对象,第二个参数是即将发布的接口Class,第三个参数是发布协议的URL。

1
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
2
    return new AbstractProxyInvoker<T>(proxy, type, url) {
3
        @Override
4
        protected Object doInvoke(T proxy, String methodName,
5
                                  Class<?>[] parameterTypes,
6
                                  Object[] arguments) throws Throwable {
7
            Method method = proxy.getClass().getMethod(methodName, parameterTypes);
8
            return method.invoke(proxy, arguments);
9
        }
10
    };
11
}

方法内代码,新建了一个抽象类AbstractProxyInvoker,并实现了抽象方法doInvoke。doInvoke中通过反射机制执行要调用的方法。
接下来:Exporter<?> exporter = protocol.export(invoker);代码 。这里protocol默认当做是DubboProtocol类。

1
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
2
     URL url = invoker.getUrl();
3
4
     // export service.
5
     String key = serviceKey(url);
6
     DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
7
     exporterMap.put(key, exporter);
8
9
     //export an stub service for dispatching event
10
     Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY, Constants.DEFAULT_STUB_EVENT);
11
     Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
12
     if (isStubSupportEvent && !isCallbackservice) {
13
         String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
14
         if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
15
             if (logger.isWarnEnabled()) {
16
                 logger.warn(new IllegalStateException("consumer [" + url.getParameter(Constants.INTERFACE_KEY) +
17
                         "], has set stubproxy support event ,but no stub methods founded."));
18
             }
19
         } else {
20
             stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
21
         }
22
     }
23
24
     openServer(url);
25
     optimizeSerialization(url);
26
     return exporter;
27
 }

这个方法将传入的Invoker对象封装到DubboExporter对象中,并生成了唯一的key值。同时将key与DubboExporter对象关联保存进入exporterMap中,它是一个支持高并发的ConcurrentHashMap类。当客户端做远程请求服务时,就是根据key值从这个MAP中取出的真正接口实现对象来响应客户端的请求。在后面的代码分析中会体现出来。

1
private void openServer(URL url) {
2
    // find server.
3
    String key = url.getAddress();
4
    //client can export a service which's only for server to invoke
5
    boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
6
    if (isServer) {
7
        ExchangeServer server = serverMap.get(key);
8
        if (server == null) {
9
            serverMap.put(key, createServer(url));
10
        } else {
11
            // server supports reset, use together with override
12
            server.reset(url);
13
        }
14
    }
15
}

首先判断serverMap中是否已经包含了当前服务的ExchangeServer对象,如果没有调用createServer(url)创建一个并保存到serverMap中。继续跟进到createServer中,在这里调用了Exchangers类的静态方法bind创建了一个ExchangeServer对象,并返回出去了。注意bind方法的两个参数,第一个是URL很熟悉对吧!就不细说了,关键是第二个参数requestHandler,它是ExchangeHandlerAdapter类。它重写了很多父接口中的方法。里面重写了一个received方法,这个就是netty框架在接收到客户端请求以后响应处理的入口。具体处理细节在后面分析。这里继续往下看是怎么启动netty服务的。

1
private ExchangeServer createServer(URL url) {
2
     // send readonly event when server closes, it's enabled by default
3
     url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString());
4
     // enable heartbeat by default
5
     url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
6
     String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER);
7
8
     if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str))
9
         throw new RpcException("Unsupported server type: " + str + ", url: " + url);
10
11
     url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
12
     ExchangeServer server;
13
     try {
14
         server = Exchangers.bind(url, requestHandler);
15
     } catch (RemotingException e) {
16
         throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
17
     }
18
     str = url.getParameter(Constants.CLIENT_KEY);
19
     if (str != null && str.length() > 0) {
20
         Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions();
21
         if (!supportedTypes.contains(str)) {
22
             throw new RpcException("Unsupported client type: " + str);
23
         }
24
     }
25
     return server;
26
 }

以下是Exchangers类的静态方法bind的所有处理,getExchanger方法最终返回了HeaderExchanger对象。

1
public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
2
    if (url == null) {
3
        throw new IllegalArgumentException("url == null");
4
    }
5
    if (handler == null) {
6
        throw new IllegalArgumentException("handler == null");
7
    }
8
    url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange");
9
    return getExchanger(url).bind(url, handler);
10
}
11
public static Exchanger getExchanger(URL url) {
12
    String type = url.getParameter(Constants.EXCHANGER_KEY, Constants.DEFAULT_EXCHANGER);
13
    return getExchanger(type);
14
}
15
public static Exchanger getExchanger(String type) {
16
    return ExtensionLoader.getExtensionLoader(Exchanger.class).getExtension(type);
17
}

HeaderExchanger类中的bind方法代码如下,将dubbo协议的handler对象最终包装成了DecodeHandler对象,并传入到了Transporters类的bind方法中。

1
public class HeaderExchanger implements Exchanger {
2
3
    public static final String NAME = "header";
4
5
    public ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException {
6
        return new HeaderExchangeClient(Transporters.connect(url, new DecodeHandler(new HeaderExchangeHandler(handler))), true);
7
    }
8
9
    public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
10
        return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
11
    }
12
13
}

继续跟进Transporters类的bind方法如下。总体思路就是获取Transporter接口的具体实现类,然后调用该实现的bind方法。它有MinaTransporter,NettyTransporter,GrizzlyTransporter三种实现类,这里我就默认使用实现类NettyTransporter了。

1
public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException {
2
    if (url == null) {
3
        throw new IllegalArgumentException("url == null");
4
    }
5
    if (handlers == null || handlers.length == 0) {
6
        throw new IllegalArgumentException("handlers == null");
7
    }
8
    ChannelHandler handler;
9
    if (handlers.length == 1) {
10
        handler = handlers[0];
11
    } else {
12
        handler = new ChannelHandlerDispatcher(handlers);
13
    }
14
    return getTransporter().bind(url, handler);
15
}
1
public class NettyTransporter implements Transporter {
2
3
    public static final String NAME = "netty";
4
5
    public Server bind(URL url, ChannelHandler listener) throws RemotingException {
6
        return new NettyServer(url, listener);
7
    }
8
9
    public Client connect(URL url, ChannelHandler listener) throws RemotingException {
10
        return new NettyClient(url, listener);
11
    }
12
13
}

NettyTransporter类的源码如下。关注下bind方法,新建了一个NettyServer对象。感觉离netty越来越近了。
在NettyServer类中重点关注两个方法doOpen(它重写了抽象类父类的AbstractServer中的doOpen抽象方法)和它的构造函数。

1
public NettyServer(URL url, ChannelHandler handler) throws RemotingException {
2
    super(url, ChannelHandlers.wrap(handler, ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME)));
3
}
4
protected void doOpen() throws Throwable {
5
    NettyHelper.setNettyLoggerFactory();
6
7
    bootstrap = new ServerBootstrap();
8
9
    bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("NettyServerBoss", true));
10
    workerGroup = new NioEventLoopGroup(getUrl().getPositiveParameter(Constants.IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
11
            new DefaultThreadFactory("NettyServerWorker", true));
12
13
    final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
14
    channels = nettyServerHandler.getChannels();
15
16
    bootstrap.group(bossGroup, workerGroup)
17
            .channel(NioServerSocketChannel.class)
18
            .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
19
            .childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
20
            .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
21
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
22
                @Override
23
                protected void initChannel(NioSocketChannel ch) throws Exception {
24
                    NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
25
                    ch.pipeline()//.addLast("logging",new LoggingHandler(LogLevel.INFO))//for debug
26
                            .addLast("decoder", adapter.getDecoder())
27
                            .addLast("encoder", adapter.getEncoder())
28
                            .addLast("handler", nettyServerHandler);
29
                }
30
            });
31
    // bind
32
    ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
33
    channelFuture.syncUninterruptibly();
34
    channel = channelFuture.channel();
35
36
}

构造函数跟进super方法的处理。查看父类AbstractServer中的构造函数,细看里面执行了doOpen方法,根据抽象模版方法模式,其实调用的是子类的doOpen方法。到此已经将netty服务开启。

1:老子《道德经》第九章,老子故里,中国鹿邑。

“上善若水,水善利万物而不争。
处众人所恶,故几於道。
居善地,心善渊,与善仁,言善信,正善治,事善能,动善时。
夫唯不争,故无尤。”1

Netty是一款高性能的网络传输框架,在各类中间件和分布式框架中都能见到它的身影。今天就从dubbo的源码中说一说netty,netty本质上是负责网络传输,网络传输自然离不开socket,socket是端到端的连接。dobbo采用的是无中心化,每个client端都能与server端连接,每个client端同时也可以作为server端。
Dubbo的client端主要实现AbstractClient,NettyClient扩展继承了它。一般来说对于同一个server端来说(ip和port相同),只有一个client实例对应,也就是dubbo所说的共享连接。
从DubboProtocol类实现可以找到:

1
private ExchangeClient[] getClients(URL url) {
2
    // whether to share connection
3
    boolean service_share_connect = false;
4
    int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
5
    // if not configured, connection is shared, otherwise, one connection for one service
6
    if (connections == 0) {
7
        service_share_connect = true;
8
        connections = 1;
9
    }
10
11
    ExchangeClient[] clients = new ExchangeClient[connections];
12
    for (int i = 0; i < clients.length; i++) {
13
        if (service_share_connect) {
14
            clients[i] = getSharedClient(url);
15
        } else {
16
            clients[i] = initClient(url);
17
        }
18
    }
19
    return clients;
20
}

NettyClient 打开连接:

1
protected void doOpen() throws Throwable {
2
     NettyHelper.setNettyLoggerFactory();
3
     final NettyClientHandler nettyClientHandler = new NettyClientHandler(getUrl(), this);
4
     bootstrap = new Bootstrap();
5
     bootstrap.group(nioEventLoopGroup)
6
             .option(ChannelOption.SO_KEEPALIVE, true)
7
             .option(ChannelOption.TCP_NODELAY, true)
8
             .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
9
             //.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getTimeout())
10
             .channel(NioSocketChannel.class);
11
12
     if (getTimeout() < 3000) {
13
         bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);
14
     } else {
15
         bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getTimeout());
16
     }
17
18
     bootstrap.handler(new ChannelInitializer() {
19
20
         protected void initChannel(Channel ch) throws Exception {
21
             NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this);
22
             ch.pipeline()//.addLast("logging",new LoggingHandler(LogLevel.INFO))//for debug
23
                     .addLast("decoder", adapter.getDecoder())
24
                     .addLast("encoder", adapter.getEncoder())
25
                     .addLast("handler", nettyClientHandler);
26
         }
27
     });
28
 }

dubbo为了实现对Channel的抽象,不依赖Netty的实现,自己设计了Channel类,而NettyChannel只不过是dubbo的Channel其中一种实现而已。NettyChannel类保存了一个静态变量channelMap,这个是map型变量。原生的Channel和dubbo定制化的NettyChannel一对一对应绑定起来。
NettyChannel:

1
private static final ConcurrentMap<Channel, NettyChannel> channelMap = new ConcurrentHashMap<Channel, NettyChannel>();

一对一绑定实现:

1
static NettyChannel getOrAddChannel(Channel ch, URL url, ChannelHandler handler) {
2
        if (ch == null) {
3
            return null;
4
        }
5
        NettyChannel ret = channelMap.get(ch);
6
        if (ret == null) {
7
            NettyChannel nettyChannel = new NettyChannel(ch, url, handler);
8
            if (ch.isActive()) {
9
                ret = channelMap.putIfAbsent(ch, nettyChannel);
10
            }
11
            if (ret == null) {
12
                ret = nettyChannel;
13
            }
14
        }
15
        return ret;
16
    }

NettyHandler是对ChannelHandler一层封装。ChannelHandler大量采用装饰器模式和委托模式,这类似Java中的IO中Stream。通过装饰器模式使得ChannelHandler具有解码,统计,分发等等功能。最里层ExchangeHandler是DubboProtocol类中的内部类。reply方法看起来不来,主要做了2件事:获取对应的Invoker,执行invoke调用。

1
2
private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
3
4
        public Object reply(ExchangeChannel channel, Object message) throws RemotingException {
5
            if (message instanceof Invocation) {
6
                Invocation inv = (Invocation) message;
7
                Invoker<?> invoker = getInvoker(channel, inv);
8
                // need to consider backward-compatibility if it's a callback
9
                if (Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INVOKE))) {
10
                    String methodsStr = invoker.getUrl().getParameters().get("methods");
11
                    boolean hasMethod = false;
12
                    if (methodsStr == null || methodsStr.indexOf(",") == -1) {
13
                        hasMethod = inv.getMethodName().equals(methodsStr);
14
                    } else {
15
                        String[] methods = methodsStr.split(",");
16
                        for (String method : methods) {
17
                            if (inv.getMethodName().equals(method)) {
18
                                hasMethod = true;
19
                                break;
20
                            }
21
                        }
22
                    }
23
                    if (!hasMethod) {
24
                        logger.warn(new IllegalStateException("The methodName " + inv.getMethodName()
25
                                + " not found in callback service interface ,invoke will be ignored."
26
                                + " please update the api interface. url is:"
27
                                + invoker.getUrl()) + " ,invocation is :" + inv);
28
                        return null;
29
                    }
30
                }
31
                RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
32
                return invoker.invoke(inv);
33
            }
34
            throw new RemotingException(channel, "Unsupported request: "
35
                    + (message == null ? null : (message.getClass().getName() + ": " + message))
36
                    + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
37
        }
38
39
        @Override
40
        public void received(Channel channel, Object message) throws RemotingException {
41
            if (message instanceof Invocation) {
42
                reply((ExchangeChannel) channel, message);
43
            } else {
44
                super.received(channel, message);
45
            }
46
        }
47
48
        @Override
49
        public void connected(Channel channel) throws RemotingException {
50
            invoke(channel, Constants.ON_CONNECT_KEY);
51
        }
52
53
        @Override
54
        public void disconnected(Channel channel) throws RemotingException {
55
            if (logger.isInfoEnabled()) {
56
                logger.info("disconnected from " + channel.getRemoteAddress() + ",url:" + channel.getUrl());
57
            }
58
            invoke(channel, Constants.ON_DISCONNECT_KEY);
59
        }
60
61
        private void invoke(Channel channel, String methodKey) {
62
            Invocation invocation = createInvocation(channel, channel.getUrl(), methodKey);
63
            if (invocation != null) {
64
                try {
65
                    received(channel, invocation);
66
                } catch (Throwable t) {
67
                    logger.warn("Failed to invoke event method " + invocation.getMethodName() + "(), cause: " + t.getMessage(), t);
68
                }
69
            }
70
        }
71
72
        private Invocation createInvocation(Channel channel, URL url, String methodKey) {
73
            String method = url.getParameter(methodKey);
74
            if (method == null || method.length() == 0) {
75
                return null;
76
            }
77
            RpcInvocation invocation = new RpcInvocation(method, new Class<?>[0], new Object[0]);
78
            invocation.setAttachment(Constants.PATH_KEY, url.getPath());
79
            invocation.setAttachment(Constants.GROUP_KEY, url.getParameter(Constants.GROUP_KEY));
80
            invocation.setAttachment(Constants.INTERFACE_KEY, url.getParameter(Constants.INTERFACE_KEY));
81
            invocation.setAttachment(Constants.VERSION_KEY, url.getParameter(Constants.VERSION_KEY));
82
            if (url.getParameter(Constants.STUB_EVENT_KEY, false)) {
83
                invocation.setAttachment(Constants.STUB_EVENT_KEY, Boolean.TRUE.toString());
84
            }
85
            return invocation;
86
        }
87
    };

NettyHandler继承了SimpleChannelHandler,是我们最需要关注和设计的类,因为它是Netty提供开发者最有控制权的类。任何依赖Netty的框架都需要定制化NettyHandler类。dubbo也不例外,对NettyHandler进行了大量抽象和封装,使其能满足自身功能的需要。

接下来会在下一篇中详细解读dubbo源码来分析发布服务时如何启动socket监听,启动netty服务。

1:老子《道德经》第八章,老子故里,中国鹿邑。