Netty整合Http与WebSocket的Demo 入门

Netty我就不多说了,是什么能看到这篇文章的都很清楚

网上很多文章直接黏贴复制的不说,还基本没办法拿出来当个例子走一遍。

我这版虽然也是照着能用的修修改改,但最起码保证能用,而且注释很详细

话不多说,直接搞重点。

我的需求是什么:

用Netty搭建一个项目,能接到Http、WebSocket请求,处理它,返回它。

请求类型eg:

http://www.anyongliang.cn:8888/Organize/login?user=root&pwd=123456

ws://www.anyongliang.cn:8888/ws

实现需求我需要什么:

电脑

Jdk1.8(推荐)

IDE(推荐idea,我也用idea做演示)

jar管理(推荐gradle,我也用gradle做演示)

开始实现:

我的注释很多,就不一一细写了,没什么卵用,demo搭起来,走个几遍,打几个断点,什么都懂了。

个别不懂的针对类去搜,实在不行去官网译本:netty 4 官网译文

1:创建项目,并添加依赖。

项目核心结构如图:

依赖:

testCompile group: 'junit', name: 'junit', version: '4.12'
    //netty-4
    compile group: 'io.netty', name: 'netty-all', version: '4.1.19.Final'
    //mongo-Bson
    compile group: 'org.mongodb', name: 'mongo-java-driver', version: '3.8.2'
    //google-Gson
    compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
    //apache-commons工具包
    compile group: 'org.apache.commons', name: 'commons-dbcp2', version: '2.1.1'
    compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.6'
    compile group: 'commons-io', name: 'commons-io', version: '2.5'
    compile group: 'commons-codec', name: 'commons-codec', version: '1.10'
    //日志-slf4j
    compile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25'

核心代码:

1:解码器,用来解析请求

package cn.ayl.socket.decoder;

import cn.ayl.config.Const;
import cn.ayl.socket.handler.HeartBeatHandler;
import cn.ayl.socket.handler.HttpAndWebSocketHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * created by Rock-Ayl 2019-11-6
 * WebSocket请求的解码器,一个请求需要先从这里走,实现init
 */
public class HttpAndWebSocketDecoder extends ChannelInitializer<SocketChannel> {

    protected static Logger logger = LoggerFactory.getLogger(HttpAndWebSocketDecoder.class);

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        logger.info("解析请求.");
        ChannelPipeline pipeline = ch.pipeline();
        //http解码
        initHttpChannel(pipeline);
        //心跳检测
        initHeartBeat(pipeline);
        //基于http的WebSocket
        initWebSocket(pipeline);
        //处理器
        pipeline.addLast(new HttpAndWebSocketHandler());
    }

    //Http部分
    private void initHttpChannel(ChannelPipeline pipeline) throws Exception {
        //http解码器(webSocket是http的升级)
        pipeline.addLast(new HttpServerCodec());
        //以块的方式来写的处理器,解决大码流的问题,ChunkedWriteHandler:可以向客户端发送HTML5文件
        pipeline.addLast(new ChunkedWriteHandler());
        //netty是基于分段请求的,HttpObjectAggregator的作用是将HTTP消息的多个部分合成一条完整的HTTP消息,参数是聚合字节的最大长度
        pipeline.addLast(new HttpObjectAggregator(Const.MaxContentLength));
    }

    //心跳部分
    private void initHeartBeat(ChannelPipeline pipeline) throws Exception {
        // 针对客户端,如果在1分钟时没有向服务端发送读写心跳(ALL),则主动断开,如果是读空闲或者写空闲,不处理
        pipeline.addLast(new IdleStateHandler(Const.ReaderIdleTimeSeconds, Const.WriterIdleTimeSeconds, Const.AllIdleTimeSeconds));
        // 自定义的空闲状态检测
        pipeline.addLast(new HeartBeatHandler());
    }

    //WebSocket部分
    private void initWebSocket(ChannelPipeline pipeline) throws Exception {
        /**
         * WebSocketServerProtocolHandler负责websocket握手以及处理控制框架(Close,Ping(心跳检检测request),Pong(心跳检测响应))。
         * 参数为ws请求的访问路径 eg:ws://127.0.0.1:8888/WebSocket。
         */
        pipeline.addLast(new WebSocketServerProtocolHandler(Const.WebSocketPath));
    }

}

2:处理器,一共两个,一个用来控制心跳,一个用来分发请求并处理

package cn.ayl.socket.handler;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * created by Rock-Ayl 2019-11-17
 * WebSocket心跳处理程序
 */
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {

    protected static Logger logger = LoggerFactory.getLogger(HeartBeatHandler.class);

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 判断evt是否是 IdleStateEvent(超时的事件,用于触发用户事件,包含读空闲/写空闲/读写空闲 )
        if (msg instanceof IdleStateEvent) {
            // 强制类型转换
            IdleStateEvent event = (IdleStateEvent) msg;
            switch (event.state()) {
                case READER_IDLE:
                    logger.info("进入读空闲...");
                    break;
                case WRITER_IDLE:
                    logger.info("进入写空闲...");
                    break;
                case ALL_IDLE:
                    logger.info("开始杀死无用通道,节约资源");
                    Channel channel = ctx.channel();
                    channel.close();
                    break;
            }
        }

    }

}
package cn.ayl.socket.handler;

import cn.ayl.json.JsonObject;
import cn.ayl.json.JsonUtil;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.handler.codec.http.multipart.MemoryAttribute;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static io.netty.buffer.Unpooled.copiedBuffer;

/**
 * created by Rock-Ayl on 2019-11-7
 * Http请求和WebSocket请求的处理程序
 */
public class HttpAndWebSocketHandler extends ChannelInboundHandlerAdapter {

    protected static Logger logger = LoggerFactory.getLogger(HttpAndWebSocketHandler.class);

    private WebSocketServerHandshaker webSocketServerHandshaker;

    /**
     * 通道,请求过来从这里分类
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //处理Http请求和WebSocket请求的分别处理
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        } else if (msg instanceof HttpContent) {
            //todo handleHttpContent
        } else if (msg instanceof WebSocketFrame) {
            handleWebSocketRequest(ctx, (WebSocketFrame) msg);
        }
    }

    /**
     * 每个channel都有一个唯一的id值
     * asLongText方法是channel的id的全名
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        //todo 连接打开时
        logger.info(ctx.channel().localAddress().toString() + " handlerAdded!, channelId=" + ctx.channel().id().asLongText());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        //todo 连接关闭时
        logger.info(ctx.channel().localAddress().toString() + " handlerRemoved!, channelId=" + ctx.channel().id().asLongText());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //todo 出现异常
        logger.error("Client:" + ctx.channel().remoteAddress() + "error", cause.getMessage());
        ctx.close();
    }

    // 处理Websocket的代码
    private void handleWebSocketRequest(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 判断是否是关闭链路的指令
        if (frame instanceof CloseWebSocketFrame) {
            webSocketServerHandshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
            return;
        }
        // 判断是否是Ping消息
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        // 文本消息,不支持二进制消息
        if (frame instanceof TextWebSocketFrame) {
            //请求text
            String request = ((TextWebSocketFrame) frame).text();
            logger.info("收到信息:" + request);
            //返回
            ctx.channel().writeAndFlush(new TextWebSocketFrame(JsonObject.Success().append("req", request).toString()));
        }
    }

    /**
     * 处理业务
     *
     * @param path   eg:  /Organize/login
     * @param params eg:  user:root pwd:123456
     * @return
     */
    private JsonObject handleServiceFactory(String path, Map<String, Object> params) {
        //todo 根据path和params处理业务并返回
        return JsonObject.Success();
    }

    private void handleHttpRequest(final ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
        //todo http请求内容分类进行细化,目前设定为全部为服务请求(可以存在页面,资源,上传等等)
        handleService(ctx, req);
    }

