原文出自:anxpp.com,转载请注明出处,想想!

1、概述

    Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

    官网:http://netty.io/

    目前最新的是版本是4.1.4(5.x版本官网已经弃用,不推荐使用)。

    Netty4.x同时也是原生支持Android的,所以后面的程序,放到android上也是可以正常运行的(亲测)。

    项目如果使用Maven开发,直接添加以下依赖即可:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.4.Final</version>
</dependency>

2、实现服务端

    首先是消息处理类ServerHandler

package com.anxpp.im.server.handler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.io.IOException;
import com.anxpp.im.common.IMMessage;
import com.anxpp.im.common.OnlineUser;
import com.anxpp.im.server.config.BaseConfig;
@ChannelHandler.Sharable
public class ServerHandler extends ChannelInboundHandlerAdapter implements BaseConfig{
    private ChannelHandlerContext ctx;
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        System.err.println("服务端Handler创建...");
        super.handlerAdded(ctx);
    }
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.err.println("channelInactive");
        super.channelInactive(ctx);
    }
    /**
     * tcp链路建立成功后调用
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        this.ctx = ctx;
        System.err.println("有客户端连接:"+ctx.channel().remoteAddress().toString());
    }
    /**
     * 发送消息
     */
    public boolean sendMsg(IMMessage msg) throws IOException {
        System.err.println("服务器推送消息:"+msg);
        ctx.writeAndFlush(msg);
        return msg.getMsg().equals("q") ? false : true;
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws IOException {
        System.err.println("服务器接收到消息:"+msg);
        IMMessage message = (IMMessage)msg;
        if(OnlineUser.get(message.getReceiveId())==null){
            OnlineUser.put(message.getUid(), ctx);
        }
        ChannelHandlerContext c = OnlineUser.get(message.getReceiveId());
        if(c==null){
            message.setMsg("对方不在线!");
            OnlineUser.get(message.getUid()).writeAndFlush(message);
        }
        else
            c.writeAndFlush(message);
    }
    /**
     * 异常处理
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("与客户端断开连接:"+cause.getMessage());
        cause.printStackTrace();
        ctx.close();
    }
}

    接收客户端连接,如果客户端没有连接过,就保存ChannelHandlerContext到Map中用于发送消息。然后判断消息接收方是否在线,若在线就直接发送消息,没有在线就返回“对方不在线”。当然,可以将消息发送者和消息接收者ID设置为相同就能仅开一个客户端自己跟自己聊天(类似Echo Server)。

    服务器程序Server

package com.anxpp.im.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import java.io.IOException;
import java.util.Scanner;
import com.anxpp.im.common.IMMessage;
import com.anxpp.im.common.MsgPackDecode;
import com.anxpp.im.common.MsgPackEncode;
import com.anxpp.im.server.config.BaseConfig;
import com.anxpp.im.server.handler.ServerHandler;
public class Server implements Runnable,BaseConfig{
    ServerHandler serverHandler = new ServerHandler();
    public static void main(String[] args) throws IOException{
        new Server().start();
    }
    public void start() throws IOException{
        new Thread(this).start();
        runServerCMD();
    }
    /**启动服务端控制台
     * @throws IOException */
    private void runServerCMD() throws IOException {
        //服务端主动推送消息
        int toID = 1;
        IMMessage message = new IMMessage(
                APP_IM,
                CLIENT_VERSION,
                SERVER_ID,
                TYPE_MSG_TEXT,
                toID,
                MSG_EMPTY);
        @SuppressWarnings("resource")
        Scanner scanner = new Scanner(System.in);
        do{
            message.setMsg(scanner.nextLine());
        }
        while (serverHandler.sendMsg(message));
    }
    public void run() {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024)
//                  .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65536, 0, 2, 0, 2));
                            ch.pipeline().addLast("msgpack decoder",new MsgPackDecode());
                            ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2));
                            ch.pipeline().addLast("msgpack encoder",new MsgPackEncode());
                            ch.pipeline().addLast(serverHandler);
                        }
                    }); 
            ChannelFuture f = b.bind(SERVER_PORT).sync();
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

    有几点要说明一下:

    1、runServerCMD方法用于开启控制台输入,可以给指定ID的客户端推送消息(推送服务器就是这么来的)。

    2、MsgPackEncode和MsgPackDecode用于消息的编解码。使用的是MessagePack(编码后字节流特小,编解码速度超快,同时几乎支持所有主流编程语言,详情见官网:http://msgpack.org/)。这样我们可以随意编写实体用于发送消息,他们的代码将在后面给出。

    3、LengthFieldBasedFrameDecoder和LengthFieldPrepender:因为TCP底层传输数据时是不了解上层业务的,所以传输消息的时候很容易造成粘包/半包的情况(一条完整的消息被拆开或者完整或者不完整的多条消息被合并到一起发送、接收),这两个工具就是Netty提供的消息编码工具,2表示消息长度(不是正真的长度为2,是2个字节)。

3、实现客户端

    客户端消息处理类ClientHandler

package com.anxpp.im.client.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.io.IOException;
import com.anxpp.im.client.Client;
import com.anxpp.im.client.config.BaseConfig;
import com.anxpp.im.common.IMMessage;
public class ClientHandler extends ChannelInboundHandlerAdapter implements BaseConfig {
    private ChannelHandlerContext ctx;
    /**
     * tcp链路简历成功后调用
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("成功连接服务器");
        this.ctx = ctx;
        IMMessage message = new IMMessage(
                APP_IM,
                CLIENT_VERSION,
                Client.UID,
                TYPE_CONNECT,
                SERVER_ID,
                MSG_EMPTY);
        sendMsg(message);
    }
    /**
     * 发送消息
     * @param msg
     * @return
     * @throws IOException 
     */
    public boolean sendMsg(IMMessage msg) throws IOException {
        System.out.println("client:" + msg);
        ctx.channel().writeAndFlush(msg);
        return msg.getMsg().equals("q") ? false : true;
    }
    /**
     * 收到消息后调用
     * @throws IOException 
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws IOException {
        IMMessage m = (IMMessage)msg;
        System.out.println(m.getUid() + ":" + m.getMsg());
    }
    /**
     * 发生异常时调用
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        System.err.println("与服务器断开连接:"+cause.getMessage());
        ctx.close();
    }
}

    代码跟服务端差不多,也就不多说了。

    客户端Client

package com.anxpp.im.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import java.io.IOException;
import java.util.Scanner;
import com.anxpp.im.client.config.BaseConfig;
import com.anxpp.im.client.handler.ClientHandler;
import com.anxpp.im.common.IMMessage;
import com.anxpp.im.common.MsgPackDecode;
import com.anxpp.im.common.MsgPackEncode;
public class Client implements Runnable,BaseConfig {
    public static int UID = 8888;
    private ClientHandler clientHandler = new ClientHandler();
    public static void main(String[] args) throws IOException{
        new Client().start();
    }
    public void start() throws IOException{
        new Thread(this).start();
        runServerCMD();
    }
    public void sendMsg(IMMessage msg) throws IOException {
        clientHandler.sendMsg(msg);
    }
    /**启动客户端控制台*/
    private void runServerCMD() throws IOException {
        IMMessage message = new IMMessage(
                APP_IM,
                CLIENT_VERSION,
                UID,
                TYPE_MSG_TEXT,
                UID,
                MSG_EMPTY);
        @SuppressWarnings("resource")
        Scanner scanner = new Scanner(System.in);
        do{
            message.setMsg(scanner.nextLine());
        }
        while (clientHandler.sendMsg(message));
    }
    @Override
    public void run() {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65536, 0, 2, 0, 2));
                    ch.pipeline().addLast("msgpack decoder",new MsgPackDecode());
                    ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2));
                    ch.pipeline().addLast("msgpack encoder",new MsgPackEncode());
                    ch.pipeline().addLast(clientHandler);
                }
            });
            ChannelFuture f = b.connect(SERVER_HOST, SERVER_PORT).sync();
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

    代码依然跟服务端大同小异,不多说。

