netty编程之基于websocket实现聊天功能
写在前面
源码 。
本文看下netty如何通过websocket实现聊天功能。
类似于实现http server,netty实现websocket也很简单,同样使用对应的编码器和解码器就行了,相关的有HttpServerCodec,HttpObjectAggregator,ChunkedWriteHandler。
1:编码
1.1: netty websocket server
server类:
package com.dahuyou.netty.chat.domain.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
@Component("nettyServer")
public class NettyServer {
private Logger logger = LoggerFactory.getLogger(NettyServer.class);
//配置服务端NIO线程组
private final EventLoopGroup parentGroup = new NioEventLoopGroup(); //NioEventLoopGroup extends MultithreadEventLoopGroup Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
private final EventLoopGroup childGroup = new NioEventLoopGroup();
private Channel channel;
public ChannelFuture bing(InetSocketAddress address) {
ChannelFuture channelFuture = null;
try {
ServerBootstrap b = new ServerBootstrap();
b.group(parentGroup, childGroup)
.channel(NioServerSocketChannel.class) //非阻塞模式
.option(ChannelOption.SO_BACKLOG, 128)
.childHandler(new MyChannelInitializer());
channelFuture = b.bind(address).syncUninterruptibly();
channel = channelFuture.channel();
} catch (Exception e) {
logger.error(e.getMessage());
} finally {
if (null != channelFuture && channelFuture.isSuccess()) {
logger.info("netty server start done. {}");
} else {
logger.error("netty server start error. {}");
}
}
return channelFuture;
}
public void destroy() {
if (null == channel) return;
channel.close();
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
public Channel getChannel() {
return channel;
}
}
MyChannelInitializer:
package com.dahuyou.netty.chat.domain.server;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import java.nio.charset.Charset;
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel) {
channel.pipeline().addLast("http-codec", new HttpServerCodec());
channel.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
channel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
// 在管道中添加我们自己的接收数据实现方法
channel.pipeline().addLast(new MyServerHandler());
}
}
其中设置了支持websocket相关的编解码器是关键。
MyServerHandler:
package com.dahuyou.netty.chat.domain.server;
import com.alibaba.fastjson.JSON;
import com.dahuyou.netty.chat.pojo.ClientMsgProtocol;
import com.dahuyou.netty.chat.types.util.ChannelHandler;
import com.dahuyou.netty.chat.types.util.MsgUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyServerHandler extends ChannelInboundHandlerAdapter {
private Logger logger = LoggerFactory.getLogger(MyServerHandler.class);
private WebSocketServerHandshaker handshaker;
/**
* 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
SocketChannel channel = (SocketChannel) ctx.channel();
logger.info("链接报告开始");
logger.info("链接报告信息:有一客户端链接到本服务端");
logger.info("链接报告IP:{}", channel.localAddress().getHostString());
logger.info("链接报告Port:{}", channel.localAddress().getPort());
logger.info("链接报告完毕");
ChannelHandler.channelGroup.add(ctx.channel());
}
/**
* 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
logger.info("客户端断开链接{}", ctx.channel().localAddress().toString());
ChannelHandler.channelGroup.remove(ctx.channel());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//http
if (msg instanceof FullHttpRequest) {
FullHttpRequest httpRequest = (FullHttpRequest) msg;
if (!httpRequest.decoderResult().isSuccess()) {
DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST);
// 返回应答给客户端
if (httpResponse.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(httpResponse.status().toString(), CharsetUtil.UTF_8);
httpResponse.content().writeBytes(buf);
buf.release();
}
// 如果是非Keep-Alive,关闭连接
ChannelFuture f = ctx.channel().writeAndFlush(httpResponse);
if (httpResponse.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
return;
}
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws:/" + ctx.channel() + "/websocket", null, false);
handshaker = wsFactory.newHandshaker(httpRequest);
if (null == handshaker) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), httpRequest);
}
return;
}
//ws
if (msg instanceof WebSocketFrame) {
WebSocketFrame webSocketFrame = (WebSocketFrame) msg;
//关闭请求
if (webSocketFrame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(), (CloseWebSocketFrame) webSocketFrame.retain());
return;
}
//ping请求
if (webSocketFrame instanceof PingWebSocketFrame) {
ctx.channel().write(new PongWebSocketFrame(webSocketFrame.content().retain()));
return;
}
//只支持文本格式,不支持二进制消息
if (!(webSocketFrame instanceof TextWebSocketFrame)) {
throw new Exception("仅支持文本格式");
}
String request = ((TextWebSocketFrame) webSocketFrame).text();
System.out.println("服务端收到:" + request);
ClientMsgProtocol clientMsgProtocol = JSON.parseObject(request, ClientMsgProtocol.class);
//1请求个人信息
if (1 == clientMsgProtocol.getType()) {
ctx.channel().writeAndFlush(MsgUtil.buildMsgOwner(ctx.channel().id().toString()));
return;
}
//群发消息
if (2 == clientMsgProtocol.getType()) {
TextWebSocketFrame textWebSocketFrame = MsgUtil.buildMsgAll(ctx.channel().id().toString(), clientMsgProtocol.getMsgInfo());
ChannelHandler.channelGroup.writeAndFlush(textWebSocketFrame);
}
}
}
/**
* 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
logger.info("异常信息:\r\n" + cause.getMessage());
}
}
对解码后的websocket消息按照不同的消息类型做了不同的处理,工作中如果有类似需求的话,这里也会是我们写代码的主要地方咯。
1.2: 通过spring boot启动netty websocket server
package com.dahuyou.netty.chat.trigger;
import com.dahuyou.netty.chat.domain.server.NettyServer;
import io.netty.channel.ChannelFuture;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import java.net.InetSocketAddress;
@SpringBootApplication
@ComponentScan("com.dahuyou.netty")
public class NettyApplication implements CommandLineRunner {
@Value("${netty.host}")
private String host;
@Value("${netty.port}")
private int port;
@Autowired
private NettyServer nettyServer;
public static void main(String[] args) {
SpringApplication.run(NettyApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
InetSocketAddress address = new InetSocketAddress(host, port);
ChannelFuture channelFuture = nettyServer.bing(address);
Runtime.getRuntime().addShutdownHook(new Thread(() -> nettyServer.destroy()));
channelFuture.channel().closeFuture().syncUninterruptibly();
}
}
1.3: websocket client
<!--
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-->
<title>大忽悠</title>
<script type="text/javascript" src="res/js/jquery.min.js"></script>
<script type="text/javascript" src="res/js/jquery.serialize-object.min.js"></script>
<script type="text/javascript" src="res/js/index.js"></script>
<style>
body{
background-image:url(res/img/1121416_071010Daniel12.jpg);
background-repeat:no-repeat;
background-size:cover;
font-family:”微软雅黑”;
}
#chatDiv{
position: relative;
margin:0 auto;
margin-top:150px;
width:839px;
height:667px;
background-color:#CCCCCC;
border-radius:3px;-moz-border-radius:3px;
}
</style>
</head>
<body>
<div id="chatDiv">
<!-- left -->
<div style="width:60px; height:667px; background-color:#2D2B2A; float:left;">
<!-- 头像 -->
<div style="width:35px; height:35px; margin:0 auto; margin-top:19px; margin-left:12px; float:left; border:1px solid #666666;border-radius:3px;-moz-border-radius:3px;">
<img src="res/img/Snipaste_2024-09-02_14-29-46.png" width="35px" height="35px"/>
</div>
<!-- 聊天 -->
<div style="width:28px; height:28px; margin:0 auto; margin-top:25px; margin-left:16px; float:left;">
<img src="res/img/chat.png" width="28px" height="28px"/>
</div>
<!-- 好友 -->
<div style="width:28px; height:28px; margin:0 auto; margin-top:20px; margin-left:16px; float:left;">
<img src="res/img/friend.png" width="28px" height="28px"/>
</div>
<!-- 收藏 -->
<div style="width:28px; height:28px; margin:0 auto; margin-top:20px; margin-left:16px; float:left;">
<img src="res/img/collection.png" width="28px" height="28px"/>
</div>
<!-- 设置 -->
<div style="width:20px; height:20px; margin:0 auto; margin-left:20px; float:left; position:absolute;bottom:0; margin-bottom:12px;">
<img src="res/img/set.png" width="20px" height="20px"/>
</div>
</div>
<!-- center -->
<div style="width:250px; height:667px; background-color:#EBE9E8; float:left;">
<div style=" background-image:url(res/img/search.png); background-repeat:no-repeat;margin:0 auto; margin-top:25px; padding-top:5px; padding-bottom:5px; width:210px; background-color:#DBD9D8;border-radius:3px;-moz-border-radius:3px; float:left; margin-left:20px; font-size:12px; color:#333333;text-indent:27px;">
找呀找呀找朋友!!!
</div>
<!-- friendList -->
<div id="friendList" style="float:left; margin-top:5px;">
<div style="width:250px; height:65px;">
<table style="width:240px; height:60px; margin:0 auto; margin-top:2px; background-color:#E5E5E5;">
<tr>
<td rowspan="2" width="50"><img src="res/img/2487997a9f274d86a61a8dffff3e0070_wang0907.jpg" width="50px" height="50px" style="border-radius:3px;-moz-border-radius:3px;"/></td>
<td style="color:#333333; text-indent:5px; font-size:12px; vertical-align:bottom; font-weight:bolder;">大忽悠</td>
</tr>
<tr>
<td style="color:#999999; text-indent:5px; font-size:10px;">你好,这里是大忽悠有限责任公司</td>
</tr>
</table>
</div>
</div>
</div>
<!-- chat -->
<div id="chat" style="width:529px; height:667px; background-color:#F5F5F5; float:right;">
<div style="width:16px; height:16px; background-image:url(res/img/exit.png); background-repeat:no-repeat; float:right; margin-top:10px; margin-right:30px;"></div>
<div style="width:16px; height:16px; background-image:url(res/img/min.png); background-repeat:no-repeat; float:right; margin-top:10px; margin-right:12px;"></div>
<div style="border-bottom:1px #E7E7E7 solid;width:509px; padding-top:0px; padding-left:20px; padding-bottom:10px; font-size:18px; font-weight:bolder;float:left;">
大忽悠(6666666)
</div>
<!-- 会话区域 begin -->
<div id="show" style="width:529px; height:450px; float:left;overflow-y:scroll;">
<!-- 消息块;系统 -->
<div class="msgBlockSystem" style="margin-left:30px; margin-top:15px; width:340px; height:auto; margin-bottom:15px; float:left;">
<div class="msgBlock_userHeadImg" style="float:left; width:35px; height:35px;border-radius:3px;-moz-border-radius:3px; background-color:#FFFFFF;">
<img class="point" src="res/img/Snipaste_2024-09-02_14-29-46.png" width="35px" height="35px" style="border-radius:3px;-moz-border-radius:3px;"/>
</div>
<div class="msgBlock_channelId" style="float:left; width:100px; margin-top:-5px; margin-left:10px; padding-bottom:2px; font-size:10px;">
大忽悠
</div>
<div class="msgBlock_msgInfo" style="height:auto;width:280px;float:left;margin-left:12px; margin-top:4px;border-radius:3px;-moz-border-radius:3px; ">
<div style="width:4px; height:20px; background-color:#CC0000; float:left;border-radius:3px;-moz-border-radius:3px;"></div>
<div class="point" style="float:left;width:260px; padding:7px; background-color:#FFFFFF; border-radius:3px;-moz-border-radius:3px; height:auto; font-size:12px;display:block;word-break: break-all;word-wrap: break-word;">
这里是大忽悠有限责任公司,你可别被我忽悠了<hr/><img width="260" height="260" src="res/img/2487997a9f274d86a61a8dffff3e0070_wang0907.jpg" />
</div>
</div>
</div>
<!-- 消息块;好友 -->
<div class="msgBlockFriendClone" style=" display:none; margin-left:30px; margin-top:15px; width:340px; height:auto; margin-bottom:15px; float:left;">
<div class="msgBlock_userHeadImg" style="float:left; width:35px; height:35px;border-radius:3px;-moz-border-radius:3px; background-color:#FFFFFF;">
<img class="headPoint" src="res/img/head5.jpg" width="35px" height="35px" style="border-radius:3px;-moz-border-radius:3px;"/>
</div>
<div class="msgBlock_channelId" style="float:left; width:100px; margin-top:-5px; margin-left:10px; padding-bottom:2px; font-size:10px;">
<!-- 名称 -->
</div>
<div class="msgBlock_msgInfo" style="height:auto;width:280px;float:left;margin-left:12px; margin-top:4px;border-radius:3px;-moz-border-radius:3px; ">
<div style="width:4px; height:20px; background-color:#CC0000; float:left;border-radius:3px;-moz-border-radius:3px;"></div>
<div class="msgPoint" style="float:left;width:260px; padding:7px; background-color:#FFFFFF; border-radius:3px;-moz-border-radius:3px; height:auto; font-size:12px;display:block;word-break: break-all;word-wrap: break-word;">
<!-- 信息 -->
</div>
</div>
</div>
<!-- 消息块;自己 -->
<div class="msgBlockOwnerClone" style=" display:none; margin-right:30px; margin-top:15px; width:340px; height:auto; margin-bottom:15px; float:right;">
<div style="float:right; width:35px; height:35px;border-radius:3px;-moz-border-radius:3px; background-color:#FFFFFF;">
<img class="headPoint" src="res/img/head3.jpg" width="35px" height="35px" style="border-radius:3px;-moz-border-radius:3px;"/>
</div>
<div class="msgBlock_msgInfo" style="height:auto;width:280px;float:left;margin-left:12px; margin-top:4px;border-radius:3px;-moz-border-radius:3px; ">
<div class="msgPoint" style="float:left;width:260px; padding:7px; background-color:#FFFFFF; border-radius:3px;-moz-border-radius:3px; height:auto; font-size:12px;display:block;word-break: break-all;word-wrap: break-word;">
<!-- 信息 -->
</div>
<div style="width:4px; height:20px; background-color:#CC0000; float:right;border-radius:3px;-moz-border-radius:3px;"></div>
</div>
</div>
<span id="msgPoint"></span>
</div>
<!-- 会话区域 end -->
<div style="width:100%; height:2px; float:left; background-color:#CCCCCC;"></div>
<div style="margin:0 auto; width:100%; height:149px; margin-top:5px; background-color:#FFFFFF; float:left;">
<textarea id="sendBox" style="font-size:14px; border:0; width:499px; height:80px; outline:none; padding:15px;font-family:”微软雅黑”;resize: none;"></textarea>
<div style="margin-top:20px; float:right; margin-right:35px; padding:5px; padding-left:15px; padding-right:15px; font-size:12px; background-color:#F5F5F5;border-radius:3px;-moz-border-radius:3px; cursor:pointer;" onclick="javascript:util.send();">发送(S)</div>
</div>
</div>
</div>
</body>
</html>
2:测试
运行:
发送一个消息:
酱!!!
写在后面
参考文章列表
idea中的html到浏览器中的乱码问题解决方案 。