基于 Netty 实现 WebSocket 服务器

  1. WebSocket 工作机制
  2. 编码
    1. 运行
  3. 完整代码
    1. 后台代码
    2. 网页代码

WebSocket 工作机制

WebSocket 是由 HTML5 提出的一个独立的协议标准。WebSocket 可以分为协议(Protocol)和API两部分,分别由 IETF 和 W3C 制定了标准。它跟 HTTP 协议基本没有关系,只是为了兼容现有浏览器的握手规范而对 HTTP 协议的一种补充。更加确切的说 WebSocket 利用了 HTTP 协议来建立连接,仅此而已

WebSocket 是一种双向通信协议,并且是二进制协议

工作原理如上图

1、先利用 HTTP 连接后台,连接的目的是希望能将会话升级成 WebSocket 协议(Upgrade WebSocket)

2、服务器同意后,客户端会再建立 WebSocket 连接

3、WebSocket 服务器端和客户端都能主动向对方发送或接收数据

编码

核心代码 Handler 注册

ch.pipeline().addLast("decode", new HttpRequestDecoder());
ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
ch.pipeline().addLast("encode", new HttpResponseEncoder());
ch.pipeline().addLast("http-servlet", new MyHttpServlet());
ch.pipeline().addLast("ws-codec", new WebSocketServerProtocolHandler("/ws"));
ch.pipeline().addLast("ws-servlet", new MyWsServlet());

核心代码 Http Servlet

private class MyHttpServlet extends SimpleChannelInboundHandler<FullHttpRequest> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        if (request.uri().equals("/")) {
            DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, indexPage.capacity());
            response.content().writeBytes(indexPage.duplicate());
            ctx.writeAndFlush(response);
        } else if (request.uri().equals("/ws")) {
            ctx.fireChannelRead(request.retain());// 转到webSocket 协议进行处理
        }
    }
}

核心代码 WebSocket Servlet


private class MyWsServlet extends  SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        // 接收客户端发送的消息
        System.out.println(msg.text());
        channels.writeAndFlush(new TextWebSocketFrame(msg.text()));
        if (msg.text().equals("add")) {
            channels.add(ctx.channel());
        }
    }
}

运行

打开浏览器,输入 http://127.0.0.1:8080/

完整代码

后台代码

package com.shar.netty.netty.websocket;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URL;
import java.nio.ByteBuffer;

public class DanmuServer {

    ChannelGroup channels;

    private ByteBuf indexPage;
    {
        try {
            initStaticPage();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void openServer(int port) throws InterruptedException {
        ServerBootstrap bootstrap = new ServerBootstrap();
        ChannelFuture sync = bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup(8))

                .channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) {
                        ch.pipeline().addLast("decode", new HttpRequestDecoder());
                        ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
                        ch.pipeline().addLast("encode", new HttpResponseEncoder());
                        ch.pipeline().addLast("http-servlet", new MyHttpServlet());
                        ch.pipeline().addLast("ws-codec", new WebSocketServerProtocolHandler("/ws"));
                        ch.pipeline().addLast("ws-servlet", new MyWsServlet());

                    }
                }).bind(port).sync();
        channels=new DefaultChannelGroup(sync.channel().eventLoop());
        System.out.println("服务开启成功");
    }

    private void initStaticPage() throws Exception {

        URL url = getClass().getResource("/WebsocketDanMu.html");
        String path = url.getFile();
        path = !path.contains("file:") ? path : path.substring(5);
        RandomAccessFile file = new RandomAccessFile(path, "r");//4
        ByteBuffer dst = ByteBuffer.allocate((int) file.length());
        file.getChannel().read(dst);
        dst.flip();
        indexPage = Unpooled.wrappedBuffer(dst);
        file.close();
    }

    private class MyHttpServlet extends SimpleChannelInboundHandler<FullHttpRequest> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
            if (request.uri().equals("/")) {
                DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
                response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
                response.headers().set(HttpHeaderNames.CONTENT_LENGTH, indexPage.capacity());
                response.content().writeBytes(indexPage.duplicate());
                ctx.writeAndFlush(response);
            } else if (request.uri().equals("/ws")) {
                ctx.fireChannelRead(request.retain());// 转到webSocket 协议进行处理
            }
        }
    }

    private class MyWsServlet extends  SimpleChannelInboundHandler<TextWebSocketFrame> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
            // 接收客户端发送的消息
            System.out.println(msg.text());
            channels.writeAndFlush(new TextWebSocketFrame(msg.text()));
            if (msg.text().equals("add")) {
                channels.add(ctx.channel());
            }
        }
    }

    public static void main(String[] args) throws InterruptedException, IOException {
        DanmuServer server=new DanmuServer();
        server.openServer(8080);
        System.in.read();
    }
}

