王某某的笔记

记录我的编程之路

计算机如何存储字节

计算机中最小的存储单位是bit只能保存0和1,整数在内存中如何存储我们都知道,将要存储的数字转成2进制即可

用windows自带的计数器可以方便的查看整数对应的2进制值
如:
byte类型(单字节)

十进制 二进制
8 0000 1000
9 0000 1001
100 0110 0100
-5 1111 1011
-8 1111 1000

第一位为符号位,负数等于正数取反 +1

那浮点类型是如何用这么少的字节(如float 4字节)表示这么大(float 最大 3.4028235E38)的数字呢?

浮点类型是如何存储的

浮点数

浮点数,是属于有理数中某特定子集的数的数字表示,在计算机中用以近似表示任意某个实数。具体的说,这个实数由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到,这种表示方法类似于基数为10的科学计数法。

科学计数法

科学计数法是一种记数的方法。把一个数表示成a与10的n次幂相乘的形式(1≤|a|<10,a不为分数形式,n为整数),这种记数法叫做科学计数法。当我们要标记或运算某个较大或较小且位数较多时,用科学计数法免去浪费很多空间和时间。

java中的浮点数字遵循 IEEE 754 标准

这也是一种目前最常用的浮点数标准!为许多CPU与浮点运算器所采用。

简单的说就是将一个浮点数字拆成3个部分(符号部分、指数部分、小数部分) 存储在连续的bit中,类似科学计数法。

用 {S,E,M}来表示一个数 V 的,即 V =(-1)S × M × 2E,如下:

S(符号位) E(指数位) M(有效数字位)

其中:

  • 符号位 s(Sign)决定数是正数(s=0)还是负数(s=1),而对于数值 0 的符号位解释则作为特殊情况处理。
  • 有效数字位 M(Significand)是二进制小数,它的取值范围为 0~1 。它也被称为尾数位(Mantissa)、系数位(Coefficient),甚至还被称作“小数”。
  • 指数位 E(Exponent)是 2 的幂(可能是负数,为了表示负数实际指数需要加一个偏移量),它的作用是对浮点数加权。

IEEE 754浮点数规范

1
+- d.ddd...d * β^e   (0 <= di < β)  

其中d.dd…d 为有效数字,β为基数,e 为指数

有效数字中 数字的个数 称为精度,我们可以用 p 来表示,即可称为 p 位有效数字精度。
每个数字 d 介于 0 和基数 β 之间,包括 0。

十进制表示

对十进制的浮点数,即基数 β 等于 10 的浮点数而言,上面的表达式非常容易理解。
如 12.34,我们可以根据上面的表达式表达为:
1×101 + 2×100 + 3×10-1 + 4×10-2
其规范的浮点数表达为:**1.234×101**。

二进制表示

但对二进制来说,上面的表达式同样可以简单地表达。
唯一不同之处在于:二进制的 β 等于 2,而每个数字 d 只能在 0 和 1 之间取值。

如二进制数 1001.101,我们可以根据上面的表达式表达为:
1×23 + 0×22 + 0×21 + 1×20 + 1×2-1 + 0×2-2 + 1×2-3
其规范浮点数表达为:**1.001101×23**。

二进制转换为十进制

二进制数 1001.101 转成十进制如下:

= 1 × 23 + 0 × 22 + 0 × 21 + 1 × 20 + 1 × 2-1 + 0 × 2-2 + 1×2-3
= 8 + 0 + 0 + 1 + 1/2 + 0 + 1/8
= 9又1/8 (9又8分之1)
= 9.625

由上面的等式,我们可以得出:
向左移动二进制小数点一位相当于这个数除以 2,而向右移动二进制小数点一位相当于这个数乘以 2。
如 101.11 = 5又3/4 (5.75),向左移动一位,得到 10.111 = 2又7/8 (2.875)。

除此之外,我们还可以得到这样一个基本规律:
一个十进制小数要能用浮点数精确地表示,最后一位必须是 5(当然这是必要条件,并非充分条件)。
如下面的示例所示:

二进制小数 2的多少次方 十进制的小数
0.1 2-1 0.5
0.01 2-2 0.25
0.001 2-3 0.125
0.0001 2-4 0.0625
0.00001 2-5 0.03125
0.000001 2-6 0.015625

十进制转换为二进制

基本换算方法:
将10进制的数拆分成整数和小数两个部分
整数部分除以2,取余数;小数部分乘以2,取整数位。

示例:
将十进制 1.1 转成 二进制

整数部分:1
1

小数部分:0.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0.1 *2 = 0.2 -> 0
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
0.6 *2 = 1.2 -> 1
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
0.6 *2 = 1.2 -> 1
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
0.6 *2 = 1.2 -> 1
0.2 *2 = 0.4 -> 0
0.4 *2 = 0.8 -> 0
0.8 *2 = 1.6 -> 1
......

二进制形式表示为:
1.000110011001100110011…

再将上面的数换算成10进制

1
2
3
4
5
6
7
1 + 1/16 + 1/32 + 1/256 + 1/512  + ...
约等于
(32 + 16 + 2 + 1)/512
约等于
51/512
约等于
0.099609375

再加上整数1,约等于:
1.099609375

计算的位数越多越精确

注意:
二进制小数不像整数一样,只要位数足够,它就可以表示所有整数。
在有限长度的编码中,二进制小数一般无法精确的表示任意小数,比如十进制小数0.2,我们并不能将其准确的表示为一个二进制数,只能增加二进制长度提高表示的精度。

十进制的0.2 转换成二进制为:0.00110011001100110011001100110011001100110011001100110……


java API 中对Double 和 Float 类型的描述

Double 双精度浮点数 64bit (8byte)

根据 IEEE 754 浮点“双精度格式”位布局。

  • 第 63 位(掩码 0x8000000000000000L 选定的位)表示浮点数的符号。
  • 第 62-52 位(掩码 0x7ff0000000000000L 选定的位)表示指数。
  • 第 51-0 位(掩码 0x000fffffffffffffL 选定的位)表示浮点数的有效数字(有时也称为尾数)。

如果参数是正无穷大,则结果为 0x7ff0000000000000L。
如果参数是负无穷大,则结果为 0xfff0000000000000L。
如果参数是 NaN,则结果为 0x7ff8000000000000L。

Float 单精度浮点数 32bit (4byte)

根据 IEEE 754 浮点“单一格式”位布局。

  • 第 31 位(掩码 0x80000000 选定的位)表示浮点数的符号。
  • 第 30-23 位(掩码 0x7f800000 选定的位)表示指数。
  • 第 22-0 位(掩码 0x007fffff 选定的位)表示浮点数的有效位数(有时也称为尾数)。

如果参数为正无穷大,则结果为 0x7f800000。
如果参数为负无穷大,则结果为 0xff800000。
如果参数为 NaN,则结果为 0x7fc00000。

掩码位说明

这里以 double类型说明

  • 第 63 位(掩码 0x8000000000000000L 选定的位)表示浮点数的符号。
    转成二进制
    1000000000000000000000000000000000000000000000000000000000000000

  • 第 62-52 位(掩码 0x7ff0000000000000L 选定的位)表示指数。
    转成二进制
    0111111111110000000000000000000000000000000000000000000000000000

  • 第 51-0 位(掩码 0x000fffffffffffffL 选定的位)表示浮点数的有效数字(有时也称为尾数)。
    转成二进制
    0000000000001111111111111111111111111111111111111111111111111111

将一个浮点数与上面的掩码进行与运算,即可得到对应的 符号位、指数位、尾数位 的值。

这里的多少多少位是从右往左数的,当转成2进制不够64位时在前面补零即可


按照浮点数计算规范要求:(划重点)

  • 符号为 1表示负数,0表示正数
  • 指数 =(为了表示负指数,实际的指数需要加上一个值,双精度为 2e10-1 = 1023)= 实际指数 + 1023
  • 有效数字省略了最高的一位1,去掉小数点左侧的 1,并用 0 在右侧补齐。
故前面十进制数(1.1)的二进制形式:

1.000110011001100110011…

用浮点类型表示:
  • 符号位:0
  • 指数为:0 + 1023 = 1111111111 = (不够11位前面补0) = 01111111111
  • 有效位:000110011001100110011…

所以存为:
0 01111111111 000110011001100110011…

用java代码输出进行验证

1
2
3
4
5
6
7
8
System.out.println(Long.toBinaryString(Double.doubleToLongBits(1.1)));
//11111111110001100110011001100110011001100110011001100110011010
//这种情况前面要补两个0
//0011111111110001100110011001100110011001100110011001100110011010

System.out.println(Long.toBinaryString(Double.doubleToLongBits(-1.1)));
//1011111111110001100110011001100110011001100110011001100110011010

浮点数精度问题

根据 IEEE 754 规范

  • Float 单精度浮点数,有效位数只有23 bit位
  • Double 双精度浮点数,有效位数只有52 bit位

在二进制,第一个有效数字必定是“1”,因此这个“1”并不会存储。
单精和双精浮点数的有效数字分别是有存储的23和52个位,加上最左边没有存储的第1个位,即是24和53个位。

通过计算其能表示的最大值,换十进制来看其精度:

  • Float
    二进制的24个1 => (二进制) 111111111111111111111111 => (计算) 2^24 - 1 => (十进制) 16777215 => (Float 精度最大8位数)

  • Double
    二进制的53个1 => (二进制) 11111111111111111111111111111111111111111111111111111 => (计算)2^53 - 1 => (十进制)9007199254740991 => (Double 精度最大16位数)

为什么会丢失精度

浮点运算很少是精确的,只要是超过精度能表示的范围就会产生误差。而往往产生误差不是因为数的大小,而是因为数的精度。

我自己理解为分两种情况(这个不一定是对)

  1. 当有小数时,浮点数本身就不能精确记录其数值,只记了一个近似值,此时进行计算就很可能不对
  2. 有效位数不够用了,导致舍入

1、不能精确记录其数值

通过上面的转换示例,我们知道小数的二进制表示一般都不是精确的,在有限的精度下只能尽量的表示近似值

值本身就不是精确的,再进行计算就很可能产生误差

0.1+0.2=0.30000000000000004
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
double d1 = 0.1;
double d2 = 0.2;
double d3 = d1 + d2;
System.out.println(d3);
System.out.println("######################################");
System.out.println(new BigDecimal(d1));
System.out.println(new BigDecimal(d2));
System.out.println(new BigDecimal(d3));

System.out.println("######################################");
System.out.println(Long.toBinaryString(Double.doubleToLongBits(d1)));
System.out.println(Long.toBinaryString(Double.doubleToLongBits(d2)));
System.out.println(Long.toBinaryString(Double.doubleToLongBits(d3)));

输出:

1
2
3
4
5
6
7
8
9
0.30000000000000004
######################################
0.1000000000000000055511151231257827021181583404541015625
0.200000000000000011102230246251565404236316680908203125
0.3000000000000000444089209850062616169452667236328125
######################################
11111110111001100110011001100110011001100110011001100110011010
11111111001001100110011001100110011001100110011001100110011010
11111111010011001100110011001100110011001100110011001100110100

0.1
原始值: 0 01111111011 1001100110011001100110011001100110011001100110011010
指数:1019 -1023 = -4
二进制形式:
0.00011001100110011001100110011001100110011001100110011010

0.2
原始值:0 01111111100 1001100110011001100110011001100110011001100110011010
指数:1020 -1023 = -3
二进制形式:
0.001001100110011001100110011001100110011001100110011010

0.3
原始值:0 01111111101 0011001100110011001100110011001100110011001100110100
指数:1021 = -2
二进制形式:
0.010011001100110011001100110011001100110011001100110100

二进制加法运算

1
2
3
4
5
0.00011001100110011001100110011001100110011001100110011010
+
0.001001100110011001100110011001100110011001100110011010
=
0.010011001100110011001100110011001100110011001100110100
其他示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
double a = 0.03;
double b = 0.01;
System.out.println(a - b);
//结果 0.019999999999999997

double x = 10.2;
double y = 10.03;
System.out.println(x + y);
//结果 20.229999999999997

double dx = 1.099999999999999999999999999999d;
System.out.println(dx);
//结果 1.1

double dy = 1.1000000000000000000000000000001d;
System.out.println(dy);
//结果 1.1

2、有效位数不够用

这里用float验证,float最大的精度是8位数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 有效位数不够
float f = 1.23456789f;
System.out.println(f); // 1.2345679 最大只能有8位

System.out.println("=====================");
float f1 = 10000f;
System.out.println(f1); // 10000.0
float f2 = 1.123456f;
System.out.println(f2); // 1.123456

// 相加之后有效位数不够
float f3 = f1 + f2;
System.out.println(f3); // 10001.123 位数不够,后面的被省略了

关于舍入

对于不能精确的表示的数,采取一种系统的方法:找到“最接近”的匹配值,它可以用期望的浮点形式表现出来,这就是舍入。

对于舍入,可以有很多种规则,可以向上舍入,向下舍入,向偶数舍入。如果我们只采用前两种中的一种,就会造成平均数过大或者过小,实际上这时候就是引入了统计偏差。如果是采用偶数舍入,则有一半的机会是向上舍入,一半的机会是向下舍入,这样子可以避免统计偏差。而 IEEE 754 就是采用向最近偶数舍入(round to nearest even)的规则。

(这段是网上抄的)


其他

大端 小端问题

这里以java语言示例,用大端的方式示例(网络序)

java中是以大端模式存储的,java对我们屏蔽了内部字节顺序的问题以实现跨平台!

实际在不同的cpu架构下,存储方式不同,我们常用的X86是以小端的模式存储的。

网络传输一般采用大端序,也被称之为网络字节序,或网络序。IP协议中定义大端序为网络字节序。


测试代码

