首先了解一下即时通信的概念。通过消息通道 传输消息对象,一个账号发往另外一账号,只要账号在线,可以即时获取到消息,这就是最简单的即使通讯。消息通道可由TCP/IP UDP实现。通俗讲就是把一个人要发送给另外一个人的消息对象(文字,音视频,文件)通过消息通道(C/S实时通信)进行传输的服务。即时通讯应该包括四种形式,在线直传、在线代理、离线代理、离线扩展。在线直传指不经过服务器,直接实现点对点传输。在线代理指消息经过服务器,在服务器实现中转,最后到达目标账号。离线代理指消息经过服务器中转到达目标账号,对方不在线时消息暂存服务器的数据库,在其上线再传发。离线扩展指将暂存消息以其它形式,例如邮件、短信等转发给目标账号。

此外,我们还需要认识一下计算机网络相关的概念。经典的计算机网络四层模型中,TCP和UDP是传输层协议,包含着消息通信内容。ip为网络层协议,是一种网络地址。TCP/IP,即传输控制协议/网间协议,定义了主机如何连入因特网及数据如何在它们之间传输的标准。Socket,又称“套接字”, 在应用层和传输层之间的一个抽象层,用于描述 IP 地址和端口,是一个通信连的句柄,应用程序通常通过“套接字”向网络发送请求或者应答网络请求,它就是网络通信过程中端点的抽象表示。它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。XMPP(可扩展消息处理现场协议)是基于可扩展标记语言(XML)的协议,应用于即时通讯场景的应用层协议,底层通过Socket实现。它用于即时消息(IM)以及在线现场探测。它在促进服务器之间的准即时操作。这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息, 即使其操作系统和浏览器不同。这样实现即时通讯就有两种方案,一是从套接字入手,直接利用socket提供的接口进行数据的传送。二是借助开源工具(服务器openfire),用XMPPConnection创建连接。

XMPP是实现即时通讯使用较为普遍的做法。XMPP中,各项工作都是通过在一个 XMPP 流上发送和接收 XMPP 节来完成的。核心 XMPP 工具集由三种基本节组成,这三种节分别为<presence>、出席<message>、<iq>。XMPP 流由两份 XML 文档组成,通信的每个方向均有一份文档。这份文档有一个根元素<stream:stream>,这个根元素的子元素由可路由的节以及与流相关的顶级子元素构成。xmpp协议同样包括客户端和服务器。客户端基于 Android 平台进行开发。负责初始化通信过程,进行即时通信时,由客户端负责向服务器发起创建连接请求。系统通过 GPRS 无线网络与Internet 网络建立连接,通过服务器实现与 Android 客户端的即时通信脚。服务器端则采用 Openfire 作为服务器。 允许多个客户端同时登录并且并发的连接到一个服务器上。服务器对每个客户端的连接进行认证,对认证通过的客户端创建会话,客户端与服务器端之间的通信就在该会话的上下文中进行。使用了 asmark 开源框架实现的即时通讯功能.该框架基于开源的 XMPP 即时通信协议,采用 C/S 体系结构,通过 GPRS 无线网络用TCP 协议连接到服务器,以架设开源的 Openfn'e 服务器作为即时通讯平台。xmpp消息通道的创建:

先配置通道信息进行连接

ConnectionConfiguration configuration = new ConnectionConfiguration(HOST, PORT),

设置Debug信息和安全模式

configuration.setDebuggerEnabled(true);

configuration.setSecurityMode(SecurityMode.disabled),

最后才是建立连接

conn.connect();

在ContentObserver的实现类中观察消息变化。XMPPConnection.getRoster()获取联系人列表对象。用xmpp协议编写通讯协议的大致思路可以如下。进入登陆界面,通过xmppconnection的login方法实现登陆,登陆成功进入主界面。主界面包含两个Fragment,分别用来显示联系人和聊天记录。创建联系人和短信的数据观察者,在联系人、短信服务中分别设定监听RosterListener()、ChatManagerListener(),接受联系人和短信信息,同时将相关信息添加到内容提供者中。在内容提供者中设定一个内容观察者,当数据发生变化时通知界面更新。

本文的重点是利用Socket的接口实现即时通讯,因为绝大多数即时通讯的底层都是通过Socket实现的。其基本的业务逻辑可描述如下。用户进入登陆界面后,提交账号密码 经服务端确定,返回相关参数用于确定连接成功。进入聊天界面或好友界面。点击联系人或聊天记录的条目,进入聊天界面。当移动端再次向服务器发送消息时,由服务器转发消息内容给目标账号。同时更新界面显示。这样就完成即时通讯的基本功能。当然,也可以添加一个后台服务,当用户推出程序时,在后台接受消息。不难看出,对于即时通讯来讲,有三个关注点:消息通道、消息内容、消息对象。因此,主要逻辑也是围绕这三个点展开。消息通道实现传输消息对象的发送和接收。为Socket(String host, int port)传入服务其地址和端口号,即可创建连接。消息内容的格式应该与服务器保持一致。接受数据时,获取输入流并用DataInputStream包装,通过输入流读取server发来的数据。发送数据时,获取输出流并用DataOutputStream包装,通过输出流往server发送数据。消息内容中应该包括发送者、接受者信息、数据类型等。消息对象就是消息的发送者和消息的接受者。接下来在代码中进行详细的讲解。

