音视频技术为什么需要微服务
微服务,英文名:microservice,百度百科上将其定义为:SOA 架构的一种变体。微服务(或微服务架构)是一种将应用程序构造为一组低耦合的服务。
微服务有着一些鲜明的特点:
- 功能单一
- 服务粒度小
- 服务间独立性强
- 服务间依赖性弱
- 服务独立维护
- 服务独立部署
对于每一个微服务来说,其提供的功能应该是单一的;其粒度很小的;它只会提供某一业务功能涉及到的相关接口。如:电商系统中的订单系统、支付系统、产品系统等,每一个系统服务都只是做该系统独立的功能,不会涉及到不属于它的功能逻辑。
微服务之间的依赖性应该是尽量弱的,这样带来的好处是:不会因为单一系统服务的宕机,而导致其它系统无法正常运行,从而影响用户的体验。同样以电商系统为例:用户将商品加入购物车后,提交订单,这时候去支付,发现无法支付,此时,可以将订单进入待支付状态,从而防止订单的丢失和用户体验的不友好。如果订单系统与支付系统的强依赖性,会导致订单系统一直在等待支付系统的回应,这样会导致用户的界面始终处于加载状态,从而导致用户无法进行任何操作。
当出现某个微服务的功能需要升级,或某个功能需要修复 bug 时,只需要把当前的服务进行编译、部署即可,不需要一个个打包整个产品业务功能的巨多服务,独立维护、独立部署。
上面描述的微服务,其实突出其鲜明特性:高内聚、低耦合,问题来了。什么是高内聚,什么是低耦合呢?所谓高内聚:就是说每个服务处于同一个网络或网域下,而且相对于外部,整个的是一个封闭的、安全的盒子。盒子对外的接口是不变的,盒子内部各模块之间的接口也是不变的,但是各模块内部的内容可以更改。模块只对外暴露最小限度的接口,避免强依赖关系。增删一个模块,应该只会影响有依赖关系的相关模块,无关的不应该受影响。
所谓低耦合:从小的角度来看,就是要每个 Java 类之间的耦合性降低,多用接口,利用 Java 面向对象编程思想的封装、继承、多态,隐藏实现细节。从模块之间来讲,就是要每个模块之间的关系降低,减少冗余、重复、交叉的复杂度,模块功能划分尽可能单一。
在音视频应用技术中,我们知道其实主要占用的资源是 cpu、memory,而且涉及到资源的共享问题,所以需要结合 NFS 来实现跨节点的资源共享。当然,单节点暴露的问题是,如果一旦客户端与服务器保持长时间的连接,而且,不同客户端同时发送请求,此时,单节点的压力是很大的。很有可能导致 cpu、memory 吃紧,从而导致节点的 crash,这样,不利于系统的高可用、服务的健壮性。此时,需要解决的是音视频通信中的资源吃紧的问题,在系统领域,通常可以采用多节点的方式,来实现分布式、高并发请求,当请求过来时,可以通过负载均衡的方式,通过一定的策略,如:根据最小请求数,或为每一个服务器赋予一个权重值,服务器响应时间越长,这个服务器的权重就越小,被选中的几率就会降低。这样来控制服务请求压力,从而让客户端与服务器能够保持长时间、有效的进行通信。
如何使用 Springboot 框架搭建微服务
介绍
这几年的快速发展,微服务已经变得越来越流行。其中,Spring Cloud 一直在更新,并被大部分公司所使用。代表性的有 Alibaba,2018 年 11 月左右,Spring Cloud 联合创始人 Spencer Gibb 在 Spring 官网的博客页面宣布:阿里巴巴开源 Spring Cloud Alibaba,并发布了首个预览版本。随后,Spring Cloud 官方 Twitter 也发布了此消息。
在 Spring Boot1.x 中,主要包括 Eureka、Zuul、Config、Ribbon、Hystrix 等。而在 Spring Boot2.x 中,网关采用了自己的 Gateway。当然在 Alibaba 版本中,其组件更是丰富:使用 Alibaba 的 Nacos 作为注册中心和配置中心。使用自带组件 Sentinel 作为限流、熔断神器。
搭建注册中心
我们今天主要来利用 Springboot 结合阿里巴巴的插件来实现微聊天系统的微服务设计。首先先来创建一个注册中心 Nacos。
我们先下载 Nacos,Nacos 地址:github.com/alibaba/nac…我们下载对应系统的二进制文件后,对应自己的系统,执行如下命令:
1 | 复制代码Linux/Unix/Mac:sh startup.sh -m standalone |
启动完成之后,访问:http://127.0.0.1:8848/nacos/,可以进入 Nacos 的服务管理页面,具体如下:
默认用户名与密码都是 nacos。
登陆后打开服务管理,可以看到注册到 Nacos 的服务列表:
可以点击配置管理,查看配置:
如果没有配置任何服务的配置,可以新建:
上面讲述了 Nacos 如何作为注册中心与配置中心的,很简单吧。
第一个微服务
接下来,对于微服务,那需要有一个服务被注册与被发现,我们讲解服务提供者代码:
1 | xml复制代码<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
一如既往的引入依赖,配置 bootstrap 文件:
1 | yaml复制代码management: |
接下来启动类:
1 | kotlin复制代码package com.damon; |
注意:注解 @EnableDiscoveryClient、@EnableOAuth2Sso 都需要。
这时,同样需要配置 ResourceServerConfig、SecurityConfig。
如果需要数据库,可以加上:
1 | java复制代码package com.damon.config; |
接下来新写一个 controller 类:
1 | kotlin复制代码package com.damon.user.controller; |
基本上一个代码就完成了。接下来测试一下:
认证:
1 | bash复制代码curl -i -X POST -d "username=admin&password=123456&grant_type=password&client_id=provider-service&client_secret=provider-service-123" http://localhost:5555/oauth-cas/oauth/token |
拿到 token 后:
1 | bash复制代码curl -i -H "Accept: application/json" -H "Authorization:bearer f4a42baa-a24a-4342-a00b-32cb135afce9" -X GET http://localhost:5555/provider-service/api/user/getCurrentUser |
这里用到了 5555 端口,这是一个网关服务,好吧,既然提到这个,我们接下来看网关吧,引入依赖:
1 | xml复制代码<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
同样利用 Nacos 来发现服务。
这里的注册配置为:
1 | yaml复制代码spring: |
前面用的是 kubernetes。
好了,网关配置好后,启动在 Nacos dashboard 可以看到该服务,表示注册服务成功。接下来就可以利用其来调用其他服务了。具体 curl 命令:
1 | bash复制代码curl -i -H "Accept: application/json" -H "Authorization:bearer f4a42baa-a24a-4342-a00b-32cb135afce9" -X GET http://localhost:5555/consumer-service/api/order/getUserInfo |
Ok,到此鉴权中心、服务提供者、服务消费者、服务的注册与发现、配置中心等功能已完成。
为什么选择 Netty 作为即时通信的技术框架
简介
Netty 是一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持。作为当前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用。
特点
- 高并发
- 传输快
- 封装好
Netty 通信的优势
Netty 是一个高性能、高可拓展性的异步事件驱动的网络应用程序框架,极大地简化了 TCP 和 UDP 客户端和服务器端开发等网络编程,它的四个重要内容:
- 内存管理:增强 ByteBuf 缓冲区
- Reactor 线程模型:一种高性能的多线程程序设计
- 增强版的通道 channel 概念
- ChannelPipeline 责任链设计模式:事件处理机制
Netty 实现了 Reactor 线程模型,Reactor 模型有四个核心概念:Resources 资源(请求/任务)、Synchronous Event Demultiplexer 同步事件复用器、Dispatcher 分配器、Request Handler 请求处理器。主要是通过 2 个 EventLoopGroup(线程组,底层是 JDK 的线程池)来分别处理连接和数据读取,从而提高线程的利用率。
Netty 中的 Channel 是一个抽象的概念,可以理解为对 JDK NIO Channel 的增强和拓展。增加了很多属性和方法。
ChannelPipeline 责任链保存了通道所有处理器信息。创建新 channel 时自动创建一个专有的 pipeline,并且在对应入站事件(通常指 I/O 线程生成了入站数据,详见 ChannelInboundHandler)和出站事件(经常是指 I/O 线程执行实际的输出操作,详见 ChannelOutboundHandler)时调用 pipeline 上的处理器。当入站事件时,执行顺序是 pipeline 的 first 执行到 last。当出站事件时,执行顺序是 pipeline 的 last 执行到 first。处理器在 pipeline 中的顺序由添加的时候决定。
JDK 的 ByteBuffer 存在如无法动态扩容、API 使用复杂的问题,Netty 自己的 ByteBuf 解决了其问题。ByteBuf 实现了四个方面的增强:API 操作便捷,动态扩容,多种 ByteBuf 实现,高效的零拷贝机制。
实现一个简单的 Netty 客户端、服务器通信
实战服务端
前面介绍了 Netty 在音视频流域实践的优势与特点,接下来,我们先写一个服务端。首先创建一个 Java 项目:
创建项目后,我们需要引入基础依赖:
1 | xml复制代码<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
服务启动类:
1 | less复制代码@EnableScheduling |
首先启动 netty 服务时,只需要我们添加 Netty 的配置:
1 | ini复制代码spring.application.name=netty-server |
添加完配置,我们可以启动服务看看,这时候有日志:
添加完 netty 服务配置后,这里需要注入一个 Server Handle,用来当客户端主动链接服务端的链接后,这时候,该处理类会被触发,从而执行一些消息:
1 | ini复制代码@Override |
意思就是说,假如这时候有个客户端连接服务端时,会被打印一些信息,这里是我提前加入客户端后打印的结果:
当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据:
1 | less复制代码@Override |
当然获取数据函数在这里:
1 | ini复制代码@Override |
如果出现异常,抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接**:**
1 | java复制代码@Override |
此外,在服务端,一般需要定义一些信息协议信息,如:连接的信息,是自发信息还是群发信息,通信管道是哪个,还有通信信息等:
1 | typescript复制代码public class ServerMsgProtocol { |
以上,就是一个简单的服务端,梳理一下还是比较清晰的。
实战客户端
接下来,我们看看客户端是如何连接服务端,并且与其通信的呢?客户端要想与服务端通信,首先肯定需要与服务端进行连接,这里加一个配置服务端 NIO 线程组:
1 | ini复制代码private EventLoopGroup workerGroup = new NioEventLoopGroup(); |
连接服务端的逻辑是:
1 | ini复制代码public ChannelFuture connect(String inetHost, int inetPort) { |
接下来再看如何销毁连接:
1 | csharp复制代码public void destroy() { |
最后,我们来连接到服务端:
1 | scss复制代码new NettyClient().connect("127.0.0.1", 9999); |
由于前面我们的服务端的 netty 的 ip 与端口设置为:本地,9999 端口,这里直接配置。
同样的,客户端如果需要接收数据信息,也需要定义如何在管道中进行接收:
1 | scala复制代码public class MyChannelInitializer extends ChannelInitializer<SocketChannel> { |
当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据:
1 | csharp复制代码@Override |
当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据:
1 | java复制代码@Override |
遇到异常时,抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接:
1 | java复制代码@Override |
客户端连接服务端、处理接收服务端发送的信息、异常处理等完成后,这时候,我们来启动客户端,客户端控制面会打印如下信息:
如果客户端主动断开连接时,这时候,服务端会提示:
1 | yaml复制代码远程主机强迫关闭了一个现有的连接。 |
到此,一个简单的 Netty 客户端、服务端的通信就完成了。
微服务 Springboot 下实战聊天系统
在前面介绍了一个简单的 Netty 客户端、服务端通信的示例,接下来,我们开始实战聊天系统。
websocket 服务端启动类
基于前面讲的 Netty 的特性,这里聊天室需要前、后端。那么,首先对于后端,我们需要创建一个 Websocket Server,这里需要有一对线程组 EventLoopGroup,定义完后,需要定义一个 Server:
1 | scss复制代码public static void main(String[] args) throws Exception { |
将线程组加入 Server,接下来,需要设置一个 channel:NioServerSocketChannel,还有一个初始化器:WSServerInitialzer。
第二步,需要对 Server 进行端口版绑定:
1 | ini复制代码ChannelFuture future = server.bind(8088).sync() |
最后,需要对 future 进行监听。而且监听结束后需要对线程资源进行关闭:
1 | ini复制代码mainGroup.shutdownGracefully(); |
websocket 子处理器 initialzer
上面说了 WebSocket Server,那么对于 socket,有一个初始化处理器,这里我们来定义一个:
1 | scala复制代码public class WSServerInitialzer extends ChannelInitializer<SocketChannel> { |
由于 websocket 是基于 http 协议,所以需要有 http 的编解码器 HttpServerCodec,同时,在一些 http 上,有一些数据流的处理,而且,数据流有大有小,那么可以添加一个大数据流的处理:ChunkedWriteHandler。
通常,会有对 httpMessage 进行聚合,聚合成 FullHttpRequest 或 FullHttpResponse,而且,几乎在 netty 中的编程,都会使用到此 hanler。
另外,websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : “/ws”,本 handler 会帮你处理一些繁重的复杂的事,比如,会帮你处理握手动作: handshaking(close, ping, pong) ping + pong = 心跳。对于 websocket 来讲,都是以 frames 进行传输的,不同的数据类型对应的 frames 也不同。
最后,我们自定义了一个处理消息的 handler:ChatHandler。
chatHandler 对消息的处理
在 Netty 中,有一个用于为 websocket 专门处理文本的对象 TextWebSocketFrame,frame 是消息的载体。
1 | java复制代码public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { |
一开始消息在载体 TextWebSocketFrame 中,这时候可以直接拿到其中的内容,并且打印出来。而且可以把消息发到对应请求的客户端。当然,也可以把消息转发给所有的客户端,这就涉及到 Netty 中的 channel。这时候,需要管理 channel 中的用户,这样才能把消息转发到所有 channel 的用户。也就是上面的 handlerAdded 函数,当客户端连接服务端之后打开连接,获取客户端的 channle,并且放到 ChannelGroup 中去进行管理。同时,客户端与服务端断开、关闭连接后,会触发 handlerRemoved 函数,同时 ChannelGroup 会自动移除对应客户端的 channel。
接下来,需要把数据获取后刷新到所有客户端:
1 | less复制代码for (Channel channel : clients) { |
注意:这里需要借助于载体来把信息 Flush,因为 writeAndFlush 函数是需要传对象载体,而不是直接字符串。其实同样,作为 ChannelGroup clients,其本身提供了 writeAndFlush 函数,可以直接输出到所有客户端:
1 | less复制代码clients.writeAndFlush(new TextWebSocketFrame("服务器时间在 " + LocalDateTime.now() + " 接受到消息, 消息为:" + content)); |
基于 js 的 websocket 相关 api 介绍
首先,需要一个客户端与服务端的连接,这个连接桥梁在 js 中就是一个 socket:
1 | ini复制代码var socket = new WebSocket("ws://192.168.174.145:8088/ws"); |
再来看看其生命周期,在后端,channel 有其生命周期,而前端 socket 中:
- onopen(),当客户端与服务端建立连接时,就会触发 onopen 事件
- onmessage(),是在客户端收到消息时,就会触发 onmessage 事件
- onerror(),出现异常时,前端会触发 onerror 事件
- onclose(),客户端与服务端连接关闭后,就会触发 onclose 事件
接下来看看两个主动的方法:
- Socket.send(),在前端主动获取内容后,通过 send 进行消息发送
- Socket.close(),当用户触发某个按钮,就会断开客户端与服务端的连接
以上就是对于前端 websocket js 相对应的 api。
实现前端 websocket
上面介绍了后端对于消息的处理、编解码等,又介绍了 websocket js 的相关。接下来,我们看看前端如何实现 websocket,首先我们先写一个文本输入、点击等功能:
1 | xml复制代码<html> |
访问连接:C:\Users\damon\Desktop\netty\WebChat\index.html,我们可以看到效果:
接下来,我们需要写 websocket js:
1 | ini复制代码<script type="application/javascript"> |
这样,一个简单的 websocket js 就写完了,接下来,我们来演示下。
打开网页,访问 index 页面,我们可以看到连接 websocket 失败,而且会打印发生错误、连接关闭信息,这是因为连接失败时,触发 onerror 事件、onclose 事件:
接下来,我们先启动后端 WSServer,同时,刷新页面,可以看到页面显示:连接成功。控制台信息:
这里由于我打开了两个页面,所以可以看到后端控制台有打印两次客户端连接的信息,分别对应不同的客户端。接下来,我们输入:Hi,Damon
发送后,我们可以看到页面上输出信息:“服务器时间在 2021-05-17T20:05:22.802 接受到消息, 消息为:Hi,Damon”。同时,在另一个客户端窗口,也可以看到输出信息:
这是因为后端接收到第一个客户端的请求信息后,将信息转发给所有客户端。接下来,如果我们关闭第一个客户端窗口,则后端会监听到,并且输出:
同样,如果我新开一个客户端,并且输入信息,也会被转发到其它客户端:
同时,后端控制台会打印对应的请求信息:
最后,如果我们主要关闭后端服务,此时,所有的客户端都会失去 socket 连接,会提示:
后端整合 Springboot 实现聊天系统
前面介绍了 Websocket 后端处理以及前端的实现逻辑,最后,我们结合 Springboot,来看看后端逻辑的实现。
首先,我们进入依赖 pom:
1 | xml复制代码<parent> |
这里主要依赖 Springboot 较高版本 2.3.10.RELEASE,同时,加入了 netty 的依赖,以及数据库 mybatis、fastdfs 等分布式文件服务的依赖。
接下来,我们看看启动类:
1 | less复制代码@SpringBootApplication |
在启动类中,我们看到依据 Springboot 来注入注解,并且,我们扫描注入有些启动 bean。接下来,我们再看看如何引入 Netty 服务端启动:
1 | typescript复制代码@Component |
这里主要通过注解@Component
注入一个监听器,同时是在主服务启动的时候来启动 Netty 服务。那么 Netty 的服务实际逻辑在前面也讲过了:
1 | csharp复制代码@Component |
对于线程组来讲,当客户端与从线程组进行通信后,从线程组会对对应的 Channel 进行处理。同时,每一个 Channel 都是有初始化器,所以这里有 childHandler 函数。channelHandler 的处理器会进行处理 Http、Websocket 等各种协议的请求的支持。
1 | scala复制代码public class WSServerInitialzer extends ChannelInitializer<SocketChannel> { |
到此,所有的后端的技术部分就都讲完了。
本文转载自: 掘金