一、前言

之前写过一篇 Spring 集成 WebSocket 协议的文章 —— Spring消息之WebSocket ,所以对于 WebSocket 协议的介绍就不多说了,可以参考这篇文章。这里只做一些补充说明。另外,Netty 对 WebSocket 协议的支持要比 Spring 好太多了,用起来舒服的多。

WebSocket 以帧的方式传输数据,每一帧代表消息的一部分。一个完整的消息可能会包含许多帧。

由 IETF 发布的 WebSocket RFC,定义了 6 种帧, Netty 为它们每种都提供了一个 POJO 实现。下表列出了这些帧类型,并描述了它们的用法。

二、聊天室功能说明

    1、A、B、C 等所有用户都可以加入同一个聊天室。

    2、A 发送的消息,B、C 可以同时收到,但是 A 收不到自己发送的消息。

    3、当用户长时间没有发送消息,系统将把他踢出聊天室。

   

三、聊天室功能实现

  1、Netty 版本

<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha2</version>
</dependency>

2、处理 HTTP 协议的 ChannelHandler —— 非 WebSocket 协议的请求,返回 index.html 页面

public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private final String wsUri;
private static File INDEX; static {
URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
try {
String path = location.toURI() + "index.html";
path = !path.contains("file:") ? path : path.substring(5);
INDEX = new File(path);
} catch (URISyntaxException e) {
e.printStackTrace();
}
} public HttpRequestHandler(String wsUri) {
this.wsUri = wsUri;
} @Override
protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
// 如果请求了Websocket,协议升级,增加引用计数(调用retain()),并将他传递给下一个 ChannelHandler
// 之所以需要调用 retain() 方法,是因为调用 channelRead() 之后,资源会被 release() 方法释放掉,需要调用 retain() 保留资源
if (wsUri.equalsIgnoreCase(request.uri())) {
ctx.fireChannelRead(request.retain());
} else {
//处理 100 Continue 请求以符合 HTTP 1.1 规范
if (HttpHeaderUtil.is100ContinueExpected(request)) {
send100Continue(ctx);
}
// 读取 index.html
RandomAccessFile randomAccessFile = new RandomAccessFile(INDEX, "r");
HttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
HttpHeaders headers = response.headers();
//在该 HTTP 头信息被设置以后,HttpRequestHandler 将会写回一个 HttpResponse 给客户端
headers.set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
boolean keepAlive = HttpHeaderUtil.isKeepAlive(request);
if (keepAlive) {
headers.setLong(HttpHeaderNames.CONTENT_LENGTH, randomAccessFile.length());
headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response);
//将 index.html 写给客户端
if (ctx.pipeline().get(SslHandler.class) == null) {
ctx.write(new DefaultFileRegion(randomAccessFile.getChannel(), 0, randomAccessFile.length()));
} else {
ctx.write(new ChunkedNioFile(randomAccessFile.getChannel()));
}
//写 LastHttpContent 并冲刷至客户端,标记响应的结束
ChannelFuture channelFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!keepAlive) {
channelFuture.addListener(ChannelFutureListener.CLOSE);
}
} } private 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 {
cause.printStackTrace();
ctx.close();
}

3、处理 WebSocket 协议的 ChannelHandler —— 处理 TextWebSocketFrame 的消息帧

/**
* WebSocket 帧:WebSocket 以帧的方式传输数据,每一帧代表消息的一部分。一个完整的消息可能会包含许多帧
*/
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { private final ChannelGroup group; public TextWebSocketFrameHandler(ChannelGroup group) {
this.group = group;
} @Override
protected void messageReceived(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
//增加消息的引用计数(保留消息),并将他写到 ChannelGroup 中所有已经连接的客户端
Channel channel = ctx.channel();
//自己发送的消息不返回给自己
group.remove(channel);
group.writeAndFlush(msg.retain());
group.add(channel);
} @Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//是否握手成功,升级为 Websocket 协议
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
// 握手成功,移除 HttpRequestHandler,因此将不会接收到任何消息
// 并把握手成功的 Channel 加入到 ChannelGroup 中
ctx.pipeline().remove(HttpRequestHandler.class);
group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel() + " joined"));
group.add(ctx.channel());
} else if (evt instanceof IdleStateEvent) {
IdleStateEvent stateEvent = (IdleStateEvent) evt;
if (stateEvent.state() == IdleState.READER_IDLE) {
group.remove(ctx.channel());
ctx.writeAndFlush(new TextWebSocketFrame("由于您长时间不在线,系统已自动把你踢下线!")).addListener(ChannelFutureListener.CLOSE);
}
} else {
super.userEventTriggered(ctx, evt);
}
}
}

 WebSocket 协议升级完成之后, WebSocketServerProtocolHandler 将会把 HttpRequestDecoder 替换为 WebSocketFrameDecoder,把 HttpResponseEncoder 替换为WebSocketFrameEncoder。为了性能最大化,它将移除任何不再被 WebSocket 连接所需要的 ChannelHandler。这也包括了 HttpObjectAggregator 和 HttpRequestHandler 。

    4、ChatServerInitializer —— 多个 ChannelHandler 合并成 ChannelPipeline 链

