Netty是对NIO的封装。虽然其使用比Java BIO(也就是传统的基于流或字符阻塞型的数据读写)也要复杂。不过效率高。没有那么多废话。本片的目标是使用Netty实现一个聊天功能。功能的完善、需要大家在亲自操刀。

服务器端

Netty服务器端的一般写法。

package com.example.gch;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class Server {
    public static void main(String[] args){
        Server server = new Server(8080);
    }
    private ServerSocketChannel serverSocketChannel;


    public Server(int serverPort) {
        bind(serverPort);
    }

    private void bind(final int serverPort) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                //服务端要建立两个group,一个负责接收客户端的连接,一个负责处理数据传输
                //连接处理group
                EventLoopGroup boss = new NioEventLoopGroup();
                //事件处理group
                EventLoopGroup worker = new NioEventLoopGroup();
                ServerBootstrap bootstrap = new ServerBootstrap();
                // 绑定处理group
                bootstrap.group(boss, worker).channel(NioServerSocketChannel.class)
                        //保持连接数
                        .option(ChannelOption.SO_BACKLOG, 128)
                        //保持连接
                        .childOption(ChannelOption.SO_KEEPALIVE, true)
                        //处理新连接
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                // 增加任务处理
                                ChannelPipeline p = socketChannel.pipeline();
//                                p.addLast(new SimpleServerHandler());
                                p.addLast(new ServerHandler());
                            }
                        })
                ;

                //绑定端口,同步等待成功
                ChannelFuture future;
                try {
                    future = bootstrap.bind(serverPort).sync();
                    if (future.isSuccess()) {
                        serverSocketChannel = (ServerSocketChannel) future.channel();
                        System.out.println("服务端开启成功");
                    } else {
                        System.out.println("服务端开启失败");
                    }

                    //等待服务监听端口关闭,就是由于这里会将线程阻塞,导致无法发送信息,所以我这里开了线程
                    future.channel().closeFuture().sync();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    //优雅地退出,释放线程池资源
                    boss.shutdownGracefully();
                    worker.shutdownGracefully();
                }
            }
        });
        thread.start();
    }

    public void sendMessage(Object msg) {
        if (serverSocketChannel != null) {
            serverSocketChannel.writeAndFlush(msg);
        }
    }
}

使用NettyConfig 记录所有的客户端连接的Channel。

package com.example.gch;

import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * 存储整个工程的全局配置
 *
 * @author NewBies
 */
public class NettyConfig {

    /**
     * 存储每一个客户端接入进来时的channel对象
     */
    public static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}

和客户端交互定义的地方(PS.需要进行着重开发)

在**channelRead**中有对单聊和多聊的不同发送方式

package com.example.gch;

import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.Iterator;

public class ServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 客户端与服务端创建连接的时候调用
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端与服务端连接开始...");
        NettyConfig.group.add(ctx.channel());
    }

    /**
     * 客户端与服务端断开连接时调用
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端与服务端连接关闭...");
        NettyConfig.group.remove(ctx.channel());
    }

    /**
     * 服务端接收客户端发送过来的数据结束之后调用
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
        System.out.println("信息接收完毕...");
    }

    /**
     * 工程出现异常的时候调用
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    /**
     * 服务端处理客户端websocket请求的核心方法,这里接收了客户端发来的信息
     */
    @Override
    public void channelRead(ChannelHandlerContext channelHandlerContext, Object info) throws Exception {
        ByteBuf result = (ByteBuf) info;
        byte[] result1 = new byte[result.readableBytes()];
        result.readBytes(result1);
        System.out.println("我是服务端,我接受到了:" + new String(result1));
        //服务端使用这个就能向 每个连接上来的客户端群发消息
//        NettyConfig.group.writeAndFlush(info);
        channelHandlerContext.writeAndFlush(info);

        Iterator<Channel> iterator = NettyConfig.group.iterator();
        while (iterator.hasNext()) {
            //打印出所有客户端的远程地址
            System.out.println((iterator.next()).remoteAddress());
        }
        try {
            Thread.sleep(3000);
            // 向客户端发送消息
            String response = "hello client";
            // 在当前场景下,发送的数据必须转换成ByteBuf数组
            ByteBuf encoded = channelHandlerContext.alloc().buffer(4 * response.length());
            encoded.writeBytes(response.getBytes());
            //单聊
//            channelHandlerContext.write(encoded);
//            channelHandlerContext.flush();
            //群聊
            NettyConfig.group.writeAndFlush(encoded);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

客户端应用

这是Netty客户端一般写法

package com.example.gch;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class Client {
    public static void main(String[] args) {
        Client bootstrap = new Client(8080, "localhost");
    }

    private int port;
    private String host;
    private SocketChannel socketChannel;

    public Client(int port, String host) {
        this.host = host;
        this.port = port;
        start();
    }

    private void start() {

        Thread thread = new Thread(new Runnable() {
            public void run() {
                EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
                Bootstrap bootstrap = new Bootstrap();
                bootstrap.channel(NioSocketChannel.class)
                        // 保持连接
                        .option(ChannelOption.SO_KEEPALIVE, true)
                        // 有数据立即发送
                        .option(ChannelOption.TCP_NODELAY, true)
                        // 绑定处理group
                        .group(eventLoopGroup).remoteAddress(host, port)
                        .handler(new SimpleClientHandler());
                // 进行连接
                ChannelFuture future;
                try {
                    future = bootstrap.connect(host, port).sync();
                    // 判断是否连接成功
                    if (future.isSuccess()) {
                        // 得到管道,便于通信
                        socketChannel = (SocketChannel) future.channel();
                        System.out.println("客户端开启成功...");
                    } else {
                        System.out.println("客户端开启失败...");
                    }
                    // 等待客户端链路关闭,就是由于这里会将线程阻塞,导致无法发送信息,所以我这里开了线程
                    future.channel().closeFuture().sync();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //优雅地退出,释放相关资源
                    eventLoopGroup.shutdownGracefully();
                }
            }
        });

        thread.start();
    }

}

这里是客户端处理服务器连接的方法

package com.example.gch;


import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class SimpleClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("SimpleClientHandler.channelRead");
        ByteBuf result = (ByteBuf) msg;
        byte[] result1 = new byte[result.readableBytes()];
        result.readBytes(result1);
        System.out.println("Server said:" + new String(result1));
        result.release();
        //write to server.
//        try {
//            Thread.sleep(3000);
//            // 向客户端发送消息
//            String response = "hello server!";
//            // 在当前场景下,发送的数据必须转换成ByteBuf数组
//            ByteBuf encoded = ctx.alloc().buffer(4 * response.length());
//            encoded.writeBytes(response.getBytes());
//            ctx.write(encoded);
//            ctx.flush();
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 当出现异常就关闭连接
        cause.printStackTrace();
        ctx.close();
    }


    // 连接成功后,向server发送消息
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String msg = "hello Server!";
        ByteBuf encoded = ctx.alloc().buffer(4 * msg.length());
        encoded.writeBytes(msg.getBytes());
        ctx.write(encoded);
        ctx.flush();
    }
}

项目地址

https://github.com/guchuanhang/HttpLongDemo 服务器端&客户端

https://github.com/guchuanhang/HttpLongAndroidDemo 安卓客户端

欢迎大家fork&star!

对于标志用户谁是谁,可以在刚和服务器建立连接的时候,发送自己的身份信息。在服务器端进行user-channel映射。