4、编解码实现

    前面已经简单的介绍了MessagePack了,这里直接给代码。

    maven依赖是这样的:

<dependency>
    <groupId>org.msgpack</groupId>
    <artifactId>msgpack</artifactId>
    <version>0.6.12</version>
</dependency>

    也可以使用core包,不过编解码更复杂,但是速度会略快一点点。

    编码MsgPackEncode

package com.anxpp.im.common;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import java.io.IOException;
import org.msgpack.MessagePack;
/**
 * 编码工具
 */
public class MsgPackEncode extends MessageToByteEncoder<IMMessage>{
    @Override
    protected void encode(ChannelHandlerContext ctx, IMMessage msg, ByteBuf out) throws IOException {
        out.writeBytes(new MessagePack().write(msg));
    }
}

    解码MsgPackDecode

package com.anxpp.im.common;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
import org.msgpack.MessagePack;
/**
 * 解码工具
 */
public class MsgPackDecode extends MessageToMessageDecoder<ByteBuf>{
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        final int length = msg.readableBytes();
        final byte[] array = new byte[length];
        msg.getBytes(msg.readerIndex(), array, 0, length);
        out.add(new MessagePack().read(array,IMMessage.class));
    }
}

5、公共代码:

    消息类IMMessage

package com.anxpp.im.common;
public class IMMessage {
    //应用ID
    private byte appId;
    //版本号
    private int version;
    //用户ID
    private int uid;
    //消息类型    0:登陆    1:文字消息
    private byte msgType;
    //接收方
    private int receiveId;
    //消息内容
    private String msg;
    public IMMessage() {}
    /**
     * 构造方法
     * @param appId     应用通道
     * @param version   应用版本
     * @param uid       用户ID
     * @param msgType   消息类型
     * @param receiveId 消息接收者
     * @param msg       消息内容
     */
    public IMMessage(byte appId, int version, int uid, byte msgType, int receiveId, String msg) {
        this.appId = appId;
        this.version = version;
        this.uid = uid;
        this.msgType = msgType;
        this.receiveId = receiveId;
        this.msg = msg;
    }
    public byte getAppId() {
        return appId;
    }
    public void setAppId(byte appId) {
        this.appId = appId;
    }
    public int getVersion() {
        return version;
    }
    public void setVersion(int version) {
        this.version = version;
    }
    public int getUid() {
        return uid;
    }
    public void setUid(int uid) {
        this.uid = uid;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public byte getMsgType() {
        return msgType;
    }
    public void setMsgType(byte msgType) {
        this.msgType = msgType;
    }
    public int getReceiveId() {
        return receiveId;
    }
    public void setReceiveId(int receiveId) {
        this.receiveId = receiveId;
    }
    @Override
    public String toString() {
        return "appId:"+this.appId+",version:"+this.version+",uid:"+this.uid+",msgType:"+this.msgType+",receiveId:"+this.receiveId+",msg:"+this.msg;
    }
}

