1. 程式人生 > >Android端使用Netty+Protocol Buffer實現聊天室

Android端使用Netty+Protocol Buffer實現聊天室

之前寫過一篇Protocol Buffer使用轉換工具將proto檔案轉換成Java檔案流程及使用,就是在這篇的基礎上,將客戶端與伺服器規定好的協議ChatServer.proto轉換成ChatServerProto.java檔案。

一、實現步驟:
0、應用目錄
1、新增依賴庫
2、定義NettyChatClient類
3、NettyChatClient類涉及的介面、類
4、定義介面卡ChatAdapter類及佈局檔案fragment_item_view.xml
5、定義客戶端與伺服器端規定好的協議檔案ChatServer.proto
6、定義聊天室MainActivity類及佈局檔案activity_main.xml
7、效果圖

二、實現過程:
0、應用目錄
在這裡插入圖片描述

1、新增依賴庫

 	compile 'io.netty:netty-all:4.1.4.Final'
    compile 'com.google.protobuf:protobuf-java:3.5.0'
    
    compile 'com.google.code.gson:gson:2.3.1'
    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'com.android.support:recyclerview-v7:25.0.1'
    compile 'com.android.support:appcompat-v7:25.0.1'
    compile 'com.android.support:multidex:1.0.1'

2、定義NettyChatClient類

package com.showly.nettydemo.netty;

import android.util.Log;
import com.showly.nettydemo.netty.inferface.ILongConnClient;
import com.showly.nettydemo.netty.inferface.ILongConnResponseTrigger;
import com.showly.nettydemo.netty.utils.Decoder;
import com.showly.nettydemo.netty.utils.Encoder;
import java.net.InetSocketAddress;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.DefaultThreadFactory;

public class NettyChatClient implements ILongConnClient {
    private static final NioEventLoopGroup group = new NioEventLoopGroup(1, new DefaultThreadFactory("netty-tcp-client-event-loop-"));
    private ChannelHandlerContext channelHandlerContext;
    private ILongConnResponseTrigger trigger;

    public NettyChatClient(InetSocketAddress address, ILongConnResponseTrigger trigger) {
        this.trigger = trigger;
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group);

        bootstrap.channel(NioSocketChannel.class);
        bootstrap.option(ChannelOption.TCP_NODELAY, true);
        bootstrap.handler(new NettyClientInitializer());
        try {
            ChannelFuture future = bootstrap.connect(address).sync();
            future.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void sendMessage(MessageContent content) {
        if (channelHandlerContext != null) {
            channelHandlerContext.channel().writeAndFlush(content);
        }
    }

    @Override
    public void close() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        channelHandlerContext.channel().close();
    }

    @Override
    public boolean isOpen() {
        boolean isOpen = false;
        if (channelHandlerContext != null) {
            isOpen = channelHandlerContext.channel().isOpen();
        }
        return isOpen;
    }

    public static void shutdown() {
        if (!group.isShutdown()) group.shutdownGracefully();
    }

    private class NettyClientInitializer extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast("Encoder", new Encoder());
            pipeline.addLast("Decoder", new Decoder(1024 * 1024 * 2, true));
            pipeline.addLast(new NettyClientHandler());
        }
    }

    private class NettyClientHandler extends ChannelInboundHandlerAdapter {

        /**
         * 成功後呼叫
         */

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            channelHandlerContext = ctx;
        }

        /**
         * 收到訊息後呼叫
         */

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            trigger.response(((MessageContent) msg));
            Log.i("aaa==", "連線成功");
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            super.exceptionCaught(ctx, cause);
            Log.i("aaa==", "與伺服器斷開連線:" + cause);
            ctx.close();
        }
    }
}

3、NettyChatClient類涉及的介面、類
3-1、涉及的介面

ILongConnClient .java

package com.showly.nettydemo.netty.inferface;


import com.showly.nettydemo.netty.MessageContent;

/**
 * 長連線客戶端
 */
public interface ILongConnClient {
	/***
	 * 傳送訊息
	 * @param content
	 */
	void sendMessage(MessageContent content);