网页代码

文件名 WebsocketDanMu.html,放到/resources文件夹下

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta name="Keywords" content="danmu">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>弹幕网站</title>
    <style type="text/css">
        body {
            font-size: 12px;
            font-family: "微软雅黑";
        }


        * {
            margin: 0;
            padding: 0;
        }
        /* screen start*/
        .screen {
            width: 300px;
            height: 100px;
            background: #669900;
        }


        .dm {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
            display: none;
        }


        .dm .d_screen .d_del {
            width: 38px;
            height: 38px;
            background: #600;
            display: block;
            text-align: center;
            line-height: 38px;
            text-decoration: none;
            font-size: 20px;
            color: #fff;
            border-radius: 19px;
            border: 1px solid #fff;
            z-index: 2;
            position: absolute;
            right: 20px;
            top: 20px;
            outline: none;
        }


        .dm .d_screen .d_del:hover {
            background: #F00;
        }


        .dm .d_screen .d_mask {
            width: 100%;
            height: 100%;
            background: #000;
            position: absolute;
            top: 0;
            left: 0;
            opacity: 0.6;
            filter: alpha(opacity = 60);
            z-index: 1;
        }


        .dm .d_screen .d_show {
            position: relative;
            z-index: 2;
        }


        .dm .d_screen .d_show div {
            font-size: 26px;
            line-height: 36px;
            font-weight: 500;
            position: absolute;
            top: 76px;
            left: 10;
            color: #fff;
        }
        /*end screen*/


        /*send start*/
        .send {
            width: 100%;
            height: 76px;
            position: absolute;
            bottom: 0;
            left: 0;
            border: 1px solid red;
        }


        .send .s_filter {
            width: 100%;
            height: 76px;
            background: #000;
            position: absolute;
            bottom: 0;
            left: 0;
            opacity: 0.6;
            filter: alpha(opacity = 60);
        }


        .send  .s_con {
            width: 100%;
            height: 76px;
            position: absolute;
            top: 0;
            left: 0;
            z-index: 2;
            text-align: center;
            line-height: 76px;
        }


        .send .s_con .s_text {
            width: 800px;
            height: 36px;
            border: 0;
            border-radius: 6px 0 0 6px;
            outline: none;
        }


        .send .s_con .s_submit {
            width: 100px;
            height: 36px;
            border-radius: 0 6px 6px 0;
            outline: none;
            font-size: 14px;
            color: #fff;
            background: #65c33d;
            font-family: "微软雅黑";
            cursor: pointer;
            border: 1px solid #5bba32;
        }


        .send .s_con .s_submit:hover {
            background: #3eaf0e;
        }
        /*end send*/
    </style>


</head>
<body>
<a href="#" id="startDm" style="font-size: 2rem">开启弹幕</a>
<!-- dm start -->
<div class="dm">
    <!-- d_screen start -->
    <div class="d_screen">
        <a href="#" class="d_del">X</a>
        <div class="d_mask"></div>
        <div class="d_show">
        </div>
    </div>
    <!-- end d_screen -->


    <!-- send start -->
    <div class="send">
        <div class="s_filter"></div>
        <div class="s_con">
            <input type="text" class="s_text" /> <input type="button"
                                                        value="发表评论" class="s_submit" id="btn"/>
        </div>
    </div>
    <!-- end send -->