    //处理http服务请求
    private void handleService(ChannelHandlerContext ctx, FullHttpRequest req) {
        FullHttpResponse response;
        JsonObject result;
        //获得请求path
        String path = getPath(req);
        //根据请求类型处理请求 get post ...
        if (req.method() == HttpMethod.GET) {
            //获取请求参数
            Map<String, Object> params = getGetParamsFromChannel(req);
            //业务
            result = handleServiceFactory(path, params);
            response = responseOKAndJson(HttpResponseStatus.OK, result);
        } else if (req.method() == HttpMethod.POST) {
            //获取请求参数
            Map<String, Object> params = getPostParamsFromChannel(req);
            //处理业务
            result = handleServiceFactory(path, params);
            response = responseOKAndJson(HttpResponseStatus.OK, result);
        } else {
            //todo 处理其他类型的请求
            response = responseOKAndJson(HttpResponseStatus.INTERNAL_SERVER_ERROR, null);
        }
        // 发送响应并关闭连接
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * Http获取请求Path
     *
     * @param req
     * @return
     */
    private String getPath(FullHttpRequest req) {
        String path = null;
        try {
            path = new URI(req.getUri()).getPath();
        } catch (Exception e) {
            logger.error("接口解析错误.");
        } finally {
            return path;
        }
    }

    /**
     * Http获取GET方式传递的参数
     *
     * @param fullHttpRequest
     * @return
     */
    private Map<String, Object> getGetParamsFromChannel(FullHttpRequest fullHttpRequest) {
        //参数组
        Map<String, Object> params = new HashMap<>();
        //如果请求为GET继续
        if (fullHttpRequest.method() == HttpMethod.GET) {
            // 处理get请求
            QueryStringDecoder decoder = new QueryStringDecoder(fullHttpRequest.uri());
            Map<String, List<String>> paramList = decoder.parameters();
            for (Map.Entry<String, List<String>> entry : paramList.entrySet()) {
                params.put(entry.getKey(), entry.getValue().get(0));
            }
            return params;
        } else {
            return null;
        }

    }

    /**
     * Http获取POST方式传递的参数
     *
     * @param fullHttpRequest
     * @return
     */
    private Map<String, Object> getPostParamsFromChannel(FullHttpRequest fullHttpRequest) {
        //参数组
        Map<String, Object> params;
        //如果请求为POST
        if (fullHttpRequest.method() == HttpMethod.POST) {
            // 处理POST请求
            String strContentType = fullHttpRequest.headers().get("Content-Type").trim();
            if (strContentType.contains("x-www-form-urlencoded")) {
                params = getFormParams(fullHttpRequest);
            } else if (strContentType.contains("application/json")) {
                try {
                    params = getJSONParams(fullHttpRequest);
                } catch (UnsupportedEncodingException e) {
                    return null;
                }
            } else {
                return null;
            }
            return params;
        } else {
            return null;
        }
    }

    /**
     * Http解析from表单数据(Content-Type = x-www-form-urlencoded)
     *
     * @param fullHttpRequest
     * @return
     */
    private Map<String, Object> getFormParams(FullHttpRequest fullHttpRequest) {
        Map<String, Object> params = new HashMap<>();
        HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(new DefaultHttpDataFactory(false), fullHttpRequest);
        List<InterfaceHttpData> postData = decoder.getBodyHttpDatas();
        for (InterfaceHttpData data : postData) {
            if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) {
                MemoryAttribute attribute = (MemoryAttribute) data;
                params.put(attribute.getName(), attribute.getValue());
            }
        }
        return params;
    }

    /**
     * Http解析json数据(Content-Type = application/json)
     *
     * @param fullHttpRequest
     * @return
     * @throws UnsupportedEncodingException
     */
    private Map<String, Object> getJSONParams(FullHttpRequest fullHttpRequest) throws UnsupportedEncodingException {
        Map<String, Object> params = new HashMap<>();
        ByteBuf content = fullHttpRequest.content();
        byte[] reqContent = new byte[content.readableBytes()];
        content.readBytes(reqContent);
        String strContent = new String(reqContent, "UTF-8");
        JsonObject jsonParams = JsonUtil.parse(strContent);
        for (Object key : jsonParams.keySet()) {
            params.put(key.toString(), jsonParams.get((String) key));
        }
        return params;
    }