二进制字符串转成Double

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
// 4607632778762754458
String s = "0011111111110001100110011001100110011001100110011001100110011010";
System.out.println("二进制字符串 = " + s);
long l = Long.parseLong(s, 2);
System.out.println("转成Long = " + l);
double d = Double.longBitsToDouble(l);
System.out.println("再将Long转成Double = " + d);
}

输出:

1
2
3
二进制字符串 = 0011111111110001100110011001100110011001100110011001100110011010
转成Long = 4607632778762754458
再将Long转成Double = 1.1

写内存的方式转Double

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* bit字符串转Double
*
* @param bitStr 64位的01字符串
* @throws Exception
*/
public static void bit2Double(String bitStr) throws Exception {
String[] array = splitString(bitStr, 8);
System.out.println(Arrays.toString(array));

Unsafe unsafe = getUnsafe();
long address = unsafe.allocateMemory(8L);

for (int i = 0; i < array.length; i++) {
String bits = array[i];
byte bt = (byte) Integer.parseInt(bits, 2);
// 因为实际上是小端模式存储的,所以这里从后面开始写入
unsafe.putByte(address + (7 - i), (byte) bt);
}

long lVal = unsafe.getLong(address);
System.out.println("对应的long值是:" + lVal);
System.out.println(Long.toBinaryString(lVal));

double dVal = unsafe.getDouble(address);
System.out.println("转成Double 类型是:" + dVal);
System.out.println(Long.toBinaryString(Double.doubleToLongBits(dVal)));

}

public static String[] splitString(String source, int length) {
String[] array = new String[length];
for (int i = 0; i < length; i++) {
array[i] = source.substring(i * length, (i + 1) * length);
}
return array;
}

public static Unsafe getUnsafe() throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
}

字节序

字节顺序,又称之为:端序尾序。在计算机科学领域中指的是:电脑内存 或者在网络通讯链路中,由多个字节组成的的排列方式。

由于不同架构的CPU处理多个字节数据的顺序不一样,比如x86的是小段端模式,KEIL C51是大端模式。但是后来互联网流行,TCP/IP协议规定为大端模式,又称为:network order。

大端和小端

在计算机体系中我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。
在编程语言中一个long类型占32bit,那就需要4个字节来存储,那按照什么样的顺序将这四个字节写到内存中?
因此就出现了大端存储模式和小端存储模式。

大端(Big-Endian): 数据的高位字节在前(内存的低地址),低位字节在后。这样的存储模式类似于把数据当做字符串处理,内存地址由小到大增长,而数据从高位字节开始写入。

这种方式符合人类的阅读习惯!

小端(Little-Endian): 数据的低位字节在前(内存的低地址),高位字节在后。这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。

示例:

如:
32位的16进制数:0x1A2B3C4D 在内存中的存储

1
2
3
4
内存地址: 00  01  02  03
------------------------------
小端模式: 4D 3C 2B 1A
大端模式: 1A 2B 3C 4D

java中的大端和小端

Java二进制文件中的所有内容均按大端顺序存储。这种存储方式也被称为network order。这意味着,如果仅仅是用java,则在所有的平台上(如Mac、 PC、 UNIX等)
所有文件的处理方式都相同,可以自由的进行二进制文件的交换,而无需担心字节顺序的问题。

java对我们隐藏了内部字节顺序的问题!

但是当我们与某些不是用Java编写的使用小端顺序的程序交换数据文件时,就会出现问题。最常见的是使用C编写的程序。某些平台在内部使用大端顺序(Mac,IBM 390),有些使用小端序(Intel)。

C语言默认是小端模式。
如果java 要读取C 写的二进制文件,就要涉及到大小端转换的问题。


java代码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

public static void printSystemInfo() {
System.out.println("##########################################");
System.out.println("系统名称:" + System.getProperty("os.name"));
System.out.println("系统架构:" + System.getProperty("os.arch"));
System.out.println("系统版本:" + System.getProperty("os.version"));
System.out.println("##########################################");
System.out.println("");
}

public static Unsafe getUnsafe() throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
}

public static void main(String[] args) throws Exception {
printSystemInfo();
try {
Unsafe UNSAFE = getUnsafe();

int intVal = 0x1A2B3C4D;
System.out.println("原始值:十六进制:" + Integer.toHexString(intVal) + " 对应十进制:" + intVal);

// 分配4个字节的内存
long address = UNSAFE.allocateMemory(4);
// 存放int类型的数据,占4个字节
UNSAFE.putInt(address, intVal);
byte b = UNSAFE.getByte(address);
// 通过getByte方法获取刚才存放的int,取第一个字节
// 如果是大端,存放顺序 —> 1A,2B,3C,4D,取第一位便是0x1A
// 如果是小端,存放顺序 —> 4D,3C,2B,1A ,取第一位便是0x4D
System.out.println("取到的第一个字节:" + Integer.toHexString(b));
ByteOrder byteOrder;
switch (b) {
case 0x1A:
System.out.println("当前使用:大端序");
byteOrder = ByteOrder.BIG_ENDIAN;
break;
case 0x4D:
System.out.println("当前使用:小端序");
byteOrder = ByteOrder.LITTLE_ENDIAN;
break;
default:
byteOrder = null;
}
System.out.println(byteOrder);
// 这里在X86架构的windows机器上跑,输出结果为:
// LITTLE_ENDIAN

// 然后我们重新从内存中读取int
int val2 = UNSAFE.getInt(address);
System.out.println("重新从内存中读取的值:" + val2);
} catch (Exception e) {
e.printStackTrace();
}
}

输出结果

window平台
1
2
3
4
5
6
7
8
9
10
11
##########################################
系统名称:Windows 7
系统架构:amd64
系统版本:6.1
##########################################

原始值:十六进制:1a2b3c4d 对应十进制:439041101
取到的第一个字节:4d
当前使用:小端序
LITTLE_ENDIAN
重新从内存中读取的值:439041101
linux平台
1
2
3
4
5
6
7
8
9
10
11
##########################################
系统名称:Linux
系统架构:amd64
系统版本:2.6.32-642.el6.x86_64
##########################################

原始值:十六进制:1a2b3c4d 对应十进制:439041101
取到的第一个字节:4d
当前使用:小端序
LITTLE_ENDIAN
重新从内存中读取的值:439041101

CMD命令

certutil -hashfile filename [MD5|SHA1|SHA256]

示例:

1
2
3
4
C:\Users\Administrator>certutil -hashfile "D:\test\123.txt" MD5
MD5 的 D:\test\123.txt 哈希:
3aed2464da46a2719ff1bf0766d2754f
CertUtil: -hashfile 命令成功完成。

添加“右键>发送到”快速计算校验值

  1. 在文件夹地址栏输入 shell:sendto 进入 “发送到” 引用目录

  2. 新建批处理文件(如:checksum.bat),将下面的代码拷贝至文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @echo OFF
    :LOOP
    set index=%1
    if %index%! == ! goto END
    echo.
    echo File : %index%
    echo.
    set /p="MD5 : "<nul
    certutil -hashfile "%index%" MD5|findstr /V "MD5"|findstr /V "CertUtil"
    set /p="SHA1 : "<nul
    certutil -hashfile "%index%" SHA1|findstr /V "SHA1"|findstr /V "CertUtil"
    set /p="SHA256 : "<nul
    certutil -hashfile "%index%" SHA256|findstr /V "SHA256"|findstr /V "CertUtil"
    shift
    goto LOOP
    :END
    echo.
    pause
  3. 在资源管理器中,右键需要校验的文件,发送至该文件即可
    ps:也可将批处理文件放在别处,创建文件的快捷方式,然后将快捷方式移动到引用目录

效果:

1
2
3
4
5
6
7
8
File   : D:\tools\bin\clear.bat

MD5 : 98bbdca979f85ee3a491769b89021306
SHA1 : ead014790c1adfdfaf10c5ee701d3e59080a662f
SHA256 : 58969ebe81e0669a2d4a995a9a0c62dcae5a711ea61126ddf8eab868a924112b

请按任意键继续. . .

我们希望在一个或多个I/O条件就绪(即输入已准备好被读取,或者描述符能够接收更多输出)时得到通知。此功能称为I/O复用,由select和poll等函数支持。

通常情况下,I/O多路复用通常用于网络应用程序:

  • 当客户端处理多个描述符时(通常是交互式输入和网络套接字)
  • 当客户端同时处理多个套接字时(这是可能的,但很少见)
  • 如果TCP服务器同时处理侦听套接字及其连接的套接字
  • 如果服务器同时处理TCP和UDP
  • 如果服务器处理多种服务,并且可能处理多种协议

但是I/O复用不限于网络编程。

Unix/Linux系统中的所有内容都是文件。每个进程都有一个文件描述符,该描述符指向文件,套接字,设备和其他操作系统对象。

我们告诉内核我们对哪些描述符感兴趣(用于读取,写入或异常情况)以及等待多长时间。我们感兴趣的描述符不限于套接字,还可以是任何的描述符。

I/O模型

Unix/Linux 下可供我们使用的五个I/O模型

  1. blocking I/O 【阻塞I/O】
  2. nonblocking I/O 【非阻塞I/O】
  3. I/O multiplexing (select and poll) 【多路复用I/O】
  4. signal driven I/O (SIGIO) 【信号驱动】
  5. asynchronous I/O (the POSIX aio_ functions) 【异步I/O,由POSIX规范定义的】

输入操作通常有两个不同的阶段:

  1. 等待数据准备就绪。这涉及等待数据到达网络。数据包到达时,它将被复制到内核中的缓冲区中。
  2. 将数据从内核复制到进程。这意味着将(就绪)数据从内核缓冲区复制到我们的应用程序缓冲区中

同步IO&异步IO

根据 POSIX 定义:

  • 同步I/O操作会阻塞请求进程,直到I/O操作完成
  • 异步I/O操作不会导致阻塞请求进程

按照这种分类,上边5种I/O模型中,只有AIO一种是异步的,其他都是同步的。
因为其中真正的IO操作(recvfrom 调用) 会阻塞进程,recvfrom 会阻塞等待内核将数据从内核空间复制到应用进程空间, 当赋值完成后, recvfrom 才返回。

但是从我们编程的角度来看,「I/O多路复用」和「信号驱动I/O」都不会导致我们的进程完全被阻塞,因为在多线程下,阻塞一个线程并不会导致整个进程被阻塞。

I/O多路复用实现方式

I/O多路复用(I/O multiplexing) 通过系统调用 select,poll,epoll 支持。

select/epoll的好处就在于单个进程就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这些个函数会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

本质是通过系统函数select、poll、epool(模块)来监听多个文件描述符(套接字描述符)。
无论epoll还是select都受限于系统中单个进程能够打开的文件句柄数。

select/poll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

一、select

select是个系统调用,提供了一种用于实现同步多路复用I/O的机制

1
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

对select()的调用将一直阻塞,直到给定的文件描述符准备执行I/O为止,或者直到经过可选的指定超时为止。

监视的文件描述符类型分为三种:

  • 监视readfds集中列出的文件描述符,以查看是否有数据可读取。
  • 监视writefds集中列出的文件描述符,以查看写操作是否将完成而不会阻塞。
  • 监视exceptionfds集中的文件描述符,以查看是否发生了异常或带外数据是否可用(这些状态仅适用于套接字)。

执行select()成功返回后,将修改每个集合,以使其仅包含准备好由该集合描述的I/O类型的文件描述符。

优缺点

优点:

select的主要优点是它具有很高的可移植性-像OS一样的每个UNIX都具有它

缺点:
  • select()使用fd_set来表示文件描述符的集合,而fd_set其实就是一个固定长度的位向量(bit vector),在Linux上,这个固定长度是FD_SETSIZE,其数值是1024。
    故select()监听的文件描述符总数必须小于1024。
  • 我们需要遍历文件描述符以检查它是否存在于select返回的集合中
注意事项:

每次select之前要重置每个入参集合的值(返回时会被修改)。

二、poll

poll提供与相似的功能select。
与select()不同,因为select()具有效率低下的三个基于位掩码的文件描述符集,poll()使用nfds pollfd结构的单个数组。原型更简单:

1
int poll (struct pollfd *fds, unsigned int nfds, int timeout);

pollfd结构的事件和返回事件具有不同的字段,因此我们不必每次都构建它

1
2
3
4
5
struct pollfd {
int fd;
short events;
short revents;
};

对于每个文件描述符,构建一个类型为pollfd的对象,并填充所需的事件。poll返回后,检查revents字段即可

就像我们对select所做的那样,我们需要检查每个pollfd对象以查看其文件描述符是否已准备就绪,但是我们不需要在每次迭代时都构建集合

优缺点

优点:

poll()系统调用将输入(events字段)与输出(revents字段)分开,从而允许入参被重复使用而无需更改。

缺点:

由于某些Unix系统不支持poll(),因此select()具有更高的可移植性。

select 和 poll 的性能问题

使用select()或poll()监听大量的文件描述符时,往往会遭遇到性能问题。当用户每次调用select()或poll()时,内核会对传入的所有文件描述符都检查一遍,并记录其中有哪些文件描述符存在I/O就绪,这个操作的耗时将随着文件描述符数量的增加而线性增长。

另一个重要因素也会影响select()和poll()的性能,例如用户每次调用poll()时,都需要传递一个pollfd数组,而poll()会将这个数组从用户空间拷贝到内核空间,当内核对这个数组作了修改之后,poll()又会将这个数组从内核空间拷贝到用户空间。随着pollfd数组长度的增加,每次拷贝的时间也会线性增长,一旦poll()需要监听大量的文件描述符,每次调用poll()时,这个拷贝操作将带来不小的开销。这个问题的根源在于select()和poll()的API设计不当,例如,对于应用程序来说,它每次调用poll()所监听的文件描述符集合应该是差不多的,所以我们不禁这样想,如果内核愿意提供一个数据结构,记录程序所要监听的文件描述符集合,这样每次用户调用poll()时,poll()就不需要将pollfd数组拷贝来拷贝去了(没错,epoll 就是这样解决的)。

