学习Netty有一个多星期,参考《Netty实战》敲了Echo C/S 代码,也学习了channelHandler等组件。不满足客户端服务器一对一聊天,所以寻思着自己实现一个客户端和客户端一对一聊天,消息由服务器转发。
如果有代码写的不合适的地方,还请评论指正。
《Netty实战》电子书下载地址
类名 | 说明 |
---|---|
ServerHandler | 继承ChannelInboundHandlerAdapter接口 |
Server | 引导服务器端 |
继承ChannelInboundHandlerAdapter接口,重写一些方法,添加一些私有变量来存储和控制channel信息
//ServerHandler类
import java.net.InetSocketAddress;
import java.util.Calendar;
import java.util.Vector;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
@Sharable
public class ServerHandler extends ChannelInboundHandlerAdapter {
private static final int MAX_CONN = 2;//指定最大连接数
private int connectNum = 0;//当前连接数
//channelHandlerContext表
private Vector contexts = new Vector<>(2);
//获取当前时间
private String getTime() {
Calendar c = Calendar.getInstance();
int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH);
int date = c.get(Calendar.DATE);
int hour = c.get(Calendar.HOUR_OF_DAY);
int minute = c.get(Calendar.MINUTE);
int second = c.get(Calendar.SECOND);
return new String(year + "/" + month + "/" + date + " " + hour + ":" + minute + ":" + second);
}
/*
* 重写channelActive()方法
* 更新当前连接数
* 控制连接客户端的个数,超过则关闭该channel
* 更新contexts数组
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
connectNum++;
//控制客户端连接数量,超过则关闭
if (connectNum > MAX_CONN) {
ctx.writeAndFlush(Unpooled.copiedBuffer("达到人数上限".getBytes()));
ctx.channel().close();
//当前连接数的更新放在channelInactive()里
}
//更新contexts
contexts.add(ctx);
//控制台输出相关信息
InetSocketAddress socket = (InetSocketAddress) ctx.channel().remoteAddress();
System.out.println(socket.getAddress().getHostAddress() + ":" + socket.getPort() + "已连接");
System.out.println("当前连接数:" + connectNum);
ctx.writeAndFlush("hello client");
}
/*
* 重写channelInactive()方法
* 更新当前连接数
* 更新contexts数组
* 控制台输出相关信息
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
//更新当前连接数
connectNum--;
//更新contexts数组
contexts.remove(ctx);
//控制台输出相关信息
InetSocketAddress socket = (InetSocketAddress) ctx.channel().remoteAddress();
System.out.println(getTime() + ' ' + socket.getAddress().getHostAddress() + ":" + socket.getPort() + "已退出");
System.out.println("当前连接数:" + connectNum);
//对另一个客户端发出通知
if (contexts.size() == 1) {
contexts.get(0).writeAndFlush("对方退出聊天");
}
}
/*
* 重写channelRead()函数
* 读取数据
* 转发消息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// TODO Auto-generated method stub
String in = (String) msg;
System.out.println(getTime() + " 客户端" + ctx.channel().remoteAddress() + ":" + in);
//当只有一方在线时,发送通知
if (contexts.size() < 2) {
ctx.writeAndFlush("对方不在线");
return;
}
//获取另一个channelhandlercontxt的下表
int currentIndex = contexts.indexOf(ctx);
int anotherIndex = Math.abs(currentIndex - 1);
//给另一个客户端转发信息
contexts.get(anotherIndex).writeAndFlush(in);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// TODO Auto-generated method stub
if (!ctx.channel().isActive()) {
System.out.println(ctx.channel().remoteAddress() + "客户端异常");
}
cause.printStackTrace();
ctx.close();
}
}
用的最常规最简单的写法,和EchoServer差不多,等以后进一步学习再丰富内容
//Server类
import java.net.InetSocketAddress;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class Server {
public static void main(String[] args) throws InterruptedException {
new Server().start();
}
public Server() {
// TODO Auto-generated constructor stub
}
// 引导类
public void start() throws InterruptedException {
ServerHandler sHandler = new ServerHandler();
InetSocketAddress localSocket = new InetSocketAddress("127.0.0.1", 9990);
ServerBootstrap b = new ServerBootstrap();
b.group(newNioEventLoopGroup())
.localAddress(localSocket)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//添加译码器解码器
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(sHandler);
}
});
final ChannelFuture f = b.bind().sync();
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) throws Exception {
// TODO Auto-generated method stub
if (f.isSuccess()) {
System.out.println("服务器开启成功");
} else {
System.out.println("服务器开启失败");
f.cause().printStackTrace();
}
}
});
}
}
类名 | 说明 |
---|---|
ClientHandler | 继承SimpleChannelInboundHandler < String > 接口 |
Client | 引导客户端 |
继承SimpleChannelInboundHandler< String >接口,重写一些方法,添加私有closeChannel()来主动关闭连接
import java.util.Calendar;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
//因为加入了String类型的编码器和译码器,所以接口实例化为String类型
public class clientHandler extends SimpleChannelInboundHandler<String> {
//保存当前ChannelHandlerContext,在后面的closeChannel()中使用
private ChannelHandlerContext chc = null;
//获取当前时间
private String getTime() {
Calendar c = Calendar.getInstance();
int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH);
int date = c.get(Calendar.DATE);
int hour = c.get(Calendar.HOUR_OF_DAY);
int minute = c.get(Calendar.MINUTE);
int second = c.get(Calendar.SECOND);
return new String(year + "/" + month + "/" + date + " " + hour + ":" + minute + ":" + second);
}
//主动关闭连接
public void closeChannel(boolean readyToClose) throws InterruptedException {
if (readyToClose) {
System.out.println("即将关闭连接");
chc.channel().closeFuture();
chc.channel().close();
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
//保存当前ChannelHandlerContext
chc = ctx;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String in) throws Exception {
// TODO Auto-generated method stub
System.out.println(getTime() + " " + in);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
System.out.println(getTime() + " 断开连接");
ctx.channel().closeFuture();
ctx.channel().close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// TODO Auto-generated method stub
cause.printStackTrace();
System.out.println("有异常");
ctx.channel().close();
}
}
简单地引导客户端
//Client类
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
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.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class Client {
public void start() throws IOException, InterruptedException {
Bootstrap b = new Bootstrap();
EventLoopGroup g = new NioEventLoopGroup();
//创建对象,后面调用closeChannel()主动关闭连接
ClientHandler cHandler = new ClientHandler();
b.group(g)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
// TODO Auto-generated method stub
//添加编码器、译码器
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new StringEncoder());
sc.pipeline().addLast(cHandler);
}
});
final ChannelFuture f = b.connect("127.0.0.1", 9990).sync();
f.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture arg0) throws Exception {
// TODO Auto-generated method stub
if (f.isSuccess()) {
System.out.println("连接服务器成功");
} else {
System.out.println("连接服务器失败");
f.cause().printStackTrace();
}
}
});
Channel channel = f.channel();
/*
* 获取控制台输入
* 当输入了“再见”或“bye”时,停止输入
* 主动关闭连接
*/
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String in = br.readLine();
while (!(in.equals("再见") || in.equals("bye"))) {
channel.writeAndFlush(in);
in = br.readLine();
}
channel.writeAndFlush(in);
cHandler.closeChannel(true);
g.shutdownGracefully().sync();
}
public Client() {
// TODO Auto-generated constructor stub
}
public static void main(String[] args) throws Exception {
new Client().start();
}
}
控制台输入使用如下代码
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String in = br.readLine();
while (!(in.equals("再见") || in.equals("bye"))) {
channel.writeAndFlush(in);
in = br.readLine();
}
channel.writeAndFlush(in);
这段代码可以放在很多位置,不一定非要放在客户端内,在ClientHandler类的channelActive()里也可以加入这段代码。也就是说,在任何你想要通过控制台输入的地方都可以输入。
为了实现消息转发,我维护了一个数组用来保存每个连接的ChannelHandlerContext,这样在读取一个客户端发送的消息时,我可以迅速找到另一个客户端的ChannelHandlerContext并向其发送刚刚读取的信息。
这个技巧是从《Netty实战》6.3.2小节中学到的,在ChannelHandler中存储一些ChannelHandlerContext的引用以供以后使用,这将很方便地管理不同的channel,更广泛地说,在其他操作中对目标channel进行操作。这也要求ChannelHandler必须标注@Sharable,让多个ChannelPipeline共享同一个ChannelHandler时不会出现异常。
不过为了避免外部调用,标记为private是个不错的选择。
这个问题发生的很突然,当我一步步实现了各个功能后,一测试发现客户端发送一条消息后莫名其妙自动下线了,并且触发了channelInactive()函数。为此我付出两个小时的代价寻找问题的根源,这也是为什么我加了那么多控制台输出信息和getTime()函数,就是为了找出在那里出的问题。
最后在群里以为大神的指点下,找到了问题所在。
在《Netty实战》中的EchoServerHandler中,有这样一段override:
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
由于没有理解透彻,我简单地把这个函数也写到了我的ServerHandler中,最后就是这里出了问题。
在 ChannelInboundHandler 的方法中,channelReadComplete()方法是当Channel上的一个读操作完成时被调用,而上面这行代码在最后使用了ChannelFutureListener.CLOSE,再看一下CLOSE的原码:
ChannelFutureListener CLOSE = new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
future.getChannel().close();
}
};
这样就一目了然了,如果这样重写channelReadComplete(),那么服务器将会在读操作完成时,关闭连接。
但是在独自找问题的时候,我也搜索到了关于断线重连和心跳包这些对象,我也确实遇到了这个问题,出去办件事后就掉线了。后面会关注这些并实现他们。
这里也感谢那位大神,愿意花时间帮我看代码、改代码、解决问题、讲解原理,再次感谢!
这是我在测试客户端时发现的问题。当连接三个客户端时,由于服务器比较简单,所以转发操作会出现问题,最终干脆限制连接数,如果超过了2个,服务器就主动关闭连接。当然这不是个好方法,我记得在一些游戏中,如果当前连接人数太多会让你等待排队,而不是直接掉线,这样太不友好了。这个功能在日后我会关注和实现它。
这个算是一个优化吧,当只有一个客户端连接时,服务器应该提醒对方不在线。
由此我联想到QQ的做法,当一对一聊天时,即使对方不在线,信息也会发送过去,当对方上线后,信息会按时间顺序展示给他。我想到大概是利用缓存,将消息暂时存起来,当channelActive()调用,就把消息按顺序发送。这个日后实现。
还有一个就是聊天记录,这个应该可以存到数据库中,需要的时候通过关键字就可以查询,这个也日后实现吧。