Android最小局域網(wǎng)單對單TCP實現(xiàn)

1. 背景

今日需要實現(xiàn)一個局域網(wǎng)的wifi數(shù)據(jù)傳輸?shù)墓δ芗奶洹2豢杀苊獾亩鞴唬M行TCP socket的操作背率。由于邏輯比較簡單话瞧,就使用最小化的Socket通信即可。

2. 目標

實現(xiàn)一個TCP模塊寝姿,供業(yè)務層調(diào)用交排。業(yè)務層把要發(fā)送的數(shù)據(jù)和發(fā)送的目標給到TCP模塊,TCP模塊完成傳輸饵筑,并將傳輸狀態(tài)和傳輸結(jié)果反饋給業(yè)務層

3. 需求分析

需要一個類來封裝所有的TCP操作埃篓,我們定義為ChannelTransport

接口:

  1. 啟動網(wǎng)絡連接
    startTcpService()
  2. 關閉網(wǎng)絡連接
    stopTcpService()
  3. 網(wǎng)絡聯(lián)通
    onConnected()
  4. 網(wǎng)絡連接失敗
    onConnectFail()
  5. 網(wǎng)絡聯(lián)通的情況下,一端close根资,另外一端收到-1
    onConnectEnd()
  6. 斷網(wǎng)
    onConnectException()
  7. 發(fā)送數(shù)據(jù)
    sendByte(Byte[] datas)
  8. 收到數(shù)據(jù)
    onRead(Byte[] datas)

4. TCP模塊封裝

4.1 接口層 IfSocket

接口層主要就是一接口定義:如打開socket,關閉socket架专,發(fā)送數(shù)據(jù),接收數(shù)據(jù)玄帕,連接狀態(tài)監(jiān)聽部脚,數(shù)據(jù)監(jiān)聽

public interface IfSocket {
    
    public void start();
    public void sendTo(byte[] var1);
    public void receive();
    public void stop();
    public void setConnectEventListener(SocketConnectEventListener connectEventListener);
    public void setReadStreamListener(OnStreamListener onReadStreamListener);

    public static interface SocketConnectEventListener {
        /**
         * 用于Socket主線程,socket連接成功
         */
        public void onConnected();
        /**
         * 用于Socket主線程裤纹,socket連接失敗
         */
        public void onConnectFail();
        
        /**
         * 用于IOReadThread委刘,socket 傳輸過程中收到-1結(jié)束符,標志對方socket close或者關閉輸入
         */
        public void onConnectEnd(); 
        
        /**
         *  用于IOReadThread和IOWriteThread,socket 傳輸過程中的Io exception
         */
        public void onConnectException();
    }

    
/**
 * 用于IO Thread 钱雷,一次socket傳輸接收到的數(shù)據(jù)
 * @author xuqiang
 *
 */
    public static interface OnStreamListener {
        public void onRead(byte[] var1);
        public void onSent();
    }


}

4.2 Socket端的具體實現(xiàn)

幾個注意點

  1. start要分server和client兩種情況
  2. IO線程用線程池實現(xiàn)TcpWriteIORunnable TcpReadIORunnable
  3. 設計一個心跳包線程TcpWriteAliveRunable骂铁,在當前沒有send數(shù)據(jù)的情況下,循環(huán)send心跳包
public class TcpSocket implements IfSocket {
    boolean isServer = true; //是不是Server
    String ipAddress;                  //Server的IP罩抗,給client用于connect的
    protected ExecutorService mThreadPool; //線程池拉庵,用于新建receive和send線程
    protected ScheduledExecutorService mScheduledThreadpool; //Timer線程池,用于發(fā)送心跳包
    protected int mState; //當前的狀態(tài)
    protected Socket mSocket; 
    protected ServerSocket mServerSocket;
    protected SocketConnectEventListener mConnectEventListener;
    protected OnStreamListener mOnStreamListener;
    private InputStream mInStream;
    private OutputStream mOutStream;
    public static final byte[] SEND_TAG = new byte[] { -5, -17, -13, -19 }; //數(shù)據(jù)頭部套蒂,用于數(shù)據(jù)校驗
    public static final byte[] SEND_ALIVE_TAG = new byte[] { -25, -31, -37, -43 }; //心跳包
    protected TcpWriteAliveRunable mTcpWriteAliveRunable;  //心跳包的task