三、epoll

epoll是为了解决select()和poll()中存在的问题

epoll是个模块,由三个系统调用(epoll_create epoll_ctl epoll_wait)组成,Epoll 系统调用可帮助我们在内核中创建和管理上下文。epoll使用红黑树(RB-tree)数据结构来跟踪当前正在监视的所有文件描述符。

我们将任务分为3个步骤:

  • 使用epoll_create在内核中创建上下文
  • 使用epoll_ctl向/从上下文中添加和删除文件描述符
  • 使用epoll_wait等待上下文中的事件

epoll_ctl()负责增加、删除或修改红黑树上的节点,而epoll_wait()则负责返回双向链表中就绪的文件描述符(及其事件)。

当网卡收到一个 packet 的时候,会触发一个硬件中断,这导致内核调用相应的中断 handler,从网卡中读入数据放到协议栈,当数据量满足一定条件时,内核将回调ep_poll_callback()这个方法,它负责把这个就绪的文件描述符添加到双向链表中。这样当用户调用epoll_wait()时,epoll_wait()所做的就只是检查双向链表是否为空,如果不为空,就把文件描述符和数量返回给用户即可。

触发模式

epoll提供边沿触发(Edge-Triggered)及状态触发(Level-Triggered)模式。

Level-Triggered 也翻译成水平触发、条件触发

边沿触发:
监控对象的状态发生改变时触发,此后如果状态一直没有发生变化应用程序将不再收到通知

状态触发:
处于某种状态下(如缓冲区可以读)就一直触发

如:
socket接收到缓存数据时,调用epoll_wait,上面两种方法都将返回,表明存在要读取的数据。
假设读取器仅消耗了缓冲区中的部分数据。
在状态触发模式下,epoll_wait只要管道的缓冲区包含要读取的数据,对epoll_wait的调用将立即返回;
但是,在边沿触发模式下,epoll_wait仅在将新数据写入缓存区后才返回。

一般编程逻辑

  1. 【epoll_create】在内核中创建epoll实例并返回一个epoll文件描述符(epfd)。

    1
    int epoll_create1(int flags);
  2. 【epoll_ctl】向epfd对应的内核epoll实例添加、修改或删除对fd(File descriptor)上事件event的监听。类型可以为EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL分别对应的是添加新的事件,修改文件描述符上监听的事件类型。

    1
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  3. 【epoll_wait】循环执行epoll_wait,当timeout 为0 时,epoll_wait 永远会立即返回。而timeout 为-1 时,epoll_wait 会一直阻塞直到任一已注册的事件变为就绪。当timeout 为一正整数时,epoll 会阻塞直到计时timeout 毫秒终了或已注册的事件变为就绪。因为内核调度延迟,阻塞的时间可能会略微超过timeout 毫秒。

    1
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

优缺点

优点:
  • 我们可以在等待时添加和删除文件描述符
  • epoll_wait仅返回具有就绪文件描述符的对象
  • epoll具有更好的性能-(Epoll时间复杂度为O(1) 代替之前的O(n))
  • epoll可以表现为状态触发或边缘触发
缺点:

epoll是特定于Linux的,因此不可移植

使用原因:弱电箱在门口,光猫在弱电箱里面,从弱电箱到电视机只有一根网线,路由器放到电视柜上面,这根网线给了路由器用了,IPTV没有线。

其他方案:

  1. 8芯网线分成两根4芯的网线(速度只有100M)
  2. 电力猫(干扰大)
  3. 使用网管交换机,用vlan (要另外买设备)
  4. 路由器和光猫上分别配置Vlan

用超级账号登陆光猫

如何获取超级账号密码需要另外想办法
有限地方的超级账号密码都是同一个,网上能查得到

去掉宽带的无线绑定

网络 -> 宽带设置

连接名称 选择 2_INTERNET_R_VID_
将下面 绑定端口: 无线(2.4G-x) 的勾 去掉,点击应用

修改端口绑定.png

设置iptv的无线绑定

网络 -> 宽带设置

选择 3_Other_B_VID_45
这个就是IPTV,将无线勾上,点击应用

IPTV无线绑定.png

修改WLAN配置启动无线

**网络 -> WLAN配置 **

SSID 使能的勾勾上
SSID使能.png

============================

机顶盒设置wifi连接

打开电视 和 机顶盒,按遥控的设置,进入维护登陆界面
机顶盒维护初始密码: 6321

进入网络选择无线,填入密码就行了(不行就试试将连接方式换成PPPOE试试,忘了)

基于netty 4.x

相关页面地址
4.x用户指南:https://netty.io/wiki/user-guide-for-4.x.html
Netty Example:https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example
API:https://netty.io/4.1/xref/index.html

Netty框架

Netty是一个基于NIO的异步事件驱动的网络应用程序框架和工具,使用他可以快速轻松的开发
出高性能和高扩展性的网络应用程序,例如自定义协议的服务器和客户端。它极大地简化和简化了网络编程,如TCP和UDP套接字服务器的开发。

线程模型与异步处理

线程模型

Reactor(反应堆模型)
Reactor模式也叫Dispatcher模式,即I/O多路复用统一监听事件,收到事件后分发(Dispatch给某进程)

Reactor就是一个执行while (true) { selector.select(); …}循环的线程,会源源不断的产生新的事件

1718249102415.png

Reactor模型中有2个关键组成:

  • Reactor Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。
  • Handlers 处理程序执行I/O事件要完成的实际事件

取决于Reactor的数量和Hanndler线程数量的不同,Reactor模型有3个变种

  • 单Reactor单线程
  • 单Reactor多线程
  • 主从Reactor多线程

Netty线程模型

Netty主要基于主从Reactors多线程模型,其中Reactor分为:MainReactor和SubReactor:

  • MainReactor负责客户端的连接请求,并将请求转交给SubReactor
  • SubReactor负责相应通道的IO读写请求
  • 非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理

虽然Netty的线程模型基于主从Reactor多线程,但是实际实现上SubReactor和Worker线程在同一个线程池中

  • bossGroup线程池则只是在bind某个端口后,获得其中一个线程作为MainReactor,专门处理端口的accept事件,每个端口对应一个boss线程
  • workerGroup线程池会被各个SubReactor和worker线程充分利用

异步处理

Netty中的I/O操作是异步的,包括bind、write、connect等操作会简单的返回一个ChannelFuture,调用者并不能立刻获得结果,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。

  • 通过isDone方法来判断当前操作是否完成
  • 通过isSuccess方法来判断已完成的当前操作是否成功
  • 通过getCause方法来获取已完成的当前操作失败的原因
  • 通过isCancelled方法来判断已完成的当前操作是否被取消
  • 通过addListener方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果future对象已完成,则立即通知指定的监听器

内存

Netty中使用了自己实现的ByteBuffer,直接内存,内存池化,引用计数器等技术,重点要看一下 io.netty.buffer.ByteBuf

ByteBuffer

零个或多个字节(八位字节)的随机且顺序可访问的序列。此接口提供一个或多个原始字节数组(byte [])和NIO buffers 的抽象视图。

创建缓冲区

建议在Unpooled中使用辅助方法创建一个新的缓冲区,而不是调用单个实现的构造函数。

随机存取索引

就像普通的原始字节数组一样,ByteBuf使用从零开始的索引。这意味着第一个字节的索引始终为0,而最后一个字节的索引始终为-1。例如,要迭代缓冲区的所有字节,您可以不管其内部实现如何,请执行以下操作:

1
2
3
4
5
ByteBuf buffer = ...;
for (int i = 0; i < buffer.capacity(); i ++) {
byte b = buffer.getByte(i);
System.out.println((char) b);
}

顺序访问索引

ByteBuf提供了两个指针变量来支持顺序读取和写入操作;分别是针对读操作的readerIndex和针对写操作的writerIndex。

无需像使用JDK中的Buffer那样手动flip()

读操作:

名称以read或skip开头的任何操作都将获取或跳过当前readerIndex处的数据,并将其增加读取字节数。如果读取操作的参数是ByteBuf,并且未指定目标索引,则指定缓冲区的writerIndex会一起增加。
如果没有足够的内容,则会引发IndexOutOfBoundsException。
新分配,包装或复制的缓冲区的readerIndex的默认值为0。

1
2
3
4
5
// 迭代缓冲区的可读字节
ByteBuf buffer = ...;
while (buffer.isReadable()) {
System.out.println(buffer.readByte());
}

写操作:

名称以write开头的任何操作都将在当前writerIndex处写入数据,并将增加其writerIndex的计数。如果写操作的参数是ByteBuf,并且未指定源索引,则指定缓冲区的readerIndex会一起增加。
如果没有足够的可写字节,则引发IndexOutOfBoundsException。新分配的缓冲区的writerIndex的默认值为0。包装或复制的缓冲区的writerIndex的默认值为缓冲区的容量。

1
2
3
4
5
// 用随机整数填充缓冲区的可写字节
ByteBuf buffer = ...;
while (buffer.maxWritableBytes() >= 4) {
buffer.writeInt(random.nextInt());
}

丢弃已经读取的字节:

通过调用discardReadBytes()来丢弃已经读取的字节,以回收使用的字节区域。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 调用 discardReadBytes() 之前 (已经读取了一定的字节)

+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity


调用 discardReadBytes()之后 (相关指针前移,已经读取过的内容被丢弃)

+------------------+--------------------------------------+
| readable bytes | writable bytes (got more space) |
+------------------+--------------------------------------+
| | |
readerIndex (0) <= writerIndex (decreased) <= capacity

清除缓冲区索引:

可以通过调用clear()将readerIndex和writerIndex都设置为0,它不会清除缓冲区内容(例如填充0),而只是清除两个指针。

请注意,此操作的语义与java.nio.ByteBuffer.clear()不同。

搜索操作

对于简单的单字节搜索,请使用indexOf(int,int,byte) 和 bytesBefore(int,int,byte) 。当处理以NUL结尾的字符串时,bytesBefore(byte) 特别有用。对于复杂的搜索,请使用带有ByteProcessorimplementation的forEachByte(int,int,ByteProcessor)。

标记和重置

每个缓冲区中都有两个标记索引。一种用于存储readerIndex,另一种用于存储writerIndex。您始终可以通过调用reset方法来重新定位两个索引之一。除了没有readlimit以外,它的工作方式与InputStream中的mark和reset方法类似。

派生缓冲区

您可以通过调用以下方法之一来创建现有缓冲区的视图:

  • duplicate()
  • slice()
  • slice(int, int)
  • readSlice(int)
  • retainedDuplicate()
  • retainedSlice()
  • retainedSlice(int, int)
  • readRetainedSlice(int)

派生的缓冲区将具有独立的readerIndex,writerIndex和标记索引,而它共享其他内部数据表示形式,就像NIO缓冲区一样。
如果需要现有缓冲区的全新副本,请调用 copy( ) 方法。

非保留和保留派生缓冲区

请注意,plicate(),slice(),slice(int,int)和readSlice(int) 不会在返回的派生缓冲区上调用retain(),因此不会增加其引用计数。如果需要创建具有增加的引用计数的派生缓冲区,请考虑使用retainedDuplicate(),retainedSlice(),retainedSlice(int,int) 和 readRetainedSlice(int),这可能会返回产生较少垃圾的缓冲区实现。

转换为现有的JDK类型

字节数组

如果ByteBuf由字节数组(即byte [])支持,则可以直接通过array()方法访问它。要确定缓冲区是否由字节数组支持,应使用hasArray()。

NIO缓冲区

如果ByteBuf可以转换为共享其内容的NIO ByteBuffer(即视图缓冲区),则可以通过nioBuffer()方法获取它。要确定是否可以将缓冲区转换为NIO缓冲区,请使用nioBufferCount()。

String

各种toString(Charset)方法将ByteBufin转换为String。请注意,toString()不是转换方法。

I/O流

请参考ByteBufInputStream和ByteBufOutputStream。


计数引用

ByteBuf 实现了 ReferenceCounted 接口

在Netty handler的设计规范中,所有输入的数据在处理结束时都会调用ReferenceCountUtil.release()释放,只是具体释放方法根据池化、非池化、直接内存、堆内存的不同实现不同。

ReferenceCounted

一个引用计数的对象,需要显式取消分配。

实例化一个新的ReferenceCounted时,它以引用计数为1开头。keep() 增加引用计数,release() 减少引用计数。
如果引用计数减小为0,则将显式释放该对象,并且 访问已释放对象通常会导致访问冲突。

如果实现ReferenceCounted的对象是其他实现ReferenceCounted的对象的容器,则当容器的引用计数变为0时,包含的对象也将通过release() 释放。

ReferenceCountUtil

一个工具方法集合,处理的对象需要实现 ReferenceCounted

直接内存(堆外内存)

实现零拷贝,既使用时无需将内存从系统空间复制到用户空间

主要是指接收和发送 ByteBuffer 使用虚拟机的堆外内存,不用将内存内容拷贝到jvm的堆中

内存块池化

对于堆外内存的申请分配以及释放是一个比较耗时的操作,不能像jvm中一样快(jvm已经提前像系统申请了一块内存空间)
Netty为了重用这些内存,将内存分成一块一块的小内存,然后将内存块像线程池中的线程一样重复使用,来避免重复的分配和回收操作。(据说性能超高,没有测试过)

ByteBufAllocator

实现负责分配缓冲区。这个接口的实现应该是线程安全的。

UnpooledByteBufAllocator

非池化内存分配器

PooledByteBufAllocator

池化内存分配器

io.netty.buffer.Unpooled

