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知識及分享個人愛好文章: