2024年8月

.NET 8 Moq mock GetRequiredKeyedService Setup报错

项目代码里有地方用到
IServiceProvider.GetRequiredKeyedService<T>
来解析服务,在写单元测试时需要Mock它,本以为像下面这样写就可以了:

var serviceProvider = new Mock<IServiceProvider>();
serviceProvider.Setup(x => x.GetRequiredKeyedService<AAA>(It.IsAny<BBB>())).Returns(new CCC());

没想到报错了:

  Test method threw exception: 
  System.NotSupportedException: Unsupported expression: x => x.GetRequiredKeyedService(It.IsAny<Type>(), It.IsAny<object>())
  Extension methods (here: ServiceProviderKeyedServiceExtensions.GetRequiredKeyedService) may not be used in setup / verification expressions.

Stack Trace: 
  Guard.IsOverridable(MethodInfo method, Expression expression) line 87
  MethodExpectation.ctor(LambdaExpression expression, MethodInfo method, IReadOnlyList`1 arguments, Boolean exactGenericTypeArguments, Boolean skipMatcherInitialization, Boolean allowNonOverridable) line 236
  ExpressionExtensions.<Split>g__Split|5_0(Expression e, Expression& r, MethodExpectation& p, Boolean assignment, Boolean allowNonOverridableLastProperty) line 256
  ExpressionExtensions.Split(LambdaExpression expression, Boolean allowNonOverridableLastProperty) line 170
  Mock.SetupRecursive[TSetup](Mock mock, LambdaExpression expression, Func`4 setupLast, Boolean allowNonOverridableLastProperty) line 728
  Mock.Setup(Mock mock, LambdaExpression expression, Condition condition) line 562
  Mock`1.Setup[TResult](Expression`1 expression) line 645

有点奇怪,难道
GetRequiredKeyedService
不是接口方法?查看.NET源代码,果然,
GetRequiredKeyedService
是IServiceProvider的扩展方法,而我们知道Moq是不支持Setup扩展方法的。

/// <summary>
/// Get service of type <typeparamref name="T"/> from the <see cref="IServiceProvider"/>.
/// </summary>
/// <typeparam name="T">The type of service object to get.</typeparam>
/// <param name="provider">The <see cref="IServiceProvider"/> to retrieve the service object from.</param>
/// <param name="serviceKey">An object that specifies the key of service object to get.</param>
/// <returns>A service object of type <typeparamref name="T"/>.</returns>
/// <exception cref="System.InvalidOperationException">There is no service of type <typeparamref name="T"/>.</exception>
public static T GetRequiredKeyedService<T>(this IServiceProvider provider, object? serviceKey) where T : notnull
{
    ThrowHelper.ThrowIfNull(provider);

    return (T)provider.GetRequiredKeyedService(typeof(T), serviceKey);
}

原因找到就好办了,翻看源码,一步步找到IServiceProvider.GetRequiredKeyedService
最终调用的接口方法,然后再mock即可。

首先看下requiredServiceSupportingProvider.GetRequiredKeyedService(serviceType, serviceKey)调的是什么方法

  /// <summary>
  /// IKeyedServiceProvider is a service provider that can be used to retrieve services using a key in addition
  /// to a type.
  /// </summary>
  public interface IKeyedServiceProvider : IServiceProvider
  {
      /// <summary>
      /// Gets the service object of the specified type.
      /// </summary>
      /// <param name="serviceType">An object that specifies the type of service object to get.</param>
      /// <param name="serviceKey">An object that specifies the key of service object to get.</param>
      /// <returns> A service object of type serviceType. -or- null if there is no service object of type serviceType.</returns>
      object? GetKeyedService(Type serviceType, object? serviceKey);

      /// <summary>
      /// Gets service of type <paramref name="serviceType"/> from the <see cref="IServiceProvider"/> implementing
      /// this interface.
      /// </summary>
      /// <param name="serviceType">An object that specifies the type of service object to get.</param>
      /// <param name="serviceKey">The <see cref="ServiceDescriptor.ServiceKey"/> of the service.</param>
      /// <returns>A service object of type <paramref name="serviceType"/>.
      /// Throws an exception if the <see cref="IServiceProvider"/> cannot create the object.</returns>
      object GetRequiredKeyedService(Type serviceType, object? serviceKey);
  }
  

可以看到IKeyedServiceProvider也是继承了IServiceProvider接口,这就更好办了,我们直接Mock IKeyedServiceProvider再Setup即可,将用到IServiceProvider的地方,换成IKeyedServiceProvider。

代码如下:

var serviceProvider = new Mock<IKeyedServiceProvider>();
serviceProvider.Setup(x => x.GetRequiredKeyedService(It.IsAny<AAA>(), It.IsAny<BBB>())).Returns(new CCC());

运行测试,完美。

总结

解决这个问题并不困难,但是如果.Net不开源,看不到源代码,还是有点头疼。

注意:请确保已经安装Redis和keepalived,本文不在介绍如何安装。

1、使用版本说明

Redis版本:5.0.2

Keepalived版本:1.3.5

Linux 版本:Centos7.9

查看Redis版本:

/usr/local/redis/bin/redis-cli -v

查看Keepalived版本信息:

rpm -qa|grep keepalived 或者 keepalived -v

2、功能实现说明:

  • 使用Keepalived提供虚拟IP对外访问Redis
  • Redis搭建主从数据同步,主用来读写数据 、从主要进行主数据同步备份。
  • 当主出现宕机,Keepalived虚拟IP自动指向从服务器。从服务器临时变为主服务器继续工作。
  • 待主服务器重新启动后,Keepalived虚拟IP重新指向主服务器。主服务器同步从服务器数据后继续工作。从服务器由临时主变为从继续进行主数据同步备份。

3、说明图

Keepalived会生成一个虚拟IP。客户端需要访问虚拟IP进行Redis连接:

3.1 、主和备服务器运行中

3.2、主宕机,备服务器运行中

3.3、 主恢复,备服务器运行中

4、搭建Redis主从

首先确保两台服务器都安装了Redis服务,Redis的端口号和密码两台服务器必须保持一致。我这里两台服务器都是使用端口号:6379和密码:1234qwer

服务器IP:

主服务器:192.168.42.130

备服务器:192.168.42.133

4.1、 修改配置文件

首先需要修改备服务器redis配置文件,把备服务器redis挂载到主服务器redis下面实现主从配置。

进入redis目录

cd /usr/local/redis/

修改redis.conf文件

vim redis.conf

找到replicaof和masterauth属性进行配置

# replicaof <masterip> <masterport>
replicaof 192.168.42.130 6379
# If the master is password protected (using the "requirepass" configuration
# directive below) it is possible to tell the replica to authenticate before
# starting the replication synchronization process, otherwise the master will
# refuse the replica request.
#
# masterauth <master-password>
masterauth "1234qwer"

replicaof:主服务器IP和端口号

masterauth:用于在进行主从复制时,保护Redis主节点的数据安全。密码就是设置相同的主从节点的密码。以确保只有经过授权的从节点才能够连接到主节点。如果不配置,会导致节点连接失败:
master_link_status:down