通过分配新空间或包装或复制现有字节数组、字节缓冲区和字符串来创建新的ByteBuf。

静态方法

1
2
3
4
5
6
import static io.netty.buffer.Unpooled.*;

ByteBuf heapBuffer = buffer(128);
ByteBuf directBuffer = directBuffer(256);
ByteBuf wrappedBuffer = wrappedBuffer(new byte[128], new byte[256]);
ByteBuf copiedBuffer = copiedBuffer(ByteBuffer.allocate(128));

分配新缓冲区
buffer(int) 分配一个新的固定容量堆缓冲区。
directBuffer(int) 分配一个新的固定容量直接缓冲区。

创建包装的缓冲区
包装缓冲区是一种缓冲区,它是一个或多个现有字节数组和字节缓冲区的视图。原始数组或缓冲区内容的任何更改都将在包装的缓冲区中可见。提供了各种包装方法,它们的名称全为wrappedBuffer()。如果要创建一个由多个数组组成的缓冲区以减少内存副本的数量,则可能需要仔细研究一下接受varargs的方法。

创建复制的缓冲区
复制缓冲区是一个或多个现有字节数组,字节缓冲区或字符串的深层副本。与包装的缓冲区不同,原始数据和复制的缓冲区之间没有共享数据。提供了各种复制方法,它们的名称全为copyedBuffer()。使用此操作将多个缓冲区合并为一个缓冲区也很方便。


重点类介绍

启动辅助类

ServerBootstrap

服务端用

ServerBootstrapAcceptor的channelRead方法,
会将进入的连接注册到workerGroup (NioEventLoopGroup)中,之后的读写以及业务操作就是NioEventLoopGroup中的EventLoop来处理了

注册时会有一定的机制(DefaultEventExecutorChooserFactory,就是轮询)来决定用哪一个EventLoop来处理本次连接

Bootstrap

客户端用

bind()方法与无连接传输如数据报(UDP)结合使用非常有用。对于常规TCP连接,请使用提供的connect()方法。

Channel (通道)

网络套接字或能够进行I/O操作(如读、写、连接和绑定)的组件的连接。

通道为用户提供:

  • 通道的当前状态(例如:是否打开,是否是连接状态)
  • 通道的配置参数(例如接收缓冲区大小)
  • 通道支持的I/O操作(例如,读、写、连接和绑定)
  • ChannelPipeline处理与通道相关的所有I/O事件和请求。

所有I/O操作都是异步的
Netty中的所有I/O操作都是异步的。这意味着任何I/O调用将立即返回,而不能保证所请求的I/O操作在调用结束时已经完成。返回一个ChannelFuture实例,该实例将在请求的I/O操作成功、失败或取消时通知你。

释放资源
一旦Channel使用完毕,调用ChannelOutboundInvoker.close()或ChannelOutboundInvoker.close(ChannelPromise)释放所有资源非常重要。这样可确保以适当的方式释放所有资源,如文件描述符。

SocketChannel

TCP/IP套接字通道

两个常用子类
NioServerSocketChannel 用于服务端,它使用基于NIO选择器的实现来接受新连接。
NioSocketChannel 用于客户端,它使用基于NIO选择器的实现

DatagramChannel

UDP/IP 通道

常用子类
NioDatagramChannel NIO的UDP数据包通道

NioEventLoop (事件循环)

SingleThreadEventLoop的实现,它将Channel注册到选择器中,并且在事件循环中对它们进行多路复用。
该类在单个线程中以有序/串行的方式执行提交的所有任务。

本质上是一个单线程的线程池,通过execute方法提交的任务都将被这个Thread线程来执行

在注册时会执行execute方法,调用doStartThread,启动事件循环

调用doStartThread 只会执行一次

代码在:

SingleThreadEventExecutor.doStartThread()
SingleThreadEventExecutor.this.run();

里面有个固定无限循环,来判断selector中是否有事件

就像我们写NOI的那个循环一样

NioEventLoop.run()

还实现了:ScheduledExecutorService 调度线程池

channel 会绑定到一个eventLoop中,这个channel对应的所有操作都会由这个eventLoop来执行,
包括所有的IO操作和handler中业务逻辑

但是多个channel 可能会被分配到同一个eventLoop中

因为是单线程的,它将以有序/串行方式处理所有提交的任务,这样不必要考虑并发同步的问题。

NioEventLoopGroup (事件循环组)

MultithreadEventLoopGroup的实现,用于基于NIO 选择器的Channel。
它同时使用多个线程处理它的任务,通过它的next()方法提供要使用的EventExecutor。除此之外,它还负责处理它们的生命周期,并允许以全局方式关闭它们。

new NioEventLoopGroup() 没有参数时:

  • 默认的NioEventLoop的数量是机器CPU数量的两倍
  • 默认的拒绝策略是:拒绝(抛异常)
  • eventLoop做事件循环时的Selector如果没有指定将使用系统默认的,如windows的WindowsSelectorProvider

NioEventLoopGroup是用来管理NioEventLoop的,里面有一个 EventExecutor[] children; 管理着这个组下的全部事件循环EventLoop对象

初始化时会实例化全部的NioEventLoop对象,通过 EventLoop newChild(Executor executor, Object... args) 方法

channel初始化时会进行注册,注册通道时调用 next().register(channel)

服务端程序在 ServerBootstrapAcceptor 的channelRead 方法中将新的channel注册到workerGroup中

next方法会通过EventExecutorChooser(默认轮询)来获取EventExecutor(NioEventLoop)

bossGroup 只需要一个 NioEventLoop 就可以了,只负责将channel注册到 workerGroup 中
workerGroup 默认CPU核心数*2 个NioEventLoop

ChannelPipeline (管道)

ChannelHandler的列表,用于处理或拦截Channel的入站事件和出站操作。
ChannelPipeline实现了一种高级的拦截、过滤模式,以使用户可以完全控制事件的处理方式以及管道中的ChannelHandler如何相互交互。

每个Channel都有其自己的ChannelPipeline,并且在创建新Channel时会自动创建它。

下图描述了ChannelPipeline典型地ChannelHandler如何处理I/O事件。 I/O事件由ChannelInboundHandler或ChannelOutboundHandler处理,并通过调用ChannelHandlerContext中定义的事件传播方法(例如ChannelHandlerContext.fireChannelRead(Object)和ChannelHandlerContext.write(Object))转发到其最近的处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
                                               I/O Request
via {@link Channel} or
{@link ChannelHandlerContext}
|
+---------------------------------------------------+---------------+
| ChannelPipeline | |
| \|/ |
| +---------------------+ +-----------+----------+ |
| | Inbound Handler N | | Outbound Handler 1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler N-1 | | Outbound Handler 2 | |
| +----------+----------+ +-----------+----------+ |
| /|\ . |
| . . |
| ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
| [ method call] [method call] |
| . . |
| . \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 2 | | Outbound Handler M-1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 1 | | Outbound Handler M | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
+---------------+-----------------------------------+---------------+
| \|/
+---------------+-----------------------------------+---------------+
| | | |
| [ Socket.read() ] [ Socket.write() ] |
| |
| Netty Internal I/O Threads (Transport Implementation) |
+-------------------------------------------------------------------+

入站事件由入站处理程序在自下而上的方向上进行处理,如该图的左侧所示。入站处理程序通常处理由图底部的I/O线程生成的入站数据。通常通过实际的输入操作(例如SocketChannel.read(ByteBuffer))从远程对等方读取入站数据。如果入站事件超出了顶部入站处理程序的范围,则将其静默丢弃,或者在需要引起注意时将其记录下来。

出站事件由出站处理程序按自上而下的方向进行处理,如该图的右侧所示。出站处理程序通常会生成或转换出站流量(例如写请求)。如果出站事件超出了底部出站处理程序,则由与通道关联的I/O线程处理。 I/O线程通常执行实际的输出操作,例如SocketChannel.write(ByteBuffer)。

假设我们创建了如下的管道

1
2
3
4
5
6
ChannelPipeline p = ...;
p.addLast("1", new InboundHandlerA());
p.addLast("2", new InboundHandlerB());
p.addLast("3", new OutboundHandlerA());
p.addLast("4", new OutboundHandlerB());
p.addLast("5", new InboundOutboundHandlerX());

在上面的示例中,名称以Inbound开头的类表示它是一个入站处理程序。名称以Outbound开头的类表示它是一个出站处理程序。
在给定的示例配置中,事件进入时处理程序的评估顺序为1、2、3、4、5。事件离开时,处理程序的评估顺序为5、4、3、2、1。

ChannelPipeline跳过某些处理程序的评估,以缩短堆栈深度:

  • 3和4没有实现ChannelInboundHandler,因此入站事件的实际评估顺序为:1、2和5。
  • 1和2没有实现ChannelOutboundHandler,因此出站事件的实际评估顺序为:5、4和3。
  • 如5同时实现ChannelInboundHandler和ChannelOutboundHandler,则入站和出站事件的评估顺序可能分别为1 2 5和5 4 3。

ChannelPipeline是线程安全的,也就是说,我们可以动态的添加、删除其中的ChannelHandler。考虑这样的场景:服务器需要对用户登录信息进行加密,而其他信息不加密,则可以首先将加密Handler添加到ChannelPipeline,验证完用户信息后,主动从ChnanelPipeline中删除,从而实现该需求

ChannelHandlerContext (处理器的上下文)

channelHandler 的上下文对象,使ChannelHandler可以与ChannelPipeline中的其他Handler进行交互。
使用上下文对象,ChannelHandler可以在上下游传递事件,动态修改管道(在管道中动态的插入或删除ChannelHandler)或存储特定于处理程序的信息(使用AttributeKeys)。

结合ChannelPipeline
处理程序必须调用ChannelHandlerContext中的事件传播方法,以将事件转发到其下一个处理程序。这些方法包括:

入站事件传播方法:

1
2
3
4
5
6
7
8
9
ChannelHandlerContext.fireChannelRegistered()
ChannelHandlerContext.fireChannelActive()
ChannelHandlerContext.fireChannelRead(Object)
ChannelHandlerContext.fireChannelReadComplete()
ChannelHandlerContext.fireExceptionCaught(Throwable)
ChannelHandlerContext.fireUserEventTriggered(Object)
ChannelHandlerContext.fireChannelWritabilityChanged()
ChannelHandlerContext.fireChannelInactive()
ChannelHandlerContext.fireChannelUnregistered()

出站事件传播方法:

1
2
3
4
5
6
7
8
ChannelHandlerContext.bind(SocketAddress,ChannelPromise)
ChannelHandlerContext.connect(SocketAddress,SocketAddress,ChannelPromise)
ChannelHandlerContext.write(Object,ChannelPromise)
ChannelHandlerContext.flush()
ChannelHandlerContext.read()
ChannelHandlerContext.disconnect(ChannelPromise)
ChannelHandlerContext.close(ChannelPromise)
ChannelHandlerContext.deregister(ChannelPromise)

如何进行事件传播:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyInboundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("Connected!");
ctx.fireChannelActive();
}
}

public class MyOutboundHandler extends ChannelOutboundHandlerAdapter {
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) {
System.out.println("Closing ..");
ctx.close(promise);
}
}

ChannelInitializer(通道初始化器)

一个特殊的ChannelHandler,用于给ChannelPipeline绑定handler
在连接建立时会调用handlerAdded方法然后调用initChannel方法,然后执行我们编写的
channelPipeline.addLast(new StringDecoder())
等代码,这里使用的是new 一个新对象,这样就能给每一个连接创建独立的handler实例了,这些实例就可以保存各种状态


ChannelHandler(处理器)

处理网络IO事件,对消息进行编码解码,和一定的业务逻辑处理。我们主要就实现各种Handler。

过长时间的业务逻辑代码如果编写在ChannelHandler中,不能直接在workerGroup线程池中处理,可能阻塞其他的IO操作
通过外部线程池,或指定Handler的执行线程池来处理

通常必须实现下面的两个子类之一:

  1. ChannelInboundHandler 处理入站I/O事件
  2. ChannelOutboundHandler 处理出站I/O事件

另外还提供了以下适配器类:

  • ChannelInboundHandlerAdapter 处理入站I/O事件
  • ChannelOutboundHandlerAdapter 处理出站I/事件
  • ChannelDuplexHandler 可以处理入站和出站事件

Handler中的状态管理

1. 在Handler实现类中使用成员变量控制状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DataServerHandler extends SimpleChannelInboundHandler<Message> {

private boolean loggedIn;

@Override
public void channelRead0(ChannelHandlerContext ctx, Message message) {
if (message instanceof LoginMessage) {
authenticate((LoginMessage) message);
loggedIn = true;
} else (message instanceof GetDataMessage) {
if (loggedIn) {
ctx.writeAndFlush(fetchSecret((GetDataMessage) message));
} else {
fail();
}
}
}
...
}

因为处理程序实例具有专用于一个连接的状态变量,所以您必须为每个新通道创建一个新的处理程序实例

1
2
3
4
5
6
public class DataServerInitializer extends ChannelInitializer<Channel> {
@Override
public void initChannel(Channel channel) {
channel.pipeline().addLast("handler", new DataServerHandler());
}
}
2. 使用ChannelHandlerContext提供的AttributeKey

当不想创建许多处理器的实例时,用可以使用ChannelHandlerContext提供的AttributeKey来存储程序的状态信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Sharable
public class DataServerHandler extends SimpleChannelInboundHandler<Message> {
private final AttributeKey<Boolean> auth =
AttributeKey.valueOf("auth");

@Override
public void channelRead(ChannelHandlerContext ctx, Message message) {
Attribute<Boolean> attr = ctx.attr(auth);
if (message instanceof LoginMessage) {
authenticate((LoginMessage) o);
attr.set(true);
} else (message instanceof GetDataMessage) {
if (Boolean.TRUE.equals(attr.get())) {
ctx.writeAndFlush(fetchSecret((GetDataMessage) o));
} else {
fail();
}
}
}
...
}