public class ChatServerInitializer extends ChannelInitializer<Channel> {

    private final ChannelGroup group;
private static final int READ_IDLE_TIME_OUT = 60; // 读超时
private static final int WRITE_IDLE_TIME_OUT = 0;// 写超时
private static final int ALL_IDLE_TIME_OUT = 0; // 所有超时 public ChatServerInitializer(ChannelGroup group) {
this.group = group;
} @Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
// 处理那些不发送到 /ws URI的请求
pipeline.addLast(new HttpRequestHandler("/ws"));
// 如果被请求的端点是 "/ws",则处理该升级握手
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// //当连接在60秒内没有接收到消息时,进会触发一个 IdleStateEvent 事件,被 HeartbeatHandler 的 userEventTriggered 方法处理
pipeline.addLast(new IdleStateHandler(READ_IDLE_TIME_OUT, WRITE_IDLE_TIME_OUT, ALL_IDLE_TIME_OUT, TimeUnit.SECONDS));
pipeline.addLast(new TextWebSocketFrameHandler(group)); }
}

ChatServerInitializer.java

tips:上面这些开箱即用 ChannelHandler 的作用,我就不一一介绍了,可以参考上一篇文章

  5、引导类 ChatServer

public class ChatServer {

    private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
private final EventLoopGroup group = new NioEventLoopGroup();
private Channel channel; public ChannelFuture start(InetSocketAddress address) {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new ChatServerInitializer(channelGroup));
ChannelFuture channelFuture = bootstrap.bind(address);
channelFuture.syncUninterruptibly();
channel = channelFuture.channel();
return channelFuture;
} public void destroy() {
if (channel != null) {
channel.close();
}
channelGroup.close();
group.shutdownGracefully();
} public static void main(String[] args) {
final ChatServer chatServer = new ChatServer();
ChannelFuture channelFuture = chatServer.start(new InetSocketAddress(9999));
// 返回与当前Java应用程序关联的运行时对象
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
chatServer.destroy();
}
});
channelFuture.channel().closeFuture().syncUninterruptibly();
} }

ChatServer.java

三、效果展示

在浏览器中输入 http://127.0.0.1:9999 即可看到预先准备好的 index.html 页面;访问 ws://127.0.0.1:9999/ws (可随意找一个 WebSocket 测试工具测试)即可加入聊天室。

有点 low 的聊天室总算是完成了,算是 Netty 对 HTTP 协议和 WebSocket 协议的一次实践吧!虽然功能欠缺,但千里之行,始于足下!不积硅步,无以至千里;不积小流,无以成江海!

参考资料:《Netty IN ACTION》

演示源代码:https://github.com/JMCuixy/NettyDemo/tree/master/src/main/java/org/netty/demo/chatroom