通过登录redis输入:
info replication
可以查看Redis集群配置信息。如果搭建成功,会显示节点信息。

注意:
在Redis 5.0及以上版本,SLAVEOF 命令已经被废弃,并且在服务器上使用该命令会导致命令失效。所以在Redis 5.0及以上版本,设置复制的正确方法是使用 REPLICAOF 命令。为了兼容旧版本,通过配置的方式仍然支持 slaveof,但是通过命令的方式则不行了。

4.2、验证主从复制功能

验证方式:登录主服务器Redis,插入一条key数据。在备服务器中登录Redis进行通过该key进行查询,查看是否获取到数据。如果数据获取成功,说明Redis主从复制搭建成功。

进入主服务器登录redis

/usr/local/redis/bin/redis-cli

进行密码认证

auth 1234qwer

输出OK,表示认证成功。存入数据

set verify-key "Test Verify is Success"

输出OK,表示插入成功。接下来登录备服务器查看数据。进入备服务器登录redis:

/usr/local/redis/bin/redis-cli

进行密码认证

auth 1234qwer

输出OK,表示认证成功。获取key数据

get verify-key

输出:"Test Verify is Success",说明Redis主从搭建成功。

5、配置Keepalived信息

/etc/keepalived目录中存放keepalived.conf文件。在该目录下创建scripts_redis文件夹,目录 /etc/keepalived/scripts_redis,将 redis_stop.sh 、redis_master.sh、redis_fault.sh 、redis_check.sh 、redis_backup.sh 放入scripts_redis文件目录下。

5.1 主服务器配置

编写keepalived.conf

! Configuration File for keepalived

global_defs {
   router_id redis-master #唯一标识 注意主备服务名不可相同
   script_user root
   enable_script_security
}

vrrp_script redis_check { #脚本检测名称,下方调用必须和这个名称一致
    script "/etc/keepalived/scripts_redis/redis_check.sh" #监听redis是否启动脚本路径
    interval 4  #监听心跳
    weight -5
    fall 3
    rise 2
}

vrrp_instance VI_redis {
    state MASTER    #当前keepalived状态  MASTER 或者 BACKUP
    interface eth0  #网卡名称根据实际情况设置可通过命令ifconfig查看
    virtual_router_id 21
    priority 110	#权重 主服务要高于备服务
    garp_master_refresh 10
    garp_master_refresh_repeat 2
    advert_int 1
    nopreempt
    unicast_src_ip 192.168.42.130 #单播模式 当前服务器主服务器IP地址
    unicast_peer {
        192.168.42.133 #备服务器Ip
    }
	
    authentication {  #keepalived之间通信的认证账号、密码
        auth_type PASS
        auth_pass 1111
    }
	
    virtual_ipaddress {
        192.168.42.161	#虚拟IP地址 客户端统一的访问地址
    }
	
    garp_master_delay 1
    garp_master_refresh 5
	track_interface {
        eth0	#网卡
    }
	
    track_script {
        redis_check #脚本检测调用名称
    }
	
    notify_master /etc/keepalived/scripts_redis/redis_master.sh	#master脚本  keepalived设置的状态为master时触发或者master停止后,backup升级为master时触发
    notify_backup /etc/keepalived/scripts_redis/redis_backup.sh #backup脚本  keepalived设置的状态为backup时触发
    notify_fault /etc/keepalived/scripts_redis/redis_fault.sh #fault脚本
    notify_stop /etc/keepalived/scripts_redis/redis_stop.sh  #stop脚本 keepalived停止时触发
	   
}

编写 redis_master.sh,当主脚本启动时,需要先同步备服务器redis数据后,在设置为主节点进行启动:

#!/bin/bash

LOGFILE=/var/log/keepalived-redis-status.log
REDISCLI="/usr/local/redis/bin/redis-cli"

echo "Running redis_master.sh..." >>$LOGFILE
echo "[Master]" >> $LOGFILE
date >> $LOGFILE
echo "Being Master..." >> $LOGFILE
echo "Running SLAVEOF cmd..." >> $LOGFILE
$REDISCLI -h 192.168.42.130 -p 6379 -a 1234qwer CONFIG SET masterauth "1234qwer" 2>&1
$REDISCLI -h 192.168.42.130 -p 6379 -a 1234qwer REPLICAOF  192.168.42.133 6379 2>&1

sleep 5s

echo "Run slaveof no one cmd..." >>$LOGFILE

$REDISCLI -h 192.168.42.130 -p 6379 -a 1234qwer REPLICAOF NO ONE >>$LOGFILE 2>&1

echo "Finished running redis_master.sh..." >>$LOGFILE

编写redis_backup.sh,

#!/bin/bash

LOGFILE=/var/log/keepalived-redis-status.log
REDISCLI="/usr/local/redis/bin/redis-cli"
echo "Running redis_bakcup.sh..." >>$LOGFILE
echo "[Backup]" >> $LOGFILE
date >> $LOGFILE
echo "Being Slave..." >> $LOGFILE
echo "Run SLAVEOF cmd..." >> $LOGFILE
$REDISCLI -h 192.168.42.130 -p 6379 -a 1234qwer CONFIG SET masterauth "1234qwer"  >>$LOGFILE 2>&1
$REDISCLI -h 192.168.42.130 -p 6379 -a 1234qwer REPLICAOF 192.168.42.133 6379 >>$LOGFILE 2>&1
echo "Finished running redis_backup.sh..." >>$LOGFILE

5.2、备服务器配置

编写keepalived.conf

! Configuration File for keepalived

global_defs {
   router_id redis-slave #唯一标识 注意主备服务名不可相同
   script_user root
   enable_script_security
}

vrrp_script redis_check {
    script "/etc/keepalived/scripts_redis/redis_check.sh" #监听redis是否启动脚本路径
    interval 4 #监听心跳
    weight -5
    fall 3  
    rise 2
}

vrrp_instance VI_redis {
    state BACKUP  #当前keepalived状态 设置为BACKUP
    interface eth0
    virtual_router_id 21
    priority 100
    garp_master_refresh 10
    garp_master_refresh_repeat 2
    advert_int 1
    nopreempt
    unicast_src_ip 192.168.42.133 #单播模式 当前服务器IP地址
    unicast_peer {
        192.168.42.130 #主服务器Ip
    }
	
	
    authentication {
        auth_type PASS
        auth_pass 1111
    }
	
    virtual_ipaddress {
        192.168.42.161 #虚拟IP地址 客户端统一的访问地址
    }
	
    garp_master_delay 1
    garp_master_refresh 5

    track_interface {
        eth0
    }

    track_script {
        redis_check
    }
	
    notify_master /etc/keepalived/scripts_redis/redis_master.sh
    notify_backup /etc/keepalived/scripts_redis/redis_backup.sh
    notify_fault /etc/keepalived/scripts_redis/redis_fault.sh 
    notify_stop /etc/keepalived/scripts_redis/redis_stop.sh 
}


编写redis_master.sh

