Netty实现简易聊天室的功能

        使用Netty来实现一个聊天室是很容易的,当有客户端连接服务端的时候,在服务端的channelActive方法里面即可感应到。使用ChannelGroup.writeAndFlush方法即可向所有客户端发送一条“上线了”的信息,ChannelGroup是Netty封装的ConcurrentHashMap,里面保存所有的客户端:

@Override
public void channelActive(ChannelHandlerContext ctx) {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Channel channel = ctx.channel();
    SocketAddress address = channel.remoteAddress();
    //将该客户加入聊天的信息推送给其它在线的客户端
    String content = "[ 客户端 " + address + " ] 上线了 " + sdf.format(new Date());
    byte[] bytes = content.getBytes(CharsetUtil.UTF_8);
    MessageProtocol messageProtocol = MessageProtocol.builder().length(bytes.length).content(bytes).build();
    CHANNEL_GROUP.writeAndFlush(messageProtocol);
    //将当前channel加入到channelGroup
    CHANNEL_GROUP.add(channel);
    if (log.isInfoEnabled()) {
        log.info(address + " 上线了");
    }
}

        同理,channelInactive是该客户端断开服务端时会回调的方法,在里面给其他客户端发一条“下线了”的信息即可。

        客户端通过channel.writeAndFlush即可向服务端发送信息,服务端通过channelRead方法即可感知到,然后把这条信息通过ChannelGroup发送给其他客户端即可:

@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) {
    String content = new String(msg.getContent(), CharsetUtil.UTF_8);
    Channel channel = ctx.channel();

    String myContent = "[ 自己 ] 发送了消息:" + content;
    byte[] myContentBytes = myContent.getBytes(CharsetUtil.UTF_8);
    MessageProtocol myMessageProtocol = MessageProtocol.builder().length(myContentBytes.length).content(myContentBytes).build();

    String otherContent = "[ 客户端 " + channel.remoteAddress() + " ] 发送了消息:" + content;
    byte[] otherContentBytes = otherContent.getBytes(CharsetUtil.UTF_8);
    MessageProtocol otherMessageProtocol = MessageProtocol.builder().length(otherContentBytes.length).content(otherContentBytes).build();

    CHANNEL_GROUP.forEach(ch -> {
        MessageProtocol messageProtocol = (ch == channel) ? myMessageProtocol : otherMessageProtocol;
        ch.writeAndFlush(messageProtocol);
    });
}

        需要注意的是,TCP会有粘包拆包的现象发生,需要进行特殊处理。这里的解决方案是发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。

        用一个Java类来保存发送的内容,如下所示:

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 自定义协议包
 *
 * @author Robert Hou
 * @date 2020年05月02日 23:09
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MessageProtocol implements Serializable {

    private static final long serialVersionUID = 609914401677840006L;
    /**
     * 一次发送包体长度
     */
    private int length;
    /**
     * 一次发送包体内容
     */
    private byte[] content;
}

        自定义一个编码器,发送数据的时候会经过它,将数据的长度和内容一块发送出去:

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

/**
 * 发送包编码
 *
 * @author Robert Hou
 * @date 2020年05月02日 23:12
 **/
public class MessageEncoder extends MessageToByteEncoder<MessageProtocol> {

    @Override
    protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) {
        out.writeInt(msg.getLength());
        out.writeBytes(msg.getContent());
    }
}

        然后写一个自定义解码器,数据到达服务端会首先经过解码,然后到达业务Handler:

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

/**
 * 接收包解码
 *
 * @author Robert Hou
 * @date 2020年05月02日 23:15
 **/
public class MessageDecoder extends ByteToMessageDecoder {

    private int length = 0;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        if (in.readableBytes() < 4) {
            /*
            读取小于4字节,说明连MessageProtocol里的length字段都没读取完整,那么就继续下一次读取
            还有一种情况是读取content也小于4字节,此时虽然也继续下一次读取,但是是无妨的,因为length字段已经有了正确值
             */
            return;
        }
        if (length == 0) {
            length = in.readInt();
        }
        if (in.readableBytes() < length) {
            //当前可读数据不够,那么就继续下一次读取
            return;
        }
        byte[] content = new byte[length];
        in.readBytes(content);
        //封装成MessageProtocol对象,传递到下一个handler业务处理
        MessageProtocol messageProtocol = MessageProtocol.builder().length(length).content(content).build();
        out.add(messageProtocol);
        //length重新复位为0,以便下此读取
        length = 0;
    }
}

        如果数据长度不够length,就等待下一次数据过来,取数据的时候也只取length长度的数据。

        完整的运行效果如下所示:

        Server:

聊天室server启动。。。
/127.0.0.1:63069 上线了
/127.0.0.1:63123 上线了
/127.0.0.1:63123 下线了
当前客户端数量:1

        Client1:

==========/127.0.0.1:63069==========
[ 客户端 /127.0.0.1:63123 ] 上线了 2020-05-05 21:59:56
123
[ 自己 ] 发送了消息:123
[ 客户端 /127.0.0.1:63123 ] 发送了消息:456
[ 客户端 /127.0.0.1:63123 ] 下线了

        Client2:

==========/127.0.0.1:63123==========
[ 客户端 /127.0.0.1:63069 ] 发送了消息:123
456
[ 自己 ] 发送了消息:456

Process finished with exit code -1

        GitHub源码:https://github.com/ACoolMonkey/netty-chatroom

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值