0%

计网 IP

IP 在 TCP/IP 参考模型中处于第三层,也就是网络层

网络层的主要作用是:实现主机与主机之间的通信,也叫点对点(end to end)通信

IP 的作用

网络层与数据链路层有什么关系呢

IP(网络层) 和 MAC (数据链路层)之间的区别和关系

IP 的作用是主机之间通信中的,而 MAC 的作用则是实现「直连」的两个设备之间通信,而 IP 则负责在「没有直连」的两个网络之间进行通信传输

IP 的作用与 MAC 的作用

在区间内移动相当于数据链路层,充当区间内两个节点传输的功能,区间内的出发点好比源 MAC 地址,目标地点好比目的 MAC 地址

整个旅游行程表就相当于网络层,充当远程定位的功能,行程的开始好比源 IP,行程的终点好比目的 IP 地址

因此,只有两者兼备,既有某个区间的车票又有整个旅行的行程表,才能保证到达目的地。与此类似,计算机网络中也需要「数据链路层」和「网络层」这个分层才能实现向最终目标地址的通信

还有重要一点,旅行途中我们虽然不断变化了交通工具,但是旅行行程的起始地址和目的地址始终都没变。其实,在网络中数据包传输中也是如此,源IP地址和目标IP地址在传输过程中是不会变化的,只有源 MAC 地址和目标 MAC 一直在变化

IP 地址

在 TCP/IP 网络通信时,为了保证能正常通信,每个设备都需要配置正确的 IP 地址,否则无法实现正常的通信

IP 地址(IPv4 地址)由 32 位正整数来表示,IP 地址在计算机是以二进制的方式处理的。

而人类为了方便记忆采用了点分十进制的标记方式,也就是将 32 位 IP 地址以每 8 位为组,共分为 4 组,每组以「.」隔开,再将每组转换成十进制。

点分十进制

那么,IP 地址最大值也就是

也就说,最大允许 43 亿台计算机连接到网络

实际上,IP 地址并不是根据主机台数来配置的,而是以网卡。像服务器、路由器等设备都是有 2 个以上的网卡,也就是它们会有 2 个以上的 IP 地址

每块网卡可以分配一个以上的IP地址)

因此,让 43 亿台计算机全部连网其实是不可能的,更何况 IP 地址是由「网络标识」和「主机标识」这两个部分组成的,所以实际能够连接到网络的计算机个数更是少了很多

那是怎么能够支持这么多 IP 的呢

根据一种可以更换 IP 地址的技术 NAT,使得可连接计算机数超过 43 亿台

IP 地址的分类

IP 地址分类成了 5 种类型,分别是 A 类、B 类、C 类、D 类、E 类

IP 地址分类

什么是 A、B、C 类地址

其中对于 A、B、C 类主要分为两个部分,分别是网络号和主机号。这很好理解,好比小林是 A 小区 1 栋 101 号,你是 B 小区 1 栋 101 号

用下面这个表格 A、B、C 分类对应的地址范围、最大主机个数

A、B、C 分类地址

A、B、C 分类地址最大主机个数是如何计算

最大主机个数,就是要看主机号的位数,如 C 类地址的主机号占 8 位,那么 C 类地址的最大主机个数:

图片

为什么要减 2 呢?

因为在 IP 地址中,有两个 IP 是特殊的,分别是主机号全为 1 和 全为 0 地址

图片

  • 主机号全为 1 指定某个网络下的所有主机,用于广播
  • 主机号全为 0 指定某个网络

因此,在分配过程中,应该去掉这两种情况

广播地址用于什么

广播地址用于在同一个链路中相互连接的主机之间发送数据包

学校班级中就有广播的例子,在准备上课的时候,通常班长会喊:“上课, 全体起立!”,班里的同学听到这句话是不是全部都站起来了?这个句话就有广播的含义。

当主机号全为 1 时,就表示该网络的广播地址。例如把 172.20.0.0/16 用二进制表示如下:

10101100.00010100.00000000.00000000

将这个地址的主机部分全部改为 1,则形成广播地址:

10101100.00010100.11111111.11111111

再将这个地址用十进制表示,则为 172.20.255.255

广播地址可以分为本地广播和直接广播两种。

  • 在本网络内广播的叫做本地广播。例如网络地址为 192.168.0.0/24 的情况下,广播地址是 192.168.0.255 。因为这个广播地址的 IP 包会被路由器屏蔽,所以不会到达 192.168.0.0/24 以外的其他链路上。
  • 在不同网络之间的广播叫做直接广播。例如网络地址为 192.168.0.0/24 的主机向 192.168.1.255/24 的目标地址发送 IP 包。收到这个包的路由器,将数据转发给192.168.1.0/24,从而使得所有 192.168.1.1~192.168.1.254 的主机都能收到这个包(由于直接广播有一定的安全问题,多数情况下会在路由器上设置为不转发)。

本地广播与直接广播

什么是 D、E 类地址

D 类和 E 类地址是没有主机号的,所以不可用于主机 IP,D 类常被用于多播,E 类是预留的分类,暂时未使用

D、E 分类地址

多播地址用于什么?

多播用于将包发送给特定组内的所有主机

还是举班级的栗子,老师说:“最后一排的同学,上来做这道数学题。”,老师是指定的是最后一排的同学,也就是多播的含义了。

由于广播无法穿透路由,若想给其他网段发送同样的包,就可以使用可以穿透路由的多播(组播)

单播、广播、多播通信

多播使用的 D 类地址,其前四位是 1110 就表示是多播地址,而剩下的 28 位是多播的组编号。

从 224.0.0.0 ~ 239.255.255.255 都是多播的可用范围,其划分为以下三类:

  • 224.0.0.0 ~ 224.0.0.255 为预留的组播地址,只能局域网中,路由器是不会进行转发的
  • 224.0.1.0 ~ 238.255.255.255 为用户可用的组播地址,可以用于 Internet 上
  • 239.0.0.0 ~ 239.255.255.255 为本地管理组播地址,可供内部网在内部使用,仅在特定的本地范围内有效

IP 分类的优点

判断其 IP 地址的首位是否为 0,为 0 则为 A 类地址,那么就能很快的找出网络地址和主机地址

IP 分类判断

这种分类地址的优点就是简单明了、选路(基于网络地址)简单

IP 分类的缺点

缺点一

同一网络下没有地址层次,比如一个公司里用了 B 类地址,但是可能需要根据生产环境、测试环境、开发环境来划分地址层次,而这种 IP 分类是没有地址层次划分的功能,所以这就缺少地址的灵活性

缺点二

A、B、C类有个尴尬处境,就是不能很好的与现实网络匹配

  • C 类地址能包含的最大主机数量实在太少了,只有 254 个,估计一个网吧都不够用。
  • 而 B 类地址能包含的最大主机数量又太多了,6 万多台机器放在一个网络下面,一般的企业基本达不到这个规模,闲着的地址就是浪费。

这两个缺点,都可以在 CIDR 无分类地址解决

无分类地址 CIDR

这种方式不再有分类地址的概念,32 比特的 IP 地址被划分为两部分,前面是网络号,后面是主机号

怎么划分网络号和主机号的呢?

表示形式 a.b.c.d/x,其中 /x 表示前 x 位属于网络号, x 的范围是 0 ~ 32,这就使得 IP 地址更加具有灵活性

比如 10.100.122.2/24,这种地址表示形式就是 CIDR,/24 表示前 24 位是网络号,剩余的 8 位是主机号

图片

还有另一种划分网络号与主机号形式,那就是子网掩码,掩码的意思就是掩盖掉主机号,剩余的就是网络号。

将子网掩码和 IP 地址按位计算 AND,就可得到网络号

图片

为什么要分离网络号和主机号?

因为两台计算机要通讯,首先要判断是否处于同一个广播域内,即网络地址是否相同。如果网络地址相同,表明接受方在本网络上,那么可以把数据包直接发送到目标主机

路由器寻址工作中,也就是通过这样的方式来找到对应的网络号的,进而把数据包转发给对应的网络内。

IP地址的网络号

怎么进行子网划分?

通过子网掩码划分出网络号和主机号,那实际上子网掩码还有一个作用,那就是划分子网

子网划分实际上是将主机地址分为两个部分:子网网络地址和子网主机地址

图片

  • 未做子网划分的 ip 地址:网络地址+主机地址
  • 做子网划分后的 ip 地址:网络地址+(子网网络地址+子网主机地址)

假设对 C 类地址进行子网划分,网络地址 192.168.1.0,使用子网掩码 255.255.255.192 对其进行子网划分。

C 类地址中前 24 位 是网络号,最后 8 位是主机号,根据子网掩码可知从 8 位主机号中借用 2 位作为子网号

图片

由于子网网络地址被划分成 2 位,那么子网地址就有 4 个,分别是 00、01、10、11,具体划分如下图:

图片

划分后的 4 个子网如下表格

图片

公有 IP 地址与私有 IP 地址

在 A、B、C 分类地址,实际上有分公有 IP 地址和 私有 IP 地址

图片

平时我们办公室、家里、学校用的 IP 地址,一般都是私有 IP 地址。因为这些地址允许组织内部的 IT 人员自己管理、自己分配,而且可以重复。因此,你学校的某个私有 IP 地址和我学校的可以是一样的。

就像每个小区都有自己的楼编号和门牌号,你小区家可以叫 1 栋 101 号,我小区家也可以叫 1 栋 101,没有任何问题。但一旦出了小区,就需要带上中山路 666 号(公网 IP 地址),是国家统一分配的,不能两个小区都叫中山路 666。

所以,公有 IP 地址是有个组织统一分配的,假设你要开一个博客网站,那么你就需要去申请购买一个公有 IP,这样全世界的人才能访问。并且公有 IP 地址基本上要在整个互联网范围内保持唯一。

公有 IP 地址与私有 IP 地址

公有 IP 地址由谁管理呢

私有 IP 地址通常是内部的 IT 人员值管理,公有 IP 地址是由 ICANN 组织管理,中文叫「互联网名称与数字地址分配机构」。

IANA 是 ICANN 的其中一个机构,它负责分配互联网 IP 地址,是按州的方式层层分配

图片

  • ARIN 北美地区
  • LACNIC 拉丁美洲和一些加勒比群岛
  • RIPE NCC 欧洲、中东和中亚
  • AfriNIC 非洲地区
  • APNIC 亚太地区

在中国是由 CNNIC 的机构进行管理,它是中国国内唯一指定的全局 IP 地址管理的组织

IP 地址与路由控制

