一、前言

之前写过一篇 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. C# WebSocket Fleck 调用非托管C++ DLL 实现通信(使用char*接收)

    [DllImport(@"C:XXX.dll", CallingConvention = CallingConvention.StdCall)] unsafe public sta ...

  2. 关于C++用法的学习心得

    通过大一一学期对C++语言的学习,我感觉c++是一门有一定难度并且很有挑战性的科目,在c++学习过程中,我们懂得了其有很多的用法. 引用是C++引入的新语言特性,是C++常用的一个重要内容之一,正确. ...

  3. Eclipse 启动报错 An internal error occurred during: "Initializing Java Tooling"

    如图所示,我的Eclispe版本是Oxygen,启动的时候turnaround弹出这种错误. 多种情况会导致这种报错.通过[重置窗口布局],可解决大部分情况: 解决办法:点击菜单导航栏的Window ...

  4. 第89节:Java中的反射技术

    第89节:Java中的反射技术 反射技术是动态的获取指定的类,和动态的调用类中的内容(没有类前就可以创建对象,将对象的动作完成,这就是动态的获取指定的类). 配置文件把具体实现的类名称定义到配置文件中 ...

  5. 如何解决微信小程序界面适配问题-引用-生命周期回调函数-优化机制-样式引入

    如何解决微信小程序界面适配问题 .wxss page{ height: 100%; width:750rpx; } this.setData({ imageWidth: wx.getSystemInf ...

  6. 《http权威指南》读书笔记17

    概述 最近对http很感兴趣,于是开始看<http权威指南>.别人都说这本书有点老了,而且内容太多.我个人觉得这本书写的太好了,非常长知识,让你知道关于http的很多概念,不仅告诉你怎么做 ...

  7. input在ios safari中的内阴影解决方法

    最近发现 input 在 ios的 safari浏览器里显示时,会默认有内阴影,如下图 解决方法: input { -webkit-appearance: none; } -webkit-appear ...

  8. OWIN 自托管静态网站

    我们知道,借助OWIN,WebApi,SignalR,WCF 都可以创建自托管(Self-Host)实例,完全不需要IIS,静态网站也可以. 最近做一个服务器监控小工具,用 SignalR 通信,监控 ...

  9. python之asyncio

    asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持. asnycio是用来编写并发代码的库,python3.5以后使用async/await语法. asyncio 被用作 ...

  10. android自动化必备之界面元素

    包名&类名 packageName(包名) 应用的身份标识,系统通过包名识别不同的应用,如两个相同包名的应用在安装时候会覆盖 activityName(类名) Activity:android ...