Android | TCP的C(Java|Android)/S(Java)通信實戰(zhàn)經(jīng)典聊天室案例(文末附本案例代碼實現(xiàn)概述倦踢、觀察者模式實現(xiàn)小結(jié))

案例GitHub地址


創(chuàng)建TCP服務(wù)端

  • 在sample模塊下朽褪,
    新建一個名為tcp的package,
    創(chuàng)建TcpServer:


  • 指定服務(wù)端端口號(ip 默認為本機ip)
    啟動循環(huán)讀取消息隊列的子線程无虚,
    死循環(huán)缔赠,不斷等待客戶端請求連接,
    一旦連接上骑科,
    直接新建一個子線程(丟給ClientTask)去處理這個socket橡淑,
    于是主線程又可以回到accept() 阻塞,等待下一個連接請求咆爽;
    同時梁棠,將連接上的socket 對應(yīng)的線程類置森,注冊為消息隊列的觀察者,
    讓線程類擔任觀察者符糊,負責接收被觀察者的通知信息并做socket 通信凫海。
/**
 * <pre>
 *     author : 李蔚蓬(簡書_凌川江雪)
 *     time   : 2019/10/30 16:57
 *     desc   :指定服務(wù)端端口號(ip 默認為本機ip)
 *             啟動循環(huán)讀取消息隊列的子線程,
 *             死循環(huán)男娄,不斷等待客戶端請求連接行贪,
 *             一旦連接上,直接新建一個子線程(丟給ClientTask)去處理這個socket模闲,
 *             于是主線程又可以回到accept() 阻塞建瘫,等待下一個連接請求;
 *             同時尸折,將連接上的socket 對應(yīng)的線程類啰脚,注冊為消息隊列的觀察者,
 *             讓線程類擔任觀察者实夹,負責接收被觀察者的通知信息并做socket 通信
 * </pre>
 */
public class TcpServer {

    public void start() {

        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(9090);
            MsgPool.getInstance().start();//啟動讀消息的子線程

            while (true) {
//            /*
//            阻塞的方法i吓ā!亮航!  等待(客戶端的) TCP 連接請求
//            客戶端有 TCP 請求并連接上了 ServerSocket荸实,.
//            那 accept() 就會返回一個 同一連接上 對應(yīng) 客戶一端socket 的 服務(wù)一端socket
//             */
                Socket socket = serverSocket.accept();

                //客戶端連接之后,打印相關(guān)信息
//            System.out.println("ip: " + socket.getInetAddress().getHostAddress() +
//                    ", port = " + socket.getPort() + " is online...");
                System.out.println("ip = " + "***.***.***.***" +
                        ", port = " + socket.getPort() + " is online...");

//            /*
//                連接上了之后不能直接拿IO流去讀寫缴淋,
//                因為getInputStream() 和 getOutputStream() 都是阻塞的W几!Q缁圆存!
//                如果直接拿IO 流,不做其他處理仇哆,
//                那么Server端的處理流程是這樣的:
//                accept()-- getInputStream()處理第一個客戶端 -- 處理完畢,accept()-- getInputStream()處理第二個客戶端....
//                所以必須開啟子線程去讀寫客戶端沦辙,才能做成聊天室
//
//                針對每一個連接上來的客戶端去單獨起一個線程,跟客戶端進行通信
//
//                過程:客戶端連上之后讹剔,打印其信息油讯,
//                然后直接新建一個子線程(丟給ClientTask)去處理這個socket,
//                于是主線程又可以回到accept() 阻塞延欠,等待下一個連接請求
//             */
                ClientTask clientTask = new ClientTask(socket);
                MsgPool.getInstance().addMsgComingListener(clientTask);
                clientTask.start();


            }


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new TcpServer().start();
    }
}
  • 針對每一個連接上來的客戶端去單獨起一個線程陌兑,跟客戶端進行通信,
    準備一個線程類,名為ClientTask由捎,
    針對每一個連接上來的客戶端去單獨起一個線程兔综,跟客戶端進行通信,
    這里便是線程類;
    run()中死循環(huán)不斷讀取本類實例對應(yīng)的客戶端發(fā)來的信息,
    或者發(fā)送給對應(yīng)的連接對面客戶端(服務(wù)端)要發(fā)送的信息软驰;
    實現(xiàn)MsgPool.MsgComingListener涧窒, 成為消息隊列的觀察者!6Э鳌纠吴!
