android 局域网聊天工具(可发送文字/语音)
最近比較有空,花了點時間寫了個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
請注意類里面每個方法的權限(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 方法發送出去。
用到一個工具類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類對象。
總結
以上是生活随笔為你收集整理的android 局域网聊天工具(可发送文字/语音)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决讯飞语音唤醒参数无效(错误码:101
- 下一篇: TI PMP解决方案简介