Kubernetes容器网络

Kubernetes容器网络

在kubernetes中要保证容器之间网络互通,网络至关重要.而kubernetes本身并没有具体实现网络,而是通过插件化的方式自由接入。在容器网络接入进来需要满足基本原则:

  • pod无论运行在任何节点都可以互相直接通信,而不需要借助NAT地址转换实现。
  • node与pod可以互相通信,在不限制的前提下,pod可以访问任意网络。
  • pod拥有独立的网络栈,pod看到自己的地址和外部看见的地址应该是一样的,并且同个pod内所有的容器共享同个网络栈。

容器网络基础

一个Linux容器的网络栈是被隔离在它自己的Network Namespace中,Network Namespace包括了:网卡(Network Interface),回环设备(Lookback Device),路由表(Routing Table)和iptables规则,对于服务进程来讲这些就构建了它发起请求和相应的基本环境。而要实现一个容器网络,离不开以下linux网络功能:

  • 网络命名空间: 将独立的网络协议栈隔离到不同的命令空间中,彼此间无法通信。
  • Veth Pair: Veth设备对的引入是为了实现在不同网络命名空间的通信,总是以两张虚拟网卡(veth peer) 的形式成对出现的。并且,从其中一端发出的数据,总是能在另外一端收到。
  • Iptables/Netfilter: Netfilter负责在内核中执行各种定义的规则(过滤、修改、丢弃等),运行在内核中;Iptables模式是在用户模式下运行的进程,负责协助维护内核中Netfilter的各种规则表;通过二者的配合来实现整个Linux网络协议栈中灵活的数据包处理机制
  • 网桥: 网桥是一个二层网络虚拟设备,类似交换机,主要功能是通过学习而来的Mac地址将数据帧转发到网桥的不同端口上.

  • 路由: Linux系统包含一个完整的路由功能,当IP层在处理数据发送或转发的时候,会使用路由表来决定发往哪里。

基于以上的基础,同宿主机的容器时间如何通信呢?
我们可以简单把他们理解成两台主机,主机之间通过网线连接起来,如果要多台主机通信,我们通过交换机就可以实现彼此互通。在容器中,以上的实现是通过docker0网桥,凡是连接到docker0网桥的容器,就可以通过它来进行通信。要想容器能够连接到docker0网桥,我们只需要类似网线的虚拟设备Veth Pair来把容器连接到网桥上即可。

我们启动一个容器:

$ docker run -d --name c1  hub.pri.ibanyu.com/devops/alpine:v3.8 /bin/sh

然后查看网卡设备:

$ docker exec -it c1  /bin/sh
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:14 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1172 (1.1 KiB)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

/ # route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U     0      0        0 eth0

可以看到其中有一张eth0的网卡,它就是veth peer其中的一端的虚拟网卡。然后通过route -n 查看容器中的路由表,eth0也正是默认路由出口。所有对172.17.0.0/16网段的请求都会从eth0出去。
我们再来看Veth peer的另一端,我们查看宿主机的网络设备:

# ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::42:6aff:fe46:93d2  prefixlen 64  scopeid 0x20<link>
        ether 02:42:6a:46:93:d2  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 656 (656.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.100.0.2  netmask 255.255.255.0  broadcast 10.100.0.255
        inet6 fe80::5400:2ff:fea3:4b44  prefixlen 64  scopeid 0x20<link>
        ether 56:00:02:a3:4b:44  txqueuelen 1000  (Ethernet)
        RX packets 7788093  bytes 9899954680 (9.2 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5512037  bytes 9512685850 (8.8 GiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 32  bytes 2592 (2.5 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 32  bytes 2592 (2.5 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth20b3dac: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::30e2:9cff:fe45:329  prefixlen 64  scopeid 0x20<link>
        ether 32:e2:9c:45:03:29  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 656 (656.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

我们可以看到,容器对应的Veth peer 另一端是宿主机上的一块虚拟网卡叫veth20b3dac,并且可以通过brctl 查看网桥信息看到这张网卡是在docker0上。

# brctl show
docker0     8000.02426a4693d2   no      veth20b3dac

然后我们再启动一个容器,从第一个容器是否能ping通第二个容器.

$ docker run -d --name c2 hub.pri.ibanyu.com/devops/alpine:v3.8 /bin/sh
$ docker exec -it c1 /bin/sh
/ # ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.291 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.129 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.142 ms
64 bytes from 172.17.0.3: seq=3 ttl=64 time=0.169 ms
64 bytes from 172.17.0.3: seq=4 ttl=64 time=0.194 ms
^C
--- 172.17.0.3 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.129/0.185/0.291 ms

可以看到,能够ping通,其原理就是我们ping 目标IP172.17.0.3时,会匹配到我们的路由表第二条规则,网关为0.0.0.0,这就意味着是一条直连路由,通过二层转发到目的地。要通过二层网络到达172.17.0.3,我们需要知道它的Mac地址,此时就需要第一个容器发送一个ARP广播,来通过IP地址查找它的mac地址。此时Veth peer另外一段是docker0网桥,它会广播到所有连接它的veth peer 虚拟网卡去,然后正确的虚拟网卡收到后会响应这个ARP报文,然后网桥再回给第一个容器.
以上就是同宿主机不同容器通过docker0通信,如下图所示:

默认情况下,通过network namespace限制的容器进程,本质上是通过Veth peer设备和宿主机网桥的方式,实现了不同network namespace 的数据交换。

与之类似地,当你在一台宿主机上,访问该宿主机上的容器的 IP 地址时,这个请求的数据包,也是先根据路由规则到达 docker0 网桥,然后被转发到对应的 Veth Pair 设备,最后出现在容器里。

跨主机网络通信

在 Docker 的默认配置下,不同宿主机上的容器通过 IP 地址进行互相访问是根本做不到的。为了解决这个问题,社区中出现了很多网络方案。同时k8s为了更好的控制网络的接入,推出了CNI即容器网络的API接口。它是k8s中标准的一个调用网络实现的接口,kubelet通过这个API来调用不同的网络插件以实现不同的网络配置,实现了这个接口的就是CNI插件,它实现了一系列的CNI API接口。目前已经有的包括flannel、calico、weave、contiv等等。

实际上CNI的容器网络通信流程跟前面的基础网络一样,只是CNI维护了一个单独的网桥来代替 docker0。这个网桥的名字就叫作:CNI 网桥,它在宿主机上的设备名称默认是:cni0。cni的设计思想就是:Kubernetes 在启动 Infra 容器之后,就可以直接调用 CNI 网络插件,为这个 Infra 容器的 Network Namespace配置符合预期的网络栈。

CNI插件三种网络实现模式:

  • overlay 模式是基于隧道技术实现的,整个容器网络和主机网络独立,容器之间跨主机通信时将整个容器网络封装到底层网络中,然后到达目标机器后再解封装传递到目标容器。不依赖与底层网络的实现。实现的插件有flannel(UDP、vxlan)、calico(IPIP)等等

  • 三层路由模式中容器和主机也属于不通的网段,他们容器互通主要是基于路由表打通,无需在主机之间建立隧道封包。但是限制条件必须依赖大二层同个局域网内。实现的插件有flannel(host-gw)、calico(BGP)等等

  • underlay网络是底层网络,负责互联互通。 容器网络和主机网络依然分属不同的网段,但是彼此处于同一层网络,处于相同的地位。整个网络三层互通,没有大二层的限制,但是需要强依赖底层网络的实现支持.实现的插件有calico(BGP)等等

我们看下路由模式的一种实现flannel Host-gw:

如图可以看到当node1上container-1要发数据给node2上的container2时,会匹配到如下的路由表规则:

10.244.1.0/24 via 10.168.0.3 dev eth0

表示前往目标网段10.244.1.0/24的IP包,需要经过本机eth0出去发往的下一跳ip地址为10.168.0.3(node2).然后到达10.168.0.3以后再通过路由表转发到cni网桥,进而进入到container2。

以上可以看到host-gw工作原理,其实就是在每个node节点配置到每个pod网段的下一跳为pod网段所在的node节点IP,pod网段和node节点ip的映射关系,flannel保存在etcd或者k8s中。flannel只需要watch 这些数据的变化来动态更新路由表即可.

这种网络模式最大的好处就是避免了额外的封包和解包带来的网络性能损耗。缺点我们也能看见主要就是容器ip包通过下一跳出去时,必须要二层通信封装成数据帧发送到下一跳。如果不在同个二层局域网,那么就要交给三层网关,而此时网关是不知道目标容器网络的(也可以静态在每个网关配置pod网段路由)。所以flannel host-gw必须要求集群宿主机是二层互通的。

而为了解决二层互通的限制性,calico提供的网络方案就可以更好的实现,calico 大三层网络模式与flannel 提供的类似,也会在每台宿主机添加如下格式的路由规则:

<目标容器IP网段> via <网关的IP地址> dev eth0

其中网关的IP地址在不同场景有不同的意思,如果宿主机是二层可达那么就是目的容器所在的宿主机的IP地址,如果是三层不同局域网那么就是本机宿主机的网关IP(交换机或者路由器地址).

不同于flannel通过k8s或者etcd存储的数据来维护本机路由信息的做法,calico是通过BGP动态路由协议来分发整个集群路由信息。

BGP全称是 Border Gateway Protocol边界网关协议,linxu原生支持的、专门用于在大规模数据中心为不同的自治系统之间传递路由信息。只要记住BGP简单理解其实就是实现大规模网络中节点路由信息同步共享的一种协议.而BGP这种协议就能代替flannel 维护主机路由表功能。

calico 主要由以下几个部分组成:

  • calico cni插件: 主要负责与kubernetes对接,供kubelet调用使用。
  • felix: 负责维护宿主机上的路由规则、FIB转发信息库等.
  • BIRD: 负责分发路由规则,类似路由器.
  • confd: 配置管理组件。

除此之外,calico还和flannel host-gw不同之处在于,它不会创建网桥设备,而是通过路由表来维护每个pod的通信,如下图所示:

可以看到calico 的cni插件会为每个容器设置一个veth pair设备,然后把另一端接入到宿主机网络空间,由于没有网桥,cni插件还需要在宿主机上为每个容器的veth pair设备配置一条路由规则,用于接收传入的IP包,路由规则如下:

10.92.77.163 dev cali93a8a799fe1 scope link

以上表示发送10.92.77.163的IP包应该发给cali93a8a799fe1设备,然后到达另外一段容器中。

有了这样的veth pair设备以后,容器发出的IP包就会通过veth pair设备到达宿主机,然后宿主机根据路由规则的下一条地址,发送给正确的网关(10.100.1.3),然后到达目标宿主机,在到达目标容器.

10.92.77.0/24 via 10.100.1.3 dev bond0 proto bird

这些路由规则都是felix维护配置的,而路由信息则是calico bird组件基于BGP分发而来。calico实际上是将集群里所有的节点都当做边界路由器来处理,他们一起组成了一个全互联的网络,彼此之间通过BGP交换路由,这些节点我们叫做BGP Peer。

需要注意的是calico 维护网络的默认模式是 node-to-node mesh ,这种模式下,每台宿主机的BGP client都会跟集群所有的节点BGP client进行通信交换路由。这样一来,随着节点规模数量N的增加,连接会以N的2次方增长,会集群网络本身带来巨大压力。
所以一般这种模式推荐的集群规模在50节点左右,超过50节点推荐使用另外一种RR(Router Reflector)模式,这种模式下,calico 可以指定几个节点作为RR,他们负责跟所有节点BGP client建立通信来学习集群所有的路由,其他节点只需要跟RR节点交换路由即可。这样大大降低了连接数量,同时为了集群网络稳定性,建议RR>=2.

以上的工作原理依然是在二层通信,当我们有两台宿主机,一台是10.100.0.2/24,节点上容器网络是10.92.204.0/24;另外一台是10.100.1.2/24,节点上容器网络是10.92.203.0/24,此时两台机器因为不在同个二层所以需要三层路由通信,这时calico就会在节点上生成如下路由表:

10.92.203.0/24 via 10.100.1.2 dev eth0 proto bird

这时候问题就来,因为10.100.1.2跟我们10.100.0.2不在同个子网,是不能二层通信的。这之后就需要使用Calico IPIP模式,当宿主机不在同个二层网络时就是用overlay网络封装以后再发出去。如下图所示:

IPIP模式下在非二层通信时,calico 会在node节点添加如下路由规则:

10.92.203.0/24 via 10.100.1.2 dev tunnel0

可以看到尽管下一条任然是node的IP地址,但是出口设备却是tunnel0,其是一个IP隧道设备,主要有Linux内核的IPIP驱动实现。会将容器的ip包直接封装宿主机网络的IP包中,这样到达node2以后再经过IPIP驱动拆包拿到原始容器IP包,然后通过路由规则发送给veth pair设备到达目标容器。

以上尽管可以解决非二层网络通信,但是仍然会因为封包和解包导致性能下降。如果calico 能够让宿主机之间的router设备也学习到容器路由规则,这样就可以直接三层通信了。比如在路由器添加如下的路由表:

10.92.203.0/24 via 10.100.1.2 dev interface1

而node1添加如下的路由表:

10.92.203.0/24 via 10.100.1.1 dev tunnel0

那么node1上的容器发出的IP包,基于本地路由表发送给10.100.1.1网关路由器,然后路由器收到IP包查看目的IP,通过本地路由表找到下一跳地址发送到node2,最终到达目的容器。这种方案,我们是可以基于underlay 网络来实现,只要底层支持BGP网络,可以和我们RR节点建立EBGP关系来交换集群内的路由信息。

以上就是kubernetes 常用的几种网络方案了,在公有云场景下一般用云厂商提供的cni plugin即可,大多数情况都是大二层子网内可以使用flannel host-gw这种更简单的方案,对于三层可以使用flannel vxlan 或者calico ipip,而私有物理机房环境中,Calico bgp项目更加适合。根据自己的实际场景,再选择合适的网络方案。

参考

  • https://github.com/coreos/flannel/blob/master/Documentation/backends.md
  • https://coreos.com/flannel/
  • https://docs.projectcalico.org/getting-started/kubernetes/
  • https://www.kancloud.cn/willseecloud/kubernetes-handbook/1321338

Android APP集成FCM介绍

1. 集成需求介绍

安卓系统的APP,如果涉及推送系统,一般会基于Socket在后台维持一个与APP服务器通信的长连接。但是这个长连接不稳定,当APP退到后台运行以后,系统会关闭APP并收回分配的资源,Socket长连接会被断掉,从而使APP失去与服务器的通信联系。

APP与服务器的通信断开之后,APP就不再能收到服务器发出的通知,导致用户不能及时收到相应的通知而错失及时处理问题的机会。

类似苹果推送,在安卓手系统上,谷歌提供推送服务FCM(Firebase Cloud Messaging)。FCM的运行依赖谷歌提供的Google Play Services服务,但是因为网络问题谷歌的服务无法在国内使用,中国大陆手机厂商都会把安卓手机上的谷歌相关的服务移除,结果在大陆地区就没有类似苹果设备统一的推送服务可用。

大陆地区有很多第三方的推送服务商,提供的服务大致分为两种,一种是手机厂商自己提供的推送服务,例如华为和小米;另一种是非手机厂商提供的消息推送服务,例如百度推送、极光推送、腾讯信鸽等,比较杂乱,效果也众说纷纭。

大陆地区有一个叫做统一推送联盟的组织,2017年由中国信息通信研究院泰尔终端实验室发起(包含:华为、小米、vivo、OPPO、三星等手机厂商;百度、阿里、腾讯、奇虎科技等互联网企业;个推、极光等第三方推送商),意在构建一个国内可用的,统一的推送服务入口,逐步解决国内安卓推送碎片化的局面。效果如何,需要持续关注。

2. FCM原理介绍

FCM集成的实现包括用于发送和接收的两个主要组件:

  • 一个受信任的环境,例如向用户APP发送消息的后台服务;
  • 一个接收消息的客户端应用;

FCM提供了连接两个组件的服务:

  • FCM的消息服务,连接用户消息服务器和用户设备。提供了HTTP和XMPP API供用户服务器管理和发送消息,还提供了Admin SDK支持开发一个基于移动设备的消息管理程序;

  • Google Play services,用于接收FCM消息服务发来的消息,分发到客户端SDK进行处理;

运行集成FCM的客户端的移动设备上需要运行安卓4.0及以上版本系统,并安装运行Google Play services服务15.0.0或者更高版本。Google Play services服务在终端设备上是常驻的,开启后不会被系统自动关闭,这样就能保证客户端在有网络连接的情况下,随时收到来自FCM消息服务器的通知和消息。如果移动终端没有安装Google Play services或者因为网络原因无法连接到谷歌的FCM消息服务器,就不能正常地接收到消息。

FCM的主要功能 功能描述
发送通知消息或数据消息 发送向用户显示的通知消息,或者发送数据消息并完全确定应用代码中会发生的情况
通用消息定位 使用以下三种方式中的任意一种将消息分发到客户端应用:分发至单一设备、分发至群组设备、分发至订阅特定主题的设备。
从客户应用发送消息 通过FCM可靠而省电的连接通道,将确认消息、聊天消息及其他消息从设备发回至服务器

FCM可以发送通知类消息和数据消息两种,这两种消息的有效负载上线均为4KB,数据中的Token是指后面要说到的注册令牌。

  • 通知类消息负载
{
  "message":{
    "token":"...",
    "notification":{
      "title":"Portugal vs. Denmark",
      "body":"great match!"
    }
  }
}
  • 数据类消息负载
{
  "message":{
    "token":"...",
    "data":{
      "Nick" : "Mario",
      "body" : "great match!",
      "Room" : "PortugalVSDenmark"
    }
  }
}

3. FCM和APP内推送的协调

应用在后台运行时,通知类消息负载会被传递到通知面板;应用在前台运行时,通知类型的消息负载不会被传递到通知面板,APP可以在FirebaseMessagingService中定义的回调函数onMessageReceived中处理接收到的消息负载,包括通知类型负载和数据类型的负载;数据类型的消息负载不论在前后台运行,都可以在这里接收处理。

如果希望对FCM展示的通知有点击效果,例如点击某个通知后可以调起某个APP内的界面或者功能,可以给消息同时加上通知类型负载和数据类型负载,通知类型负载会被显示在通知面板中,数据类型的负载可以在点击事件后,在启动器Intent的extras中获取处理。

集成FCM就要在APP内的推送和FCM之间就需要做一个协调,避免出现一个用户通知在移动设备上被展示两遍的现象,一遍来做FCM,一遍来自APP内推送通道。

建议的方式是,只对通知面板的显示做协调处理,对APP内功能性的弹出式提醒或或静默式的触发操作依旧只在APP内推送通道处理,既FCM只负责传递需要在通知面板展示的通知类消息推送。

APP启动设置默认关闭FCM通道,在Application初始化过程中检查Google Play Services的可用性,一旦确定打开FCM就关闭APP内推送通道的Notification功能。

4. 集成流程介绍

  • 第一步,要到Firebase控制台申请创建APP,按照下面的的步骤,可以得到一个名为google-services.json的配置文件,把这个文件下载并放在工程内app/目录下。




  • 第二步,配置Google Services插件,向根级build.gradle文件中添加规则,以引入google-services插件和Google的maven仓库:

buildscript {
    // ...
    dependencies {
        // ...
        classpath 'com.google.gms:google-services:4.0.1' // google-services plugin
    }
}
allprojects {
    // ...
    repositories {
        // ...
        maven {
            url "https://maven.google.com" // Google's Maven repository
        }
    }
}
  • 第三步,在app/build.gradle的底部添加apply plugin代码,启用google-services插件,并添加FCM所需要的库

需要及时更新firebase-core和firebase-messaging,否则可能会收不到FCM服务器发来的消息。

apply plugin: 'com.android.application'
android {
  // ...
}
dependencies {
  // ...
  compile 'com.google.firebase:firebase-core:16.0.1'
  compile 'com.google.firebase:firebase-messaging:17.0.0'
}
// 虽然通常把apply放在文件顶部,但在文档中明确要求放在文件的底部,就放在底部
apply plugin: 'com.google.gms.google-services'
  • 第四步,在应用清单中设置通知面板图标和颜色并设置FCM默认不启动,ic_notification图片不宜过大,过大的图片在一些手机上显示不出来,建议100*100左右合适,不要留边框或者边缘透明区域;
<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_notification" />
<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/color_src" />
<meta-data android:name="firebase_messaging_auto_init_enabled"
    android:value="false" />
  • 第五步,APP初始化过程中检查Google Play Services的可用性,并决定是否开启FCM,如果开启则根据“协调”中的建议或者自选的逻辑关闭APP内推送通知的Notification功能。
// 示例程序,这个方法是写在Application类中的,所以这里的this指的是Application实例对象
private void checkGoogleService() {
    final int available = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this);
    switch (available) {
        case ConnectionResult.SUCCESS:
            FirebaseMessaging.getInstance().setAutoInitEnabled(true);
            // TODO:对APP推送通知的处理
            break;
        case ConnectionResult.SERVICE_MISSING:
            Log.d(TAG,"check google service service missing");
            break;
        case ConnectionResult.SERVICE_UPDATING:
            Log.d(TAG,"check google service service updating");
            break;
        case ConnectionResult.SERVICE_DISABLED:
            Log.d(TAG,"check google service service disabled");
            break;
        case ConnectionResult.SERVICE_INVALID:
            LogEx.d(TAG,"check google service service invalid");
            break;
        case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
            Log.d(TAG,"check google service version too old");
            ToastUtil.showLENGTH_LONG("Google play service need to update");
            break;
        default:
            Log.d(TAG,"check google service default: available = " + available);
            break;
    }
}
  • 第六步,注册令牌。FCM消息服务器是通过一个叫注册令牌的字符串来定位设备的,APP初次启动时,FCM SDK会为客户端应用生成一个注册令牌,客户端应用需要获取到这个注册令牌并发送到APP服务器并和当前APP用户关联保存,当APP服务器需要向指定用户发送通知消息,需要检索到与用户关联保存的注册令牌,通过这个注册令牌向FCM消息服务器发送通知消息,FCM消息服务器通过这个注册令牌,定位移动设备,并发送通知消息。

注册令牌在以下几种情况会有变更:

    1. 通过FirebaseInstanceId.getInstance().deleteToken()主动删除注册令牌
    1. 用户卸载重装APP
    1. 用户清除应用数据
    1. 应用在新设备上恢复

以上可知,注册令牌跟客户端APP具体的登录用户是没有关联的。所以,在APP启动完成后要确认获取注册令牌并与用户关联保存到APP服务器,在用户退出客户端APP时,要通知APP服务器把关联保存的注册令牌删除,避免服务器错误地把已经退出的用户的通知消息,发送到无效的设备上。

监听注册令牌的生成,需要继承FirebaseInstanceIdService并在onTokenRefresh回调中使用FirebaseInstanceId.getInstance().getToken()方法获取并在本地持久化保存。

<service android:name="com.fcm.MyFireBaseInstanceIDService">
    <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
    </intent-filter>
</service>
package com.fcm;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.FirebaseInstanceIdService;
public class MyFireBaseInstanceIDService extends FirebaseInstanceIdService {
    @Override
    public void onTokenRefresh() {
        String token = FirebaseInstanceId.getInstance().getToken();
        // TODO: 本地持久化保存获取到的注册令牌
    }
}
  • 第七步,如果按照”协调”中的建议,FCM只处理通知类型的消息,这一步不需要。如果希望对FCM消息做更多的处理,需要继承PalFishFireBaseMessagingService并在onMessageReceived回调方法中拆解消息内容,根据APP内部协议进行相应的处理。
<service android:name="com.fcm.MyFireBaseMessagingService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
package com.fcm;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
public class MyFireBaseMessagingService extends FirebaseMessagingService {
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // TODO: 处理接收到的FCM消息
    }
    // 在某些情况下,FCM 可能不会传递消息。如果在特定设备连接 FCM 时,您的应用在该设备上的待处理消息过多(超过 100 条),
    // 或者如果设备超过一个月未连接到 FCM,就会发生这种情况。在这些情况下,您可能会收到对 FirebaseMessagingService.onDeletedMessages() 的回调。
    // 当应用实例收到此回调时,应会执行一次与您的应用服务器的完全同步。如果您在过去 4 周内未向该设备上的应用发送消息,FCM 将不会调用 onDeletedMessages()。
    @Override
    public void onDeletedMessages() {
    }
}

5. 集成过程中的问题

    1. 在app/build.gradle中引入firebase-core和firebase-messaging库时,遇到firebase-messaging版本不匹配,导致开启FCM功能FirebaseMessaging.getInstance().setAutoInitEnabled(true)时,出现问题:No virtual method zzcz(Z)V in class com.google.firebase.iid.FirebaseInstanceId,解决办法就是调整firebase-core和firebase-messaging的版本,两个库的版本可以在maven仓库中搜索。
    1. 检查Google Play Services可用性的方法GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)并不总是可靠的,例如中国大陆运行的手机,如果安装了Google Play Services服务,返回值也会是true,对于从中国大陆以外进入大陆地区的用户,会因为网络不通无法接收FCM消息,而启动时又依据这个判定打开FCM并关闭APP内推动通知,导致用户在一段时间内收不到任何通知。建议在APP设置中给一个FCM的开关,如果遇到这种情况,关闭FCM开关之后不做可用性检查。