精华内容
下载资源
问答
  • 微服务笔记

    千次阅读 2019-08-20 15:32:29
    微服务笔记 参考资料: 漫画:什么是微服务?...所有的功能打包在个 WAR包里,基本没有外部依赖(除了容器),部署在个JEE容器(Tomcat,JBoss,WebLogic)里,包含了 DO/DAO,Service,UI等所有逻辑。...

    微服务笔记

    参考资料:

    微服务简介

    单体架构(Monolithic)的痛点

    传统的MVC架构,所有业务子模块都集成在一个很重的JVM进程当中。

    所有的功能打包在一个 WAR包里,基本没有外部依赖(除了容器),部署在一个JEE容器(Tomcat,JBoss,WebLogic)里,包含了 DO/DAO,Service,UI等所有逻辑。

    优点:

    1. 开发简单,集中式管理,所有代码都在同一个项目当中

    2. 基本不会重复开发

    3. 功能都在本地,没有分布式的管理和调用消耗

    缺点:

    1. 开发效率低:开发都在同一个项目改代码,相互等待,冲突不断

    2. 代码维护难:代码功功能耦合在一起,新人不知道何从下手

    3. 部署不灵活:构建时间长,任何小修改都要重构整个项目,耗时

    4. 稳定性差:一个微小的问题,都可能导致整个应用挂掉

    5. 扩展性不够:无法满足高并发下的业务需求

    6. 资源无法隔离:整个单体系统的各个功能模块都依赖于同样的数据库、内存等资源,一旦某个功能模块对资源使用不当,整个系统都会被拖垮。

    7. 无法灵活扩展:当系统的访问量越来越大的时候,单体系统固然可以进行水平扩展,部署在多台机器上组成集群。但是这种扩展并非灵活的扩展。比如希望只针对某个功能模块做水平扩展,这一点在单体系统是做不到的。

    什么是微服务(Microservice Architecture)?

    微服务架构风格是一种将单个应用程序作为一套小型服务开发的方法,每种应用程序都在自己的进程中运行,并与轻量级机制(通常是HTTP资源API)进行通信。 这些服务是围绕业务功能构建的,可以通过全自动部署机制独立部署。 这些服务的集中管理最少,可以用不同的编程语言编写,并使用不同的数据存储技术。

    基于微服务架构的设计目的:有效的拆分应用,实现敏捷开发和部署。

    常见的系统架构遵循的三个标准和业务驱动力:

    1. 提高敏捷性:及时响应业务需求,促进企业发展

    2. 提升用户体验:提升用户体验,减少用户流失

    3. 降低成本:降低增加产品、客户或业务方案的成本

    微服务的特点

    1. 独立部署,灵活扩展。 传统的单体架构是以整个系统为单位进行部署,而微服务则是以每一个独立组件(例如用户服务,商品服务)为单位进行部署。

    用一张经典的图来表现,就是下面这个样子:

    图中左边是单体架构的集群,右边是微服务集群。

    什么意思呢?比如根据每个服务的吞吐量不同,支付服务需要部署20台机器,用户服务需要部署30台机器,而商品服务只需要部署10台机器。这种灵活部署只有微服务架构才能实现。

    而近几年流行的Docker,为微服务架构提供了有效的容器。

    2. 资源的有效隔离。 微服务设计的原则之一,就是每一个微服务拥有独立的数据源,假如微服务A想要读写微服务B的数据库,只能调用微服务B对外暴露的接口来完成。这样有效避免了服务之间争用数据库和缓存资源所带来的问题。

    同时,由于每一个微服务实例在Docker容器上运行,实现了服务器资源(内存、CPU资源等)的有效隔离。

    3. 团队组织架构的调整。 微服务设计的思想也改变了原有的企业研发团队组织架构。传统的研发组织架构是水平架构,前端有前端的团队,后端有后端的团队,DBA有DBA的团队,测试有测试的团队。

    而微服务的设计思想对团队的划分有着一定的影响,使得团队组织架构的划分更倾向于垂直架构,比如用户业务是一个团队来负责,支付业务是一个团队来负责。

    当然,这种垂直划分只是一个理想的架构,实际在企业中并不会把团队组织架构拆分得这么绝对

    微服务架构的不足

    微服务把原有的项目拆成多个独立工程,增加了开发和测试的复杂度。

    微服务架构需要保证不同服务之间的数据一致性,引入了分布式事务和异步补偿机制,为设计和开发带来一定挑战。

    微服务与面向服务架构SOA的区别

    SOA架构是一种粗粒度、松耦合的服务架构,其更多的是强调异构系统之间的服务通信。

    SOA是什么样子呢?可以是下面这样的Web Service:

    也可以是下面这样的ESB企业服务总线:

    总之,SOA架构强调的是异构系统之间的通信和解耦合,而微服务架构强调的是系统按业务边界做细粒度的拆分和部署。

    微服务设计原则

    1. 单一职责原则

      意思是每个微服务只需要实现自己的业务逻辑就可以了,比如订单管理模块,它只需要处理订单的业务逻辑就可以了,其它的不必考虑。

    2. 服务自治原则

      意思是每个微服务从开发、测试、运维等都是独立的,包括存储的数据库也都是独立的,自己就有一套完整的流程,我们完全可以把它当成一个项目来对待。不必依赖于其它模块。

    3. 轻量级通信原则

      首先是通信的语言非常的轻量,第二,该通信方式需要是跨语言、跨平台的,之所以要跨平台、跨语言就是为了让每个微服务都有足够的独立性,可以不受技术的钳制。

    4. 接口明确原则

      由于微服务之间可能存在着调用关系,为了尽量避免以后由于某个微服务的接口变化而导致其它微服务都做调整,在设计之初就要考虑到所有情况,让接口尽量做的更通用,更灵活,从而尽量避免其它模块也做调整。

    微服务实践先知

    1. 客户端如何访问这些服务?(API Gateway)

    传统的开发方式,所有的服务都是本地的,UI可以直接调用,现在按功能拆分成独立的服务,跑在独立的一般都在独立的虚拟机上的 Java进程了。客户端UI如何访问他的?后台有N个服务,前台就需要记住管理N个服务,一个服务下线/更新/升级,前台就要重新部署,这明显不服务我们 拆分的理念,特别当前台是移动应用的时候,通常业务变化的节奏更快。另外,N个小服务的调用也是一个不小的网络开销。还有一般微服务在系统内部,通常是无状态的,用户登录信息和权限管理最好有一个统一的地方维护管理(OAuth)。

    所以,一般在后台N个服务和UI之间一般会一个代理或者叫API Gateway,他的作用包括

    • 提供统一服务入口,让微服务对前台透明

    • 聚合后台的服务,节省流量,提升性能

    • 提供安全,过滤,流控等API管理功能

    2. 服务之间如何通信?(服务调用)

    因为所有的微服务都是独立的Java进程跑在独立的虚拟机上,所以服务间的通行就是IPC(inter process communication),已经有很多成熟的方案。现在基本最通用的有两种方式。这几种方式,展开来讲都可以写本书,而且大家一般都比较熟悉细节了, 就不展开讲了。

    • REST(JAX-RS,Spring Boot)

    • RPC(Thrift, Dubbo)

    • 异步消息调用(Kafka, Notify)

    一般同步调用比较简单,一致性强,但是容易出调用问题,性能体验上也会差些,特别是调用层次多的时候。RESTful和RPC的比较也是一个很有意 思的话题。一般REST基于HTTP,更容易实现,更容易被接受,服务端实现技术也更灵活些,各个语言都能支持,同时能跨客户端,对客户端没有特殊的要 求,只要封装了HTTP的SDK就能调用,所以相对使用的广一些。RPC也有自己的优点,传输协议更高效,安全更可控,特别在一个公司内部,如果有统一个的开发规范和统一的服务框架时,他的开发效率优势更明显些。就看各自的技术积累实际条件,自己的选择了。

    异步消息的方式在分布式系统中有特别广泛的应用,他既能减低调用服务之间的耦合,又能成为调用之间的缓冲,确保消息积压不会冲垮被调用方,同时能 保证调用方的服务体验,继续干自己该干的活,不至于被后台性能拖慢。不过需要付出的代价是一致性的减弱,需要接受数据最终一致性;还有就是后台服务一般要 实现幂等性,因为消息发送出于性能的考虑一般会有重复(保证消息的被收到且仅收到一次对性能是很大的考验);最后就是必须引入一个独立的broker,如 果公司内部没有技术积累,对broker分布式管理也是一个很大的挑战。

    3. 这么多服务怎么查找?(服务发现)

    在微服务架构中,一般每一个服务都是有多个拷贝,来做负载均衡。一个服务随时可能下线,也可能应对临时访问压力增加新的服务节点。服务之间如何相互 感知?服务如何管理?这就是服务发现的问题了。一般有两类做法,也各有优缺点。基本都是通过zookeeper等类似技术做服务注册信息的分布式管理。当 服务上线时,服务提供者将自己的服务信息注册到ZK(或类似框架),并通过心跳维持长链接,实时更新链接信息。服务调用者通过ZK寻址,根据可定制算法,找到一个服务,还可以将服务信息缓存在本地以提高性能。当服务下线时,ZK会发通知给服务客户端。

    客户端做: 优点是架构简单,扩展灵活,只对服务注册器依赖。缺点是客户端要维护所有调用服务的地址,有技术难度,一般大公司都有成熟的内部框架支持,比如Dubbo。

    服务端做: 优点是简单,所有服务对于前台调用方透明,一般在小公司在云服务上部署的应用采用的比较多。

    4. 服务挂了怎么办?

    分布式最大的特性就是网络是不可靠 的。通过微服务拆分能降低这个风险,不过如果没有特别的保障,结局肯定是噩梦。我们刚遇到一个线上故障就是一个很不起眼的SQL计数功能,在访问量上升 时,导致数据库load彪高,影响了所在应用的性能,从而影响所有调用这个应用服务的前台应用。所以当我们的系统是由一系列的服务调用链组成的时候,我们必须确保任一环节出问题都不至于影响整体链路。相应的手段有很多:

    • 重试机制

    • 限流

    • 熔断机制

    • 负载均衡

    • 降级(本地缓存) 这些方法基本上都很明确通用,就不详细说明了。比如Netflix的Hystrix:https://github.com/Netflix/Hystrix

    5. 微服务需要考虑的问题

    这里有一个图非常好的总结微服务架构需要考虑的问题,包括

    • API Gateway

    • 服务间调用

    • 服务发现

    • 服务容错

    • 服务部署

    • 数据调用

    微服务重要部件

    一个完整的微服务系统,它的底座最少要包含以下功能:

    • 日志和审计,主要是日志的汇总,分类和查询
    • 监控和告警,主要是监控每个服务的状态,必要时产生告警
    • 消息总线,轻量级的MQ或HTTP
    • 注册发现
    • 负载均衡
    • 部署和升级
    • 事件调度机制
    • 资源管理,如:底层的虚拟机,物理机和网络管理

    以下功能不是最小集的一部分,但也属于底座功能:

    • 认证和鉴权
    • 微服务统一代码框架,支持多种编程语言
    • 统一服务构建和打包
    • 统一服务测试
    • 微服务CI/CD流水线
    • 服务依赖关系管理
    • 统一问题跟踪调试框架,俗称调用链
    • 灰度发布
    • 蓝绿部署

    服务注册中心

    服务之间需要创建一种服务发现机制,用于帮助服务之间互相感知彼此的存在。服务启动时会将自身的服务信息注册到注册中心,并订阅自己需要消费的服务。

    服务注册中心是服务发现的核心。它保存了各个可用服务实例的网络地址(IPAddress和Port)。服务注册中心必须要有高可用性和实时更新功能。上面提到的 Netflix Eureka 就是一个服务注册中心。它提供了服务注册和查询服务信息的REST API。服务通过使用POST请求注册自己的IPAddress和Port。每30秒发送一个PUT请求刷新注册信息。通过DELETE请求注销服务。客户端通过GET请求获取可用的服务实例信息。 Netflix的高可用(Netflix achieves high availability )是通过在Amazon EC2运行多个实例来实现的,每一个Eureka服务都有一个弹性IP Address。当Eureka服务启动时,有DNS服务器动态的分配。Eureka客户端通过查询 DNS来获取Eureka的网络地址(IP Address和Port)。一般情况下,都是返回和客户端在同一个可用区Eureka服务器地址。 其他能够作为服务注册中心的有:

    • etcd —– 高可用,分布式,强一致性的,key-value,Kubernetes和Cloud Foundry都是使用了etcd。

    • consul —–一个用于discovering和configuring的工具。它提供了允许客户端注册和发现服务的API。Consul可以进行服务健康检查,以确定服务的可用性。

    • zookeeper —— 在分布式应用中被广泛使用,高性能的协调服务。 Apache Zookeeper 最初为Hadoop的一个子项目,但现在是一个顶级项目。

    zookeeper服务注册和发现

    简单来讲,zookeeper可以充当一个服务注册表(Service Registry),让多个服务提供者形成一个集群,让服务消费者通过服务注册表获取具体的服务访问地址(ip+端口)去访问具体的服务提供者。

    具体来说,zookeeper就是个分布式文件系统,每当一个服务提供者部署后都要将自己的服务注册到zookeeper的某一路径上: /{service}/{version}/{ip:port}, 比如我们的HelloWorldService部署到两台机器,那么zookeeper上就会创建两条目录:分别为/HelloWorldService/1.0.0/100.19.20.01:16888 /HelloWorldService/1.0.0/100.19.20.02:16888。

    zookeeper提供了“心跳检测”功能,它会定时向各个服务提供者发送一个请求(实际上建立的是一个 socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其剔除,比如100.19.20.02这台机器如果宕机了,那么zookeeper上的路径就会只剩/HelloWorldService/1.0.0/100.19.20.01:16888。

    服务消费者会去监听相应路径(/HelloWorldService/1.0.0),一旦路径上的数据有任务变化(增加或减少),zookeeper都会通知服务消费方服务提供者地址列表已经发生改变,从而进行更新。

    更为重要的是zookeeper 与生俱来的容错容灾能力(比如leader选举),可以确保服务注册表的高可用性。

    负载均衡

    服务高可用的保证手段,为了保证高可用,每一个微服务都需要部署多个服务实例来提供服务。此时客户端进行服务的负载均衡。

    负载均衡的常见策略

    1. 随机

      把来自网络的请求随机分配给内部中的多个服务器。

    2. 轮询

      每一个来自网络中的请求,轮流分配给内部的服务器,从1到N然后重新开始。此种负载均衡算法适合服务器组内部的服务器都具有相同的配置并且平均服务请求相对均衡的情况。

    3. 加权轮询

      根据服务器的不同处理能力,给每个服务器分配不同的权值,使其能够接受相应权值数的服务请求。例如:服务器A的权值被设计成1,B的权值是3,C的权值是6,则服务器A、B、C将分别接受到10%、30%、60%的服务请求。此种均衡算法能确保高性能的服务器得到更多的使用率,避免低性能的服务器负载过重。

    4. IP Hash

      这种方式通过生成请求源IP的哈希值,并通过这个哈希值来找到正确的真实服务器。这意味着对于同一主机来说他对应的服务器总是相同。使用这种方式,你不需要保存任何源IP。但是需要注意,这种方式可能导致服务器负载不平衡。

    5. 最少连接数

      客户端的每一次请求服务在服务器停留的时间可能会有较大的差异,随着工作时间加长,如果采用简单的轮循或随机均衡算法,每一台服务器上的连接进程可能会产生极大的不同,并没有达到真正的负载均衡。最少连接数均衡算法对内部中需负载的每一台服务器都有一个数据记录,记录当前该服务器正在处理的连接数量,当有新的服务连接请求时,将把当前请求分配给连接数最少的服务器,使均衡更加符合实际情况,负载更加均衡。此种均衡算法适合长时处理的请求服务,如FTP。

    容错

    容错,这个词的理解,直面意思就是可以容下错误,不让错误再次扩张,让这个错误产生的影响在一个固定的边界之内,“千里之堤毁于蚁穴”我们用容错的方式就是让这种蚁穴不要变大。那么我们常见的降级,限流,熔断器,超时重试等等都是容错的方法。

    在调用服务集群时,如果一个微服务调用异常,如超时,连接异常,网络异常等,则根据容错策略进行服务容错。目前支持的服务容错策略有快速失败,失效切换。如果连续失败多次则直接熔断,不再发起调用。这样可以避免一个服务异常拖垮所有依赖于他的服务。

    容错策略

    1. 快速失败

      服务只发起一次待用,失败立即报错。通常用于非幂等下性的写操作

    2. 失效切换

      服务发起调用,当出现失败后,重试其他服务器。通常用于读操作,但重试会带来更长时间的延迟。重试的次数通常是可以设置的

    3. 失败安全

      失败安全, 当服务调用出现异常时,直接忽略。通常用于写入日志等操作。

    4. 失败自动恢复

      当服务调用出现异常时,记录失败请求,定时重发。通常用于消息通知。

    5. forking Cluster

      并行调用多个服务器,只要有一个成功,即返回。通常用于实时性较高的读操作。可以通过forks=n来设置最大并行数。

    6. 广播调用

      广播调用所有提供者,逐个调用,任何一台失败则失败。通常用于通知所有提供者更新缓存或日志等本地资源信息。

    熔断

    熔断技术可以说是一种“智能化的容错”,当调用满足失败次数,失败比例就会触发熔断器打开,有程序自动切断当前的RPC调用,来防止错误进一步扩大。实现一个熔断器主要是考虑三种模式,关闭,打开,半开。

    我们在处理异常的时候,要根据具体的业务情况来决定处理方式,比如我们调用商品接口,对方只是临时做了降级处理,那么作为网关调用就要切到可替换的服务上来执行或者获取托底数据,给用户友好提示。还有要区分异常的类型,比如依赖的服务崩溃了,这个可能需要花费比较久的时间来解决。也可能是由于服务器负载临时过高导致超时。作为熔断器应该能够甄别这种异常类型,从而根据具体的错误类型调整熔断策略。增加手动设置,在失败的服务恢复时间不确定的情况下,管理员可以手动强制切换熔断状态。最后,熔断器的使用场景是调用可能失败的远程服务程序或者共享资源。如果是本地缓存本地私有资源,使用熔断器则会增加系统的额外开销。还要注意,熔断器不能作为应用程序中业务逻辑的异常处理替代品。

    有一些异常比较顽固,突然发生,无法预测,而且很难恢复,并且还会导致级联失败(举个例子,假设一个服务集群的负载非常高,如果这时候集群的一部分挂掉了,还占了很大一部分资源,整个集群都有可能遭殃)。如果我们这时还是不断进行重试的话,结果大多都是失败的。因此,此时我们的应用需要立即进入失败状态(fast-fail),并采取合适的方法进行恢复。

    我们可以用状态机来实现CircuitBreaker,它有以下三种状态:

    • 关闭( Closed ):默认情况下Circuit Breaker是关闭的,此时允许操作执行。CircuitBreaker内部记录着最近失败的次数,如果对应的操作执行失败,次数就会续一次。如果在某个时间段内,失败次数(或者失败比率)达到阈值,CircuitBreaker会转换到开启( Open )状态。在开启状态中,Circuit Breaker会启用一个超时计时器,设这个计时器的目的是给集群相应的时间来恢复故障。当计时器时间到的时候,CircuitBreaker会转换到半开启( Half-Open )状态。

    • 开启( Open ):在此状态下,执行对应的操作将会立即失败并且立即抛出异常。

    • 半开启( Half-Open ):在此状态下,Circuit Breaker会允许执行一定数量的操作。如果所有操作全部成功,CircuitBreaker就会假定故障已经恢复,它就会转换到关闭状态,并且重置失败次数。如果其中 任意一次 操作失败了,Circuit Breaker就会认为故障仍然存在,所以它会转换到开启状态并再次开启计时器(再给系统一些时间使其从失败中恢复)

    限流和降级

    保证核心服务的稳定性。为了保证核心服务的稳定性,随着访问量的不断增加,需要为系统能够处理的服务数量设置一个极限阀值,超过这个阀值的请求则直接拒绝。同时,为了保证核心服务的可用,可以对否些非核心服务进行降级,通过限制服务的最大访问量进行限流,通过管理控制台对单个微服务进行人工降级

    SLA

    SLA:Service-LevelAgreement的缩写,意思是服务等级协议。 是关于网络服务供应商和客户间的一份合同,其中定义了服务类型、服务质量和客户付款等术语。 典型的SLA包括以下项目:

    • 分配给客户的最小带宽;

    • 客户带宽极限;

    • 能同时服务的客户数目;

    • 在可能影响用户行为的网络变化之前的通知安排;

    • 拨入访问可用性;

    • 运用统计学;

    • 服务供应商支持的最小网络利用性能,如99.9%有效工作时间或每天最多为1分钟的停机时间;

    • 各类客户的流量优先权;

    • 客户技术支持和服务;

    • 惩罚规定,为服务供应商不能满足 SLA需求所指定。

    API网关

    这里说的网关是指API网关,直面意思是将所有API调用统一接入到API网关层,有网关层统一接入和输出。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。有了网关之后,各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问题。

    多级缓存

    最简单的缓存就是查一次数据库然后将数据写入缓存比如redis中并设置过期时间。因为有过期失效因此我们要关注下缓存的穿透率,这个穿透率的计算公式,比如查询方法queryOrder(调用次数1000/1s)里面嵌套查询DB方法queryProductFromDb(调用次数300/s),那么redis的穿透率就是300/1000,在这种使用缓存的方式下,是要重视穿透率的,穿透率大了说明缓存的效果不好。还有一种使用缓存的方式就是将缓存持久化,也就是不设置过期时间,这个就会面临一个数据更新的问题。一般有两种办法,一个是利用时间戳,查询默认以redis为主,每次设置数据的时候放入一个时间戳,每次读取数据的时候用系统当前时间和上次设置的这个时间戳做对比,比如超过5分钟,那么就再查一次数据库。这样可以保证redis里面永远有数据,一般是对DB的一种容错方法。还有一个就是真正的让redis做为DB使用。就是图里面画的通过订阅数据库的binlog通过数据异构系统将数据推送给缓存,同时将将缓存设置为多级。可以通过使用jvmcache作为应用内的一级缓存,一般是体积小,访问频率大的更适合这种jvmcache方式,将一套redis作为二级remote缓存,另外最外层三级redis作为持久化缓存。

    超时和重试

    超时与重试机制也是容错的一种方法,凡是发生RPC调用的地方,比如读取redis,db,mq等,因为网络故障或者是所依赖的服务故障,长时间不能返回结果,就会导致线程增加,加大cpu负载,甚至导致雪崩。所以对每一个RPC调用都要设置超时时间。对于强依赖RPC调用资源的情况,还要有重试机制,但是重试的次数建议1-2次,另外如果有重试,那么超时时间就要相应的调小,比如重试1次,那么一共是发生2次调用。如果超时时间配置的是2s,那么客户端就要等待4s才能返回。因此重试+超时的方式,超时时间要调小。这里也再谈一下一次PRC调用的时间都消耗在哪些环节,一次正常的调用统计的耗时主要包括: ①调用端RPC框架执行时间 + ②网络发送时间 + ③服务端RPC框架执行时间 + ④服务端业务代码时间。调用方和服务方都有各自的性能监控,比如调用方tp99是500ms,服务方tp99是100ms,找了网络组的同事确认网络没有问题。那么时间都花在什么地方了呢,两种原因,客户端调用方,还有一个原因是网络发生TCP重传。所以要注意这两点。

    线程池隔离

    在抗量这个环节,Servlet3异步的时候,有提到过线程隔离。线程隔离的之间优势就是防止级联故障,甚至是雪崩。当网关调用N多个接口服务的时候,我们要对每个接口进行线程隔离。比如,我们有调用订单、商品、用户。那么订单的业务不能够影响到商品和用户的请求处理。如果不做线程隔离,当访问订单服务出现网络故障导致延时,线程积压最终导致整个服务CPU负载满。就是我们说的服务全部不可用了,有多少机器都会被此刻的请求塞满。那么有了线程隔离就会使得我们的网关能保证局部问题不会影响全局。

    降级和限流

    关于降级限流的方法业界都已经有很成熟的方法了,比如FAILBACK机制,限流的方法令牌桶,漏桶,信号量等。这里谈一下我们的一些经验,降级一般都是由统一配置中心的降级开关来实现的,那么当有很多个接口来自同一个提供方,这个提供方的系统或这机器所在机房网络出现了问题,我们就要有一个统一的降级开关,不然就要一个接口一个接口的来降级。也就是要对业务类型有一个大闸刀。还有就是 降级切记暴力降级,什么是暴力降级的,比如把论坛功能降调,结果用户显示一个大白板,我们要实现缓存住一些数据,也就是有托底数据。限流一般分为分布式限流和单机限流,如果实现分布式限流的话就要一个公共的后端存储服务比如redis,在大nginx节点上利用lua读取redis配置信息。我们现在的限流都是单机限流,并没有实施分布式限流。

    网关监控和统计

    API网关是一个串行的调用,那么每一步发生的异常要记录下来,统一存储到一个地方比如elasticserach中,便于后续对调用异常的分析。鉴于公司docker申请都是统一分配,而且分配之前docker上已经存在3个agent了,不再允许增加。我们自己实现了一个agent程序,来负责采集服务器上面的日志输出,然后发送到kafka集群,再消费到elasticserach中,通过web查询。现在做的追踪功能还比较简单,这块还需要继续丰富。

    展开全文
  • 本来以为写完了,结果最近和个同事在讨论心跳的事情,这里再做个补充。先说我的结论: WebSocket协议已经设计了心跳,这个功能可以到达检测链接是否可用 心跳是用来检测链接是否可用的,不一定支持携带数据,...

    0x00 心跳

    本来以为写完了,结果最近和一个同事在讨论心跳的事情,这里再做一个补充。先说我的结论:

        WebSocket协议已经设计了心跳,这个功能可以到达检测链接是否可用
        心跳是用来检测链接是否可用的,不一定支持携带数据,可要看具体实现
        如果非要心跳中带上复杂数据,那这个可作为应用层的一个功能自己去实现。

    心跳逻辑
    0x01 WebSocket协议的控制帧

    上一篇的最后简单提到了心跳,下面是对websocket协议控制帧的描述:

    5.5.  Control Frames

       Control frames are identified by opcodes where the most significant
       bit of the opcode is 1.  Currently defined opcodes for control frames
       include 0x8 (Close), 0x9 (Ping), and 0xA (Pong).  Opcodes 0xB-0xF are
       reserved for further control frames yet to be defined.

       Control frames are used to communicate state about the WebSocket.
       Control frames can be interjected in the middle of a fragmented
       message.

       All control frames MUST have a payload length of 125 bytes or less
       and MUST NOT be fragmented.

     

        Ping的协议头是0x9,Pong的协议头是0xA
        控制帧最大载荷为125bytes且不能拆分

    0x02 WebSocket协议的心跳

    下面再来看看对心跳的规定:

    5.5.2.  Ping

       The Ping frame contains an opcode of 0x9.

       A Ping frame MAY include "Application data".
       // 注:Ping帧中可能会携带数据

       Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
       response, unless it already received a Close frame.  It SHOULD
       respond with Pong frame as soon as is practical.  Pong frames are
       discussed in Section 5.5.3.
       // 注:在收到Ping帧后,端点必须发送Pong帧响应,除非已经收到了Close帧。在实际中应尽可能快的响应。

       An endpoint MAY send a Ping frame any time after the connection is
       established and before the connection is closed.

       NOTE: A Ping frame may serve either as a keepalive or as a means to
       verify that the remote endpoint is still responsive.

    5.5.3.  Pong

       The Pong frame contains an opcode of 0xA.

       Section 5.5.2 details requirements that apply to both Ping and Pong
       frames.

       A Pong frame sent in response to a Ping frame must have identical
       "Application data" as found in the message body of the Ping frame
       being replied to.
       // 注:在响应Ping帧的的Pong帧中,必须携和被响应的Ping帧中相同的数据。

       If an endpoint receives a Ping frame and has not yet sent Pong
       frame(s) in response to previous Ping frame(s), the endpoint MAY
       elect to send a Pong frame for only the most recently processed Ping
       frame.

    从上面的描述我们可以得到如下结论:

        心跳包中可能会携带数据
        当收到Ping帧的时候需要立即返回一个Pong帧
        在连接建立之后,随时都可以发送Ping帧
        心跳是用来测试链接是否存在和对方是否在线
        在响应Ping帧的的Pong帧中,必须携和被响应的Ping帧中相同的数据

    0x03 测试

    和之前一样,自己本地搭建的服务器,用的库是 org.java_websocket。
    在其源码中我们可以找到这样一段:

    package org.java_websocket;

    public abstract class WebSocketAdapter implements WebSocketListener {
        ...

        public void onWebsocketPing(WebSocket conn, Framedata f) {
            FramedataImpl1 resp = new FramedataImpl1(f);
            resp.setOptcode(Opcode.PONG);
            conn.sendFrame(resp);
        }

        public void onWebsocketPong(WebSocket conn, Framedata f) {
        }

        ...
    }

     

    客户端也可以使用这个库,相同的逻辑,代码也是这一份。

    然后我们再换一个库,com.squareup.okhttp3:okhttp-ws:3.4.2,他的实现如下:


    package okhttp3.internal.ws;

    public abstract class RealWebSocket implements WebSocket {
      ...
      public RealWebSocket(boolean isClient, BufferedSource source, BufferedSink sink, Random random,
          final Executor replyExecutor, final WebSocketListener listener, final String url) {
        this.listener = listener;

        writer = new WebSocketWriter(isClient, sink, random);
        reader = new WebSocketReader(isClient, source, new FrameCallback() {
          @Override public void onMessage(ResponseBody message) throws IOException {
            listener.onMessage(message);
          }

          @Override public void onPing(final Buffer buffer) {
            replyExecutor.execute(new NamedRunnable("OkHttp %s WebSocket Pong Reply", url) {
              @Override protected void execute() {
                try {
                  writer.writePong(buffer);
                } catch (IOException ignored) {
                }
              }
            });
          }

          @Override public void onPong(Buffer buffer) {
            listener.onPong(buffer);
          }

          @Override public void onClose(final int code, final String reason) {
            readerSentClose = true;
            replyExecutor.execute(new NamedRunnable("OkHttp %s WebSocket Close Reply", url) {
              @Override protected void execute() {
                peerClose(code, reason);
              }
            });
          }
        });
      }
    ...
    }

     

    在处理Ping帧的时候,也是将协议字段改为Pong然后返回。

    对心跳的测试代码已经上传到Github:ttdevs
    WebSocketActivity.java
    WebSocketOKActivity.java
    SocketServer.java

    在实际的测试中,可能会遇到一些异常,比如在我们自己的生产环境:当客户端发送带了简单数据的Ping帧后,服务器立马返回Pong帧,但是它会将携带的数据丢弃。这个就是服务端的问题了。

    展开全文
  • 例如,对于个即时通讯软件来说,如果服务器没有消息时,我们确实不会和服务器有任何数据交换,但是如果连接被关闭了,有新消息来时,我们再也没法收到了,这违背了“即时通讯”的设计要求。 2)情形二:通常...

    本文原文由作者“张小方”原创发布于“高性能服务器开发”微信公众号,原题《心跳包机制设计详解》。

    考虑以下两种典型的即时通讯网络层问题情型:

    1)情形一:一个客户端连接服务器以后,如果长期没有和服务器有数据来往,可能会被防火墙程序关闭连接,有时候我们并不想要被关闭连接。例如,对于一个即时通讯软件来说,如果服务器没有消息时,我们确实不会和服务器有任何数据交换,但是如果连接被关闭了,有新消息来时,我们再也没法收到了,这就违背了“即时通讯”的设计要求。

    2)情形二:通常情况下,服务器与某个客户端一般不是位于同一个网络,其之间可能经过数个路由器和交换机,如果其中某个必经路由器或者交换器出现了故障,并且一段时间内没有恢复,导致这之间的链路不再畅通,而此时服务器与客户端之间也没有数据进行交换,由于 TCP 连接是状态机,对于这种情况,无论是客户端或者服务器都无法感知与对方的连接是否正常,这类连接我们一般称之为“死链”。

    对于上述问题情型,即时通讯应用通常的解决思路:

    1)针对情形一:此应用场景要求必须保持客户端与服务器之间的连接正常,就是我们通常所说的“保活“。如上所述,当服务器与客户端一定时间内没有有效业务数据来往时,我们只需要给对端发送心跳包即可实现保活。

    2)针对情形二:要解决死链问题,只要我们此时任意一端给对端发送一个数据包即可检测链路是否正常,这类数据包我们也称之为”心跳包”,这种操作我们称之为“心跳检测”。顾名思义,如果一个人没有心跳了,可能已经死亡了;一个连接长时间没有正常数据来往,也没有心跳包来往,就可以认为这个连接已经不存在,为了节约服务器连接资源,我们可以通过关闭 socket,回收连接资源。

    总之,心跳检测机制一般有两个作用:

    1)保活;
    2)检测死链。
    展开全文
  • TCP网络长连接 手机能够使用联网功能是因为手机底层实现了TCP/IP协议,可以使手机终端通过无线网络建立TCP连接。TCP协议可以对上层网络提供接口,使上层网络数据...第二次握手:服务器收到syn包,必须确认客户的SYN...

    TCP网络长连接
    手机能够使用联网功能是因为手机底层实现了TCP/IP协议,可以使手机终端通过无线网络建立TCP连接。TCP协议可以对上层网络提供接口,使上层网络数据的传输建立在“无差别”的网络之上。

    建立起一个TCP连接需要经过“三次握手”:
    第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
    第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
    第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

    握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)

    什么是心跳
    刚才说到长连接建立连接后,理想状态下是不会断开的,但是由于网络问题,可能导致一方断开后,另一方仍然在发送数据,或者有些客户端长时间不发送消息,服务器还维持这他的客户端不必要的引用,增加了服务器的负荷。因此我们引入了心跳机制。

    心跳包之所以叫心跳包是因为:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。

    总的来说,心跳包主要也就是用于长连接的保活和断线处理。一般的应用下,判定时间在30-40秒比较不错。如果实在要求高,那就在6-9秒。
    怎么发送心跳?
    心跳包的发送,通常有两种技术
    方法1:应用层自己实现的心跳包 
    由应用程序自己发送心跳包来检测连接是否正常,大致的方法是:服务器在一个 Timer事件中定时 向客户端发送一个短小精悍的数据包,然后启动一个低级别的线程,在该线程中不断检测客户端的回应, 如果在一定时间内没有收到客户端的回应,即认为客户端已经掉线;同样,如果客户端在一定时间内没 有收到服务器的心跳包,则认为连接不可用。

    方法2:TCP的KeepAlive保活机制
    因为要考虑到一个服务器通常会连接多个客户端,因此由用户在应用层自己实现心跳包,代码较多 且稍显复杂,而利用TCP/IP协议层为内置的KeepAlive功能来实现心跳功能则简单得多。 不论是服务端还是客户端,一方开启KeepAlive功能后,就会自动在规定时间内向对方发送心跳包, 而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。 因为开启KeepAlive功能需要消耗额外的宽带和流量,所以TCP协议层默认并不开启KeepAlive功 能,尽管这微不足道,但在按流量计费的环境下增加了费用,另一方面,KeepAlive设置不合理时可能会 因为短暂的网络波动而断开健康的TCP连接。并且,默认的KeepAlive超时需要7,200,000 MilliSeconds, 即2小时,探测次数为5次。对于很多服务端应用程序来说,2小时的空闲时间太长。因此,我们需要手工开启KeepAlive功能并设置合理的KeepAlive参数。

    心跳检测步骤:
    1客户端每隔一个时间间隔发生一个探测包给服务器
    2客户端发包时启动一个超时定时器
    3服务器端接收到检测包,应该回应一个包
    4如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器
    5如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了

    C#多线程实现一个简单的心跳 

    
    using System;
    using System.Collections.Generic;
    using System.Threading;
     
    namespace ConsoleApplication1
    {
        // 客户端离线委托
        public delegate void ClientOfflineHandler(ClientInfo client);
     
        // 客户端上线委托
        public delegate void ClientOnlineHandler(ClientInfo client);
     
        public class Program
        {
            /// <summary>
            /// 客户端离线提示
            /// </summary>
            /// <param name="clientInfo"></param>
            private static void ClientOffline(ClientInfo clientInfo)
            {
                Console.WriteLine(String.Format("客户端{0}离线,离线时间:\t{1}", clientInfo.ClientID, clientInfo.LastHeartbeatTime));
            }
     
            /// <summary>
            /// 客户端上线提示
            /// </summary>
            /// <param name="clientInfo"></param>
            private static void ClientOnline(ClientInfo clientInfo)
            {
                Console.WriteLine(String.Format("客户端{0}上线,上线时间:\t{1}", clientInfo.ClientID, clientInfo.LastHeartbeatTime));
            }
     
            static void Main()
            {
                // 服务端
                Server server = new Server();
     
                // 服务端离线事件
                server.OnClientOffline += ClientOffline;
     
                // 服务器上线事件
                server.OnClientOnline += ClientOnline;
     
                // 开启服务器
                server.Start();
     
                // 模拟100个客户端
                Dictionary<Int32, Client> dicClient = new Dictionary<Int32, Client>();
                for (Int32 i = 0; i < 100; i++)
                {
                    // 这里传入server只是为了方便而已
                    Client client = new Client(i + 1, server);
                    dicClient.Add(i + 1, client);
     
                    // 开启客户端
                    client.Start();
                }
     
                System.Threading.Thread.Sleep(1000);
     
                while (true)
                {
                    Console.WriteLine("请输入要离线的ClientID,输入0则退出程序:");
                    String clientID = Console.ReadLine();
                    if (!String.IsNullOrEmpty(clientID))
                    {
                        Int32 iClientID = 0;
                        Int32.TryParse(clientID, out iClientID);
                        if (iClientID > 0)
                        {
                            Client client;
                            if (dicClient.TryGetValue(iClientID, out client))
                            {
                                // 客户端离线
                                client.Offline = true;
                            }
                        }
                        else
                        {
                            return;
                        }
                    }
                }
            }
        }
     
        /// <summary>
        /// 服务端
        /// </summary>
        public class Server
        {
            public event ClientOfflineHandler OnClientOffline;
            public event ClientOnlineHandler OnClientOnline;
     
            private Dictionary<Int32, ClientInfo> _DicClient;
     
            /// <summary>
            /// 构造函数
            /// </summary>
            public Server()
            {
                _DicClient = new Dictionary<Int32, ClientInfo>(100);            
            }
     
            /// <summary>
            /// 开启服务端
            /// </summary>
            public void Start()
            {
                // 开启扫描离线线程
                Thread t = new Thread(new ThreadStart(ScanOffline));
                t.IsBackground = true;
                t.Start();
            }
     
            /// <summary>
            /// 扫描离线
            /// </summary>
            private void ScanOffline()
            {
                while (true)
                {
                    // 一秒扫描一次
                    System.Threading.Thread.Sleep(1000);
     
                    lock (_DicClient)
                    {
                        foreach (Int32 clientID in _DicClient.Keys)
                        {
                            ClientInfo clientInfo = _DicClient[clientID];
     
                            // 如果已经离线则不用管
                            if (!clientInfo.State)
                            {
                                continue;
                            }
     
                            // 判断最后心跳时间是否大于3秒
                            TimeSpan sp = System.DateTime.Now - clientInfo.LastHeartbeatTime;
                            if (sp.Seconds >= 3)
                            {
                                // 离线,触发离线事件
                                if (OnClientOffline != null)
                                {
                                    OnClientOffline(clientInfo);
                                }
     
                                // 修改状态
                                clientInfo.State = false;
                            }
                        }
                    }
                }
            }
     
            /// <summary>
            /// 接收心跳包
            /// </summary>
            /// <param name="clientID">客户端ID</param>
            public void ReceiveHeartbeat(Int32 clientID)
            {
                lock (_DicClient)
                {
                    ClientInfo clientInfo;
                    if (_DicClient.TryGetValue(clientID, out clientInfo))
                    {
                        // 如果客户端已经上线,则更新最后心跳时间
                        clientInfo.LastHeartbeatTime = System.DateTime.Now;
                    }
                    else
                    {
                        // 客户端不存在,则认为是新上线的
                        clientInfo = new ClientInfo();
                        clientInfo.ClientID = clientID;
                        clientInfo.LastHeartbeatTime = System.DateTime.Now;
                        clientInfo.State = true;
     
                        _DicClient.Add(clientID, clientInfo);
     
                        // 触发上线事件
                        if (OnClientOnline != null)
                        {
                            OnClientOnline(clientInfo);
                        }
                    }
                }
            }
        }
     
        /// <summary>
        /// 客户端
        /// </summary>
        public class Client
        {
            public Server Server;
            public Int32 ClientID;
            public Boolean Offline;
     
            /// <summary>
            /// 构造函数
            /// </summary>
            /// <param name="clientID"></param>
            /// <param name="server"></param>
            public Client(Int32 clientID, Server server)
            {
                ClientID = clientID;
                Server = server;
                Offline = false;
            }
     
            /// <summary>
            /// 开启客户端
            /// </summary>
            public void Start()
            {
                // 开启心跳线程
                Thread t = new Thread(new ThreadStart(Heartbeat));
                t.IsBackground = true;
                t.Start();
            }
     
            /// <summary>
            /// 向服务器发送心跳包
            /// </summary>
            private void Heartbeat()
            {
                while (!Offline)
                {
                    // 向服务端发送心跳包
                    Server.ReceiveHeartbeat(ClientID);
                     
                    System.Threading.Thread.Sleep(1000);
                }
            }
        }
     
        /// <summary>
        /// 客户端信息
        /// </summary>
        public class ClientInfo
        {
            // 客户端ID
            public Int32 ClientID;
     
            // 最后心跳时间
            public DateTime LastHeartbeatTime;
     
            // 状态
            public Boolean State;
        }
    }

     

    展开全文
  • Netty 是个高性能的 NIO 网络框架,本文基于 SpringBoot 以常见的心跳机制来认识 Netty。 最终能达到的效果: 客户端每隔 N 秒检测是否需要发送心跳。 服务端也每隔 N 秒检测是否需要发送心跳。 服务端可以主动...
  • 分布式服务框架

    千次阅读 2016-01-29 10:42:11
    个大型、稳健、成熟的分布式系统的背后,往往会涉及众多的支撑系统,我们将这些支 撑系统称为分布式系统的基础设施。除了前面所介绍的分布式协作及配置管理系统ZooKeeper, 我们进行系统架构设计所依赖的基础设施...
  • Android心跳包()——心跳机制

    万次阅读 2017-11-21 16:40:25
    在写之前,我们首先了解一下为什么android维护长连接需要心跳机制,首先我们知道,维护任何...服务器,服务器给客户端心跳应答,这样形成客户端服务器的次完整的握手,这个握手是让双方都知道他们之间的连
  • MQTT协议笔记之连接和心跳

    千次阅读 2018-06-21 11:57:56
    2017年03月11日 20:28:22阅读数:506前言本篇会把连接(CONNECT)、心跳(PINGREQ/PINGRESP)、确认(CONNACK)、断开连接(DISCONNECT)和在一起。CONNECT像前面所说,MQTT有关字符串部分采用的修改版的UTF-8编码,CONNECT...
  • Android微信智能心跳方案 Android休眠问题探讨Android推送服务的几种实现方式、推送方式基础知识: 在移动互联网时代以前的手机,如果有事情发生需要通知用户,则会有个窗口弹出,将告诉用户正在发生什么事情...
  • tcp心跳机制

    2020-07-19 23:02:06
    心跳包 很多应用层协议都有HeartBeat机制,通常是客户端每隔小段时间向服务器发送个数据包,通知服务器自己仍然在线,并传输一些可能必要的数据。使用心跳包的典型协议是IM,比如QQ/MSN/飞信等协议。 心跳...
  • java 心跳机制

    2017-08-24 17:21:24
     用来判断对方(设备,进程或其它网元)是否正常运行,采用定时发送简单的通讯包,如果在指定时间段内未收到对方响应,则判断对方已经离线。用于检测TCP的异常断开。基本原因是服务器端不能有效的判断客户端是否...
  • Java实现心跳机制

    万次阅读 2018-11-07 16:15:30
    心跳机制简介  在分布式系统中,分布在不同主机上的...为了检测对方节点的有效性,每隔固定时间发送个固定信息给对方,对方回复个固定信息,如果长时间没有收到对方的回复,则断开与对方的连接。  ...
  • Flink 心跳服务机制

    2020-10-08 15:53:51
    心跳机制是用于检测客户端或者服务端是否存活的种机制,通过定期向对方发送请求方法,常见的心跳检测有两种: socket 套接字SO_KEEPALIVE本身带有的心跳机制,定期向对方发送心跳包,对方收到心跳包后会自动...
  • 前言 最近研究Android推送的实现, 研究了两天一夜, 有了一点... 主要还是毕业设计要做个即时通信app, 我是不喜欢做什么社交app的, 也象牙塔里的人想得出来, 说实话有这功夫还不如钻研个小技术点, 把个点研究
  • java心跳机制

    千次阅读 2017-12-10 02:21:29
    心跳机制是定时发送个自定义的结构体(心跳包),让对方知道自己还活着,以确保连接的有效性的机制。 什么是心跳机制? 就是每隔几分钟发送个固定信息给服务端,服务端收到后回复个固定信息如果服务端几分钟内...
  • Mina心跳检测实现

    千次阅读 2018-03-10 01:40:42
    篇文章讲了Mina的简单使用,这篇将要讲讲怎么用Mina实现心跳检测。网上有很多相关的文章,但是我觉得比较难理解,自己折腾了好久才明白,所以想用我觉得容易理解的话总结一下。也给自己做个笔记。心跳机制...
  • 心跳机制就是每隔几分钟发送个固定信息给服务端,服务端收到后回复个固定信息如果服务端几分钟内没有收到客户端信息则视客户端断开。发包方可以是客户也可以是服务端,具体看哪边实现更方便合理 心跳包的发送...
  • websocket心跳检测前后端架构

    千次阅读 2020-03-19 18:50:35
    下面介绍内容参照 廖雪峰 的描述,为了切换页面麻烦,这里直接挑简要的信息复制黏贴过来。 廖雪峰 – WebSocket 教程。 为什么需要 WebSocket? 初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP ...
  • 收到心跳,一般可以认为发送心跳的这个节点在当前的网络拓扑中是良好的。 当然,心跳汇报时,一般也会携带一些附加的状态、元数据信息、当前节点的信息,以便其他节点管理。 心跳有两种方式: 单向的 heartbeat; ...
  • MQTT心跳机制

    万次阅读 2019-12-14 17:09:57
    若Broker超过1.5T时间没收到心跳请求则断开连接,并且投递遗嘱消息到订阅方;同样,若客户端超过一定时间仍没收到心跳响应PINGRESP则断开连接。  连接空闲时发送心跳报文可以降低网络请求,弱...
  • 心跳机制

    千次阅读 2019-03-17 20:13:43
    心跳机制是定时发送个自定义的结构体(心跳包),让对方知道自己还活着,以确保连接的有效性的机制。 应用场景: 在长连接下,有可能很长段时间都没有数据往来。理论上说,这个连接是一直保持连接的,但是实际...
  • 、计算机网络通信之心跳概念 网络中的接收和发送数据都是使用操作系统中的SOCKET进行实现。但是如果此套接字已经断开,...所谓“心跳”就是定时发送个自定义的结构体(心跳包),让对方知道自己还活着。 以确保链接
  • android长连接心跳机制

    千次阅读 2017-12-12 11:20:49
    服务器,服务器给客户端心跳应答,这样形成客户端服务器的次完整的握手,这个握手是让双方都知道他们之间的连接是没有断开,客户端是在线 的。如果超过个时间的阈值,客户端没有收到服务器的应答,或者...
  • 心跳包机制

    千次阅读 2016-01-06 00:30:12
    简介之所以叫心跳包是因为:它像心跳一样每隔固定时间发次,以此来告诉服务器,这个客户端还活着。事实上这是为了保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,958
精华内容 4,383
关键字:

一收到对方消息就心跳