创建一个消息的基类,实现xml文件和字符串的转换,用到Xsream第三方jar包。这样当创建消息类时,继承该方法,就可以直接在类中实现数据的转换。

  1. /**
  2. * Created by huang on 2016/12/3.
  3. */
  4. public class ProtacolObjc implements Serializable {
  5. public String toXml() {
  6. XStream stream = new XStream();
  7. //将根节点转换为类名
  8. stream.alias(this.getClass().getSimpleName(), this.getClass());
  9. return stream.toXML(this);
  10. }
  11.  
  12. public Object fromXml(String xml) {
  13. XStream x = new XStream();
  14. x.alias(this.getClass().getSimpleName(), this.getClass());
  15. return x.fromXML(xml);
  16. }
  17.  
  18. //创建Gson数据和字符串之间转换的方法,适应多种数据
  19. public String toGson() {
  20. Gson gson = new Gson();
  21. return toGson();
  22. }
  23.  
  24. public Object fromGson(String result) {
  25. Gson gson = new Gson();
  26. return gson.fromJson(result, this.getClass());
  27. }
  28. }

创建线程工具,指定方法运行在子线程和主线程中。由于网络操作需要在子线程中,界面更新需要在主线程中,创建线程工具可以方便选择线程。

  1. import android.os.Handler;
  2. /**
  3. * Created by huang on 2016/12/5.
  4. */
  5. public class ThreadUtils {
  6. private static Handler handler = new Handler();
  7. public static void runUIThread(Runnable r){
  8. handler.post(r);
  9. }
  10. public static void runINThread(Runnable r){
  11. new Thread(r).start();
  12. }
  13. }

创建消息的工具类,包括消息内容、消息类型、消息本省等。由于服务器返回的内容中包含消息的包名信息所以消息本身的包名应该于服务其保持一直。

  1. /**
  2. * Created by huang on 2016/12/3.
  3. * 消息内容
  4. */
  5. public class QQMessage extends ProtacolObjc {
  6. public String type = QQmessageType.MSG_TYPE_CHAT_P2P;// 类型的数据 chat login
  7. public long from = 0;// 发送者 account
  8. public String fromNick = "";// 昵称
  9. public int fromAvatar = 1;// 头像
  10. public long to = 0; // 接收者 account
  11. public String content = ""; // 消息的内容 约不?
  12. public String sendTime = getTime(); // 发送时间
  13.  
  14. public String getTime() {
  15. Date date = new Date(System.currentTimeMillis());
  16. java.text.SimpleDateFormat format = new java.text.SimpleDateFormat("mm-DD HH:mm:ss");
  17. return format.format(date);
  18. }
  19.  
  20. public String getTime(Long time) {
  21. Date date = new Date(time);
  22. java.text.SimpleDateFormat format = new java.text.SimpleDateFormat("mm-DD HH:mm:ss");
  23. return format.format(date);
  24. }
  25. }
  26.  
  27. /**
  28. * Created by huang on 2016/12/3.
  29. * 消息类型
  30. */
  31. public class QQmessageType {
  32. public static final String MSG_TYPE_REGISTER = "register";// 注册
  33. public static final String MSG_TYPE_LOGIN = "login";// 登录
  34. public static final String MSG_TYPE_LOGIN_OUT = "loginout";// 登出
  35. public static final String MSG_TYPE_CHAT_P2P = "chatp2p";// 聊天
  36. public static final String MSG_TYPE_CHAT_ROOM = "chatroom";// 群聊
  37. public static final String MSG_TYPE_OFFLINE = "offline";// 下线
  38. public static final String MSG_TYPE_SUCCESS = "success";//成功
  39. public static final String MSG_TYPE_BUDDY_LIST = "buddylist";// 好友
  40. public static final String MSG_TYPE_FAILURE = "failure";// 失败
  41. }
  42.  
  43. import com.example.huang.imsocket.bean.ProtacolObjc;
  44. /*
  45. *消息本身 包括 账号、头像和昵称
  46. *
  47. */
  48. public class QQBuddy extends ProtacolObjc {
  49. public long account;
  50. public String nick;
  51. public int avatar;
  52. }
  53.  
  54. /**
  55. * Created by huang on 2016/12/3.
  56. */
  57.  
  58. public class QQBuddyList extends ProtacolObjc {
  59. public ArrayList<QQBuddy> buddyList = new ArrayList<>();
  60. }

