Android开发--IM聊天项目(一)
2017-02-08 22:54
375 查看
在知乎上看了一篇文章,感觉受益匪浅。认真迭代一个项目比盲目的多写几个app的收益会更大,还有就是认真的夯实基础,拿offer面试的时候也会更注重基础,还有半年的时间来准备,也就不打算再写其它的项目了,博客方面的话就不定期来写写最近的学习心得,还有这个项目的进展吧。每篇文章的最后都会分析一下目前的缺点以及短期计划。
项目方向:IM(Instant Messenge)聊天项目
项目要求:Android端:UI、消息持久化(读写、缓存)、长链接网络(解决NAT 超时、DHCP续租、参考TLS1.3的安全机制、私有协议设计、大文件分片、失败重试机制设计)、多线程、消息推送、消息同步等。
服务端:持久化(表设计、分库机制)、发送队列、连接管理、并发、负载均衡等。
下面开始进入正文:截至本篇文章、已经实现了网页端、服务器端(NodeJs)、和android端的最简单基础的功能,只能够实现简单的聊天。网页地址为:http://www.spwannasing.cn:4000/ 网页端目前只打算用来测试、不会过多的投入。
如此迅速的就能实现三个端的核心功能,还是用了不少外部库和框架的。比如android端,用了一个socket的支持库,实现和服务器连接。这一点要反思,下面几天要研究一下这个socket源码,还有就是相关的一些传输协议、原来太年轻不重视的计算机网络的基础知识还是相当重要的,切记切记啊。
先来看一下服务端,直接上代码:
代码很清晰简单,首先是监听4000端口,然后用socket通讯,监听 对应String事件。也没有什么数据库和持久化,所以直接emit出去信息就可以了。
知识点:利用Socket建立网络连接的步骤
建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
1、服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
2、客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。
为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
3、连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。
而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
然后是网页端:就展示与服务器交互的一部分吧,其余的给出GitHub:https://github.com/CallMeSp/MyChattingRoom_backend-Webend.git 服务器端和网页端的内容都在里面。
函数名称很清晰的说明了功能,这里也就不多说了。
下面来到了android端。GitHub地址是:https://github.com/CallMeSp/MyChattingRoom_android
界面目前还比较粗糙
先说一下明显的缺点再放出代码吧:目前的功能唯一的难点。。socket连接用了一个socket.io的外部库。。。导致写出来毫无成就感,应该逐步自己来实现这些功能。
菜的抠脚。前路漫漫。
当前要做的事
弄清socket.io的源码。
android端实现按天的聊天记录持久化
优化UI、显示发言人的ID
项目方向: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) { 4000 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、连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。
而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
然后是网页端:就展示与服务器交互的一部分吧,其余的给出GitHub:https://github.com/CallMeSp/MyChattingRoom_backend-Webend.git 服务器端和网页端的内容都在里面。
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', funct e2c9 ion(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; } };
函数名称很清晰的说明了功能,这里也就不多说了。
下面来到了android端。GitHub地址是:https://github.com/CallMeSp/MyChattingRoom_android
界面目前还比较粗糙
先说一下明显的缺点再放出代码吧:目前的功能唯一的难点。。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
相关文章推荐
- Android开发--IM聊天项目(三)
- Android开发--IM聊天项目(二)
- Android开发--IM聊天项目(五)--进程保活踩坑
- Android开发--IM聊天项目(四)--IPC实战
- Android基于XMPP Smack Openfire下学习开发IM(四)单人聊天和多人聊天(发送消息、接收消息)
- Android开发智能机器人聊天项目(1)- 异步请求数据
- Android基于XMPP Smack Openfire下学习开发IM(四)单人聊天和多人聊天(发送消息、接收消息)
- Android基于XMPP Smack Openfire下学习开发IM(四)单人聊天和多人聊天(发送消息、接收消息)
- Android基于环信SDK开发IM即时聊天(二)
- Android基于XMPP Smack Openfire下学习开发IM(四)单人聊天和多人聊天(发送消息、接收消息)
- Android 的开源电话/通讯/IM聊天项目全集
- Android基于XMPP Smack Openfire下学习开发IM(四)单人聊天和多人聊天(发送消息、接收消息)
- Android基于环信SDK开发IM即时聊天
- Android 的开源电话/通讯/IM聊天项目全集
- android的开源电话/通讯/IM聊天项目全集
- Android基于XMPP Smack Openfire下学习开发IM(四)单人聊天和多人聊天(发送消息、接收消息)
- Android开发智能机器人聊天项目(2)- Json数据解析
- Android基于XMPP Smack Openfire下学习开发IM(四)单人聊天和多人聊天(发送消息、接收消息)
- Android 的开源电话/通讯/IM聊天项目全集
- Android-IM从零开始开发一个即时通讯项目