只需要一个处理器的实例即可

1
2
3
4
5
6
7
8
9
10
public class DataServerInitializer extends ChannelInitializer<Channel> {

private static final DataServerHandler SHARED = new DataServerHandler();

@Override
public void initChannel(Channel channel) {
channel.pipeline().addLast("handler", SHARED);
}
}

要注意这些ChannelHandler的线程安全性

@Sharable注解

提供此注释是为了文档目的,就像JCIP注释一样。实际上是否创建多个实例取决于上面的两种写法

如果ChannelHandler使用@Sharable注解进行标注,则表示可以只创建一次该处理程器的实例,然后将其多次添加到一个或多个ChannelPipelines中

如果未指定此注释,则每次将其添加到管道时都必须创建一个新的处理程序实例,因为它具有未共享的状态,例如成员变量。


其他

ChannelOption

ChannelOption允许以类型安全的方式配置ChannelConfig。 支持哪个ChannelOption取决于ChannelConfig的实际实现,并且可能取决于其所属传输的性质。

ChannelConfig

通道的一组配置属性

请向下转换为更特定的配置类型,例如SocketChannelConfig或使用setOptions(Map)设置特定于传输的属性:

1
2
3
Channel ch = ...;
SocketChannelConfig cfg = (SocketChannelConfig) ch.getConfig();
cfg.setTcpNoDelay(false);

Option map

选项map 属性是动态只写属性,它允许配置Channel而不向下转换其关联的ChannelConfig。要更新选项map,请调用setOptions(Map)。
所有ChannelConfig都具有以下选项:

名称 关联的setter方法
ChannelOption.CONNECT_TIMEOUT_MILLIS setConnectTimeoutMillis(int)
ChannelOption.WRITE_SPIN_COUNT setWriteSpinCount(int)
ChannelOption.WRITE_BUFFER_WATER_MARK setWriteBufferWaterMark(WriteBufferWaterMark)
ChannelOption.ALLOCATOR setAllocator(ByteBufAllocator)
ChannelOption.AUTO_READ setAutoRead(boolean)

SocketChannelConfig

可用选项

除了ChannelConfig提供的选项外,SocketChannelConfig允许在选项映射中使用以下选项:

名称 关联的setter方法
ChannelOption.SO_KEEPALIVE setKeepAlive(boolean)
ChannelOption.SO_REUSEADDR setReuseAddress(boolean)
ChannelOption.SO_LINGER setSoLinger(int)
ChannelOption.TCP_NODELAY setTcpNoDelay(boolean)
ChannelOption.SO_RCVBUF setReceiveBufferSize(int)
ChannelOption.SO_SNDBUF setSendBufferSize(int)
ChannelOption.IP_TOS setTrafficClass(int)
ChannelOption.ALLOW_HALF_CLOSURE setAllowHalfClosure(boolean)

主动释放对象

这里主要指的是ByteBuf

ByteBuf是一个引用计数对象(ReferenceCounted),必须通过该release()方法显式释放它。请记住,释放任何传递给处理程序的引用计数对象是处理程序的责任。通常,channelRead()处理程序方法的实现如下:

1
2
3
4
5
6
7
8
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
// Do something with msg
} finally {
ReferenceCountUtil.release(msg);
}
}

读数据

从字节流到数据包(碎片化问题)

在基于流的传输(例如TCP / IP)中,将接收到的数据存储到套接字接收缓冲区中。基于流的传输的缓冲区不是数据包队列而是字节队列。这意味着,即使您将两个消息作为两个独立的数据包发送,操作系统也不会将它们视为两个消息,而只是一堆字节。因此,不能保证你读到的内容与远程对等方写的完全一样。

例如,让我们假设操作系统的TCP/IP堆栈已收到三个数据包:

【ABC】【DEF】【GHI】

由于是基于流的传输协议,因此很有可能在你的应用程序中以以下分段形式读取它们:

【AB】【CDEFG】【H】【I】

因此,无论是服务器端还是客户端,接收方都应将接收到的数据整理到一个或多个有意义的帧中,以使应用程序逻辑易于理解。在上面的示例中,接收到的数据应采用以下格式:

【ABC】【DEF】【GHI】

处理方案一

创建一个内部累积缓冲区,然后等待直到所有的字节都被接收到内部缓冲区中为止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package io.netty.example.time;

import java.util.Date;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private ByteBuf buf;

@Override
public void handlerAdded(ChannelHandlerContext ctx) {
buf = ctx.alloc().buffer(4); // (1)
}

@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
buf.release(); // (1)
buf = null;
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg;
buf.writeBytes(m); // (2)
m.release();

if (buf.readableBytes() >= 4) { // (3)
long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));
ctx.close();
}
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
  1. 一个ChannelHandler有两个生命周期侦听器方法:handlerAdded()和handlerRemoved()。可以执行任意(取消)初始化任务,只要它不会长时间阻塞即可。
  2. 应将所有接收到的数据累加到中buf
  3. 然后,处理程序必须检查是否buf有足够的数据(在此示例中为4个字节),然后继续进行实际的业务逻辑。否则,Netty将channelRead()在有更多数据到达时再次调用该方法,最终将累加所有4个字节。
处理方案二

添加多个ChannelHandler到ChannelPipeline,拆分一个ChannelHandler成多个,使其模块化减少应用程序的复杂性。

使用编码解码器

1
2
3
4
5
6
7
8
9
10
11
12
package io.netty.example.time;

public class TimeDecoder extends ByteToMessageDecoder { // (1)
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // (2)
if (in.readableBytes() < 4) {
return; // (3)
}

out.add(in.readBytes(4)); // (4)
}
}
  1. ByteToMessageDecoder是一个实现ChannelInboundHandler,可以轻松处理碎片问题。
  2. ByteToMessageDecoderdecode()每当接收到新数据时,都使用内部维护的累积缓冲区调用该方法。
  3. decode()可以决定out不向累积缓冲区中没有足够数据的位置添加任何内容。收到更多数据时ByteToMessageDecoder将decode()再次调用。
  4. 如果decode()将对象添加到out,则表示解码器成功解码了一条消息。ByteToMessageDecoder将丢弃累积缓冲区的读取部分。请记住,您不需要解码多条消息。ByteToMessageDecoder会一直调用该decode()方法,直到该方法不添加任何内容out。

累积缓冲区有两种实现

  1. MERGE_CUMULATOR
    通过使用内存副本,将它们合并到一个ByteBuf中来累积ByteBufs。
  2. COMPOSITE_CUMULATOR
    通过将bytebuf添加到CompositeByteBuf中来累积bytebuf,因此尽可能不进行内存复制。注意CompositeByteBuf使用了一个更复杂的索引实现,所以取决于你的用例和解码器实现,这可能比仅仅使用merge_accumulator要慢。

或者使用重复解码器 ReplayingDecoder

ByteToMessageDecoder的变体,使得能够在阻塞I/O范例中实现非阻塞解码器。
ReplayingDecoder您可以像已收到所有必需字节一样实现decode()和decodeLast()方法,而不用检查所需字节的可用性。

1
2
3
4
5
6
7
public class TimeDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(
ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
out.add(in.readBytes(4));
}
}

ReplayingDecoder通过一个特殊的ByteBuf 实现,Error当缓冲区中没有足够的数据时,该实现将抛出某种类型的实现。在上面的例子中只是假设调用时缓冲区中将有4个或更多字节。如果缓冲区中确实有4个字节,它将按预期返回整数标头。否则将引发Error。如果ReplayingDecoder捕获到 Error,则它将readerIndex把缓冲区的内容倒回到“初始”位置(即缓冲区的开头),并decode(..)在缓冲区中接收到更多数据时再次调用该方法。

详见:https://netty.io/4.1/api/io/netty/handler/codec/ReplayingDecoder.html

此外,Netty提供了开箱即用的解码器,使您能够非常轻松地实现大多数协议

  • io.netty.example.factorial 对于二进制协议
  • io.netty.example.telnet 用于基于文本行的协议

用POJO代替ByteBuf

在handler中将字节流转成对象,然后在调用链中传递,会使我们开发程序变得更方便

经过解码器,将字节流转成对象之后再out出去,在调用链后面的Handler中就可以直接取到对象了

如:
解码器

1
2
3
4
5
6
7
8
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 4) {
return;
}

out.add(new UnixTime(in.readUnsignedInt()));
}

handler

1
2
3
4
5
6
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
UnixTime m = (UnixTime) msg;
System.out.println(m);
ctx.close();
}

写数据

通过ChannelHandlerContext的write方法异步发送应答消息个客户端
数据不直接写入SocketChannel,而是写入待发送数据的缓存区中

ChannelHandlerContext的flush方法将队列中的消息写入到SockChannel中

优雅关闭

原理同线程池的关闭

channel.closeFuture().sync();

关闭所有事件循环以终止所有线程

1
2
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

耗时任务处理

bossGroup用于来接受连接,workGroup用于处理业务逻辑和IO操作,如果在handler中执行的业务逻辑特别慢会阻塞IO操作

处理方法

一、在hander中启动新的线程来执行耗时操作

二、告诉netty不要用之前定义的workGroup 来执行某些hander

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 事件处理线程
EventExecutorGroup eventExecutorGroup = new DefaultEventExecutorGroup(16);

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new StringDecoder());
p.addLast(new StringEncoder());

// 如果你的业务逻辑是完全异步的,或者完成得非常快,那么就不需要这样做
// 需要指定一个EventExecutorGroup,将用于执行ChannelHandler中的方法
p.addLast(eventExecutorGroup, "String Echo", new StringEchoHandler());
}
});


工具类



大部分都是代码中的注释翻译过来的,没写完,有空再补充吧!

常用功能

心跳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//心跳检测,6分钟无心跳,触发关闭
public void initChannel(SocketChannel ch) throws Exception {
......
pipeline.addLast( new IdleStateHandler( 6, 0, 0, TimeUnit.MINUTES ) );
......
}

//6分钟没有读取到消息,会触发:userEventTriggered 方法

XxxxHandler

@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
logger.warn("到达指定时间间隔没有收到心跳,关闭连接:{}", ctx.channel().remoteAddress());
ctx.fireUserEventTriggered(evt);
ctx.close();
}
} else {
super.userEventTriggered(ctx, evt);
}
}

ARP协议

地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。
主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。

RARP协议

反向地址转换协议(RARP)是局域网的物理机器从网关服务器的ARP表或者缓存上根据MAC地址请求IP地址的协议,其功能与地址解析协议相反。与ARP相比,RARP的工作流程也相反。首先是查询主机向网路送出一个RARP Request广播封包,向别的主机查询自己的IP地址。这时候网络上的RARP服务器就会将发送端的IP地址用RARP Reply封包回应给查询者,这样查询主机就获得自己的IP地址了。

为什么需要这个

数据包在物理链路上传输 以太网帧 需要目的地的物理地址(MAC)
通过ARP协议来获取同一个网络内的机器的IP地址和Mac的对应关系,为上层协议提供支持,因为上层协议使用IP地址进行通信。

  • 局域网内的IP地址(通过子网掩码计算)之间的通信,就可以理解为设备(MAC)对设备(MAC)之间的通信
  • 跨局域网的通信就是设备(MAC)对 网关(MAC)之间的通信

ARP 数据包

Wireshark 抓包

ARP 广播请求 (由192.168.1.88 发起的广播请求,询问谁的IP是192.168.1.1)

1
4595	296.111634	Giga-Byt_18:04:4a	Broadcast	ARP	42	Who has 192.168.1.1? Tell 192.168.1.88

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Frame 4595: 42 bytes on wire (336 bits), 42 bytes captured (336 bits) on interface 0
Ethernet II, Src: Giga-Byt_18:04:4a (e0:d5:5e:18:04:4a), Dst: Broadcast (ff:ff:ff:ff:ff:ff)
Destination: Broadcast (ff:ff:ff:ff:ff:ff)
Source: Giga-Byt_18:04:4a (e0:d5:5e:18:04:4a)
Type: ARP (0x0806)
Address Resolution Protocol (request)
Hardware type: Ethernet (1)
Protocol type: IPv4 (0x0800)
Hardware size: 6
Protocol size: 4
Opcode: request (1)
Sender MAC address: Giga-Byt_18:04:4a (e0:d5:5e:18:04:4a)
Sender IP address: 192.168.1.88
Target MAC address: 00:00:00_00:00:00 (00:00:00:00:00:00)
Target IP address: 192.168.1.1

ARP 应答 (192.168.1.1 直接应答 192.168.1.88,告知其Mac地址)

1
4596	296.111872	NewH3CTe_95:65:a9	Giga-Byt_18:04:4a	ARP	60	192.168.1.1 is at 04:40:a9:95:65:a9

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Frame 4596: 60 bytes on wire (480 bits), 60 bytes captured (480 bits) on interface 0
Ethernet II, Src: NewH3CTe_95:65:a9 (04:40:a9:95:65:a9), Dst: Giga-Byt_18:04:4a (e0:d5:5e:18:04:4a)
Destination: Giga-Byt_18:04:4a (e0:d5:5e:18:04:4a)
Source: NewH3CTe_95:65:a9 (04:40:a9:95:65:a9)
Type: ARP (0x0806)
Padding: 000000000000000000000000000000000000
Address Resolution Protocol (reply)
Hardware type: Ethernet (1)
Protocol type: IPv4 (0x0800)
Hardware size: 6
Protocol size: 4
Opcode: reply (2)
Sender MAC address: NewH3CTe_95:65:a9 (04:40:a9:95:65:a9)
Sender IP address: 192.168.1.1
Target MAC address: Giga-Byt_18:04:4a (e0:d5:5e:18:04:4a)
Target IP address: 192.168.1.88