    /**
     * Http响应OK并返回Json
     *
     * @param status 状态
     * @param result 返回值
     * @return
     */
    private FullHttpResponse responseOKAndJson(HttpResponseStatus status, JsonObject result) {
        ByteBuf content = copiedBuffer(result.toString(), CharsetUtil.UTF_8);
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content);
        if (content != null) {
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json;charset=UTF-8");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
        }
        return response;
    }

    /**
     * Http响应OK并返回文本
     *
     * @param status 状态
     * @param result 返回值
     * @return
     */
    private FullHttpResponse responseOKAndText(HttpResponseStatus status, String result) {
        ByteBuf content = copiedBuffer(result, CharsetUtil.UTF_8);
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content);
        if (content != null) {
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
        }
        return response;
    }
}

3:服务管控

package cn.ayl.socket.server;

import cn.ayl.config.Const;
import cn.ayl.socket.decoder.HttpAndWebSocketDecoder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * created by Rock-Ayl 2019-11-4
 * 通信服务
 */
public class SocketServer {

    protected static Logger logger = LoggerFactory.getLogger(SocketServer.class);

    private Channel channel;
    /**
     * NioEventLoopGroup是一个处理I/O操作的事件循环器 (其实是个线程池)。
     * netty为不同类型的传输协议提供了多种NioEventLoopGroup的实现。
     * 在本例中我们要实现一个服务端应用,并使用了两个NioEventLoopGroup。
     * 第一个通常被称为boss,负责接收已到达的 connection。
     * 第二个被称作 worker,当 boss 接收到 connection 并把它注册到 worker 后,worker 就可以处理 connection 上的数据通信。
     * 要创建多少个线程,这些线程如何匹配到Channel上会随着EventLoopGroup实现的不同而改变,或者你可以通过构造器去配置他们。
     */
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();

    /**
     * 创建一个默认配置的HttpServerBootstrap
     *
     * @param bossGroup   netty-boss
     * @param workerGroup netty-work-IO
     * @return
     */
    public static ServerBootstrap createDefaultServerBootstrap(EventLoopGroup bossGroup, EventLoopGroup workerGroup) {
        return new ServerBootstrap()
                /**
                 * 组装Boss和IO
                 */
                .group(bossGroup, workerGroup)
                /**
                 * 这里我们指定NioServerSocketChannel类,用来初始化一个新的Channel去接收到达的connection。
                 */
                .channel(NioServerSocketChannel.class)
                /**
                 * 你可以给Channel配置特有的参数。
                 * 这里我们写的是 TCP/IP 服务器,所以可以配置一些 socket 选项,例如 tcpNoDeply 和 keepAlive。
                 * 请参考ChannelOption和ChannelConfig文档来获取更多可用的 Channel 配置选项,并对此有个大概的了解。
                 * BACKLOG用于构造服务端套接字ServerSocket对象,标识当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1,Java将使用默认值50。
                 */
                .option(ChannelOption.SO_BACKLOG, Const.ChannelOptionSoBacklogValue)
                /**
                 * 注意到option()和childOption()了吗?
                 * option()用来配置NioServerSocketChannel(负责接收到来的connection),
                 * 而childOption()是用来配置被ServerChannel(这里是NioServerSocketChannel) 所接收的Channel
                 *
                 * ChannelOption.SO_KEEPALIVE表示是否开启TCP底层心跳机制,true为开启
                 * ChannelOption.SO_REUSEADDR表示端口释放后立即就可以被再次使用,因为一般来说,一个端口释放后会等待两分钟之后才能再被使用
                 * ChannelOption.TCP_NODELAY表示是否开始Nagle算法,true表示关闭,false表示开启,通俗地说,如果要求高实时性,有数据发送时就马上发送,就关闭,如果需要减少发送次数减少网络交互就开启
                 */
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childOption(ChannelOption.SO_REUSEADDR, true)
                .childOption(ChannelOption.TCP_NODELAY, true)
                .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
    }