关于socket的创建连接和发送消息、接受消息。

  1. import android.util.Log;
  2. import com.example.huang.imsocket.bean.QQMessage;
  3. import java.io.DataInputStream;
  4. import java.io.DataOutputStream;
  5. import java.io.IOException;
  6. import java.net.Socket;
  7. import java.util.ArrayList;
  8. import java.util.List;
  9.  
  10. /**
  11. * Created by huang on 2016/12/3.
  12. * 连接 服务器
  13. */
  14. public class QQConnection extends Thread {
  15. private static final String TAG = "QQConnection";
  16. private Socket client;
  17. private DataOutputStream write;
  18. private DataInputStream read;
  19. public static final String HOST = "192.168.23.48";
  20. public static final int POST = 5225;
  21.  
  22. private boolean flag = true;
  23.  
  24. private List<OnQQmwssagereceiveLisener> mOnQQmwssagereceiveLisener = new ArrayList<>();
  25.  
  26. public void addOnQQmwssagereceiveLisener(OnQQmwssagereceiveLisener lisener) {
  27. mOnQQmwssagereceiveLisener.add(lisener);
  28. }
  29.  
  30. public void removeOnQQmwssagereceiveLisener(OnQQmwssagereceiveLisener lisener) {
  31. mOnQQmwssagereceiveLisener.remove(lisener);
  32. }
  33.  
  34. public interface OnQQmwssagereceiveLisener {
  35. public void onReiceive(QQMessage qq);
  36. }
  37.  
  38. @Override
  39. public void run() {
  40. super.run();
  41. while (flag) {
  42. try {
  43. String utf = read.readUTF();
  44. QQMessage message = new QQMessage();
  45. QQMessage msg = (QQMessage) message.fromXml(utf);
  46. if (msg != null) {
  47. for (OnQQmwssagereceiveLisener lisner : mOnQQmwssagereceiveLisener)
  48. lisner.onReiceive(msg);
  49. }
  50. } catch (IOException e) {
  51. e.printStackTrace();
  52. }
  53. }
  54. }
  55.  
  56. public void connect() {
  57. try {
  58. if (client == null) {
  59. client = new Socket(HOST, POST);
  60. write = new DataOutputStream(client.getOutputStream());
  61. read = new DataInputStream(client.getInputStream());
  62. flag = true;
  63. this.start();
  64. Log.e(TAG, "connect: "+(write==null)+"---"+ (read == null));
  65. }
  66. } catch (Exception e) {
  67. e.printStackTrace();
  68. }
  69. }
  70.  
  71. public void disconnect() {
  72. if (client != null) {
  73. flag = false;
  74. this.stop();
  75. try {
  76. read.close();
  77. } catch (IOException e) {
  78. e.printStackTrace();
  79. }
  80.  
  81. try {
  82. write.close();
  83. } catch (IOException e) {
  84. e.printStackTrace();
  85. }
  86.  
  87. try {
  88. client.close();
  89. } catch (IOException e) {
  90. e.printStackTrace();
  91. }
  92. }
  93. }
  94.  
  95. public void send(String xml) throws IOException {
  96. write.writeUTF(xml);
  97. write.flush();
  98. }
  99.  
  100. public void send(QQMessage qq) throws IOException {
  101. write.writeUTF(qq.toXml());
  102. write.flush();
  103. }
  104. }

闪屏界面的布局

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. android:id="@+id/activity_splash"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. android:background="@mipmap/splash_bg">
  8.  
  9. <ImageView
  10. android:layout_width="wrap_content"
  11. android:layout_height="wrap_content"
  12. android:layout_centerInParent="true"
  13. android:src="@mipmap/conversation_bg_logo" />
  14. </RelativeLayout>

闪屏界面,保持4秒钟进入登陆界面。一般来见,闪屏界面可以加载数据、获取版本号、更新版本等操作。这里没有做的那么复杂。

  1. import com.example.huang.imsocket.R;
  2.  
  3. public class SplashActivity extends AppCompatActivity {
  4.  
  5. @Override
  6. protected void onCreate(Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. getSupportActionBar().hide(); //隐藏标栏
  9. getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); //全屏显示
  10. setContentView(R.layout.activity_splash);
  11.  
  12. new Handler().postDelayed(new Runnable() {
  13. @Override
  14. public void run() {
  15. startActivity(new Intent(SplashActivity.this, LoginActivity.class));
  16. finish();
  17. }
  18. }, 4000);
  19. }
  20. }

登陆界面的布局

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:background="#aabbdd"
  6. android:gravity="center"
  7. android:orientation="vertical">
  8.  
  9. <TableLayout
  10. android:layout_width="match_parent"
  11. android:layout_height="wrap_content">
  12.  
  13. <ImageView
  14. android:layout_width="wrap_content"
  15. android:layout_height="wrap_content"
  16. android:src="@mipmap/conversation_bg_logo" />
  17.  
  18. <TableRow
  19. android:layout_width="match_parent"
  20. android:layout_height="wrap_content"
  21. android:layout_marginLeft="20dp"
  22. android:layout_marginRight="20dp"
  23. android:layout_marginTop="8dp"
  24. android:gravity="center_horizontal">
  25.  
  26. <TextView
  27. android:layout_width="0dp"
  28. android:layout_height="wrap_content"
  29. android:layout_weight="1"
  30. android:gravity="center"
  31. android:text="账号:"
  32. android:textColor="#000" />
  33.  
  34. <EditText
  35. android:id="@+id/et_accoun"
  36. android:layout_width="0dp"
  37. android:layout_height="wrap_content"
  38. android:layout_weight="3"
  39. android:gravity="center"
  40. android:hint="输入账号" />
  41. </TableRow>
  42.  
  43. <TableRow
  44. android:layout_width="match_parent"
  45. android:layout_height="wrap_content"
  46. android:layout_marginLeft="20dp"
  47. android:layout_marginRight="20dp"
  48. android:layout_marginTop="4dp"
  49. android:gravity="center_horizontal">
  50.  
  51. <TextView
  52. android:layout_width="0dp"
  53. android:layout_height="wrap_content"
  54. android:layout_weight="1"
  55. android:gravity="center"
  56. android:text="密码:"
  57. android:textColor="#000" />
  58.  
  59. <EditText
  60. android:id="@+id/et_pwd"
  61. android:layout_width="0dp"
  62. android:layout_height="wrap_content"
  63. android:layout_weight="3"
  64. android:gravity="center"
  65. android:hint="输入密码" />
  66. </TableRow>
  67.  
  68. <Button
  69. android:id="@+id/btn_login"
  70. android:layout_width="match_parent"
  71. android:layout_height="wrap_content"
  72. android:layout_marginLeft="80dp"
  73. android:layout_marginRight="80dp"
  74. android:layout_marginTop="8dp"
  75. android:onClick="sendmessage"
  76. android:text="登录" />
  77.  
  78. </TableLayout>
  79. </LinearLayout>