	/***
	 * 關閉
	 */
	void close();

	/***
	 * 關閉
	 */
	boolean isOpen();

}

ILongConnResponseTrigger.java

package com.showly.nettydemo.netty.inferface;

import com.showly.nettydemo.netty.MessageContent;
import java.io.UnsupportedEncodingException;

public interface ILongConnResponseTrigger {
	/***
	 * 觸發的響應
	 * @param data
	 */
	public void response(MessageContent data) throws UnsupportedEncodingException;
}

3-2、涉及的類

MessageContent.java

package com.showly.nettydemo.netty;

import com.showly.nettydemo.netty.utils.CrcUtil;
import com.showly.nettydemo.netty.utils.PooledBytebufFactory;
import com.showly.nettydemo.netty.utils.ProtocolHeader;

import io.netty.buffer.ByteBuf;

/**
 *  上下行訊息的封裝類.
 *  netty 只跟byte陣列打交道.
 *  其它自行解析
 */
public class MessageContent {
	protected byte [] bytes;
	protected int protocolId;

	public MessageContent(int protocolId, byte [] bytes) {
		this.bytes = bytes;
		this.protocolId = protocolId;
	}

	public int getProtocolId() {
		return protocolId;
	}

	public byte [] bytes() {
		return bytes;
	}

	/***
	 * 把header資訊也encode 進去. 返回bytebuf
	 *
	 * 業務不要呼叫這個方法.
	 *
	 * @return
	 */
	public ByteBuf encodeToByteBuf(){
		ByteBuf byteBuf = PooledBytebufFactory.getInstance().alloc(bytes.length + ProtocolHeader.REQUEST_HEADER_LENGTH);
		ProtocolHeader header = new ProtocolHeader(bytes.length, protocolId, (int) CrcUtil.getCrc32Value(bytes));
		header.writeToByteBuf(byteBuf);
		byteBuf.writeBytes(bytes);
		return byteBuf;
	}
}

PooledBytebufFactory.java

package com.showly.nettydemo.netty.utils;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;

/**
 * Bytebuf 使用堆內記憶體
 */
public class PooledBytebufFactory {

	private PooledByteBufAllocator allocor;

	private volatile static PooledBytebufFactory instance;

	private PooledBytebufFactory() {
		if (instance != null) throw new RuntimeException("Instance Duplication!");
		allocor = PooledByteBufAllocator.DEFAULT;
		instance = this;
	}

	public static PooledBytebufFactory getInstance() {
		if (instance == null) {
			synchronized (PooledBytebufFactory.class) {
				if (instance == null)
				{
					new PooledBytebufFactory();
				}
			}
		}
		return instance;
	}

	/**
	 * 分配一個bytebuf
	 * @return
	 */
	public ByteBuf alloc(){
		return alloc(256);
	}

	/**
	 * 分配一個bytebuf
	 * @param initialCapacity 初始容量
	 * @return
	 */
	public ByteBuf alloc(int initialCapacity){
		return allocor.directBuffer(initialCapacity);
	}
	/***
	 * 使用指定的bytes 分配一個bytebuf
	 * @param bytes
	 * @return
	 */
	public ByteBuf alloc(byte [] bytes){
		ByteBuf bytebuf =  alloc(bytes.length);
		bytebuf.writeBytes(bytes);
		return bytebuf;
	}
}

ProtocolHeader.java

package com.showly.nettydemo.netty.utils;

import io.netty.buffer.ByteBuf;

/**
 * 請求的固定頭
 */
public class ProtocolHeader {
	/**包頭識別碼*/
	private static  final byte [] MAGIC_CONTENTS = {'f', 'a', 's', 't'};


	/**請求頭固定長度*/
	public static final int REQUEST_HEADER_LENGTH = 16;
	/**辨別 請求使用*/
	private byte [] magic;
	// 長度
	private int length;
	// 請求的 響應的協議 id
	private int protocolId;
	// crc code
	private int crc;

