转载--netty开发聊天CS与BS

本文为转载后,加上了部分修改而成。具体入下:

Client-Server模式

使用netty写客户端与服务器,完成聊天。为了优化用户体验,这里加了一个bean类,ClientBean,用于描述客户端。

Server端

netty本身就有很好的分层设计,框架和业务逻辑分界明显,对于不同的业务,我们只需要实现不同的handler即可。

SimpleChatServerHandler.java

/**
 * @Date 2018年5月16日
 *
 * @author 郭  璞
 *
 */
package simplechat;

import java.util.HashMap;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * @author 郭  璞
 *
 */
public class SimpleChatServerHandler extends SimpleChannelInboundHandler<String> {

    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    public static HashMap<Channel, ClientBean> channelmap = new HashMap<Channel, ClientBean>();

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelHandlerAdapter#handlerAdded(io.netty.channel.ChannelHandlerContext)
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        for(Channel channel: channels) {
            channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + " 加入啦!\n");
        }
        channels.add(ctx.channel());
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelHandlerAdapter#handlerRemoved(io.netty.channel.ChannelHandlerContext)
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        for(Channel channel: channels) {
            channel.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + "离开了\n");
        }
        channels.remove(ctx.channel());
    }

    /* (non-Javadoc)
     * @see io.netty.channel.SimpleChannelInboundHandler#channelRead0(io.netty.channel.ChannelHandlerContext, java.lang.Object)
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        Channel incoming = ctx.channel();
        for(Channel channel: channels) {
            if(channel == incoming) {
                channel.writeAndFlush("[YOU] " + msg + "\n");
            }else{
                // 这里其实放到外层感觉会更好一点,逻辑上
                ClientBean clientBean = this.channelmap.get(ctx.channel());
                if(clientBean.isIsfirstconnect() == true) {
                    // 第一次輸入,將msg作為用戶暱稱即可
                    clientBean.setIsfirstconnect(false);
                    clientBean.setNickname(msg);
                    clientBean.setRemoteaddress(incoming.remoteAddress().toString());

                }else{
                    String nickname = this.channelmap.get(ctx.channel()).getNickname();
                    String remoteaddress = this.channelmap.get(ctx.channel()).getRemoteaddress();
                    System.out.println("DEBUG:" + nickname);
                    channel.writeAndFlush("[" + nickname+"@"+ remoteaddress + "]" + msg + "\n");
                }
            }
        }
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#channelActive(io.netty.channel.ChannelHandlerContext)
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient:" + incoming.remoteAddress() + "在线!\n");
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#channelInactive(io.netty.channel.ChannelHandlerContext)
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient: " + incoming.remoteAddress() + "离线!\n");
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#exceptionCaught(io.netty.channel.ChannelHandlerContext, java.lang.Throwable)
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient: " + incoming.remoteAddress() + "异常连接!\n");
        cause.printStackTrace();
        ctx.close();
    }
}

SimpleChatServer.java

/**
 * @Date 2018年5月16日
 *
 * @author 郭  璞
 *
 */
package simplechat;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * @author 郭  璞
 *
 */
public class SimpleChatServer {
    public void run() {
        int port = 8888;
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                     .channel(NioServerSocketChannel.class)
                     .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast("handler", new SimpleChatServerHandler());
                            // 客户端第一次连接会触发此操作,然后是active,再是read
                            SimpleChatServerHandler.channelmap.put(ch, new ClientBean(true, ch.remoteAddress().toString(), ch.remoteAddress().toString()));
                            System.out.println("SimpleChatClient: " + ch.remoteAddress() + "连接上了!");
                        }
                    })
                     .option(ChannelOption.SO_BACKLOG, 128)
                     .childOption(ChannelOption.SO_KEEPALIVE, true);
            System.out.println("SimpleChatServer is running...");
            ChannelFuture f = bootstrap.bind(port).sync();
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            throw new RuntimeException(" :\n" + e);
        }finally{
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
            System.out.println("SimpleChatServer shutdown gracefully.\n");
        }
    }

    public static void main(String[] args) {
        new SimpleChatServer().run();
    }

}