IP地址的网络地址这一部分是用于进行路由控制。

路由控制表中记录着网络地址与下一步应该发送至路由器的地址,在主机和路由器上都会有各自的路由器控制表。

在发送 IP 包时,首先要确定 IP 包首部中的目标地址,再从路由控制表中找到与该地址具有相同网络地址的记录,根据该记录将 IP 包转发给相应的下一个路由器。如果路由控制表中存在多条相同网络地址的记录,就选择相同位数最多的网络地址,也就是最长匹配

图片

  1. 主机 A 要发送一个 IP 包,其源地址是 10.1.1.30 和目标地址是 10.1.2.10,由于没有在主机 A 的路由表找到与目标地址 10.1.2.10 的网络地址,于是把包被转发到默认路由(路由器 1
  2. 路由器 1 收到 IP 包后,也在路由器 1 的路由表匹配与目标地址相同的网络地址记录,发现匹配到了,于是就把 IP 数据包转发到了 10.1.0.2 这台路由器 2
  3. 路由器 2 收到后,同样对比自身的路由表,发现匹配到了,于是把 IP 包从路由器 210.1.2.1 这个接口出去,最终经过交换机把 IP 数据包转发到了目标主机

环回地址是不会流向网络

环回地址是在同一台计算机上的程序之间进行网络通信时所使用的一个默认地址

计算机使用一个特殊的 IP 地址 127.0.0.1 作为环回地址,与该地址具有相同意义的是一个叫做 localhost 的主机名

使用这个 IP 或主机名时,数据包不会流向网络

IP 分片与重组

每种数据链路的最大传输单元 MTU 都是不相同的,如 FDDI 数据链路 MTU 4352、以太网的 MTU 是 1500 字节等。

每种数据链路的 MTU 之所以不同,是因为每个不同类型的数据链路的使用目的不同。使用目的不同,可承载的 MTU 也就不同。

其中,我们最常见数据链路是以太网,它的 MTU 是 1500 字节。

那么当 IP 数据包大小大于 MTU 时, IP 数据包就会被分片

经过分片之后的 IP 数据报在被重组的时候,只能由目标主机进行,路由器是不会进行重组的

假设发送方发送一个 4000 字节的大数据报,若要传输在以太网链路,则需要把数据报分片成 3 个小数据报进行传输,再交由接收方重组成大数据报

分片与重组

在分片传输中,一旦某个分片丢失,则会造成整个 IP 数据报作废,所以 TCP 引入了 MSS 也就是在 TCP 层进行分片不由 IP 层分片,那么对于 UDP 我们尽量不要发送一个大于 MTU 的数据报文。

IPv6 基本认识

Pv4 的地址是 32 位的,大约可以提供 42 亿个地址,但是早在 2011 年 IPv4 地址就已经被分配完了。

但是 IPv6 的地址是 128 位的,这可分配的地址数量是大的惊人,说个段子 IPv6 可以保证地球上的每粒沙子都能被分配到一个 IP 地址。

但 IPv6 除了有更多的地址之外,还有更好的安全性和扩展性,说简单点就是 IPv6 相比于 IPv4 能带来更好的网络体验。

但是因为 IPv4 和 IPv6 不能相互兼容,所以不但要我们电脑、手机之类的设备支持,还需要网络运营商对现有的设备进行升级,所以这可能是 IPv6 普及率比较慢的一个原因

IPv6 的亮点

IPv6 不仅仅只是可分配的地址变多了,他还有非常多的亮点。

  • IPv6 可自动配置,即使没有 DHCP 服务器也可以实现自动分配IP地址,真是便捷到即插即用啊。
  • IPv6 包头包首部长度采用固定的值 40 字节,去掉了包头校验和,简化了首部结构,减轻了路由器负荷,大大提高了传输的性能
  • IPv6 有应对伪造 IP 地址的网络安全功能以及防止线路窃听的功能,大大提升了安全性

IPv6 地址的标识方法

IPv4 地址长度共 32 位,是以每 8 位作为一组,并用点分十进制的表示方式。

IPv6 地址长度是 128 位,是以每 16 位作为一组,每组用冒号 「:」 隔开

IPv6 地址表示方法

如果出现连续的 0 时还可以将这些 0 省略,并用两个冒号 「::」隔开。但是,一个 IP 地址中只允许出现一次两个连续的冒号

IPv6 地址缺省表示方法

IPv6 地址的结构

Pv6 类似 IPv4,也是通过 IP 地址的前几位标识 IP 地址的种类。

IPv6 的地址主要有一下类型地址:

  • 单播地址,用于一对一的通信
  • 组播地址,用于一对多的通信
  • 任播地址,用于通信最近的节点,最近的节点是由路由协议决定
  • 没有广播地址

IPv6地址结构

IPv6 单播地址类型

对于一对一通信的 IPv6 地址,主要划分了三类单播地址,每类地址的有效范围都不同。

  • 在同一链路单播通信,不经过路由器,可以使用链路本地单播地址,IPv4 没有此类型
  • 在内网里单播通信,可以使用唯一本地地址,相当于 IPv4 的私有 IP
  • 在互联网通信,可以使用全局单播地址,相当于 IPv4 的公有 IP

图片

IPv4 首部与 IPv6 首部

图片

IPv6 相比 IPv4 的首部改进:

  • 取消了首部校验和字段。 因为在数据链路层和传输层都会校验,因此 IPv6 直接取消了 IP 的校验。
  • 取消了分片/重新组装相关字段。 分片与重组是耗时的过程,IPv6 不允许在中间路由器进行分片与重组,这种操作只能在源与目标主机,这将大大提高了路由器转发的速度。
  • 取消选项字段。 选项字段不再是标准 IP 首部的一部分了,但它并没有消失,而是可能出现在 IPv6 首部中的「下一个首部」指出的位置上。删除该选项字段是的 IPv6 的首部成为固定长度的 40 字节。

IP 协议相关技术

跟 IP 协议相关的技术也不少,接下来说说与 IP 协议相关的重要且常见的技术。

  • DNS 域名解析
  • ARP 与 RARP 协议
  • DHCP 动态获取 IP 地址
  • NAT 网络地址转换
  • ICMP 互联网控制报文协议
  • IGMP 因特网组管理协

DNS

我们在上网的时候,通常使用的方式域名,而不是 IP 地址,因为域名方便人类记忆。

那么实现这一技术的就是 DNS 域名解析,DNS 可以将域名网址自动转换为具体的 IP 地址

域名的层级关系

DNS 中的域名都是用句点来分隔的,比如 www.server.com,这里的句点代表了不同层次之间的界限

在域名中,越靠右的位置表示其层级越高

毕竟域名是外国人发明,所以思维和中国人相反,比如说一个城市地点的时候,外国喜欢从小到大的方式顺序说起(如 XX 街道 XX 区 XX 市 XX 省),而中国则喜欢从大到小的顺序(如 XX 省 XX 市 XX 区 XX 街道)。

根域是在最顶层,它的下一层就是 com 顶级域,再下面是 server.com。

所以域名的层级关系类似一个树状结构:

  • 根 DNS 服务器
  • 顶级域 DNS 服务器(com)
  • 权威 DNS 服务器(server.com)

图片

根域的 DNS 服务器信息保存在互联网中所有的 DNS 服务器中。这样一来,任何 DNS 服务器就都可以找到并访问根域 DNS 服务器了。

因此,客户端只要能够找到任意一台 DNS 服务器,就可以通过它找到根域 DNS 服务器,然后再一路顺藤摸瓜找到位于下层的某台目标 DNS 服务器。

域名解析的工作流程

浏览器首先看一下自己的缓存里有没有,如果没有就向操作系统的缓存要,还没有就检查本机域名解析文件 hosts,如果还是没有,就会 DNS 服务器进行查询,查询的过程如下:

  1. 客户端首先会发出一个 DNS 请求,问 www.server.com 的 IP 是啥,并发给本地 DNS 服务器(也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址)。
  2. 本地域名服务器收到客户端的请求后,如果缓存里的表格能找到 www.server.com,则它直接返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器:“老大, 能告诉我 www.server.com 的 IP 地址吗?” 根域名服务器是最高层次的,它不直接用于域名解析,但能指明一条道路。
  3. 根 DNS 收到来自本地 DNS 的请求后,发现后置是 .com,说:“www.server.com 这个域名归 .com 区域管理”,我给你 .com 顶级域名服务器地址给你,你去问问它吧。”
  4. 本地 DNS 收到顶级域名服务器的地址后,发起请求问“老二, 你能告诉我 www.server.com 的 IP 地址吗?”
  5. 顶级域名服务器说:“我给你负责 www.server.com 区域的权威 DNS 服务器的地址,你去问它应该能问到”。
  6. 本地 DNS 于是转向问权威 DNS 服务器:“老三,www.server.com对应的IP是啥呀?” server.com 的权威 DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。
  7. 权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。
  8. 本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接。

至此,我们完成了 DNS 的解析过程。现在总结一下,整个过程我画成了一个图。

图片

DNS 域名解析的过程蛮有意思的,整个过程就和我们日常生活中找人问路的过程类似,只指路不带路

ARP

在传输一个 IP 数据报的时候,确定了源 IP 地址和目标 IP 地址后,就会通过主机「路由表」确定 IP 数据包下一跳。然而,网络层的下一层是数据链路层,所以我们还要知道「下一跳」的 MAC 地址。

由于主机的路由表中可以找到下一条的 IP 地址,所以可以通过 ARP 协议,求得下一跳的 MAC 地址。

那么 ARP 又是如何知道对方 MAC 地址的呢?

ARP 是借助 ARP 请求与 ARP 响应两种类型的包确定 MAC 地址的

ARP 广播

  • 主机会通过广播发送 ARP 请求,这个包中包含了想要知道的 MAC 地址的主机 IP 地址。
  • 当同个链路中的所有设备收到 ARP 请求时,会去拆开 ARP 请求包里的内容,如果 ARP 请求包中的目标 IP 地址与自己的 IP 地址一致,那么这个设备就将自己的 MAC 地址塞入 ARP 响应包返回给主机。

操作系统通常会把第一次通过 ARP 获取的 MAC 地址缓存起来,以便下次直接从缓存中找到对应 IP 地址的 MAC 地址。

不过,MAC 地址的缓存是有一定期限的,超过这个期限,缓存的内容将被清除

RARP

ARP 协议是已知 IP 地址 求 MAC 地址,那 RARP 协议正好相反。

它是已知 MAC 地址求 IP 地址。例如将打印机服务器等小型嵌入式设备接入到网络时就经常会用得到。

通常这需要架设一台 RARP 服务器,在这个服务器上注册设备的 MAC 地址及其 IP 地址。然后再将这个设备接入到网络,接着:

  • 该设备会发送一条「我的 MAC 地址是XXXX,请告诉我,我的IP地址应该是什么」的请求信息。
  • RARP 服务器接到这个消息后返回「MAC地址为 XXXX 的设备,IP地址为 XXXX」的信息给这个设备

最后,设备就根据从 RARP 服务器所收到的应答信息设置自己的 IP 地址

RARP

DHCP

我们的电脑通常都是通过 DHCP 动态获取 IP 地址,大大省去了配 IP 信息繁琐的过程。

接下来,我们来看看我们的电脑是如何通过 4 个步骤的过程,获取到 IP 的

DHCP 工作流程

先说明一点,DHCP 客户端进程监听的是 68 端口号,DHCP 服务端进程监听的是 67 端口号。

DHCP 交互的 4 个步骤

  • 客户端首先发起 DHCP 发现报文(DHCP DISCOVER) 的 IP 数据报,由于客户端没有 IP 地址,也不知道 DHCP 服务器的地址,所以使用的是 UDP 广播通信,其使用的广播目的地址是 255.255.255.255(端口 67) 并且使用 0.0.0.0(端口 68) 作为源 IP 地址。DHCP 客户端将该 IP 数据报传递给链路层,链路层然后将帧广播到所有的网络中设备。
  • DHCP 服务器收到 DHCP 发现报文时,用 DHCP 提供报文(DHCP OFFER) 向客户端做出响应。该报文仍然使用 IP 广播地址 255.255.255.255,该报文信息携带服务器提供可租约的 IP 地址、子网掩码、默认网关、DNS 服务器以及 IP 地址租用期
  • 客户端收到一个或多个服务器的 DHCP 提供报文后,从中选择一个服务器,并向选中的服务器发送 DHCP 请求报文(DHCP REQUEST进行响应,回显配置的参数。
  • 最后,服务端用 DHCP ACK 报文对 DHCP 请求报文进行响应,应答所要求的参数。

一旦客户端收到 DHCP ACK 后,交互便完成了,并且客户端能够在租用期内使用 DHCP 服务器分配的 IP 地址。

如果租约的 DHCP IP 地址快期后,客户端会向服务器发送 DHCP 请求报文:

  • 服务器如果同意继续租用,则用 DHCP ACK 报文进行应答,客户端就会延长租期。
  • 服务器如果不同意继续租用,则用 DHCP NACK 报文,客户端就要停止使用租约的 IP 地址。

可以发现,DHCP 交互中,全程都是使用 UDP 广播通信

用的是广播,那如果 DHCP 服务器和客户端不是在同一个局域网内,路由器又不会转发广播包,那不是每个网络都要配一个 DHCP 服务器?

DHCP 中继代理

为了解决这一问题,就出现了 DHCP 中继代理

有了 DHCP 中继代理以后,对不同网段的 IP 地址分配也可以由一个 DHCP 服务器统一进行管理。

DHCP 中继代理

  • DHCP 客户端会向 DHCP 中继代理发送 DHCP 请求包,而 DHCP 中继代理在收到这个广播包以后,再以单播的形式发给 DHCP 服务器。
  • 服务器端收到该包以后再向 DHCP 中继代理返回应答,并由 DHCP 中继代理将此包转发给 DHCP 客户端

因此,DHCP 服务器即使不在同一个链路上也可以实现统一分配和管理IP地址

NAT

提出了一个种网络地址转换 NAT 的方法,再次缓解了 IPv4 地址耗尽的问题

简单的来说 NAT 就是在同个公司、家庭、教室内的主机对外部通信时,把私有 IP 地址转换成公有 IP 地址

NAT

那不是 N 个 私有 IP 地址,你就要 N 个公有 IP 地址?这怎么就缓解了 IPv4 地址耗尽的问题?

IPv4 地址耗尽问题

确实是,普通的 NAT 转换没什么意义。

由于绝大多数的网络应用都是使用传输层协议 TCP 或 UDP 来传输数据的。

因此,可以把 IP 地址 + 端口号一起进行转换。

这样,就用一个全球 IP 地址就可以了,这种转换技术就叫网络地址与端口转换 NAPT

NAPT

图中有两个客户端 192.168.1.10 和 192.168.1.11 同时与服务器 183.232.231.172 进行通信,并且这两个客户端的本地端口都是 1025。

此时,两个私有 IP 地址都转换 IP 地址为公有地址 120.229.175.121,但是以不同的端口号作为区分。

于是,生成一个 NAPT 路由器的转换表,就可以正确地转换地址跟端口的组合,令客户端 A、B 能同时与服务器之间进行通信

这种转换表在 NAT 路由器上自动生成。例如,在 TCP 的情况下,建立 TCP 连接首次握手时的 SYN 包一经发出,就会生成这个表。而后又随着收到关闭连接时发出 FIN 包的确认应答从表中被删除。

NAT 那么牛逼,难道就没缺点了吗

当然有缺陷,肯定没有十全十美的方案。

由于 NAT/NAPT 都依赖于自己的转换表,因此会有以下的问题:

  • 外部无法主动与 NAT 内部服务器建立连接,因为 NAPT 转换表没有转换记录。
  • 转换表的生产与转换操作都会产生性能开销。
  • 通信过程中,如果 NAT 路由器重启了,所有的 TCP 连接都将被重置。

如何解决 NAT 潜在的问题呢

解决 NAT 潜在的问题

解决的方法主要两种方法。

第一种就是改用 IPv6

IPv6 可用范围非常大,以至于每台设备都可以配置一个公有 IP 地址,就不搞那么多花里胡哨的地址转换了,但是 IPv6 普及速度还需要一些时间。

第二种 NAT 穿透技术

NAT 穿越技术拥有这样的功能,它能够让网络应用程序主动发现自己位于 NAT 设备之后,并且会主动获得 NAT 设备的公有 IP,并为自己建立端口映射条目,注意这些都是 NAT设备后的应用程序自动完成的。

也就是说,在 NAT 穿越技术中,NAT 设备后的应用程序处于主动地位,它已经明确地知道 NAT 设备要修改它外发的数据包,于是它主动配合 NAT 设备的操作,主动地建立好映射,这样就不像以前由 NAT 设备来建立映射了。

说人话,就是客户端主动从 NAT 设备获取公有 IP 地址,然后自己建立端口映射条目,然后用这个条目对外通信,就不需要 NAT 设备来进行转换了

ICMP

CMP 全称是 Internet Control Message Protocol,也就是互联网控制报文协议

里面有个关键词 —— 控制,如何控制的呢?

网络包在复杂的网络传输环境里,常常会遇到各种问题。

当遇到问题的时候,总不能死个不明不白,没头没脑的作风不是计算机网络的风格。所以需要传出消息,报告遇到了什么问题,这样才可以调整传输策略,以此来控制整个局面

ICMP 功能都有啥

ICMP 功能

ICMP 主要的功能包括:确认 IP 包是否成功送达目标地址、报告发送过程中 IP 包被废弃的原因和改善网络设置等。

IP 通信中如果某个 IP 包因为某种原因未能达到目标地址,那么这个具体的原因将由 ICMP 负责通知

ICMP 目标不可达消息

如上图例子,主机 A 向主机 B 发送了数据包,由于某种原因,途中的路由器 2 未能发现主机 B 的存在,这时,路由器 2 就会向主机 A 发送一个 ICMP 目标不可达数据包,说明发往主机 B 的包未能成功。

ICMP 的这种通知消息会使用 IP 进行发送

因此,从路由器 2 返回的 ICMP 包会按照往常的路由控制先经过路由器 1 再转发给主机 A 。收到该 ICMP 包的主机 A 则分解 ICMP 的首部和数据域以后得知具体发生问题的原因

ICMP 类型

ICMP 类型

ICMP 大致可以分为两大类:

  • 一类是用于诊断的查询消息,也就是「查询报文类型
  • 另一类是通知出错原因的错误消息,也就是「差错报文类型

常见的 ICMP 类型

IGMP

ICMP 跟 IGMP 是一点关系都没有的,就好像周杰与周杰伦的区别

前面我们知道了组播地址,也就是 D 类地址,既然是组播,那就说明是只有一组的主机能收到数据包,不在一组的主机不能收到数组包,怎么管理是否是在一组呢?那么,就需要 IGMP 协议了。

组播模型

GMP 是因特网组管理协议,工作在主机(组播成员)和最后一跳路由之间,如上图中的蓝色部分。

  • IGMP 报文向路由器申请加入和退出组播组,默认情况下路由器是不会转发组播包到连接中的主机,除非主机通过 IGMP 加入到组播组,主机申请加入到组播组时,路由器就会记录 IGMP 路由器表,路由器后续就会转发该组播地址的数据包了。
  • IGMP 报文采用 IP 封装,IP 头部的协议号为 2,而且 TTL 字段值通常 为 1,因为 IGMP 是工作在主机与连接的路由器之间。

IGMP 工作机制

IGMP 工作机制

IGMP 分为了三个版本分别是,IGMPv1、IGMPv2、IGMPv3。

接下来,以 IGMPv2 作为例子,说说常规查询与响应和离开组播组这两个工作机制。

常规查询与响应工作机制

IGMP 常规查询与响应工作机制

  1. 路由器会周期性发送目的地址为 224.0.0.1(表示同一网段内所有主机和路由器) IGMP 常规查询报文;
  2. 主机1 和 主机 3 收到这个查询,随后会启动「报告延迟计时器」,计时器的时间是随机的,通常是 0~10 秒,计时器超时后主机就会发送 IGMP 成员关系报告报文(源 IP 地址为自己主机的 IP 地址,目的 IP 地址为组播地址)。如果在定时器超时之前,收到同一个组内的其他主机发送的成员关系报告报文,则自己不再发送,这样可以减少网络中多余的 IGMP 报文数量;
  3. 路由器收到主机的成员关系报告报文后,就会在 IGMP 路由表中加入该组播组,后续网络中一旦该组播地址的数据到达路由器,它会把数据包转发出去
离开组播组工作机制

离开组播组的情况一,网段中仍有该组播组:

IGMPv2 离开组播组工作机制 情况1

  1. 主机 1 要离开组 224.1.1.1,发送 IGMPv2 离组报文,报文的目的地址是 224.0.0.2(表示发向网段内的所有路由器);
  2. 路由器收到该报文后,以 1 秒为间隔连续发送 IGMP 特定组查询报文(共计发送 2 个),以便确认该网络是否还有 224.1.1.1 组的其他成员;
  3. 主机 3 仍然是组 224.1.1.1 的成员,因此它立即响应这个特定组查询。路由器知道该网络中仍然存在该组播组的成员,于是继续向该网络转发 224.1.1.1 的组播数据包

离开组播组的情况二,网段中没有该组播组

IGMPv2 离开组播组工作机制 情况2

注解的概念

​ 注解:
​ 用在类上,方法上,成员变量,构造器,…上对成分进行编译约束,标记等操作的。
​ 注解是JDK1.5的新特性。
​ 注解相当一种标记,是类的组成部分,可以给类携带一些额外的信息。
​ 注解是给编译器或JVM看的,编译器或JVM可以根据注解来完成对应的功能。
​ 注解作用:
​ 1.标记。
​ 2.方法重写约束 @Override
​ 3.函数式接口约束。 @FunctionalInterface.
​ 4.现今最牛逼的框架技术多半都是在使用注解和反射。都是属于框架的底层基础技术。

自定义注解

1
2
3
4
自定义注解的格式:
修饰符 @interface 注解名{
// 注解属性
}

注解的属性

属性的格式
- 格式1:数据类型 属性名();
- 格式2:数据类型 属性名() default 默认值;

属性适用的数据类型:
八种数据数据类型(int,short,long,double,byte,char,boolean,float)
String,Class
以上类型的数组形式都支持
小结:
注解可以有属性,属性名必须带()
在用注解的时候,属性必须赋值,除非这个属性有默认值!!

注解的特殊属性名称

value
value属性,如果只有一个value属性的情况下,
使用value属性的时候可以省略value名称不写!!
但是如果有多个属性,且多个属性没有默认值,那么value是不能省略的。

元注解

元注解是sun公司提供的。
元注解是用在自定义注解上的注解。
元注解是用来注解自定义注解的。

 元注解有两个:
     @Target:约束自定义注解只能在哪些地方使用,
         -- 但是默认的注解可以在类,方法,构造器,成员变量,... 使用。

     @Retention:申明注解的生命周期
         -- 申明注解的作用范围:编译时,运行时。

 @Target
      * 作用:用来标识注解使用的位置,如果没有使用该注解标识,则自定义的注解可以使用在任意位置。
      * 可使用的值定义在ElementType枚举类中,常用值如下
         TYPE,类,接口
         FIELD, 成员变量
         METHOD, 成员方法
         PARAMETER, 方法参数
         CONSTRUCTOR, 构造器
         LOCAL_VARIABLE, 局部变量

 @Retention
     作用:用来标识注解的生命周期(有效存活范围)
      * 可使用的值定义在RetentionPolicy枚举类中,常用值如下
      * SOURCE:注解只作用在源码阶段,生成的字节码文件中不存在
      * CLASS:注解作用在源码阶段,字节码文件阶段,运行阶段不存在,默认值.
      * RUNTIME:注解作用在源码阶段,字节码文件阶段,运行阶段(开发常用)
 小结:
    @Target约束自定义注解可以标记的范围。
    @Retention用来约束自定义注解的存活范围。

注解的解析(了解)

我们会使用注解注释一个类的成分,那么就设计到要解析出这些注解的数据。
开发中经常要知道一个类的成分上面到底有哪些注解,注解有哪些属性数据,这都需要进行注解的解析。

 与注解解析相关的接口
      1. Annotation: 注解类型,该类是所有注解的父类。注解都是一个Annotation的对象
      2. AnnotatedElement:该接口定义了与注解解析相关的方法
        所有的类成分Class, Method , Field , Constructor:都实现了AnnotatedElement接口
        他们都拥有解析注解的能力:
         a.Annotation[]	getDeclaredAnnotations()
            获得当前对象上使用的所有注解,返回注解数组。
         b.T getDeclaredAnnotation(Class<T> annotationClass)
             根据注解类型获得对应注解对象
         c.boolean isAnnotationPresent(Class<Annotation> annotationClass)
            判断当前对象是否使用了指定的注解,如果使用了则返回true,否则false

 解析注解数据的原理
     * 注解在哪个成分上,我们就先拿哪个成分对象。
     * 比如注解作用成员方法,则要获得该成员方法对应的Method对象,再来拿上面的注解
     * 比如注解作用在类上,则要该类的Class对象,再来拿上面的注解
     * 比如注解作用在成员变量上,则要获得该成员变量对应的Field对象,再来拿上面的注解

网络通信

基本软件结构

1
2
3
4
5
6
7
8
9
10
通信一定是基于软件结构实现的:
1.C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。
常见程序有QQ、迅雷,IDEA等软件。

2.B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。
常见浏览器有谷歌、火狐等、软件:博学谷、京东、淘宝。
(开发中的重点,基于网页设计界面,界面效果可以更丰富: Java Web开发)

两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,
就是在一定的协议下,实现两台计算机的通信的技术。

网络通信的三要素

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
1.协议
协议:计算机网络客户端与服务端通信必须事先约定和彼此遵守的通信规则。
HTTP , FTP , TCP , UDP , SSH , SMTP。

2.IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。
IP地址用来给一个网络中的计算机设备做唯一的编号。

IPv4:4个字节,32位组成。 192.168.70.70
局域网
城域网
广域网(公网)

局域网:公司内部用。
公网:可以在任何地方访问。

IPv6: 可以实现为所有设备分配IP 128位

ipconfig:查看本机的IP
ping 检查本机与某个IP指定的机器是否联通,或者说是检测对方是否在线。
ping 空格 IP地址
ping 220.181.57.216
ping www.baidu.com
3.端口:
端口号就可以唯一标识设备中的进程(应用程序)了
端口号:
用两个字节表示的整数,它的取值范围是0~65535。
0~1023之间的端口号用于一些知名的网络服务和应用。
普通的应用程序需要使用1024以上的端口号。
如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。报出端口被占用异常!!

利用`协议`+`IP地址`+`端口号` 三元组合,
就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。

*** 注意:特殊的IP地址: 本机IP地址.(不受环境的影响,任何时候都存在这两个ip,可以直接找本机!)
127.0.0.1 == localhost。***

网络通信的分层和协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
网络通信协议:通信协议是对计算机必须遵守的规则,
只有遵守这些规则,计算机之间才能进行通信。

-------------------------------------------------------------------------------
应用层 :应用程序(QQ,微信,浏览器),可能用到的协议(HTTP,FTP,SMTP) 通常程序员只需要关心这一层
------------------------------------------------------------------------------
传输层 :TCP/IP协议 - UDP协议 计算机网络工程师需要精通的协议,有些技术我们也需要精通这一层协议,
-----------------------------------------------------------------
网络层 :IP协议 封装自己的IP和对方的IP和端口
-----------------------------------------------------------------
数据链路层 : 进入到硬件(网)
-----------------------------------------------------------------
TCP/IP协议:传输控制协议 (Transmission Control Protocol)。
TCP协议是面向连接的安全的可靠的传输通信协议。
1.在通信之前必须确定对方在线并且连接成功才可以通信。
2.例如下载文件、浏览网页等(要求可靠传输)

UDP:用户数据报协议(User Datagram Protocol)。
UDP协议是一个面向无连接的不可靠传输的协议。
1.直接发消息给对方,不管对方是否在线,发消息后也不需要确认。
2.无线(视频会议,通话),性能好,可能丢失一些数据!!

InetAddress类概述

1
2
3
4
5
6
7
8
9
10
一个该类的对象就代表一个IP地址对象。
InetAddress类成员方法:
static InetAddress getLocalHost()
* 获得本地主机IP地址对象。
static InetAddress getByName(String host)
* 根据IP地址字符串或主机名获得对应的IP地址对象。
String getHostName()
* 获得主机名。
String getHostAddress()
* 获得IP地址字符串。

UDP通信

UDP通信的使用(了解即可)

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
UDP协议的特点
* 面向无连接的协议
* 发送端只管发送,不确认对方是否能收到。
* 基于数据包进行数据传输。
* 发送数据的包的大小限制64KB以内
* 因为面向无连接,速度快,但是不可靠。会丢失数据!

UDP协议的使用场景
* 在线视频
* 网络语音电话

UDP协议相关的两个类
* DatagramPacket
* 数据包对象
* 作用:用来封装要发送或要接收的数据,比如:集装箱
* DatagramSocket
* 发送对象
* 作用:用来发送或接收数据包,比如:码头
DatagramPacket类构造器
发送端用:
new DatagramPacket(byte[] buf, int length, InetAddress address, int port)
创建发送端数据包对象
* buf:要发送的内容,字节数组
* length:要发送内容的长度,单位是字节
* address:接收端的IP地址对象
* port:接收端的端口号

接收端用:
new DatagramPacket(byte[] buf, int length)
* 创建接收端的数据包对象
* buf:用来存储接收到内容
* length:能够接收内容的长度

DatagramPacket类常用方法
* int getLength() 获得实际接收到的字节个数

DatagramSocket类构造方法
* DatagramSocket() 创建发送端的Socket对象,系统会随机分配一个端口号。
* DatagramSocket(int port) 创建接收端的Socket对象并指定端口号

DatagramSocket类成员方法
* void send(DatagramPacket dp) 发送数据包
* void receive(DatagramPacket p) 接收数据包

TCP通信(重要)

TCP可靠传输通信

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
TCP/IP协议 ==> Transfer Control Protocol ==> 传输控制协议
TCP/IP协议的特点
* 面向连接的协议
* 只能由客户端主动发送数据给服务器端,服务器端接收到数据之后,可以给客户端响应数据。
* 通过三次握手建立连接,连接成功形成数据传输通道。
* 通过四次挥手断开连接
* 基于IO流进行数据传输
* 传输数据大小没有限制
* 因为面向连接的协议,速度慢,但是是可靠的协议。

TCP协议的使用场景
* 文件上传和下载
* 邮件发送和接收
* 远程登录

TCP协议相关的类
* Socket
* 一个该类的对象就代表一个客户端程序。
* ServerSocket
* 一个该类的对象就代表一个服务器端程序。

TCP通信也叫Socket网络编程,只要代码基于Socket开发,底层就是基于了可靠传输的
TCP通信。

Socket类构造方法
* Socket(String host, int port)
* 根据ip地址字符串和端口号创建客户端Socket对象
* 注意事项:只要执行该方法,就会立即连接指定的服务器程序,如果连接不成功,则会抛出异常。
如果连接成功,则表示三次握手通过。

Socket类常用方法
* OutputStream getOutputStream(); 获得字节输出流对象
* InputStream getInputStream();获得字节输入流对象

客户端的开发流程:
1.客户端要请求于服务端的socket管道连接。
2.从socket通信管道中得到一个字节输出流
3.通过字节输出流给服务端写出数据。
服务端的开发流程:
1.注册端口。
2.接收客户端的Socket管道连接。
3.从socket通信管道中得到一个字节输入流。
4.从字节输入流中读取客户端发来的数据。

小结:
1.客户端用Socket连接服务端。
2.服务端用ServerSocket注册端口,接收客户端的Socket连接。
3.通信是很严格的,对方怎么发,你就应该怎么收,对方发多少你就只能收多少。
4.实现的面向连接的socket端到端的通信管道,一方如果出现对象,另一方会出现异常!

需求:客户端发送一行数据,服务端接收一行数据!!

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws Exception {
// 1.客户端要请求于服务端的socket管道连接。
// Socket(String host, int port)
Socket socket = new Socket("127.0.0.1" , 9999);
// 2.从socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.把低级的字节输出流包装成高级的打印流。
PrintStream ps = new PrintStream(os);
// 4.开始发消息出去
ps.println("我是客户端,喜欢你很久了,第一次给你发消息,只想说:约吗?");
ps.flush();
System.out.println("客户端发送完毕~~~~");
}

服务器

1.注册端口。
2.接收客户端的Socket管道连接。
3.从socket通信管道中得到一个字节输入流。
4.从字节输入流中读取客户端发来的数据。
ServerSocket类:
构造器:public ServerSocket(int port)
方法:public Socket accept():
– 等待接收一个客户端的Socket管道连接请求,连接成功返回一个Socket对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1.注册端口: public ServerSocket(int port)
ServerSocket serverSocket = new ServerSocket(9999);
// 2.开始等待接收客户端的Socket管道连接。
Socket socket = serverSocket.accept();
// 3.从socket通信管道中得到一个字节输入流。
InputStream is = socket.getInputStream();
// 4.把字节输入流转换成字符输入流
Reader isr = new InputStreamReader(is);
// 5.把字符输入流包装成缓冲字符输入流。
BufferedReader br = new BufferedReader(isr);
// 6.按照行读取消息 。
String line ;
if((line = br.readLine())!=null){
System.out.println(line);
}
}

目标:客户端可以反复发送数据,服务端可以反复数据!!

分析:
只需要让客户端通过循环进行数据的发送。

产生的问题:目前服务端只能接收一个客户端请求。

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception {
// 1.客户端要请求于服务端的socket管道连接。
// Socket(String host, int port)
Socket socket = new Socket("127.0.0.1" , 9999);
// 2.从socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.把低级的字节输出流包装成高级的打印流。
PrintStream ps = new PrintStream(os);
// 4.开始发消息出去
while(true){
Scanner sc = new Scanner(System.in);
System.out.print("请说:");
ps.println(sc.nextLine());
ps.flush();
}
}

服务器

1.注册端口。
2.接收客户端的Socket管道连接。
3.从socket通信管道中得到一个字节输入流。
4.从字节输入流中读取客户端发来的数据。
ServerSocket类:
构造器:public ServerSocket(int port)
方法:public Socket accept():
– 等待接收一个客户端的Socket管道连接请求,连接成功返回一个Socket对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1.注册端口: public ServerSocket(int port)
ServerSocket serverSocket = new ServerSocket(9999);
// 2.开始等待接收客户端的Socket管道连接。
Socket socket = serverSocket.accept();
// 3.从socket通信管道中得到一个字节输入流。
InputStream is = socket.getInputStream();
// 4.把字节输入流转换成字符输入流
Reader isr = new InputStreamReader(is);
// 5.把字符输入流包装成缓冲字符输入流。
BufferedReader br = new BufferedReader(isr);
// 6.按照行读取消息 。
String line ;
while((line = br.readLine())!=null){
System.out.println(line);
}
}

