一、导入Netty依赖包:
<!-- 导入Netty依赖包 -->
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.51.Final</version>
</dependency>
<dependencies>
二、服务端实现:
1.创建WebSocket心跳处理类:
public class WSHRHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {//客户端长时间没发心跳包时,会触发此方法
if (!(evt instanceof IdleStateEvent)) {
return;
}
IdleStateEvent state = (IdleStateEvent) evt;
if (state.state() == IdleState.READER_IDLE) { //读空闲
return;
}
if (state.state() == IdleState.WRITER_IDLE) { //写空闲
return;
}
if (state.state() == IdleState.ALL_IDLE) { //客户端连接关闭
ctx.channel().close();
return;
}
}
}
2.创建用户连接列表管理类:
public class WSUserChannel {
private static WSUserChannel I = new WSUserChannel();
private Map<String, Channel> channelMap = new HashMap<>();
public void addChannel(String userNo, Channel channel) {
channelMap.put(userNo, channel);
}
public void remove(String userNo) {
channelMap.remove(userNo);
}
public void removeByChannel(String channelId) {
Set<String> userNoSet = channelMap.keySet();
for (String userNo : userNoSet) {
Channel channel = channelMap.get(userNo);
if (channel.id().asLongText().equals(channelId)) {
channelMap.remove(userNo);
return;
}
}
}
public Channel getChannel(String userNo) {
return channelMap.get(userNo);
}
}
3.创建WebSocket消息处理类:
(1)创建消息实体类:
//消息实现类
public class Message {
public MessageHead head; //头信息,消息类别等
public MessageBody body; //消息内容
}
//消息实现类
public class MessageHead {
public String userNo; //用户id
public int type; //大类别,区分登录、登出、普通消息等
}
//消息实现类
public class MessageBody {
public String senderUserNo; //发送方用户Id
public String receiverUsrNo; //接收方用户Id
public int msgType; //小类别,聊天消息类别
public String content; //消息内容
}
(2)创建消息处理类:
public class WSMsgHandler extends SimpleChannelInboundHandler<Object> {
private WebSocketServerHandshaker handshaker;
//所有用户连接
private static ChannelGroup clientList = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Autowired
private WSUserChannel userChannel;//用户通道列表,以userNo为Key,Channel为value
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {//新用户连接时触发
clientList.add(ctx.channel()); //将用户连接保存起来
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {//接收消息
System.out.println("服务器 ->>>>> channelRead0");
if (msg instanceof FullHttpRequest) {
processHttpMsg(ctx, (FullHttpRequest) msg);
} else if (msg instanceof WebSocketFrame) {
processWebSocketMsg(ctx, (WebSocketFrame) msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {//消息接收完成
System.out.println("服务器 ->>>>> channelReadComplete");
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {//某个用户连接异常
System.out.println("服务器 ->>>>> exceptionCaught");
cause.printStackTrace();
userChannel.removeByChannel(ctx.channel().id().asLongText()); //移除这个用户的连接
ctx.channel().close(); //关闭这个用户的连接
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {//某个用户连接断开
userChannel.removeByChannel(ctx.channel().id().asLongText()); //移除这个用户的连接
}
private void processHttpMsg(ChannelHandlerContext ctx, FullHttpRequest req) {
// 如果HTTP解码失败,则返回HTTP异常
if (!req.decoderResult().isSuccess() || !"websocket".equals(req.headers().get("Upgrade"))) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return;
}
// 构造握手响应返回
WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory("ws:localhost:8080/websocket", null, false);
handshaker = factory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), req);
}
}
private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, DefaultFullHttpResponse res) {
// 返回应答给客户端
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
}
// 如果是非Keep-Alive,关闭连接
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
private boolean isKeepAlive(FullHttpRequest req) {
return false;
}
private void processWebSocketMsg(ChannelHandlerContext ctx, WebSocketFrame frame) {
// 判断是否关闭链路的指令
if (frame instanceof CloseWebSocketFrame) {
System.out.println("服务器 ->>>>> 收到关闭链路的指令");
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}
// 判断是否为Ping消息
if (frame instanceof PingWebSocketFrame) {
System.out.println("服务器 ->>>>> Ping消息");
ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
return;
}
// 接收文本消息
if (frame instanceof TextWebSocketFrame) {
String body = ((TextWebSocketFrame) frame).text();
System.out.println("服务器 ->>>>> 收到新消息:" + body);
parseBody(body, ctx.channel());
}
// 发送消息
System.out.println("服务器 ->>>>> 发送响应消息");
ctx.channel().write(new TextWebSocketFrame("服务器应答消息"));
}
private void parseBody(String body, Channel channel) {
Message msg = null; //调用gson解析body为Message
switch (msg.head.type) {
case 1: //登录时保存用户连接
userChannel.addChannel(msg.head.userNo, channel);
break;
case -1://登出时移出用户连接
userChannel.remove(msg.head.userNo);
break;
case 2://心跳包
break;
case 3://普通聊天消息
//1.保存消息到DB中
saveMsg(msg.body);
//2.转发消息给接收的用户
sendMsgToOtherUser(msg);
break;
}
}
private void saveMsg(MessageBody msg) {
//此处保存消息到DB
}
private void sendMsgToOtherUser(Message msg) {//转发消息给接收的用户
Channel channel = userChannel.getChannel(msg.body.receiverUsrNo); //获取接收方的连接
if(channel == null){ //不在线,不转发
return;
}
channel.writeAndFlush(new TextWebSocketFrame("json串")); //将msg转为json发出去
}
}
4.创建WebSocket通道初始化类设置:
public class WSChannelInitializer extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//将请求一应答消息编码或解码为HTTP消息
pipeline.addLast("http-codec", new HttpServerCodec());
//将HTTP消息的多个部分组合成一条完整的HTTP消息
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
//向客户端发送HTML5文件,用于支持浏览器与服务器进行WebSocket通信
pipeline.addLast("http-chunked", new ChunkedWriteHandler());
//只允许ws方式连接
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
//设置空闲时间,第1参数为读空闲,第2参数为写空闲,第3参数为读写空闲
pipeline.addLast(new IdleStateHandler(5, 5, 10));
//添加心跳超时处理,WSHRHandler为自定义类
pipeline.addLast(new WSHRHandler());
//添加消息处理适配器
pipeline.addLast(new WSMsgHandler());
}
}
5.创建WebSocket服务端启动类:
(1)在工程的中添加websocket服务端口:
websocket: #自定义名称,websocket服务器端口号
port: 10001
(2)创建服务端启动类:
@Component
public class WSServer {
private EventLoopGroup bossGroup; //主线程池,接收客户端连接
private EventLoopGroup workerGroup; //从(工作)线程池,进行读写消息
private ServerBootstrap serverBootstrap; //服务端启动工具
private ChannelFuture channelFuture;
@Value("${websocket.port}")
private int port;
//初始化
public WSServer() {
bossGroup = new NioEventLoopGroup(); //主线程池
workerGroup = new NioEventLoopGroup(); //从(工作)线程池
serverBootstrap = new ServerBootstrap(); //配置主从线程池和通道处理类
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class) //配置主从线程池和通道处理类
.option(ChannelOption.SO_BACKLOG, 1024) //设置队列中等待连接的个数
.childOption(ChannelOption.SO_KEEPALIVE, true) //保持连接状态
.childHandler(new WSChannelInitializer()); //配置消息处理类
}
//启动
public void start() {
//绑定指定端口,并启动WebSocket服务器
channelFuture = serverBootstrap.bind(port); //.sync()为同步方式启动
System.out.println("WebSocket服务端启动成功");
}
}
6.Spring容器启动时启动WebSocket服务端:
@Component //Spring容器启动时触发,启动WebSocket服务端
public class NettyApplicationListener implements org.springframework.context.ApplicationListener<ContextRefreshedEvent> {
@Autowired
private WSServer wsServer; //WebSocket服务端启动类
@Override
public void onApplicationEvent(ContextRefreshedEvent contextEvent) {
if (contextEvent.getApplicationContext().getParent() == null) {
try {
wsServer.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
7.SpringBoot的Application类:
@SpringBootApplication
public class NettyServerApplication {
public static void main(String[] args) {
SpringApplication.run(NettyServerApplication.class, args);
}
}