登陆界面,创建和服务器的连接,向服务器发送登陆信息,接受服务器返回的信息。

  1. import android.app.Activity;
  2. import android.content.Intent;
  3. import android.os.Bundle;
  4. import android.util.Log;
  5. import android.view.View;
  6. import android.widget.EditText;
  7. import android.widget.Toast;
  8.  
  9. import com.example.huang.imsocket.R;
  10. import com.example.huang.imsocket.bean.Myapp;
  11. import com.example.huang.imsocket.bean.QQBuddyList;
  12. import com.example.huang.imsocket.bean.QQMessage;
  13. import com.example.huang.imsocket.bean.QQmessageType;
  14. import com.example.huang.imsocket.core.QQConnection;
  15. import com.example.huang.imsocket.service.IMService;
  16. import com.example.huang.imsocket.util.ThreadUtils;
  17.  
  18. import java.io.IOException;
  19.  
  20. /**
  21. * Created by huang on 2016/12/3.
  22. */
  23. public class LoginActivity extends Activity {
  24. private static final String TAG = "LoginActivity";
  25.  
  26. private EditText et_accoun;
  27. private EditText et_pwd;
  28.  
  29. private String accoun;
  30. private QQConnection conn;
  31. private QQConnection.OnQQmwssagereceiveLisener lisener = new QQConnection.OnQQmwssagereceiveLisener() {
  32. @Override
  33. public void onReiceive(final QQMessage qq) {
  34.  
  35. final QQBuddyList list = new QQBuddyList();
  36. final QQBuddyList list2 = (QQBuddyList) list.fromXml(qq.content);
  37. if (QQmessageType.MSG_TYPE_BUDDY_LIST.equals(qq.type)) {
  38. ThreadUtils.runUIThread(new Runnable() {
  39. @Override
  40. public void run() {
  41. Toast.makeText(getBaseContext(), "成功", Toast.LENGTH_SHORT).show();
  42.  
  43. Myapp.me = conn;
  44. Myapp.username = accoun;
  45. Myapp.account = accoun + "@qq.com";
  46.  
  47. Intent intent = new Intent(LoginActivity.this, contactActivity.class);
  48. intent.putExtra("list", list2);
  49. startActivity(intent);
  50.  
  51. Intent data = new Intent(LoginActivity.this, IMService.class);
  52. startService(data);
  53. finish();
  54. }
  55. });
  56. } else {
  57. ThreadUtils.runUIThread(new Runnable() {
  58. @Override
  59. public void run() {
  60. Toast.makeText(getBaseContext(), "登陆失败", Toast.LENGTH_SHORT).show();
  61. }
  62. });
  63. }
  64. }
  65. };
  66.  
  67. @Override
  68. protected void onCreate(Bundle savedInstanceState) {
  69. super.onCreate(savedInstanceState);
  70. setContentView(R.layout.activity_login);
  71. et_accoun = (EditText) findViewById(R.id.et_accoun);
  72. et_pwd = (EditText) findViewById(R.id.et_pwd);
  73. ThreadUtils.runINThread(new Runnable() {
  74. @Override
  75. public void run() {
  76. try {
  77. conn = new QQConnection();
  78. conn.addOnQQmwssagereceiveLisener(lisener);
  79. conn.connect();
  80. } catch (Exception e) {
  81. e.printStackTrace();
  82. }
  83. }
  84. });
  85. }
  86.  
  87. public void sendmessage(View view) {
  88. accoun = et_accoun.getText().toString().trim();
  89. final String password = et_pwd.getText().toString().trim();
  90. Log.i(TAG, "sendmessage: " + accoun + "#" + password);
  91. ThreadUtils.runINThread(new Runnable() {
  92. @Override
  93. public void run() {
  94. QQMessage message = new QQMessage();
  95. message.type = QQmessageType.MSG_TYPE_LOGIN;
  96. message.content = accoun + "#" + password;
  97. String xml = message.toXml();
  98. if (conn != null) {
  99. try {
  100. conn.send(xml);
  101. } catch (IOException e) {
  102. e.printStackTrace();
  103. }
  104. }
  105. }
  106. });
  107. }
  108.  
  109. @Override
  110. protected void onDestroy() {
  111. super.onDestroy();
  112. conn.removeOnQQmwssagereceiveLisener(lisener);
  113. }
  114. }

好友列表界面

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:background="#aabbcc"
  6. android:orientation="vertical">
  7.  
  8. <TextView
  9. android:id="@+id/tv_title"
  10. android:layout_width="match_parent"
  11. android:layout_height="50dp"
  12. android:gravity="center"
  13. android:text="联系人列表"
  14. android:textColor="#6d00"
  15. android:textSize="23dp" />
  16.  
  17. <ListView
  18. android:id="@+id/lv_contact"
  19. android:layout_width="match_parent"
  20. android:layout_height="match_parent"></ListView>
  21. </LinearLayout>