Netty 系列八(基于 WebSocket 的简单聊天室).的更多相关文章

  1. Flask基于websocket的简单聊天室

    1.安装gevent-websocket pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ gevent-websocket 2.cha ...

  2. workerman-chat(PHP开发的基于Websocket协议的聊天室框架)(thinkphp也是支持socket聊天的)

    workerman-chat(PHP开发的基于Websocket协议的聊天室框架)(thinkphp也是支持socket聊天的) 一.总结 1.下面链接里面还有一个来聊的php聊天室源码可以学习 2. ...

  3. 分享基于 websocket 网页端聊天室

    博客地址:https://ainyi.com/67 有一个月没有写博客了,也是因为年前需求多.回家过春节的原因,现在返回北京的第二天,想想,应该也要分享技术专题的博客了!! 主题 基于 websock ...

  4. .NET Core 基于Websocket的在线聊天室

    什么是Websocket 我们在传统的客户端程序要实现实时双工通讯第一想到的技术就是socket通讯,但是在web体系是用不了socket通讯技术的,因为http被设计成无状态,每次跟服务器通讯完成后 ...

  5. C#基于Socket的简单聊天室实践

    序:实现一个基于Socket的简易的聊天室,实现的思路如下: 程序的结构:多个客户端+一个服务端,客户端都是向服务端发送消息,然后服务端转发给所有的客户端,这样形成一个简单的聊天室功能. 实现的细节: ...

  6. 用swoole和websocket开发简单聊天室

    首先,我想说下写代码的一些习惯,第一,任何可配置的参数或变量都要写到一个config文件中.第二,代码中一定要有日志记录和完善的报错并记录报错.言归正传,swoole应该是每个phper必须要了解的, ...

  7. 基于WebSocket的简易聊天室

    用的是Flash + WebSocket 哦~ Flask 之 WebSocket 一.项目结构: 二.导入模块 pip3 install gevent-websocket 三.先来看一个一对一聊天的 ...

  8. 基于GUI的简单聊天室01

    运用了Socket编程,gui,流的读入和写出,线程控制等 思路: 1.首先是在客户端中先建立好聊天的GUI 2.建立服务器端,设置好端口号(用SocketServer),其中需要两个boolean变 ...

  9. 基于GUI的简单聊天室03

    上一版本,客户端关闭后会出现“socket close”异常问题,这个版本用捕捉异常来解决,实际上只是把异常输出的语句改为用户退出之类,并没真正解决 服务器类 package Chat03; /** ...

随机推荐

  1. 迈向高阶:优秀Android程序员必知必会的网络基础

    1.前言 网络通信一直是Android项目里比较重要的一个模块,Android开源项目上出现过很多优秀的网络框架,从一开始只是一些对HttpClient和HttpUrlConnection简易封装使用 ...

  2. 【翻译】Neural Collaborative Filtering--神经协同过滤

    [说明] 本文翻译自新加坡国立大学何向南博士 et al.发布在<World Wide Web>(2017)上的一篇论文<Neural Collaborative Filtering ...

  3. Python学习笔记【第十篇】:Python面向对象进阶

    保护对象的属性 如果有一个对象,当需要对其进行修改属性时,有2种方法 对象名.属性名 = 数据 ---->直接修改 对象名.方法名() ---->间接修改 为了更好的保存属性安全,即不能随 ...

  4. 判断浏览器是否支持HTML5 video

    话不多说,下面是我从W3C扒的判断浏览器是是否支持H5视频的代码,有需要的小伙伴,拿走不谢 HTML <div id="checkVideoResult"><bu ...

  5. 如何使用 GDB

    前期准备 启动GDB方法 设置运行参数 查看源码 断点break 使用 运行程序 查看运行时数据 查看内存数据 分割窗口 问题汇总 参考文献 GDB, The GNU Project debugger ...

  6. AI - Tips

    01- Playground http://playground.tensorflow.org TensorFlow的网页工具Playground提供了几种简单类型的data,可以调节网络结构.学习率 ...

  7. python学习的准备工作

    1.python安装 1.下载: https://www.python.org/downloads/windows/ 2.安装: 安装很简单,就是下一步,只是在最后要勾选上 Add Python 3. ...

  8. maven的标准pom.xml详解

    maven是构建和管理理项目的利器,pom.xml 是其核心.一个标准的pom.xml该怎么写?其中的标签又有什么意义?话不多说,请看代码: <?xml version="1.0&qu ...

  9. [干货,阅后进BAT不是梦]面试心得与总结---阿里、小米、腾讯

    之前实习的时候就想着写一篇面经,后来忙就给忘了,现在找完工作了,也是该静下心总结一下走过的路程了,我全盘托出,奉上这篇诚意之作,希望能给未来找工作的人一点指引和总结, 也希望能使大家少走点弯路 , 如 ...

  10. 32位汇编第五讲,逆向实战干货,(OD)快速定位扫雷内存.

    32位第五讲,逆向实战干货,快速定位扫雷内存. 首先,在逆向之前,大家先对OD有一个认识. 一丶OD的使用 标号1: 反汇编窗口 (显示代码的地址,二进制指令,汇编代码,注释) 标号2: 寄存器窗口( ...