在SpringBoot项目中启动NettyService
public class ApplicationStart implements CommandLineRunner {
private static Logger logger = LoggerFactory.getLogger(ApplicationStart.class);
@Autowired
private NettyService nettyService;
@Order(value=1)
public static void main(String[] args) throws UnknownHostException {
SpringApplication.run(ApplicationStart.class, args);
}
@Bean
public NettyService nettyService() {
return new NettyService();
}
@Override
public void run(String... args) throws Exception {
// TODO Auto-generated method stub
ChannelFuture future = new NettyService().nettyRun(9996);
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
logger.info("[系统消息]:NettyService 开始注销 !");
try {
nettyService.destroy();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
future.channel().closeFuture().syncUninterruptibly();
}
}
创建NettyService服务类
public class NettyService{
private static Logger logger = LoggerFactory.getLogger(NettyService.class);
private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
private final EventLoopGroup bossGroup = new NioEventLoopGroup();
private final EventLoopGroup workerGroup = new NioEventLoopGroup();
private Channel channel;
public ChannelFuture nettyRun(int port) throws Exception {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WebsocketChatServerInitializer())
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = bootstrap.bind(port).sync();
channel = future.channel();
logger.info("NettyService 已经启动...");
return future;
}
public void destroy() throws Exception {
if(channel != null) {
channel.closeFuture().sync();
}
channelGroup.close();
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
logger.info("NettyService 已停止运行【END】");
}
}
创建WebsocketChatServerInitializer类
public class WebsocketChatServerInitializer extends ChannelInitializer { // 1
@Override
public void initChannel(SocketChannel ch) throws Exception {// 2
ChannelPipeline pipeline = ch.pipeline();
// 处理日志
pipeline.addLast(new LoggingHandler(LogLevel.INFO));
/**
* 将字节解码为HttpRequest、HttpContent 和LastHttpContent。
* 并将HttpRequest、HttpContent 和LastHttpContent 编码为字节
*/
pipeline.addLast(new HttpServerCodec());
// 将http消息的多个部分组成一条完整的Http消息
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
// 写入一个文件的内容
pipeline.addLast(new ChunkedWriteHandler());
// 处理FullHttpRequest(那些不发送到/websocket URI的请求)
pipeline.addLast(new HttpRequestHandler("/websocket"));
/**
* 按照WebSocket 规范的要求,处理WebSocket 升级握手、PingWebSocketFrame 、PongWebSocketFrame
* 和CloseWebSocketFrame
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/websocket"));
// 处理TextWebSocketFrame 和握手完成事件
pipeline.addLast(new TextWebSocketFrameHandler());
}
}
创建HttpRequestHandler类
//继承SimpleChannelInboundHandler类处理FullHttpRequest消息
public class HttpRequestHandler extends SimpleChannelInboundHandler {
private final String wsUri;
private static final File INDEX;
static {
URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
try {
String path = location.toURI() + "nettyInfo.html";
path = !path.contains("file:") ? path : path.substring(5);
INDEX = new File(path);
} catch (URISyntaxException e) {
throw new IllegalStateException("Unable to locate nettyInfo.html", e);
}
}
public HttpRequestHandler(String wsUri) {
this.wsUri = wsUri;
}
@Override
public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
//如果请求了WebSocket协议升级,则增加引用计数(调用retain()方法)
//并将它传递给下一个ChannelInboundHandler
//或者说是不处理WebSocket相关的请求,将其转发给下一个handler
if (wsUri.equalsIgnoreCase(request.uri())) {
ctx.fireChannelRead(request.retain()); //2
} else {
//处理100 Continue请求以符合HTTP1.1 规范
if (HttpUtil.is100ContinueExpected(request)) {
send100Continue(ctx); //3
}
RandomAccessFile file = new RandomAccessFile(INDEX, "r");//4
HttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
boolean keepAlive = HttpUtil.isKeepAlive(request);
//如果请求了keep-alive,则添加所需要的HTTP头信息
if (keepAlive) { //5
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, file.length());
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response); //6
if (ctx.pipeline().get(SslHandler.class) == null) { //7
ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
} else {
ctx.write(new ChunkedNioFile(file.getChannel()));
}
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); //8
// 如果没有请求keep-alive,在写操作完成后关闭Channel
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE); //9
}
file.close();
}
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
Channel incoming = ctx.channel();
System.out.println("Client:"+incoming.remoteAddress()+"异常");
// 当出现异常就关闭连接
cause.printStackTrace();
ctx.close();
}
}
创建TextWebSocketFrameHandler类
/**
* @ClassName: TextWebSocketFrameHandler
* @Description: TODO(
* WebSocketFrame 中定义的对应6种帧的类型:
* BinaryWebSocketFrame 包含了二进制数据
* TextWebSocketFrame 包含了文本数据
* ContinuationWebSocketFrame 包含属于上一个BinaryWebSocketFrame或TextWebSocketFrame 的文本数据或者二进制数据
* CloseWebSocketFrame 表示一个CLOSE 请求,包含一个关闭的状态码和关闭的原因
* PingWebSocketFrame 请求传输一个PongWebSocketFrame
* PongWebSocketFrame 作为一个对于PingWebSocketFrame 的响应被发送
* )
*/
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler {
public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { // (1)
Channel incoming = ctx.channel();
for (Channel channel : channels) {
if (channel != incoming) {
channel.writeAndFlush(new TextWebSocketFrame("[" + incoming.remoteAddress() + "]" + msg.text()));
} else {
channel.writeAndFlush(new TextWebSocketFrame("[you]" + msg.text()));
}
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception { // (2)
Channel incoming = ctx.channel();
// Broadcast a message to multiple Channels
channels.writeAndFlush(new TextWebSocketFrame("[SERVER] - " + incoming.remoteAddress() + " 加入"));
channels.add(incoming);
System.out.println("Client:" + incoming.remoteAddress() + "加入");
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { // (3)
Channel incoming = ctx.channel();
// Broadcast a message to multiple Channels
channels.writeAndFlush(new TextWebSocketFrame("[SERVER] - " + incoming.remoteAddress() + " 离开"));
System.out.println("Client:" + incoming.remoteAddress() + "离开");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { // (5)
Channel incoming = ctx.channel();
System.out.println("Client:" + incoming.remoteAddress() + "在线");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception { // (6)
Channel incoming = ctx.channel();
System.out.println("Client:" + incoming.remoteAddress() + "掉线");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) // (7)
throws Exception {
Channel incoming = ctx.channel();
System.out.println("Client:" + incoming.remoteAddress() + "异常");
// 当出现异常就关闭连接
cause.printStackTrace();
ctx.close();
}
}
创建Chat.html聊天页面
聊天页面