netty实战(一)聊天服务
经典例子,netty聊天服务,用来作为第一个实战来理解netty流程适合不过了。
netty出现是为了方便大家使用NIO来进行编程,特别是socket编程。而很多的socket编程都会有很多的协议,我们此次只是为了体验netty,所以不设置协议,直接裸体读写。
聊天消息结构体
public class MsgData {
String name;
String msg;
String data;
int device;
//省略
}
其实搞什么都无所谓,毕竟目前走通pipeline,msg最重要。
handler
netty中handler可以说是我们编码著需要注意的,对于对称消息来说,无非就解码和编码。所以此处我们使用序列化将对象转化为byte,同理在返回,轻快简介。
关于理论篇可以去看netty理论部分
以下是主要的handler处理部分,使用的是Kryo的序列化。功能就是把消息转化为byte,byte转化为消息。然后把byte写入btyebuff将bytebuff转化为消息。由于消息是相同的类型,我们只用了一套编码器与解码器。
public class MsgDataDecoder extends ReplayingDecoder<MsgData> {
private Kryo kryo = new Kryo();
public static final int HEAD_LENGTH = 4;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < HEAD_LENGTH) { //这个HEAD_LENGTH是我们用于表示头长度的字节数。 由于Encoder中我们传的是一个int类型的值,所以这里HEAD_LENGTH的值为4.
return;
}
in.markReaderIndex(); //我们标记一下当前的readIndex的位置
int dataLength = in.readInt(); // 读取传送过来的消息的长度。ByteBuf 的readInt()方法会让他的readIndex增加4
if (dataLength < 0) { // 我们读到的消息体长度为0,这是不应该出现的情况,这里出现这情况,关闭连接。
ctx.close();
}
if (in.readableBytes() < dataLength) { //读到的消息体长度如果小于我们传送过来的消息长度,则resetReaderIndex. 这个配合markReaderIndex使用的。把readIndex重置到mark的地方
in.resetReaderIndex();
return;
}
byte[] body = new byte[dataLength]; //传输正常
in.readBytes(body);
Object o = convertToObject(body); //将byte数据转化为我们需要的对象
out.add(o);
}
private Object convertToObject(byte[] body) {
Input input = null;
ByteArrayInputStream bais = null;
try {
bais = new ByteArrayInputStream(body);
input = new Input(bais);
MsgData obj= kryo.readObject(input, MsgData.class);
System.out.println(obj);
return obj;
} catch (KryoException e) {
e.printStackTrace();
}finally{
IOUtils.closeQuietly(input);
IOUtils.closeQuietly(bais);
}
return null;
}
}
public class MsgDataEncoder extends MessageToByteEncoder<MsgData> {
private final Charset charset = Charset.forName("UTF-8");
private Kryo kryo = new Kryo();
private byte[] convertToBytes(MsgData car) {
ByteArrayOutputStream bos = null;
Output output = null;
try {
bos = new ByteArrayOutputStream();
output = new Output(bos);
kryo.writeObject(output, car);
output.flush();
return bos.toByteArray();
} catch (KryoException e) {
e.printStackTrace();
}finally{
IOUtils.closeQuietly(output);
IOUtils.closeQuietly(bos);
}
return null;
}
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MsgData msgData, ByteBuf byteBuf) throws Exception {
byte[]body=convertToBytes(msgData);
int dataLen=body.length;
byteBuf.writeInt(dataLen);
byteBuf.writeBytes(body);
}
}
在服务端有需要额外处理部分,比如每个人的上限与下限,在netty眼中,每个人就是个channel罢了,所以active与unactive代表着上限与下限。同理每个人的消息都是如此,这里没写很复杂的功能,想写,可以加handler,但是得注意handler得顺序与用法,具体还是看上篇文章关于handler责任链得部分。netty理论部分
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//内部会用ConcurrentMap来维护,线程安全
private static ChannelGroup channelGroup=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* channel处于就绪状态,客户端刚上线
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
String remoteAddress = channel.remoteAddress().toString();
//加入全局变量中
//channelGroup.writeAndFlush(Unpooled.copiedBuffer("【客户端】"+remoteAddress+"上线啦 "+format.format(new Date()),CharsetUtil.UTF_8));
channelGroup.writeAndFlush("【客户端】"+remoteAddress+"上线啦 "+format.format(new Date()));
channelGroup.add(channel);
//将当前channel加入到ChannelGroup
System.out.println("【客户端】"+remoteAddress+"上线啦");
}
/**
* channel离线
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
String remoteAddress = channel.remoteAddress().toString();
channelGroup.remove(channel);
channelGroup.writeAndFlush("【客户端】"+remoteAddress+"已下线 "+format.format(new Date()));
System.out.println("【客户端】"+remoteAddress+"已下线");
}
//读取数据事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Channel channel = ctx.channel();
String remoteAddress = channel.remoteAddress().toString();
System.out.println("【客户端】"+remoteAddress+":"+msg);
// msg= (MsgData)msg;
channelGroup.forEach(ch -> {
if(ch==channel) {
((MsgData) msg).setName("self");
ch.writeAndFlush(msg);
}else {
ch.writeAndFlush("【客户端】"+remoteAddress+":"+msg);
}
}
);
}
//异常发生事件
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//日志:远程主机强迫关闭了一个现有的连接。
//System.out.println(cause.getMessage());
ctx.close();
}
}
server
服务端代码
public class ServerNetty {
private int port=8080;
public ServerNetty(int port) {
this.port = port;
}
public static void main(String[] args) throws Exception {
int port = args.length > 0
? Integer.parseInt(args[0])
: 9999;
new ServerNetty(port).run();
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new MsgDataEncoder(),new MsgDataDecoder(),new NettyServerHandler());
}
}).option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
client
每一次channel得write和read如果抵达了NIO的缓冲区,pipeline的handler都会触发,所以在主线程,可以通过channel进行写操作触发到服务器。
public class ClientNetty {
public static void main(String[] args)throws Exception {
//1、创建一个线程组
EventLoopGroup group = new NioEventLoopGroup();
//2、创建客户端启动助手,完成相关配置
Bootstrap b = new Bootstrap();
b.group(group)//3、设置线程组
.channel(NioSocketChannel.class)//4、设置客户端通道的实现类
.handler(new ChannelInitializer<SocketChannel>() {//创建一个初始化通道对象
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new MsgDataEncoder());//对 String 对象自动编码,属于出站站处理器
sc.pipeline().addLast(new MsgDataDecoder());//把网络字节流自动解码为 String 对象,属于入站处理器
//6、在pipline中添加自定义的handler
sc.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("【客户端已启动】");
//7、启动客户端去连接服务器端 connect方法是异步的,sync方法是同步阻塞的
ChannelFuture cf =b.connect("127.0.0.1", 9999).sync();
System.out.println("---"+cf.channel().remoteAddress()+"------");
//循环监听用户键盘输入
Scanner scanner = new Scanner(System.in);
while(scanner.hasNextLine()) {
String msg = scanner.nextLine();
System.out.println("用户输入:"+msg);
MsgData msgData=new MsgData();
msgData.setMsg(msg);
//通过channel发送到服务器端
cf.channel().writeAndFlush(msgData);
}
//8、关闭连接(异步非阻塞)
cf.channel().closeFuture().sync();
System.out.print("Client is end.....");
}
}
总结
目前写的部分很简单,只是基础的实现部分,下一步可能会实现一些基本功能,如群发,私发,缓冲等待发等等。功能的实现可能有很多不同的细节,但思路都大差不差。所以走通pipeline是很重要。