Client端

ClientBean

/**
 * @Date 2018年5月16日
 *
 * @author 郭  璞
 *
 */
package simplechat;

/**
 * @author 郭  璞
 *
 */
public class ClientBean {
    private boolean isfirstconnect=true;
    private String nickname;
    private String remoteaddress;
    public boolean isIsfirstconnect() {
        return isfirstconnect;
    }
    public void setIsfirstconnect(boolean isfirstconnect) {
        this.isfirstconnect = isfirstconnect;
    }
    public String getNickname() {
        return nickname;
    }
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    public String getRemoteaddress() {
        return remoteaddress;
    }
    public void setRemoteaddress(String remoteaddress) {
        this.remoteaddress = remoteaddress;
    }
    @Override
    public String toString() {
        return "ClientBean [isfirstconnect=" + isfirstconnect + ", nickname=" + nickname + ", remoteaddress="
                + remoteaddress + "]";
    }
    public ClientBean(boolean isfirstconnect, String nickname, String remoteaddress) {
        super();
        this.isfirstconnect = isfirstconnect;
        this.nickname = nickname;
        this.remoteaddress = remoteaddress;
    }



}

SimpleChatClientHandler.java

/**
 * @Date 2018年5月16日
 *
 * @author 郭  璞
 *
 */
package simplechat;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * @author 郭  璞
 *
 */
public class SimpleChatClientHandler extends SimpleChannelInboundHandler<String> {

    /* (non-Javadoc)
     * @see io.netty.channel.SimpleChannelInboundHandler#channelRead0(io.netty.channel.ChannelHandlerContext, java.lang.Object)
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(msg);
    }

}

SimpleChatClient.java

/**
 * @Date 2018年5月16日
 *
 * @author 郭  璞
 *
 */
package simplechat;

import java.io.BufferedReader;
import java.io.InputStreamReader;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * @author 郭  璞
 *
 */
public class SimpleChatClient {

    public void run(String host, int port) {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .handler(new ChannelInitializer<SocketChannel>() {

                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
                        pipeline.addLast("decoder", new StringDecoder());
                        pipeline.addLast("encoder", new StringEncoder());
                        pipeline.addLast("handler", new SimpleChatClientHandler());
                  }
              });

            Channel channel = b.connect(host, port).sync().channel();
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            // \r\n很重要,否則可能會出現假死
            System.out.println("恭喜您链接服务器成功\n请输入您的昵称:");channel.writeAndFlush(in.readLine()+"\r\n");
            while(true){
                channel.writeAndFlush(in.readLine() + "\r\n");
            }
        } catch (Exception e) {
            throw new RuntimeException(" :\n" + e);
        }finally {
            group.shutdownGracefully();
            System.out.println("客户端平滑关闭了\n");
        }
    }

    public static void main(String[] args) {
        new SimpleChatClient().run("localhost", 8888);
    }
}

测试效果

首先要将SimpleChatServer跑起来,然后再开启多个SimpleChatClient实例,首次运行会提示输入昵称,后面输入就可以很方便的实现聊天了。

SimpleChatServer is running...
SimpleChatClient: /127.0.0.1:58424连接上了!
SimpleChatClient:/127.0.0.1:58424在线!

SimpleChatClient: /127.0.0.1:58448连接上了!
SimpleChatClient:/127.0.0.1:58448在线!

DEBUG:李四
DEBUG:张三
SimpleChatClient: /127.0.0.1:58448异常连接!(此处省略cause.printStackTrace()日志)
SimpleChatClient: /127.0.0.1:58448离线!

-----------------------------------------------
恭喜您链接服务器成功
请输入您的昵称:
张三
[YOU] 张三
[李四@/127.0.0.1:58424]哈喽,我是李四
嗨,李四,我是张三
[YOU] 嗨,李四,我是张三
------------------------------------------
恭喜您链接服务器成功
请输入您的昵称:
[SERVER] - /127.0.0.1:58448 加入啦!
李四
[YOU] 李四
哈喽,我是李四
[YOU] 哈喽,我是李四
[张三@/127.0.0.1:58448]嗨,李四,我是张三
[SERVER] - /127.0.0.1:58448离开了