	/***
	 * 建構函式
	 * 不使用datainputstream了.  不確定外面使用的是什麼.
	 * 由外面讀取後 調建構函式傳入
	 * @param length 後面byte陣列 長度
	 * @param protocolId 請求的id
	 * @param crc crc 完整校驗 (最後強轉int 校驗使用. int足夠)
	 */
	public ProtocolHeader(int length, int protocolId, int crc) {
		this.magic = MAGIC_CONTENTS;
		this.crc = crc;
		this.length = length;
		this.protocolId = protocolId;
	}

	/***
	 * 直接使用bytebuf 讀入一個header
	 * @param in
	 */
	public ProtocolHeader(ByteBuf in) {
		this.magic = new byte[MAGIC_CONTENTS.length];
		in.readBytes(magic);
		this.length = in.readInt();
		this.protocolId = in.readInt();
		this.crc = in.readInt();
	}
	/***
	 * crc是否有效
	 * @param crc
	 * @return
	 */
	public boolean crcIsValid(long crc) {
		return (int)crc == this.crc;
	}

	/**
	 * 得到魔數
	 * @return
	 */
	public byte[] getMagic() {
		return magic;
	}

	/***
	 * 後面的長度
	 * @return
	 */
	public int getLength() {
		return length;
	}

	public int getCrc() {
		return crc;
	}

	/***
	 * protocol 協議id
	 * @return
	 */
	public int getProtocolId() {
		return protocolId;
	}

	/**
	 * 檢查包頭是否是自己的包.
	 * @return
	 */
	public boolean isMagicValid(){
		for (int i = 0; i < MAGIC_CONTENTS.length; i++) {
			if (this.magic[i] != MAGIC_CONTENTS[i]) {
				return false;
			}
		}
		return true;
	}
	/**
	 * 將當前header 寫入 bytebuf
	 * @param out
	 */
	public  void writeToByteBuf(ByteBuf out) {
		out.writeBytes(magic);
		out.writeInt(length);
		out.writeInt(protocolId);
		out.writeInt(crc);
	}
}

Encoder.java

package com.showly.nettydemo.netty.utils;

import com.showly.nettydemo.netty.MessageContent;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class Encoder extends MessageToByteEncoder<MessageContent> {
	//private QLogger logger = LoggerManager.getLogger(LoggerType.FLASH_HANDLER);
	@Override
	protected void encode(ChannelHandlerContext ctx, MessageContent msg, ByteBuf out) throws Exception {
		ByteBuf srcMsg = msg.encodeToByteBuf();
		try {
			out.writeBytes(srcMsg);
		}finally {
			srcMsg.release();
		}
	}
}

Decoder.java

package com.showly.nettydemo.netty.utils;

import com.showly.nettydemo.netty.MessageContent;
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

public class Decoder extends ByteToMessageDecoder {
	//private QLogger logger = LoggerManager.getLogger(LoggerType.FLASH_HANDLER);
	private int maxReceivedLength;
	private boolean crc;
	public Decoder(int maxReceivedLength, boolean needCrc) {
		this.crc = needCrc;
		this.maxReceivedLength = maxReceivedLength;
	}
	@Override
	protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
		if (! in.isReadable(ProtocolHeader.REQUEST_HEADER_LENGTH)) return;
		in.markReaderIndex();

		ProtocolHeader header = new ProtocolHeader(in);
		if (! header.isMagicValid()) {
			//Log.e("aaa==","Invalid message, magic is error! "+ Arrays.toString(header.getMagic()));
			ctx.channel().close();
			return;
		}

		if (header.getLength() < 0 || header.getLength() > maxReceivedLength) {
			//Log.e("aaa==","Invalid message, length is error! length is : "+ header.getLength());
			ctx.channel().close();
			return;
		}

		if (! in.isReadable(header.getLength())) {
			in.resetReaderIndex();
			return;
		}
		byte [] bytes = new byte[header.getLength()];
		in.readBytes(bytes);

