1. 前言
聊天模块
xmpp IM模块 越来越多的主流的app集成模块
自主研发 | 开源服务 im xmpp openfire |
socket | xmpp |
底层 | 框架 |
2. IM基本概念
QQ 微信 陌陌 IM/SNS
概念
instant messager
通过底层协议实现的消息通道传输消息。由一个账号 发往另外一个账,只要对方在线,马上/实时接收到消息。
底层:TCP/UDP
上层:http smtp ftp
消息通道:连接对象 发送消息 接收消息 I/O
实时: 无延时
2.1. TCP/IP UDP
转输数据的协议
将数据 封装一个信封,添加上ip地址。发送。
IP | 网络上设备的编号 |
PORT | 端口 网络程序 |
TCP | UDP |
1.电话确认 2.送货 | 1.送货 |
效率低 | 效率高 |
可靠性强 | 可靠性差 |
大文件 | 64K |
面向连接: 三次握手 | 非面向连接 |
2.2. 三次握手
2.3. 常见形式
直接通讯 | p2p peer to peer 消息不经过服务器 直接发送 |
在线代理 | 消息经过服务器的转发 到达目标账号 |
离线代理 | 消息经过服务器的转发 对方不在线,暂存消息。在上线时 再转发到达目标账号 |
离线扩展 | 消息经过服务器的转发 对方不在线,暂存消息。以其他形式通知 目标账号 email msn sms |
2.4. IM原理
2.5. 注意点
理解含义 Socket 套接字 ServerSocket | Socket :java 对tcp/ip的实现 客服端 插头 ServerSocket :java 对tcp/ip的实现 服务端 插座 |
要求 | Android 客户端的理解 服务端不管: php c++ |
自主开发的服务端 |
|
3. 核心概念
3.1. 消息内容_IM服务端接口文档
Http接口文档
IM接口文档:一个协议的文本形式 规定 字段+格式.
格式良好
xml | json |
黑马 |
|
<message>黑马</message> | {name:黑马} |
流量消耗大 | 流量消耗小 |
扩展好 pull sax dom4j | 扩展性 |
XStream 快速开发库 java对象与 xml互转 Xstream java对象 -->xml toXml(对象); xml->java对象 fromXml(); | Gson 快速解析包 java对象与 json互转 Gson java对象-->json toJson(); json-->java对象 fromJson(json,类) |
3.2. 消息内容_Xstream自动生成
Junit测试
① Junit库
② 功能清单配置
③ 编写测试用例
<!-- 引用组件 -->
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.itheima.im.socket" >
</instrumentation>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<!-- 使用 -->
<uses-library
android:name="android.test.runner"
android:required="true" />
QQMessage msg = new QQMessage();
msg.type = QQMessageType.MSG_TYPE_BUDDYLIST;
msg.content = "在不在";
// 创建核心类
XStream x = new XStream();
x.alias("QQMessage", QQMessage.class);
// 调用toxml
String xml = x.toXML(msg);
System.out.println(xml);
QQMessage msg2 = (QQMessage) x.fromXML(xml);
System.out.println(msg2.content);
3.3. 项目应用:继承基类
http | im |
post 提交表单对象 Map 集合 | 消息对象 内容+附加字段 含有标签的表单 |
//强大 的对象 与 xml ,json
public class ProtocalObj {
public String toXml() {
// 创建核心类
XStream x = new XStream();
x.alias(getClass().getSimpleName(), getClass());
// 调用toxml
String xml = x.toXML(this);
return xml;
}
public Object fromXml(String xml) {
// 创建核心类
XStream x = new XStream();
x.alias(getClass().getSimpleName(), getClass());
// 调用toxml
return x.fromXML(xml);
}
public String toJson() {
Gson gson=new Gson();
return gson.toJson(this);
}
public Object fromJson(String json) {
Gson gson=new Gson();
return gson.fromJson(json, getClass());
}
}
3.4. 消息通道-连接对象
传输消息通道: 发消息 接收消息
Socket客户端程序
① 联网权限
② Socket连接
I | readUTF | InputStream |--DataInputStream |
O | writeUTF | OutputStream |-DataOutputStream |
private DataInputStream reader=null;
private DataOutputStream writer=null;
public void connect(View view) {
new Thread(){
public void run() {
String ip = "192.168.15.97";
int port = 5224;
try {
//4.0
Socket socket = new Socket(ip, port);
//获取输入流
reader=new DataInputStream(socket.getInputStream());
writer=new DataOutputStream(socket.getOutputStream());
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
};
}.start();
}
3.5. 消息发送
public void sendmessage(View view)
{
//101 test 101#test
new Thread(){
public void run() {
QQMessage msg=new QQMessage();
msg.type=QQMessageType.MSG_TYPE_LOGIN;
msg.content="101#test";
try {
writer.writeUTF(msg.toXml());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
};
}.start();
}
3.6. 消息接收
创建一个等待线程 。等待消息的到来
核心类
1.打开通道
2.关闭通道
3.发送消息
4.接收消息
3.7. 监听器
Button |
|
OnClickListener onClick 接口 |
|
setOnClickListener(监听器) | add N set 1 |
事件:点击 | 事件:消息接收事件 |
监听器好处:事件处理程序写在核心类的外边 灵活低耦合的目的。
自定义控件 事件监听器
① 创建一个接口
② 响应方法 接收参数
③ 添加 /移除方法
④ 事件产生的地方执行
public class QQConnnection extends Thread {
private String ip;
private int port;
// Alt+Shift+S
public QQConnnection(String ip, int port) {
super();
this.ip = ip;
this.port = port;
}
// 1.打开通道
// 2.关闭通道
// 3.发送消息
// 4.接收消息
private Socket client = null;
private DataInputStream reader;
private DataOutputStream writer;
private boolean isRunning = true;
// 打开连接
public void connect() throws Exception {
client = new Socket(ip, port);
reader = new DataInputStream(client.getInputStream());
writer = new DataOutputStream(client.getOutputStream());
while (isRunning) {
String xml = reader.readUTF();
QQMessage msg = new QQMessage();
msg = (QQMessage) msg.fromXml(xml);
if (msg != null) {
// 登录代码处理
// 解析好友列表
// 下线成功
for (OnReceiveMsgListener listener : listeners) {
listener.onReceive(msg);
}
}
}
}
// 添加多个监听
private List<OnReceiveMsgListener> listeners = new ArrayList<OnReceiveMsgListener>();
public void addOnReceiveMsgListener(OnReceiveMsgListener listener) {
listeners.add(listener);
}
public void removeOnReceiveMsgListener(OnReceiveMsgListener listener) {
listeners.remove(listener);
}
// 断开
public void disconnect() {
isRunning = false;
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (client != null) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 发送消息
public void sendMessage(QQMessage msg) throws Exception {
String xml = msg.toXml();
if (writer != null) {
writer.writeUTF(xml);
}
}
}
Command设计模式
http | im |
String json=get();/post(); | void sendmessage(); |
只能接收一次 | 通过监听器接收参数 N次 addListener removeListener |
4. 项目功能-Socket
4.1. 模块:启动页面
拆分法 :分离出所有控件元素,帮我们分析界面
ThreadUtils.runInThread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
//1.广告
//2.初始工作 检测新版本
startActivity(new Intent(getBaseContext(),LoginActivity.class));
finish();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
4.2. 模块:登录
提交账号密码给服务器端验证
① 布局 layout TableLayout
② 编写事件的逻辑
TableLayout | 表格布局 子标签 代表一行 |
TableRow | 代表表格的行 一行可以有多列 |
findViewById | @InjectView set |
findViewById setOnClickListener | @OnClick |
ButterKnife 黄油刀 注解开发库 |
|
ButterKnife.inject(this) | apt annotation process tool |
public class LoginActivity extends Activity {
@InjectView(R.id.username)
EditText username;
@InjectView(R.id.pwd)
EditText pwd;
@InjectView(R.id.login)
Button login;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
// 初始化注解生效
ButterKnife.inject(this);
System.out.println(login);
}
@OnClick(R.id.login)
public void login(View view) {
Toast.makeText(this, "登录", 0).show();
}
}
2
登录 url http://ip:port/pro/login | ip port |
post form | sendMessnage QQMessage |
username password | username#test content |
String json= HttpUtil.post(); | void sendMessage(); 使用监听器接收 |
登录成功提示 |
|
| 保持 连接 长连接 |
① 创建消息通道
② 发送消息
③ 创建监听接收消息
④ 提示:成功/失败
ThreadUtils.runInThread(new Runnable() {
@Override
public void run() {
if (conn == null) {
try {
// 192.168.15.97 5225
conn = new QQConnnection("192.168.15.97", 5225);
conn.addOnReceiveMsgListener(listener);
conn.connect();// start等待线程
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
private OnReceiveMsgListener listener = new OnReceiveMsgListener() {
@Override
public void onReceive(final QQMessage msg) {
// 子线程
ThreadUtils.runUIThread(new Runnable() {
@Override
public void run() {
if (msg.type.equals(QQMessageType.MSG_TYPE_BUDDYLIST)) {
System.out.println(msg.content);
String json = msg.content;
// 保存消息通道
MyApp.conn = conn;
// 保存账号
MyApp.username = usernameString;
Toast.makeText(getBaseContext(), "登录成功!", 0).show();
startActivity(new Intent(getBaseContext(), MainActivity.class));
finish();
} else {
Toast.makeText(getBaseContext(), "账号或者密码出错!", 0).show();
}
}
});
}
};
private QQConnnection conn = null;
4.3. 模块.好友
//BaseAdapter x4
// |--ArrayAdapter x1
public class ContactAdapter extends ArrayAdapter<QQBuddy> {
public ContactAdapter(Context context, List<QQBuddy> objects) {
super(context, 0, objects);
}
// 1. static 2.ButterKnife.inject(this, view);
static class ViewHolder {
@InjectView(R.id.head)
ImageView head;
@InjectView(R.id.title)
TextView title;
@InjectView(R.id.desc)
TextView desc;
public ViewHolder(View view) {
ButterKnife.inject(this, view);
}
}
// 返回行视图,显示指定下标的数据
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 数据
QQBuddy info = getItem(position);
ViewHolder holder = null;
// 视图 1.inflate 内存
if (convertView == null) {
// 打气 inflate 将xml转换成对象
// convertView=View.inflate(上下文, 视图文件, null);
convertView = View.inflate(getContext(), R.layout.item_contact, null);
holder = new ViewHolder(convertView);
// 2.findViewById
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.title.setText(info.nick);
if(MyApp.username.equals(info.account+""))
{
holder.title.setText("[我自己]");
}
holder.desc.setText(info.account+"@qq.com");
return convertView;
}
}
4.4. 模块.好友--上线更新
上线/下线
① 刷新页面
② 接收到消息的时候刷新 监听器:拦截处理
101-->199 test
MyApp.conn.addOnReceiveMsgListener(listener);
// 获取意图
Intent intent = getIntent();
String json = intent.getStringExtra("json");
System.out.println(json);
QQBuddyList temp = (QQBuddyList) new QQBuddyList().fromJson(json);
setAdapterOrNitifyDataSetChange(temp);
}
@Override
protected void onDestroy() {
super.onDestroy();
MyApp.conn.removeOnReceiveMsgListener(listener);
}
private OnReceiveMsgListener listener = new OnReceiveMsgListener() {
@Override
public void onReceive(final QQMessage msg) {// 子线程
ThreadUtils.runUIThread(new Runnable() {
@Override
public void run() {
// 控件 null
if (QQMessageType.MSG_TYPE_BUDDYLIST.equals(msg.type)) {
String json = msg.content;
QQBuddyList temp = (QQBuddyList) new QQBuddyList().fromJson(json);
setAdapterOrNitifyDataSetChange(temp);
}
}
});
}
};
// new Thread().start();
public void setAdapterOrNitifyDataSetChange(QQBuddyList temp) {
list.buddyList.clear();
list.buddyList.addAll(temp.buddyList);
if (adapter == null) {
if (list.buddyList.size() < 1) {
return;
}
// 06-12 03:05:15.215: I/System.out(2120):
// {"buddyList":[{"account":101,"nick":"QQ 1","avatar":0}]}
adapter = new ContactAdapter(this, list.buddyList);
contactlistview.setAdapter(adapter);
} else {
adapter.notifyDataSetChanged();
}
}
private QQBuddyList list = new QQBuddyList();
4.5. 模块:聊天
① 布局Layout ListView 行视图多种类型
② 创建Activity
③ 获取 nick account
④ 静态UI 假数据-->循环显示数据
⑤ 假数据换真数据
public class ChatAdapter extends ArrayAdapter<QQMessage> {
public ChatAdapter(Context context, List<QQMessage> objects) {
super(context, 0, objects);
}
// 行视图的种类数
@Override
public int getViewTypeCount() {
return 2; // 0 ,1
}
// 0发送 1接收
// 根据数据返回指定下标的视图类型
@Override
public int getItemViewType(int position) {
long me = Long.parseLong(MyApp.username);
QQMessage msg = getItem(position);
if (me == msg.from) {
return 0;// 发送
}
return 1;// 接收
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
int type = getItemViewType(position);
if (type == 0)// 发送
{
return View.inflate(getContext(), R.layout.item_chat_send, null);
} else // 接收
{
return View.inflate(getContext(), R.layout.item_chat_receive, null);
}
}
}
优化
@Override
public View getView(int position, View convertView, ViewGroup parent) {
int type = getItemViewType(position);
if (type == 0)// 发送
{
ViewHolder holder = null;
if (convertView == null) {
convertView = View.inflate(getContext(), R.layout.item_chat_send, null);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
QQMessage msg = getItem(position);
holder.time.setText(msg.sendTime);
holder.content.setText(msg.content);
return convertView;
} else // 接收
{
ViewHolder holder = null;
if (convertView == null) {
convertView = View.inflate(getContext(), R.layout.item_chat_receive, null);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
QQMessage msg = getItem(position);
holder.time.setText(msg.sendTime);
holder.content.setText(msg.content);
return convertView;
}
}
发送消息
@OnClick(R.id.send)
public void send(View view) {
String inputMessage = input.getText().toString().trim();
input.setText("");
// 创建消息
final QQMessage msg = new QQMessage();
msg.type = QQMessageType.MSG_TYPE_CHAT_P2P;
msg.content = inputMessage;
msg.from = Long.parseLong(MyApp.username);
msg.to = toChatAccount;
messages.add(msg);
setAdapterOrNotify();
ThreadUtils.runInThread(new Runnable() {
@Override
public void run() {
try {
MyApp.conn.sendMessage(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
消息接收
private OnReceiveMsgListener listener = new OnReceiveMsgListener() {
public void onReceive(final QQMessage msg) {//子线程
ThreadUtils.runUIThread(new Runnable() {
@Override
public void run() {
if (msg.type.equals(QQMessageType.MSG_TYPE_CHAT_P2P)) {
messages.add(msg);
setAdapterOrNotify();
Toast.makeText(getBaseContext(), "好友消息:"+msg.content, 0).show();
}
}
});
}
};
protected void onDestroy() {
super.onDestroy();
MyApp.conn.removeOnReceiveMsgListener(listener);
};
private List<QQMessage> messages = new ArrayList<QQMessage>();
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat);
ButterKnife.inject(this);
MyApp.conn.addOnReceiveMsgListener(listener);
4.6. 后台Service运行消息接收
QQ软件 所有的界面退出 还是通接收到消息
Activity/Fragment 窗口/ 片段 | Service |
返回关闭当前的页面,聊天核心程序。杀死 | 没有界面,隐蔽,长期运行的程序 |
//四大组件 1.继承 2.重写 3.配置 4.打开
public class ChatService extends Service {
@Override
public void onCreate() {
super.onCreate();// Notification QQ
Toast.makeText(this, "聊天..后台服务.", 0).show();
MyApp.conn.addOnReceiveMsgListener(listener);
}
private OnReceiveMsgListener listener=new OnReceiveMsgListener() {
@Override
public void onReceive(final QQMessage msg) {//子线程
ThreadUtils.runUIThread(new Runnable() {
@Override
public void run() {
if(msg.type.equals(QQMessageType.MSG_TYPE_CHAT_P2P))
{
Toast.makeText(getBaseContext(), "好友消息."+msg.content, 0).show();
}
}
});
}
};
@Override
public void onDestroy() {
super.onDestroy();
MyApp.conn.removeOnReceiveMsgListener(listener);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
Provider复习--保存联系人
练习
Chat功能 |
|
Provider 练习 |
|