目标:实现一个服务端可以同时接收多个客户端的消息

总结:
需要在服务端引入多线程。
每接收一个客户端的Socket通道,就为它分配一个独立的线程来处理它的消息。
如此便可实现:一个服务端可以同时接收多个客户端的消息。

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception {
// 1.客户端要请求于服务端的socket管道连接。
// Socket(String host, int port)
Socket socket = new Socket("127.0.0.1" , 9999);
// 2.从socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.把低级的字节输出流包装成高级的打印流。
PrintStream ps = new PrintStream(os);
// 4.开始发消息出去
while(true){
Scanner sc = new Scanner(System.in);
System.out.print("请说:");
ps.println(sc.nextLine());
ps.flush();
}
}

服务器

1.注册端口。
2.接收客户端的Socket管道连接。
3.从socket通信管道中得到一个字节输入流。
4.从字节输入流中读取客户端发来的数据。

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
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1.注册端口: public ServerSocket(int port)
ServerSocket serverSocket = new ServerSocket(9999);
// 2.定义一个循环不断的接收客户端的连接请求
while(true){
// 3.开始等待接收客户端的Socket管道连接。
Socket socket = serverSocket.accept();
// 4.每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。
new ServerReaderThread(socket).start();
}
}

class ServerReaderThread extends Thread{
private Socket socket ;
public ServerReaderThread(Socket socket){
this.socket = socket;
}

@Override
public void run() {
try{
// 3.从socket通信管道中得到一个字节输入流。
InputStream is = socket.getInputStream();
// 4.把字节输入流转换成字符输入流
Reader isr = new InputStreamReader(is);
// 5.把字符输入流包装成缓冲字符输入流。
BufferedReader br = new BufferedReader(isr);
// 6.按照行读取消息 。
String line ;
while((line = br.readLine())!=null){
System.out.println(socket.getRemoteSocketAddress()+"说:"+line);
}
}catch (Exception e){
System.out.println(socket.getRemoteSocketAddress()+"下线了~~~~~~");
}
}
}

