使用Netty-websocket构建一个简易的聊天室
What's netty? The king of network programming;
- 下面是使用Netty编写的一个very very simple 的一个聊天室服务,但是基本上涵盖了Netty的核心使用,直接上代码
- 关于Netty的知识点,本人计划单独写一篇以做一总结,一方面消化看过的知识,另一方面也希望能帮助到其他的小伙伴
直接上代码:
- 进行Http请求处理的Handler
package com.netty.websocket.ws;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import lombok.extern.slf4j.Slf4j;
/**
* Http请求处理handler
*
* @create 2021-01-18 10:55
*/
@Slf4j
public class HttpRequestHandler extends SimpleChannelInboundHandler {
private final String wsUrl;
public HttpRequestHandler(String wsUrl) {
this.wsUrl = wsUrl;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
//如果发送的是到websocket端点的连接,则传递给下一个Handler
if (wsUrl.equalsIgnoreCase(request.uri())) {
ctx.fireChannelRead(request.retain());
} else {
if (HttpUtil.is100ContinueExpected(request)) {
send100Continue(ctx);
}
DefaultHttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");
boolean keepAlive = HttpUtil.isKeepAlive(request);
if (keepAlive) {
response.headers()
.set(HttpHeaderNames.CONTENT_LENGTH, 1024 * 10)
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response);
ChannelFuture channelFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!keepAlive){
//如果不是keep-Alive连接,则在发送完一次response后关闭channel
channelFuture.addListener(ChannelFutureListener.CLOSE);
}
}
}
private static void send100Continue(ChannelHandlerContext ctx) {
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("caught exception:",cause);
ctx.close();
}
}
- 处理websocket消息的 WebSocketFrameHandler
package com.netty.websocket.ws;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import lombok.extern.slf4j.Slf4j;
/**
* TextWebSocketFrame处理handler
*
* @create 2021-01-18 14:07
*/
@Slf4j
public class WebSocketFrameHandler extends SimpleChannelInboundHandler {
private final ChannelGroup group;
public WebSocketFrameHandler(ChannelGroup group) {
this.group = group;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
//此处,因为是继承了 SimpleChannelInboundHandler,会在channelRead()方法自动释放资源
//为了保持异步发送中msg的引用不被清理,需要使用retain()使其引用计数+1
this.writeAndFlush(ctx,msg.retain());
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//握手完成后,上线通知
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
ctx.pipeline().remove(HttpRequestHandler.class);
TextWebSocketFrame socketFrame = new TextWebSocketFrame("client " + ctx.channel() + " joined");
writeAndFlush(ctx,socketFrame);
log.info("client channelId={} joined",ctx.channel().id());
group.add(ctx.channel());
} else {
super.userEventTriggered(ctx, evt);
}
}
/**
* 广播除自身以外的其它channel
* @param ctx
* @param socketFrame
*/
private void writeAndFlush(ChannelHandlerContext ctx,TextWebSocketFrame socketFrame) {
//使用group进行广播
for (Channel channel : group) {
if (channel.id()!=ctx.channel().id()){
channel.writeAndFlush(socketFrame);
}
}
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
log.info("client channelId={} exited", ctx.channel().id());
ctx.channel().close();
}
}
- websocket服务构建类 ChatServer
package com.netty.websocket.ws;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
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.util.concurrent.ImmediateEventExecutor;
import lombok.extern.slf4j.Slf4j;
/**
* initializer初始化channelPipeline
*
* @create 2021-01-18 14:29
*/
@Slf4j
public class ChatServer {
private final int port;
/**
* 不指定group线程数时默认实现为 NettyRuntime.availableProcessors() * 2
*/
private final EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private final EventLoopGroup workGroup = new NioEventLoopGroup();
private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
private Channel channel;
public ChatServer(int port) {
this.port = port;
}
/**
* 初始化websocket服务并绑定端口
* @throws Exception
*/
public void init() throws Exception {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec())
.addLast(new ChunkedWriteHandler())
.addLast(new HttpObjectAggregator(64 * 1024))
.addLast(new HttpRequestHandler("/ws"))
.addLast(new WebSocketServerProtocolHandler("/ws", true))
.addLast(new WebSocketFrameHandler(channelGroup));
}
});
bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
//禁用Nagle算法,小数据实时低延迟发送
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);
ChannelFuture future = bootstrap.bind(port);
future.syncUninterruptibly();
future.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
log.info("Websocket server has bind at port {}", port);
} else {
log.info("Websocket server failed to bind at port {}", port);
}
}
});
channel = future.channel();
}
/**
* 服务销毁时关闭channel并释放线程资源
*/
public void destroy(){
log.info("Websocket server is stopping...");
try {
if (channel!=null){
channel.close();
}
channelGroup.close();
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
} catch (Exception e) {
log.error("Websocket server destroy exception:",e);
}
log.info("Websocket server has stopped");
}
}
- 初始化服务启动类 ChatServerStarter
package com.netty.websocket.ws;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
* websocket服务bean
*
* @create 2021-01-18 15:36
*/
@Component
@Slf4j
public class ChatServerStarter {
@Value("${websocket.server.port}")
private String port;
static ChatServer chatServer;
@PostConstruct
public void serverStart() {
try {
chatServer = new ChatServer(Integer.parseInt(port));
chatServer.init();
} catch (Exception e) {
log.error("websocket server start error:", e);
}
}
@PreDestroy
public void serverDestroy() {
chatServer.destroy();
}
}
websocket连接测试可以用这个地址:websocket在线测试
拓展:目前只是有了实时的消息功能,相当与是模拟了一个聊天室,后续可以通过在服务中维护一个队列(可以借助Redis,es等)实现历史记录功能