其他收到该广播的机器判断【192.168.1.1】不是自己的IP,直接丢弃

ARP 命令说明

windows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
显示和修改地址解析协议(ARP)使用的“IP 到物理”地址转换表。

ARP -s inet_addr eth_addr [if_addr]
ARP -d inet_addr [if_addr]
ARP -a [inet_addr] [-N if_addr] [-v]

-a 通过询问当前协议数据,显示当前 ARP 项。
如果指定 inet_addr,则只显示指定计算机
的 IP 地址和物理地址。如果不止一个网络
接口使用 ARP,则显示每个 ARP 表的项。
-g 与 -a 相同。
-v 在详细模式下显示当前 ARP 项。所有无效项
和环回接口上的项都将显示。
inet_addr 指定 Internet 地址。
-N if_addr 显示 if_addr 指定的网络接口的 ARP 项。
-d 删除 inet_addr 指定的主机。inet_addr 可
以是通配符 *,以删除所有主机。
-s 添加主机并且将 Internet 地址 inet_addr
与物理地址 eth_addr 相关联。物理地址是用
连字符分隔的 6 个十六进制字节。该项是永久的。
eth_addr 指定物理地址。
if_addr 如果存在,此项指定地址转换表应修改的接口
的 Internet 地址。如果不存在,则使用第一
个适用的接口。
示例:
> arp -s 157.55.85.212 00-aa-00-62-c6-09.... 添加静态项。
> arp -a .... 显示 ARP 表。

linux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# man arp
ARP(8) Linux Programmer’s Manual ARP(8)
NAME
arp - manipulate the system ARP cache
SYNOPSIS
arp [-evn] [-H type] [-i if] -a [hostname]
arp [-v] [-i if] -d hostname [pub]
arp [-v] [-H type] [-i if] -s hostname hw_addr [temp]
arp [-v] [-H type] [-i if] -s hostname hw_addr [netmask nm] pub
arp [-v] [-H type] [-i if] -Ds hostname ifa [netmask nm] pub
arp [-vnD] [-H type] [-i if] -f [filename]
NOTE
This program is obsolete. For replacement check ip neighbor.
DESCRIPTION
Arp manipulates the kernel’s ARP cache in various ways. The primary options are clearing an address mapping entry and manually setting up one. For debugging purposes, the arp pro-
gram also allows a complete dump of the ARP cache.
......
......

测试

ping不存在的局域网机器

1
2
3
4
5
6
7
8
9
10
11
# cmd
C:\Users\Administrator>ping 192.168.1.111

正在 Ping 192.168.1.111 具有 32 字节的数据:
来自 192.168.1.88 的回复: 无法访问目标主机。
来自 192.168.1.88 的回复: 无法访问目标主机。
来自 192.168.1.88 的回复: 无法访问目标主机。
来自 192.168.1.88 的回复: 无法访问目标主机。

192.168.1.111 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),

wireshark 抓包

过滤条件:arp.dst.proto_ipv4==192.168.1.111 or icmp

1
2
3
4
2910	206.611909	Giga-Byt_18:04:4a	Broadcast	ARP	42	Who has 192.168.1.111? Tell 192.168.1.88
2917 207.570302 Giga-Byt_18:04:4a Broadcast ARP 42 Who has 192.168.1.111? Tell 192.168.1.88
2924 208.570334 Giga-Byt_18:04:4a Broadcast ARP 42 Who has 192.168.1.111? Tell 192.168.1.88
......

只有arp请求

结论

可以看到只会发送ARP请求,且ARP请求没有获得响应,此时无法获取ping的目的地,故不会发送ping(ICMP)请求
且错误显示为:“无法访问目标主机”

添加静态arp映射后再ping

先添加IP对MAC的映射关系

1
arp -s 192.168.1.111 00-2b-3c-f0-ff-ff

用 arp -a 查看结果

1
2
3
4
5
6
7
C:\Users\Administrator>arp -a

接口: 192.168.1.88 --- 0xb
Internet 地址 物理地址 类型
192.168.1.1 04-40-a9-95-65-a9 动态
192.168.1.111 00-2b-3c-f0-ff-ff 静态
......

ping 测试

1
2
3
4
5
6
7
8
9
10
C:\Users\Administrator>ping 192.168.1.111

正在 Ping 192.168.1.111 具有 32 字节的数据:
请求超时。
请求超时。
请求超时。
请求超时。

192.168.1.111 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 0,丢失 = 4 (100% 丢失),

wireshark 抓包

过滤条件:arp.dst.proto_ipv4==192.168.1.111 or icmp

1
2
3
275	19.179418	192.168.1.88	192.168.1.111	ICMP	74	Echo (ping) request  id=0x0001, seq=26/6656, ttl=64 (no response found!)
315 23.681918 192.168.1.88 192.168.1.111 ICMP 74 Echo (ping) request id=0x0001, seq=27/6912, ttl=64 (no response found!)
......

只有ping

数据报详情:

1
2
3
4
5
6
7
8
Frame 315: 74 bytes on wire (592 bits), 74 bytes captured (592 bits) on interface 0
Ethernet II, Src: Giga-Byt_18:04:4a (e0:d5:5e:18:04:4a), Dst: 00:2b:3c:f0:ff:ff (00:2b:3c:f0:ff:ff)
Destination: 00:2b:3c:f0:ff:ff (00:2b:3c:f0:ff:ff)
Source: Giga-Byt_18:04:4a (e0:d5:5e:18:04:4a)
Type: IPv4 (0x0800)
Internet Protocol Version 4, Src: 192.168.1.88, Dst: 192.168.1.111
Internet Control Message Protocol

结论:

当本地的Arp缓存中有IP地址和Mac地址的对应关系时,会直接往目标地址发送数据报,从以太网帧中可以看到目的地Mac就是我们设置的Mac。
此时不会再发送ARP请求,直接发送了Ping请求,且错误显示为:“请求超时”

修改网关Mac,让本机无法上网

找网关地址

1
2
3
4
5
6
7
8
>ipconfig

以太网适配器 本地连接:

连接特定的 DNS 后缀 . . . . . . . :
IPv4 地址 . . . . . . . . . . . . : 192.168.1.88
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.1.1

查网关真实的Mac地址

1
2
3
C:\Users\Administrator>arp -a | findstr 192.168.1.1
192.168.1.1 04-40-a9-95-66-a9 动态
192.168.1.111 00-2b-3c-f0-ff-ff 静态

修改本地arp缓存中的网关Mac地址

这里修改 192.168.1.1 的Mac地址
arp 命令修改失败:拒绝访问

1
2
3
4
arp -d 192.168.1.1
arp -s 192.168.1.1 00-2b-3c-f0-ff-f1 192.168.1.88
ARP 项添加失败: 拒绝访问。

改用netsh命令修改

  1. 先找本地网卡的 Idx,这里是:11
    1
    2
    3
    4
    5
    6
    7
    netsh i i show in

    Idx Met MTU 状态 名称
    --- ---------- ---------- ------------ ---------------------------
    1 50 4294967295 connected Loopback Pseudo-Interface 1
    11 10 1500 connected 本地连接
    16 30 1500 connected Npcap Loopback Adapter
  2. 使用如下命令修改本地Mac地址缓存
    1
    2
    3
    4
    netsh -c "i i" add neighbors 11 "网关IP" "Mac地址"  -- 这里11是idx号。

    netsh -c "i i" add neighbors 11 "192.168.1.1" "00-2b-3c-f0-ff-f1"

  3. 验证修改结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    arp -a

    C:\Users\Administrator>arp -a

    接口: 192.168.1.88 --- 0xb
    Internet 地址 物理地址 类型
    192.168.1.1 00-2b-3c-f0-ff-f1 静态
    192.168.1.171 d4-5d-df-01-32-f0 动态
    ......
    ......

导致的结果是本机不能再上网了,但局域网访问正常

比如在浏览器中访问百度

使用wireshar 抓包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 这里先进行DNS解析

6 0.783394 192.168.1.88 114.114.114.114 DNS 73 Standard query 0x341b A www.baidu.com

# 可以看到直接将数据包发送到了我们上面配置的假Mac地址: 00-2b-3c-f0-ff-f1

Frame 6: 73 bytes on wire (584 bits), 73 bytes captured (584 bits) on interface 0
Ethernet II, Src: Giga-Byt_18:04:4a (e0:d5:5e:18:04:4a), Dst: 00:2b:3c:f0:ff:f1 (00:2b:3c:f0:ff:f1)
Destination: 00:2b:3c:f0:ff:f1 (00:2b:3c:f0:ff:f1)
Source: Giga-Byt_18:04:4a (e0:d5:5e:18:04:4a)
Type: IPv4 (0x0800)
Internet Protocol Version 4, Src: 192.168.1.88, Dst: 114.114.114.114
User Datagram Protocol, Src Port: 64752, Dst Port: 53
Domain Name System (query)

还原网关对应的mac地址

使用netsh 命令设置之后,再用 arp -d 192.168.1.1命令删除,在重启之前是可以上网的,下次重启网关的mac地址还将是错误的

查看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
netsh -c "i i" show neighbors 11

C:\Users\Administrator>netsh -c "i i" show neighbors 11

接口 11: 本地连接


Internet 地址 物理地址 类型
-------------------------------------------- ----------------- -----------
192.168.1.1 00-2b-3c-f0-ff-f1 永久
192.168.1.11 00-0c-29-47-fd-8d 停滞
192.168.1.63 00-00-00-00-00-00 无法访问
192.168.1.92 00-00-00-00-00-00 无法访问
......
删除:
1
2
netsh -c "i i" delete neighbors 11

需要使用上面的命令删除,完了之后就会变成 “可以访问”


ARP欺骗

ARP欺骗可以分成两种情况:

一、欺骗路由器

发送一系列错误的内网MAC地址给路由器,并按照一定的频率不断进行,使真实的地址信息无法通过更新保存在路由器中,结果路由器的所有数据只能发送给错误的MAC地址,造成正常PC无法收到信息。

二、欺骗局域网内的机器

不停发送错误的网关Mac到局域网的机器中,让局域网内的机器建立错误的绑定关系,让局域网内的机器不能正确的将数据包发送到网关设备,导致PC不能上网。

ARP欺骗测试

使用发包工具

……
有时间再试试
……

其他

获取局域网某IP对应的Mac

1
2
3
4
5
> arping 192.168.1.88
ARPING 192.168.1.88 from 192.168.1.216 eth0
Unicast reply from 192.168.1.88 [E0:D5:5E:18:04:4A] 1.767ms
Unicast reply from 192.168.1.88 [E0:D5:5E:18:04:4A] 1.312ms
Unicast reply from 192.168.1.88 [E0:D5:5E:18:04:4A] 1.635ms

测试局域网个中某个IP是否被占用

返回值为1表示已被使用,0表示没有被使用

1
2
3
4
5
6
7
8
9
10
[root@RD-WEBSERVER-03 ~]# arping -D 192.168.1.163 -w 5  
ARPING 192.168.1.163 from 0.0.0.0 eth0
Sent 6 probes (6 broadcast(s))
Received 0 response(s)

[root@RD-WEBSERVER-03 ~]# arping -D 192.168.1.88 -w 5
ARPING 192.168.1.88 from 0.0.0.0 eth0
Unicast reply from 192.168.1.88 [E0:D5:5E:18:04:4A] 1.848ms
Sent 1 probes (1 broadcast(s))
Received 1 response(s)

wireshark

过滤目的地址
arp.dst.proto_ipv4==192.168.1.214

路由器型号 小米R2D

路由器端口

wan 口 编号:4
lan 口 编号:0 2 3
CPU端口: 5


参考资料

https://openwrt.org/zh-cn/doc/uci/network
https://openwrt.org/docs/guide-user/network/vlan/switch
https://openwrt.org/zh/docs/guide-user/network/vlan/switch_configuration

相关资料

一般来说,0、1、2、3是路由器LAN口,4是路由器WAN口,5表示CPU,而5*表示这个接口是trunk

使用“ *”和“ u”分别表示PVID和未标记的端口(因为它们具有隐式标记的CPU端口,因此需要使用“ u”来取消标记) )。

在端口上收到的未标记的数据包将被定向到默认端口VLAN(通常称为PVID)。需要一个单独的config switch_port部分来设置默认端口VLAN
虚拟局域网。

小米是定制的openwrt系统,采用的是博通闭源驱动,因此vlan设置不能采用openwrt的设定方式,必须采用类似于dd-wr闭源驱动nvram set方式才能使vlan生效。具体是修改/etc/config/misc,将相应的vlanXports参数修改成/etc/config/network里面的port端口号,甚至需要修改/etc/init.d/boot里面的nvram vlan配置参数,然后reboot,重启,新的vlan端口充当wan才能生效


原来的

端口编号 5 0 2 3 4
物理接口 CPU (eth0) LAN 1 LAN 2 LAN 3 WAN
VLAN ID 1 (eth0_1) 已标记 未标记 未标记 未标记 禁用
VLAN ID 2 (eth0_2) 已标记 禁用 禁用 禁用 未标记

修改后

端口编号 5 0 2 3 4
物理接口 CPU (eth0) LAN 1 LAN 2 LAN 3 WAN
VLAN ID 1 (eth0_1) 已标记 未标记 未标记 禁用 禁用
VLAN ID 2 (eth0_2) 已标记 禁用 禁用 禁用 已标记(Internet)
VLAN ID 3 (eth0_3) 已标记 禁用 禁用 未标记 已标记 (IPTV)

LAN3口直接连接机顶盒

这个没有测试,因为下面这个更简单







使用robocfg 配置VLAN

