用netty寫一個簡單的聊天程序

intro

netty封裝了nio,利用責任鏈模式,組織了許多handler。

上圖就是典型的責任鏈模式。

我們將寫一個簡單的聊天程序,學習更多handler。

server

必須要先寫server端,因爲它可以用telnet單獨測試。

首先是啓動類,一堆模版代碼:

public class ChatServer {
	public static void main(String[] args) {
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();

		try{
			ServerBootstrap serverBootstrap = new ServerBootstrap();
			serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)
					.childHandler(new ChatServerInitializer());

			ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
			channelFuture.channel().closeFuture().sync();
		}
		catch (InterruptedException e) {
			e.printStackTrace();
		}
		finally {
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}
}

我們只需看一下自己定義的初始化器就行了:

public class ChatServerInitializer 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 ChatServerInboundHandler());
	}
}

這裏有一個新的handler:DelimiterBasedFrameDecoder

如果有多個分隔符,這種基於frame的decoder會選擇將一行分割成最小的frame。

比如ABC\nDEF\r\n,有\r\n兩種分隔符,選擇\n會得到更小的單元。


然後是具體的處理邏輯,它在自定義的inboundhandler中:

public class ChatServerInboundHandler extends SimpleChannelInboundHandler<String> {

	private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
		Channel channel = ctx.channel();
		channelGroup.forEach(ch -> {
			if (channel != ch) {
				ch.writeAndFlush(channel.remoteAddress() + " 發送消息: " + msg + "\n");
			}
			else {
				channel.writeAndFlush("自己發的消息:" + msg + "\n");
			}
		});
	}

	@Override
	public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
		Channel channel = ctx.channel();
		channelGroup.writeAndFlush("用戶-" + channel.remoteAddress() + "加入\n");
		channelGroup.add(channel);
	}

	@Override
	public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
		Channel channel = ctx.channel();
		channelGroup.writeAndFlush("服務器-" + channel.remoteAddress() + "離開\n");
		channelGroup.remove(channel);
	}

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		Channel channel = ctx.channel();
		System.out.println(channel.remoteAddress() + "上線");
	}

	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		Channel channel = ctx.channel();
		System.out.println(channel.remoteAddress() + "下線");
	}
}

這其實就是channel的生命週期。一個channel表示與客戶端的一次連接。

要注意的就是ChannelGroup

/**
 * A thread-safe {@link Set} that contains open {@link Channel}s and provides
 * various bulk operations on them.  Using {@link ChannelGroup}, you can
 * categorize {@link Channel}s into a meaningful group (e.g. on a per-service
 * or per-state basis.)  A closed {@link Channel} is automatically removed from
 * the collection, so that you don't need to worry about the life cycle of the
 * added {@link Channel}.  A {@link Channel} can belong to more than one
 * {@link ChannelGroup}.
 *
 */

首先它是個set,所以我們可以遍歷。

然後它的作用是把一個channel加入到一個有意義的group,比如per-state basis這樣一個group。

它會把斷了的channel自動移除掉。所以channelGroup.remove(channel);是可以不用寫的。

當你調用channelGroup.writeAndFlush,它會向group中的每一個channel都發消息。


server端寫好了,我們啓動它,並拿telnet測試:

我開了三個窗口連接server,所以服務端的打印是:

第一個連的人會得到兩次服務端返回的消息:

我輸入一個字符串:

自己:

別人:

這其實已經可以用了。但是我們還是要完成客戶端的java代碼。

client

ublic class ChatClient {
	public static void main(String[] args) {
		EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
		try {
			Bootstrap bootstrap = new Bootstrap();
			bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
					.handler(new ChatClientInitializer());
			Channel channel = bootstrap.connect("localhost", 8899).channel();

			BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

			for (; ; ) {
				channel.writeAndFlush(bufferedReader.readLine() + "\r\n");
			}

		}
		catch (Exception e) {
			e.printStackTrace();
		}
		finally {
			eventLoopGroup.shutdownGracefully();
		}
	}
}

這是客戶端的主類。

客戶端連上服務器之後,就會去獲取控制檯的輸入,然後發給服務器。

客戶端的初始化器是一樣的:

public class ChatClientInitializer 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());
	}
}

客戶端的處理邏輯就是打印服務端發過來的消息:

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

開多個client連接服務器,會得到和telnet連接一樣的效果。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章