Netty实现WebSocket聊天室

前面的文章主要以分析 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. 测试

页面样式着实丑的不忍直视,这不重要啦!!!Netty实现WebSocket聊天室


原文始发于微信公众号(程序员小潘):Netty实现WebSocket聊天室

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/29423.html

(1)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!