</div>
<!-- end dm-->
<script type="text/javascript"
        src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.0.js"></script>

<script type="text/javascript" >

    String.prototype.endWith=function(str){
        if(str==null||str==""||this.length==0||str.length>this.length)
            return false;
        if(this.substring(this.length-str.length)==str)
            return true;
        else
            return false;
        return true;
    }

    String.prototype.startWith=function(str){
        if(str==null||str==""||this.length==0||str.length>this.length)
            return false;
        if(this.substr(0,str.length)==str)
            return true;
        else
            return false;
        return true;
    }
</script>
<!--<script type="text/javascript" src="websocket.js"></script>-->
<script type="text/javascript">
    $(function() {

        $("#startDm,.d_del").click(function() {
            $("#startDm,.dm").toggle(1000);
//init_screen();
        });
        $("#btn").click(function(){
            send();
        });
        $(".s_text").keydown(function() {
            var code = window.event.keyCode;





            if (code == 13)//回车键按下时,输出到弹幕


            {
                send();
            }
        });


    });


    function launch()
    {

        var _height = $(window).height();
        var _left = $(window).width() - $("#"+index).width();
        var time=10000;
        if(index%2==0)
            time=20000;
        _top+=80;
        if(_top>_height-100)
            _top=80;
        $("#"+index).css({
            left:_left,
            top:_top,
            color:getRandomColor()

        });
        $("#"+index).animate({
                left:"-"+_left+"px"},
            time,
            function(){});
        index++;
    }



    /* //初始化弹幕
     function init_screen() {
     var _top = 0;
     var _height = $(window).height();
     $(".d_show").find("div").show().each(function() {
     var _left = $(window).width() - $(this).width();
     var time=10000;
     if($(this).index()%2==0)
     time=20000;
     _top+=80;
     if(_top>_height-100)
     _top=80;
     $(this).css({
     left:_left,
     top:_top,
     color:getRandomColor()

     });
     $(this).animate({
     left:"-"+_left+"px"},
     time,
     function(){});


     });
     } */

    //随机获取颜色值
    function getRandomColor() {
        return '#' + (function(h) {
                return new Array(7 - h.length).join("0") + h
            })((Math.random() * 0x1000000 << 0).toString(16))
    }
</script>

<script type="text/javascript">

    var websocket=null;
    var _top=80;
    var index=0;

    var host=window.location.host;
    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window){
        websocket=new WebSocket("ws://"+host+"/ws");
    } else {
        alert("Not Support WebSocket!");
    }


    //连接发生错误的回调方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function(event){
        setMessageInnerHTML("open");
        websocket.send("add");
    }

    //接收到消息的回调方法
    // 收到服务器发送的消息
    websocket.onmessage = function(){
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }


    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        websocket.close();
    }


    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML){
        //修改背景图
        var imgurl;
        if (innerHTML.startWith("~background,")) {
            var cmd = innerHTML;
            imgurl = cmd.split(",")[1];
            document.body.style.background = "url("+imgurl+")";
        }else{
            $(".d_show").append("<div id='"+index+"'>"+ innerHTML + "</div>");
        }

        launch();
    }


    //发送消息
    function send(){
        //var message = document.getElementById('text').value;
        var message = $(".s_text").val();
        $(".s_text").val("");
        websocket.send(message);
    }
</script>

</body>
</html>

转载请注明来源。 欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。 可以在下面评论区评论,也可以邮件至 sharlot2050@foxmail.com。

文章标题:基于 Netty 实现 WebSocket 服务器

字数:2.1k

本文作者:夏来风

发布时间:2020-08-01, 23:58:34

原始链接:http://www.demo1024.com/blog/netty-WebSocket/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。