- TNClientInitializer.class里面没有用TLS加密 文档说用了
- 文档中说用对方的公钥加密,也被注释了,实际上没有加密
- 主要在NettyService中
- 注册用户:
- TNRegisterRequest(rUserName, rPasswd, rNickName, rPublicKey, machineId, deviceName)
- TNLoginResponse
- 用户登陆:
- new TNLoginRequest(userName, password, machineId, deviceName)
- 用户注销:
- 关闭长连接netty_ctx.close()
- 发送消息:
- netty_ctx.writeAndFlush((TNSendMessage) msg.obj)
- 发送文件:
- TNFileSendData tnFileSendData = (TNFileSendData) msg.obj;
- 查询用户信息:
- TNSearchUserInfoRequest((String)msg.obj)
- 查询用户群信息:
- TNSearchGroupInfoRequest((String)msg.obj,userName)
- 没有实装
- 通过公钥查询用户信息
- TNSearchUserInfoRequestByPublicKey((String)msg.obj)
- 创建群聊
- TNCreateGroupRequest tnCreateGroupRequest = (TNCreateGroupRequest) msg.obj
- 没有实装
- 添加好友
- TNFollowRequest(username, name)
- 使用的是TNRegisterRequest这个类
public class TNRegisterRequest implements Serializable {
private String userName;
private String password;
private String nickName;
// 公钥是通过RSA算法生成的,再确定创建用户时生成
// 生成的私钥会在注册成功后放入本地的数据库
private String publicKey;
private String macAddress;
private String macName;
// 头像的二进制数组
private byte[] avatar;
public TNRegisterRequest(String userName, String password, String nickName, String publicKey, String macAddress,String macName) {
this.userName = userName;
this.password = password;
this.nickName = nickName;
this.macAddress = macAddress;
this.macName = macName;
this.publicKey = publicKey;
}
...
}
- 一个注册请求示例
- 密码password和userName都经过了md5加密
- 使用的是TNRegisterResponse类
- message的可能值:
- "注册成功"
- "注册失败,该用户名已经存在"
- "注册失败"
- 主要根据isRegistered判断是否注册成功
- message的可能值:
public class TNRegisterResponse implements Serializable {
private boolean isRegistered;
private String message;
private String publicKey;
public TNRegisterResponse(boolean isRegistered, String message, String publicKey) {
this.isRegistered = isRegistered;
this.message = message;
this.publicKey = publicKey;
}
...
}
- 一个注册返回示例
- 如果注册成功将用户信息存储到本地
UserInfo userInfo = new UserInfo(
// String.valueOf(binding.editTextRegisterUsername.getText()),
MD5Util.getStringmd5(String.valueOf(binding.editTextRegisterUsername.getText())),
null,
String.valueOf(binding.editTextRegisterNickname.getText().toString()),
rsaPublicKey,
AESEncrypt.encrypt(rsaPrivateKey,String.valueOf(binding.editTextRegisterPassword1.getText().toString())+MD5substring),
// rsaPrivateKey,
deviceName,
machineId,
0
);
- 使用的是TNLoginRequest这个类
public class TNLoginRequest implements Serializable {
private String userName;
private String passwd;
// session没有实装
private String session;
private String machineId;
private String macName;
public TNLoginRequest(String userName, String passwd, String machineId, String macName) {
this.userName = userName;
this.passwd = passwd;
this.machineId = machineId;
this.macName = macName;
}
...
}
- 一个登陆请求实例
- passwd和userName都经过了md5加密
- 主要使用的是TNLoginResponse这个类
- 根据isLogined判断是否登陆成功
- message可能值:
- "登陆成功"
- "用户名或密码错误"
- "该设备未绑定此账号,访问被拒绝"
public class TNLoginResponse implements Serializable {
private boolean isLogined;
private String message;
// session没有实装
private String session;
private String userName;
public TNLoginResponse(boolean isLogined, String message) {
this.isLogined = isLogined;
this.message = message;
}
...
}
- 一个登陆返回实例
- 里面的session没有实装
- 在登陆成功后会发送请求向服务端获取用户的信息
- 使用的是TNGetUserInfoRequest这个类
- 只用用户名一个属性,通过用户名向服务器获取信息
public class TNGetUserInfoRequest implements Serializable {
private String userNameToGet;
public TNGetUserInfoRequest(String userNameToGet) {
this.userNameToGet = userNameToGet;
}
...
}
- 使用TNGetUserInfoResponse这个类
- followList存放的是你加了好友的ueserName(经过md5加密后的)
- followersList存放的是加了你好友的ueserName(经过md5加密后的)
public class TNGetUserInfoResponse implements Serializable {
String userName;
String nickName;
String bio;
String publicKey;
String deviceName;
String deviceAddress;
byte[] avatar;
boolean isExist;
boolean isOnline;
boolean isFollowYou = false;
boolean isFollowedByMe = false;
List<String> followList;
List<String> followersList;
String message;
public TNGetUserInfoResponse(boolean isExist, String userName, String message) {
this.isExist = isExist;
this.userName = userName;
this.message = message;
}
...
}
- 一个返回示例
- 这个类可以根据用户名返回用户的头像、好友、公钥等信息,在登陆后会向服务器请求登陆用户的信息,更新用户的好友列表
- 消息类型,使用了一个枚举类来区分消息类型
public enum MsgType {
TEXT ,//文本消息
IMAGE ,//图片消息
FILE ,//文件消息
VIDEO ,//视频消息
AUDIO ,//语音消息
LOCATION, //位置消息
}
public class TNSendMessage implements Serializable {
private String uuid;
//是群消息还是个人消息
// 群聊没有实装
private int type;
// 文字0,图片1,文件2,视频3,语音4
private int contentType;
private int sendStatus;
private String author;
private String nameToMessage;
private String groupToMessage;
// 一个给对方的消息,用对方的公钥加密
private String message;
// 一个发送到服务端等服务端发回来的消息用自己的公钥加密
private String messageToMe;
private String sendTime;
// 文件加解密
private String key;
private String keyToMe = "";
// 是否是历史消息
private boolean isHistory;
//文件后缀名
private String ext ;
//文件名称,可以和文件名不同,仅用于界面展示
private String displayName;
//文件长度(字节)
private long size;
//本地文件保存路径
private String localPath;
//文件下载地址
private String remoteUrl;
//文件内容的MD5
private String md5;
//扩展信息,可以放置任意的数据内容,也可以去掉此属性
private String extra;
//缩略图文件的本地路径
private String thumbPath;
//缩略图远程地址
private String thumbUrl;
//缩略图的字节 (因为压缩过所以直接发)
private byte[] thumb;
//是否压缩(false:原图,true:压缩过)
private boolean compress;
//高度
private int height;
//宽度
private int width;
//视频消息长度
private long duration;
//是否是阅后即焚
private int isFire;
//阅后即焚时间
private long fireTime;
//是否是密聊
private int isSecret;
public TNSendMessage() {
}
public TNSendMessage(String uuid, int type, int contentType, int sendStatus, String author, String nameToMessage, String groupToMessage, String message, String messageToMe , String sendTime) {
this.uuid = uuid;
this.type = type;
this.contentType = contentType;
this.sendStatus = sendStatus;
this.author = author;
// 下面两个属性是接受方的username
this.nameToMessage = nameToMessage;
// 群聊功能没有实装
this.groupToMessage = groupToMessage;
// 给对方的消息
this.message = message;
// 给自己的消息
this.messageToMe = messageToMe;
this.sendTime = sendTime;
this.keyToMe = "";
this.isSecret = 0;
}
...
}
- 发送消息代码详解
//文本消息
// 这里的输入就是发送的消息,和mEtContent.getText().toString()相同
private void sendTextMsg(String message_text) {
// 这里的ChatMessage应该是用于UI展示
final ChatMessage mMessgaeText=getBaseSendMessage(MsgType.TEXT);
TextMsgBody mTextMsgBody=new TextMsgBody();
try {
// 用自己的公钥加密发送的信息
byte[] encryptBymPublicKey = RSAUtils.encryptByPublicKey(mEtContent.getText().toString().getBytes(Charsets.toCharset("UTF-8")), mPublicKey);
mTextMsgBody.setMessage(Base64.encodeToString(encryptBymPublicKey,Base64.DEFAULT));
}catch (Exception e){
e.printStackTrace();
mTextMsgBody.setMessage(message_text);
}finally {
mMessgaeText.setMsgBody(gson.toJson(mTextMsgBody));
//开始发送
// 这里是更新UI
// mMessageText中的MsgBody属性就是用自己公钥加密后的消息
mAdapter.addData(mMessgaeText);
}
// 接下来是构建聊天消息的数据包结构,通过NettyService发送到服务端
Message messageText = Message.obtain();
messageText.what = 0x30;
String messageToMe = "";
String messageToContact = "";
try {
Log.d("mytest", "contact_name: "+contact_name);
Log.d("mytest", "contact_publicKey: "+contact_publicKey);
Log.d("mytest", "mPublicKey: "+mPublicKey);
// 构建了两个字符串一个用自己的公钥加密,一个用对方的公钥加密
// 这里是用自己的公钥加密,此消息最终由服务器发送给我,存到我自己的数据库
byte[] encryptBymPublicKey = RSAUtils.encryptByPublicKey(mEtContent.getText().toString().getBytes(Charsets.toCharset("UTF-8")), mPublicKey);
messageToMe = Base64.encodeToString(encryptBymPublicKey,Base64.DEFAULT);
// 用对方的公钥加密的字符串,此消息最终由服务器发送给对方,存到对方的数据库
byte[] encryptByContactPublicKey = RSAUtils.encryptByPublicKey(mEtContent.getText().toString().getBytes(Charsets.toCharset("UTF-8")), contact_publicKey);
// messageToContact = Base64.encodeBase64String(encryptByContactPublicKey);
messageToContact = Base64.encodeToString(encryptByContactPublicKey,Base64.DEFAULT);
}
catch (Exception e){
e.printStackTrace();
}
finally {
// 构建TNSendMessage
TNSendMessage tnSendMessage = new TNSendMessage(mMessgaeText.getUuid(),contact_type,0,1,userName,contact_type==1?contact_name:"",contact_type==2?contact_name:"",messageToContact,messageToMe,"");
messageText.obj = tnSendMessage;
messageText.replyTo = chatMessageMessenger;
try {
mService.send(messageText);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
- 一个文字消息发送示例
-
依旧是使用TNSendMessage这个类
-
一个接收示例
- 这个示例可以是上个示例的作者接收到的传递给自己的消息
- 相比于发送出去的消息的区别
- 已经是历史消息了
- 服务端填写了sendTime
- 另一个接收示例
- 这是上个示例中接受方收到的消息
- 如果是在登陆状态下接收到对方的消息,那么就不是历史消息
- 如果在下线的时候对方发了消息,上线后显示接收,那么就是历史消息
- 图片/视频/文件/语音消息本质上都是数据包结构告诉对方有这样一个数据,然后再分片将文件传输给对方
public class TNFileSendData implements Serializable {
private String message_uuid;
// private String file_uuid;
private String sender_name; // 哪个用户传输的
private String sender_machine; // 哪个用户传输的
// 用户 1, 群 2
private int receive_type;
private String receive_name; // 哪个用户传输的
private String receive_machine; // 哪个用户传输的
private String receive_group;
private long record_id; // 传输ID
private boolean isHistory;
// private byte[] token; // 数据合法性验证
private String file;// 文件
// 文字0,图片1,文件2,视频3,语音4
private int file_type;
private String file_hash;// md5
private String file_name;// 文件名
//文件后缀名
private String ext ;
private long starPos;// 开始位置
private byte[] bytes;// 文件字节数组
private long endPos;// 结尾位置
private long start;
private long size;
...
}
- 代码分析
- 注意这里用的是ChatMessageActivity/ChatMessageSecretActivity里面的代码而不是ChatMessageFragment里面的!
- ChatMessageFragment也有sendImageMessage但是没有被调用
private void sendImageMessage(final LocalMedia media) throws Exception {
boolean isOneShotPhoto = false;
String mediaPath = media.getPath();
//这里需要更严格的判断,比方说 /PictureSelector/CameraImage/PictureSelector_20210224_181345.JPEG
//将时间算进去,因为即拍即传的话,时间不会差很久
Log.d("mytest11", "sendImageMessage: PictureSelector0: "+mediaPath.toLowerCase());
// 下面是根据图片路径判断是否是即拍即传
if (mediaPath.toLowerCase().contains("pictureselector")){
isOneShotPhoto = true;
Log.d("mytest11", "sendImageMessage: getCompressPath: "+media.getCompressPath());
}
// 以下到几行代码直到开始发送负责的是UI方面的内容
final ChatMessage mMessgaeImage=getBaseSendMessage(MsgType.IMAGE);
ImageMsgBody mImageMsgBody=new ImageMsgBody();
mImageMsgBody.setThumbUrl(media.getCompressPath());
mImageMsgBody.setThumbPath(media.getPath());
mImageMsgBody.setExt(FileUtils.getFileExtension(media.getPath()));
mMessgaeImage.setMsgBody(gson.toJson(mImageMsgBody));
//开始发送
mAdapter.addData( mMessgaeImage);
Message messageImage = Message.obtain();
messageImage.what = 0x30;
// 构造TNSendMessage
// contact_type:1是用户 ,2 是群
// 群没有实装所以contact_type一直是1
TNSendMessage tnSendMessage = new TNSendMessage(mMessgaeImage.getUuid(),contact_type,1,1,userName,contact_type==1?contact_name:"",contact_type==2?contact_name:"","[图片]","[图片]","");
// 存储压缩后图片路径
tnSendMessage.setThumbUrl(media.getCompressPath());
// 获得图片后缀名
tnSendMessage.setExt(FileUtils.getFileExtension(media.getPath()));
// 下面代码是获取文件的缩略图并进行高斯模糊处理
File image = new File(media.getPath());
if (image.exists()) {
Bitmap bitmap = BitmapFactory.decodeFile(media.getCompressPath());
bitmap.getHeight();
bitmap.getWidth();
Bitmap bitmap1 = BlurBitmapUtil.blurBitmap(this, bitmap, 18f);
String imgname = System.currentTimeMillis() + ".jpg";
if (isOneShotPhoto) {
imgname = mMessgaeImage.getUuid() + ".jpg";
}
// 压缩并模糊处理后的图片存储到urlpath中
String urlpath = getFilesDir().getAbsolutePath() + "/" + imgname;
File f = new File(urlpath);
try {
if (f.exists()) {
f.delete();
}
FileOutputStream out = new FileOutputStream(f);
// PNG会 忽略quality的参数
// 高斯模糊后到图片再进行压缩
bitmap1.compress(Bitmap.CompressFormat.JPEG, 50, out);
// 最后将压缩后到图片存储到urlpath
out.flush();
out.close();
}
catch ( Exception e) {
LogUtil.d("图片缩略图路径获取失败:"+e.toString());
e.printStackTrace();
}
if (new File(urlpath).exists()){
// 将压缩后的图片(缩略图)字节数组存储到thumb中
// 因为压缩过可以直接存储
tnSendMessage.setThumb(FileToArrayOfBytes.readBytesFromFile(urlpath));
tnSendMessage.setThumbUrl(urlpath);
}
}
// 存储图片的原始路径
tnSendMessage.setLocalPath(media.getPath());
// 这里thumbPath和localPath一致,不知道为什么这么设定
tnSendMessage.setThumbPath(media.getPath());
// 生成文件加密后的加密文件路径,app路径+meaage的uuid+.tndata
String uuidPath = getFilesDir().getAbsolutePath() + "/" +mMessgaeImage.getUuid()+".tndata";
String uuidDir = getFilesDir().getAbsolutePath() ;
Log.d("mytest11", "uuidDir: "+uuidDir);
if (isOneShotPhoto){
// 如果是即拍即传的话就是使用加密文件路径,因为即拍即传存储到app内部路径中,外面的文件一会儿后会删除
tnSendMessage.setLocalPath(uuidPath);
tnSendMessage.setThumbPath(uuidPath);
}
// 填入文件的文件名和后缀名
tnSendMessage.setDisplayName(FileUtils.getFileName(media.getPath()));
tnSendMessage.setExt(FileUtils.getFileExtension(media.getPath()));
// 生成一个AES密钥将这个密钥用对方的公钥加密
// 放到tnSendMessage的key属性中
String key1 = generateKey();
byte[] encryptByContactPublicKey = RSAUtils.encryptByPublicKey(key1.getBytes(Charsets.toCharset("UTF-8")), contact_publicKey);
String key = Base64.encodeToString(encryptByContactPublicKey,Base64.DEFAULT);
tnSendMessage.setKey(key);
// 因为即拍即传的图片是经过加密后存储到app内部路径中的,所以如果是即拍即传的图片,那么还需要一个给自己的密钥
// 生成一个AES密钥将该密钥用自己的公钥加密
// 注意这个AES密钥和上面那个给别人的AES密钥是不同的
String key02 = generateKey();
String key2 = "";
if (isOneShotPhoto){
byte[] encryptByContactPublicKey2 = RSAUtils.encryptByPublicKey(key02.getBytes(Charsets.toCharset("UTF-8")), mPublicKey);
key2 = Base64.encodeToString(encryptByContactPublicKey2,Base64.DEFAULT);
// 将这个加密后的AES密钥放到keyToMe中
tnSendMessage.setKeyToMe(key2);
}
Log.d("mytest12", "key2: "+key2);
Log.d("mytest12", "mPublicKey: "+mPublicKey);
Log.d("mytest12", "key02: "+key02);
//是否是阅后即焚
if (isFireOpen){
tnSendMessage.setIsFire(1);
tnSendMessage.setFireTime(fireTime);
}
// 将数据发送到NettyService处理
messageImage.obj = tnSendMessage;
messageImage.replyTo = chatMessageMessenger;
try {
mService.send(messageImage);
}
catch (RemoteException e) {
e.printStackTrace();
}
// 在发送了TNSendMessage后启动一个线程,构造第一个TNFileSendData数据结构
final boolean _isOneShotPhoto = isOneShotPhoto;
final String _uuidDir = uuidDir;
new Thread(new Runnable() {
@Override
public void run() {
try {
// 先等待TNSendMessage发送完毕
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Message messageImage2 = Message.obtain();
messageImage2.what = 0x31;
TNFileSendData tnFileSendData = new TNFileSendData();
// 连接类型,现在群聊功能没有实装,所以都是1
tnFileSendData.setReceive_type(contact_type);
// 接收群名称,再群聊功能没有实装的情况下应该没有用
// 经过检测这行注释掉也能正常传输图片
tnFileSendData.setReceive_group(contact_name);
// 文件路径
tnFileSendData.setFile(media.getPath());
// 文件名
tnFileSendData.setFile_name(FileUtils.getFileName(media.getPath()));
// UUID
tnFileSendData.setMessage_uuid(mMessgaeImage.getUuid());
// 接受者的userName
tnFileSendData.setReceive_name(contact_name);
tnFileSendData.setReceive_machine(null);
// 图片后缀名
tnFileSendData.setExt(FileUtils.getFileExtension(media.getPath()));
// 图片大小
tnFileSendData.setSize(FileUtils.getFileSize2(media.getPath()));
// 发送者的userName
tnFileSendData.setSender_name(userName);
// 文件类型
// 文字0,图片1,文件2,视频3,语音4
tnFileSendData.setFile_type(1);
tnFileSendData.setHistory(false);
// 对文件用之前的密钥进行AES加密
File f = new File(media.getPath());
String imageHash = "";
if (f.exists()) {
// 给文件用给对方的AES密钥进行AES加密,放到缓存文件夹下
File aseF = AESEncrypt.encryptFile(f, getApplication().getCacheDir().getAbsolutePath(),mMessgaeImage.getUuid()+".tndata", key1);
if (_isOneShotPhoto){
// 如果是即拍即传,还要用给自己的AES密钥进行AES加密,之后存储到app内部路径中
// 放到app内部的文件夹
File encryptFile = AESEncrypt.encryptFile(new File(f.getAbsolutePath()), _uuidDir, mMessgaeImage.getUuid() + ".tndata", key02);
Log.d("mytest12", "encryptFile : "+encryptFile.getAbsolutePath());
}
try {
// 存入hash值
// 传说完成后做数据校验
imageHash = MD5Util.getFileMD5(aseF);
tnFileSendData.setFile_hash(imageHash);
} catch (Exception e) {
e.printStackTrace();
tnFileSendData.setFile_hash("");
}
// 将文件的路径替换成加密文件路径
System.out.println(aseF.getAbsolutePath());
tnFileSendData.setFile(aseF.getAbsolutePath());
// 开始位置
tnFileSendData.setStarPos(0);
RandomAccessFile randomAccessFile;
try {
randomAccessFile = new RandomAccessFile(aseF,
"r");
randomAccessFile.seek(0);
int lastLength =524288;
if (randomAccessFile.length()< (long) lastLength){
lastLength = (int) randomAccessFile.length();
}
byte[] bytes = new byte[lastLength];
int byteRead;
if ((byteRead = randomAccessFile.read(bytes)) != -1) {
// 读取文件放到字节数组中并记录结束位置
tnFileSendData.setEndPos(byteRead);
tnFileSendData.setBytes(bytes);
messageImage2.obj = tnFileSendData;
messageImage2.replyTo = chatMessageMessenger;
try {
mService.send(messageImage2);
}
catch (RemoteException e) {
e.printStackTrace();
}
}
randomAccessFile.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (_isOneShotPhoto){
f.delete();
Log.d("mytest16", "Uri.fromFile(new File(media.getPath())).toString(): "+Uri.fromFile(new File(media.getPath())).toString());
try {
getContentResolver().delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStore.Images.Media.DATA + "=?", new String[]{Uri.fromFile(new File(media.getPath())).toString()});
scanFileAsync(getApplication(),media.getPath());
}catch (Exception e){
}
Log.d("mytest11", "_isOneShotPhoto: f.delete()");
}
}
}
}
}).start();
}
- 一个发送图片的tnSendMessage示例
- 属性详解:
- author:发送方的userName
- compress:是否压缩过,false指得是原图,代码中没有填,默认false
- contentType: 文字0,图片1,文件2,视频3,语音4
- displayName: 文件名称,用于文件展示,用文件路径中的文件名替代
- duration:视频信息长度,在图片信息这里没用
- ext:图片后缀名
- key:通过对方的公钥加密后的AES密钥
- keyToMe:用自己的公钥加密的AES密钥,只有即拍即传才有这个属性,因为即拍即传的照片也是通过AES加密存储到APP到内部路径中,后缀名是tnData
- localPath和thumbPath是图片的本地路径,可以理解为文件再发送方的路径
- 如果不是即拍即传,这两个属性是文件的本身的路径
media.getPath()
- 如果是即拍即传,这两个属性是app内部加密存储文件的路径
getFilesDir().getAbsolutePath() + "/" +mMessgaeImage.getUuid()+".tndata"
(也就是放到file文件夹下)
- 如果不是即拍即传,这两个属性是文件的本身的路径
- message和messageToMe这两个属性图片的话统一是
"[图片]"
- nameToMessage:接受者的userName
- thumbUrl是经过压缩和高斯模糊后的图片的路径
- thumb是压缩和高斯模糊后的图片的字节数组,因为压缩过可以直接放进去
- uuid消息唯一标识
- type是群消息还是个人信息,1是个人,2是群聊,群聊功能没有实装,所以一直是1
- 属性详解:
- 发送方tnSendMessage返回示例
- 和发送发发送到服务端示例的区别是sendTime被服务端填写了
- 接收方TNFileSendData接收示例
- file是发送方加密文件的路径,一般是放到cache文件夹中
- 接受方接收TNFileReceiveInfo示例