好友列表及时收到从哪个服务其发挥的好友更新信息,点击条目跳到聊天界面。

  1. import android.app.Activity;
  2. import android.content.Intent;
  3. import android.graphics.Color;
  4. import android.os.Bundle;
  5. import android.view.View;
  6. import android.view.ViewGroup;
  7. import android.widget.AdapterView;
  8. import android.widget.ArrayAdapter;
  9. import android.widget.ImageView;
  10. import android.widget.ListView;
  11. import android.widget.TextView;
  12. import android.widget.Toast;
  13.  
  14. import com.example.huang.imsocket.R;
  15. import com.example.huang.imsocket.bean.Myapp;
  16. import com.example.huang.imsocket.bean.QQBuddyList;
  17. import com.example.huang.imsocket.bean.QQMessage;
  18. import com.example.huang.imsocket.bean.QQmessageType;
  19. import com.example.huang.imsocket.core.QQConnection;
  20. import com.example.huang.imsocket.util.ThreadUtils;
  21.  
  22. import java.util.ArrayList;
  23.  
  24. import butterknife.Bind;
  25. import butterknife.ButterKnife;
  26. import cn.itcast.server.bean.QQBuddy;
  27.  
  28. /**
  29. * Created by huang on 2016/12/5.
  30. */
  31.  
  32. public class contactActivity extends Activity {
  33. private static final String TAG = "contactActivity";
  34. @Bind(R.id.tv_title)
  35. TextView tv_title;
  36. @Bind(R.id.lv_contact)
  37. ListView lv_contact;
  38. private QQBuddyList list;
  39. private ArrayList<QQBuddy> BuddyList = new ArrayList<>();
  40. private ArrayAdapter adapter = null;
  41.  
  42. private QQConnection.OnQQmwssagereceiveLisener listener = new QQConnection.OnQQmwssagereceiveLisener() {
  43. @Override
  44. public void onReiceive(QQMessage qq) {
  45. if (QQmessageType.MSG_TYPE_BUDDY_LIST.equals(qq.type)) {
  46. QQBuddyList qqlist = new QQBuddyList();
  47. QQBuddyList qqm = (QQBuddyList) qqlist.fromXml(qq.content);
  48. BuddyList.clear();
  49. BuddyList.addAll(qqm.buddyList);
  50. ThreadUtils.runUIThread(new Runnable() {
  51. @Override
  52. public void run() {
  53. saveAndNotify();
  54. }
  55. });
  56. }
  57. }
  58. };
  59.  
  60. @Override
  61. protected void onCreate(Bundle savedInstanceState) {
  62. super.onCreate(savedInstanceState);
  63. setContentView(R.layout.activity_contact);
  64. ButterKnife.bind(this);
  65. Myapp.me.addOnQQmwssagereceiveLisener(listener);
  66. Intent intent = getIntent();
  67. list = (QQBuddyList) intent.getSerializableExtra("list");
  68. BuddyList.clear();
  69. BuddyList.addAll(list.buddyList);
  70. saveAndNotify();
  71. }
  72.  
  73. @Override
  74. protected void onDestroy() {
  75. super.onDestroy();
  76. Myapp.me.removeOnQQmwssagereceiveLisener(listener);
  77. }
  78.  
  79. private void saveAndNotify() {
  80. if (BuddyList.size() < 1) {
  81. return;
  82. }
  83. if (adapter == null) {
  84. adapter = new ArrayAdapter<QQBuddy>(getBaseContext(), 0, BuddyList) {
  85. @Override
  86. public View getView(int position, View convertView, ViewGroup parent) {
  87. viewHolder holder;
  88. if (convertView == null) {
  89. convertView = View.inflate(getContext(), R.layout.item_contacts, null);
  90. holder = new viewHolder(convertView);
  91. convertView.setTag(holder);
  92. } else {
  93. holder = (viewHolder) convertView.getTag();
  94. }
  95. QQBuddy qqBuddy = BuddyList.get(position);
  96. holder.tv_nick.setText(qqBuddy.nick);
  97. holder.tv_account.setText(qqBuddy.account + "@qq.com");
  98.  
  99. if (Myapp.username.equals(qqBuddy.account + "")) {
  100. holder.tv_nick.setText("[自己]");
  101. holder.tv_nick.setTextColor(Color.GRAY);
  102. } else {
  103. holder.tv_nick.setTextColor(Color.RED);
  104. }
  105. return convertView;
  106. }
  107. };
  108. lv_contact.setAdapter(adapter);
  109.  
  110. lv_contact.setOnItemClickListener(new AdapterView.OnItemClickListener() {
  111. @Override
  112. public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
  113. QQBuddy qqbuddy = BuddyList.get(position);
  114. if (Myapp.username.equals(qqbuddy.account + "")) {
  115. Toast.makeText(getBaseContext(), "不能和自己聊天", Toast.LENGTH_SHORT).show();
  116. } else {
  117. Intent intent = new Intent(contactActivity.this, ChatActivity.class);
  118. intent.putExtra("account", qqbuddy.account + "");
  119. intent.putExtra("nick", qqbuddy.nick + "");
  120. startActivity(intent);
  121. }
  122. }
  123. });
  124. } else {
  125. adapter.notifyDataSetChanged();
  126. }
  127. }
  128.  
  129. static class viewHolder {
  130. @Bind(R.id.iv_contact)
  131. ImageView iv_contact;
  132. @Bind(R.id.tv_nick)
  133. TextView tv_nick;
  134. @Bind(R.id.tv_account)
  135. TextView tv_account;
  136. public viewHolder(View view) {
  137. ButterKnife.bind(this, view);
  138. }
  139. }
  140. }

聊天界面

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="vertical">
  6.  
  7. <TextView
  8. android:id="@+id/tv_name"
  9. android:layout_width="match_parent"
  10. android:layout_height="40dp"
  11. android:background="#aa119988"
  12. android:gravity="center"
  13. android:text="和谁谁聊天中........."
  14. android:textSize="19dp" />
  15.  
  16. <ListView
  17. android:id="@+id/lv_chat"
  18. android:layout_width="match_parent"
  19. android:layout_height="0dp"
  20. android:layout_weight="1" />
  21.  
  22. <LinearLayout
  23. android:layout_width="match_parent"
  24. android:layout_height="wrap_content"
  25. android:orientation="horizontal">
  26.  
  27. <EditText
  28. android:id="@+id/et_sms"
  29. android:layout_width="0dp"
  30. android:layout_height="40dp"
  31. android:layout_weight="1"
  32. android:hint="输入聊天" />
  33.  
  34. <Button
  35. android:id="@+id/btn_send"
  36. android:layout_width="wrap_content"
  37. android:layout_height="wrap_content"
  38. android:text="发送" />
  39. </LinearLayout>
  40. </LinearLayout>

聊天界面中消息接收和消息发送都需要及时更新列表。

  1. import android.app.Activity;
  2. import android.content.Intent;
  3. import android.os.Bundle;
  4. import android.text.TextUtils;
  5. import android.view.View;
  6. import android.view.ViewGroup;
  7. import android.widget.ArrayAdapter;
  8. import android.widget.EditText;
  9. import android.widget.ListView;
  10. import android.widget.TextView;
  11. import android.widget.Toast;
  12.  
  13. import com.example.huang.imsocket.R;
  14. import com.example.huang.imsocket.bean.Myapp;
  15. import com.example.huang.imsocket.bean.QQMessage;
  16. import com.example.huang.imsocket.bean.QQmessageType;
  17. import com.example.huang.imsocket.core.QQConnection;
  18. import com.example.huang.imsocket.util.ThreadUtils;
  19.  
  20. import java.io.IOException;
  21. import java.util.ArrayList;
  22.  
  23. import butterknife.Bind;
  24. import butterknife.ButterKnife;
  25. import butterknife.OnClick;
  26.  
  27. /**
  28. * Created by huang on 2016/12/3.
  29. */
  30. public class ChatActivity extends Activity {
  31. private static final String TAG = "ChatActivity";
  32. @Bind(R.id.tv_name)
  33. TextView tv_name;
  34. @Bind(R.id.lv_chat)
  35. ListView lv_chat;
  36. @Bind(R.id.et_sms)
  37. EditText et_sms;
  38.  
  39. private ArrayAdapter<QQMessage> adapter = null;
  40. private ArrayList<QQMessage> list = new ArrayList<>();
  41. private String account;
  42.  
  43. @OnClick(R.id.btn_send)
  44. public void send(View view) {
  45. String sendsms = et_sms.getText().toString().trim();
  46. if (TextUtils.isEmpty(sendsms)) {
  47. Toast.makeText(this, "消息不能为空", Toast.LENGTH_SHORT).show();
  48. return;
  49. }
  50. et_sms.setText("");
  51. final QQMessage qq = new QQMessage();
  52. qq.type = QQmessageType.MSG_TYPE_CHAT_P2P;
  53. qq.content = sendsms;
  54. qq.from = Long.parseLong(Myapp.username);
  55. qq.to = Long.parseLong(account);
  56. list.add(qq);
  57. setAdapteORNotify();
  58. ThreadUtils.runINThread(new Runnable() {
  59. @Override
  60. public void run() {
  61. try {
  62. Myapp.me.send(qq);
  63. } catch (IOException e) {
  64. e.printStackTrace();
  65. }
  66. }
  67. });
  68. }
  69.  
  70. private QQConnection.OnQQmwssagereceiveLisener listener = new QQConnection.OnQQmwssagereceiveLisener() {
  71. @Override
  72. public void onReiceive(final QQMessage qq) {
  73. if (QQmessageType.MSG_TYPE_CHAT_P2P.equals(qq.type)) {
  74. ThreadUtils.runUIThread(new Runnable() {
  75. @Override
  76. public void run() {
  77. list.add(qq);
  78. setAdapteORNotify();
  79. }
  80. });
  81. }
  82. }
  83. };
  84.  
  85. @Override
  86. protected void onCreate(Bundle savedInstanceState) {
  87. super.onCreate(savedInstanceState);
  88. setContentView(R.layout.activity_chat);
  89. ButterKnife.bind(this);
  90. Myapp.me.addOnQQmwssagereceiveLisener(listener);
  91. Intent intent = getIntent();
  92. account = intent.getStringExtra("account");
  93. String nick = intent.getStringExtra("nick");
  94. tv_name.setText("和" + nick + "聊天中......");
  95. setAdapteORNotify();
  96. }
  97.  
  98. @Override
  99. protected void onDestroy() {
  100. super.onDestroy();
  101. Myapp.me.removeOnQQmwssagereceiveLisener(listener);
  102. }
  103.  
  104. private void setAdapteORNotify() {
  105. if (list.size() < 1) {
  106. return;
  107. }
  108. if (adapter == null) {
  109. adapter = new ArrayAdapter<QQMessage>(this, 0, list) {
  110.  
  111. @Override
  112. public int getViewTypeCount() {
  113. return 2;
  114. }
  115.  
  116. @Override
  117. public int getItemViewType(int position) {
  118. QQMessage msg = list.get(position);
  119. long fromId = Long.parseLong(Myapp.username);
  120. if (fromId == msg.from) {
  121. return 0;
  122. }
  123. return 1;
  124. }
  125.  
  126. @Override
  127. public View getView(int position, View convertView, ViewGroup parent) {
  128. int type = getItemViewType(position);
  129. if (type == 0) {
  130. viewHolder holder1 = null;
  131. if (convertView == null) {
  132. holder1 = new viewHolder();
  133. convertView = View.inflate(getBaseContext(), R.layout.item_sms_send, null);
  134. holder1.tv_send_time = (TextView) convertView.findViewById(R.id.tv_send_time);
  135. holder1.tv_send = (TextView) convertView.findViewById(R.id.tv_send);
  136. convertView.setTag(holder1);
  137. } else {
  138. holder1 = (viewHolder) convertView.getTag();
  139. }
  140. QQMessage qqMessage = list.get(position);
  141. holder1.tv_send_time.setText(qqMessage.sendTime);
  142. holder1.tv_send.setText(qqMessage.content);
  143. return convertView;
  144. } else if (type == 1) {
  145. viewHolder holder2 = null;
  146. if (convertView == null) {
  147. holder2 = new viewHolder();
  148. convertView = View.inflate(getBaseContext(), R.layout.item_sms_receive, null);
  149. holder2.tv_receive_time = (TextView) convertView.findViewById(R.id.tv_receive_time);
  150. holder2.tv_receive = (TextView) convertView.findViewById(R.id.tv_receive);
  151. convertView.setTag(holder2);
  152. } else {
  153. holder2 = (viewHolder) convertView.getTag();
  154. }
  155. QQMessage qqMessage = list.get(position);
  156. holder2.tv_receive_time.setText(qqMessage.sendTime);
  157. holder2.tv_receive.setText(qqMessage.content);
  158. return convertView;
  159. }
  160. return convertView;
  161. }
  162. };
  163. lv_chat.setAdapter(adapter);
  164. } else {
  165. adapter.notifyDataSetChanged();
  166. }
  167.  
  168. if (lv_chat.getCount() > 0) {
  169. lv_chat.setSelection(lv_chat.getCount() - 1);
  170. }
  171. }
  172.  
  173. class viewHolder {
  174. TextView tv_send_time;
  175. TextView tv_send;
  176. TextView tv_receive_time;
  177. TextView tv_receive;
  178. }
  179. }

