音视频技术为什么须要微服务
微服务,英文名: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 地址:https://github.com/alibaba/nacos/releases 咱们下载对应零碎的二进制文件后,对应本人的零碎,执行如下命令:
Linux/Unix/Mac:sh startup.sh -m standalone
Windows:cmd startup.cmd -m standalone
启动实现之后,拜访:http://127.0.0.1:8848/nacos/,能够进入 Nacos 的服务治理页面,具体如下:
默认用户名与明码都是 nacos。
登陆后关上服务治理,能够看到注册到 Nacos 的服务列表:
能够点击配置管理,查看配置:
如果没有配置任何服务的配置,能够新建:
下面讲述了 Nacos 如何作为注册核心与配置核心的,很简略吧。
第一个微服务
接下来,对于微服务,那须要有一个服务被注册与被发现,咱们解说服务提供者代码:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.damon</groupId>
<artifactId>provider-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>provider-service</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<swagger.version>2.6.1</swagger.version>
<xstream.version>1.4.7</xstream.version>
<pageHelper.version>4.1.6</pageHelper.version>
<fastjson.version>1.2.51</fastjson.version>
<!-- <springcloud.version>2.1.8.RELEASE</springcloud.version> -->
<springcloud.version>Greenwich.SR3</springcloud.version>
<springcloud.kubernetes.version>1.1.1.RELEASE</springcloud.kubernetes.version>
<mysql.version>5.1.46</mysql.version>
<alibaba-cloud.version>2.1.1.RELEASE</alibaba-cloud.version>
<springcloud.alibaba.version>0.9.0.RELEASE</springcloud.alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${alibaba-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency> -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${springcloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${springcloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<!-- 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>${pageHelper.version}</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- datasource pool-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.3</version>
</dependency>
<!-- 对 redis 反对, 引入的话我的项目缓存就反对 redis 了, 所以必须加上 redis 的相干配置, 否则操作相干缓存会报异样 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
<fork>true</fork>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.8</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 主动生成代码 插件 begin -->
<!-- <plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
</plugin> -->
</plugins>
</build>
</project>
判若两人的引入依赖,配置 bootstrap 文件:
management:
endpoint:
restart:
enabled: true
health:
enabled: true
info:
enabled: true
spring:
application:
name: provider-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
refreshable-dataids: actuator.properties,log.properties
http:
encoding:
charset: UTF-8
enabled: true
force: true
mvc:
throw-exception-if-no-handler-found: true
main:
allow-bean-definition-overriding: true #当遇到同样名称时,是否容许笼罩注册
logging:
path: /data/${spring.application.name}/logs
cas-server-url: http://oauth-cas #http://localhost:2000# 设置能够拜访的地址
security:
oauth2: #与 cas 对应的配置
client:
client-id: provider-service
client-secret: provider-service-123
user-authorization-uri: ${cas-server-url}/oauth/authorize #是受权码认证形式须要的
access-token-uri: ${cas-server-url}/oauth/token #是明码模式须要用到的获取 token 的接口
resource:
loadBalanced: true
#jwt: #jwt 存储 token 时开启
#key-uri: ${cas-server-url}/oauth/token_key
#key-value: test_jwt_sign_key
id: provider-service
#指定用户信息地址
user-info-uri: ${cas-server-url}/api/user #指定 user info 的 URI,原生地址后缀为 /auth/user
prefer-token-info: false
#token-info-uri:
authorization:
check-token-access: ${cas-server-url}/oauth/check_token #当此 web 服务端接管到来自 UI 客户端的申请后,须要拿着申请中的 token 到认证服务端做 token 验证,就是申请的这个接口
application 文件;server:
port: 2001
undertow:
accesslog:
enabled: false
pattern: combined
servlet:
session:
timeout: PT120M
cookie:
name: PROVIDER-SERVICE-SESSIONID #避免 Cookie 抵触,抵触会导致登录验证不通过
client:
http:
request:
connectTimeout: 8000
readTimeout: 30000
mybatis:
mapperLocations: classpath:mapper/*.xml
typeAliasesPackage: com.damon.*.model
backend:
ribbon:
client:
enabled: true
ServerListRefreshInterval: 5000
ribbon:
ConnectTimeout: 3000
# 设置全局默认的 ribbon 的读超时
ReadTimeout: 1000
eager-load:
enabled: true
clients: oauth-cas,consumer-service
MaxAutoRetries: 1 #对第一次申请的服务的重试次数
MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包含第一个服务)#listOfServers: localhost:5556,localhost:5557
#ServerListRefreshInterval: 2000
OkToRetryOnAllOperations: true
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
hystrix.command.BackendCall.execution.isolation.thread.timeoutInMilliseconds: 5000
hystrix.threadpool.BackendCallThread.coreSize: 5
接下来启动类:
package com.damon;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author Damon
* @date 2020 年 1 月 13 日 下午 3:23:06
*
*/
@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackages = {"com.damon"})
@EnableDiscoveryClient
@EnableOAuth2Sso
public class ProviderApp {public static void main(String[] args) {SpringApplication.run(ProviderApp.class, args);
}
}
留神:注解 @EnableDiscoveryClient、@EnableOAuth2Sso 都须要。
这时,同样须要配置 ResourceServerConfig、SecurityConfig。
如果须要数据库,能够加上:
package com.damon.config;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.github.pagehelper.PageHelper;
/**
*
*
* created by Damon
* 2018 年 5 月 23 日 下午 7:39:37
*
*/
@Component
@Configuration
@EnableTransactionManagement
@MapperScan("com.damon.*.dao")
public class MybaitsConfig {
@Autowired
private EnvConfig envConfig;
@Autowired
private Environment env;
@Bean(name = "dataSource")
public DataSource getDataSource() throws Exception {Properties props = new Properties();
props.put("driverClassName", envConfig.getJdbc_driverClassName());
props.put("url", envConfig.getJdbc_url());
props.put("username", envConfig.getJdbc_username());
props.put("password", envConfig.getJdbc_password());
return DruidDataSourceFactory.createDataSource(props);
}
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
// 指定数据源(这个必须有,否则报错)
fb.setDataSource(dataSource);
// 下边两句仅仅用于 *.xml 文件,如果整个长久层操作不须要应用到 xml 文件的话(只用注解就能够搞定),则不加
fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包
fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations")));// 指定 xml 文件地位
// 分页插件
PageHelper pageHelper = new PageHelper();
Properties props = new Properties();
// 启用合理化时,如果 pageNum<1 会查问第一页,如果 pageNum>pages 会查问最初一页
// 禁用合理化时,如果 pageNum<1 或 pageNum>pages 会返回空数据
props.setProperty("reasonable", "true");
// 指定数据库
props.setProperty("dialect", "mysql");
// 反对通过 Mapper 接口参数来传递分页参数
props.setProperty("supportMethodsArguments", "true");
// 总是返回 PageInfo 类型,check 查看返回类型是否为 PageInfo,none 返回 Page
props.setProperty("returnPageInfo", "check");
props.setProperty("params", "count=countSql");
pageHelper.setProperties(props);
// 增加插件
fb.setPlugins(new Interceptor[] {pageHelper});
try {return fb.getObject();
} catch (Exception e) {throw e;}
}
/**
* 配置事务管理器
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) throws Exception {return new DataSourceTransactionManager(dataSource);
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory);
}
}
接下来新写一个 controller 类:
package com.damon.user.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.damon.commons.Response;
import com.damon.user.service.UserService;
/**
*
*
* @author Damon
* @date 2020 年 1 月 13 日 下午 3:31:07
*
*/
@RestController
@RequestMapping("/api/user")
public class UserController {private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserService userService;
@GetMapping("/getCurrentUser")
@PreAuthorize("hasAuthority('admin')")
public Object getCurrentUser(Authentication authentication) {logger.info("test password mode");
return authentication;
}
@PreAuthorize("hasAuthority('admin')")
@GetMapping("/auth/admin")
public Object adminAuth() {logger.info("test password mode");
return "Has admin auth!";
}
@GetMapping(value = "/get")
@PreAuthorize("hasAuthority('admin')")
//@PreAuthorize("hasRole('admin')")// 有效
public Object get(Authentication authentication){//Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
authentication.getCredentials();
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
String token = details.getTokenValue();
return token;
}
@GetMapping("/getUserInfo")
@PreAuthorize("hasAuthority('admin')")
public Response<Object> getUserInfo(Authentication authentication) {logger.info("test password mode");
Object principal = authentication.getPrincipal();
if(principal instanceof String) {String username = (String) principal;
return userService.getUserByUsername(username);
}
return null;
}
}
基本上一个代码就实现了。接下来测试一下:
认证:
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 后:
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 端口,这是一个网关服务,好吧,既然提到这个,咱们接下来看网关吧,引入依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.damon</groupId>
<artifactId>alibaba-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>alibaba-gateway</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<swagger.version>2.6.1</swagger.version>
<xstream.version>1.4.7</xstream.version>
<pageHelper.version>4.1.6</pageHelper.version>
<fastjson.version>1.2.51</fastjson.version>
<!-- <springcloud.version>2.1.8.RELEASE</springcloud.version> -->
<springcloud.version>Greenwich.SR3</springcloud.version>
<springcloud.kubernetes.version>1.1.1.RELEASE</springcloud.kubernetes.version>
<mysql.version>5.1.46</mysql.version>
<alibaba-cloud.version>2.1.1.RELEASE</alibaba-cloud.version>
<springcloud.alibaba.version>0.9.0.RELEASE</springcloud.alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${alibaba-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency> -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${springcloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${springcloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 不要依赖 spring-boot-starter-web,会和 spring-cloud-starter-gateway 抵触,启动时异样 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 基于 reactive stream 的 redis -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency> -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
<fork>true</fork>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.8</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 主动生成代码 插件 begin -->
<!-- <plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
</plugin> -->
</plugins>
</build>
</project>
同样利用 Nacos 来发现服务。
这里的注册配置为:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #并且咱们并没有给每一个服务独自配置路由 而是应用了服务发现主动注册路由的形式
lowerCaseServiceId: true
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
refreshable-dataids: actuator.properties,log.properties
后面用的是 kubernetes。
好了,网关配置好后,启动在 Nacos dashboard 能够看到该服务,示意注册服务胜利。接下来就能够利用其来调用其余服务了。具体 curl 命令:
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 我的项目:
创立我的项目后,咱们须要引入根底依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.damon</groupId>
<artifactId>netty-client-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>netty-client-service</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.1.1.RELEASE</spring-boot.version>
<springcloud.kubernetes.version>1.0.1.RELEASE</springcloud.kubernetes.version>
<springcloud.version>2.1.1.RELEASE</springcloud.version>
<swagger.version>2.6.1</swagger.version>
<fastjson.version>1.2.51</fastjson.version>
<pageHelper.version>4.1.6</pageHelper.version>
<protostuff.version>1.0.10</protostuff.version>
<objenesis.version>2.4</objenesis.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<type>pom</type>
<scope>import</scope>
<version>${spring-boot.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-core</artifactId>
<version>${springcloud.kubernetes.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-kubernetes-discovery</artifactId>
<version>${springcloud.kubernetes.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
<version>${springcloud.kubernetes.version}</version>
</dependency> -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
<version>${springcloud.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.3</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<!-- mybatis -->
<!-- <dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency> -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- <dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.4.1</version>
</dependency> -->
<!-- <dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>opencv-platform</artifactId>
<version>3.4.1-1.4.1</version>
</dependency> -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.64.Final</version>
</dependency>
<!-- protobuf -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>com.googlecode.protobuf-java-format</groupId>
<artifactId>protobuf-java-format</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>${protostuff.version}</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>${protostuff.version}</version>
</dependency>
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>${objenesis.version}</version>
</dependency>
</dependencies>
</project>
服务启动类:
@EnableScheduling
@SpringBootApplication(scanBasePackages = { "com.damon"})
public class StorageServer {public static void main(String[] args) {SpringApplication.run(StorageServer.class, args);
}
}
首先启动 netty 服务时,只须要咱们增加 Netty 的配置:
spring.application.name=netty-server
server.port=2002
netty.host=127.0.0.1
netty.port=9999
logging.path=/data/${spring.application.name}/logs
spring.profiles.active=dev
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.mvc.throw-exception-if-no-handler-found=true
server.undertow.accesslog.enabled=false
server.undertow.accesslog.pattern=combined
client.http.request.readTimeout=30000
client.http.request.connectTimeout=8000
增加完配置,咱们能够启动服务看看,这时候有日志:
增加完 netty 服务配置后,这里须要注入一个 Server Handle,用来当客户端被动链接服务端的链接后,这时候,该解决类会被触发,从而执行一些音讯:
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {SocketChannel channel = (SocketChannel) ctx.channel();
logger.info("链接报告开始");
logger.info("链接报告信息:有一客户端链接到本服务端");
logger.info("链接报告 IP:{}", channel.localAddress().getHostString());
logger.info("链接报告 Port:{}", channel.localAddress().getPort());
logger.info("链接报告结束");
ChannelHandler.channelGroup.add(ctx.channel());
// 告诉客户端链接建设胜利
String str = "告诉客户端链接建设胜利" + "" + new Date() +" "+ channel.localAddress().getHostString() +"\r\n";
ByteBuf buf = Unpooled.buffer(str.getBytes().length);
buf.writeBytes(str.getBytes("GBK"));
ctx.writeAndFlush(buf);
}
意思就是说,如果这时候有个客户端连贯服务端时,会被打印一些信息,这里是我提前退出客户端后打印的后果:
当客户端被动断开服务端的链接后,这个通道就是不沉闷的。也就是说客户端与服务端的敞开了通信通道并且不能够传输数据:
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {logger.info("客户端断开链接{}", ctx.channel().localAddress().toString());
ChannelHandler.channelGroup.remove(ctx.channel());
}
当然获取数据函数在这里:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws UnsupportedEncodingException {if(msg instanceof ByteBuf) {ByteBuf buf = (ByteBuf) msg;
byte[] msgByte = new byte[buf.readableBytes()];
buf.readBytes(msgByte);
System.out.println(new String(msgByte, Charset.forName("GBK")));
// 告诉客户端链音讯发送胜利
String str = "服务端收到:" + new Date() + "" + new String(msgByte, Charset.forName("GBK")) +"\r\n";
ByteBuf buf2 = Unpooled.buffer(str.getBytes().length);
buf2.writeBytes(str.getBytes("GBK"));
ctx.writeAndFlush(buf2);
}
}
如果出现异常,抓住异样,当产生异样的时候,能够做一些相应的解决,比方打印日志、敞开链接:
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {ctx.close();
logger.info("异样信息:\r\n" + cause.getMessage());
}
此外,在服务端,个别须要定义一些信息协定信息,如:连贯的信息,是自发信息还是群发信息,通信管道是哪个,还有通信信息等:
public class ServerMsgProtocol {
private int type; // 链接信息;1 自发信息、2 群发音讯
private String channelId; // 通信管道 ID,理论应用中会映射成用户名
private String userHeadImg; // 用户头像[模仿调配]
private String msgInfo; // 通信音讯
public int getType() {return type;}
public void setType(int type) {this.type = type;}
public String getChannelId() {return channelId;}
public void setChannelId(String channelId) {this.channelId = channelId;}
public String getUserHeadImg() {return userHeadImg;}
public void setUserHeadImg(String userHeadImg) {this.userHeadImg = userHeadImg;}
public String getMsgInfo() {return msgInfo;}
public void setMsgInfo(String msgInfo) {this.msgInfo = msgInfo;}
}
以上,就是一个简略的服务端,梳理一下还是比拟清晰的。
实战客户端
接下来,咱们看看客户端是如何连贯服务端,并且与其通信的呢?客户端要想与服务端通信,首先必定须要与服务端进行连贯,这里加一个配置服务端 NIO 线程组:
private EventLoopGroup workerGroup = new NioEventLoopGroup();
private Channel channel;
连贯服务端的逻辑是:
public ChannelFuture connect(String inetHost, int inetPort) {
ChannelFuture channelFuture = null;
try {Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.AUTO_READ, true);
b.handler(new MyChannelInitializer());
channelFuture = b.connect(inetHost, inetPort).syncUninterruptibly();
this.channel = channelFuture.channel();
channel.closeFuture();} catch (Exception e) {e.printStackTrace();
} finally {if (null != channelFuture && channelFuture.isSuccess()) {System.out.println("demo-netty client start done.");
} else {System.out.println("demo-netty client start error.");
}
}
return channelFuture;
}
接下来再看如何销毁连贯:
public void destroy() {if (null == channel) return;
channel.close();
workerGroup.shutdownGracefully();}
最初,咱们来连贯到服务端:
new NettyClient().connect("127.0.0.1", 9999);
因为后面咱们的服务端的 netty 的 ip 与端口设置为:本地,9999 端口,这里间接配置。
同样的,客户端如果须要接收数据信息,也须要定义如何在管道中进行接管:
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
// 在管道中增加咱们本人的接收数据实现办法
channel.pipeline().addLast(new MyClientHandler());
}
}
当客户端被动链接服务端的链接后,这个通道就是沉闷的了。也就是客户端与服务端建设了通信通道并且能够传输数据:
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {SocketChannel channel = (SocketChannel) ctx.channel();
System.out.println("链接报告开始");
System.out.println("链接报告信息:本客户端链接到服务端。channelId:" + channel.id());
System.out.println("链接报告 IP:" + channel.localAddress().getHostString());
System.out.println("链接报告 Port:" + channel.localAddress().getPort());
System.out.println("链接报告结束");
}
当客户端被动断开服务端的链接后,这个通道就是不沉闷的。也就是说客户端与服务端的敞开了通信通道并且不能够传输数据:
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {System.out.println("断开链接" + ctx.channel().localAddress().toString());
super.channelInactive(ctx);
}
遇到异样时,抓住异样,当产生异样的时候,能够做一些相应的解决,比方打印日志、敞开链接:
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {ctx.close();
System.out.println("异样信息:\r\n" + cause.getMessage());
}
客户端连贯服务端、解决接管服务端发送的信息、异样解决等实现后,这时候,咱们来启动客户端,客户端管制面会打印如下信息:
如果客户端被动断开连接时,这时候,服务端会提醒:
近程主机强制敞开了一个现有的连贯。2021-05-13 19:33:35.691 INFO 148736 --- [ntLoopGroup-3-2] com.leinao.handler.ServerHandler : 客户端断开链接 /127.0.0.1:9999
到此,一个简略的 Netty 客户端、服务端的通信就实现了。
微服务 Springboot 下实战聊天零碎
在后面介绍了一个简略的 Netty 客户端、服务端通信的示例,接下来,咱们开始实战聊天零碎。
websocket 服务端启动类
基于后面讲的 Netty 的个性,这里聊天室须要前、后端。那么,首先对于后端,咱们须要创立一个 Websocket Server,这里须要有一对线程组 EventLoopGroup,定义完后,须要定义一个 Server:
public static void main(String[] args) throws Exception {EventLoopGroup mainGroup = new NioEventLoopGroup();
EventLoopGroup subGroup = new NioEventLoopGroup();
try {ServerBootstrap server = new ServerBootstrap();
server.group(mainGroup, subGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WSServerInitialzer());
ChannelFuture future = server.bind(8088).sync();
future.channel().closeFuture().sync();} finally {mainGroup.shutdownGracefully();
subGroup.shutdownGracefully();}
}
将线程组退出 Server,接下来,须要设置一个 channel:NioServerSocketChannel,还有一个初始化器:WSServerInitialzer。
第二步,须要对 Server 进行端口版绑定:
ChannelFuture future = server.bind(8088).sync()
最初,须要对 future 进行监听。而且监听完结后须要对线程资源进行敞开:
mainGroup.shutdownGracefully();
subGroup.shutdownGracefully();
websocket 子处理器 initialzer
下面说了 WebSocket Server,那么对于 socket,有一个初始化处理器,这里咱们来定义一个:
public class WSServerInitialzer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(1024*64));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new ChatHandler());
}
}
因为 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 是音讯的载体。
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg)
throws Exception {String content = msg.text();
System.out.println("承受到的数据:" + content);
clients.writeAndFlush(new TextWebSocketFrame("服务器工夫在" + LocalDateTime.now() + "承受到音讯, 音讯为:" + content));
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {clients.add(ctx.channel());
System.out.println("客户端连贯,channle 对应的长 id 为:" + ctx.channel().id().asLongText());
System.out.println("客户端连贯,channle 对应的短 id 为:" + ctx.channel().id().asShortText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {System.out.println("客户端断开,channle 对应的长 id 为:" + ctx.channel().id().asLongText());
System.out.println("客户端断开,channle 对应的短 id 为:" + ctx.channel().id().asShortText());
}
}
一开始音讯在载体 TextWebSocketFrame 中,这时候能够间接拿到其中的内容,并且打印进去。而且能够把音讯发到对应申请的客户端。当然,也能够把音讯转发给所有的客户端,这就波及到 Netty 中的 channel。这时候,须要治理 channel 中的用户,这样能力把音讯转发到所有 channel 的用户。也就是下面的 handlerAdded 函数,当客户端连贯服务端之后关上连贯,获取客户端的 channle,并且放到 ChannelGroup 中去进行治理。同时,客户端与服务端断开、敞开连贯后,会触发 handlerRemoved 函数,同时 ChannelGroup 会主动移除对应客户端的 channel。
接下来,须要把数据获取后刷新到所有客户端:
for (Channel channel : clients) {channel.writeAndFlush(new TextWebSocketFrame("[服务器在]" + LocalDateTime.now() + "承受到音讯, 音讯为:" + content));
}
留神:这里须要借助于载体来把信息 Flush,因为 writeAndFlush 函数是须要传对象载体,而不是间接字符串。其实同样,作为 ChannelGroup clients,其自身提供了 writeAndFlush 函数,能够间接输入到所有客户端:
clients.writeAndFlush(new TextWebSocketFrame("服务器工夫在" + LocalDateTime.now() + "承受到音讯, 音讯为:" + content));
基于 js 的 websocket 相干 api 介绍
首先,须要一个客户端与服务端的连贯,这个连贯桥梁在 js 中就是一个 socket:
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,首先咱们先写一个文本输出、点击等性能:
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<div>send msg:</div>
<input type="text" id="msgContent"/>
<input type="button" value="send" onclick="CHAT.chat()"/>
<div>receive msg:</div>
<div id="receiveMsg" style="background-color: gainsboro;"></div>
</body>
</html>
拜访连贯:C:\Users\damon\Desktop\netty\WebChat\index.html,咱们能够看到成果:
接下来,咱们须要写 websocket js:
<script type="application/javascript">
window.CHAT = {
socket: null,
init: function() {if (window.WebSocket) {CHAT.socket = new WebSocket("ws://192.168.174.145:8088/ws");
CHAT.socket.onopen = function() {console.log("连贯建设胜利...");
},
CHAT.socket.onclose = function() {console.log("连贯敞开...");
},
CHAT.socket.onerror = function() {console.log("产生谬误...");
},
CHAT.socket.onmessage = function(e) {console.log("承受到音讯:" + e.data);
var receiveMsg = document.getElementById("receiveMsg");
var html = receiveMsg.innerHTML;
receiveMsg.innerHTML = html + "<br/>" + e.data;
}
} else {alert("浏览器不反对 websocket 协定...");
}
},
chat: function() {var msg = document.getElementById("msgContent");
CHAT.socket.send(msg.value);
}
};
CHAT.init();
</script>
这样,一个简略的 websocket js 就写完了,接下来,咱们来演示下。
关上网页,拜访 index 页面,咱们能够看到连贯 websocket 失败,而且会打印产生谬误、连贯敞开信息,这是因为连贯失败时,触发 onerror 事件、onclose 事件:
接下来,咱们先启动后端 WSServer,同时,刷新页面,能够看到页面显示:连贯胜利。控制台信息:
这里因为我关上了两个页面,所以能够看到后端控制台有打印两次客户端连贯的信息,别离对应不同的客户端。接下来,咱们输出:Hi,Damon
发送后,咱们能够看到页面上输入信息:“服务器工夫在 2021-05-17T20:05:22.802 承受到音讯, 音讯为:Hi,Damon”。同时,在另一个客户端窗口,也能够看到输入信息:
这是因为后端接管到第一个客户端的申请信息后,将信息转发给所有客户端。接下来,如果咱们敞开第一个客户端窗口,则后端会监听到,并且输入:
同样,如果我新开一个客户端,并且输出信息,也会被转发到其它客户端:
同时,后端控制台会打印对应的申请信息:
最初,如果咱们次要敞开后端服务,此时,所有的客户端都会失去 socket 连贯,会提醒:
后端整合 Springboot 实现聊天零碎
后面介绍了 Websocket 后端解决以及前端的实现逻辑,最初,咱们联合 Springboot,来看看后端逻辑的实现。
首先,咱们进入依赖 pom:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.10.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.64.Final</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!--mapper -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
<!--pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
<!-- 高性能分布式文件服务器 -->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<!-- 二维码 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.3</version>
</dependency>
这里次要依赖 Springboot 较高版本 2.3.10.RELEASE,同时,退出了 netty 的依赖,以及数据库 mybatis、fastdfs 等分布式文件服务的依赖。
接下来,咱们看看启动类:
@SpringBootApplication
// 扫描 mybatis mapper 包门路
@MapperScan(basePackages="com.damon.mapper")
// 扫描 所有须要的包, 蕴含一些自用的工具类包 所在的门路
@ComponentScan(basePackages= {"com.damon", "org.n3r.idworker"})
public class Application {
@Bean
public SpringUtil getSpingUtil() {return new SpringUtil();
}
public static void main(String[] args) {SpringApplication.run(Application.class, args);
}
}
在启动类中,咱们看到根据 Springboot 来注入注解,并且,咱们扫描注入有些启动 bean。接下来,咱们再看看如何引入 Netty 服务端启动:
@Component
public class NettyBooter implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {if (event.getApplicationContext().getParent() == null) {
try {WSServer.getInstance().start();} catch (Exception e) {e.printStackTrace();
}
}
}
}
这里次要通过注解 @Component
注入一个监听器,同时是在主服务启动的时候来启动 Netty 服务。那么 Netty 的服务理论逻辑在后面也讲过了:
@Component
public class WSServer {
private static class SingletionWSServer {static final WSServer instance = new WSServer();
}
public static WSServer getInstance() {return SingletionWSServer.instance;}
private EventLoopGroup mainGroup;
private EventLoopGroup subGroup;
private ServerBootstrap server;
private ChannelFuture future;
public WSServer() {mainGroup = new NioEventLoopGroup();
subGroup = new NioEventLoopGroup();
server = new ServerBootstrap();
server.group(mainGroup, subGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WSServerInitialzer());
}
public void start() {this.future = server.bind(8088);
System.err.println("netty websocket server start over");
}
}
对于线程组来讲,当客户端与从线程组进行通信后,从线程组会对对应的 Channel 进行解决。同时,每一个 Channel 都是有初始化器,所以这里有 childHandler 函数。channelHandler 的处理器会进行解决 Http、Websocket 等各种协定的申请的反对。
public class WSServerInitialzer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();
// websocket 基于 http 协定,所以要有 http 编解码器
pipeline.addLast(new HttpServerCodec());
// 对写大数据流的反对
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(1024*64));
pipeline.addLast(new IdleStateHandler(8, 10, 12));
// 自定义的闲暇状态检测
pipeline.addLast(new HeartBeatHandler());
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// 自定义的 handler
pipeline.addLast(new ChatHandler());
}
}
到此,所有的后端的技术局部就都讲完了。