目录
摘要
网络这块是个重点也是个难点,大家在学习网络编程的时候,不知道有没有这样的困扰,明明每个知识点都听懂了但却又不完全懂。于是我在仔细学习大量相关知识的同时,通过自己的话语以一个读者的口吻将它们再叙述分析一遍,希望大家看了能有所收获,欢迎后台私聊交流,共同进步。
一:UDP协议
1.1 UDP的的特点
UDP
传输的过程类似于寄信
.
无连接
:
知道对端的
IP
和端口号就直接进行传输
,
不需要建立连接
;
不可靠
:
没有确认机制
,
没有重传机制
;
如果因为网络故障该段无法发到对方
, UDP
协议层也不会给应用层返 回任何错误信息;
面向数据报
:
不能够灵活的控制读写数据的次数和数量
重点:UDP是不“可靠传输”
1.2面向数据报
应用层交给
UDP
多长的报文
, UDP
原样发送
,
既不会拆分
,
也不会合并
;
用
UDP
传输
100
个字节的数据
:
如果发送端调用一次
sendto,
发送
100
个字节
,
那么接收端也必须调用对应的一次
recvfrom,
接收
100
个字
节
;
而不能循环调用
10
次
recvfrom,
每次接收
10
个字节
;
1.3 UDP的缓冲区
UDP
没有真正意义上的
发送缓冲区
.
调用
sendto
会直接交给内核
,
由内核将数据传给网络层协议进行后续
的传输动作
;
UDP
具有接收缓冲区
.
但是这个接收缓冲区不能保证收到的
UDP
报的顺序和发送
UDP
报的顺序一致
;
如果缓
冲区满了
,
再到达的
UDP
数据就会被丢弃
;
UDP
的
socket
既能读
,
也能写
,
这个概念叫做
全双工
1.4 UDP使用注意事项
我们注意到
, UDP
协议首部中有一个
16
位的最大长度
.
也就是说一个
UDP
能传输的数据最大长度是
64K(
包含
UDP
首部
).
然而
64K
在当今的互联网环境下
,
是一个非常小的数字
.
如果我们需要传输的数据超过
64K,
就需要在应用层手动的分包
,
多次发送
,
并在接收端手动拼装
;
基于
UDP
的应用层协议
NFS:
网络文件系统
TFTP:
简单文件传输协议
DHCP:
动态主机配置协议
BOOTP:
启动协议
(
用于无盘设备启动
)
DNS:
域名解析协议
当然
,
也包括你自己写
UDP
程序时自定义的应用层协议
;
二:TCP协议
TCP
全称为
"
传输控制协议
(Transmission Control Protocol").
人如其名
,
要对数据的传输进行一个详细的控制
;
2.1 TCP协议段格式
源
/
目的端口号
:
表示数据是从哪个进程来
,
到哪个进程去
;
32
位序号
/32
位确认号
:
后面详细讲
;
4
位
TCP
报头长度
:
表示该
TCP
头部有多少个
32
位
bit(
有多少个
4
字节
);
所以
TCP
头部最大长度是
15 * 4 = 60
6
位标志位
:
URG:
紧急指针是否有效
ACK:
确认号是否有效
PSH:
提示接收端应用程序立刻从
TCP
缓冲区把数据读走
RST:
对方要求重新建立连接
;
我们把携带
RST
标识的称为
复位报文段
SYN:
请求建立连接
;
我们把携带
SYN
标识的称为
同步报文段
FIN:
通知对方
,
本端要关闭了
,
我们称携带
FIN
标识的为
结束报文段
16
位窗口大小
:
后面再说
16
位校验和
:
发送端填充
, CRC
校验
.
接收端校验不通过
,
则认为数据有问题
.
此处的检验和不光包含
TCP
首部
,
也
包含
TCP
数据部分
.
16
位紧急指针
:
标识哪部分数据是紧急数据
;
40
字节头部选项
:
暂时忽略
;
2.2 确认应答机制(ACK)
TCP
将每个字节的数据都进行了编号
.
即为序列号
.
![](https://img-blog.csdnimg.cn/16e5069f59574447b2e6351ca656dcb5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_17,color_FFFFFF,t_70,g_se,x_16)
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.
2.3 超时重传机制
主机
A
发送数据给
B
之后
,
可能因为网络拥堵等原因
,
数据无法到达主机
B;
如果主机
A
在一个特定时间间隔内没有收到
B
发来的确认应答
,
就会进行重发
;
但是
,
主机
A
未收到
B
发来的确认应答
,
也可能是因为
ACK
丢失了
;
![](https://img-blog.csdnimg.cn/47695c778ea64ceca2a473d8ec498253.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_13,color_FFFFFF,t_70,g_se,x_16)
因此主机
B
会收到很多重复数据
.
那么
TCP
协议需要能够识别出那些包是重复的包
,
并且把重复的丢弃掉
.
这时候我们可以利用前面提到的序列号
,
就可以很容易做到去重的效果
.
那么
,
如果超时的时间如何确定
?
最理想的情况下
,
找到一个最小的时间
,
保证
"
确认应答一定能在这个时间内返回
".
但是这个时间的长短
,
随着网络环境的不同
,
是有差异的
.
如果超时时间设的太长
,
会影响整体的重传效率
;
如果超时时间设的太短
,
有可能会频繁发送重复的包
;
TCP
为了保证无论在任何环境下都能比较高性能的通信
,
因此会动态计算这个最大超时时间
.
Linux
中
(BSD Unix
和
Windows
也是如此
),
超时以
500ms
为一个单位进行控制
,
每次判定超时重发的超时时
间都是
500ms
的整数倍
.
如果重发一次之后
,
仍然得不到应答
,
等待
2*500ms
后再进行重传
.
如果仍然得不到应答
,
等待
4*500ms
进行重传
.
依次类推
,
以指数形式递增
.
累计到一定的重传次数
, TCP
认为网络或者对端主机出现异常
,
强制关闭连接
.
2.4 连接管理机制
在正常情况下
, TCP
要经过三次握手建立连接
,
四次挥手断开连接
![](https://img-blog.csdnimg.cn/5c412ddf76ac447aa54a911ebe6af933.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_20,color_FFFFFF,t_70,g_se,x_16)
2.4.1服务端状态转化:
[CLOSED -> LISTEN]
服务器端调用
listen
后进入
LISTEN
状态
,
等待客户端连接
;
[LISTEN -> SYN_RCVD]
一旦监听到连接请求
(
同步报文段
),
就将该连接放入内核等待队列中
,
并向客户端发
送
SYN
确认报文
.
[SYN_RCVD -> ESTABLISHED]
服务端一旦收到客户端的确认报文
,
就进入
ESTABLISHED
状态
,
可以进行读
写数据了
.
[ESTABLISHED -> CLOSE_WAIT]
当客户端主动关闭连接
(
调用
close),
服务器会收到结束报文段
,
服务器返
回确认报文段并进入
CLOSE_WAIT;
[CLOSE_WAIT -> LAST_ACK]
进入
CLOSE_WAIT
后说明服务器准备关闭连接
(
需要处理完之前的数据
);
当服
务器真正调用
close
关闭连接时
,
会向客户端发送
FIN,
此时服务器进入
LAST_ACK
状态
,
等待最后一个
ACK
到
来
(
这个
ACK
是客户端确认收到了
FIN)
[LAST_ACK -> CLOSED]
服务器收到了对
FIN
的
ACK,
彻底关闭连接
2.4.2 客户端状态转化:
[CLOSED -> SYN_SENT]
客户端调用
connect,
发送同步报文段
;
[SYN_SENT -> ESTABLISHED] connect
调用成功
,
则进入
ESTABLISHED
状态
,
开始读写数据
;
[ESTABLISHED -> FIN_WAIT_1]
客户端主动调用
close
时
,
向服务器发送结束报文段
,
同时进入
FIN_WAIT_1;
[FIN_WAIT_1 -> FIN_WAIT_2]
客户端收到服务器对结束报文段的确认
,
则进入
FIN_WAIT_2,
开始等待服务
器的结束报文段
;
[FIN_WAIT_2 -> TIME_WAIT]
客户端收到服务器发来的结束报文段
,
进入
TIME_WAIT,
并发出
LAST_ACK;
[TIME_WAIT -> CLOSED]
客户端要等待一个
2MSL(Max Segment Life,
报文最大生存时间
)
的时间
,
才会进
入
CLOSED
状态
.
下图是
TCP
状态转换的一个汇总
:
![](https://img-blog.csdnimg.cn/66cebd82ac384776b96fbc45d0248ed8.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_20,color_FFFFFF,t_70,g_se,x_16)
较粗的虚线表示服务端的状态变化情况
;
较粗的实线表示客户端的状态变化情况
;
CLOSED
是一个假想的起始点
,
不是真实状态
;
2.5 滑动窗口
刚才我们讨论了确认应答策略
,
对每一个发送的数据段
,
都要给一个
ACK
确认应答
.
收到
ACK
后再发送下一个数据段
.
这
样做有一个比较大的缺点
,
就是性能较差
.
尤其是数据往返的时间较长的时候
.
![](https://img-blog.csdnimg.cn/64134bd862fa4e5cb45558f4ab2ff250.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_20,color_FFFFFF,t_70,g_se,x_16)
既然这样一发一收的方式性能较低
,
那么我们一次发送多条数据
,
就可以大大的提高性能
(
其实是将多个段的等待时间
重叠在一起了
).
![](https://img-blog.csdnimg.cn/1ededb345b5946f2a141bd20657257ba.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_18,color_FFFFFF,t_70,g_se,x_16)
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值
.
上图的窗口大小就是
4000
个字节
(
四个
段
).
发送前四个段的时候
,
不需要等待任何
ACK,
直接发送
;
收到第一个
ACK
后
,
滑动窗口向后移动
,
继续发送第五个段的数据
;
依次类推
;
操作系统内核为了维护这个滑动窗口
,
需要开辟
发送缓冲区
来记录当前还有哪些数据没有应答
;
只有确认
应答过的数据
,
才能从缓冲区删掉
;
窗口越大
,
则网络的吞吐率就越高
;
2.6 流量控制
接收端处理数据的速度是有限的
.
如果发送端发的太快
,
导致接收端的缓冲区被打满
,
这个时候如果发送端继续发送
,
就
会造成丢包
,
继而引起丢包重传等等一系列连锁反应
.
因此
TCP
支持根据接收端的处理能力
,
来决定发送端的发送速度
.
这个机制就叫做
流量控制
(Flow Control)
;
接收端将自己可以接收的缓冲区大小放入
TCP
首部中的
"
窗口大小
"
字段
,
通过
ACK
端通知发送端
;
窗口大小字段越大
,
说明网络的吞吐量越高
;
接收端一旦发现自己的缓冲区快满了
,
就会将窗口大小设置成一个更小的值通知给发送端
;
发送端接受到这个窗口之后
,
就会减慢自己的发送速度
;
如果接收端缓冲区满了
,
就会将窗口置为
0;
这时发送方不再发送数据
,
但是需要定期发送一个窗口探测数据
段
,
使接收端把窗口大小告诉发送端。
![](https://img-blog.csdnimg.cn/f9aef74931714fc0b7b0e0992b23c4e5.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_20,color_FFFFFF,t_70,g_se,x_16)
接收端如何把窗口大小告诉发送端呢
?
回忆我们的
TCP
首部中
,
有一个
16
位窗口字段
,
就是存放了窗口大小信息
;
那么问题来了
, 16
位数字最大表示
65535,
那么
TCP
窗口最大就是
65535
字节么
?
实际上
, TCP
首部
40
字节选项中还包含了一个窗口扩大因子
M,
实际窗口大小是 窗口字段的值左移
M
位
;
2.7 拥塞控制
虽然
TCP
有了滑动窗口这个大杀器
,
能够高效可靠的发送大量的数据
.
但是如果在刚开始阶段就发送大量的数据
,
仍然
可能引发问题
.
因为网络上有很多的计算机
,
可能当前的网络状态就已经比较拥堵
.
在不清楚当前网络状态下
,
贸然发送大量的数据
,
是
很有可能引起雪上加霜的
.
TCP
引入
慢启动
机制
,
先发少量的数据
,
探探路
,
摸清当前的网络拥堵状态
,
再决定按照多大的速度传输数据
;
![](https://img-blog.csdnimg.cn/0777160643d04ec795babdb63d821113.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_18,color_FFFFFF,t_70,g_se,x_16)
此处引入一个概念程为
拥塞窗口
发送开始的时候
,
定义拥塞窗口大小为
1;
每次收到一个
ACK
应答
,
拥塞窗口加
1;
每次发送数据包的时候
,
将拥塞窗口和接收端主机反馈的窗口大小做比较
,
取较小的值作为实际发送的窗 口;
像上面这样的拥塞窗口增长速度
,
是指数级别的
. "
慢启动
"
只是指初使时慢
,
但是增长速度非常快
.
为了不增长的那么快
,
因此不能使拥塞窗口单纯的加倍
.
此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候
,
不再按照指数方式增长
,
而是按照线性方式增长。
2.8 延迟应答
如果接收数据的主机立刻返回
ACK
应答
,
这时候返回的窗口可能比较小
.
假设接收端缓冲区为
1M.
一次收到了
500K
的数据
;
如果立刻应答
,
返回的窗口就是
500K;
但实际上可能处理端处理的速度很快
, 10ms
之内就把
500K
数据从缓冲区消费掉了
;
在这种情况下
,
接收端处理还远没有达到自己的极限
,
即使窗口再放大一些
,
也能处理过来
;
如果接收端稍微等一会再应答
,
比如等待
200ms
再应答
,
那么这个时候返回的窗口大小就是
1M;
一定要记得
,
窗口越大
,
网络吞吐量就越大
,
传输效率就越高
.
我们的目标是在保证网络不拥塞的情况下尽量提高传输效
率
;
那么所有的包都可以延迟应答么
?
肯定也不是
;
数量限制
:
每隔
N
个包就应答一次
;
时间限制
:
超过最大延迟时间就应答一次
;
具体的数量和超时时间
,
依操作系统不同也有差异
;
一般
N
取
2,
超时时间取
200ms。
![](https://img-blog.csdnimg.cn/6c57acc69eb64f889383c740734bea29.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_16,color_FFFFFF,t_70,g_se,x_16)
2.9 捎带应答
在延迟应答的基础上
,
我们发现
,
很多情况下
,
客户端服务器在应用层也是
"
一发一收
"
的
.
意味着客户端给服务器说了
"How are you",
服务器也会给客户端回一个
"Fine, thank you";
那么这个时候
ACK
就可以搭顺风车
,
和服务器回应的
"Fine, thank you"
一起回给客户端。
![](https://img-blog.csdnimg.cn/c5deba5c6b7d44b48791eae95ac1716e.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_20,color_FFFFFF,t_70,g_se,x_16)
2.10 面向字节流
创建一个
TCP
的
socket,
同时在内核中创建一个
发送缓冲区
和一个
接收缓冲区
;
调用
write
时
,
数据会先写入发送缓冲区中
;
如果发送的字节数太长
,
会被拆分成多个
TCP
的数据包发出
;
如果发送的字节数太短
,
就会先在缓冲区里等待
,
等到缓冲区长度差不多了
,
或者其他合适的时机发送出去
;
接收数据的时候
,
数据也是从网卡驱动程序到达内核的接收缓冲区
;
然后应用程序可以调用
read
从接收缓冲区拿数据
;
另一方面
, TCP
的一个连接
,
既有发送缓冲区
,
也有接收缓冲区
,
那么对于这一个连接
,
既可以读数据
,
也可以
写数据
.
这个概念叫做
全双工
由于缓冲区的存在
, TCP
程序的读和写不需要一一匹配
,
例如
:
写
100
个字节数据时
,
可以调用一次
write
写
100
个字节
,
也可以调用
100
次
write,
每次写一个字节
;
读
100
个字节数据时
,
也完全不需要考虑写的时候是怎么写的
,
既可以一次
read 100
个字节
,
也可以一次
read
一个字节
,
重复
100
次
;
三:TCP和UDP对比
我们说了
TCP
是可靠连接
,
那么是不是
TCP
一定就优于
UDP
呢
? TCP
和
UDP
之间的优点和缺点
,
不能简单
,
绝对的进行比
较
TCP
用于可靠传输的情况
,
应用于文件传输
,
重要状态更新等场景
;
UDP
用于对高速传输和实时性要求较高的通信领域
,
例如
,
早期的
QQ,
视频传输等
.
另外
UDP
可以用于广播
;
归根结底
, TCP
和
UDP
都是程序员的工具
,
什么时机用
,
具体怎么用
,
还是要根据具体的需求场景去判定
.
四:网络层
4.1 ip协议
基本概念
主机
:
配有
IP
地址
,
但是不进行路由控制的设备
;
路由器
:
即配有
IP
地址
,
又能进行路由控制
;
节点
:
主机和路由器的统称
;
协议头格式
![](https://img-blog.csdnimg.cn/40a7a4c055df497bb3894b3d4d556c35.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_16,color_FFFFFF,t_70,g_se,x_16)
4
位版本号
(version):
指定
IP
协议的版本
,
对于
IPv4
来说
,
就是
4.
4
位头部长度
(header length): IP
头部的长度是多少个
32bit,
也就是
length * 4
的字节数
. 4bit
表示最大的
数字是
15,
因此
IP
头部最大长度是
60
字节
.
8
位服务类型
(Type Of Service): 3
位优先权字段
(
已经弃用
), 4
位
TOS
字段
,
和
1
位保留字段
(
必须置为
0). 4
位
TOS
分别表示
:
最小延时
,
最大吞吐量
,
最高可靠性
,
最小成本
.
这四者相互冲突
,
只能选择一个
.
对于
ssh/telnet
这样的应用程序
,
最小延时比较重要
;
对于
ftp
这样的程序
,
最大吞吐量比较重要
.
16
位总长度
(total length): IP
数据报整体占多少个字节
.
16
位标识
(id):
唯一的标识主机发送的报文
.
如果
IP
报文在数据链路层被分片了
,
那么每一个片里面的这个
id
都是相同的
.
3
位标志字段
:
第一位保留
(
保留的意思是现在不用
,
但是还没想好说不定以后要用到
).
第二位置为
1
表示禁
止分片
,
这时候如果报文长度超过
MTU, IP
模块就会丢弃报文
.
第三位表示
"
更多分片
",
如果分片了的话
,
最
后一个分片置为
1,
其他是
0.
类似于一个结束标记
.
13
位分片偏移
(framegament offffset):
是分片相对于原始
IP
报文开始处的偏移
.
其实就是在表示当前分片在
原报文中处在哪个位置
.
实际偏移的字节数是这个值
* 8
得到的
.
因此
,
除了最后一个报文之外
,
其他报文的
长度必须是
8
的整数倍
(
否则报文就不连续了
).
8
位生存时间
(Time To Live, TTL):
数据报到达目的地的最大报文跳数
.
一般是
64.
每次经过一个路由
, TTL -=
1,
一直减到
0
还没到达
,
那么就丢弃了
.
这个字段主要是用来防止出现路由循环
8
位协议
:
表示上层协议的类型
16
位头部校验和
:
使用
CRC
进行校验
,
来鉴别头部是否损坏
.
32
位源地址和
32
位目标地址
:
表示发送端和接收端
.
选项字段
(
不定长
,
最多
40
字节
):
4.2 网段划分
IP
地址分为两个部分
,
网络号和主机号
网络号
:
保证相互连接的两个网段具有不同的标识
;
主机号
:
同一网段内
,
主机之间具有相同的网络号
,
但是必须有不同的主机号
;
![](https://img-blog.csdnimg.cn/09d19bf61a6049548bb70918b7078286.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_17,color_FFFFFF,t_70,g_se,x_16)
不同的子网其实就是把网络号相同的主机放到一起
.
如果在子网中新增一台主机
,
则这台主机的网络号和这个子网的网络号一致
,
但是主机号必须不能和子网中
的其他主机重复
.
通过合理设置主机号和网络号
,
就可以保证在相互连接的网络中
,
每台主机的
IP
地址都不相同
.
那么问题来了
,
手动管理子网内的
IP,
是一个相当麻烦的事情
.
有一种技术叫做
DHCP,
能够自动的给子网内新增主机节点分配
IP
地址
,
避免了手动管理
IP
的不便
.
一般的路由器都带有
DHCP
功能
.
因此路由器也可以看做一个
DHCP
服务器
.
过去曾经提出一种划分网络号和主机号的方案
,
把所有
IP
地址分为五类
,
如下图所示
(
该图出 自
[TCPIP])
。
![](https://img-blog.csdnimg.cn/51346afdd3bb42b28c7f94d098693b75.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA54ix5pWy5Luj56CB55qE5bCP6auY,size_16,color_FFFFFF,t_70,g_se,x_16)
A
类
0.0.0.0
到
127.255.255.255
B
类
128.0.0.0
到
191.255.255.255
C
类
192.0.0.0
到
223.255.255.255
D
类
224.0.0.0
到
239.255.255.255
E
类
240.0.0.0
到
247.255.255.255
随着
Internet
的飞速发展
,
这种划分方案的局限性很快显现出来
,
大多数组织都申请
B
类网络地址
,
导致
B
类地址很快就分
配完了
,
而
A
类却浪费了大量地址
;
例如
,
申请了一个
B
类地址
,
理论上一个子网内能允许
6
万
5
千多个主机
. A
类地址的子网内的主机数更多
.
然而实际网络架设中
,
不会存在一个子网内有这么多的情况
.
因此大量的
IP
地址都被浪费掉了
.
针对这种情况提出了新的划分方案
,
称为
CIDR(Classless Interdomain Routing):
引入一个额外的子网掩码
(subnet mask)
来区分网络号和主机号
;
子网掩码也是一个
32
位的正整数
.
通常用一串
"0"
来结尾
;
将
IP
地址和子网掩码进行
"
按位与
"
操作
,
得到的结果就是网络号
;
网络号和主机号的划分与这个
IP
地址是
A
类、
B
类还是
C
类无关
;
总结
这段时间考试给我丫的喘不过气来,感觉电子信息太卷了,不知道大家是不是这样呢?忙完第一时间来维护我的博客,编程真的是熟能生巧,一段时间不咋敲代码,感觉啥也不会了,码文不易。大家多多互动,感激不尽!