最后可以添加一个服务当程序退出时,接受消息。

  1. import android.app.Service;
  2. import android.content.Intent;
  3. import android.os.IBinder;
  4. import android.widget.Toast;
  5.  
  6. import com.example.huang.imsocket.bean.Myapp;
  7. import com.example.huang.imsocket.bean.QQMessage;
  8. import com.example.huang.imsocket.core.QQConnection;
  9. import com.example.huang.imsocket.util.ThreadUtils;
  10.  
  11. /**
  12. * Created by huang on 2016/12/7.
  13. */
  14.  
  15. public class IMService extends Service {
  16. private QQConnection.OnQQmwssagereceiveLisener lisener = new QQConnection.OnQQmwssagereceiveLisener() {
  17. @Override
  18. public void onReiceive(final QQMessage qq) {
  19. ThreadUtils.runUIThread(new Runnable() {
  20. @Override
  21. public void run() {
  22. Toast.makeText(getBaseContext(), "收到好友消息: " + qq.content, Toast.LENGTH_SHORT).show();
  23. }
  24. });
  25. }
  26. };
  27.  
  28. @Override
  29. public IBinder onBind(Intent intent) {
  30. return null;
  31. }
  32.  
  33. @Override
  34. public void onCreate() {
  35. super.onCreate();
  36. Toast.makeText(getBaseContext(), "服务开启", Toast.LENGTH_SHORT).show();
  37. Myapp.me.addOnQQmwssagereceiveLisener(lisener);
  38. }
  39.  
  40. @Override
  41. public void onDestroy() {
  42. Myapp.me.removeOnQQmwssagereceiveLisener(lisener);
  43. super.onDestroy();
  44. }
  45. }

Activity和Service节点配置,以及相应的权限。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  3. package="com.example.huang.imsocket">
  4.  
  5. <uses-permission android:name="android.permission.INTERNET" />
  6. <application
  7. android:allowBackup="true"
  8. android:icon="@mipmap/ic_launcher"
  9. android:label="@string/app_name"
  10. android:supportsRtl="true"
  11. android:theme="@style/AppTheme">
  12.  
  13. <activity android:name="com.example.huang.imsocket.activity.SplashActivity">
  14. <intent-filter>
  15. <action android:name="android.intent.action.MAIN" />
  16.  
  17. <category android:name="android.intent.category.LAUNCHER" />
  18. </intent-filter>
  19. </activity>
  20. <activity
  21. android:name="com.example.huang.imsocket.activity.LoginActivity"
  22. android:theme="@android:style/Theme.NoTitleBar"></activity>
  23. <activity
  24. android:name="com.example.huang.imsocket.activity.ChatActivity"
  25. android:theme="@android:style/Theme.NoTitleBar"></activity>
  26. <activity
  27. android:name="com.example.huang.imsocket.activity.contactActivity"
  28. android:theme="@android:style/Theme.NoTitleBar"></activity>
  29.  
  30. <service android:name=".service.IMService" />
  31. </application>
  32.  
  33. </manifest>