上面配置比较麻烦,还是下载一个 robocfg工具,通过工具来进行配置

下载地址:
https://www.right.com.cn/forum/forum.php?mod=viewthread&tid=334441&page=1

就是按照这个弄的

工具说明

Broadcom BCM5325/535x/536x/5311x switch configuration utility

其实这个CPU是 CPU BCM4709C

复制文件

先弄到路由器的硬盘上

为了之后使用方便,再复制文件到/usr/bin目录

1
2
cp /userdisk/data/ftp/robocfg /usr/bin/
cp: can't create '/usr/bin/robocfg': Read-only file system

报错,提示是只读的

以读写方式重新挂载根目录

1
mount -o remount rw /

然后再复制就可以了

加上执行权限

1
chmod +x robocfg

查看现有VLAN配置

robocfg show

1
2
3
4
5
6
7
8
9
10
11
# ./robocfg show
Switch: enabled
Port 0: DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
Port 1: DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
Port 2: 1000FD enabled stp: none vlan: 1 jumbo: off mac: ec:00:00:d4:00:xx
Port 3: DOWN enabled stp: none vlan: 1 jumbo: off mac: d4:00:00:c1:00:xx
Port 4: 1000FD enabled stp: none vlan: 2 jumbo: off mac: 00:00:01:00:00:xx
Port 8: DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
VLANs: BCM5301x enabled mac_check mac_hash
1: vlan1: 0 2 3 5t
2: vlan2: 4 5t

重新配置VLAN

多插拔两次就可以确定 物理网卡 与 port 0 1 2 3 4 的对应关系了

Port 物理端口
Port 0 LAN 口 1
Port 2 LAN 口 2
Port 3 LAN 口 3
Port 4 WAN 口
Port 5 CPU端口
1
robocfg vlan 3 ports "3 4t"

配置之后

1
2
3
4
5
6
7
8
9
10
11
12
13
root@XiaoQiang:~# robocfg vlan 3 ports "3 4t"
root@XiaoQiang:~# robocfg show
Switch: enabled
Port 0: 1000FD enabled stp: none vlan: 1 jumbo: off mac: xx:xx:xx:xx:40:75
Port 1: DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
Port 2: DOWN enabled stp: none vlan: 1 jumbo: off mac: xx:xx:xx:xx:40:75
Port 3: DOWN enabled stp: none vlan: 3 jumbo: off mac: xx:xx:xx:xx:d0:4f
Port 4: 1000FD enabled stp: none vlan: 2 jumbo: off mac: 00:xx:xx:xx:xx:58
Port 8: DOWN enabled stp: none vlan: 1 jumbo: off mac: 00:00:00:00:00:00
VLANs: BCM5301x enabled mac_check mac_hash
1: vlan1: 0 2 3 5t
2: vlan2: 4 5t
3: vlan3: 3 4t

光猫配置

取消 Internet 和 IPTV 连接的端口绑定,使用VLAN绑定

默认的 Internet 是没有VLAN的,IPTV默认有两个VLAN:45 和 47

配置VLAN绑定

用户侧Vlan ID 为上面定义的 3,Wan口Vlan ID 就填写IPTV的Vlan ID

选千兆口

类型 用户侧Vlan WAN侧Vlan
IPTV 单播 3 45
IPTV 组播 3 47

开机自动执行

将上面的命令写入 /etc/rc.local 文件中

1
2
3
......
robocfg vlan 3 ports "3 4t"
exit 0

路由器连接光猫

将路由器的3号lan口与机顶盒用网线连接即可

总结

看4K 高清不卡顿,比wifi稳定,wifi卡是因为干扰太多,弱电箱的位置不好,弱电箱有金属屏蔽了信号,导致看4K高清时偶尔会卡顿,wifi的带宽其实是足够了,机顶盒的网口也是百兆的,wifi还有300兆

网络通信过程中的数据包流转

资料整理自网络的各个地方

首先 我碰到了一个问题,一个数据包从我们的电脑上,经过层层的交换机、路由器到达目标服务器的过程中,数据包会有哪些改动,是如何一步步传递过去又是如何返回回来的?

先需要了解一些基本的概念

网络模型

OSI七层协议模型

OSI(Open System Interconnection)开放系统互连参考模型是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系。

应用层

DHCP · DNS · FTP · Gopher ·GTP · HTTP · IMAP4 · IRC · NNTP · NTP · POP3 · RPC · RTCP · RTP ·RTSP · SIP · SMTP ·SNMP · SSH · SDP · SOAP .STUN. SSDP · TELNET · XMPP
表示层
HTTP/HTML · FTP · Telnet · ASN.1(具有表示层功能)

会话层

ADSP ·ASP ·H.245·ISO-SP ·iSNS ·NetBIOS ·PAP ·RPC·
RTCP ·SMPP ·SCP ·SSH ·ZIP ·SDP(具有会话层功能)

传输层

TCP · UDP · TLS · DCCP · SCTP ·RSVP · PPTP

网络层

IP (IPv4 · IPv6) · ICMP · ICMPv6 · IGMP ·IS-IS · IPsec · BGP · RIP · OSPF ·ARP · RARP

数据链路层

Wi-Fi(IEEE 802.11) · WiMAX(IEEE 802.16) ·ATM · DTM · 令牌环 · 以太网路 ·
FDDI · 帧中继 · GPRS · EVDO · HSPA · HDLC · PPP · L2TP · ISDN ·STP

物理层

以太网路卡 · 调制解调器 · 电力线通信(PLC) · SONET/SDH(光同步数字传输网)
G.709(光传输网络) · 光导纤维 · 同轴电缆 · 双绞线

TCP/IP四层模型

TCP/IP协议栈是美国国防部高级研究计划局计算机网(ARPANET)和其后继因特网使用的参考模型。ARPANET是由美国国防部赞助的研究网络。最初,它只连接了美国境内的四所大学。随后的几年中,它通过租用的电话线连接了数百所大学和政府部门。最终ARPANET发展成为全球规模最大的互连网络-因特网。最初的ARPANET于1990年永久性地关闭。  

ISO制定的OSI参考模型的过于庞大、复杂招致了许多批评。与此对照,由技术人员自己开发的TCP/IP协议栈获得了更为广泛的应用。

应用层
  • · DHCP(动态主机分配协议)
  • · DNS (域名解析)
  • · FTP(File Transfer Protocol)文件传输协议
  • · Gopher (英文原义:The Internet Gopher Protocol 中文释义:(RFC-1436)网际Gopher协议)
  • · HTTP (Hypertext Transfer Protocol)超文本传输协议
  • · IMAP4 (Internet Message Access Protocol 4) 即 Internet信息访问协议的第4版本
  • · IRC (Internet Relay Chat )网络聊天协议
  • · NNTP (Network News Transport Protocol)RFC-977)网络新闻传输协议
  • · XMPP 可扩展消息处理现场协议
  • · POP3 (Post Office Protocol 3)即邮局协议的第3个版本
  • · SIP 信令控制协议
  • · SMTP (Simple Mail Transfer Protocol)即简单邮件传输协议
  • · SNMP (Simple Network Management Protocol,简单网络管理协议)
  • · SSH (Secure Shell)安全外壳协议
  • . SSL: 安全套接字层协议;
  • · TELNET 远程登录协议
  • · RPC (Remote Procedure Call Protocol)(RFC-1831)远程过程调用协议
  • · RTCP (RTP Control Protocol)RTP 控制协议
  • · RTSP (Real Time Streaming Protocol)实时流传输协议
  • · TLS (Transport Layer Security Protocol)传输层安全协议
  • · SDP( Session Description Protocol)会话描述协议
  • · SOAP (Simple Object Access Protocol)简单对象访问协议
  • · GTP 通用数据传输平台
  • · STUN (Simple Traversal of UDP over NATs,NAT 的UDP简单穿越)是一种网络协议
  • · NTP (Network Time Protocol)网络校时协议
传输层
  • ·TCP(Transmission Control Protocol)传输控制协议
  • · UDP (User Datagram Protocol)用户数据报协议
  • · DCCP (Datagram Congestion Control Protocol)数据报拥塞控制协议
  • · SCTP(STREAM CONTROL TRANSMISSION PROTOCOL)流控制传输协议
  • · RTP(Real-time Transport Protocol或简写RTP)实时传送协议
  • · RSVP (Resource ReSer Vation Protocol)资源预留协议
  • · PPTP ( Point to Point Tunneling Protocol)点对点隧道协议
网络层
  • ·IP(IPv4 · IPv6) Internet Protocol(网络之间互连的协议)
  • ·ARP : Address Resolution Protocol即地址解析协议,实现通过IP地址得知其物理地址。
  • ·RARP :Reverse Address Resolution Protocol - 反向地址转换协议允许局域网的物理机器从网关服务器的 ARP 表或者缓存上请求其 IP 地址。
  • ·ICMP :(Internet Control Message - Protocol)Internet控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器- 之间传递控制消息。
  • ·ICMPv6:
  • ·IGMP :Internet 组管理协议(IGMP)是因特网协议家族中的一个组播协议,用于IP - 主机向任一个直接相邻的路由器报告他们的组成员情况。
  • ·RIP : 路由信息协议(RIP)是一种在网关与主机之间交换路由选择信息的标准。
  • ·OSPF : (Open Shortest Path First开放式最短路径优先).
  • ·BGP :(Border Gateway Protocol - )边界网关协议,用来连接Internet上独立系统的路由选择协议
  • ·IS-IS:(Intermediate System to Intermediate System Routing - Protocol)中间系统到中间系统的路由选择协议.
  • ·IPsec:“Internet 协议安全性”是一种开放标准的框架结构,通过使用加密的安全服务以确保- 在Internet 协议 (IP) 网络上进行保密而安全的通讯。
数据链路层

802.11 · 802.16 · Wi-Fi · WiMAX · ATM · DTM · 令牌环 · 以太网 · FDDI · 帧中继 · GPRS · EVDO · HSPA · HDLC · PPP · L2TP · ISDN   

物理层

以太网物理层 · 调制解调器 · PLC · SONET/SDH · G.709 · 光导纤维 · 同轴电缆 · 双绞线

OSI七层和TCP/IP四层的关系

  1. OSI引进了服务、接口、协议、分层的概念,TCP/IP借鉴了OSI的这些概念建立TCP/IP模型。
  2. OSI先有模型,后有协议,先有标准,后进行实践;而TCP/IP则相反,先有协议和应用,再提出了模型,且是参照OSI模型。
  3. OSI是一种理论下的模型,而TCP/IP已经被广泛应用,称为网络互联实施上的标准。

数据包的封装与解析

封包

应用程序产生并发送数据,数据经过层层包装,最后数据将封装成以太网帧,再从链路中发送出去。
通信过程中,每层协议都要加上一个数据首部(header),称为封装(Encapsulation)

解包

数据包到达目标机器后经过相反的过程,最终被目标程序接收

各层的数据报文结构

TCP数据报

IP数据报

以太网数据帧

  • Preamble:前导码,序言:7byte或56bits的长度,为交替的0和1,来进行时钟同步。
  • SFD:Start frame delimiter (SFD)帧开始符号. 该符号 (1 byte: 10101011)表示了下面就是数据了,不能继续用来时钟同步了。10101011与preamble的1结尾相连接,形成2个1作为标志。
  • Destination addresssource address 就是源mac和目的mac地址
  • Type :类型. 此字段定义包封装在其中的上层协议的框架。该协议可以是IP、ARP、OSPF等。
  • Data:数据来源于上一层,大小应在46到1500byte之间,如果小于46,则会自动补0,反之需要分割
  • CRC:错误检测:检测源和目的mac地址与数据的和,如果发现错误,则该帧丢弃。

Wireshark 抓包数据

数据包的传输

传输示意图

以太网帧在数据链路层传输

TCP数据传输过程

TCP的三次握手和四次挥手

建立连接

  • 第一次握手:客户端发送一个TCP的SYN标志位置1的包指明客户打算连接的服务器的端口,以及初始序号X,保存在包头的序列号(Sequence Number)字段里。
  • 第二次握手:服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将确认序号(Acknowledgement Number)设置为客户的ISN加1以.即X+1。
  • 第三次握手:客户端再次发送确认包(ACK) SYN标志位为0,ACK标志位为1.并且把服务器发来ACK的序号字段+1,放在确定字段中发送给对方.并且在数据段放写ISN的+1。
断开连接

  • 第一次挥手:客户端发送一个FIN,用来关闭服务端到Server的数据传送,客户端进入FIN_WAIT_1状态。
  • 第二次挥手:服务端 收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),服务端进入CLOSE_WAIT状态
  • 第三次挥手:服务端发送一个FIN,用来关闭Server到Client的数据传送,服务端进入LAST_ACK状态。
  • 第四次挥手:客服端收到FIN后,服务端进入TIME_WAIT状态,接着发送一个ACK给服务端,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手

网络设备与网络结构

首先大概知道什么是交换机、路由器和网关设备等,以及常见的网络结构

交换机

这里指以太网交换机,以太网交换机的结构是每个端口都直接与主机相连,并且一般都工作在全双工方式。交换机能同时连通许多对端口,使每一对相互通信的主机都能像独占通信媒体那样,进行无冲突地传输数据。

以太网交换机工作于OSI网络参考模型的第二层(即数据链路层),是一种基于MAC(Media Access Control,介质访问控制)地址识别、完成以太网数据帧转发的网络设备。

交换机在端口上接受计算机发送过来的数据帧,根据帧头的目的MAC地址查找MAC地址表然后将该数据帧从对应端口上转发出去,从而实现数据交换。

路由器

路由器是连接两个或多个网络的硬件设备,在网络间起网关的作用,是读取每一个数据包中的地址然后决定如何传送的专用智能性的网络设备。