		if (crc && ! header.crcIsValid(CrcUtil.getCrc32Value(bytes))) {
			//Log.e("aaa==","Invalid message crc! server is : "+ CrcUtil.getCrc32Value(bytes) +" client is "+header.getCrc());
			ctx.channel().close();
			return;
		}

		MessageContent context = new MessageContent(header.getProtocolId(), bytes);
		out.add(context);
	}
}

CrcUtil.java

package com.showly.nettydemo.netty.utils;

import java.util.zip.CRC32;

public class CrcUtil {
	/**
	 * 得到crc32計算的crc值
	 * @param bytes
	 * @return
	 */
	public static long getCrc32Value(byte [] bytes) {
		CRC32 crc32 = new CRC32();
		crc32.update(bytes);
		return crc32.getValue();
	}
}

4、定義介面卡ChatAdapter類及佈局檔案fragment_item_view.xml
ChatAdapter.java

package com.showly.nettydemo.adapter;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.pinpin.app.chat.proto.ChatServerProto;
import com.showly.nettydemo.R;
import com.showly.nettydemo.utils.CustomRoundView;
import java.util.ArrayList;
import java.util.List;

public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ViewHolder> {
    private Context mContext;
    private List<ChatServerProto.ChatInfo> mUserData = new ArrayList<>();

    public ChatAdapter(Context context) {
        this.mContext = context;
    }


    public void setChatWorldData(List<ChatServerProto.ChatInfo> chatsList) {
        this.mUserData = chatsList;
    }


    @Override
    public ChatAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //例項化展示view
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fragment_item_view, parent, false);
        //例項化viewHolder
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }


    @Override
    public void onBindViewHolder(ViewHolder holder, final int position) {
        //將position儲存在itemView的Tag中,以便點選時進行獲取
        holder.itemView.setTag(position);

        if (mUserData != null) {
            ChatServerProto.UserInfo userInfo = mUserData.get(mUserData.size() - position-1).getInfo();
            holder.tvUserName.setText(userInfo.getNickName());//使用者名稱
            holder.infosContent.setText((mUserData.get(mUserData.size() - position-1).getContent()).toStringUtf8());//內容

            //頭像
            Glide.with(mContext).load(userInfo.getHeadPic())
                    .diskCacheStrategy(DiskCacheStrategy.RESULT)
                    .placeholder(R.drawable.face)
                    .into(holder.ivUserHead);

            if (userInfo.getGenderValue() == 1) {//1為男 , 0或2為女
                holder.mUserSex.setBackgroundResource(R.drawable.i8live_icon_male);
            } else {
                holder.mUserSex.setBackgroundResource(R.drawable.i8live_icon_female);
            }
        }
    }

    @Override
    public int getItemCount() {
        return mUserData.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        TextView tvContent;
        TextView tvUserName;
        TextView tvLocation;
        CustomRoundView ivUserHead;
        ImageView mUserSex;
        TextView infosContent;
        TextView atMe;

        public ViewHolder(View itemView) {
            super(itemView);
            tvContent = (TextView) itemView.findViewById(R.id.tv_comtent);
            tvUserName = (TextView) itemView.findViewById(R.id.user_name);
            tvLocation = (TextView) itemView.findViewById(R.id.tv_location);
            ivUserHead = (CustomRoundView) itemView.findViewById(R.id.chat_user_head);
            mUserSex = (ImageView) itemView.findViewById(R.id.iv_sex);
            infosContent = (TextView) itemView.findViewById(R.id.tv_infomation_content);
            atMe = (TextView) itemView.findViewById(R.id.tv_at);
        }
    }
}