android环境下的即时通讯的更多相关文章

  1. Android基于XMPP的即时通讯3-表情发送

    这篇博文主要讲表情发送的一些东西. 参考:Android基于XMPP的即时通讯1-基本对话 1.准备好资源文件 采用的是emoji的表情,我打包好了,下载地址:http://files.cnblogs ...

  2. Android基于XMPP的即时通讯2-文件传输

    本文是在上一篇博文Android基于XMPP的即时通讯1-基本对话的基础上,添加新的功能,文件传输 1.初始化文件传输管理类 public static FileTransferManager get ...

  3. cocos2d-x 在android环境下开发遇到的一些bug

    今天在弄一个关于android环境下解析xml的东东,遇到了2个比较麻烦问题 1.android的apk下文件是压缩文件,io.open模式无法读取到数据的, 解决思路就是: CCFileUtils: ...

  4. Android 环境下编译FFmpeg

    Android 环境下编译FFmpeg 开发环境:Ubuntu 12.04.2 LTS , android-sdk-linux, android-ndk-r8e 一 .X264 编译 1.    X2 ...

  5. Android基于XMPP的即时通讯1-基本对话

    闲暇之余,自己写了个简单的即时通讯,基于OpenFire服务器平台. 整个项目包括两个部分,一个是服务器端,一个是android手机端: 一.关于服务器端没什么好说的,下载安装配置即可 推荐下载带ja ...

  6. android环境下两种md5加密方式

    在平时开发过程中,MD5加密是一个比较常用的算法,最常见的使用场景就是在帐号注册时,用户输入的密码经md5加密后,传输至服务器保存起来.虽然md5加密经常用,但是md5的加密原理我还真说不上来,对md ...

  7. 在高通平台Android环境下编译内核模块【转】

    本文转载自:http://blog.xeonxu.info/blog/2012/12/04/zai-gao-tong-ping-tai-androidhuan-jing-xia-bian-yi-nei ...

  8. Android基于xmpp的即时通讯应用

    xmpp是一个通信协议.因为这是个开放的协议,为了节俭开发成本,很多即时应用都采用了这个协议.Android上最常用的组合asmack +openfire.Asmack是smack的android版, ...

  9. cocos2d-x 3.x丨搭建Android环境下的开发环境

    所需要的一些工具软件: 1.JDK  官网下载地址:http://www.oracle.com/ttechnetwork/java/javase/downloads/index.html 2.Andr ...

随机推荐

  1. Android开发学习之路-LeakCanary使用

    LeakCanary是一个内存泄漏检测库,它可以在我们的应用发生内存泄漏的时候发出提醒,提醒包括通知和Log.GitHub 这个库使用起来比较简单: ①添加依赖: dependencies { deb ...

  2. [Java Collection]List分组之简单应用.

    前言 今天有一个新需求, 是对一个List进行分组, 于是便百度到一些可用的代码以及我们项目使用的一些tools, 在这里总结下方便以后查阅. 一: 需求 现在我们一个数据库表t_series_val ...

  3. Kafka 文档引言

    原文地址:https://kafka.apache.org/documentation.html#semantics 1.开始 1.1 引言 Kafka是一个分布式,分区队列,冗余备份的消息存储服务. ...

  4. Python标准模块--os

    1.模块简介 os模块主要包含普遍的操作系统相关操作,如果开发者希望自己开发的Python应用能够与平台无关,尤其需要关注os这个模块. 2.模块使用 2.1 os模块 1. os.name,输出字符 ...

  5. ASP.NET Core 1.0中实现文件上传的两种方式(提交表单和采用AJAX)

    Bipin Joshi (http://www.binaryintellect.net/articles/f1cee257-378a-42c1-9f2f-075a3aed1d98.aspx) Uplo ...

  6. Python基础(三)

    本章内容: 深浅拷贝 函数(全局与局部变量) 内置函数 文件处理 三元运算 lambda 表达式 递归(斐波那契数列) 冒泡排序 深浅拷贝 一.数字和字符串 对于 数字 和 字符串 而言,赋值.浅拷贝 ...

  7. 【中文分词】隐马尔可夫模型HMM

    Nianwen Xue在<Chinese Word Segmentation as Character Tagging>中将中文分词视作为序列标注问题(sequence labeling ...

  8. 让VIM支持Python2 by update-alternatives

    前言  Ubuntu 16+中$ sudo apt install vim所安装的vim只支持Python3,但很多插件如YCM和powerline均需要Python2,那就来场"生命贵在折 ...

  9. 一款批量修改AE模板的工具

    一.需求分析 对于视频后期剪辑及相关从业人员来说,AE(After Effects)模板效果是一个不错的开始点.在模板效果的基础上,可以很快的做出各种炫酷的后期效果.但是在网上下载的模板工程中,往往包 ...

  10. C#开机自动启动程序代码

    新建一个winform拖一个checkbox进来.. 然后设置它的changed事件. 已经测试过,可以直接复制使用. private void checkBox1_CheckedChanged(ob ...