Netty实现WebSocket聊天室

本文通过实战演示了如何利用Netty实现一个基于浏览器的WebSocket聊天室应用。从前端页面搭建到后端服务端的代码实现,包括HTML、JavaScript及Netty服务端的配置与处理流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面的文章主要以分析Netty源码为主,后面的文章以Netty实践为主。本篇文章会带大家使用Netty实现一个WebSocket聊天室。

代码已上传至Gitee:https://gitee.com/panchanghe/netty-project

1. 前端代码

因为是基于网页的聊天室,所以HTML等文件必不可少。笔者是后端开发,前端写的确实不怎么样,大家凑合看吧,主要是学习Netty的使用哈~

index.html:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>在线聊天室</title>
    <script src="http://apps.bdimg.com/libs/jquery/1.9.1/jquery.min.js"></script>
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
    <script src="/static/js/index.js"></script>
</head>
<body>
<div id="app">
    <div>
        <input type="text" v-model="sendMessage"/>
        <input type="button" onclick="send()" value="发送">
    </div>
    <hr>
    <div>
        <ul>
            <li v-for="m in messages">
                <span style="color: #555;">{{m.time}}</span>
                <span>{{m.from}}</span>
                :
                <span style="color: red;">{{m.text}}</span>
            </li>
        </ul>
    </div>
</div>
</body>
</html>

index.js:

var vue;
var ws;
window.onload = () => {
    vue = new Vue({
        el: '#app',
        data: {
            sendMessage:'',
            messages:[]
        }
    });

    ws = new WebSocket('ws://127.0.0.1:9999/ws');
    ws.onopen = function(){
        console.log('连接成功.');
    }
    ws.onmessage = function(e){
        //当客户端收到服务端发来的消息时,触发onmessage事件,参数e.data包含server传递过来的数据
        vue.messages.push(JSON.parse(e.data));
    }
    ws.onerror = function (eerror) {
        //如果出现连接、处理、接收、发送数据失败的时候触发onerror事件
        console.log(error);
    };
};


function send() {
    ws.send(vue.sendMessage);
}

2. 后端代码

后端才是核心,真正的硬货。

2.1 CharServer

聊天室的服务启动类。

public class CharServer {
	// 服务绑定的本地端口
	private final int port;

	public CharServer(int port) {
		this.port = port;
	}

	public static void main(String[] args) {
		new CharServer(9999).start();
	}

	// 启动服务
	public void start(){
		EventLoopGroup boss = new NioEventLoopGroup(1);
		EventLoopGroup worker = new NioEventLoopGroup();
		new ServerBootstrap()
				.group(boss, worker)
				.channel(NioServerSocketChannel.class)
				.childHandler(ChannelInit.INSTANCE)
				.bind(port);
	}
}

2.2 ChannelInit

客户端SocketChannel的初始化类。

@ChannelHandler.Sharable
public class ChannelInit extends ChannelInitializer<SocketChannel> {
	public static ChannelInit INSTANCE = new ChannelInit();

	@Override
	protected void initChannel(SocketChannel sc) throws Exception {
		ChannelPipeline pipeline = sc.pipeline();
		// HTTP请求编解码器
		pipeline.addFirst(new HttpServerCodec());
		// 文件分块传输
		pipeline.addFirst(new ChunkedWriteHandler());
		// HTTP聚合器
		pipeline.addLast(new HttpObjectAggregator(1024 * 1024 * 5));
		// 自定义的HTTP请求处理器
		pipeline.addLast(HttpRequestHandler.INSTANCE);
		// WebSocket服务端协议处理
		pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
		// WebSocket帧处理器
		pipeline.addLast(new TextWebSocketFrameHandler());
	}
}

2.3 HttpRequestHandler

因为要响应Html、Js等文件,所以服务需要能处理HTTP请求。

@ChannelHandler.Sharable
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
	public static HttpRequestHandler INSTANCE = new HttpRequestHandler();
	private static File BASE_FILE;
	static {
		BASE_FILE = new File(HttpRequestHandler.class.getResource("/static").getFile());
	}

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
		String uri = request.uri();
		if (uri.startsWith("/ws")) {
			// 升级为WebSocket协议
			ctx.fireChannelRead(request.retain());
		} else if (uri.startsWith("/static")) {
			// 文件传输
			HttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
			String contentType = "text/html;charset=UTF-8";
			if (uri.endsWith(".js")) {
				contentType = "application/javascript; charset=utf-8";
			}
			response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);

			RandomAccessFile file = new RandomAccessFile(new File(BASE_FILE, uri.replace("/static/", "")), "r");
			FileRegion fileRegion = new DefaultFileRegion(file.getChannel(), 0, file.length());
			response.headers().add(HttpHeaderNames.CONTENT_LENGTH, file.length());
			ctx.write(response);
			ctx.write(fileRegion);
			ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
		} else {
			// 处理HTTP请求 ...
		}
	}
}

2.4 TextWebSocketFrameHandler

WebSocket的文本帧处理器,当有新的客户端加入、或者有客户端发送消息时,广播通知所有客户端。

public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
	private static ChannelGroup group = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);

	@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
		if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
			group.add(ctx.channel());
			// 新的连接
			for (Channel channel : group) {
				String message = Message.fromSystem(ctx.channel().id() + " 加入聊天室...").toJson();
				channel.writeAndFlush(new TextWebSocketFrame(message));
			}
		} else {
			super.userEventTriggered(ctx, evt);
		}
	}

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
		// 客户端发送的文本
		String text = msg.text();
		// 构建Message
		String message = Message.from(ctx.channel().id().asShortText(), text).toJson();

		// 广播所有客户端
		for (Channel channel : group) {
			channel.writeAndFlush(new TextWebSocketFrame(message));
		}
	}
}

3. 测试

页面样式着实丑的不忍直视,这不重要啦!!!
image.png

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值