fragment_item_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
    android:id="@+id/tv_comtent"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:layout_marginTop="18dp"
    android:text="12:36"
    android:visibility="gone"
    android:textColor="#777777"
    android:textSize="10sp" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="18dp">

        <LinearLayout
            android:id="@+id/chat_user_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:orientation="vertical">

            <com.showly.nettydemo.utils.CustomRoundView
                android:id="@+id/chat_user_head"
                android:layout_width="43dp"
                android:layout_height="43dp"
                android:src="@drawable/face" />

            <TextView
                android:id="@+id/tv_location"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/chat_user_head"
                android:layout_gravity="center"
                android:layout_marginTop="5dp"
                android:visibility="gone"
                android:text="廣州" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/chat_user_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="4dp"
            android:layout_toRightOf="@id/chat_user_view"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/user_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="千里小飛飛"
                android:textColor="#000000"
                android:textSize="12sp" />

            <ImageView
                android:id="@+id/iv_sex"
                android:layout_width="12dp"
                android:layout_height="12dp"
                android:layout_marginLeft="8dp"
                android:background="@drawable/i8live_sex_girl" />

            <RelativeLayout
                android:layout_width="27dp"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:visibility="gone"
                android:background="@drawable/i8live_vip1">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentRight="true"
                    android:layout_marginRight="3dp"
                    android:text="12"
                    android:textSize="10sp"/>
            </RelativeLayout>

        </LinearLayout>

        <RelativeLayout
            android:id="@+id/rl_tv_content"
            android:layout_below="@id/chat_user_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="9dp"
            android:layout_marginLeft="4dp"
            android:background="@drawable/i8live_message_bg3_6"
            android:layout_toRightOf="@id/chat_user_view">

            <TextView
                android:id="@+id/tv_at"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="[有人@我]"
                android:textColor="#ff5959"
                android:textSize="13sp"
                android:layout_marginRight="8dp"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:visibility="gone"
                />

            <TextView
                android:id="@+id/tv_infomation_content"
                android:layout_toLeftOf="@+id/tv_at"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:layout_centerVertical="true"
                android:layout_marginLeft="20dp"
                android:layout_marginRight="10dp"
                android:layout_marginTop="7dp"
                android:layout_marginBottom="7dp"
                android:textSize="15sp"
                android:text="反正我什麼也沒說,附近的時刻反倒是第三方几十塊地方的看法的說法"
                android:textColor="#000000"/>
        </RelativeLayout>
    </RelativeLayout>
</LinearLayout>

5、定義客戶端與伺服器端規定好的協議檔案ChatServer.proto

syntax = "proto3";

option java_package = "com.pinpin.app.chat.proto";
option java_outer_classname = "ChatServerProto";


// 聊天內容型別
enum ContentType {
	NORMAL = 0; 		// 普通文字聊天
	ANONYMOUS = 1; 		// 匿名文字聊天(輸入框旁邊有個勾選)
}
// 性別
enum GenderType {
	SECRET = 0; 			// 保密
	MALE = 1;				// 男
	FEMALE = 2;				// 女

}
// 使用者資訊
message UserInfo {
	int32 uid = 1;
	string headPic = 2;
	GenderType gender = 3;
	bool vip = 4; //Vip
	int32 level = 5; //等級
	string nickName = 6; //暱稱
}

// 響應訊息的一個頭. 每個訊息都會帶上.
message ResponseHeader {
	int32  status = 1;			// 狀態 非0 為失敗
	string  msg = 2; 			// 狀態描述
}

// 聊天使用的訊息體物件
message ChatInfo {
	UserInfo info = 1; 			// 使用者資訊
	string location = 2; 			// 使用者的位置. 
	ContentType type = 3; 			// 訊息型別
	bytes content = 4; 			// 訊息內容
	int64 dt = 5; 				// 時間戳
}

// cmdId = 1000
message LoginRequest {
	int32 uid = 1; 			//uid
	string token = 2;		// token
}

// cmdId = 1000000
message LoginResponse {
	ResponseHeader header = 1;
	repeated ChatInfo chats = 2;		// 10條歷史記錄
	bool roomReconn = 3; 				// 房間重連
}

// cmdId = 1001 切換城市 世界為 "WORLD"
message ChangeCityRequest {
	string  city = 1; 		 		// city code
}


// cmdId = 1000001
message ChangeCityResponse {
	ResponseHeader header = 1;
	repeated ChatInfo chats = 2;			// 10條歷史記錄
}

