1. 程式人生 > >Android開發--IM聊天專案(一)

Android開發--IM聊天專案(一)

在知乎上看了一篇文章,感覺受益匪淺。認真迭代一個專案比盲目的多寫幾個app的收益會更大,還有就是認真的夯實基礎,拿offer面試的時候也會更注重基礎,還有半年的時間來準備,也就不打算再寫其它的專案了,部落格方面的話就不定期來寫寫最近的學習心得,還有這個專案的進展吧。每篇文章的最後都會分析一下目前的缺點以及短期計劃。
專案方向:IM(Instant Messenge)聊天專案
專案要求:Android端:UI、訊息持久化(讀寫、快取)、長連結網路(解決NAT 超時、DHCP續租、參考TLS1.3的安全機制、私有協議設計、大檔案分片、失敗重試機制設計)、多執行緒、訊息推送、訊息同步等。
服務端:持久化(表設計、分庫機制)、傳送佇列、連線管理、併發、負載均衡等。

下面開始進入正文:截至本篇文章、已經實現了網頁端、伺服器端(NodeJs)、和android端的最簡單基礎的功能,只能夠實現簡單的聊天。網頁地址為:http://www.spwannasing.cn:4000/ 網頁端目前只打算用來測試、不會過多的投入。
如此迅速的就能實現三個端的核心功能,還是用了不少外部庫和框架的。比如android端,用了一個socket的支援庫,實現和伺服器連線。這一點要反思,下面幾天要研究一下這個socket原始碼,還有就是相關的一些傳輸協議、原來太年輕不重視的計算機網路的基礎知識還是相當重要的,切記切記啊。
先來看一下服務端,直接上程式碼:

