1. 前言
先看实现效果:
由于篇幅有限,而且代码量还是有的,所以下面只给出重要部分的代码,详细大家可以去项目里面看,完整代码已经完全开源。(仓库链接在文章末尾)
到这里我们可以看到接下来要实现的内容包括:
- 聊天主页面收到新消息后提示并刷新最新消息。
- 消息已读/未读状态实时变更。
- 消息实时收与发。
- 聊天页面收到新消息或者发送了消息,聊天主页面消息展示同步更新。(视频未展示)
只要思路正确,最后实现出效果所产生的代码量并不是很多。
寥寥的四条实现效果,可能并不多,但是你要学习的东西却与之成反比例。
通过本篇文章您大至能够了解到或者学习到:
- Netty的基础知识
- websocket基础知识
- 小程序开发基础知识(uni-app vscode开发版)
- vue、vuex的一些内容
2. 前置知识准备
这里说一下实现这些功能需要具备的一些基础,读者可以进一步的深入了解,更有利于自身的学习与成长,仅仅介绍技术的基础,认识到这个是做什么的,不深入讨论某些功能。
2.1 简单介绍一下Netty
Netty是什么?Netty 是一个基于 Java 的高性能网络应用框架,主要用于开发客户端-服务器 (Client-Server) 的通信程序,支持多种传输协议(如 TCP、UDP、HTTP 等)。它是一个异步事件驱动的网络框架,能够大幅简化网络编程,特别是在高并发场景下。
- 异步和事件驱动
基于事件驱动机制,通过Channel
、EventLoop
、Future
等模型实现高效的异步通信。 - 多协议支持
支持 TCP、UDP、HTTP/2、WebSocket 等协议,也可以自定义协议。 - 高性能
- 线程模型优化:采用 Reactor 模式,使用少量线程处理大量连接。
- 内存优化:通过对象池和零拷贝技术优化内存使用。
- 可扩展性
通过ChannelHandler
和Pipeline
实现灵活的业务逻辑扩展。(重要,本文介绍的内容基于该特性实现)
它的一些应用场景:
- 即时通讯:IM 系统、聊天服务(如微信、QQ)。
- 网关系统:微服务网关、API 网关。
- 高性能 HTTP 服务:如 HTTP 反向代理、流媒体服务。
- 游戏服务器:需要长连接和低延迟的实时通信场景。
- 分布式系统:消息队列、RPC 框架的底层实现。
选择使用Netty,它有这比较丰富的API和工具,简化socket编程,在了解它的前提下可以基于它快速构建自己的应用。
我有一篇文章介绍了Netty的一部分源码内容,感兴趣的可以去研究,看源码是一项有挑战性的事情。Netty部分源码
2.2 WebSocket
WebSocket表示一种网络传输协议,位于OSI模型的应用层,并且依赖于传输层的TCP协议。它是一种不同于http的协议,但是RFC 6455中:it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries(WebSocket通过HTTP端口80和443进行工作,并支持HTTP代理和中介),为了实现兼容性,WebSocket握手使用HTTP Upgrade头从HTTP协议更改为WebSocket协议。
WebSocket协议支持Web浏览器(或其他客户端应用程序)与Web服务器之间的交互,具有较低的开销,便于实现客户端与服务器的实时数据传输。
2.3 开发工具
使用的是uniapp跨端开发语言,实际使用和vue一样,只是有一些新组件,看官方文档就知道如何使用。
开发工具使用的是vscode,至于如何在vscode上面开发uniapp,要先调教一下vscode,笔者是参考的这篇文章。
小程序开发工具下载
可以去微信开发者平台注册一个账号,拿到appid后面方便自己调试使用。
3. 数据库设计
看到这里,相信你已经对用到的技术和工具有了大致的了解,现在下面开始动手如何去实现。
首先我们需要一张消息表来记录各自收发的信息,不能离开聊天界面把消息都丢了,当然如果你不需要存储消息,只是即时聊天,也可以不用搞一个表去存储消息。
create table tbl_message
(
id varchar(32) primary key,
message_type smallint not null comment '消息类型:待办通知:0 申请结果通知:1',
read_state smallint not null default 0 comment '阅读状态:0未读,1已读',
content varchar(1024) not null comment '消息内容',
message_receiver_id varchar(32) not null comment '消息接收者id',
message_sender_id varchar(32) not null comment '消息发送者id',
create_time bigint not null comment '创建时间',
update_time bigint not null comment '修改时间',
constraint message_receiver_fk foreign key (message_receiver_id) references tbl_user (id),
constraint message_sender_fk foreign key (message_sender_id) references tbl_user (id)
) comment '消息表';
避免篇幅过长这里的用户表就不贴出了。
位置在这里:src/main/resources/db/migration/V1.0__init.sql
3. 后端
netty的依赖要引入到项目中:
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.110.Final</version>
</dependency>
创建Netty启动类:
@Component
@Slf4j
public class NettyBootstrapRunner implements ApplicationRunner, ApplicationListener<ContextClosedEvent>, ApplicationContextAware {
@Value("${netty.websocket.port}")
private int port;
@Value("${netty.websocket.pemFile}")
private String pemFile;
@Value("${netty.websocket.keyFile}")
private String keyFile;
private Channel serverChannel;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void run(ApplicationArguments args) throws Exception {
// 使用自签名证书进行 SSL 配置
ApplyRoomRecordConfig globalConfig = applicationContext.getBean(ApplyRoomRecordConfig.class);
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.localAddress(new InetSocketAddress(port));
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
addSslHandler(pipeline, socketChannel, globalConfig);
pipeline.addLast(new HttpServerCodec());//请求解码器
pipeline.addLast(new HttpObjectAggregator(65536));//将多个消息转换成单一的消息对象
pipeline.addLast(new ChunkedWriteHandler());//支持异步发送大的码流,一般用于发送文件流
pipeline.addLast(new WebSocketServerCompressionHandler());//压缩处理
pipeline.addLast(applicationContext.getBean(UserAuthHandler.class));// 用户认证处理
// 参数配置请百度
pipeline.addLast(new WebSocketServerProtocolHandler("/websocket", null, true, 16384, false, true, 60000L));//websocket协议处理
pipeline.addLast(applicationContext.getBean(SocketConnectedHandler.class)); // 自定义处理器,处理消息发送与在线统计
}
});
serverChannel = serverBootstrap.bind().sync().channel();
log.info("websocket 服务启动,port={}", this.port);
serverChannel.closeFuture().sync();
} catch (Exception e) {
log.error(e.getMessage());
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
if (this.serverChannel != null) {
this.serverChannel.close();
}
log.info("websocket 服务停止");
}
private void addSslHandler(ChannelPipeline pipeline, SocketChannel socketChannel, ApplyRoomRecordConfig globalConfig) {
// 取决于你的配置,如果配置了是,那么请您同时配置pem和key文件
// 这两个文件请放在resource目录下
if (globalConfig.getUseWebsocketSSL())