    public TcpSocket(boolean isServer, String ipAddress) {
        super();
        this.isServer = isServer;
        this.ipAddress = ipAddress;
    }

    @Override
    public void setConnectEventListener(
            SocketConnectEventListener connectEventListener) {
        this.mConnectEventListener = connectEventListener;
    }

    @Override
    public void setReadStreamListener(OnStreamListener onReadStreamListener) {
        this.mOnStreamListener = onReadStreamListener;
    }

    @Override
    public void start() {
        this.mThreadPool = Executors.newCachedThreadPool();
        this.mScheduledThreadpool = Executors.newScheduledThreadPool(1);
        this.mTcpWriteAliveRunable = new TcpWriteAliveRunable(
                mOutStream, mConnectEventListener);
        try {
            if (isServer) {
                mServerSocket = new ServerSocket(TcpVar.PORT);
                this.mSocket = this.mServerSocket.accept();
            } else {
                this.mSocket = new Socket(ipAddress, TcpVar.PORT);
            }
            mState = TcpVar.STATE_CONNECTED;
            mConnectEventListener.onConnected();
            Dbg.i(TcpVar.TAG, " create socket sucess");
            mSocket.setSoTimeout(20000); // 加入超時
            mScheduledThreadpool.scheduleAtFixedRate(mTcpWriteAliveRunable, 4, 4, TimeUnit.SECONDS);
        } catch (Exception e) {
            mState = TcpVar.STATE_CONNECT_FAIL;
            mConnectEventListener.onConnectFail();
            Dbg.w(TcpVar.TAG, " create socket failed", e);
        }

    }

    @Override
    public void receive() {
        if (mState != TcpVar.STATE_CONNECTED) {
            return;
        }
        try {
            mInStream = new BufferedInputStream(this.mSocket.getInputStream());
        } catch (IOException e) {
            mInStream = null;
        }
        mThreadPool.execute(new TcpReadIORunnable(mInStream,
                mConnectEventListener, mOnStreamListener));
    }