    在线用户OnlineUser

package com.anxpp.im.common;
import io.netty.channel.ChannelHandlerContext;
import java.util.HashMap;
/**
 * 在线用户表
 * @author Administrator
 *
 */
public class OnlineUser {
    //用户表
    private static HashMap<Integer, ChannelHandlerContext> onlineUser = new HashMap<Integer, ChannelHandlerContext>();
    public static void put(Integer uid, ChannelHandlerContext uchc){
        onlineUser.put(uid, uchc);
    }
    public static void remove(Integer uid){
        onlineUser.remove(uid);
    }
    public static ChannelHandlerContext get(Integer uid){
        return onlineUser.get(uid);
    }
}

    公共配置IMConfig

package com.anxpp.im.common;
public interface IMConfig {
    /**客户端配置*/
    int     CLIENT_VERSION = 1;         //版本号
    /**服务端配置*/
    String  SERVER_HOST = "127.0.0.1";  //服务器IP
    int     SERVER_PORT = 9090;         //服务器端口
    /**消息相关*/
    int     SERVER_ID   = 0;            //表示服务器消息
    byte    APP_IM = 1;         //即时通信应用ID为1
    byte    TYPE_CONNECT = 0;   //连接后第一次消息确认建立连接和发送认证信息
    byte    TYPE_MSG_TEXT = 1;  //文本消息
    String  MSG_EMPTY = "";     //空消息
}

6、完成

    此时就可以运行服务端可客户端测试了,本人亲测可用的。

    我的整个工程是使用Maven依赖的:

10

    目前已经将客户端部分移动到Android下使用了,不过功能还很简单,等完成后在放到Github上。

    时间真的是不够用啊,想想有个BUG调了我大半天全都是泪(最后还是强大的Google帮我解决了)。

    代码非常简陋,只是简单的实现了一下,解释得也很少,如果哪里不清楚,再一起探讨吧...