netty-聊天服务器

服务端

ServerMain

public class ChatServerMain {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            ChannelFuture channelFuture  = serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChatServerInitialize()).bind(8989).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

fuck说,没新东西。

ServerInitializer

public class ChatServerInitialize extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new ChatServerHandler());
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

这里用到了新的解码器。

之前的LengthFieldBasedFrameDecoder针对的是底层数据流的,也就是Frame

这次的DelimiterBasedFrameDecoder完全针对的就是字符串了。

其中的lineDelimiter也就是以\n为分隔符,接下来你会很深刻的感触到。

关于编解码器,后续专注研究。

ServerHandler

public class ChatServerHandler extends SimpleChannelInboundHandler<String> {
   public static final  ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        Channel channel = ctx.channel();
        String user = channel.remoteAddress().toString();
        channelGroup.forEach(ch->{
            if(ch == channel){
                ch.writeAndFlush("[myself]:"+msg+"\n");
                return;
            }
                ch.writeAndFlush("["+user+"]:" + msg+"\n");
        });
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        channelGroup.forEach(ch -> {
            ch.writeAndFlush("用户[" + channel.remoteAddress() + "]加入,当前在线"+(channelGroup.size()+1)+"人\n");
        });
        channelGroup.add(channel);
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("用户["+ctx.channel().remoteAddress()+"]上线");
    }
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("用户["+ctx.channel().remoteAddress()+"]下线");
    }
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
       channelGroup.forEach(ch->{
           ch.writeAndFlush("用户["+ ctx.channel().remoteAddress()+"]离开,当前在线"+channelGroup.size()+"人\n");
       });
    }
}

粘包

先简单了解概念吧,刚好有事例进行对比。

如果一个包1024,你要发的数据是256,然后怎么去发最方便呢,要发几个包呢?

我们256就是一个整体,所以按道理说,应该发四个包。

不过呢,TCP自己的管理,当然是只发一个包更节省资源啦,当然了,你还得间隔比较短的时候并发。

也就是说,当连续并发,且间隔较短的时候,一个包里面有两个包的数据。

不一定是全体的数据,但一个包的确要拆成两个或以上的部分,才能辨识其中的内容。

前面提到的编解码,如果不仔细拆分,混杂在一起的信息不能被解码,被丢弃的可能大大滴。

本来好好地数据,就被这样丢弃简直暴殄天物,但是粘包是无法避免的啊。

就和现在看到的诡异一样:每句传输的字符串,后面都跟上了\n

之前可没有这么麻烦的。

a\nb\nc这个当几行呢,直接一看就是一行,但是\n转义之后,算作的是三行。

本来是一个包,我们需要的就是找个标准,然后进行拆包

粘包对应的解决办法,就是如此。想到了,于是对比一下。

lineDelimiter呢,就是只认\n然后进行拆分并解析的,如果你不加\n的话,根本出发不了操作方法。

  • 会话管理(channelGroup)

说好的聊天呢,还是群聊的情况,会话管理是必须的,因为一个人发送的消息你必须把它发送给其他人。

我们可以使用Set进行每个channel的存储,添加或移除对应的时候进行挂你即可。

需要把消息进行广播的时候,慢慢遍历发送即可。

不过呢,netty提供了channelGroup这么一个管理工具,还有其他便捷方法,那我们就懒得去自己编写了。

// 创建channelGroup
public static final  ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// 添加
channelGroup.add(channel);
// 大小
channelGroup.size();
// 移除
channelGroup.remove(channel);

代码中并没有使用remove,因为channelGroup中使用的是GlobalEventExecutor,全局的,自动移除。

当会话建立,就添加进去进行管理,便于后续广播操作,会话注销就移除。

当然了,如果检测到@user信息,你可以找出channel然后进行单点发送啊,甚至私密信息也可以。

  • 生命周期

之前说过的生命周期没有问题,现在添加两个:handlerAddedhandlerRemoved

这个更类似于classobject的关系,同一个类,可以实例化多个实体。

所谓的add就是来了一个链接,同一个handler被实例化了一个新的对象,remove就是销毁了这个对象。

之前介绍的其他方法,好比对象的方法,你可以更清晰的区使用和感知。

而这两个东西,更像是类创建的实例化过程,更加的底层的感觉,比前面的更早或更晚发生。

也就是创建对象和内存回收一样,一般都是JVM来做,而不是我们去调用的感觉。

handlerAddedhandlerRemoved更像是handler的实例计数的东西,每次创建和销毁都会触发。

毕竟只有了,才有所谓的激活,是吧。

上线下线的判断,在这里面,会更加的精确一些。

电话呼叫中了,但是接通之前挂掉了,也算作打过电话了不是。

  • 逻辑分析
  1. 新链接加入,广播通知其他人有人上线,新加入用户不接受消息,所以发送后再add
  2. 链接断开,广播其他人有人下线
  3. 个人发送信息,除本人外都广播消息

客户端

ClientMain

public class ChatClientMain {
    public static void main(String[] args) throws IOException {
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try{
            Bootstrap bootstrap = new Bootstrap();
            Channel channel = bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class).handler(new ChatClientInitialize()).connect(new InetSocketAddress("localhost",8989)).channel();
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            while(true){
                channel.writeAndFlush(reader.readLine()+ "\n");
            }
        }finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

外部获取channel

这的确解决了我之前的一大疑惑:handler外如何获取channel

呵呵,怪自己吧,现在慢慢学,好好记。

这就是的区别,只有学全面了,用的时候才不会迷茫和无助。

外部循环获取控制台输入,发送时别忘记\n

ClientInitializer

public class ChatClientInitialize extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new DelimiterBasedFrameDecoder(4096, Delimiters.lineDelimiter()));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new ChatClientHandler());
    }
}

同服务器,不再复述

Clienthandler

public class ChatClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println( msg);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

输入逻辑已在外部完成,handler仅仅输出服务端数据即可,更多数据格式在服务端进行定制。

命令定制

查询当前人数

   protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        if("many".equals(msg)){
            ctx.writeAndFlush("当前在线人数:"+channelGroup.size() + "人\n");
            return;
        }
        Channel channel = ctx.channel();
        String user = channel.remoteAddress().toString();
        channelGroup.forEach(ch->{
            if(ch == channel){
                ch.writeAndFlush("[myself]:"+msg+"\n");
                return;
            }
                ch.writeAndFlush("["+user+"]:" + msg+"\n");
        });
    }

当然了,写在方法内部不太合适。

你可以更加的规范一下。

至于私信?点对点发送?可以自己尝试一下。

小结

聊天室后台如此,有精力可以写一个GUI,区分一下输入和输出,自定义一下用户名。。。

人靠衣装嘛,有了外貌会更有个模样。

垃圾代码在此;

你可能感兴趣的:(netty)