之前写过一篇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、效果图
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 {
@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 {
//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
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 {
private Context mContext;
private List mUserData = new ArrayList<>();
public ChatAdapter(Context context) {
this.mContext = context;
}
public void setChatWorldData(List 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
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 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
7、效果图 ![在这里插入图片描述](https://img-blog.csdn.net/20180927181525983?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xvbmd4dWFuemhpZ3U=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
Demo代码链接:底部公众号回复"NettyDemo"即可获取。
以下是个人公众号(longxuanzhigu),之后发布的文章会同步到该公众号,方便交流学习Android知识及分享个人爱好文章: