在前几篇中,我们完成了netty和springboot的整合,大致领略了一下netty作为web通讯服务器的魅力,据我所知,netty在不少做聊天应用型的公司里面使用很广泛,主要还是得益于netty底层的实现原理对于高并发性能的良好支持和扩展,以及netty可定制化的API组件和其成熟的使用场景,当然,你也可以选择使用诸如socketJS或者原生的socket来实现聊天的功能,但总体来说都不如netty使用灵活,下面就以springboot整合netty实现一个简单的单聊功能;
首先,在实现功能之前我们先了解一下netty的API中有一个很重要的组件叫做,Handler,在netty服务器的实现中,handler是一个很重要的组件,通俗来说,handler相当于是我们处理客户端消息的一个助手类,或者叫做业务逻辑的补充实现类更贴切,因为netty底层使用的是nio模型,里面有个channel的玩意,
1、客户端连接成功,netty创建一个和客户端通信的channel,客户端发消息给服务端并不是直接发给服务端,而是发送到channel中,更多的客户端连接服务端时,netty将会开辟更多的channel,这样每个客户端通过这根chanel"管道"就建立了和服务端的联系;
2、客户端发送消息到channel;
3、服务端通过获取这个channel,从channel中收到客服端发来的消息;
4、服务端再将响应通过这个chennel发出去;
可以看到,netty中必然对应着某个组件能够获取到客户端初始化连接成功的channel,然后发生后面的交互,而上面所说的handler就是这样的一个组件,通过继承 SimpleChannelInboundHandler 这个类,就可以获取当前连接的channel,然后通过复写这个类里面的相关方法就可以使用里面的API进行channel的操作了,有了上面的概念,就开始说说具体的整合,整个项目结构如下:
1、pom先关依赖:
org.springframework.boot
spring-boot-starter-parent
2.0.1.RELEASE
UTF-8
UTF-8
1.8
io.netty
netty-all
4.1.25.Final
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
2.9.0
com.alibaba
fastjson
1.2.8
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.0
mysql
mysql-connector-java
runtime
com.alibaba
druid-spring-boot-starter
1.1.10
commons-collections
commons-collections
3.2.2
net.sourceforge.jexcelapi
jxl
2.6.12
com.google.code.gson
gson
2.8.2
net.sf.ehcache
ehcache
2.10.4
org.projectlombok
lombok
1.18.2
provided
org.apache.httpcomponents
httpclient
4.5.2
org.javassist
javassist
3.24.0-GA
org.springframework.kafka
spring-kafka
2.1.11.RELEASE
org.apache.kafka
kafka-clients
1.1.0
org.springframework.kafka
spring-kafka
org.apache.kafka
kafka_2.12
2.2.0
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-cache
redis.clients
jedis
2.9.0
org.springframework.boot
spring-boot-starter-freemarker
org.springframework.boot
spring-boot-maven-plugin
2、application.properties,这里面我整合了mybatis,如果没有业务用到数据库的连接,也可以不用整合,
server.port=8083
spring.datasource.url=jdbc:mysql://localhost:3306/muxin-dev?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.pool-name=DatebookHikariCP
spring.datasource.hikari.max-lifetime=28740000
spring.datasource.hikari.connection-test-query=SELECT 1
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.locale=GMT+8
#mybatis配置
mybatis.type-aliases-package=com.congge.entity
mybatis.mapper-locations=classpath:/mybatis/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
#打印sql语句
logging.level.com.congge.mapper= debug
3、下面是netty服务的相关配置,也是本篇的核心所在,由于是整合了springboot,需要程序在启动的时候就要把netty服务器开启,因此,我们需要把netty的启动类做成bean被spring管理,
3.1 这里为了保证这个服务启动类不会受其他类干扰,直接做成了单例类,
/**
* 服务端基本配置,通过一个静态单例类,保证启动时候只被加载一次
* @author asus
*/
@Component
public class WssServer {
/**
* 单例静态内部类
*/
public static class SingletionWSServer {
static final WssServer instance = new WssServer();
}
public static WssServer getInstance() {
return SingletionWSServer.instance;
}
private EventLoopGroup mainGroup;
private EventLoopGroup subGroup;
private ServerBootstrap server;
private ChannelFuture future;
public WssServer() {
mainGroup = new NioEventLoopGroup();
subGroup = new NioEventLoopGroup();
server = new ServerBootstrap();
server.group(mainGroup, subGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WssServerInitialzer()); // 添加自定义初始化处理器
}
public void start() {
future = this.server.bind(8088);
System.err.println("netty 服务端启动完毕 .....");
}
}
3.2 初始化netty连接channel的相关配置
/**
* 初始化netty连接channel的相关配置,用于规定数据读写的相关规则,可以添加多项
* @author asus
*
*/
public class WssServerInitialzer extends ChannelInitializer{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//websocket基于http协议,所以需要http编解码器
pipeline.addLast(new HttpServerCodec());
//添加对于读写大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());
//对httpMessage进行聚合
pipeline.addLast(new HttpObjectAggregator(1024*64));
// ================= 上述是用于支持http协议的 ==============
//websocket 服务器处理的协议,用于给指定的客户端进行连接访问的路由地址
//比如处理一些握手动作(ping,pong)
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
//自定义handler
pipeline.addLast(new ChatHandler());
}
}
3.3 当springboot启动完毕,启动netty,
/**
* netty服务端启动加载配置
*
*/
@Component
public class NettybootServerInitConfig implements ApplicationListener{
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if(event.getApplicationContext().getParent() == null){
try {
WssServer.getInstance().start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
3.4 接下来就是前面讲到的一个很重要的组件,handler,当我们的handler类继承了这个SimpleChannelInboundHandler 类之后,你会发现里面有很多的可以重写的方法,这个里面的API也是我们下面做单聊用的API组件,有点儿类似于组件的生命周期概念,大家可以尝试一下加载所有的方法,然后打印出相关的语句进行调试一下,这里我就不对每一个做详细讲解了,
在上诉的一些组件中,我们这里要用到其中很重要的两个组件,channelRead0 和 handlerAdded ,其中前者是对应着每一个连接进来的客户端的一个channel,通过这个chennel我们可以获取客户端推送过来的消息,并发送消息响应客户端,第二个组件表示每一个和服务端建立连接的客户端,都会触发这个方法,我们运用这个原理,是不是可以设想一下,将每一个连接进来的客户端通过某种标识加入到一个地方,然后就可以互相发送消息了,不久实现单聊了吗?
其实实现的话就是这样实现的,具体的逻辑大家可以参考里面的注释部门,整个的实现原理如下,
1、netty 通过ChannelGroup 管理所有连接进来的channel,每个channel,因此可以将每个连接的channel加入到这个group中进行统一管理;
2、每个chennel对应着唯一的长ID或者端ID,通过这层关系,将chennel和ID进行绑定;
3、针对每一个发送消息的客户端,在read0这个方法中截取消息,并将消息转发到指定的chennel中
/**
* 聊天的ehandler TextWebSocketFrame 用于为websockt处理文本的对象
*
*/
public class ChatHandler extends SimpleChannelInboundHandler {
public static Map userMap = new HashMap<>();
// 用于记录和管理所有客户端的channel
// 用于记录和管理所有客户端的channle
public static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// 客户端创建的时候触发,当客户端连接上服务端之后,就可以获取该channel,然后放到channelGroup中进行统一管理
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("接收到了一个客户端的连接请求啦,当前的连接用户的channel的短ID是:" + ctx.channel().id().asShortText());
users.add(ctx.channel());
userMap.put(ctx.channel().id().asShortText(), ctx.channel());
}
// 客户端销毁的时候触发,
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("客户端断开,当前被移除的channel的短ID是:" + ctx.channel().id().asShortText());
}
//执行channel消息读取和发送的关键方法
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 获取当前的channel
Channel currentChannel = ctx.channel();
// 获取接收到的消息
String msgContent = msg.text();
// 这里直接使用channel的短ID作为标识 ,每个ID对应着当前的channel,可以认为是唯一的,利用这个特性,我们将channel和这个端ID进行绑定
String shortedId = ctx.channel().id().asShortText();
for (Channel userOne : users) {
if (!userMap.get(shortedId).equals(userOne)) {
userOne.writeAndFlush(new TextWebSocketFrame(msgContent));
}
}
}
}
启动springboot程序,还是使用我们之前的一个简易的聊天界面,可以看到,当我们打开页面的时候,里面的那个handlerAdded 方法就被触发了,
我们从一个页面发送消息,
来到另一个页面,可以看到如下内容,已经成功收到消息,这时候,我们在这个页面也发送一个消息,
可以看到,两个页面相互之间已经能够成功收到对方发来的消息了,那么真实的业务场景是怎么样的呢?
比如微信聊天,一对一聊天,对于每个微信用户来说都会有一个设备号或者注册成功后的用户ID号,那么就通过这个设备号或者用户ID然后和具体的channel进行绑定即可,我在上面做了简化处理,直接使用的是channel的短ID,实际业务中大家替换一下即可达到同样效果,
然后我们关闭其中一个页面看看,可以看到,通过handlerRemoved 这个方法被触发,可以截取到某个客户端对应的channel的连接断开了,是不是很强大,
其实采用netty作为聊天服务器最关键的部门就是这个组件了,搞清楚里面的原理就不会觉得很复杂了,组件里面还有其他的生命周期方法可以为我们处理在不同场景下的功能,比如异常处理等,大家可以在此基础上继续研究下,本篇整合到此结束,感谢观看!