#!/bin/bash
# LOGFILE文件需要跟据实际情况更改
LOGFILE=/var/log/keepalived-redis-status.log
REDISCLI="/usr/local/redis/src/redis-cli"

echo "Running redis_master.sh..." >>$LOGFILE
echo "[Master]" >> $LOGFILE
date >> $LOGFILE
echo "Begin Master ..." >> $LOGFILE
echo "Run slaveof no one cmd...">>$LOGFILE
# SLAVEOF 5.0以上已经弃用 REPLICAOF 
$REDISCLI -h 192.168.42.133 -p 6379 -a 1234qwer REPLICAOF  NO ONE >>$LOGFILE 2>&1
echo "Finished running redis_master.sh..." >>$LOGFILE

编写redis_backup.sh

#!/bin/bash

LOGFILE=/var/log/keepalived-redis-status.log
REDISCLI="/usr/local/redis/src/redis-cli"

echo "Running redis_bakcup.sh..." >>$LOGFILE
echo "[Backup]" >> $LOGFILE
date >> $LOGFILE
echo "Being Slave..." >> $LOGFILE
sleep 15s #休眠15秒,确保主服务器脚本redis_master.sh执行完毕后在执行主从命令
echo "Run SLAVEOF cmd..." >> $LOGFILE
# SLAVEOF 5.0已经弃用 改为:REPLICAOF
$REDISCLI -h 192.168.42.133 -p 6379 -a 1234qwer CONFIG SET masterauth "1234qwer"  >>$LOGFILE 2>&1
$REDISCLI -h 192.168.42.133 -p 6379 -a 1234qwer REPLICAOF  192.168.42.130 6379 >>$LOGFILE 2>&1
echo "Finished running redis_backup.sh..." >>$LOGFILE
5.3、编写验证Redis是否启动脚本

编写redis_check.sh脚本,通过监听端口号判断(主备一致)

#!/bin/bash
LOGFILE=/var/log/check-redis-status.log
echo "Running redis_check.sh..." >> $LOGFILE
date >> $LOGFILE
CHECK=$(ss -tnlp|grep 6379)
if [ $? -ne 0 ]; then
   echo "redis-server is not running..." >> $LOGFILE
   systemctl stop keepalived.service
   exit 1
else
   echo "redis-server is running..." >> $LOGFILE
   exit 0
fi
echo "Finished running redis_check.sh..." >> $LOGFILE
5.4 其他脚本

编写 redis_fault.sh (主备一致)

#!/bin/bash

LOGFILE=/var/log/keepalived-redis-status.log
echo "Running redis_fault.sh..." >>$LOGFILE
echo "[Fault]" >> $LOGFILE
date >> $LOGFILE
echo "Finished running redis_fault.sh..." >> $LOGFILE

编写 redis_stop.sh (主备一致)

#!/bin/bash
LOGFILE=/var/log/keepalived-redis-status.log
echo "Running redis_stop.sh...." >>$LOGFILE
echo "[Stop]" >> $LOGFILE
date >> $LOGFILE
echo "Finished running redis_stop.sh...." >>$LOGFILE
5.5、给脚本授可执行权限

chmod +x /etc/keepalived/scripts_redis/*.sh

5.6、 keepalived相关命令

Keepalived 安装命令

yum install keepalived -y

Keepalived配置所在目录

/etc/keepalived

Keepalived 日志文件

/var/log/message

启动Keepalived命令

systemctl start keepalived.service

重启Keepalived命令

systemctl restart keepalived.service

查看Keepalived状态命令

systemctl status keepalived.service

查看Keepalived虚拟VIP ip

ip addr

关闭Keepalived命令

systemctl stop keepalived.service

6、验证主备双活

我们可以通过连接工具RedisDesktopManager进行测试主备双活。首先连接地址填写keepalived生成的虚拟IP地址:192.168.42.161、 输入端口号:6379和密码:1234qwer

连接成功后,插入一条数据进行数据测试。之后在把主服务器redis停止模拟服务器宕机,测试连接继续进行数据插入。然后在把主服务器redis启动,keepalived也需要启动。启动完成后查看数据是否一致。如果一致说明主备双活搭建成功。

开心一刻

有一天螃蟹出门,不小心撞倒了泥鳅
泥鳅很生气地说:你是不是瞎啊!
螃蟹说:不是啊,我是螃蟹

开心一刻

概述

maven-shade-plugin
官网已经介绍的很详细了,我给大家简单翻译一下

This plugin provides the capability to package the artifact in an uber-jar, including its dependencies and to
shade
- i.e. rename - the packages of some of the dependencies.

这段话简明扼要的概述了 maven-shade-plugin 的功能

  1. 能够将项目连同其依赖,一并打包到一个
    uber-jar

    uber-jar 就是一个超级 jar,不仅包含我们的工程代码,还包括依赖的 jar,和
    spring-boot-maven-plugin
    类似

  2. 能够对依赖 jar 中的包名进行重命名

    这个功能就有意思了,后面我们详说

maven-shade-plugin 必须和 Maven 构建生命周期的 package 阶段绑定,那么当 Maven 执行
mvn package
时会自动触发 maven-shade-plugin;使用很简单,在
pom.xml
添加该插件依赖即可

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.6.0</version>
    <executions>
        <execution>
            <!-- 和 maven package 阶段绑定 -->
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>

            <configuration>
                <!-- 按需自定义配置 -->
            </configuration>
        </execution>
    </executions>
</plugin>

phase

goal
按如上固定配置,
configuration
才是我们自由发挥的平台;有了基本了解后,我们再结合官方提供的
Examples
来看看 maven-shade-plugin 具体能干啥

选择打包内容

假设我们有项目
maven-shade-plugin-demo
,其项目结构如下

项目结构

如果不做任何剔除,可以按如下配置进行全打包

<dependencies>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.26</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.6.0</version>
            <executions>
                <execution>
                    <!-- 和 package 阶段绑定 -->
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <!-- 按需自定义配置 -->
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

执行
mvn package
后,我们会看到两个包

全打包

maven-shade-plugin-demo-1.0-SNAPSHOT.jar
就是 uber-jar;解压可看其结构

uber-jar结构

不仅包括
package
、还包括各种配置文件、元文件,统统打包进 uber-jar;而
original-maven-shade-plugin-demo-1.0-SNAPSHOT.jar
则是不包括依赖 jar 的原始项目包;如果我们比较细心的话,会发现打包的时候告警了

全打包告警

意思是说 hutool jar 包中有
META-INF/MANIFEST.MF
,而
maven-shade-plugin-demo
打包成 jar 后也包含
META-INF/MANIFEST.MF
,两者重复了,只会将其中一个复制进 uber jar;默认情况下,是将我们项目的 jar 中的
META-INF/MANIFEST.MF
复制进 uber jar

默认用项目的MANIFEST

那如果我们想保留 hutool 下的 MANIFEST.MF,而去掉 maven-shade-plugin-demo 中的 MANIFEST.MF,该如何处理呢?只需要微调下
configuration

<configuration>
    <filters>
        <filter>
            <artifact>com.qsl:maven-shade-plugin-demo</artifact>
            <excludes>
                <exclude>META-INF/*.MF</exclude>
            </excludes>
        </filter>
    </filters>
</configuration>

此时 uber jar 中的 MANIFEST.MF 就来自 hutool jar 了

换成hutool下的MANIFEST

回到前面的
configuration
配置,我们需要明白其每个子标签的含义

  1. filter
    :过滤器,可以配置多个

  2. artifact
    :复合标识符,用来匹配 jar,简单点说,就是匹配 jar 的
    匹配规则

    按 Maven 的坐标:groupId:artifactId[[:type]:classifier] 进行配置,
    groupId:artifactId
    必配,
    [[:type]:classifier]
    选配;支持通配符
    *

    ?
    ,例如:
    <artifact>*:*</artifact>
    (相当于匹配上所有jar)

  3. exclude
    :排除项,也就是不会复制进 uber-jar;支持通配符配置

  4. include
    :包含项,也就是
    只有
    这些会被复制进 uber-jar;支持通配符配置

我们实战下,假设我们项目结构如下所示

明细配置项目结构

configuration
配置如下

<configuration>
    <filters>
        <filter>
            <artifact>com.qsl:maven-shade-plugin-demo</artifact>
            <excludes>
                <exclude>com/qsl/test/**</exclude>
                <exclude>com/qsl/Entry.class</exclude>
            </excludes>
        </filter>
        <filter>
            <artifact>cn.hutool:hutool-all</artifact>
            <includes>
                <include>cn/hutool/Hutool.class</include>
                <include>cn/hutool/json/**</include>
            </includes>
        </filter>
    </filters>
</configuration>

执行
mvn package
后,uber-jar 内部结构你们能想到吗?我们来看看实际结果

明细配置后uber-jar结构

是不是和跟你们想的一样?

除了手动指定
filter
外,此插件还支持自动移除项目中没有使用到的依赖类,以此来最小化 uber jar 的体积;configuration 配置如下

<configuration>
    <minimizeJar>true</minimizeJar>
</configuration>

我们在
StringUtil
中引入 hutool 的 StrUtil(相当于项目依赖了 StrUtil)

package com.qsl.util;

import cn.hutool.core.util.StrUtil;

/**
 * @author: 青石路
 */
public class StringUtil {

    public static boolean isBlank(String str) {
        return StrUtil.isBlank(str);
    }
}

然后打包,uber-jar 内部结构如下所示

最小依赖

从 maven-shade-plugin 1.6 开始,
minimizeJar
会保留
filter

include
配置的类,但是要注意:

inlcude 默认会排除所有不在 include 配置中的类

这就会导致问题,我们来看个案例,我们引入
logback
依赖,但代码中未用到它,而我们又想将其下的 class 复制进 uber-jar,另外我们还想将 hutool 的
cn/hutool/json
包下的全部类都复制进 uber-jar,并且开启
minimizeJar
,是不是按如下配置?

<dependencies>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.26</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.3.14</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.6.0</version>
            <executions>
                <execution>
                    <!-- 和 package 阶段绑定 -->
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <minimizeJar>true</minimizeJar>
                        <filters>
                            <filter>
                                <artifact>ch.qos.logback:logback-classic</artifact>
                                <includes>
                                    <include>**</include>
                                </includes>
                            </filter>
                            <filter>
                                <artifact>cn.hutool:hutool-all</artifact>
                                <includes>
                                    <include>cn/hutool/json/**</include>
                                </includes>
                            </filter>
                        </filters>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

打包后看 uber-jar 目录结构

最小依赖_include

hutool 的
core
包没有复制进来,这是因为我们对 hutool 配置了
include
,默认把最小依赖的
core
包给排除掉了,那怎么办呢?插件提供了配置
<excludeDefaults>false</excludeDefaults>
来处理此种情况,它会覆盖
include
默认排除行为

<filter>
    <artifact>cn.hutool:hutool-all</artifact>
    <excludeDefaults>false</excludeDefaults>
    <includes>
        <include>cn/hutool/json/**</include>
    </includes>
</filter>

这样配置之后,既能包含 hutool 的 json 包,又能包含最小依赖的 core 包

false
通常配合
true
来使用,不然

<configuration>
    <filters>
        <filter>
            <artifact>cn.hutool:hutool-all</artifact>
            <excludeDefaults>false</excludeDefaults>
            <includes>
                <include>cn/hutool/json/**</include>
            </includes>
        </filter>
    </filters>
</configuration>

这么配置有何意义?

重定位 class

如果 uber-jar 被其他项目依赖,而我们的 uber-jar 又是保留了依赖 jar 的 class 的全类名,那么就可能类重复而导致类加载冲突;比如项目A依赖了我们的
maven-shade-plugin-demo
,还依赖了 B.jar,两个 jar 中都存在
cn.hutool.core.util.StrUtil.class
,但 api 完全不一样,根据
双亲委派模型
,只会成功加载其中某个
cn.hutool.core.util.StrUtil.class
,那么另一个的 api 则使用不了。为了解决这个问题,插件提供了重定位功能,通过创建 class 字节码的私有副本,按新配置的 package,打包进 uber-jar

我们来看个案例,假设我们只需要 hutool 的 core 包,将其下所有的 class 按
com.qsl.core
包打包进 uber-jar,可以按如下配置

<dependencies>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.26</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.6.0</version>
            <executions>
                <execution>
                    <!-- 和 package 阶段绑定 -->
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <relocations>
                            <relocation>
                                <pattern>cn.hutool.core</pattern>
                                <shadedPattern>com.qsl.core</shadedPattern>
                            </relocation>
                        </relocations>
                        <filters>
                            <filter>
                                <artifact>cn.hutool:hutool-all</artifact>
                                <includes>
                                    <include>cn/hutool/core/**</include>
                                </includes>
                            </filter>
                        </filters>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

打包后 uber-jar 目录结构如下

relocate

我们来看下 uber-jar 中的 StringUtil.class

StringUtil中的StrUtil路径

依赖的
StrUtil
也被正确调整了,是不是很牛皮?

有点东西

此时项目A 依赖 B.jar 的同时,又依赖我们的
maven-shade-plugin-demo
,就不会类重名了(package 不一致了)

relocation
同样支持
exclude

include

<configuration>
    <relocations>
        <relocation>
            <pattern>cn.hutool.core</pattern>
            <shadedPattern>com.qsl.core</shadedPattern>
            <!-- exclude 指定的不重定向,其他重定向 -->
            <excludes>
                <exclude>cn.hutool.core.util.ObjUtil</exclude>
                <!-- 一个*只会过滤包下的class,两个*会过滤包下的class和子包 -->
                <exclude>cn.hutool.core.date.**</exclude>
            </excludes>
        </relocation>
        <relocation>
            <pattern>cn.hutool.json</pattern>
            <shadedPattern>com.qsl.json</shadedPattern>
            <!-- include 指定的重定向,其他不重定向 -->
            <includes>
                <include>cn.hutool.json.JSONUtil</include>
                <!-- 一个*只会过滤包下的class,两个*会过滤包下的class和子包 -->
                <include>cn.hutool.json.xml.**</include>
            </includes>
        </relocation>
    </relocations>
    <filters>
        <filter>
            <artifact>cn.hutool:hutool-all</artifact>
            <includes>
                <include>cn/hutool/core/**</include>
                <include>cn/hutool/json/**</include>
            </includes>
        </filter>
    </filters>
</configuration>

此时 uber-jar 的目录结构是怎样的?你们自己去试!

生成附属包

前面已经介绍过,打包后会生成两个包

全打包


original
开头的那个明显不是按 Maven 坐标命名的,所以它是不能够
install
到本地或者远程仓库的;如果需要将两个 jar 都
install
到仓库中,那么就需要用到插件的
Attaching the Shaded Artifact
(生成附属包)功能

<configuration>
    <relocations>
        <relocation>
            <pattern>cn.hutool.core</pattern>
            <shadedPattern>com.qsl.core</shadedPattern>
            <!-- exclude 指定的不重定向,其他重定向 -->
            <excludes>
                <exclude>cn.hutool.core.util.ObjUtil</exclude>
                <!-- 一个*只会过滤包下的class,两个*会过滤包下的class和子包 -->
                <exclude>cn.hutool.core.date.**</exclude>
            </excludes>
        </relocation>
        <relocation>
            <pattern>cn.hutool.json</pattern>
            <shadedPattern>com.qsl.json</shadedPattern>
            <!-- include 指定的重定向,其他不重定向 -->
            <includes>
                <include>cn.hutool.json.JSONUtil</include>
                <!-- 一个*只会过滤包下的class,两个*会过滤包下的class和子包 -->
                <include>cn.hutool.json.xml.**</include>
            </includes>
        </relocation>
    </relocations>
    <filters>
        <filter>
            <artifact>cn.hutool:hutool-all</artifact>
            <includes>
                <include>cn/hutool/core/**</include>
                <include>cn/hutool/json/**</include>
            </includes>
        </filter>
    </filters>
    <shadedArtifactAttached>true</shadedArtifactAttached>
    <shadedClassifierName>qsl</shadedClassifierName>
</configuration>

部署到仓库的 jar 如下

生成附属包

可执行 JAR

这个就比较简单了,我们直接看配置

<configuration>
    <transformers>
        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <mainClass>com.qsl.Entry</mainClass>
        </transformer>
    </transformers>
</configuration>

如上配置会将
Main-Class
写进 uber-jar 的 MANIFEST.MF,还可以通过
manifestEntries
自定义属性

<configuration>
    <transformers>
        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <manifestEntries>
                <mainClass>com.qsl.Entry</mainClass>
                <Build-Author>qsl</Build-Author>
            </manifestEntries>
        </transformer>
    </transformers>
</configuration>

打包之后,uber-jar 的 MANIFEST.MF 内容如下

可执行jar

资源转换器

Resource Transformers
已经介绍的很详细了,我就不一一介绍了,挑几个个人认为比较重要的简单讲一下

ServicesResourceTransformer

合并
META-INF/services/
下的文件,并对文件中的 class 进行重定向;我们来看个例子,hutool 下有文件
cn.hutool.aop.proxy.ProxyFactory

services_proxyFactory

我们也自定义一个

自定义QslFactory

configuration 配置如下

<configuration>
    <relocations>
        <relocation>
            <pattern>cn.hutool.aop</pattern>
            <shadedPattern>com.qsl.aop</shadedPattern>
        </relocation>
    </relocations>
    <transformers>
        <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
    </transformers>
</configuration>

打包后,hutool 与 uber-jar 的 cn.hutool.aop.proxy.ProxyFactory 文件内容差异如下

services_合并后前后对比

如果不配置
ServicesResourceTransformer
,结果是怎样,你们自己去试

AppendingTransformer

将多个同名文件的内容合并追加到一起(不配置的情况下会覆盖,最终文件内容只是其中某个文件的内容),configuration 配置如下

<configuration>
    <transformers>
        <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.factories</resource>
        </transformer>
    </transformers>
</configuration>

打包后文件内容合并如下

Append转换器

XmlAppendingTransformer

ResourceBundleAppendingTransformer
功能类似,只是针对的文件内容格式略微有点特殊,就不演示了,你们自行去测试

同包同名 class 共存

回到我们的主题,如果我们项目依赖的 jar 中出现了同名的 class (包名和类名均相同),根据
双亲委派模型
,只会加载其中某一个 class,虽然两个 class 同名了,但功能完全不一样,另一个未被加载的 class 的功能则用不了,如果想同时使用这两个同名 class 的功能,我们该如何处理?

甲方扔给两个存在包名与类名均相同的Jar包,要在工程中同时使用怎么办?

文中给出了几种解决方案(注意看评论区),最高效最实用的当属
maven-shade-plugin
;假设我们项目依赖的 A.jar 和 B.jar 都存在
com.qsl.Hello.class
,我们可以新建一个项目,名字叫
qsl-a
,没有任何代码,仅仅依赖 A.jar,然后利用 maven-shade-plugin 的
Relocating Classes
功能对 A.jar 中存在重名的 class 进行重定向,例如

<configuration>
    <relocations>
        <relocation>
            <pattern>com.qsl</pattern>
            <shadedPattern>com.qsla</shadedPattern>
        </relocation>
        <includes>
            <include>com.qsl.Hello</include>
        </includes>
    </relocations>
</configuration>

然后打包得到 uber jar(qsl-a.jar),项目依赖从 A.jar 更改成 qsl-a.jar,B.jar 依赖继续保留,那么项目中可用的 Hello.class 就包括

com.qsl.Hello(B.jar)

com.qsla.Hello(qsl-a.jar)

问题是不是就得到解决了?更实际的案例,敬请期待我下篇博客

总结

  1. maven-shade-plugin 的输入目标是
    项目原始jar
    以及
    项目依赖的所有jar
    ,而输出目标是
    uber-jar
    ,所以 maven-shade-plugin 的规则对
    项目原始jar
    是无效的
  2. minimizeJar
    针对的只是
    class
    ,其他类型的文件不受此约束
  3. 同 class 共存问题,可以利用 maven-shade-plugin 的 Relocating Classes 功能,将其中一个或多个 jar 重新打包成新的 jar,保证类名相同但包名不同,然后项目依赖新的 jar,变相解决了同 class 共存问题
  4. 示例项目:
    maven-shade-plugin-demo

上篇文章很多小伙伴留言也讲到自己被公司裁员,还有的细心的小伙伴
说去年九月就被裁了,在看一下文章的发布时间,绷不住了。
先和大家说一下,我已经找到工作,因为最近工作一直都很忙,加上自己也比较懒,所以就拖了很久才写的。之前没说就是为了方便写后面的文章。也是通过本文分享一下自己找工作的方法,希望可以给大家一点帮助。

开始找工作,基本都是
Boss不回、前程堪忧
。因为现在的行情就是不太好,公司的需求减少,大量的公司裁员。
在职的时候和大多数程序员一样,每天按部就班的工作,偶尔吐槽公司,也不敢离职找工作。等到被裁的时候,心里一片茫然

开始的一个月,就两三个面试。而且基本都是一面就挂了。基本的都是准备不足,但是一个月后,半个月的时候我就拿了 4 个offer,这里面也有运气和自己的策略问题。

面临的问题

  • 八股文不会,或者背了就忘。
  • 简历准备不够,只是写简单项目经历
  • boss 不回就投递少。
  • 外包填了几个信息没信之后,就很少回。

解决方案

被裁之后本来心气就低,特别又遇到现在这种行情,基本公司都是已读不回或者外包要填写各种各样的信息,填完就没消息了,就很容易气馁。

脸皮要厚

首先就要海投,不管什么公司,只要差不多符合要求的公司就投递,boss 不回就多投,一天最低要投递几十份简历。先要找到机会去面试,有机会面试,找到面试的感觉,根据面试的反馈做调整,聊天沟通几千家,投递一百多的简历,才有几次面试。

当时找工作的时候,外包的回复比较多,不过都是要求填写各种信息,还会问各种问。然后问完了就消息了。我后面从一个 HR 朋友才知道,原来外包的 HR 都是按照招的人数拿提成,他们有一个表格,填完表格之后,就直接交给用人部门,一个岗位招十个人,他可能聊了几百的候选人,这种情况下没有下文也正常。

针对外包 HR,聊了几次之后,基本都知道他们要什么信息,都会把新增存在聊天记录里面。下次要这些信息的时候就方便很多。

背八股文

大部分面试的都会问到八股文。

  • 比较常见的八股文可以看看
    https://cyc2018.xyz/
  • 图解网络、操作系统、计算机组成
    小林coding

图表加深记忆

如果光背八股文,没有给成套的体系的话,基本的都是背了就忘。我的记忆性不是很好,会辅助一下图表理解八股文,比如 HashMap 的数据结构,使用图表和流程就可以快速的理解和掌握,

比如 HashMap 结构:

先通过图表结构,大概了解他的结构,在通过一些流程图获取数据添加到流程:

通过上面的的方法,写了几篇关于 HashMap 的文章:

知识成体系

很多知识点会有很多的关联性。比如 HashMap 适用于并发度不高的情况,而如果想要保证线程安全就需要使用 ConCurrentHashMap,再看看 add 和 get 方法是如何线程线程安全,一环扣一环。

不同的知识点,底层都是有关联性,比如 ArrayList 和 Redis 的简单字符串的扩容,当数据不够的时候,都会扩大自己的 1.5,这样的都是为了减少扩容的次数。

带着问题或者解决的难点去看八股文,比如 MQ 基本都会问消息的
可靠性

不可重复
,MQ 主要就是做一个消息的传递,在正常情况下,消息都能正常消费一次,但是如果服务器重启了,或者接收的服务重启,这都可能导致消息不可靠,带着问题,最好本地搭建服务,模拟消息丢失的情况,进而解决这个问题。
把这个问题解决的思路和过程改成自己在工作中解决了这种问题,再加一点自己的思考,这样比纯背题加分很多。

准备简历

简历主要展示两个技能:

  • 项目经验
  • 技术技能

项目经验

项目包含几个点:

  • 项目背景

让面试官和 hr 知道解决了什么问题,hr 会通过项目来匹配相同的经历的面试者。简单点就是说,这个项目解决了那些问题,整个项目是如何运的。

项目背景或者需求一般都是产品最了解,程序员大部分时间都用在如何实现功能上,用在需求的理解比较少。了解项目又会技术就给面试官印象比较深,面试通过的概率也比较高。

  • 你参与的角色,负责模块

参与的模块,负责那些代码,不要简单写自己做了xx模块,而是从一整个项目出发,解决那些问题。比如一个数据分析系统。这是修改前项目职责:

  • 订单商品维度销量统计
  • 订单炼厂维度销售统计
  • 订单客户统计
  • 订单新客户统计

流水账记录实现的功能,没有了解到具体的需求和目标。好的技术是要先了解需求、技术设计、功能实现、配合同事完成任务对接。

下面是优化后的项目职责:

  • 和业务团队收集需求,明确数据分析的需求和指标。
  • 设计并实现数据统计和多维度分析统计。
  • 主要统计每天或每周的订单、商品、供应商、业务员、新客户等销售数据。
  • 配合前端提供图形化展示,帮助业务团队快速洞察问题。

相对一个只会干活的程序员,一个会思考需求的来源、项目解决的难点和痛点、设计技术实现方案以及高效的沟通的优秀程序员肯定的更受青睐,而且修改后的项目职责也相对更加的专业和规范。

技术技能

先看一下技术技能对比,这是修改前的技能:

修改前技能问题:

  • 前后端都熟悉,没有突出的技能,现在很多公司都是前后端分离,没有突出的技能,面试那关估计就被刷掉。
  • 技能比较简单,工作5,6年就不适用简单的技能了。
  • 技术技能需要关联到相关的业务技能和沟通技能。

修改上面的问题,这是优化后的技能:

技能除了介绍技术之外,更重要的要介绍自己的工作经验,主要涉及的业务方向。此外博客也是一个很好的展示技能。

准备完毕,开始面试

一共面试了 6 家,其中拿到了 4 个 offer,因为是去年面试的,过程只记了一个大概。

某喜到家

这是第一家面试的公司,是一家 o2o的公司,面试架构组职位。主要问了一些项目,如何实现,几个八股文,微服务的有什么缺点,分布式事务的替换方案。面试出奇的顺利,有的问题不太熟悉的,面试官也会给点提示。一下午技术面试过了两轮,hr面试也过了两轮。然后就回去等通知。

过了一天之后,通知我面试通过了。但是他们的上班时间是上午9点半到中午12点,中午休息2个小时。下午两点到六点,晚上休息半个个小时,六点半点到8点,而且还是大小周。给出的薪资相对之前基本是没涨(虽然工资涨了,工作时间也增加了),还是先拿个offer保底,再继续找工作。

这么长的工作时间,应该也很难招到人。工资也卡的比较死。这种情况自然面试也比较容易就通过了。

银行外包

面试过了一天,来了一个珠海外包的电话面试,主要问了一些八股文:

  • SpringBoot自动装配原理
  • 线程池线程数量设置,拒绝策略
  • JMM

然后介绍自己的项目经历,自己负责的模块,如何实现对应的技术模块。印象比较的深一个点,是自己写博客地址,就问了下是不是自己写的,说看了还是写的挺不错的。而且还说:
我最近几天面试了十多个人,你是唯一一个让我感觉挺满意,背八股文不是应付任务一样的背,还会结合一些实际的工作常见使用。做项目也有自己的想法,而且还会把自己的心得、总结写成博客。
当时就感觉自己写了这么久的博客有人表扬还是很开心的。

二面时候,也是问了一个技术的问题,面试也过了。给的薪资还可以,比上面的 o2o 的工资好一点。但是毕竟外包还是工作比较累,还是拿个offer,再继续找工作。

某外贸公司

这是一家跨境电商的公司,主营的是电子烟,想要做一个独立站,跨境销售产品。面试形式是笔试 + 面试。比如主要写了一些八股文:

  • CPU飙升到 100% 如何处理
  • MQ 消息可靠性如何保证
  • Mysql 事务隔离级别,以及各自的问题
  • HashMap、ConCurrentHashMap、Hashtable 的区别

面试主要问项目经验和一些八股文,对 HashMap 的细节一直问的比较细节。后面人事总监面试也过了,第二天回复面试也通过了。给的薪资比前面都高,基本上对这个工资是比较满意,也准备拿了 offer 就准备上班了。

半路杀出个珠宝公司

本来就准备下周就去新的公司报道了,在 boss 上又来了一个面试,反正也没啥事,多面试几次也没事。前面几次面试基本上都是问
项目 + 八股文
,都问出经验了,一面主要是问项目经验,自己的负责的模块用哪些技术实现。最后就问了一下如何实现点赞功能,要看到每天点赞的人数、每个人点赞的数量,这么统计的话,就不能使用 Redis 统计了,只能使用关系型数据库,比如 Mysql 实现。

一面过了,就来到了技术总监的面试,主要讲项目,还叫我在画板上画下流程图,面试也通过了,之后就是 HR 谈薪资。薪资和之前的外贸公司差不多,但是这个公司包吃住,相当于涨了三千的工资。

最后选择

最后就在外贸公司和珠宝公司选择,两个公司都是大小周,(后面面试的公司基本都是大小周)无论是薪资还是福利方面,珠宝公司都更好,而且相对来说珠宝行业也比较稳定点,所以最后就选择了珠宝公司。

总结

断断续续终于写完几篇失业日记,最开始失业的不适应、迷茫,后面找工作的屡屡碰壁,一次又一次的失败。简历一遍一遍的修改,疯狂打招呼,疯狂投简历。那个时候也不知道什么时候能找到工作,开始找工作的 10 月份的时候感觉过年都可能找不到工作,没想到 11 月份,半个月就拿了 4 个 offer,就像《阿甘正传》的那句台词一样:

Life was like a box of chocolate. You never know what you're gonna get

一 、 概述

PeerJS 是一个基于浏览器
WebRTC功能实现的js功能包,简化了WebrRTC的开发过程,对底层的细节做了封装,直接调用API即可,再配合surging 协议组件化从而做到稳定,高效可扩展的微服务,再利用RtmpToWebrtc 引擎组件可以做到不仅可以利用httpflv 观看rtmp推流直播,还可以采用基于
Webrtc
的peerjs 进行观看,那么今天要讲的是如何结合开发语音视频通话功能。放到手机和电脑上都可以实现语音视频通话。

一键运行打包成品下载:
https://pan.baidu.com/s/1MVsjKtVYpUonauAz9ZXtPg?pwd=1q2g

测试用户:fanly

测试密码:123456

为了让大家节约时间,能尽快运行产品看到效果,上面有 一键运行打包成品可以进行下载测试运行。

二、如何测试运行

以下是目录结构,

IDE:consul 注册中心

kayak.client: 网关

kayak.server:微服务

apache-skywalking-apm:skywalking链路跟踪

以上是目录结构, 不需要进入管理界面配置网络组件,启动后自带端口96的ws协议主机,只要打开video文件夹,里面有两个语音通话的html测试文件,在同一一个局域网只要输入对方的name就可以进行语音通话

打开界面如图

三、基于surging如何开发

以上是没有开发环境的进行下载进行下载测试,那么正在使用surging 的如何开发此功能呢?

1. 创建服务接口,继承于
IServiceKey

 [ServiceBundle("Device/{Service}")]public  interfaceIChatService : IServiceKey
{
}

2. 创建中间服务,继承于WSBehavior, IChatService

  internal classChatService : WSBehavior, IChatService
{
private static readonly ConcurrentDictionary<string, string> _users = new ConcurrentDictionary<string, string>();private static readonly ConcurrentDictionary<string, string> _clients = new ConcurrentDictionary<string, string>();protected override voidOnOpen()
{
var _name = Context.QueryString["name"];if (!string.IsNullOrEmpty(_name))
{
_clients[ID]
=_name;
_users[_name]
=ID;
}
}
protected override voidOnError( WebSocketCore.ErrorEventArgs e)
{
var msg =e.Message;
}
protected override voidOnMessage(MessageEventArgs e)
{
if(_clients.ContainsKey(ID))
{
var message = JsonConvert.DeserializeObject<Dictionary<string, object>>(e.Data);//消息类型 message.TryGetValue("type",out object@type);
message.TryGetValue(
"toUser", out objecttoUser);
message.TryGetValue(
"fromUser", out objectfromUser);
message.TryGetValue(
"msg", out objectmsg);
message.TryGetValue(
"sdp", out objectsdp);
message.TryGetValue(
"iceCandidate", out objecticeCandidate);


Dictionary
<String, Object> result = new Dictionary<String, Object>();
result.Add(
"type", @type);//呼叫的用户不在线 if (!_users.ContainsKey(toUser?.ToString()))
{
result[
"type"]= "call_back";
result.Add(
"fromUser", "系统消息");
result.Add(
"msg", "Sorry,呼叫的用户不在线!");this.Client().SendTo(JsonConvert.SerializeObject(result), ID);return;
}
//对方挂断 if ("hangup".Equals(@type))
{
result.Add(
"fromUser", fromUser);
result.Add(
"msg", "对方挂断!");
}
//视频通话请求 if ("call_start".Equals(@type))
{
result.Add(
"fromUser", fromUser);
result.Add(
"msg", "1");
}
//视频通话请求回应 if ("call_back".Equals(type))
{
result.Add(
"fromUser", toUser);
result.Add(
"msg", msg);
}
//offer if ("offer".Equals(type))
{
result.Add(
"fromUser", toUser);
result.Add(
"sdp", sdp);
}
//answer if ("answer".Equals(type))
{
result.Add(
"fromUser", toUser);
result.Add(
"sdp", sdp);
}
//ice if ("_ice".Equals(type))
{
result.Add(
"fromUser", toUser);
result.Add(
"iceCandidate", iceCandidate);
}
this.Client().SendTo(JsonConvert.SerializeObject(result), _users.GetValueOrDefault(toUser?.ToString()));
}
}
protected override voidOnClose(CloseEventArgs e)
{
if( _clients.TryRemove(ID, out stringname))
_users.TryRemove (name,
out stringvalue);
}

}

3.设置surgingSettings的WSPort端口配置,默认96

以上就是利用websocket协议中转消息,下面是页面如何编号,代码如下:

<!DOCTYPE>
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<!--suppress ALL-->
<htmlxmlns:th="http://www.thymeleaf.org">
<head>
    <metacharset="UTF-8">
    <title>WebRTC + WebSocket</title>
    <metaname="viewport"content="width=device-width,initial-scale=1.0,user-scalable=no">
    <style>html,body{margin:0;padding:0;
        }#main{position:absolute;width:370px;height:550px;
        }#localVideo{position:absolute;background:#757474;top:10px;right:10px;width:100px;height:150px;z-index:2;
        }#remoteVideo{position:absolute;top:0px;left:0px;width:100%;height:100%;background:#222;
        }#buttons{z-index:3;bottom:20px;left:90px;position:absolute;
        }#toUser{border:1px solid #ccc;padding:7px 0px;border-radius:5px;padding-left:5px;margin-bottom:5px;
        }#toUser:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}#call{width:70px;height:35px;background-color:#00BB00;border:none;margin-right:25px;color:white;border-radius:5px;
        }#hangup{width:70px;height:35px;background-color:#FF5151;border:none;color:white;border-radius:5px;
        }
    </style>
</head>
<body>
    <divid="main">
        <videoid="remoteVideo"playsinline autoplay></video>
        <videoid="localVideo"playsinline autoplay muted></video>

        <divid="buttons">
            <inputid="toUser"placeholder="输入在线好友账号"/><br/>
            <buttonid="call">视频通话</button>
            <buttonid="hangup">挂断</button>
        </div>
    </div>
</body>
<!--可引可不引-->
<!--<script th:src="@{/js/adapter-2021.js}"></script>-->
<scripttype="text/javascript"th:inline="javascript">let username= "fanly";
let localVideo
=document.getElementById('localVideo');
let remoteVideo
=document.getElementById('remoteVideo');
let websocket
= null;
let peer
= null;

WebSocketInit();
ButtonFunInit();
/*WebSocket*/ functionWebSocketInit(){//判断当前浏览器是否支持WebSocket if('WebSocket' inwindow) {
websocket
= newWebSocket("ws://127.0.0.1:961/device/chat?name="+username);
}
else{
alert(
"当前浏览器不支持WebSocket!");
}
//连接发生错误的回调方法 websocket.onerror= function(e) {
alert(
"WebSocket连接发生错误!");
};
//连接关闭的回调方法 websocket.onclose= function() {
console.error(
"WebSocket连接关闭");
};
//连接成功建立的回调方法 websocket.onopen= function() {
console.log(
"WebSocket连接成功");
};
//接收到消息的回调方法 websocket.onmessage=asyncfunction(event) {
let { type, fromUser, msg, sdp, iceCandidate }
=JSON.parse(event.data.replace(/\n/g,"\\n").replace(/\r/g,"\\r"));