在服务端引入线程池,使用线程池来处理与客户端的消息通信!!

我们之前引入的线程解决一个服务端可以接收多个客户端消息
客户端与服务端的线程模型是: N-N的关系。 一个客户端要一个线程。
这种模型是不行的,并发越高,系统瘫痪的越快!!

使用线程池来处理与客户端的消息通信!!
线程池不会引起出现过多的线程而导致系统死机!!

这种方案的优劣势:
优势:不会引起系统的死机,可以控制并发线程的数量。
劣势:同时可以并发的线程将受到限制

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
try {
// 1.客户端要请求于服务端的socket管道连接。
// Socket(String host, int port)
Socket socket = new Socket("127.0.0.1" , 9999);
// 2.从socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.把低级的字节输出流包装成高级的打印流。
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while(true){
System.out.print("请说:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}

线程池处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HandlerSocketThreadPool {

// 线程池
private ExecutorService executor;
// 线程池:3个线程 100个
public HandlerSocketThreadPool(int maxPoolSize, int queueSize){
executor = new ThreadPoolExecutor(
maxPoolSize,
maxPoolSize,
120L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(queueSize) );
}

public void execute(Runnable task){
this.executor.execute(task);
}
}

线程任务类

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
class ReaderClientRunnable implements Runnable {
private Socket socket ;

public ReaderClientRunnable(Socket socket) {
this.socket = socket;
}

@Override
public void run() {
try {
// 读取一行数据
InputStream is = socket.getInputStream() ;
// 转成一个缓冲字符流
Reader fr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(fr);
// 一行一行的读取数据
String line = null ;
while((line = br.readLine())!=null){ // 阻塞式的!!
System.out.println("服务端收到了数据:"+line);
}
} catch (Exception e) {
System.out.println("有人下线了");
}
}
}

服务端

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
/ 放弃了1 客户端 一个线程的模型了
// 提供了线程池:
public static void main(String[] args) {
try {
System.out.println("----------服务端启动成功------------");
ServerSocket ss = new ServerSocket(9999);

// 一个服务端只需要对应一个线程池
HandlerSocketThreadPool handlerSocketThreadPool =
new HandlerSocketThreadPool(3, 100);

// 客户端可能有很多个
while(true){
Socket socket = ss.accept() ;
System.out.println("有人上线了!!");
// 每次收到一个客户端的socket请求,都需要为这个客户端分配一个
// 独立的线程 专门负责对这个客户端的通信!!
handlerSocketThreadPool.execute(new ReaderClientRunnable(socket));
}

} catch (Exception e) {
e.printStackTrace();
}
}

即时通信

客户端

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
public class ClientChat implements ActionListener {
/** 1.设计界面 */
private JFrame win = new JFrame();
/** 2.消息内容框架 */
public JTextArea smsContent =new JTextArea(23 , 50);
/** 3.发送消息的框 */
private JTextArea smsSend = new JTextArea(4,40);
/** 4.在线人数的区域 */
/** 存放人的数据 */
/** 展示在线人数的窗口 */
public JList<String> onLineUsers = new JList<>();

// 是否私聊按钮
private JCheckBox isPrivateBn = new JCheckBox("私聊");
// 消息按钮
private JButton sendBn = new JButton("发送");

// 登录界面
private JFrame loginView;

private JTextField ipEt , nameEt , idEt;

private Socket socket ;

public static void main(String[] args) {
new ClientChat().initView();

}

private void initView() {
/** 初始化聊天窗口的界面 */
win.setSize(650, 600);

/** 展示登录界面 */
displayLoginView();

/** 展示聊天界面 */
//displayChatView();


}

private void displayChatView() {

JPanel bottomPanel = new JPanel(new BorderLayout());
//-----------------------------------------------
// 将消息框和按钮 添加到窗口的底端
win.add(bottomPanel, BorderLayout.SOUTH);
bottomPanel.add(smsSend);
JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));
btns.add(sendBn);
btns.add(isPrivateBn);
bottomPanel.add(btns, BorderLayout.EAST);
//-----------------------------------------------
// 给发送消息按钮绑定点击事件监听器
// 将展示消息区centerPanel添加到窗口的中间
smsContent.setBackground(new Color(0xdd,0xdd,0xdd));
// 让展示消息区可以滚动。
win.add(new JScrollPane(smsContent), BorderLayout.CENTER);
smsContent.setEditable(false);
//-----------------------------------------------
// 用户列表和是否私聊放到窗口的最右边
Box rightBox = new Box(BoxLayout.Y_AXIS);
onLineUsers.setFixedCellWidth(120);
onLineUsers.setVisibleRowCount(13);
rightBox.add(new JScrollPane(onLineUsers));
win.add(rightBox, BorderLayout.EAST);
//-----------------------------------------------
// 关闭窗口退出当前程序
win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
win.pack(); // swing 加上这句 就可以拥有关闭窗口的功能
/** 设置窗口居中,显示出来 */
setWindowCenter(win,650,600,true);
// 发送按钮绑定点击事件
sendBn.addActionListener(this);
}

private void displayLoginView(){

/** 先让用户进行登录
* 服务端ip
* 用户名
* id
* */
/** 显示一个qq的登录框 */
loginView = new JFrame("登录");
loginView.setLayout(new GridLayout(3, 1));
loginView.setSize(400, 230);

JPanel ip = new JPanel();
JLabel label = new JLabel(" IP:");
ip.add(label);
ipEt = new JTextField(20);
ip.add(ipEt);
loginView.add(ip);

JPanel name = new JPanel();
JLabel label1 = new JLabel("姓名:");
name.add(label1);
nameEt = new JTextField(20);
name.add(nameEt);
loginView.add(name);

JPanel btnView = new JPanel();
JButton login = new JButton("登陆");
btnView.add(login);
JButton cancle = new JButton("取消");
btnView.add(cancle);
loginView.add(btnView);
// 关闭窗口退出当前程序
loginView.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setWindowCenter(loginView,400,260,true);

/** 给登录和取消绑定点击事件 */
login.addActionListener(this);
cancle.addActionListener(this);

}

private static void setWindowCenter(JFrame frame, int width , int height, boolean flag) {
/** 得到所在系统所在屏幕的宽高 */
Dimension ds = frame.getToolkit().getScreenSize();

/** 拿到电脑的宽 */
int width1 = ds.width;
/** 高 */
int height1 = ds.height ;

System.out.println(width1 +"*" + height1);
/** 设置窗口的左上角坐标 */
frame.setLocation(width1/2 - width/2, height1/2 -height/2);
frame.setVisible(flag);
}

@Override
public void actionPerformed(ActionEvent e) {
/** 得到点击的事件源 */
JButton btn = (JButton) e.getSource();
switch(btn.getText()){
case "登陆":
String ip = ipEt.getText().toString();
String name = nameEt.getText().toString();
// 校验参数是否为空
// 错误提示
String msg = "" ;
// 12.1.2.0
// \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\
if(ip==null || !ip.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")){
msg = "请输入合法的服务端ip地址";
}else if(name==null || !name.matches("\\S{1,}")){
msg = "姓名必须1个字符以上";
}

if(!msg.equals("")){
/** msg有内容说明参数有为空 */
// 参数一:弹出放到哪个窗口里面
JOptionPane.showMessageDialog(loginView, msg);
}else{
try {
// 参数都合法了
// 当前登录的用户,去服务端登陆
/** 先把当前用户的名称展示到界面 */
win.setTitle(name);
// 去服务端登陆连接一个socket管道
socket = new Socket(ip, Constants.PORT);

//为客户端的socket分配一个线程 专门负责收消息
new ClientReader(this,socket).start();

// 带上用户信息过去
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeInt(1); // 登录消息
dos.writeUTF(name.trim());
dos.flush();

// 关系当前窗口 弹出聊天界面
loginView.dispose(); // 登录窗口销毁
displayChatView(); // 展示了聊天窗口了


} catch (Exception e1) {
e1.printStackTrace();
}
}
break;
case "取消":
/** 退出系统 */
System.exit(0);
break;
case "发送":
// 得到发送消息的内容
String msgSend = smsSend.getText().toString();
if(!msgSend.trim().equals("")){
/** 发消息给服务端 */
try {
// 判断是否对谁发消息
String selectName = onLineUsers.getSelectedValue();
int flag = 2 ;// 群发 @消息
if(selectName!=null&&!selectName.equals("")){
msgSend =("@"+selectName+","+msgSend);
/** 判断是否选中了私法 */
if(isPrivateBn.isSelected()){
/** 私法 */
flag = 3 ;//私发消息
}

}

DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeInt(flag); // 群发消息 发送给所有人
dos.writeUTF(msgSend);
if(flag == 3){
// 告诉服务端我对谁私发
dos.writeUTF(selectName.trim());
}
dos.flush();

} catch (Exception e1) {
e1.printStackTrace();
}

}
smsSend.setText(null);
break;

}

}
}

class ClientReader extends Thread {

private Socket socket;
private ClientChat clientChat ;

public ClientReader(ClientChat clientChat, Socket socket) {
this.clientChat = clientChat;
this.socket = socket;
}

@Override
public void run() {
try {
DataInputStream dis = new DataInputStream(socket.getInputStream());
/** 循环一直等待客户端的消息 */
while(true){
/** 读取当前的消息类型 :登录,群发,私聊 , @消息 */
int flag = dis.readInt();
if(flag == 1){
// 在线人数消息回来了
String nameDatas = dis.readUTF();
// 展示到在线人数的界面
String[] names = nameDatas.split(Constants.SPILIT);

clientChat.onLineUsers.setListData(names);
}else if(flag == 2){
// 群发消息
String msg = dis.readUTF() ;
clientChat.smsContent.append(msg);
//滾動到底端
clientChat.smsContent.setCaretPosition(clientChat.smsContent.getText().length());
}
}
} catch (Exception e) {
e.printStackTrace();
}

}
}

Constants类

1
2
3
4
5
6
7
public class Constants {
/** 常量 */
public static final int PORT = 7778 ;

/** 协议分隔符 */
public static final String SPILIT = "003197♣♣㏘♣④④♣";
}

服务端

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
public class ServerChat {

/** 定义一个集合存放所有在线的socket */
public static Map<Socket, String> onLineSockets = new HashMap<>();

public static void main(String[] args) {
try {
/** 注册端口 */
ServerSocket serverSocket = new ServerSocket(Constants.PORT);

/** 循环一直等待所有可能的客户端连接 */
while(true){
Socket socket = serverSocket.accept();
/** 把客户端的socket管道单独配置一个线程来处理 */
new ServerReader(socket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

class ServerReader extends Thread {

private Socket socket;

public ServerReader(Socket socket) {
this.socket = socket;
}

@Override
public void run() {
DataInputStream dis = null;
try {
dis = new DataInputStream(socket.getInputStream());
/** 循环一直等待客户端的消息 */
while(true){
/** 读取当前的消息类型 :登录,群发,私聊 , @消息 */
int flag = dis.readInt();
if(flag == 1){
/** 先将当前登录的客户端socket存到在线人数的socket集合中 */
String name = dis.readUTF() ;
System.out.println(name+"---->"+socket.getRemoteSocketAddress());
ServerChat.onLineSockets.put(socket, name);
}
writeMsg(flag,dis);
}
} catch (Exception e) {
System.out.println("--有人下线了--");
// 从在线人数中将当前socket移出去
ServerChat.onLineSockets.remove(socket);
try {
// 从新更新在线人数并发给所有客户端
writeMsg(1,dis);
} catch (Exception e1) {
e1.printStackTrace();
}
}

}

private void writeMsg(int flag, DataInputStream dis) throws Exception {
// DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
// 定义一个变量存放最终的消息形式
String msg = null ;
if(flag == 1){
/** 读取所有在线人数发给所有客户端去更新自己的在线人数列表 */
/** onlineNames = [徐磊,zhangsan,李刚]*/
StringBuilder rs = new StringBuilder();
Collection<String> onlineNames = ServerChat.onLineSockets.values();
// 判断是否存在在线人数
if(onlineNames != null && onlineNames.size() > 0){
for(String name : onlineNames){
rs.append(name+ Constants.SPILIT);
}
// 徐磊003197♣♣㏘♣④④♣zhangsan003197♣♣㏘♣④④♣李刚003197♣♣㏘♣④④♣
// 去掉最后的一个分隔符
msg = rs.substring(0, rs.lastIndexOf(Constants.SPILIT));

/** 将消息发送给所有的客户端 */
sendMsgToAll(flag,msg);
}
}else if(flag == 2 || flag == 3){
// 读到消息 群发的 或者 @消息
String newMsg = dis.readUTF() ; // 消息
// 得到发件人
String sendName = ServerChat.onLineSockets.get(socket);

// 李刚 时间
// 内容--
StringBuilder msgFinal = new StringBuilder();
// 时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE");
if(flag == 2){
msgFinal.append(sendName).append(" ").append(sdf.format(System.currentTimeMillis())).append("\r\n");
msgFinal.append(" ").append(newMsg).append("\r\n");
sendMsgToAll(flag,msgFinal.toString());
}else if(flag == 3){
msgFinal.append(sendName).append(" ").append(sdf.format(System.currentTimeMillis())).append("对您私发\r\n");
msgFinal.append(" ").append(newMsg).append("\r\n");
// 私发
// 得到给谁私发
String destName = dis.readUTF();
sendMsgToOne(destName,msgFinal.toString());
}
}
}
/**
* @param destName 对谁私发
* @param msg 发的消息内容
* @throws Exception
*/
private void sendMsgToOne(String destName, String msg) throws Exception {
// 拿到所有的在线socket管道 给这些管道写出消息
Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
for(Socket sk : allOnLineSockets){
// 得到当前需要私发的socket
// 只对这个名字对应的socket私发消息
if(ServerChat.onLineSockets.get(sk).trim().equals(destName)){
DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
dos.writeInt(2); // 消息类型
dos.writeUTF(msg);
dos.flush();
}
}

}

private void sendMsgToAll(int flag, String msg) throws Exception {
// 拿到所有的在线socket管道 给这些管道写出消息
Set<Socket> allOnLineSockets = ServerChat.onLineSockets.keySet();
for(Socket sk : allOnLineSockets){
DataOutputStream dos = new DataOutputStream(sk.getOutputStream());
dos.writeInt(flag); // 消息类型
dos.writeUTF(msg);
dos.flush();
}
}
}

User

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
public class User {
private Integer id ;
private String name ;

public User(Integer id, String name) {
this.id = id;
this.name = name;
}

public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + "]";
}


}

文件上传

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
/**
目标:实现客户端上传图片给服务端保存起来。

开发客户端:本地图片: ‪D:\图片资源\beautiful.jpg
开发服务端:服务器路径: D:\约吧图片服务器
*/
public static void main(String[] args) throws Exception {
// 1.请求于服务端的Socket管道连接。
Socket socket = new Socket(Constants.SERVER_IP , Constants.SERVER_PORT);
// 2.从socket管道中得到一个字节输出流包装成缓冲字节输出流
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
// 3.提取本机的图片上传给服务端
// 4.得到一个缓冲字节输入流与本地图片接通
BufferedInputStream bis =
new BufferedInputStream(new FileInputStream(Constants.SRC_IMAGE));
// 5.定义一个字节数组
byte[] buffer = new byte[1024];
int len ;
while((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0 ,len);
}
bos.flush(); // 刷新图片数据到服务端!!
socket.shutdownOutput(); // 告诉服务端我的数据已经发送完毕,请不要在等我了!
bis.close(); // 可以关闭

// 6.等待着服务端的响应数据!!
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("收到服务端响应:"+br.readLine());
}
1
2
3
4
5
6
7
8
9
10
/**
* 客户端常量包
*/
public class Constants {
public static final String SRC_IMAGE = "D:\\itcast\\图片资源\\beautiful.jpg";
public static final String SERVER_DIR = "D:\\itcast\\约吧图片服务器\\";
public static final String SERVER_IP = "127.0.0.1";
public static final int SERVER_PORT = 8888;

}
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
/**
功能点:
1.接收多个客户端传输来的图片数据存储到服务器路径:
2.响应一个成功的消息给当前客户端。
*/
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1.注册端口: public ServerSocket(int port)
ServerSocket serverSocket = new ServerSocket(Constants.SERVER_PORT);
// 2.定义一个循环不断的接收客户端的连接请求
while(true){
// 3.开始等待接收客户端的Socket管道连接。
Socket socket = serverSocket.accept();
// 4.每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。
new ServerReaderThread(socket).start();
}
}
}

class ServerReaderThread extends Thread{
private Socket socket ;
public ServerReaderThread(Socket socket){
this.socket = socket;
}

@Override
public void run() {
try{
// 1.从socket通信管道中得到一个字节输入流读取客户端发来的图片数据!
InputStream is = socket.getInputStream();
// 2.包装成高级的缓冲字节输入流
BufferedInputStream bis = new BufferedInputStream(is);
// 3.定义一个缓冲字节输出流通向目标路径(服务端路径)
BufferedOutputStream bos =
new BufferedOutputStream(new FileOutputStream(Constants.SERVER_DIR+ UUID.randomUUID().toString()+".jpg"));
byte[] buffer = new byte[1024];
int len ;
while((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0 ,len);
}
bos.close();
System.out.println("服务端接收完毕了!");

// 4.响应数据给客户端
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("您好,已成功接收您上传的图片!");
ps.flush();

Thread.sleep(100000); // 等消失发送完毕被客户端接收后死亡!
}catch (Exception e){
System.out.println(socket.getRemoteSocketAddress()+"下线了~~~~~~");
}
}
}

BS-浏览器-服务器

之前客户端和服务端都需要自己开发。也就是CS架构。
接下来模拟一下BS架构。

客户端:浏览器。(无需开发)
服务端:自己开发。
需求:在浏览器中请求本程序,响应一个网页文字给浏览器显示。

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
public class BSserverDemo {
public static void main(String[] args) {
try {
// 1.注册端口
ServerSocket ss = new ServerSocket(8080);
// 2.创建一个循环接收多个客户端的请求。
while(true){
Socket socket = ss.accept();
// 3.交给一个独立的线程来处理!
new ServerReaderThread(socket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 响应消息数据给浏览器显示。
// 浏览器是基于HTTP协议通信!响应格式必须满足HTTP协议数据格式的要求,浏览器
// 才能够识别,否则响应消息浏览器根本不认识。
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("HTTP/1.1 200 OK"); // 响应数据的响应头数据!
ps.println("Content-Type:text/html;charset=UTF-8");//响应数据的类型。网页或者文本内容!
ps.println(); // 必须换一行
// 以下开始响应真实的数据!!
ps.println("<span style='color:green;font-size:100px;'>牛逼的128期<span>");

Thread.sleep(4000);
ps.close();

} catch (Exception e) {
e.printStackTrace();
}


}
}

通信模型

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
1.BIO通信模式:同步阻塞式通信。(Socket网络编程也就是上面的通信架构)
-- 同步:当前线程要自己进行数据的读写操作。(自己去银行取钱)
-- 异步: 当前线程可以去做其他事情,(委托一小弟拿银行卡到银行取钱,然后给你)
-- 阻塞: 在数据没有的情况下,还是要继续等待着读。(排队等待)
-- 非阻塞:在数据没有的情况下,会去做其他事情,一旦有了数据再来获取。(柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理)
BIO表示同步阻塞式IO,服务器实现模式为一个连接一个线程,
即客户端有连接请求时服务器端就需要启动一个线程进行处理,
如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
同步阻塞式性能极差:大量线程,大量阻塞。

2.伪异步通信:引入了线程池。
不需要一个客户端一个线程,可以实现1个线程复用来处理很多个客户端!
这种架构,可以避免系统的死机,因为不会出现很多线程,线程可控。
但是高并发下性能还是很差:a.线程数量少,数据依然是阻塞的。数据没有来线程还是要等待!

3.NIO表示同步非阻塞IO,服务器实现模式为请求对应一个线程,
即客户端发送的连接请求都会注册到多路复用器上,
多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

1个主线程专门负责接收客户端:
1个线程[c1 ,s2 ,c3,c4, ,s2 ,c3,c4,,c3,c4, ,s2 ,c3,c4]轮询所有的客户端,发来了数据才会开启线程处理
这种架构性能还可以!!
同步:线程还是要不断的接收客户端连接,以及处理数据。
非阻塞:如果一个管道没有数据,不需要等待,可以轮询下一个管道是否有数据!

4.AIO表示异步非阻塞IO,服务器实现模式为一个有效请求一个线程,
客户端的I/O请求都是由操作系统先完成IO操作后再通知服务器应用来启动线程进行处理。
异步:服务端线程接收到了客户端管道以后就交给底层处理它的io通信。
自己可以做其他事情。
非阻塞:底层也是客户端有数据才会处理,有了数据以后处理好通知服务器应用来启动线程进行处理。

小结:
各种模型应用场景:
BIO适用于连接数目比较小且固定的架构,该方式对服务器资源要求比较高,JDK 1.4以前的唯一选择。
NIO适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,编程复杂,
JDK 1.4开始支持。
AIO适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,
充分调用操作系统参与并发操作,编程复杂,JDK 1.7开始支持。

Lambda表达式的概述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
什么是Lambda表达式?
Lambda表达式是JDK1.8开始之后的新技术,是一种代码的新语法。
是一种特殊写法,
作用:“核心目的是为了简化匿名内部类的代码写法”。

Lambda表达式的格式:
(匿名内部类被重写方法的形参列表) -> {
被重写方法的方法体代码。
}

-> 就是一个新语法,没有实际含义,但是不能省略!

Lambda表达式的使用前提:
(1)Lambda表达式并不能简化所有匿名内部类的写法。
(2)Lambda表达式只能简化接口中只有一个抽象方法的匿名内部类形式。

Lambda表达式只能简化函数式接口的匿名内部类写法:
a.首先必须是接口。
b.接口中只能有一个抽象方法。
小结:
Lambda表达式只能简化接口中只有一个抽象方法的匿名内部类写法。
接口中只有一个抽象方法的接口称为函数式接口。
Lambda只能简化函数式接口的匿名内部类写法。
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
Lambda表达式简化Runnable接口的匿名内部类写法

@FunctionalInterface函数式接口注解:
一旦某个接口加上了这个注解,这个接口只能有且仅有一个抽象方法。
这个接口就可以被Lambda表达式简化。

public class LambdaDemo {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":执行~~~");
}
});
t.start();

Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+":执行~~~");
});
t1.start();