Browser-Server模式

使用浏览器和服务器的模式进行聊天,一般会用websocket协议,netty对此支持的也不赖。

Server端

正如参考文献中写的,只有在服务器收到客户端请求升级protocol的时候,才会升级websocket协议,然后进行通信。其他情况,依旧是普通的http请求,所以server端要完成两件事情,一个是普通的http协议,一个是websocket协议。

HTTP协议,HttpRequestHandler.java

/**
 * @Date 2018年5月16日
 *
 * @author 郭  璞
 *
 */
package websocket;

import java.io.File;
import java.io.RandomAccessFile;
import java.net.URL;

import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultFileRegion;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioFile;

/**
 * @author 郭  璞
 *
 */
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private  String WS_URI;
    private static  File INDEX;

    static {
        URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
        try {
            String path = location.toURI() + "WebsocketChatClient.html";
            path = !path.contains("file:")?path:path.substring(5);
            INDEX = new File(path);
        } catch (Exception e) {
            throw new RuntimeException("Unable to locate WebsocketChatClient.html :\n" + e);
        }
    }

    public HttpRequestHandler(String wsUri) {
        this.WS_URI = wsUri;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        if(this.WS_URI.equalsIgnoreCase(request.getUri())) {
            ctx.fireChannelRead(request.retain());
        }else{
            //
            if(HttpHeaders.is100ContinueExpected(request)) {
                send1000Continue(ctx);
            }
            RandomAccessFile file = new RandomAccessFile(this.INDEX, "r");
            HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), HttpResponseStatus.OK);
            response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/html;charset=UTF-8");

            boolean keepAlive = HttpHeaders.isKeepAlive(request);

            if(keepAlive) {
                response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, file.length());
                response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
            }
            ctx.writeAndFlush(response);

            if(ctx.pipeline().get(SslHandler.class) == null) {
                ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
            }else{
                ctx.write(new ChunkedNioFile(file.getChannel()));
            }
            ChannelFuture f = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            if(!keepAlive) {
                f.addListener(ChannelFutureListener.CLOSE);
            }
            file.close();
        }

    }

    /**
     * @param ctx
     */
    private void send1000Continue(ChannelHandlerContext ctx) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#exceptionCaught(io.netty.channel.ChannelHandlerContext, java.lang.Throwable)
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("Client: " + incoming.remoteAddress() + "異常:" + cause.getMessage());
        ctx.close();
    }

}

WebSocket协议,TextWebSocketFrameHandler.java

/**
 * @Date 2018年5月16日
 *
 * @author 郭  璞
 *
 */
package websocket;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * @author 郭  璞
 *
 */
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        Channel incoming = ctx.channel();
        TextWebSocketFrame responsemsg = null;
        for(Channel channel: channels) {
            if(channel == incoming) {
                responsemsg = new TextWebSocketFrame("["+incoming.remoteAddress()+"]" + msg.text());
            }else{
                responsemsg = new TextWebSocketFrame("["+incoming.remoteAddress()+"]" + msg.text());
            }
            channel.writeAndFlush(responsemsg);
        }

    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {  // (2)
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            channel.writeAndFlush(new TextWebSocketFrame("[SERVER] - " + incoming.remoteAddress() + " 加入"));
        }
        channels.add(ctx.channel());
        System.out.println("Client:"+incoming.remoteAddress() +"加入");
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {  // (3)
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            channel.writeAndFlush(new TextWebSocketFrame("[SERVER] - " + incoming.remoteAddress() + " 离开"));
        }
        System.out.println("Client:"+incoming.remoteAddress() +"离开");
        channels.remove(ctx.channel());
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception { // (5)
        Channel incoming = ctx.channel();
        System.out.println("Client:"+incoming.remoteAddress()+"在线");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception { // (6)
        Channel incoming = ctx.channel();
        System.out.println("Client:"+incoming.remoteAddress()+"掉线");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("Client:"+incoming.remoteAddress()+"异常: " + cause.getMessage());
        // 当出现异常就关闭连接
        cause.printStackTrace();
        ctx.close();
    }

}