console.log(type);
if(type=== 'hangup') {
console.log(msg);
document.getElementById(
'hangup').click();return;
}
if(type=== 'call_start') {
let msg
= "0" if(confirm(fromUser+ "发起视频通话,确定接听吗")==true){
document.getElementById(
'toUser').value=fromUser;
WebRTCInit();
msg
= "1"}

websocket.send(JSON.stringify({
type:
"call_back",
toUser:fromUser,
fromUser:username,
msg:msg
}));
return;
}
if(type=== 'call_back') {if(msg=== "1"){
console.log(document.getElementById(
'toUser').value+ "同意视频通话");//创建本地视频并发送offer let stream=await navigator.mediaDevices.getUserMedia({ video:true, audio:true})
localVideo.srcObject
=stream;
stream.getTracks().forEach(track
=>{
peer.addTrack(track, stream);
});

let offer
=await peer.createOffer();
await peer.setLocalDescription(offer);
let newOffer
=offer;

newOffer[
"fromUser"]=username;
newOffer[
"toUser"]=document.getElementById('toUser').value;
websocket.send(JSON.stringify(newOffer));
}
else if(msg=== "0"){
alert(document.getElementById(
'toUser').value+ "拒绝视频通话");
document.getElementById(
'hangup').click();
}
else{
alert(msg);
document.getElementById(
'hangup').click();
}
return;
}
if(type=== 'offer') {
let stream
=await navigator.mediaDevices.getUserMedia({ video:true, audio:true});
localVideo.srcObject
=stream;
stream.getTracks().forEach(track
=>{
peer.addTrack(track, stream);
});

await peer.setRemoteDescription(
newRTCSessionDescription({ type, sdp }));
let answer
=await peer.createAnswer();
let newAnswer
=answer;

newAnswer[
"fromUser"]=username;
newAnswer[
"toUser"]=document.getElementById('toUser').value;
websocket.send(JSON.stringify(newAnswer));

await peer.setLocalDescription(answer);
return;
}
if(type=== 'answer') {
peer.setRemoteDescription(
newRTCSessionDescription({ type, sdp }));return;
}
if(type=== '_ice') {
peer.addIceCandidate(iceCandidate);
return;
}

}
}
/*WebRTC*/ functionWebRTCInit(){
peer
= newRTCPeerConnection();//ice peer.onicecandidate= function(e) {if(e.candidate) {
websocket.send(JSON.stringify({
type:
'_ice',
toUser:document.getElementById(
'toUser').value,
fromUser:username,
iceCandidate: e.candidate
}));
}
};
//track peer.ontrack= function(e) {if(e&&e.streams) {
remoteVideo.srcObject
=e.streams[0];
}
};
}
/*按钮事件*/ functionButtonFunInit(){//视频通话 document.getElementById('call').onclick= function(e){
document.getElementById(
'toUser').style.visibility= 'hidden';

let toUser
=document.getElementById('toUser').value;if(!toUser){
alert(
"请先指定好友账号,再发起视频通话!");return;
}
if(peer== null){
WebRTCInit();
}

websocket.send(JSON.stringify({
type:
"call_start",
fromUser:username,
toUser:toUser,
}));
}
//挂断 document.getElementById('hangup').onclick= function(e){
document.getElementById(
'toUser').style.visibility= 'unset';if(localVideo.srcObject){
const videoTracks
=localVideo.srcObject.getVideoTracks();
videoTracks.forEach(videoTrack
=>{
videoTrack.stop();
localVideo.srcObject.removeTrack(videoTrack);
});
}
if(remoteVideo.srcObject){
const videoTracks
=remoteVideo.srcObject.getVideoTracks();
videoTracks.forEach(videoTrack
=>{
videoTrack.stop();
remoteVideo.srcObject.removeTrack(videoTrack);
});
//挂断同时,通知对方 websocket.send(JSON.stringify({
type:
"hangup",
fromUser:username,
toUser:document.getElementById(
'toUser').value,
}));
}
if(peer){
peer.ontrack
= null;
peer.onremovetrack
= null;
peer.onremovestream
= null;
peer.onicecandidate
= null;
peer.oniceconnectionstatechange
= null;
peer.onsignalingstatechange
= null;
peer.onicegatheringstatechange
= null;
peer.onnegotiationneeded
= null;

peer.close();
peer
= null;
}

localVideo.srcObject
= null;
remoteVideo.srcObject
= null;
}
}
</script> </html>

以上是页面的代码,如需要添加其它账号测试只要更改
username
,或者ws地址也可以更改标记红色的区域。

三、总结

本人正在开发平台,如有疑问可以联系作者,QQ群:744677125