new Thread(() -> {
System.out.println(Thread.currentThread().getName()+":执行~~~");
}).start();

new Thread(() -> System.out.println(Thread.currentThread().getName()+":执行~~~")).start();
}
}
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
Lambda简化Comparator接口匿名内部类写法

public class CollectionsDemo02 {
public static void main(String[] args) {
List<Student> lists = new ArrayList<>();
Student s1 = new Student("李铭",18,'男');
Student s2 = new Student("冯龙",23,'男');
Student s3 = new Student("王乐乐",21,'男');
Collections.addAll(lists , s1 , s2 , s3);

// 按照年龄进行升序排序!
Collections.sort(lists, new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
return s1.getAge() - s2.getAge();
}
});

// 简化写法
Collections.sort(lists ,(Student t1, Student t2) -> {
return t1.getAge() - t2.getAge();
});

Collections.sort(lists ,(Student t1, Student t2) -> t1.getAge() - t2.getAge());

// 参数类型可以省略
Collections.sort(lists ,( t1, t2) -> t1.getAge() - t2.getAge());

System.out.println(lists);


}
}

Lambda表达式的省略写法

1
2
3
4
5
1)如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写,同时要省略分号!
(2)如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写。
此时,如果这行代码是return语句,必须省略return不写,同时也必须省略";"不写
(3)参数类型可以省略不写。
(4)如果只有一个参数,参数类型可以省略,同时()也可以省略。
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
public class LambdaDemo01 {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("胡伟光");
names.add("甘挺");
names.add("洪磊");

names.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});