    @Override
    public void sendTo(byte[] var1) {
        if (mState != TcpVar.STATE_CONNECTED) {
            return;
        }
        try {
            mOutStream = new BufferedOutputStream(
                    this.mSocket.getOutputStream());
        } catch (IOException e) {
            mOutStream = null;
        }

        try {
            //發(fā)送時阻塞當前線程钞支,心跳包暫停發(fā)送,發(fā)送完畢后操刀,心跳包重新發(fā)送
            mScheduledThreadpool.shutdownNow();
            mThreadPool.submit(new TcpWriteIORunnable(mOutStream,
                    mConnectEventListener, mOnStreamListener,var1)).get();
            mScheduledThreadpool.scheduleAtFixedRate(mTcpWriteAliveRunable, 4, 4, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        };

    }

    @Override
    public void stop() {
        mThreadPool.shutdownNow();
        mScheduledThreadpool.shutdownNow();
        try {
            mSocket.close();
            mServerSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        

    }

}

4.3 TcpWriteIoRunable的實現(xiàn)

數(shù)據(jù)格式很簡單

  1. SEND_TAG
  2. data_length
  3. data
public class TcpWriteIORunnable implements Runnable {
    OutputStream mOutStream;
    SocketConnectEventListener mConnectEventListener;
    OnStreamListener mOnStreamListener;
    byte[] data;

    public TcpWriteIORunnable(OutputStream mOutStream,
            SocketConnectEventListener mConnectEventListener,
            OnStreamListener mOnStreamListener, byte[] datas) {
        this.mOutStream = mOutStream;
        this.mConnectEventListener = mConnectEventListener;
        this.mOnStreamListener = mOnStreamListener;
        this.data = data;
    }

    @Override
    public void run() {
        try {
            mOutStream.write(TcpSocket.SEND_TAG);
            mOutStream.write(Util.int2bytes(this.data.length));
            mOutStream.write(this.data);
            mOutStream.flush();
            mOnStreamListener.onSent();
        } catch (Exception e) {
            mConnectEventListener.onConnectException();
        }

    }
}

4.4 TcpWriteAliveRunable的實現(xiàn)

心跳包的設計非常簡單烁挟,就是循環(huán)發(fā)送SEND_ALIVE_TAG

mScheduledThreadpool.scheduleAtFixedRate(mTcpWriteAliveRunable, 4, 4, TimeUnit.SECONDS);

public class TcpWriteAliveRunable implements Runnable {
    OutputStream mOutStream;
    SocketConnectEventListener mConnectEventListener;
    
    public TcpWriteAliveRunable(OutputStream mOutStream,
            SocketConnectEventListener mConnectEventListener) {
        super();
        this.mOutStream = mOutStream;
        this.mConnectEventListener = mConnectEventListener;
    }

    @Override
    public void run() {
        try{
            mOutStream.write(TcpSocket.SEND_ALIVE_TAG);
        }
         catch (Exception e) {
                mConnectEventListener.onConnectException();
            }
    }

}

4.5 TcpReadIORunnable的實現(xiàn)

Read線程的流程主要分三步:

  1. 校驗SEND_TAG。校驗的過程中我們是一個字節(jié)一個字節(jié)的校驗
  2. 第二步還是在讀取數(shù)據(jù)長度
  3. 第三步就是讀取真正的數(shù)據(jù)了骨坑。有三種策略讀數(shù)據(jù):
    1. 一個byte一個byte的讀撼嗓,這樣效率較低
    2. mmInStream.read(len)。但是InputStream.read(len)有個問題就是欢唾,他可能實際讀取的長度是小于len的且警。這個len是數(shù)據(jù)讀取的最大值,所以也不能直接使用;
    3. 我的算法是:mmInStream.read(len)礁遣,每次記錄已經(jīng)read的數(shù)據(jù)量斑芜,然后通過len-readBytes得到還剩下的數(shù)據(jù)長度,然后依次循環(huán)讀取祟霍,直到數(shù)據(jù)量讀滿len或者read==-1(斷網(wǎng))為止杏头。
public class TcpReadIORunnable implements Runnable {

    private boolean isStoped = false;
    InputStream mInStream;
    SocketConnectEventListener mConnectEventListener;
    OnStreamListener mOnReadStreamListener;

    public TcpReadIORunnable(InputStream mInStream,
            SocketConnectEventListener mConnectEventListener,
            OnStreamListener mOnReadStreamListener) {
        this.mInStream = mInStream;
        this.mConnectEventListener = mConnectEventListener;
        this.mOnReadStreamListener = mOnReadStreamListener;
    }

    @Override
    public void run() {
        int i = 0;
        ByteBuffer errorByteBuffer = ByteBuffer.allocate(1024 * 16);
        while (!this.isStoped) {
            try {
                // 1.判斷起始標記 start
                int t = this.mInStream.read();
                if (t == -1) {
                    Dbg.e(TcpVar.TAG, "read stream is -1!!!!!!!"); // 網(wǎng)絡一旦斷了,或者一端關閉沸呐,則出循環(huán)醇王,結(jié)束io線程
                    mConnectEventListener.onConnectEnd();
                    break;
                }
                Dbg.d(TcpVar.TAG, "mmInStream.read() one sucess ");
                byte b = (byte) (t & 0xFF);
                if (TcpSocket.SEND_TAG[i] != b) {
                    errorByteBuffer.put(b);
                    Dbg.e(TcpVar.TAG,
                            "!read byte error i:"
                                    + i
                                    + "  b:"
                                    + EncrypUtil
                                            .byteArrayToHexStr(new byte[] { b })
                                    + "  tag:"
                                    + EncrypUtil
                                            .byteArrayToHexStr(new byte[] { TcpSocket.SEND_TAG[i] }));
                    i = 0;
                    continue;
                }
                i++;
                if (i != TcpSocket.SEND_TAG.length) {
                    continue;//繼續(xù)讀下一個數(shù)據(jù),直到SEND_TAG讀完
                }
                i = 0;//到此處全部SEND_TAG全部讀完
                //下面是數(shù)據(jù)的打印垂谢,用于調(diào)試
                if (errorByteBuffer.position() != 0) {
                    byte[] dst = new byte[errorByteBuffer.position()];
                    errorByteBuffer.position(0);
                    errorByteBuffer.get(dst, 0, dst.length);
                    errorByteBuffer.clear();
                    Dbg.e(TcpVar.TAG,
                            "!read byte error data:"
                                    + EncrypUtil.byteArrayToHexStr(dst));
                }

                errorByteBuffer.clear();
                // 2.讀取包長度
                byte[] len = new byte[4];
                for (int j = 0; j < len.length; j++) {
                    len[j] = (byte) (this.mInStream.read() & 0xFF);
                }

                // mmInStream.read(len);
                int length = Util.bytes2int(len);
                // Dbg.d("read length:"+length);
                byte[] data = new byte[length];
                Dbg.e(TcpVar.TAG, "start read data,length =  " + length);
                // 3. 讀取數(shù)據(jù)

                int readBytes = 0;
                while (readBytes < data.length) {
                    int read = mInStream.read(data, readBytes, data.length
                            - readBytes);
                    if (read == -1) {
                        break;
                    }
                    readBytes += read;
                }

                mOnReadStreamListener.onRead(data);
                Dbg.d("read byte end!!!!!!!");
            } catch (Exception e) {
                Dbg.e("WifiTransferService",
                        "Fail to read bytes from input stream of Wifiiothread "
                                + e.getMessage(), e.getMessage());
                mConnectEventListener.onConnectException();
                return;
            }

        }
    }


}

5.5 Android業(yè)務層調(diào)用的注意事項厦画。

  1. 將ChannelTransport中使用TcpSocket做相應的操作疮茄,并且實現(xiàn)OnStreamListener和SocketConnectEventListener滥朱,即可。
  2. Socket的開啟/關閉/發(fā)送/接收力试,以及OnStreamListener和SocketConnectEventListener的回調(diào)都是在不同的線程中工作的徙邻。為了保證線程同步問題,我們需要使用一個HandlerThread畸裳,并將所有的callback讓HandlerThread去處理缰犁;然后使用ChannelTransport去extends Handler或者再新建一個Handler與這個HandlerThread對應起來。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市帅容,隨后出現(xiàn)的幾起案子颇象,更是在濱河造成了極大的恐慌,老刑警劉巖并徘,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件遣钳,死亡現(xiàn)場離奇詭異,居然都是意外死亡麦乞,警方通過查閱死者的電腦和手機蕴茴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姐直,“玉大人倦淀,你說我怎么就攤上這事∩罚” “怎么了撞叽?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長插龄。 經(jīng)常有香客問我能扒,道長,這世上最難降的妖魔是什么辫狼? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任初斑,我火速辦了婚禮,結(jié)果婚禮上膨处,老公的妹妹穿的比我還像新娘见秤。我一直安慰自己,他們只是感情好真椿,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布鹃答。 她就那樣靜靜地躺著,像睡著了一般突硝。 火紅的嫁衣襯著肌膚如雪测摔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天解恰,我揣著相機與錄音锋八,去河邊找鬼。 笑死护盈,一個胖子當著我的面吹牛挟纱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播腐宋,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼紊服,長吁一口氣:“原來是場噩夢啊……” “哼檀轨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起欺嗤,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤参萄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后煎饼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拧揽,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年腺占,在試婚紗的時候發(fā)現(xiàn)自己被綠了淤袜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡衰伯,死狀恐怖铡羡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情意鲸,我是刑警寧澤烦周,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站怎顾,受9級特大地震影響读慎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜槐雾,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一夭委、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧募强,春花似錦株灸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鸠儿,卻和暖如春屹蚊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背进每。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工汹粤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人品追。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓玄括,卻偏偏與公主長得像冯丙,于是被迫代替她去往敵國和親肉瓦。 傳聞我的和親對象是個殘疾皇子遭京,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 大綱 一.Socket簡介 二.BSD Socket編程準備 1.地址 2.端口 3.網(wǎng)絡字節(jié)序 4.半相關與全相...
    VD2012閱讀 2,274評論 0 5
  • (一)Java部分 1、列舉出JAVA中6個比較常用的包【天威誠信面試題】 【參考答案】 java.lang;ja...
    獨云閱讀 7,071評論 0 62
  • 之前去過一些公司做筆試題泞莉,排序算是比較基礎的知識了哪雕,當時要求用JavaScript寫出快速排序,當時不會就用Jav...
    BrianAguilar閱讀 514評論 0 1
  • 周六上午兩個孩子出門學畫畫鲫趁,整好我去聽武校長教育講座斯嚎。 聽完后真是受益匪淺,明白了平時自己怎么都想不透的問題挨厚!也學...
    要改掉壞脾氣的媽媽閱讀 234評論 0 0
  • 轉(zhuǎn)眼間堡僻,已經(jīng)到了2017年的深秋季節(jié)了!決定回憶一下往昔疫剃!記錄一下過往钉疫!小女子出生于1994.12.02。印象深刻...
    f75637cd56f2閱讀 387評論 0 0