var express = require
('express'), app = express(), server = require('http').createServer(app), io = require('socket.io').listen(server), users = []; app.use('/', express.static(__dirname + '/www')); server.listen(process.env.PORT || 4000); io.sockets.on('connection', function(socket) { socket.on('login', function(nickname) { if
(users.indexOf(nickname) > -1) { socket.emit('nickExisted'); } else { //socket.userIndex = users.length; socket.nickname = nickname; users.push(nickname); socket.emit('loginSuccess'); io.sockets.emit('system', nickname, users.length, 'login'); }; }); //user leaves socket.on('disconnect', function() { if (socket.nickname != null) { //users.splice(socket.userIndex, 1); users.splice(users.indexOf(socket.nickname), 1); socket.broadcast.emit('system', socket.nickname, users.length, 'logout'); } }); //new message get socket.on('postMsg', function(msg, color) { socket.broadcast.emit('newMsg', socket.nickname, msg, color); }); //new image get socket.on('img', function(imgData, color) { socket.broadcast.emit('newImg', socket.nickname, imgData, color); }); });

程式碼很清晰簡單,首先是監聽4000埠,然後用socket通訊,監聽 對應String事件。也沒有什麼資料庫和持久化,所以直接emit出去資訊就可以了。

知識點:利用Socket建立網路連線的步驟

  建立Socket連線至少需要一對套接字,其中一個運行於客戶端,稱為ClientSocket ,另一個運行於伺服器端,稱為ServerSocket 。

  套接字之間的連線過程分為三個步驟:伺服器監聽,客戶端請求,連線確認。

  1、伺服器監聽:伺服器端套接字並不定位具體的客戶端套接字,而是處於等待連線的狀態,實時監控網路狀態,等待客戶端的連線請求。

  2、客戶端請求:指客戶端的套接字提出連線請求,要連線的目標是伺服器端的套接字。

  為此,客戶端的套接字必須首先描述它要連線的伺服器的套接字,指出伺服器端套接字的地址和埠號,然後就向伺服器端套接字提出連線請求。

  3、連線確認:當伺服器端套接字監聽到或者說接收到客戶端套接字的連線請求時,就響應客戶端套接字的請求,建立一個新的執行緒,把伺服器端套接字的描述發給客戶端,一旦客戶端確認了此描述,雙方就正式建立連線。

window.onload = function() {
    var hichat = new HiChat();
    hichat.init();
};
var HiChat = function() {
    this.socket = null;
};
HiChat.prototype = {
    init: function() {
        var that = this;
        this.socket = io.connect();
        this.socket.on('connect', function() {
            document.getElementById('info').textContent = 'get yourself a nickname :)';
            document.getElementById('nickWrapper').style.display = 'block';
            document.getElementById('nicknameInput').focus();
        });
        this.socket.on('nickExisted', function() {
            document.getElementById('info').textContent = '!nickname is taken, choose another pls';
        });
        this.socket.on('loginSuccess', function() {
            document.title = 'hichat | ' + document.getElementById('nicknameInput').value;
            document.getElementById('loginWrapper').style.display = 'none';
            document.getElementById('messageInput').focus();
        });
        this.socket.on('error', function(err) {
            if (document.getElementById('loginWrapper').style.display == 'none') {
                document.getElementById('status').textContent = '!fail to connect :(';
            } else {
                document.getElementById('info').textContent = '!fail to connect :(';
            }
        });
        this.socket.on('system', function(nickName, userCount, type) {
            var msg = nickName + (type == 'login' ? ' joined' : ' left');
            that._displayNewMsg('system ', msg, 'red');
            document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online';
        });
        this.socket.on('newMsg', function(user, msg, color) {
            that._displayNewMsg(user, msg, color);
        });
        this.socket.on('newImg', function(user, img, color) {
            that._displayImage(user, img, color);
        });
        document.getElementById('loginBtn').addEventListener('click', function() {
            var nickName = document.getElementById('nicknameInput').value;
            if (nickName.trim().length != 0) {
                that.socket.emit('login', nickName);
            } else {
                document.getElementById('nicknameInput').focus();
            };
        }, false);
        document.getElementById('nicknameInput').addEventListener('keyup', function(e) {
            if (e.keyCode == 13) {
                var nickName = document.getElementById('nicknameInput').value;
                if (nickName.trim().length != 0) {
                    that.socket.emit('login', nickName);
                };
            };
        }, false);
        document.getElementById('sendBtn').addEventListener('click', function() {
            var messageInput = document.getElementById('messageInput'),
                msg = messageInput.value,
                color = document.getElementById('colorStyle').value;
            messageInput.value = '';
            messageInput.focus();
            if (msg.trim().length != 0) {
                that.socket.emit('postMsg', msg, color);
                that._displayNewMsg('me', msg, color);
                return;
            };
        }, false);
        document.getElementById('messageInput').addEventListener('keyup', function(e) {
            var messageInput = document.getElementById('messageInput'),
                msg = messageInput.value,
                color = document.getElementById('colorStyle').value;
            if (e.keyCode == 13 && msg.trim().length != 0) {
                messageInput.value = '';
                that.socket.emit('postMsg', msg, color);
                that._displayNewMsg('me', msg, color);
            };
        }, false);
        document.getElementById('clearBtn').addEventListener('click', function() {
            document.getElementById('historyMsg').innerHTML = '';
        }, false);
        document.getElementById('sendImage').addEventListener('change', function() {
            if (this.files.length != 0) {
                var file = this.files[0],
                    reader = new FileReader(),
                    color = document.getElementById('colorStyle').value;
                if (!reader) {
                    that._displayNewMsg('system', '!your browser doesn\'t support fileReader', 'red');
                    this.value = '';
                    return;
                };
                reader.onload = function(e) {
                    this.value = '';
                    that.socket.emit('img', e.target.result, color);
                    that._displayImage('me', e.target.result, color);
                };
                reader.readAsDataURL(file);
            };
        }, false);
        this._initialEmoji();
        document.getElementById('emoji').addEventListener('click', function(e) {
            var emojiwrapper = document.getElementById('emojiWrapper');
            emojiwrapper.style.display = 'block';
            e.stopPropagation();
        }, false);
        document.body.addEventListener('click', function(e) {
            var emojiwrapper = document.getElementById('emojiWrapper');
            if (e.target != emojiwrapper) {
                emojiwrapper.style.display = 'none';
            };
        });
        document.getElementById('emojiWrapper').addEventListener('click', function(e) {
            var target = e.target;
            if (target.nodeName.toLowerCase() == 'img') {
                var messageInput = document.getElementById('messageInput');
                messageInput.focus();
                messageInput.value = messageInput.value + '[emoji:' + target.title + ']';
            };
        }, false);
    },
    _initialEmoji: function() {
        var emojiContainer = document.getElementById('emojiWrapper'),
            docFragment = document.createDocumentFragment();
        for (var i = 69; i > 0; i--) {
            var emojiItem = document.createElement('img');
            emojiItem.src = '../content/emoji/' + i + '.gif';
            emojiItem.title = i;
            docFragment.appendChild(emojiItem);
        };
        emojiContainer.appendChild(docFragment);
    },
    _displayNewMsg: function(user, msg, color) {
        var container = document.getElementById('historyMsg'),
            msgToDisplay = document.createElement('p'),
            date = new Date().toTimeString().substr(0, 8),
            //determine whether the msg contains emoji
            msg = this._showEmoji(msg);
        msgToDisplay.style.color = color || '#000';
        msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg;
        container.appendChild(msgToDisplay);
        container.scrollTop = container.scrollHeight;
    },
    _displayImage: function(user, imgData, color) {
        var container = document.getElementById('historyMsg'),
            msgToDisplay = document.createElement('p'),
            date = new Date().toTimeString().substr(0, 8);
        msgToDisplay.style.color = color || '#000';
        msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span> <br/>' + '<a href="' + imgData + '" target="_blank"><img src="' + imgData + '"/></a>';
        container.appendChild(msgToDisplay);
        container.scrollTop = container.scrollHeight;
    },
    _showEmoji: function(msg) {
        var match, result = msg,
            reg = /\[emoji:\d+\]/g,
            emojiIndex,
            totalEmojiNum = document.getElementById('emojiWrapper').children.length;
        while (match = reg.exec(msg)) {
            emojiIndex = match[0].slice(7, -1);
            if (emojiIndex > totalEmojiNum) {
                result = result.replace(match[0], '[X]');
            } else {
                result = result.replace(match[0], '<img class="emoji" src="../content/emoji/' + emojiIndex + '.gif" />');//todo:fix this in chrome it will cause a new request for the image
            };
        };
        return result;
    }
};

先說一下明顯的缺點再放出程式碼吧:目前的功能唯一的難點。。socket連線用了一個socket.io的外部庫。。。導致寫出來毫無成就感,應該逐步自己來實現這些功能。

package com.sp.chattingroom;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import com.github.nkzawa.emitter.Emitter;
import com.github.nkzawa.socketio.client.IO;
import com.github.nkzawa.socketio.client.Socket;
import com.sp.chattingroom.Adapter.ChatRecyclerAdpter;
import com.sp.chattingroom.Model.Msg;

import java.net.URISyntaxException;
import java.util.ArrayList;


import butterknife.BindView;
import butterknife.ButterKnife;

public class ChatActivity extends AppCompatActivity {
    private static final String TAG = "ChatActivity";
    @BindView(R.id.text_list)RecyclerView recyclerView;
    @BindView(R.id.send_btn)Button button;
    @BindView(R.id.chat_edit)EditText editText;
    private static String IPAddress="115.159.38.75";
    private static int PORT=4000;
    private Socket socket=null;
    private ChatRecyclerAdpter adpter;
    private ArrayList<Msg> msg_list=new ArrayList<>(),msg_list_save=new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        adpter=new ChatRecyclerAdpter(this,msg_list);
        LinearLayoutManager manager=new LinearLayoutManager(this);
        recyclerView.setLayoutManager(manager);
        recyclerView.setAdapter(adpter);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String msg_send=editText.getText().toString();
                Msg msg=new Msg();
                msg.setType(1);
                msg.setContent(msg_send);
                msg_list_save.add(msg);
                Message.obtain(handler).sendToTarget();
                socket.emit("postMsg",msg_send);
                editText.setText("");
            }
        });
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Log.e(TAG, "onCreate: start" );
                    socket= IO.socket("http://115.159.38.75:4000");
                    socket.connect();
                }catch (URISyntaxException e){
                    Log.e(TAG, "run: "+"error" );
                    e.printStackTrace();
                }
                Log.e(TAG, "run: "+socket.connected());
                socket.emit("login","CallMeSp");
                socket.on("nickExisted", new Emitter.Listener() {
                    @Override
                    public void call(Object... args) {
                        Log.e(TAG, "nickExisted" );
                    }
                });
                socket.on("loginSuccess", new Emitter.Listener() {
                    @Override
                    public void call(Object... args) {
                        Log.e(TAG, "loginSuccess");
                    }
                });
                socket.on("newMsg", new Emitter.Listener() {
                    @Override
                    public void call(Object... args) {
                        String newcontent=args[1].toString();
                        Msg msg=new Msg();
                        msg.setContent(newcontent);
                        msg.setType(0);
                        msg_list_save.add(msg);
                        Message.obtain(handler).sendToTarget();
                        Log.e(TAG, "call: "+args.length );
                        Log.e(TAG, "newmsg:"+args[2].toString()+"\nnewmsg:"+args[1].toString()+"newmsg:"+args[0]);
                    }
                });
            }
        }).start();
    }
    Handler handler=new Handler(){
        @Override
        public void handleMessage(Message message){
            msg_list.clear();
            msg_list.addAll(msg_list_save);
            adpter.notifyDataSetChanged();
        }
    };
    @Override
    public void onDestroy(){
        super.onDestroy();
        socket.close();
    }
}

菜的摳腳。前路漫漫。
當前要做的事

  • 弄清socket.io的原始碼。
  • android端實現按天的聊天記錄持久化
  • 優化UI、顯示發言人的ID