最近比较有空,花了点时间写了个android局域网聊天工具,使用java的异步tcp通信。基本功能实现(简单的界面,聊天记录,发送文字,发送语音),在此小结一下。

Java (非android)局域网聊天工具源码,跟android的差别不大,参考:

http://download.csdn.net/detail/yarkey09/7052573

0,整个程序源码结构

1,聊天功能 (ServerSocketChannel & SocketChannel)

实现这个功能的时候有一个非常大的感受,就是写java程序真是方便!因为自己以前就写过windows上的java异步socket通信程序,所以这次几乎不需要修改很多代码,就可以搬过来。颇有Write one, run everywhere的feel。

个人认为java.nio的核心就是Selector和Buffer吧。通过Selector轮询各个已注册的socket的事件。若没有事件,则阻塞,若有事件则返回。因为在android,主线程不能做太多事情,

所以我起了一个新的线程,让Selector自个儿跑去。

以下是TcpWorkerThread类的源码,主要完成三件事

1,"开启"一个Selector

2,提供registToSelector方法

3,处理客户端的连接事件,处理socket接收消息事件

Class : TcpWorkerThread

package com.yarkey.tcp;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.Iterator;import java.util.Set;import android.os.Handler;import android.util.Log;public class TcpWorkerThread extends Thread {private static final String TAG = "TcpWorkerThread";/** 出错!返回String,描述出错原因 */public static final int EVENT_ERROR = 0;/** 线程结束, 停止运行 */public static final int EVENT_STOPPED = 1;/** 收到来自客户端的tcp连接, 报告一个socketchannel */public static final int EVENT_ACCEPTED = 2;/** 收到来自客户端的tcp消息, 报告一个TcpArgs, content为FileSerial对象 */public static final int EVENT_RECEIVED = 3;/** SocketChannel,ServerSocketChannel关闭 */public static final int EVENT_CLOSED = 4;/** TCP线程往主线程通信 */private Handler mHandler;private Selector mSelector;private boolean mIsRun = true;protected static class TcpArgs {SocketChannel sc;Object content;// 接收消息}/** * 如果抛出异常,不能进行异步通信了 *  * @throws Exception */public TcpWorkerThread(Handler handler) throws Exception {Log.d(TAG, "TcpWorkerThread contructor");if (handler == null) {throw new Exception("Handler is null!");} else {mHandler = handler;}mSelector = Selector.open();}/** * 将一个服务端的ServerSocketChannel设置为非阻塞模式,并将其注册到selector中(OP_ACCEPT) *  * @param ssc * @throws IOException */public void registToSelector(ServerSocketChannel ssc) throws IOException {Log.d(TAG, "registToSelector, ServerSocketChannel");ssc.configureBlocking(false);mSelector.wakeup();ssc.register(mSelector, SelectionKey.OP_ACCEPT);}/** * 将一个客户端的SocketChannel设置为非阻塞模式,并将其注册到selector中(OP_READ) *  * @param ss * @throws IOException */public void registToSelector(SocketChannel ss) throws IOException {Log.d(TAG, "registToSelector, SocketChannel");ss.configureBlocking(false);mSelector.wakeup();ss.register(mSelector, SelectionKey.OP_READ);}/** * 停止线程运行 */public void stopWorkerThread() {Log.d(TAG, "stopWorkerThread");mIsRun = false;mSelector.wakeup();}@Overridepublic void run() {// TODO Auto-generated method stubLog.d(TAG, "线程开始运行,run()");// 用于装入接收到的数据ByteBuffer buffer = ByteBuffer.allocate(1024);while (mIsRun) {int events = 0;try {events = mSelector.select();} catch (IOException e1) {// TODO Auto-generated catch blocke1.printStackTrace();mHandler.obtainMessage(EVENT_ERROR, "Selector IOException").sendToTarget();// 出错break;}if (events <= 0) {// 走到这里,只能说明被wakeup了,应该是别的地方需要,因此这里暂停100msLog.d(TAG, "sleep 100 ms >>>");try {sleep(100);} catch (InterruptedException e) {// interrupt! ignore thise.printStackTrace();}Log.d(TAG, "sleep 100 ms <<< wake up.");continue;}Log.d(TAG, "mSelector.select(), events ===========================> " + events);Set<SelectionKey> selectionKeys = mSelector.selectedKeys();Iterator<SelectionKey> iter = selectionKeys.iterator();// 代表连接成功后的socketSocketChannel socketChannel;while (iter.hasNext()) {SelectionKey key = iter.next();socketChannel = null;// 服务端收到连接if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {ServerSocketChannel ssc = (ServerSocketChannel) key.channel();try {socketChannel = ssc.accept();Log.d(TAG, "ssc.accept()");} catch (IOException e) {e.printStackTrace();try {ssc.close();} catch (IOException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}mHandler.obtainMessage(EVENT_CLOSED, ssc).sendToTarget();}if (socketChannel != null) {try {socketChannel.configureBlocking(false);socketChannel.register(mSelector, SelectionKey.OP_READ);Log.d(TAG, "来自客户端的新连接");mHandler.obtainMessage(EVENT_ACCEPTED, socketChannel).sendToTarget();} catch (IOException e) {e.printStackTrace();try {socketChannel.close();} catch (IOException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}mHandler.obtainMessage(EVENT_CLOSED, socketChannel).sendToTarget();}} else {Log.e(TAG, "socketChannel is null !");}iter.remove();}// 接收到客户的消息else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {socketChannel = (SocketChannel) key.channel();Log.d(TAG, "接收到新消息");boolean hasException = false;ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();while (true) {// 把position设为0,把limit设为capacitybuffer.clear();int a = 0;try {a = socketChannel.read(buffer);} catch (Exception e) {e.printStackTrace();try {socketChannel.close();} catch (IOException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}hasException = true;mHandler.obtainMessage(EVENT_CLOSED, socketChannel).sendToTarget();break;}Log.d(TAG, "a=" + a);if (a == 0) {break;}if (a == -1) {try {socketChannel.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}hasException = true;mHandler.obtainMessage(EVENT_CLOSED, socketChannel).sendToTarget();Log.w(TAG, "读取到EOS,我们关闭了一个连接!");break;}if (a > 0) {buffer.flip();try {byteOutput.write(buffer.array());} catch (IOException e) {// TODO Auto-generated catch blockhasException = true;e.printStackTrace();}}}if (!hasException) {byte[] b = byteOutput.toByteArray();TcpArgs args = new TcpArgs();args.sc = socketChannel;args.content = SerialUtil.toObject(b);mHandler.obtainMessage(EVENT_RECEIVED, args).sendToTarget();}iter.remove();}}}// 关闭资源try {mSelector.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}Log.d(TAG, "线程结束运行");mHandler.sendEmptyMessage(EVENT_STOPPED);}}

一般我们需要对socket处理的事件应该有三个:来自客户端的连接(accept),接收消息(read),发送消息(write)。TcpWorkerThread完成了前两件事,至于发送消息,我在另外一个类里面完成,也是新起一个线程,不过消息发送完后,发送线程也就停止了。

以下TcpManager类有几个特点:

1,静态单例

2,拥有一个TcpWorkerThread对象

3,拥有当前所有连接成功的socket

4,具有“新建服务端”“新建客户端”“发送消息”方法

5,采用register/notify机制,提供注册监听的方法

6,处理两个特殊的TCP事件( socket连接后,还需要双方互发昵称,才算聊天建立成功;如果接收到音频文件,需要保存到SD卡中;)

Class : TcpManager

package com.yarkey.tcp;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.ArrayList;import android.os.Handler;import android.os.Message;import android.util.Log;import com.yarkey.groupchat.Global;import com.yarkey.tcp.TcpWorkerThread.TcpArgs;import com.yarkey.utils.RegistrantList;/** * 拥有TcpWorkerThread *  * @author yeqi.zhang *  */public class TcpManager {private static final String TAG = "TcpManager";private static TcpManager mInstance;// 单例private static final String mRecFolder = TcpConfig.TCP_REC_FOLDER;// 保存文件的路径private TcpWorkerThread mThread;// 异步线程private ServerSocketChannel mServerSocketChannel;private ArrayList<SocketChannel> mSocketChannelList;// 保存连接的socketChannelprivate ArrayList<String> mNameList;// 保存对方名字public static class MessageArgs {public String name;// 对方的昵称public String content;// 文件路径,或者消息内容}protected RegistrantList mRegistrantListConnect = new RegistrantList();protected RegistrantList mRegistrantListReceiveAudio = new RegistrantList();protected RegistrantList mRegistrantListReceiveText = new RegistrantList();protected RegistrantList mRegistrantListError = new RegistrantList();public ServerSocketChannel getServerSocketChannel() {return mServerSocketChannel;}public ArrayList<SocketChannel> getSocketChannelList() {return mSocketChannelList;}public ArrayList<String> getNameList() {return mNameList;}/** 连接建立成功, obj=name */public void registerForConnect(Handler h, int what, Object obj) {mRegistrantListConnect.addUnique(h, what, obj);}public void unRegisterForConnect(Handler h) {mRegistrantListConnect.remove(h);}/** 收到音频消息, obj=MessageArgs */public void registerForReceiveAudio(Handler h, int what, Object obj) {mRegistrantListReceiveAudio.addUnique(h, what, obj);}public void unRegisterForReceiveAudio(Handler h) {mRegistrantListReceiveAudio.remove(h);}/** 收到文字消息, obj=MessageArgs */public void registerForReceiveText(Handler h, int what, Object obj) {mRegistrantListReceiveText.addUnique(h, what, obj);}public void unRegisterForReceiveText(Handler h) {mRegistrantListReceiveText.remove(h);}/** 有错误发生, obj=errorReason */public void registerForError(Handler h, int what, Object obj) {mRegistrantListError.addUnique(h, what, obj);}public void unRegisterForError(Handler h) {mRegistrantListError.remove(h);}/** * 如果TcpWorkerThread没有初始化成功,返回null ! *  * @return */public static TcpManager getInstance() {Log.d(TAG, "getInstance");if (mInstance == null) {mInstance = new TcpManager();}if (mInstance.mThread == null) {Log.e(TAG, "TcpWorkerThread 初始化不成功,这是致命的错误!");return null;}return mInstance;}public static void release() {Log.d(TAG, "release");mInstance.mThread.stopWorkerThread();// 释放监听的socketif (mInstance.mServerSocketChannel != null) {try {mInstance.mServerSocketChannel.close();Log.d(TAG, "release ServerSocketChannel!");} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}// 释放连接的socketint i = 0;for (; i < mInstance.mSocketChannelList.size(); i++) {try {mInstance.mSocketChannelList.get(i).close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}Log.i(TAG, "release " + (i - 1) + " SocketChannels!");}private TcpManager() {Log.d(TAG, "TcpManager private contructor");mSocketChannelList = new ArrayList<SocketChannel>();mNameList = new ArrayList<String>();try {mThread = new TcpWorkerThread(mThreadEventHandler);mThread.start();} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();mThread = null;// 这个会导致getInstance 返回null}}// public void setHandler(Handler h) {// Log.d(TAG, "setHandler,h=" + h);// mHandler = h;// }/** * 处理TcpWorkerThread */private Handler mThreadEventHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {Log.d(TAG, "mThreadEventHandler, handleMessage");// TODO Auto-generated method stubswitch (msg.what) {case TcpWorkerThread.EVENT_ACCEPTED:Log.d(TAG, "TcpWorkerThread.EVENT_ACCEPTED");sendName((SocketChannel) msg.obj);break;case TcpWorkerThread.EVENT_RECEIVED:Log.d(TAG, "TcpWorkerThread.EVENT_RECEIVED");TcpWorkerThread.TcpArgs argsRec = (TcpArgs) msg.obj;SocketChannel socketch = argsRec.sc;FileSerial objSerial = (FileSerial) argsRec.content;// 准备发送一个MessageArgs对象MessageArgs msgargs = null;if (objSerial.getType() != FileSerial.TYPE_NAME) {msgargs = new MessageArgs();int index = 0;for (; index < mSocketChannelList.size(); index++) {if (socketch.equals(mSocketChannelList.get(index))) {break;}}msgargs.name = mNameList.get(index);}switch (objSerial.getType()) {case FileSerial.TYPE_AUDIO:Log.d(TAG, "收到音频信息");FileOutputStream fileOut;try {fileOut = new FileOutputStream(mRecFolder + objSerial.getFileName());fileOut.write(objSerial.getFileContent(), 0, (int) objSerial.getFileLength());fileOut.close();Log.d(TAG, "音频文件已保存到本地!");} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}// if (mHandler != null) {// mHandler.obtainMessage(EVENT_REC_AUDIO, mRecFolder +// objSerial.getFileName()).sendToTarget();// }msgargs.content = mRecFolder + objSerial.getFileName();mRegistrantListReceiveAudio.notifyResult(msgargs);break;case FileSerial.TYPE_TEXT:Log.d(TAG, "收到文字信息");// if (mHandler != null) {// mHandler.obtainMessage(EVENT_REC_TEXT,// objSerial.getFileName()).sendToTarget();// }msgargs.content = objSerial.getFileName();Log.d(TAG, "message=" + objSerial.getFileName());mRegistrantListReceiveText.notifyResult(msgargs);break;case FileSerial.TYPE_NAME:Log.d(TAG, "收到对方名称,连接正式成功!");mSocketChannelList.add(argsRec.sc);mNameList.add(objSerial.getFileName());// if (mHandler != null) {// mHandler.obtainMessage(EVENT_CONNECT,// objSerial.getFileName()).sendToTarget();// }mRegistrantListConnect.notifyResult(objSerial.getFileName());break;}break;case TcpWorkerThread.EVENT_ERROR:Log.w(TAG, "error:" + (String) msg.obj);break;case TcpWorkerThread.EVENT_STOPPED:Log.w(TAG, "EVENT_STOPPED");break;// ---------------------------------------------case TcpAsyncClient.EVENT_CONNECTED:TcpAsyncClient.TcpArgs args = (TcpAsyncClient.TcpArgs) msg.obj;try {mThread.registToSelector(args.result);Log.d(TAG, "客户端连接服务端成功,发送昵称...");sendName(args.result);} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();this.sendEmptyMessage(TcpAsyncClient.EVENT_ERROR);}break;case TcpAsyncClient.EVENT_ERROR:Log.e(TAG, "客户端发起连接,发生错误!");// mHandler.obtainMessage(EVENT_ERROR,// "客户端发起连接失败").sendToTarget();mRegistrantListError.notifyResult("客户端发起连接失败");break;}}};/** * 作为客户端,连接到指定的地址。如果返回false,表示连接没成功,如果返回true,那么需要等待 *  * @deprecated android较高的版本,不允许在主线程访问network * @param ip * @param port * @return */public boolean newConnection(String ip, int port) {TcpClient client = new TcpClient(ip, port);SocketChannel sc = client.connect();try {mThread.registToSelector(sc);} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();return false;}return true;}/** * 作为客户端,异步创建连接 *  * @param ip * @param port */public void newAsyncConnection(String ip, int port) {Log.d(TAG, "newAsyncConnection, ip=" + ip + ",port=" + port);TcpAsyncClient client = new TcpAsyncClient();TcpAsyncClient.TcpArgs args = new TcpAsyncClient.TcpArgs();args.handler = mThreadEventHandler;args.ip = ip;args.port = port;client.connect(args);}/** * 作为服务端,启动服务监听 *  * @param port * @return */public boolean newServer(int port) {Log.d(TAG, "newServer, port=" + port);if (mServerSocketChannel != null) {// serversocketchannel 不一定注册到selector成功,但是这里暂时只能允许存在一个Log.w(TAG, "can only have one server for accepting !");} else {TcpServer server = new TcpServer(port);mServerSocketChannel = server.accepting();}try {mThread.registToSelector(mServerSocketChannel);} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();return false;}return true;}/** * 发送给对方,昵称/文字消息/文件 *  * @param sc *            SocketChannel * @param content *            name/message/filepath * @param fileName *            if type==AUDIO, you should set fileName * @param type *            {@link FileSerial#TYPE_TEXT}, {@link FileSerial#TYPE_NAME}, *            {@link FileSerial#TYPE_AUDIO} */private void send(final SocketChannel sc, String content, String fileName, int type) {Log.d(TAG, "content=" + content + ",fileName=" + fileName + ",type=" + type);FileSerial fpo = new FileSerial();// typefpo.setType(type);switch (type) {case FileSerial.TYPE_AUDIO:// namefpo.setFileName(fileName);// lengthFile f = new File(content);long fileLength = f.length();fpo.setFileLength(fileLength);// contentFileInputStream fis = null;byte[] fileContent = new byte[(int) fileLength];try {fis = new FileInputStream(content);fis.read(fileContent, 0, (int) fileLength);fis.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}fpo.setFileContent(fileContent);break;case FileSerial.TYPE_TEXT:case FileSerial.TYPE_NAME:// namefpo.setFileName(content);// lengthfpo.setFileLength(content.length());// content// nullbreak;}// toByte -> sendbyte[] bytes = SerialUtil.toByte(fpo);final ByteBuffer buffer = ByteBuffer.wrap(bytes);// E/AndroidRuntime(18606): android.os.NetworkOnMainThreadExceptionnew Thread() {@Overridepublic void run() {// TODO Auto-generated method stubLog.d(TAG, "send, async ! 异步发送 ... ");try {// 此处在主线程调用, 有可能会阻塞!sc.write(buffer);} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}.start();}/** * 发送音频文件 *  * @param index * @param content * @param fileName */public void sendAudioFile(int index, String content, String fileName) {Log.d(TAG, "sendAudioFile, index=" + index + ",content=" + content + ",fileName=" + fileName);SocketChannel sc = mSocketChannelList.get(index);send(sc, content, fileName, FileSerial.TYPE_AUDIO);}/** * 发送音频文件 *  * @param name * @param content * @param fileName */public void sendAudioFile(String name, String content, String fileName) {Log.d(TAG, "sendAudioFile, name=" + name + ",content=" + content + ",fileName=" + fileName);int index = -1;for (int i = 0; i < mNameList.size(); i++) {if (mNameList.get(i).equals(name)) {index = i;break;}}if (index != -1) {sendAudioFile(index, content, fileName);} else {Log.w(TAG, "sendAudioFile, name=" + name + " not found !");}}/** * 发送文字消息 *  * @param index * @param msg */public void sendMessage(int index, String msg) {Log.d(TAG, "sendMessage, index=" + index + ",msg=" + msg);SocketChannel sc = mSocketChannelList.get(index);send(sc, msg, null, FileSerial.TYPE_TEXT);}/** * 发送文字消息 *  * @param name * @param msg */public void sendMessage(String name, String msg) {Log.d(TAG, "sendMessage, name=" + name + ",msg=" + msg);int index = -1;for (int i = 0; i < mNameList.size(); i++) {if (mNameList.get(i).equals(name)) {index = i;break;}}if (index != -1) {sendMessage(index, msg);} else {Log.w(TAG, "sendMessage, name=" + name + " not found !");}}/** * TCP连接后,需要双方相互发送昵称,才能算连接正式成立! *  * @param sc * @param name */private void sendName(SocketChannel sc) {Log.d(TAG, "sendName, name=" + Global.NAME_LOCAL);send(sc, Global.NAME_LOCAL, null, FileSerial.TYPE_NAME);}}

请注意类里面每个方法的权限(private,protected,public),我把这个类作为整个聊天工具TCP的核心类,具有所有需要的TCP操作方法。

TcpManager中还用到TcpAsyncClient,TcpServer,这两个类完成ServerSocketChannel, SocketChannel的“打开”,打开完成后便可以将他们注册到Selector了。

Class : TcpAsyncClient

主要方法:由于较新版本的android,不允许在主线程做网络操作,所以以下代码在一个新的线程中执行!

SocketChannel sc = SocketChannel.open();sc.connect(new InetSocketAddress(args.ip, args.port));

Class : TcpServer

主要方法:

/** * 如果初始化不成功,返回null *  * @param port * @return */protected ServerSocketChannel accepting(int port) {Log.d(TAG, "accepting,port=" + port);ServerSocketChannel ssc = null;try {ssc = ServerSocketChannel.open();ssc.configureBlocking(false);ServerSocket ss = ssc.socket();ss.bind(new InetSocketAddress(port));} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();return null;}return ssc;}

TCP通信功能大概就这么多吧,另外有一个关于序列化的类FileSerial,在“语音发送与接收”再说一下。

2,聊天记录 (AsyncQueryHandler)

聊天记录使用android自带的Sqlite数据库做持久化。由于数据库访问可能会花很多时间,不宜在主线程操作,所以在这里主要关键考虑如果完成“异步”访问数据库。

android的AsyncQueryHandler类写得很好,我只是在它的基础上做了一点小修改就可以用了。拿来主义^_^

Class : ChatAsyncQueryHandler

package com.yarkey.database;import android.content.ContentValues;import android.database.Cursor;import android.os.Handler;import android.os.HandlerThread;import android.os.Looper;import android.os.Message;import android.util.Log;public abstract class ChatAsyncQueryHandler extends Handler {private static final String TAG = "ChatAsyncQueryHandler";private ChatDatabase mDatabase;private static Looper mLooper = null;private Handler mWorkerHandler = null;private static final int EVENT_QUERY = 0;private static final int EVENT_INSERT = 1;private static final int EVENT_UPDATE = 2;private static final int EVENT_DELECT = 3;public ChatAsyncQueryHandler(ChatDatabase database) {mDatabase = database;synchronized (ChatAsyncQueryHandler.class) {if (mLooper == null) {HandlerThread thread = new HandlerThread("ChatAsyncQueryHandler");thread.start();mLooper = thread.getLooper();}}mWorkerHandler = new WorkerHandler(mLooper);}protected static final class WorkerArgs {public Handler handler;public String[] projection;public String selection;public ContentValues values;public Object result;}/** * 查询。通过两个字段,查询ID,DATA,TIME,TYPE,SELF,CONTENT *  * @param token * @param localName * @param remoteName */public void startQuery(int token, String localName, String remoteName) {Log.d(TAG, "startQuery, token=" + token + ",localName=" + localName + ",remoteName=" + remoteName);Message msg = mWorkerHandler.obtainMessage(token);msg.arg1 = EVENT_QUERY;WorkerArgs args = new WorkerArgs();args.handler = this;args.projection = new String[] { ChatLog.C_ID, ChatLog.C_DATE, ChatLog.C_TIME, ChatLog.C_TYPE, ChatLog.C_SELF,ChatLog.C_CONTENT };args.selection = ChatLog.C_LOCAL + "=\"" + localName + "\" AND " + ChatLog.C_REMOTE + "=\"" + remoteName + "\"";msg.obj = args;mWorkerHandler.sendMessage(msg);}public void startInsert(int token, ContentValues values) {Log.d(TAG, "startInsert, values=" + values);Message msg = mWorkerHandler.obtainMessage(token);msg.arg1 = EVENT_INSERT;WorkerArgs args = new WorkerArgs();args.handler = this;args.values = values;msg.obj = args;mWorkerHandler.sendMessage(msg);}protected class WorkerHandler extends Handler {public WorkerHandler(Looper looper) {super(looper);// TODO Auto-generated constructor stub}@Overridepublic void handleMessage(Message msg) {// TODO Auto-generated method stubWorkerArgs args = (WorkerArgs) msg.obj;int token = msg.what;int event = msg.arg1;switch (event) {case EVENT_QUERY:args.result = mDatabase.query(args.projection, args.selection);break;case EVENT_INSERT:mDatabase.insert(args.values);break;case EVENT_UPDATE:break;case EVENT_DELECT:break;}Message reply = args.handler.obtainMessage(token);reply.obj = args;reply.arg1 = event;reply.sendToTarget();}}protected void onQueryCompleted(int token, Cursor cursor) {// empty}protected void onInsertCompleted(int token) {// empty}@Overridepublic void handleMessage(Message msg) {// TODO Auto-generated method stubWorkerArgs args = (WorkerArgs) msg.obj;int token = msg.what;int event = msg.arg1;switch (event) {case EVENT_QUERY:onQueryCompleted(token, (Cursor) args.result);break;case EVENT_INSERT:onInsertCompleted(token);break;case EVENT_UPDATE:break;case EVENT_DELECT:break;}}}


至于ChatDatabase类,继承于SQLiteOpenHelper,就不多说了。

3,语音发送与接收 (ObjectOutputStream)

本聊天工具通过发送音频文件,来实现语音聊天的。其他真正的聊天工具怎么实现的就不得而知了,有知道的网友也请分享一下^^。在这里主要需要解决几个问题:

1,录音与录音播放 (MediaRecorder, MediaPlayer)

MediaRecorder 与 MediaPlayer 的例子网上有很多了,本人也是一知半解,不敢在这里说太多。主要是通过MediaRecorder调用录音方法,结束后,我们得到一个存放在SD卡中的.3gp文件,有了这个文件,便可以调用MediaPlayer来播放它了!

可能需要注意的地方有几个:MediaRecorder 录音超时,系统是有一个上限的,设计程序的时候需要注意一下;另外,是关于MediaRecorder什么时候释放,在本程序里面,每次用的时候就new一个,每次用完都调用release方法释放它。不过,这里似乎需要考虑一些问题,就没有做过多了解了。

2,录音文件的发送与接收

TCP发送文件,一开始用java.io的时候,我用的是ObjectOutputStream, ObjectInputStream来序列化一个类然后发出去( 即writeObject方法 )。

writeObject 方法的输入参数是一个FileSerial对象,实现Serializable接口

Class : FileSerial

package com.yarkey.tcp;import java.io.Serializable;public class FileSerial implements Serializable {private static final long serialVersionUID = 1L;private String fileName; // 文件名称private long fileLength; // 文件长度private byte[] fileContent; // 文件内容private int type;/** name保存在filename字段里面! */protected static final int TYPE_NAME = 0;// 连接成功后,向对方发出自己的名字/** text保存在filename字段里面! */protected static final int TYPE_TEXT = 1;// 文字protected static final int TYPE_AUDIO = 2;// 音频// public static final int TYPE_PICTURE = 3;// 图片public int getType() {return type;}public void setType(int t) {type = t;}public String getFileName() {return fileName;}public void setFileName(String fileName) {this.fileName = fileName;}public long getFileLength() {return fileLength;}public void setFileLength(long fileLength) {this.fileLength = fileLength;}public byte[] getFileContent() {return fileContent;}public void setFileContent(byte[] fileContent) {this.fileContent = fileContent;}}

java.io发送FileSerial对象源码:其中:

ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());

/** *  * @param filePath *            C:/haha.java * @param fileName *            haha.java */public void sendFile(String filePath, String fileName) {Log.d(TAG, fileName);if (out == null) {return;}try {FileSerial fpo = new FileSerial();// typefpo.setType(FileSerial.TYPE_AUDIO);// namefpo.setFileName(fileName);// lengthFile f = new File(filePath);long fileLength = f.length();fpo.setFileLength(fileLength);// contentFileInputStream fis = new FileInputStream(filePath);byte[] fileContent = new byte[(int) fileLength];fis.read(fileContent, 0, (int) fileLength);fis.close();fpo.setFileContent(fileContent);// sendlong start = System.currentTimeMillis();out.writeObject(fpo);long end = System.currentTimeMillis();System.out.println("It takes " + (end - start) + "ms");out.flush();out.reset();} catch (FileNotFoundException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}


可是,用java.nio的时候,就会出错了!我们需要将需要发送的内容装在一个Buffer( ByteBuffer )中,然后通过SocketChannel的 write 方法发送出去。

// toByte -> sendbyte[] bytes = SerialUtil.toByte(fpo);final ByteBuffer buffer = ByteBuffer.wrap(bytes);// E/AndroidRuntime(18606): android.os.NetworkOnMainThreadExceptionnew Thread() {@Overridepublic void run() {// TODO Auto-generated method stubLog.d(TAG, "send, async ! 异步发送 ... ");try {// 此处不能在主线程调用!sc.write(buffer);} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}.start();


用到一个工具类SerialUtil,转换对象 FileSerial -> byte[] , byte[] -> FileSerial

class : SerialUtil

package com.yarkey.tcp;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import android.util.Log;public class SerialUtil {private static final String TAG = "SerialUtil";public static byte[] toByte(Object obj) {Log.d(TAG, "toByte");ByteArrayOutputStream baos = new ByteArrayOutputStream();ObjectOutputStream oos = null;try {oos = new ObjectOutputStream(baos);oos.writeObject(obj);byte[] bytes = baos.toByteArray();return bytes;} catch (IOException ex) {throw new RuntimeException(ex.getMessage(), ex);} finally {try {oos.close();} catch (Exception e) {}}}/** 此方法byte[]数组长度确定 */public static Object toObject(byte[] bytes) {Log.d(TAG, "toObject");ByteArrayInputStream bais = new ByteArrayInputStream(bytes);ObjectInputStream ois = null;try {ois = new ObjectInputStream(bais);Object object = ois.readObject();return object;} catch (IOException ex) {throw new RuntimeException(ex.getMessage(), ex);} catch (ClassNotFoundException ex) {throw new RuntimeException(ex.getMessage(), ex);} finally {try {ois.close();} catch (Exception e) {}}}}

音频文件的发送大概情况就是上面说的了,但是接收有另外一个问题。我们不知道音频文件的大小,所以不知道需要多大的ByteBuffer来接收它。

所以,在TcpWorkerThread中,处理接收到的消息时,还用到一个

ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();

for( 循环 ){

byteOutput.write(buffer.array()); // 分次保存byteBuffer中的数据

}

byte[] b = byteOutput.toByteArray(); //最后一口气弄出来


我们申请的bytebuffer就1024个字节的空间,超过1024个字节,所以我们分次“存”到这个ByteArrayOutputStream中,最后一口气全部弄出来。然后调用SerialUtil将byte[]装换成FileSerial类对象。

更多相关文章

  1. Android的调试原理--学习笔记
  2. Android(安卓)进程间通信(IPC)
  3. 和兄弟们聊内存的事
  4. cordova + ionic前端框架 js和android ios原生(native)交互
  5. android 开发中判断网络是否连接的代码
  6. 用网络adb连接调试Android
  7. Android下使用Socket连接网络电脑
  8. android 向web服务器发送post请求并获取结果
  9. 推送功能,(服务器向android客户端推送信息,通知显示在应用栏)(2018-0

随机推荐

  1. Android(安卓)欢迎界面淡出动画效果(Anim
  2. Android简单例子(拨号服务调用)
  3. TelephonyManager
  4. Android自定义view三验证码输入控件
  5. android 输入法界面显示的开关
  6. cocos2d-x-2.1.4在NDK r9下的编译问题
  7. 【Android开发小记--9】触摸事件---实现
  8. Android(安卓)导入项目出错
  9. Android(安卓)inject input events 注入T
  10. android 自定义PopupMenu详解