names.forEach((String s) -> {
System.out.println(s);
});

names.forEach((s) -> {
System.out.println(s);
});

names.forEach(s -> {
System.out.println(s);
});

names.forEach(s -> System.out.println(s) );

names.forEach(System.out::println);


}
}

方法引用的概述。(了解)

1
2
3
4
5
6
7
8
方法引用:
方法引用是为了进一步简化Lambda表达式的写法。
方法引用的格式:类型或者对象::引用的方法。
关键语法是:“::”

小结:
方法引用可以进一步简化Lambda表达式的写法。
关键语法是:“::”

方法引用有四种形式

1
2
3
4
1.静态方法的引用。
2.实例方法的引用。
3.特定类型方法的引用。
4.构造器引用。

静态方法的引用

1
2
3
4
5
6
7
8
9
10
1.静态方法的引用。
引用格式:
类名::静态方法。
简化步骤:
a.定义一个静态方法,把需要简化的代码放到一个静态方法中去。
静态方法引用的注意事项
” 重要:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致。“
小结:
静态方法引用的格式: 类名::静态方法。
重要:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致,才可以引用简化!

实例方法的引用

1
2
3
4
5
6
2.实例方法的引用
格式: 对象::实例方法。
简化步骤:
a.定义一个实例方法,把需要的代码放到实例方法中去。
实例方法引用的注意事项
” 重要:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致。“