    /**
     * 开启Http与WebSocket
     */
    public void startup() {
        try {
            try {
                /**
                 * ServerBootstrap是用来搭建 server 的协助类。
                 * 你也可以直接使用Channel搭建 server,然而这样做步骤冗长,不是一个好的实践,大多数情况下建议使用ServerBootstrap。
                 */
                ServerBootstrap bootstrap = SocketServer.createDefaultServerBootstrap(bossGroup, workerGroup);
                /**
                 * 这里的 handler 会被用来处理新接收的Channel。
                 * ChannelInitializer是一个特殊的 handler,
                 * 帮助开发者配置Channel,而多数情况下你会配置Channel下的ChannelPipeline,
                 * 往 pipeline 添加一些 handler (例如DiscardServerHandler) 从而实现你的应用逻辑。
                 * 当你的应用变得复杂,你可能会向 pipeline 添加更多的 handler,并把这里的匿名类抽取出来作为一个单独的类。
                 */
                bootstrap.childHandler(new HttpAndWebSocketDecoder());
                /**
                 * 剩下的事情就是绑定端口并启动服务器,这里我们绑定到机器的8080端口。你可以多次调用bind()(基于不同的地址)。
                 * Bind and start to accept incoming connections.(绑定并开始接受传入的连接)
                 */
                ChannelFuture f = bootstrap.bind(Const.SocketPort).sync();
                /**
                 * Wait until the server socket is closed.(等待,直到服务器套接字关闭)
                 * In this example, this does not happen, but you can do that to gracefully(在本例中,这种情况不会发生,但是您可以优雅地这样做)
                 * shut down your server.(关闭你的服务)
                 */
                channel = f.channel();
                channel.closeFuture().sync();
            } finally {
                workerGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
        } catch (Exception e) {
            logger.error("Run Socket Fail!");
        }
    }

    //关闭Socket
    public void destroy() {
        if (channel != null) {
            channel.close();
        }
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
        System.out.println("WebsocketChatServer Destroy:" + Const.SocketPort);
    }

    public static void main(String[] args) {
        new SocketServer().startup();
    }


}

4:常量池:

package cn.ayl.config;

/**
 * created by Rock-Ayl 2019-11-5
 * 常量
 */
public class Const {

    //SocketPort
    public static int SocketPort = 8888;
    //http请求聚合字节最大长度
    public static int MaxContentLength = 65535;
    //WebSocket读空闲时间闲置/秒
    public static int ReaderIdleTimeSeconds = 8;
    //WebSocket写空闲时间闲置/秒
    public static int WriterIdleTimeSeconds = 10;
    //WebSocket所有空闲时间闲置/秒
    public static int AllIdleTimeSeconds = 12;
    public static String WebSocketPath = "/WebSocket";
    //BACKLOG值用于构造服务端套接字ServerSocket对象,标识当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1,Java将使用默认值50。
    public static int ChannelOptionSoBacklogValue = 1024;

}

5:WebSocket测试

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket客户端</title>
</head>
<body>
<script type="text/javascript">
    var socket;
    //如果浏览器支持WebSocket
    if (window.WebSocket) {
        //参数就是与服务器连接的地址
        socket = new WebSocket("ws://127.0.0.1:8888/WebSocket");

        //客户端收到服务器消息的时候就会执行这个回调方法
        socket.onmessage = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value + "\n" + event.data;
        }

        //连接建立的回调函数
        socket.onopen = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = "连接开启";
        }

        //连接断掉的回调函数
        socket.onclose = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value + "\n" + "连接关闭";
        }
    } else {
        alert("浏览器不支持WebSocket!");
    }

    //发送数据
    function send(message) {
        if (!window.WebSocket) {
            return;
        }
        //当websocket状态打开
        if (socket.readyState == WebSocket.OPEN) {
            socket.send(message);
        } else {
            alert("连接没有开启");
        }
    }
</script>
<form onsubmit="return false">
    <textarea name="message" style="width: 400px;height: 200px"></textarea>
    <input type="button" value="发送数据" onclick="send(this.form.message.value);">
    <h3>服务器输出:</h3>
    <textarea id="responseText" style="width: 400px;height: 300px;"></textarea>
    <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空数据">
</form>
</body>
</html>

-----------------------------------------------End-----------------------------------------------------------------------------------------------------------------

所有的核心代码都在这里,替换掉里面Json就可以拿来去用,启动后调用接口。

http://127.0.0.1:8888

就可以测试http请求

HTML5代码直接打开可以测试WebSocket接口

如果你照着我这个还是搭不上,好,gitHub会用吧?Demo-GitHub地址

代码思想和一些注释都是网上汇总的

我也不太了解Netty比较核心的东西,本博客也只是学习自用,目的是让我这种人不看官网的能够快速上手Netty大概怎么玩的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章