WebSocketServer.java

/**
 * @Date 2018年5月16日
 *
 * @author 郭  璞
 *
 */
package websocket;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
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;

/**
 * @author 郭  璞
 *
 */
public class WebSocketChatServer {

    public void run() {
        int port = 9999;
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                protected void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                    pipeline.addLast(new HttpServerCodec());
                    pipeline.addLast(new HttpObjectAggregator(64*1024));
                    pipeline.addLast(new ChunkedWriteHandler());
                    pipeline.addLast(new HttpRequestHandler("/ws"));
                    pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
                    pipeline.addLast(new TextWebSocketFrameHandler());
                }
            })
             .option(ChannelOption.SO_BACKLOG, 128)
             .childOption(ChannelOption.SO_KEEPALIVE, true);
            System.out.println("WebSocketChatServer is running...");
            //
            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            throw new RuntimeException(" :\n" + e);
        }finally{
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
            System.out.println("WebSocketChatServer finally closed!");
        }
    }

    public static void main(String[] args) {
        new WebSocketChatServer().run();
    }

}

Browser端

根据刚才HTTPRequestHandler.java设置的uri以及对应的HTML路径,我们需要把WebsocketChatClient.html文件放到项目的bin目录下,否则不能被正常找到。

WebsocketChatClient.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Chat</title>
</head>
<body>
    <script type="text/javascript">
        var socket;
        //检查浏览器是否支持WebSocket
        if(window.WebSocket){
            console.log('This browser supports WebSocket');
        }else{
            console.log('This browser does not supports WebSocket');
        }

        if (!window.WebSocket) {
            window.WebSocket = window.MozWebSocket;
        }
        if (window.WebSocket) {
            socket = new WebSocket("ws://localhost:9999/ws");
            socket.onmessage = function(event) {
                var ta = document.getElementById('responseText');
                console.log("来自客户端的消息:" + event.data);
                ta.value = ta.value + '\n' + event.data
            };
            socket.onopen = function(event) {
                var ta = document.getElementById('responseText');
                console.log(event);
                ta.value = "连接开启!";
            };
            socket.onclose = function(event) {
                var ta = document.getElementById('responseText');
                console.log(event);
                ta.value = ta.value + "连接被关闭";
            };
        } else {
            alert("你的浏览器不支持 WebSocket!");
        }

        function send() {
            if (!window.WebSocket) {
                return;
            }
            if (socket.readyState == WebSocket.OPEN) {
                message = document.getElementById("message").value;
                console.log("\n======\n");
                console.log(message);
                console.log("\n======\n");
                socket.send(message);
            } else {
                alert("连接没有开启.");
            }
        }
    </script>
    <form onsubmit="return false;">
        <h3>WebSocket 聊天室:</h3>
        <textarea id="responseText" style="width: 500px; height: 300px;"></textarea>

        <input type="text" name="message" id="message" style="width: 300px" value="ping">
        <input type="button" value="发送消息" onclick="send()">
        <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空聊天记录">
    </form>
</body>
</html>

测试效果

netty实现websocket聊天效果图

对这个例子而言,同样可以通过一个HashMap来实现用户可自由填写昵称的方式来优化用户体验。具体的实现方式和上文类似,这里就不过多的叙述了。


总结

经过这两个小例子,顶多也就是对netty整体的工作流程了解下,至于实际的开发,相比还远远不够。可以多想想如何基于上面的例子实现多人聊天,多房间聊天。

参考链接:

  1. https://my.oschina.net/waylau/blog/380957
  2. http://www.importnew.com/21561.html

猜你喜欢

转载自blog.csdn.net/marksinoberg/article/details/80337779