特定类型方法的引用

1
2
3
4
5
6
3.特定类型方法的引用。
特定类型:String ,任何类型。
格式:特定类型::方法。
注意:
如果第一个参数列表中的形参中的第一个参数作为了后面的方法的调用者,
并且其余参数作为后面方法的形参,那么就可以用特定类型方法引用了。

构造器引用

1
2
3
4
5
6
7
4.构造器引用。
格式是:类名::new。
注意点:前后参数一致的情况下,又在创建对象就可以使用构造器引用
s -> new Student(s) => Student::new

小结:
方法引用是可遇不可求,能用则用,不能用就不要用!

Stream流的强大

1
2
3
4
5
6
7
8
9
10
11
什么是Stream流?
在Java 8中,得益于Lambda所带来的函数式编程,
引入了一个全新的Stream流概念 ,用于解决已有集合/数组类库有的弊端。

Stream流能解决什么问题?
可以解决已有集合类库或者数组API的弊端。
Stream认为集合和数组操作的API很不好用,所以采用了Stream流简化集合和数组的操作!!

小结:
Stream流是用来简化集合类库或者数组API的弊端。
Stream流其实就一根传送带,元素在上面可以被Stream流操作。

Stream流的获取

1
2
3
4
5
6
7
8
9
10
11
Stream流式思想的核心:
是先得到集合或者数组的Stream流(就是一根传送带)
然后就用这个Stream流操作集合或者数组的元素。
然后用Stream流简化替代集合操作的API.

