【socket.io研究】3.手机网页间聊天核心问题
前面我们已经说了服务器相关的一些内容,且又根据官网给出的一个例子写了一个可以聊天的小程序,但是这还远远不够呀,这只能算是应用前的准备工作。接下来,一起来考虑完善一个小的聊天程序吧。
首先,修改服务器的代码以前就是单纯的接收转发,现在我们考虑定向转发,及这个消息发送给需要接收的接受者,而不是全部客户端用户,并且考虑不使用默认namespace,那样太不安全了。
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http); var fs = require('fs'), os = require('os'), url = require('url'); var clients = []; var sockets = []; app.get('/', function (req, res) { res.sendFile(__dirname + '/chat_to_everyone.html'); }); io = io.of('/test'); io.on('connection', function (socket) { // register socket.on('online', function (msg) { console.log('new user: ' + msg + '; socket id: ' + socket.id); var client_info = new Object(); client_info.socket = socket; sockets[msg] = client_info; // return all registered list var ret = ""; for (var key in sockets) { if (sockets.hasOwnProperty(key)) { ret += key + ' '; } } console.log('users: ' + ret); io.emit('online', ret); }); // private socket.on('private', function(data) { console.log('private: ' + data['uesrname'] + ' --> ' + data['to'] + ' : ' + data['msg']); //io.to(room).emit('private', data); io.emit(data['to']+'', data); io.emit(data['uesrname']+'', data); }); // leave socket.on('disconnect', function(msg){ // delete from sockets for (var key in sockets) { if (sockets.hasOwnProperty(key)) { if (sockets[key].socket.id == socket.id) { console.log('leave: ', msg); delete(sockets[key]); // return all registered list var ret = ""; for (var key in sockets) { if (sockets.hasOwnProperty(key)) { ret += key + ' '; } } io.emit('online', ret); break; } } } }); }); http.listen(3000, function () { console.log('listening on *:3000'); });
监听3000端口,namespace为test,监听事件:用户连接,上线online,发送消息private,下线disconnect,注意这里的消息不是普通的字符串了,其中至少包含了发送者用户名username,接受者to,消息msg,当然,其中还有其他消息,包括时间等,具体情况具体分析,服务器在接收到消息后,打印日志,将消息发送给接受者和发送者,为什么需要发送给发送者,因为这样发送者在接收到服务器的返回消息时可以确定服务器一定是接收到消息了。
那客户端什么样的呢?
<!doctype html> <html> <head> <title>Socket.IO chat with room</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font: 13px Helvetica, Arial; } form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; } form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; } form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; } #messages { list-style-type: none; margin: 0; padding: 0; } #messages li { padding: 5px 10px; } #messages li:nth-child(odd) { background: #eee; } </style> </head> <body> <h1>Online users</h1> <ul id="online-users"> </ul> <h1 id="room">Messages</h1> <ul id="messages"></ul> <form action=""> <input id="m" autocomplete="off" /><button>Send</button> </form> <script src="https://cdn.socket.io/socket.io-1.2.0.js"></script> <script src="http://code.jquery.com/jquery-1.11.1.js"></script> <script> var myid = Math.floor(Math.random() * 100) + 1; var talkto = 0; var myroom = ''; var socket = io('/test'); var password = '123456'; // register online socket.emit('online', myid); socket.on('online', function(msg) { //$('#online-users').append($('<li>').text(msg)); var users = msg.split(' '); $('#online-users').empty(); for (var i in users) { if (users[i]) { if (users[i] == myid) { $('#online-users').append($('<li>').append($('<a>').attr('href', '#').text(users[i] + ' is me'))); }else { $('#online-users').append($('<li>').append($('<a>').attr('href', '#').text(users[i]))); } } } $('#online-users li a').click(function(){ var target = $(this).text(); if (myid != parseInt(target)) { var from = myid, to = target; talkto = to; myroom = from + '#' + to; } }); }); // create room socket.on('talkwith', function(msg) { $('#room').text(msg); myroom = msg; }); socket.on('' + myid, function(data){ $('#messages').append($('<li>').text(data['uesrname'] + ' --> ' + data['to'] + ' : ' + data['msg'])); }); // private message $('form').submit(function(){ //socket.emit('private', { 'room':myroom, 'msg': myid + ' says: ' + $('#m').val()}); socket.emit('private', {'uesrname':myid, 'password':password, 'to':talkto, 'msg':$('#m').val(), 'date':new Date().Format("yyyy-MM-dd HH:mm:ss")}); $('#m').val(''); return false; }); socket.on('private', function(data){ // switch to the new room myroom = data['room']; $('#room').text(myroom); $('#messages').append($('<li>').text(data['msg'])); }); //格式化时间,来自网络 Date.prototype.Format = function (fmt) { //author: meizz var o = { "M+": this.getMonth() + 1, //月份 "d+": this.getDate(), //日 "H+": this.getHours(), //小时 "m+": this.getMinutes(), //分 "s+": this.getSeconds(), //秒 "q+": Math.floor((this.getMonth() + 3) / 3), //季度 "S": this.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); return fmt; } </script> </body> </html>
其实也是挺简单的,界面显示在线用户,点击在线用户名,则向该用户发送消息,消息内容包括自己的用户名username,接收者to,消息内容msg,时间date,这里使用了一个格式化的方法,也不难理解,注意这里监听的是发送给自己的消息,对其他消息则不处理不接收。
可以试验,网页客户端确实可以通行聊天了,那跟android相关的部分呢,主要有以下几个点需要注意:
1.app不在聊天窗口,关闭了程序,就完全不监听发过来的消息了吗?这不好,需要在后台监听,通知栏通知,这样的话,必然用到了service,在service中处理监听等,并可以把消息保存到本地数据库中,也好日后查看显示。
2.不同的人发送的消息应该有一个联系人的列表吧,就想qq一样,那就是对于接收到的消息,按照发送者分组,数据库查询就是group by了,时间降序排序,这里的在我的表中,id是根据时间递增的,我可以按照id降序就是时间的降序了,order by ** desc,好多联系人,ListView和Adapter是不可少的。
3.如果已经在聊天窗口中,要不要继续在通知栏中通知了,不需要了吧,要有一个标志。
4.点击一个列表的某一项,要显示详细聊天内容,这个在下一篇文章中讨论。
大致思路清楚了,看看代码吧:
import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; /** * 聊天相关数据库操作 * Created by RENYUZHUO on 2015/12/14. */ public class SQLOperation extends SQLiteOpenHelper { Context context; String name; SQLiteDatabase.CursorFactory factory; int version; String sqlMessage = "create table message(" + "id integer primary key autoincrement," + "fromwho text," + "msg text," + "data text)"; public SQLOperation(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, name, factory, version); this.context = context; this.name = name; this.factory = factory; this.version = version; } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(sqlMessage); Log.i("create sqlMessage", "success"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // switch (oldVersion){ // case 1:{ // db.execSQL(sqlMessage); // } // } } }
//数据库初始化,开启服务 sqlOperation = new SQLOperation(this, "fanshop.db", null, 1); sqLiteDatabase = sqlOperation.getWritableDatabase(); Intent intent = new Intent(context, ChatService.class); startService(intent);
import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.sqlite.SQLiteDatabase; import android.os.Build; import android.os.IBinder; import android.util.Log; import com.bafst.fanshop.FanShopApplication; import com.bafst.fanshop.R; import com.bafst.fanshop.model.Chat.Message; import com.bafst.fanshop.net.JsonUtils; import com.bafst.fanshop.util.Global; import com.github.nkzawa.emitter.Emitter; import com.github.nkzawa.socketio.client.IO; import com.github.nkzawa.socketio.client.Socket; import java.net.URISyntaxException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; /** * 监听消息,保存到数据库中,neededNotice和notice方法设置是否需要在通知栏中通知 */ public class ChatService extends Service { Socket mSocket; Context context; /** * 身份信息 */ private String myname; private String str = ""; private String textShow = ""; public static int num = 0; public static boolean notice = true; { try { mSocket = IO.socket(Global.CHAT_URL); } catch (URISyntaxException e) { } } public ChatService() { Log.i("ChatService", "in ChatService"); context = this; myname = getUserName(); mSocket.on("online", online); mSocket.on(myname, myMessage); mSocket.connect(); mSocket.emit("online", myname); } /** * 发送登陆信息 */ Emitter.Listener online = new Emitter.Listener() { @Override public void call(final Object... args) { Log.i("online", "in online"); new Runnable() { @Override public void run() { Log.i("online.run", "in online.run"); String msg = args[0].toString(); String[] users = msg.split(" "); str = ""; for (String user : users) { if (myname.equals(user)) { str += "my name:" + user + "\n"; } else { str += user + "\n"; } } textShow = str; Log.i("textShow", textShow); } }.run(); } }; NotificationManager manager; Notification myNotication; /** * 获取发给本用户的信息 */ Emitter.Listener myMessage = new Emitter.Listener() { @Override public void call(final Object... args) { new Runnable() { @Override public void run() { Log.i("myMessage", "in myMessage"); str = "" + args[0]; Message message = JsonUtils.fromJson(str, Message.class); textShow = message.toString(); Log.i("message", textShow); ContentValues values = new ContentValues(); values.put("fromwho", message.getUesrname()); values.put("msg", message.getMsg()); // values.put("data", message.getDate().replace("-", "T").replace(" ", "U").replace(":", "V")); values.put("data", message.getDate()); SQLiteDatabase sqliteDatabase = FanShopApplication.getSqLiteDatabase(); sqliteDatabase.insert("message", null, values); if (notice) { manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); Intent intent = new Intent(context, ChatDetailActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, 0); Notification.Builder builder = new Notification.Builder(context); builder.setAutoCancel(true); builder.setTicker(message.getMsg()); builder.setContentTitle(getResources().getString(R.string.app_name)); builder.setContentText(message.getUesrname()); builder.setSmallIcon(R.mipmap.ic_launcher); builder.setContentIntent(pendingIntent); builder.setOngoing(false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { builder.setSubText(message.getMsg()); builder.build(); } builder.setNumber(++num); myNotication = builder.getNotification(); manager.notify(1, myNotication); } } }.run(); } }; /** * 获取用户名或者是与用户名可以相互对应的唯一身份验证标识 * * @return username */ private String getUserName() { return String.valueOf((int) (Math.random() * 100)); } @Override public IBinder onBind(Intent intent) { throw new UnsupportedOperationException("Not yet implemented"); } @Override public void onDestroy() { mSocket.off("online", online); mSocket.off(myname, online); mSocket.close(); super.onDestroy(); } public static void notice() { notice = true; } public static void neededNotice() { notice = false; } }
//从数据库中读取数据并通过适配器显示在界面上,没有考虑头像问题 private void dealTab1() { ChatService.neededNotice(); List<Message> messages = new ArrayList<Message>(); SQLiteDatabase sqliteDatabase = FanShopApplication.getSqLiteDatabase(); Cursor result = sqliteDatabase.query("message", null, null, null, "fromwho", null, "id desc"); Message message; while (result.moveToNext()) { message = new Message(); message.setUesrname(result.getString(result.getColumnIndex("fromwho")) + ""); message.setId(result.getInt(result.getColumnIndex("id")) + ""); message.setDate(result.getString(result.getColumnIndex("data")) + ""); message.setMsg(result.getString(result.getColumnIndex("msg"))); messages.add(message); } mesList = (ListView) findViewById(R.id.mesList); messageListAdapter = new MessageListAdapter(this, messages); mesList.setAdapter(messageListAdapter); }
//列表中的每一个的的布局文件 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:id="@+id/heap" android:layout_width="60sp" android:layout_height="60sp" android:src="@drawable/a" /> <ImageView android:layout_width="60sp" android:layout_height="60sp" android:src="@drawable/message_item_pit_top" /> <TextView android:id="@+id/username" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/heap" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/heap_top" android:textSize="20sp" android:text="ooo" /> <TextView android:id="@+id/msg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/username" android:layout_toRightOf="@id/heap" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/msg_top" android:text="222" /> <TextView android:id="@+id/time" android:text="ddd" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/heap_top" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </RelativeLayout> </RelativeLayout>
//列表整体布局 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/mesList" android:layout_width="match_parent" android:layout_height="wrap_content"> </ListView> </LinearLayout>
这些内容合理组织就可以了,可以实现手机与手机,手机与网页之间的通信,可以接收消息,保存到数据库中,列表显示不同发来消息的用户,就像QQ中的列表一样,这里没有考虑如何显示详细的聊天内容,这是因为要通过点击进入到详情中查看,在其他的activity中,其他的sql语句,因此,在下一篇文章中介绍。