enum LocationType {
	WORLD = 0;//世界資訊
	REDBAGROOM = 1; //紅包活動房間
}

// cmdId = 1002
message SendChatMsgRequest {
	string location = 1;        //位置
	ContentType type = 2; 		// 訊息型別
	bytes content = 3; 			// 訊息內容. 以後可能圖片什麼. 我這裡不寫死. 客戶端給我位元組陣列.
	LocationType locationType = 4 ;// 訊息位置
}

// cmdId = 1000002  推送的聊天資訊廣播協議
message ChatInfoBroadcastResponse {
	ResponseHeader header = 1; 	
	ChatInfo chat = 2; 		// 廣播的內容
	LocationType locationType = 3 ;// 訊息位置
} 

// cmdId = 1003 心跳. 不需要傳送東西. 告訴伺服器還活著
message HeartRequest {

}

// 這裡僅服務端使用這個, 客戶端按照下行的id 解析即可.
message DefaultHeaderResponse {
	ResponseHeader header = 1; 		// 頭
}

6、定義聊天室MainActivity類及佈局檔案activity_main.xml
MainActivity.java

package com.showly.nettydemo;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.pinpin.app.chat.proto.ChatServerProto;
import com.showly.nettydemo.adapter.ChatAdapter;
import com.showly.nettydemo.netty.MessageContent;
import com.showly.nettydemo.netty.NettyChatClient;
import com.showly.nettydemo.netty.inferface.ILongConnResponseTrigger;
import com.showly.nettydemo.utils.WrapContentLinearLayoutManager;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.util.List;

public class MainActivity extends Activity {
    //Netty框架中使用的域名和埠號
    public static final String CHAT_HOST = "域名";
    public static final int CHAT_PORT = 18888;
    private RecyclerView chatRecyclerView;
    private WrapContentLinearLayoutManager wrapContentLinearLayoutManager;
    private NettyChatClient nettyChatClient;
    private ChatServerProto.LoginResponse loginReponse;
    private List<ChatServerProto.ChatInfo> chatsList;
    private Handler handler;
    private int protocolId;
    private EditText chatEdit;
    private Button sentBtn;
    private ChatAdapter chatAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getActionBar().hide();//隱藏掉整個ActionBar
        setContentView(R.layout.activity_main);

        //建立屬於主執行緒的handler
        handler = new Handler();

