系统整体功能描述
前端使用了vue框架实现用户登录、通讯录列表、联系人搜索以及聊天功能。具体的聊天功能使用了netty+WebSocket技术实现,所有的聊天数据都保存在系统本地,服务器只进行了数据转发。
联系人搜索
聊天详情
前端主要代码
1. 对websokcet连接进行封装socket.js
const defaultData = {
event: 'msg',
username: '',
from: '',
to: "all",
type: 'text',
data: ''
}
class WSocket {
wss;
constructor() {
if (!("WebSocket" in window)) {
alert("您的浏览器不支持 WebSocket!");
}
}
open (c) {
// 貌似不在短时间内调用onopen 会自动调用,所以在此处new
try {
this.wss = new WebSocket("ws://127.0.0.1:8088/ws");
} catch( err ) {
alert('连接服务器失败');
}
c = Object.assign(defaultData, c);
let _self = this;
this.wss.onopen = function() {
this.send(JSON.stringify(c));
setInterval(function(){
let c = {
event: 'heartbeat',
username: '',
from: '1015',
to: "1015",
type: 'text',
data: '心跳'
}
console.log("心跳")
_self.send(c)
}, 50000);
};
this.wss.onclose = function (e) {
console.log('websocket 断开: ' + e.code + ' ' + e.reason + ' ' + e.wasClean)
console.log(e)
}
}
message (f) {
this.wss.onmessage = function(evt) {
f(evt.data);
};
}
send (c) {
c = Object.assign(defaultData, c);
this.wss.send(JSON.stringify(c));
}
}
export default new WSocket();
2.使用vuex(至关重要)
import Vue from "vue";
import Vuex from "vuex";
import ws from "@/net/socket";
import { getLocal } from '@/utils/mylocal'
Vue.use(Vuex);
export default new Vuex.Store({
state: {
// 用户信息
user: getLocal('userInfo') || {},
currentChat: { // 当前窗口信息
who: '',
name: ''
},
isNeedPush: 0, // 控制了解界面刷新显示消息
},
mutations: {
// 初始化连接 注册
initConnect (state, callback) {
const u = {
event: "reg",
from: state.user.userId,
username: state.user.username,
to: "all",
data: state.user.username + '上线啦'
}
ws.open(u);
ws.message((msg)=>{
msg = JSON.parse(msg);
if (msg.event == 'msg') {
// 消息
callback(msg);
}
});
},
// 消息推送
send(state, conf) {
let _conf = {
event: "msg",
from: state.user.userId,
username: state.user.username,
to: "all",
data: ""
};
_conf = Object.assign(_conf, conf);
ws.send(_conf);
},
// 保存聊天记录 sessionStorage.historyChat
addChatStorage(state, info) {
let hc = (sessionStorage.historyChat && JSON.parse(sessionStorage.historyChat)) || {};
const _ct = {
sendobj: info.sendobj, // 谁发的
content: info.content, // 发送内容
myHeadUrl: info.myHeadUrl,
username: info.username, //发送人名称
headUrl: info.headUrl,
window: info.window, // 聊天窗口对象
date: Number(new Date())
}
// 聊天记录对应某个聊天窗口对象
if (!hc[_ct.window]) {
hc[_ct.window] = [];
}
hc[_ct.window].push(_ct);
sessionStorage.historyChat = JSON.stringify(hc);
},
/**
* 新消息提示
*/
newMsg (state, cwindow) {
// 先判断 如果是当前窗口,直接更新 无需加入新消息提示数组
if (cwindow == state.currentChat.who) {
state.isNeedPush++; // 改变即可
} else {
// // 没有则加入提示数组
// if (!state.noRead.includes(String(cwindow))) {
// state.noRead.push(String(cwindow));
// }
}
},
setUserInfo (state, data) {
Object.assign(state.user,data)
}
},
actions: {},
modules: {}
});
app.vue 中进行初始化websocket连接
聊天详情页面的处理逻辑
实现客户端的长连接以及心跳检测的配置等功能
主要代码
pom.xml依赖
io.netty
netty-all
4.1.32.Final
org.projectlombok
lombok
1.18.12
net.sf.json-lib
json-lib
2.2.3
jdk15
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
mysql
mysql-connector-java
runtime
netty及数据库配置
yml文件的具体配置
server:
port: 8090
netty:
tcp:
server:
host: 127.0.0.1
port: 8088
spring:
datasource: #数据库
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/vue_websocket_netty?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
mybatis:
mapper-locations: classpath:mapping/*Mapper.xml
type-aliases-package: com.netty.chat.entity
nettyServer类:
package com.netty.chat.chatRoom;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@Slf4j
public class NettyServer implements CommandLineRunner {
private static NettyServer nettyServer;
private ServerBootstrap server;
private EventLoopGroup boss;
private EventLoopGroup woker;
private ChannelFuture future;
@PostConstruct
public void init(){
nettyServer = this;
}
@Value("${netty.tcp.server.port}")
private int port;
private static class SingletionNettyServer{
static final NettyServer instance = new NettyServer();
}
public static NettyServer getInstance(){
return SingletionNettyServer.instance;
}
public void start() {
// 创建服务类
server = new ServerBootstrap();
// 创建boss和woker
boss = new NioEventLoopGroup();
woker = new NioEventLoopGroup();
try {
// 设置线程池
server.group(boss, woker);
// 设置channel工厂
server.channel(NioServerSocketChannel.class);
// 设置管道
server.childHandler(new WSServerInitializer());
log.info("通道配置");
// 服务器异步创建绑定
this.future = server.bind(nettyServer.port);
log.info("netty server 启动完毕,启动端口为:"+nettyServer.port);
// 等待服务端关闭
this.future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
woker.shutdownGracefully();
}
}
@Override
public void run(String... args) throws Exception {
this.start();
}
}
构建初始化类WSServerInitializer:
package com.netty.chat.chatRoom;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
public class WSServerInitializer extends ChannelInitializer {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//编解码
pipeline.addLast(new HttpServerCodec());
//大数据流
pipeline.addLast(new ChunkedWriteHandler());
//聚合httpMessage 响应或者请求
pipeline.addLast(new HttpObjectAggregator(1024*1024));
//心跳机制
pipeline.addLast(new IdleStateHandler(20, 40, 60));
pipeline.addLast(new HeartBeatHandler());
//webSocket的支持
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new MyServerHandler());
}
}
自定义处理类MyServerHandler:
package com.netty.chat.chatRoom;
import com.netty.chat.entity.Message;
import com.netty.chat.entity.UserChannel;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import net.sf.json.JSONObject;
public class MyServerHandler extends
SimpleChannelInboundHandler {
public static ChannelGroup clients = new DefaultChannelGroup(
GlobalEventExecutor.INSTANCE);
public static UserChannel userChannel = new UserChannel();
/**
* 监听客户端注册
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 新客户端连接,加入队列
clients.add(ctx.channel());
}
/**
* 监听客户端断开
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 整理队列
clients.remove(ctx.channel());
}
/**
* 读取客户端发过来的消息
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame tmsg)
throws Exception {
JSONObject jsonobject = JSONObject.fromObject(tmsg.text());
Message message= (Message)JSONObject.toBean(jsonobject, Message.class);
UserChannel.put(message.getFrom(),ctx.channel());
if(message.getEvent().equals("heartbeat")){
//心跳消息不做处理
System.out.println("心跳");
return;
}
if(message.getTo().equals("all")){
// 处理群聊消息
System.out.println("群聊");
for (Channel channel : clients) {
// 判断是否是当前用户的消息
if (channel != ctx.channel()) {
channel.writeAndFlush(msgPot(tmsg.text()));
} else {
// 自己
}
}
}else{
//
System.out.println("单聊");
Channel targetChannel = UserChannel.get(message.getTo());
targetChannel.writeAndFlush(msgPot(tmsg.text()));
}
}
/**
* 监听连接异常
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.close(); // 关闭
}
/**
* 封装消息
*
* @param msg
* @return
*/
public TextWebSocketFrame msgPot(String msg) {
return new TextWebSocketFrame(msg);
}
}
心跳检测类HeartBeatHandler:
package com.netty.chat.chatRoom;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//super.userEventTriggered(ctx, evt);
// 判断evt是否是idleStateEvent 读空闲/写空闲/读写空闲
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
System.out.println("读空闲...");
} else if (event.state() == IdleState.WRITER_IDLE) {
System.out.println("写空闲...");
} else if (event.state() == IdleState.ALL_IDLE) {
//System.out.println(CustomChannelHandler.clients.size());
Channel channel = ctx.channel();
channel.close();
//System.out.println(CustomChannelHandler.clients.size());
}
}
}
}
如果有大神觉得可以有更好的设计思路的话,欢迎留言。
前后台源码 :
https://gitee.com/smallgrey/netty_websocket_vue
https://github.com/smallgrey/vue_websocket_netty.git