/**
 * <pre>
 *     author : 李蔚蓬(簡書_凌川江雪)
 *     time   : 2019/10/30 17:23
 *     desc   :針對每一個連接上來的客戶端去單獨起一個線程,跟客戶端進行通信,
 *             這里便是線程類慧瘤;
 *             run()中死循環(huán)不斷讀取客戶端發(fā)來的信息戴已,發(fā)送給客戶端(服務(wù)端)要發(fā)送的信息;
 *             實現(xiàn)MsgPool.MsgComingListener锅减, 成為消息隊列的觀察者L抢堋!上煤!
 * </pre>
 */
public class ClientTask extends Thread implements MsgPool.MsgComingListener {

    private Socket mSocket;
    private InputStream mIs;
    private OutputStream mOs;

    public ClientTask(Socket socket) {

        try {
            mSocket = socket;
            mIs = socket.getInputStream();
            mOs = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void run() {
        BufferedReader br = new BufferedReader(new InputStreamReader(mIs));

        String line = null;
        /*
            讀取并輸出客戶端信息休玩。
            如果沒有客戶端發(fā)送信息著淆,readLine() 便會阻塞在原地
         */
        try {
            while ((line = br.readLine()) != null) {
                System.out.println("read " + mSocket.getPort() + " = " + line);
                //把信息發(fā)送加入到消息隊列劫狠,
                // 借助消息隊列的被觀察者通知方法,
                // 將消息轉(zhuǎn)發(fā)至其他Socket(所有socket都在創(chuàng)建ClientTask的時候永部,
                // 備注成為MsgPool 的觀察者)
                MsgPool.getInstance().sendMsg(mSocket.getPort() + ": " + line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    //作為消息隊列的觀察者對應(yīng)的更新方法,
    // 消息隊列中最新的消息會推送通知到這里的msg參數(shù)独泞,
    // 這里拿到最新的推送消息后,寫進輸出流苔埋,
    // 推到TCP 連接的客戶一端的 socket
    @Override
    public void onMsgComing(String msg) {
        try {
            mOs.write(msg.getBytes());
            mOs.write("\n".getBytes());
            mOs.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 準備一個消息隊列懦砂,
    每一個Client發(fā)送過來的消息,
    都會被加入到隊列當中去组橄,
    隊列中默認有一個子線程荞膘,
    專門從隊列中,死循環(huán)玉工,不斷去取數(shù)據(jù)(取出隊列的隊頭)羽资,
    取到數(shù)據(jù)就做相關(guān)處理,比如分發(fā)給其他的socket遵班;
/**
 * <pre>
 *     author : 李蔚蓬(簡書_凌川江雪)
 *     time   : 2019/10/30 17:45
 *     desc   :每一個Client發(fā)送過來的消息屠升,
 *             都會被加入到隊列當中去,
 *             隊列中默認有一個子線程狭郑,
 *             專門從隊列中腹暖,死循環(huán),不斷去取數(shù)據(jù)翰萨,
 *             取到數(shù)據(jù)就做相關(guān)處理脏答,比如分發(fā)給其他的socket;
 * </pre>
 */
public class MsgPool {

    private static MsgPool mInstance = new MsgPool();

    /*
        這里默認消息是String類型,
        或者可以自行封裝一個Model 類殖告,存儲更詳細的信息

        block n.塊糙麦; 街區(qū);障礙物丛肮,阻礙
        顧名思義赡磅,這是一個阻塞的隊列,當有消息過來時宝与,就把消息發(fā)送給這個隊列焚廊,
        這邊會起一個線程專門從隊列里面去取消息,
        如果隊列中沒有消息习劫,就會阻塞在原地
     */
    private LinkedBlockingQueue<String> mQueue = new LinkedBlockingQueue<>();

    public static MsgPool getInstance() {
        return mInstance;
    }

    private MsgPool() {
    }

    //這是一個阻塞的隊列咆瘟,
    // 當有消息過來時,即客戶端接收到消息時诽里,
    // 就把消息發(fā)送(添加)到這個隊列中
    //現(xiàn)在所有的客戶端都可以發(fā)送消息到這個隊列中
    public void sendMsg(String msg) {
        try {
            mQueue.put(msg);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    //要一早就調(diào)用本方法袒餐,
    // 啟動這個讀取消息的線程,在后臺不斷運行
    public void start() {
        //開啟一個線程去讀隊列的數(shù)據(jù)
        new Thread() {
            @Override
            public void run() {
                //無限循環(huán)讀取信息
                while (true) {
                    try {
                        //取出并移除隊頭谤狡;沒有消息時灸眼,take()是阻塞的
                        String msg = mQueue.take();
                        notifyMsgComing(msg);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }

    //被觀察者方法,遍歷所有已注冊的觀察者墓懂,一次性通知更新
    private void notifyMsgComing(String msg) {
        for (MsgComingListener listener : mListeners) {
            listener.onMsgComing(msg);
        }
    }

    //觀察者接口
    public interface MsgComingListener {
        void onMsgComing(String msg);//更新方法
    }

    //被觀察者焰宣,存放觀察者
    private List<MsgComingListener> mListeners = new ArrayList<>();

    //被觀察者方法,添加觀察者到列表
    public void addMsgComingListener(MsgComingListener listener) {
        mListeners.add(listener);
    }
}

所有的客戶端都可發(fā)送消息到隊列中捕仔,
然后所有的客戶端都在等待
消息隊列的消息新增(mQueue.put())這個時刻匕积,
消息隊列一新增消息,
即一接收到某個客戶端發(fā)送過來消息(mQueue.put())榜跌,
則消息都會一次性轉(zhuǎn)發(fā)給所有客戶端闪唆,
所以這里涉及到一個觀察者設(shè)計模式,
消息隊列(MsgPool)或消息(Msg)是被觀察者钓葫,
所有客戶端處理線程(ClientTask)都是觀察者

觀察者模式實現(xiàn)小結(jié):

觀察者接口準備更新(數(shù)據(jù)或UI的)方法悄蕾;
被觀察者接口準備三個抽象方法;

觀察者實現(xiàn)類具體實現(xiàn)更新邏輯瓤逼,可以有參數(shù)笼吟,參數(shù)為更新需要的數(shù)據(jù);

被觀察者實現(xiàn)類準備一個觀察者List以及實現(xiàn)三個方法:
1.觀察者注冊方法:
參數(shù)為某觀察者霸旗,功能是把觀察者參數(shù)加到觀察者List中贷帮;
2.注銷觀察者方法:
參數(shù)為某觀察者,功能是把觀察者參數(shù)從觀察者List中移除诱告;
3.通知觀察者方法:無參數(shù)或者把需要通知的數(shù)據(jù)作為參數(shù)撵枢,
功能是遍歷所有已注冊的觀察者,
即遍歷 注冊添加到 觀察者List中的觀察者,逐個調(diào)用List中所有觀察者的更新方法锄禽;即一次性更新所有已注冊的觀察者潜必!

使用時,
實例化一個被觀察者和若干個觀察者沃但,
將所有觀察者注冊到被觀察者處磁滚,
調(diào)用被觀察者的通知方法,一次性更新所有已注冊的觀察者宵晚!

創(chuàng)建TCP客戶端

  • 創(chuàng)建兩個Package垂攘,整理一下項目架構(gòu),創(chuàng)建一個TcpClient:
/**
 * <pre>
 *     author : 李蔚蓬(簡書_凌川江雪)
 *     time   : 2019/10/31 15:36
 *     desc   :
 * </pre>
 */
public class TcpClient {

    private Scanner mScanner;

    public TcpClient() {
        mScanner = new Scanner(System.in);
        mScanner.useDelimiter("\n");
    }

    /**
     * 配置socket
     * 準備IO 流淤刃,
     * 主線程寫晒他,子線程讀
     *
     */
    public void start() {
        try {
            Socket socket = new Socket("***", 9090);
            InputStream is = socket.getInputStream();
            OutputStream os = socket.getOutputStream();

            final BufferedReader br = new BufferedReader(new InputStreamReader(is));
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));

            /*
                實現(xiàn):
                通過 reader,
                在任何時候 能夠讀到 Server端 發(fā)來的數(shù)據(jù)
                通過 writer逸贾,
                在任何時候 能夠向 Server端 去寫數(shù)據(jù)
             */
            //在等待客戶端 發(fā)送消息過來的話陨仅,這里是需要阻塞的,
            // 阻塞的時候又沒有辦法向客戶端發(fā)送數(shù)據(jù)铝侵,所以讀寫?yīng)毩⒌脑捵粕耍隙ㄊ且鹁€程的

            //起一個線程,專門用于
            // 讀Server 端 發(fā)來的數(shù)據(jù)哟沫,數(shù)據(jù)一過來就讀然后輸出,
            // 輸出服務(wù)端發(fā)送的數(shù)據(jù)
            new Thread() {
                @Override
                public void run() {

                    try {
                        String line = null;
                        while ((line = br.readLine()) != null) {
                            System.out.println(line);
                        }
                    } catch (IOException e) {
                    }
                }
            }.start();

            //給Server端 發(fā)送數(shù)據(jù)
            while (true) {
                //next() 是阻塞的饺蔑,不斷地讀控制面板,有數(shù)據(jù)就會通過bufferWriter,
                // 即outputStream 寫給Server
                String msg = mScanner.next();
                bw.write(msg);
                bw.newLine();
                bw.flush();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new TcpClient().start();
    }
}
  • 反復(fù)測試:

移植客戶端到Android移動端

  • 復(fù)制TcpClient到biz包下迭代嗜诀,名為TcpClientBiz:
/**
 * <pre>
 *     author : 李蔚蓬(簡書_凌川江雪)
 *     time   : 2019/10/31 15:36
 *     desc   : 定義接口勤众,完成客戶端的收發(fā)邏輯
 * </pre>
 */
public class TcpClientBiz {

    private Socket mSocket;
    private InputStream mIs;
    private OutputStream mOs;

    /**
     * Looper.getMainLooper()绵估,將主線程中的 Looper 扔進去了,
     * 也就是說 handleMessage 會運行在主線程中亲族,
     * 4藁邸7餍!;淌摇N伦浴!;食5棵凇!夹界!
     * 這樣可以在主線程中更新 UI 而不用把 Handler 定義在主線程中馆里。
     * !!p佟1摺!S堋P得健!F捞滥沫!
     */
    private Handler mUiHandler = new Handler(Looper.getMainLooper());

//    /*
//        注意,因為UdpClient 的send 和 receive 是綁定的键俱,
//        所以其 返回信息的處理接口 是作為 發(fā)送信息方法 的參數(shù)的兰绣,由此產(chǎn)生綁定邏輯
//
//        但是這里 TcpClient 就不是send 和 receive 一一綁定了,
//        其沒有數(shù)量的對應(yīng)關(guān)系编振,只是一個持續(xù)的 任意數(shù)據(jù)包數(shù)量的 全雙工的連接缀辩,
//        無需Udp 的綁定邏輯, Listener 由此不使用跟send 方法綁定的邏輯踪央,
//        使用單獨set 的邏輯表達方式
//     */

    public interface onMsgComingListener {
        void onMsgComing(String msg);
        void onError(Exception ex);
        void popToast();
    }

    private onMsgComingListener mListener;

    public void setOnMsgComingListener(onMsgComingListener listener) {
        mListener = listener;
    }

    //------------------------------------------------------------------------

    public TcpClientBiz() {

//        //socket 的new 到 IO 流的獲取 這幾行代碼是已經(jīng)做了網(wǎng)絡(luò)操作的臀玄,
//        // 所以必須開一個子線程去進行,3濉=∥蕖!液斜!
//        // 畢竟 TcpClientBiz() 在調(diào)用的時候肯定是在UI 線程進行的
//
//        /*
//            另外需要注意一點@巯汀!少漆!
//            下面的socket 和 IO 流初始化是在子線程中進行的臼膏,
//            所以我們不知道什么時候會完成初始化,
//            因此在使用的時候是需要進行一個UI 交互提醒的示损,
//            比如loading 動畫渗磅,啟動頁面時使用loading動畫,初始化完成之后再取消loading 動畫检访,
//
//         */
        new Thread() {
            @Override
            public void run() {
                try {
                    mSocket = new Socket("172.18.1.59", 9090);//連接到 Server端
                    mIs = mSocket.getInputStream();
                    mOs = mSocket.getOutputStream();

                    mUiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            mListener.popToast();
                        }
                    });

                    //讀到消息則 借用回調(diào) 回到MainActivity 進行UI 更新
                    readServerMsg();

                } catch (final IOException e) {

                    mUiHandler.post(new Runnable() {
                        @Override
                        public void run() {

                            if (mListener != null) {
                                mListener.onError(e);
                            }
                        }
                    });
                }
            }
        }.start();


    }

    /**
     * 一旦本類被實例化始鱼,馬上啟動
     * 不斷阻塞等待Server端 信息
     * readLine() 沒有消息時阻塞,
     * 一有消息脆贵,馬上發(fā)給接口處理邏輯
     *
     * @throws IOException
     */
    private void readServerMsg() throws IOException {

        final BufferedReader br = new BufferedReader(new InputStreamReader(mIs));

        String line = null;

        while ((line = br.readLine()) != null) {

            final String finalLine = line;

            /*
                R角濉!5べ鳌W辞凇P场!3炙选C芩啤!:巍2须纭!F兜肌E酌ā!孩灯!
                基于主線程MainLooper 以及 回調(diào)機制
                在 業(yè)務(wù)類內(nèi)部 調(diào)用 外部實現(xiàn)的處理邏輯方法
                9虢稹!7宓怠0芷ァ!<パ病O颇丁!;肚辍槽棍!
             */
            mUiHandler.post(new Runnable() {
                @Override
                public void run() {

                    //讀到消息則 借用回調(diào) 回到MainActivity 進行UI 更新
                    if (mListener != null) {
                        mListener.onMsgComing(finalLine);
                    }
                }
            });

        }
    }

    /**
     * 把參數(shù)msg 寫入BufferWriter(O流),發(fā)送給Server端,
     * 一般這個msg 消息 是EditText 中的內(nèi)容抬驴,
     *
     * 調(diào)用時機:一般是EditText 右邊的按鈕被點擊的時候
     *
     * 調(diào)用時炼七,封裝輸出流,
     * 把參數(shù)msg 寫入BufferWriter(O流)怎爵,發(fā)送給Server端,
     *
     * 在要發(fā)送消息給Server 的時候調(diào)用
     * 發(fā)送的消息會在Server 端的 ClientTask 類中
     * 的run() 中的while ((line = br.readLine()) != null) 處被讀取到特石,
     * 并通過 MsgPool.getInstance().sendMsg() 被添加到消息隊列中
     *
     * @param msg  要發(fā)送的信息
     */
    public void sendMsg(final String msg) {

        //開一個線程去做輸出,完成任務(wù)之后線程就自動回收
        new Thread(){
            @Override
            public void run() {
                try {
                    //一有消息過來鳖链,就封裝輸出流,寫入并 發(fā)送信息到 Server端
                    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(mOs));
                    bw.write(msg);
                    bw.newLine();
                    bw.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

    public void onDestroy() {
        //6漳\轿!狂秦!
        // 獨立地try...catch...的原因:
        // 9嗦隆!A盐省侧啼!
        // 如果把三個close 都放在同一個try 塊里面
        // 那假如第一個close 出現(xiàn)了異常牛柒,
        // 后面兩個close 即使沒異常,
        // 也處理不了了痊乾,這顯然是不符合條件的
        // Fけ凇!D纳蟆6昶恰!

        try {
            if (mIs != null) {
                mIs.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if (mOs != null) {
                mOs.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if (mSocket != null) {
                mSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • rename一下MainActivity為UdpActivity:

    復(fù)制UdpActivity一份湿滓,原地粘貼滴须,命名為TcpActivity:
public class TcpActivity extends AppCompatActivity {

    private EditText mEtMsg;
    private Button mBtnSend;
    private TextView mTvContent;

    private TcpClientBiz mTcpClientBiz = new TcpClientBiz();


    public Context getTcpActivityContext() {
        return getApplicationContext();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initViews();

        mTcpClientBiz.setOnMsgComingListener(new TcpClientBiz.onMsgComingListener() {
            @Override
            public void onMsgComing(String msg) {
                appendMsgToContent("Server:" + msg);
            }

            @Override
            public void onError(Exception ex) {
                ex.printStackTrace();
            }

            @Override
            public void popToast() {
                Toast.makeText(TcpActivity.this, "初始化完成!_窗隆H铀!可以開始發(fā)送信息了3ァD小!", Toast.LENGTH_SHORT).show();
            }
        });
    }

    private void initViews() {
        mEtMsg = findViewById(R.id.id_et_msg);
        mBtnSend = findViewById(R.id.id_btn_send);
        mTvContent = findViewById(R.id.id_tv_content);

        mBtnSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String msg = mEtMsg.getText().toString();
                if (TextUtils.isEmpty(msg)) {
                    return;
                }

                //發(fā)送后清除編輯框文本
                mEtMsg.setText("");

                //msg 負責發(fā)送數(shù)據(jù)膀篮,onMsgReturnedListener() 則負責處理對應(yīng)的返回的信息
                mTcpClientBiz.sendMsg(msg);
            }
        });
    }

    private void appendMsgToContent(String msg) {
        mTvContent.append(msg + "\n");
    }

    /*
        回收資源
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mTcpClientBiz.onDestroy();
    }
}

更改啟動頁面:
  • 反復(fù)測試(一個模擬機和兩臺真機的聊天哈哈哈):
  • 最終Server端聊天記錄:
服務(wù)端諸類代碼實現(xiàn)概述(TcpServer嘹狞、ClientTask、MsgPool)
  • TcpServer:
    死循環(huán)誓竿,阻塞磅网,等待客戶端請求連接,while (true) & .accept();
    一旦連接上筷屡,獲取對應(yīng)的socket對象并
    把它丟給ClientTask的構(gòu)造方法涧偷,new ClientTask(socket)
    直接新建一個子線程,去處理這個socket(.start())毙死,
    將連接上的socket 對應(yīng)的線程類燎潮,注冊到消息隊列類中的隊列中,
    成為消息隊列的觀察者扼倘;MsgPool.getInstance().addMsgComingListener(clientTask)
    啟動消息隊列讀讀隊列的線程确封,
    MsgPool.getInstance().start();

  • ClientTask:
    public class ClientTask extends Thread implements MsgPool.MsgComingListener
    讓線程類作為消息隊列的觀察者,
    負責接收被觀察者的通知信息并做socket 通信再菊;
    類中:

    • 1/3 構(gòu)造方法:
      接收TcpServer對過來的socket對象爪喘,
      用之初始化其IO流;

    • 2/3 run()<讀取Client的 I流纠拔,加入 MsgPool.mQueue>
      封裝輸入流秉剑,
      讀取客戶端發(fā)送過來的信息并輸出:
      while ((line = br.readLine()) != null){...}
      System.out.println(...);
      把信息發(fā)送加入到消息隊列:MsgPool.getInstance().sendMsg(...);
      如果沒有客戶端發(fā)送信息,
      readLine() 便會阻塞(注意這里會阻塞稠诲!所以要放在子線程U炫簟)在原地

    • 3/3 onMsgComing(String msg)<取出 MsgPool.mQueue诡曙,寫入Client的 O流>
      作為消息隊列的觀察者對應(yīng)的更新方法,
      消息隊列中最新的消息會推送通知到這里的msg參數(shù),
      (消息隊列類有一個子線程死循環(huán)阻塞讀取隊頭略水,
      String msg = mQueue.take();
      notifyMsgComing(msg);
      notifyMsgComing中遍歷所有已注冊的觀察者,
      遍歷時調(diào)用觀察者的onMsgComing(msg)价卤,
      正是本方法!>矍搿\瘛)
      本方法中拿到最新的推送消息后,
      寫進輸出流驶赏,
      發(fā)送給對應(yīng)的 TCP 連接的客戶一端socket

  • class MsgPool消息列表類

    • 實現(xiàn)單例模式
      private static MsgPool mInstance = new MsgPool();
      public static MsgPool getInstance() { return mInstance; }
      private MsgPool() { }

    • 準備消息列表底層數(shù)據(jù)結(jié)構(gòu):
      private LinkedBlockingQueue<String> mQueue = new LinkedBlockingQueue<>();

    • sendMsg(String msg):
      當有消息過來時炸卑,即客戶端接收到消息時,
      就把消息發(fā)送(添加)到消息隊列中:mQueue.put(msg);
      ClientTaskrun()調(diào)用本方法C喊8俏摹!蚯姆;

    • start()
      啟動讀取消息的子線程五续,在后臺不斷運行,
      死循環(huán) 阻塞 讀取隊頭龄恋,
      一有消息取出就通知所有已注冊的觀察者疙驾,
      String msg = mQueue.take();
      notifyMsgComing(msg);
      在TcpServer中一開始配置好服務(wù)ip和端口就調(diào)用了;

    • 實現(xiàn)被觀察者通知方法:notifyMsgComing(String msg)
      實現(xiàn)被觀察者方法郭毕,添加觀察者到列表:
      public void addMsgComingListener(MsgComingListener listener)
      觀察者接口MsgComingListener
      被觀察者列表private List<MsgComingListener> mListeners = new ArrayList<>();

客戶端諸類代碼實現(xiàn)概述(TcpClientBiz它碎、TcpActivity)
  • TcpClientBiz:
    連接Server端,
    后臺子線程不斷接收Server端發(fā)送過來的信息显押,
    前臺封裝扳肛、提供向Server端發(fā)送信息的方法

    • 準備一個綁定了mainLooperHandler

    • 定義<回調(diào)機制>
      回調(diào)接口及其抽象方法;
      聲明 全局 回調(diào)接口變量乘碑;
      回調(diào)接口置入函數(shù)挖息;
      setOnMsgComingListener(onMsgComingListener listener) { mListener = listener; }

    • 構(gòu)造方法:

      • 開啟子線程!J薹簟套腹!,
        配置連接到Server端的socket资铡;mSocket = new Socket("***.**.*.**", 9090);

      • 通過socket獲得IO流沉迹;
        (以上,socket害驹,IO流都初始化給全局變量)

      • 使用全局 回調(diào)接口變量,
        抽象調(diào)用業(yè)務(wù)方法蛤育;(Toast提醒宛官、Error處理之類)

      • 調(diào)用readServerMsg():伞!底洗!;

    • readServerMsg()
      一旦本類被實例化腋么,就會被啟動!:ヒ尽珊擂!

      開啟一個子線程,
      拿著全局變量I流费变,封裝成BufferReader摧扇,
      死循環(huán) 阻塞等待 讀取Server端信息
      while ((line = br.readLine()) != null)
      一旦有信息,
      借助Handler.post()挚歧,
      使用全局 回調(diào)接口變量抽象調(diào)用接口方法onMsgComing()
      通過回調(diào)機制交給Activity層處理扛稽;

    • sendMsg(final String msg)
      開啟一個子線程,
      拿著全局變量O流滑负,封裝成BufferWriter在张,
      把參數(shù)msg 寫入BufferWriter(O流),發(fā)送給Server端矮慕;
      調(diào)用時機:在要發(fā)送消息給Server 的時候調(diào)用帮匾,
      一般是EditText 右邊的按鈕被點擊的時候

    • onDestroy():
      回收socket、IO流

  • TcpActivity
    主要是各種組件的配置痴鳄,
    注意幾點即可:

    • 需要實例化一個全局TcpClientBiz實例
      然后用匿名內(nèi)部類實現(xiàn)回調(diào)接口及其方法瘟斜,
      再set 給TcpClientBiz實例;

    • 點擊按鈕時把EditText的內(nèi)容發(fā)送給Server端夏跷;
      msg = mEtMsg.getText().toString();
      mTcpClientBiz.sendMsg(msg);

    • onDestroy()中調(diào)用mTcpClientBiz.onDestroy();回收資源

所有的客戶端都可發(fā)送消息到隊列中哼转,
然后所有的客戶端都在等待
消息隊列的消息新增(mQueue.put())這個時刻,
消息隊列一新增消息槽华,
即一接收到某個客戶端發(fā)送過來消息(mQueue.put())壹蔓,
則消息都會一次性轉(zhuǎn)發(fā)給所有客戶端,
所以這里涉及到一個觀察者設(shè)計模式猫态,
消息隊列(MsgPool)或消息(Msg)是被觀察者佣蓉,
所有客戶端處理線程(ClientTask)都是觀察者

觀察者模式實現(xiàn)小結(jié):

觀察者接口準備更新(數(shù)據(jù)或UI的)方法;
被觀察者接口準備三個抽象方法亲雪;

觀察者實現(xiàn)類具體實現(xiàn)更新邏輯勇凭,可以有參數(shù),參數(shù)為更新需要的數(shù)據(jù)义辕;

被觀察者實現(xiàn)類準備一個觀察者List以及實現(xiàn)三個方法:
1.觀察者注冊方法:
參數(shù)為某觀察者虾标,功能是把觀察者參數(shù)加到觀察者List中;
2.注銷觀察者方法:
參數(shù)為某觀察者灌砖,功能是把觀察者參數(shù)從觀察者List中移除璧函;
3.通知觀察者方法:無參數(shù)或者把需要通知的數(shù)據(jù)作為參數(shù)傀蚌,
功能是遍歷所有已注冊的觀察者,
即遍歷 注冊添加到 觀察者List中的觀察者蘸吓,逐個調(diào)用List中所有觀察者的更新方法善炫;即一次性更新所有已注冊的觀察者!

使用時库继,
實例化一個被觀察者和若干個觀察者箩艺,
將所有觀察者注冊到被觀察者處,
調(diào)用被觀察者的通知方法宪萄,一次性更新所有已注冊的觀察者艺谆!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市雨膨,隨后出現(xiàn)的幾起案子擂涛,更是在濱河造成了極大的恐慌,老刑警劉巖聊记,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件撒妈,死亡現(xiàn)場離奇詭異,居然都是意外死亡排监,警方通過查閱死者的電腦和手機狰右,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舆床,“玉大人棋蚌,你說我怎么就攤上這事“ざ樱” “怎么了谷暮?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盛垦。 經(jīng)常有香客問我湿弦,道長,這世上最難降的妖魔是什么腾夯? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任颊埃,我火速辦了婚禮,結(jié)果婚禮上蝶俱,老公的妹妹穿的比我還像新娘班利。我一直安慰自己,他們只是感情好榨呆,可當我...
    茶點故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布罗标。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪馒稍。 梳的紋絲不亂的頭發(fā)上皿哨,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天,我揣著相機與錄音纽谒,去河邊找鬼。 笑死如输,一個胖子當著我的面吹牛鼓黔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播不见,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼澳化,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了稳吮?” 一聲冷哼從身側(cè)響起缎谷,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎灶似,沒想到半個月后列林,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡酪惭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年希痴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片春感。...
    茶點故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡砌创,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鲫懒,到底是詐尸還是另有隱情嫩实,我是刑警寧澤,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布窥岩,位于F島的核電站甲献,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏谦秧。R本人自食惡果不足惜竟纳,卻給世界環(huán)境...
    茶點故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疚鲤。 院中可真熱鬧锥累,春花似錦、人聲如沸集歇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至际歼,卻和暖如春惶翻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鹅心。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工吕粗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人旭愧。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓颅筋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親输枯。 傳聞我的和親對象是個殘疾皇子议泵,可洞房花燭夜當晚...
    茶點故事閱讀 43,494評論 2 348

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

  • Android消息處理機制估計都被寫爛了瞳收,但是依然還是要寫一下碉京,因為Android應(yīng)用程序是通過消息來驅(qū)動的,An...
    一碼立程閱讀 4,461評論 4 36
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,092評論 1 32
  • 本文出自 Eddy Wiki 缎讼,轉(zhuǎn)載請注明出處:http://eddy.wiki/interview-java.h...
    eddy_wiki閱讀 2,072評論 0 14
  • 前言 在Android開發(fā)的多線程應(yīng)用場景中收夸,Handler機制十分常用 今天,我將手把手帶你深入分析Handle...
    BrotherChen閱讀 469評論 0 0
  • Category基本概念 1.什么是Category Category有很多種翻譯: 分類 \ 類別 \ 類目 (...
    越天高閱讀 183評論 0 1