        initView();
        initNettyClient();//連線地址
        initData();
        initListener();
    }

    private void initNettyClient() {
        nettyConnent();//建立連線

        //登入Netty伺服器
        ChatServerProto.LoginRequest loginRequest = ChatServerProto.LoginRequest
                .newBuilder()
                .setUid(3915447)
                .setToken("a3b41060249d995dce0108dff3d318fba95f5f62")
                .build();

        MessageContent messageContent = new MessageContent(1000, loginRequest.toByteArray());

        if (nettyChatClient != null) {
            nettyChatClient.sendMessage(messageContent);
        }
    }

    //建立連線
    private void nettyConnent() {
        nettyChatClient = new NettyChatClient(new InetSocketAddress(CHAT_HOST, CHAT_PORT), new ILongConnResponseTrigger() {
            @Override
            public void response(MessageContent data) throws UnsupportedEncodingException {
                protocolId = data.getProtocolId();

                Log.i("aaaa==123==", protocolId + "");

                switch (protocolId) {
                    case 1000000://登入Netty成功後返回初始聊天資料
                        try {
                            loginReponse = ChatServerProto.LoginResponse.parseFrom(data.bytes());
                            chatsList = loginReponse.getChatsList();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        break;
                    case 1000002://推送的聊天資訊廣播協議
                        try {
                            ChatServerProto.ChatInfoBroadcastResponse chatInfoBroadcastResponse
                                    = ChatServerProto.ChatInfoBroadcastResponse.parseFrom(data.bytes());
                            ChatServerProto.ChatInfo chatInfo = chatInfoBroadcastResponse.getChat();
                            chatsList.add(chatsList.size(), chatInfo);//將資料插入最後一條
                        } catch (InvalidProtocolBufferException e) {
                            e.printStackTrace();
                        }
                        break;
                }

                //通知更新介面
                new Thread() {
                    public void run() {
                        handler.post(runnableUi);
                    }
                }.start();
            }
        });
    }

    private void initView() {
        chatRecyclerView = (RecyclerView) findViewById(R.id.chat_recyclerview);
        chatEdit = (EditText) findViewById(R.id.chat_et);
        sentBtn = (Button) findViewById(R.id.chat_send_btn);

        wrapContentLinearLayoutManager = new WrapContentLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        wrapContentLinearLayoutManager.setStackFromEnd(true);//滑動底部
        chatRecyclerView.setLayoutManager(wrapContentLinearLayoutManager);
        chatRecyclerView.setHasFixedSize(true);
    }

    private void initData() {

    }

    private void initListener() {
        sentBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String content = chatEdit.getText().toString().trim();
                if (TextUtils.isEmpty(content)) {
                    showToast("內容不能為空");
                }

                ChatServerProto.SendChatMsgRequest chatMsgRequest = ChatServerProto.SendChatMsgRequest
                        .newBuilder()
                        .setLocation("300110000")//300110000為廣州地址程式碼
                        .setType(ChatServerProto.ContentType.ATMSG)
                        .setContent(ByteString.copyFromUtf8(content))
                        .setLocationType(ChatServerProto.LocationType.WORLD)
                        .build();

                MessageContent messageContent = new MessageContent(1002, chatMsgRequest.toByteArray());

                if (nettyChatClient != null) {
                    nettyChatClient.sendMessage(messageContent);
                }

            }
        });
    }

    // 構建Runnable物件,在runnable中更新介面
    Runnable runnableUi = new Runnable() {
        @Override
        public void run() {
            switch (protocolId) {
                case 1000000:
                    // Collections.reverse(chatsList);//將集合資料倒敘
                    //介面卡
                    chatAdapter = new ChatAdapter(MainActivity.this);
                    chatAdapter.setChatWorldData(chatsList);//傳遞資料
                    chatRecyclerView.setAdapter(chatAdapter);//繫結介面卡
                    break;
                case 1000002:
                    if (chatAdapter != null) {
                        chatAdapter.setChatWorldData(chatsList);//傳遞使用者資料
                        chatAdapter.notifyDataSetChanged();//更新列表資料
                        chatRecyclerView.scrollToPosition(chatsList.size() - 1);
                    }
                    break;
            }
        }
    };

    //吐司
    private void showToast(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
    android:id="@+id/chat_recyclerview"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"/>

<LinearLayout
    android:id="@+id/chat_edit_ll"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="#fff"
    android:orientation="horizontal"
    android:padding="5dp"
    >

    <RelativeLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="5dp"
        android:layout_weight="6"
        android:background="@drawable/edittext_chat">

        <EditText
            android:id="@+id/chat_et"
            android:layout_width="match_parent"
            android:layout_height="36dp"
            android:background="@drawable/edittext_chat"
            android:hint="說點什麼(輸入框)"
            android:paddingLeft="8dp"
            android:textColor="#777777"
            android:textCursorDrawable="@drawable/edittext_cursor_color"
            android:textSize="15sp" />
    </RelativeLayout>


    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="5dp">

        <Button
            android:id="@+id/chat_send_btn"
            android:layout_width="45dp"
            android:layout_height="28dp"
            android:layout_centerVertical="true"
            android:background="@drawable/corners_with_red"
            android:text="傳送"
            android:textColor="#fff"
            android:textSize="13sp" />
    </RelativeLayout>

</LinearLayout>
7、效果圖 ![在這裡插入圖片描述](https://img-blog.csdn.net/20180927181525983?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xvbmd4dWFuemhpZ3U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

以下是個人公眾號(longxuanzhigu),之後釋出的文章會同步到該公眾號,方便交流學習Android知識及分享個人愛好文章:
在這裡插入圖片描述