路由器又可以称之为网关设备。工作于OSI网络参考模型的第三层(即网络层),对不同的网络之间的数据包进行存储、分组转发处理,而数据在一个子网中传输到另一个子网中,可以通过路由器的路由功能进行处理。在网络通信中,路由器具有判断网络地址以及选择IP路径的作用,可以在多个网络环境中,构建灵活的链接系统,通过不同的数据分组以及介质访问方式对各个子网进行链接。路由器在操作中仅接受源站或者其他相关路由器传递的信息,是一种基于网络层的互联设备。

路由器只能根据具体的IP地址来转发数据,IP地址由网络地址和主机地址两部分组成。计算机之间的通信只能在具有相同网络地址的IP地址之间进行,如果想要与其他网段的计算机进行通信,则必须经过路由器转发出去。

路由器的多个端口可以连接多个网段,每个端口的IP地址的网络地址都必须与所连接的网段的网络地址一致。不同的端口它的网络地址是不同的,所对应的网段也是不同的,这样才能使各个网段中的主机通过自己网段的IP地址把数据发送到路由器上。

对于每一个接收到的数据包,路由器都会重新计算其校验值,并写入新的物理地址。

路由器的主要工作就是为经过路由器的每个数据帧寻找一条最佳传输路径,并将该数据有效地传送到目的站点。

网关

网关(Gateway)又称网间连接器、协议转换器。默认网关在【网络层】上以实现网络互连,是最复杂的网络互连设备,仅用于两个高层协议不同的网络互连。网关的结构也和路由器类似,不同的是互连层。网关既可以用于广域网互连,也可以用于局域网互连。

【说明:由于历史的原因,许多有关TCP/IP的文献曾经把网络层使用的路由器称为网关,在今天很多局域网采用都是路由来接入网络,因此通常指的网关就是路由器的IP!】

那么网关到底是什么呢?网关实质上是一个网络通向其他网络的IP地址。比如有网络A和网络B,网络A的IP地址范围为“192.168.1.1192. 168.1.254”,子网掩码为255.255.255.0;网络B的IP地址范围为“192.168.2.1192.168.2.254”,子网掩码为255.255.255.0。在没有路由器的情况下,两个网络之间是不能进行TCP/IP通信的,即使是两个网络连接在同一台交换机(或集线器)上,TCP/IP协议也会根据子网掩码(255.255.255.0)判定两个网络中的主机处在不同的网络里。而要实现这两个网络之间的通信,则必须通过网关。如果网络A中的主机发现数据包的目的主机不在本地网络中,就把数据包转发给它自己的网关,再由网关转发给网络B的网关,网络B的网关再转发给网络B的某个主机

只有设置好网关的IP地址,TCP/IP协议才能实现不同网络之间的相互通信。那么这个IP地址是哪台机器的IP地址呢?网关的IP地址是具有路由功能的设备的IP地址,具有路由功能的设备有路由器、启用了路由协议的服务器(实质上相当于一台路由器)、代理服务器(也相当于一台路由器)。

网关和路由器的区别

首先‘网关’一个大概念,不具体特指一类产品,只要连接两个不同的网络的设备都可以叫网关;而‘路由器’么一般特指能够实现路由寻找和转发的特定类产品,路由器很显然能够实现网关的功能。

我们在PC上设置的默认网关是什么,默认网关事实上不是一个产品而是一个网络层的概念,PC本身不具备路由寻址能力,所以PC要把所有的IP包发送到一个默认的中转地址上面进行转发,也就是默认网关。这个网关可以在路由器上,可以在三层交换机上,可以在防火墙上,可以在服务器上,所以和物理的设备无关。

家用路由器

家用路由器 = 路由器 + 防火墙 + 交换机 = 防火墙 + 交换机 + NAT

防火墙:路由器在许多方面扮演着基本防火墙的角色,包括自动拒绝不属于网络内的计算机与外部世界之间正在进行的交换的一部分的传入数据。另一方面,如果来自未知地址的端口探测突然出现,你的路由器就会充当保镖,拒绝请求,有效地隐藏你的计算机。

网关:家庭路由器还充当网络交换机。网络交换机是一种硬件,它可以促进内部网络上计算机之间的通信。如果没有交换功能,这些设备可以通过路由器与更大的互联网通信,但不能相互通信。

NAT:我们都知道,一台家用路由器可以允许多台设备同时连接上网,那么当设备通过家用路由器向网络发送请求后,返回的响应到达路由器时,路由器必须要知道该响应对应的是哪台设备发送的请求。我们向网络供应商(ISP)申请网络访问权限时,ISP会给路由器分配一个公网ip,路由器内部的设备只能使用内网ip。NAT的作用就是实现公网/内网ip以及端口的转换。为此,需要一张表,用于记录内外ip和端口的映射关系。
假设内网中有两台设备A和B,同时访问同一个外网ip的相同端口。那么在路由器处就会记录如下映射关系:

(remote ip_r : port_r)–(local ip_a : a_port)
(remote ip_r : port_r)–(local ip_b : b_port)

假设恰好a_port和b_port的值相同,那么来自远端的响应数据到达路由器时,路由器就无法确定该请求应该给A还是给B。对于这种情况,NAT采用一个三元组来进行区分:(remote ip_r : port_r)(nat port)(local ip_a : a_port),即通过增加nat port来进行区分。当A和B请求到来时,为两个请求生成两个尚未使用的端口:a_port和b_port,并记录如下关系:

(remote ip_r : port_r)(nat a_port)(local ip_a : same_port)
(remote ip_r : port_r)(nat b_port)(local ip_b : same_port)

即当A请求到达路由器时,路由器将请求的源ip换成ISP分配的公网ip,并将源端口换成a_port;而当请求B到达路由器时,路由器将请求的源ip换成ISP分配的公网ip,并将源端口换成b_port。当A和B的请求返回时,根据返回的目的端口(返回的目的端口就是请求的源端口)是a_port还是b_port即可确定该将响应给A还是B。

Modem

通过电话线上网的时代,我们通过猫来连接互联网。现在运营商都升级成了光纤了,所以原来的猫换成光猫。

调制解调器(英文名Modem),俗称“猫”,是一种计算机硬件。

它能把计算机的数字信号翻译成可沿普通电话线传送的脉冲信号,而这些脉冲信号又可被线路另一端的另一个调制解调器接收,并译成计算机可懂的语言。
计算机内的信息是由“0”和“1”组成数字信号,而在电话线上传递的却只能是模拟电信号。于是,当两台计算机要通过电话线进行数据传输时,就需要一个设备负责数模的转换。

光猫

光猫和上面说的猫也是一样的,只是将传输脉冲信号变成了传输光信号。

现在一般的光猫外形都是和无线路由器相似,并且光猫上的天线也是可以发射无线信号的,这种能够自带路由器功能的,并且还能内置wifi功能的光猫,是一种集合光猫和路由一体的机器。

目前,家庭上网基本上都实现了FTTH,也就是光纤入户。 宽带上网、IPTV、电话均通过一根光纤接入,实现了“三网融合”。

生活中,我们使用手电筒时,可以通过明暗的变化传递信息,光纤传输信息跟这个差不多。

在网络世界里,只有0和1,数字信号不能直接变成光信号,需要将数字信号转换成电信号,比如1用高压表示,0用低压表示。将这些电信号输入到激光、二极管等光源,光源根据信号电压变化而发光,比如高电压发光亮,低电压发光暗。 光信号到达对端时,通过光敏元件根据光的亮度产生不同的电压,转换成电信号,最后将电信号转换成数字信号,这样我们就收到信号了。

常见的网络结构

只是在网上随便找的示例

家庭

1687944231217.png

学校

1687944275543.png

公司

1687944314510.png

最终结论

1、当发送的目的地在局域网时,通过IP和子网掩码可以判断,然后根据本地的(IP地址 - 物理地址)映射表(arp协议),就能找到目标的mac直接发送数据包了。

2、当发送的目标地不在局域网中时,数据包会发送到网关(一般就是路由器)

1. 从内网发到公网的数据包,在经过网关后,利用NAT,会被转换成网关的MAC,IP层地址被替换成公网真实IP。网关处会有一个映射关系表,这样返回的数据就会根据此映射关系做相应数据转发。
2. 数据包在经过交换机时不会修改数据,直接转发。
3. 数据包在经过中间路由器之后 源mac 和 目标mac 都是要改变,源mac改成路由器出网卡的mac,目标mac改成下一跳的mac,当然还会重新计算以太网帧的CRC等

NAT(网络地址转换)

NAT(Network Address Translation,网络地址转换)是1994年提出的。当在专用网内部的一些主机本来已经分配到了本地IP地址(即仅在本专用网内使用的专用地址),但现在又想和因特网上的主机通信(并不需要加密)时,可使用NAT方法。

这种方法需要在专用网连接到因特网的路由器上安装NAT软件。装有NAT软件的路由器叫做NAT路由器,它至少有一个有效的外部全球IP地址。这样,所有使用本地地址的主机在和外界通信时,都要在NAT路由器上将其本地地址转换成全球IP地址,才能和因特网连接。

NAT的几种实现方式

静态转换:是指将内部网络的私有IP地址转换为公有IP地址,IP地址对是一对一的,是一成不变的,某个私有IP地址只转换为某个公有IP地址。借助于静态转换,可以实现外部网络对内部网络中某些特定设备(如服务器)的访问。

动态转换:是指将内部网络的私有IP地址转换为公用IP地址时,IP地址是不确定的,是随机的,所有被授权访问上Internet的私有IP地址可随机转换为任何指定的合法IP地址。也就是说,只要指定哪些内部地址可以进行转换,以及用哪些合法地址作为外部地址时,就可以进行动态转换。动态转换可以使用多个合法外部地址集。当ISP提供的合法IP地址略少于网络内部的计算机数量时。可以采用动态转换的方式。

**端口多路复用(Port address Translation,PAT)**:是指改变外出数据包的源端口并进行端口转换,即端口地址转换(PAT,Port Address Translation).采用端口多路复用方式。内部网络的所有主机均可共享一个合法外部IP地址实现对Internet的访问,从而可以最大限度地节约IP地址资源。同时,又可隐藏网络内部的所有主机,有效避免来自internet的攻击。因此,目前网络中应用最多的就是端口多路复用方式。

ALG(Application Level Gateway):即应用程序级网关技术:传统的NAT技术只对IP层和传输层头部进行转换处理,但是一些应用层协议,在协议数据报文中包含了地址信息。为了使得这些应用也能透明地完成NAT转换,NAT使用一种称作ALG的技术,它能对这些应用程序在通信时所包含的地址信息也进行相应的NAT转换。例如:对于FTP协议的PORT/PASV命令、DNS协议的 “A” 和 “PTR” queries命令和部分ICMP消息类型等都需要相应的ALG来支持。

NAPT

NAPT(Network Address Port Translation),即网络地址端口转换,可将多个内部地址映射为一个合法公网地址,但以不同的协议端口号与不同的内部地址相对应,也就是<内部地址+内部端口>与<外部地址+外部端口>之间的转换。NAPT普遍用于接入设备中,它可以将中小型的网络隐藏在一个合法的IP地址后面。NAPT也被称为“多对一”的NAT,或者叫PAT(Port Address Translations,端口地址转换)、地址超载(address overloading)。

NAPT与动态地址NAT不同,它将内部连接映射到外部网络中的一个单独的IP地址上,同时在该地址上加上一个由NAT设备选定的TCP端口号。NAPT算得上是一种较流行的NAT变体,通过转换TCP或UDP协议端口号以及地址来提供并发性。除了一对源和目的IP地址以外,这个表还包括一对源和目的协议端口号,以及NAT盒使用的一个协议端口号。

NAPT的主要优势在于,能够使用一个全球有效IP地址获得通用性。主要缺点在于其通信仅限于TCP或UDP。当所有通信都采用TCP或UDP,NAPT允许一台内部计算机访问多台外部计算机,并允许多台内部主机访问同一台外部计算机,相互之间不会发生冲突。

内网上网方案

内网实现上网的方案一般就是Nat 和 Proxy


其他

查看地址解析协议(ARP)使用的“IP 到物理”地址转换表

1
c:\>arp -a

查路由表

1
c:\>route print

追踪路由

1
c:\>tracert baidu.com

抓包
Wireshark

1 最新git源码下载地址:

https://github.com/git/git/releases
https://www.kernel.org/pub/software/scm/git/
可以手动下载下来在上传到服务器上面

2 移除旧版本git

centos自带Git,7.x版本自带git 1.8.3.1(应该是,也可能不是),
安装新版本之前需要使用yun remove git卸载(安装后卸载也可以)。

1
2
3
[root@Git ~]# git --version    ## 查看自带的版本
git version 1.8.3.1
[root@Git ~]# yum remove git ## 移除原来的版本

3 安装所需软件包

1
2
[root@Git ~]# yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel 
[root@Git ~]# yum install gcc-c++ perl-ExtUtils-MakeMaker

4 下载&安装

1
2
[root@Git ~]# cd /usr/src
[root@Git ~]# wget https://www.kernel.org/pub/software/scm/git/git-2.7.3.tar.gz

5 解压

1
[root@Git ~]# tar xf git-2.7.3.tar.gz

6 配置编译安装

1
2
3
4
5
[root@Git ~]# cd git-2.7.3
[root@Git ~]# make configure
[root@Git ~]# ./configure --prefix=/usr/local/git ##配置目录
[root@Git ~]# make profix=/usr/local/git
[root@Git ~]# make install

7 加入环境变量

1
2
[root@Git ~]# echo "export PATH=$PATH:/usr/local/git/bin" >> /etc/profile
[root@Git ~]# source /etc/profile

8 检查版本

1
2
[root@Git git-2.7.3]# git --version 
git version 2.7.3
0%