集合获取流的API:
(1) default Stream<E> stream();

小结:
集合获取Stream流用: stream();
数组:Arrays.stream(数组) / Stream.of(数组);

Stream流的常用API

1
2
3
4
5
6
7
8
9
forEach : 逐一处理(遍历)
count:统计个数
-- long count();
filter : 过滤元素
-- Stream<T> filter(Predicate<? super T> predicate)
limit : 取前几个元素
skip : 跳过前几个
map : 加工方法
concat : 合并流。
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
public class StreamDemo01 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
list.add("张三丰");

list.stream().filter( s -> s.length() == 3 ).filter( s -> s.startsWith("张"))
.forEach( System.out::println);
// 统计数量
long count = list.stream().filter( s -> s.length() == 3 )
.filter( s -> s.startsWith("张")).count();
System.out.println(count);
// 取前2个
list.stream().filter(s -> s.length() == 3).limit(2)
.forEach( System.out::println);
// 跳过前2个
list.stream().filter(s -> s.length() == 3).skip(2)
.forEach( s-> System.out.println(s));

// 需求:把名称都加上“黑马的:+xxx”
list.stream().map(a -> "黑马的:"+a).forEach(System.out::println);

// 需求:把名称都加工厂学生对象放上去!!
// list.stream().map(name -> new Student(name)).forEach(System.out::println);

list.stream().map(Student::new).forEach(System.out::println);

// 数组流
Stream<Integer> s1 = Stream.of(10, 20 ,30 ,40);
// 集合流
Stream<String> s2 = list.stream();
// 合并流
Stream<Object> s3 = Stream.concat(s1,s2);
s3.forEach(System.out::println);

}
}

终结与非终结方法

1
2
3
4
5
6
终结方法:一旦Stream调用了终结方法,流的操作就全部终结了,不能继续使用,
只能创建新的Stream操作。
终结方法: foreach , count。

非终结方法:每次调用完成以后返回一个新的流对象,
可以继续使用,支持链式编程!

收集Stream流:把Stream流的数据转回成集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
引入:
Stream的作用是:把集合转换成一根传送带,借用Stream流的强大功能进行的操作。
但是实际开发中数据最终的形式还是应该是集合,最终Stream流操作完毕以后还是要转换成集合。
这就是收集Stream流。

收集Stream流的含义:就是把Stream流的数据转回到集合中去。

Stream流:手段。
集合:才是目的。

小结:
收集Stream流的含义:就是把Stream流的数据转回到集合中去。

熟练函数式编程,如Lambda表达式简化函数式接口的匿名内部类形式,
以及四种方法引用简化Lambda表达式,以及流式编程思想如Stream简化集合或者数组的操作。
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
public class StreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
list.add("张三丰");

Stream<String> zhangLists = list.stream().filter(s -> s.startsWith("张"));
// 把stream流转换成Set集合。
Set<String> sets = zhangLists.collect(Collectors.toSet());
System.out.println(sets);

// 把stream流转换成List集合。
Stream<String> zhangLists1 = list.stream().filter(s -> s.startsWith("张"));
List<String> lists= zhangLists1.collect(Collectors.toList());
System.out.println(lists);

// 把stream流转换成数组。
Stream<String> zhangLists2 = list.stream().filter(s -> s.startsWith("张"));
Object[] arrs = zhangLists2.toArray();
// 可以借用构造器引用申明转换成的数组类型!!!
String[] arrs1 = zhangLists2.toArray(String[]::new);
}
}

反射的概念

反射,注解,代理,泛型是Java的高级技术,是以后框架的底层原理必须使用到的技术。

反射:是Java独有的技术。是Java技术显著的特点。

反射是指对于任何一个类,在"运行的时候"都可以直接得到这个类全部成分。
    在运行时,可以直接得到这个类的构造器对象。(Constructor)
    在运行时,可以直接得到这个类的成员变量对象。(Field)
    在运行时,可以直接得到这个类的成员方法对象。(Method)

反射的核心思想和关键就是得到:编译以后的class文件对象。

反射提供了一个Class类型,就是可以得到编译以后的class类对象。
    HelloWorld.java -> javac -> HelloWorld.class

    Class c = HelloWorld.class;


注意:反射是工作在运行时的技术,因为只有运行之后才会有class类对象。

反射的核心思想和关键就是得到:编译以后的class文件对象。
反射是在运行时获取类的字节码文件对象:然后可以解析类中的全部成分。

获取Class类对象

反射技术的第一步永远是先得到Class类对象:有三种方式获取
(1) 类名.class
(2) 通过类的对象.getClass()方法
(3) Class.forName(“类的全限名”)
– public static Class<?> forName(String className)

Class类下的方法:
     String getSimpleName(); 获得类名字符串:类名
     String getName();  获得类全名:包名+类名
     T newInstance() ;  创建Class对象关联类的对象,其实底层也是调用无参数构造器,已经被淘汰。
小结:
    Class类对象的获取有三种方式:
        1.类名.class。
        2.通过类的对象.getClass()方法。
        3.Class.forName("类的全限名")。
    Class类的方法:
        String getSimpleName(); 获得类名字符串:类名
        String getName();  获得类全名:包名+类名

获取Constructor构造器对象

射中Class类型获取构造器提供了很多的API:
1. Constructor getConstructor(Class… parameterTypes)
根据参数匹配获取某个构造器,只能拿public修饰的构造器,几乎不用!
2. Constructor getDeclaredConstructor(Class… parameterTypes)
根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符,建议使用!
3. Constructor[] getConstructors()
获取所有的构造器,只能拿public修饰的构造器。几乎不用!!太弱了!
4. Constructor[] getDeclaredConstructors()
获取所有申明的构造器,只要你写我就能拿到,无所谓权限。建议使用!!
小结:
获取类的全部构造器对象: Constructor[] getDeclaredConstructors()
– 获取所有申明的构造器,只要你写我就能拿到,无所谓权限。建议使用!!
获取类的某个构造器对象:Constructor getDeclaredConstructor(Class… parameterTypes)
– 根据参数匹配获取某个构造器,只要申明就可以定位,不关心权限修饰符,建议使用!

通过这个构造器初始化对象

反射获取Class中的构造器对象Constructor作用:
也是初始化并得到类的一个对象返回。

Constructor的API:
     1. T newInstance(Object... initargs)
            创建对象,注入构造器需要的数据。
     2. void setAccessible(true)
            修改访问权限,true代表暴力攻破权限,false表示保留不可访问权限(暴力反射)
小结:
    可以通过定位类的构造器对象。
    如果构造器对象没有访问权限可以通过:void setAccessible(true)打开权限
    构造器可以通过T newInstance(Object... initargs)调用自己,传入参数!

取Field成员变量对象

反射的第一步是先得到Class类对象。

 1、Field getField(String name);
        根据成员变量名获得对应Field对象,只能获得public修饰
 2.Field getDeclaredField(String name);
        根据成员变量名获得对应Field对象,只要申明了就可以得到
 3.Field[] getFields();
        获得所有的成员变量对应的Field对象,只能获得public的
 4.Field[] getDeclaredFields();
        获得所有的成员变量对应的Field对象,只要申明了就可以得到
 小结:
    获取全部成员变量:getDeclaredFields
    获取某个成员变量:getDeclaredField

获取成员变量: 取值和赋值

Field的方法:给成员变量赋值和取值
void set(Object obj, Object value):给对象注入某个成员变量数据
Object get(Object obj):获取对象的成员变量的值。
void setAccessible(true);暴力反射,设置为可以直接访问私有类型的属性。
Class getType(); 获取属性的类型,返回Class对象。
String getName(); 获取属性的名称。

获取Method方法对象

反射获取类的Method方法对象:
1、Method getMethod(String name,Class…args);
根据方法名和参数类型获得对应的方法对象,只能获得public的
2、Method getDeclaredMethod(String name,Class…args);
根据方法名和参数类型获得对应的方法对象,包括private的
3、Method[] getMethods();
获得类中的所有成员方法对象,返回数组,只能获得public修饰的且包含父类的
4、Method[] getDeclaredMethods();
获得类中的所有成员方法对象,返回数组,只获得本类申明的方法。
Method的方法执行:
Object invoke(Object obj, Object… args)
参数一:触发的是哪个对象的方法执行。
参数二: args:调用方法时传递的实际参数

拓展

1.反射可以破坏面向对象的封装性(暴力反射)。
2.同时可以破坏泛型的约束性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws Exception {
// 泛型只能工作在编译阶段,运行阶段泛型就消失了,
// 反射工作在运行时阶段。
List<Double> scores = new ArrayList<>();
scores.add(99.3);
scores.add(199.3);
scores.add(89.5);

// 拓展:通过反射暴力的注入一个其他类型的数据进去。
// a.先得到集合对象的Class文件对象
Class c = scores.getClass();
// b.从ArrayList的Class对象中定位add方法
Method add = c.getDeclaredMethod("add", Object.class);
// c.触发scores集合对象中的add执行(运行阶段,泛型不能约束了)
add.invoke(scores,"波仔");

System.out.println(scores);


}