使用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