1. 程式人生 > >android進階3step2:Android App通訊——Socket通訊

android進階3step2:Android App通訊——Socket通訊

改:https://www.jianshu.com/p/089fb79e308b

掌握

Android中Socket程式設計,包括TCPUDP通訊協議,以及加密傳輸、身份認證的網路協議Https的相關知識。

先掃一下盲:
什麼是觀察者模式 

埠號IP等網路基礎知識掃盲

網路基礎知識Http、Https

前言

  • Socket的使用在 Android網路程式設計中非常重要
  • 今天我將帶大家全面瞭解 Socket 及 其使用方法 

目錄

1.網路基礎


1.1 計算機網路分層

計算機網路分為五層:物理層、資料鏈路層、網路層、運輸層、應用層

其中:

  • 網路層:負責根據IP找到目的地址的主機
  • 運輸層:通過埠把資料傳到目的主機的目的程序,來實現程序與程序之間的通訊

1.2 埠號(PORT)

埠號規定為16位,即允許一個IP主機有2的16次方65535個不同的埠。其中:

  • 0~1023:分配給系統的埠號 

我們不可以亂用

 

  • 1024~49151:登記埠號,主要是讓第三方應用使用

但是必須在IANA(網際網路數字分配機構)按照規定手續登記,

  • 49152~65535:短暫埠號,是留給客戶程序選擇暫時使用,一個程序使用完就可以供其他程序使用。

Socket使用時,可以用1024~65535的埠號



1.3 C/S結構

  • 定義:即客戶端/伺服器結構,是軟體系統體系結構
  • 作用:充分利用兩端硬體環境的優勢,將任務合理分配到Client端和Server端來實現,降低了系統的通訊開銷。 

Socket正是使用這種結構建立連線的,一個套接字接客戶端,一個套接字接伺服器。

如圖:


可以看出,Socket的使用可以基於TCP或者UDP協議。



1.4 TCP協議

  • 定義:Transmission Control Protocol,即傳輸控制協議,是一種傳輸層通訊協議 

基於TCP的應用層協議有FTP、Telnet、SMTP、HTTP、POP3與DNS。

  • 特點:面向連線、面向位元組流、全雙工通訊、可靠
  1. 面向連線:指的是要使用TCP傳輸資料,必須先建立TCP連線,傳輸完成後釋放連線,就像打電話一樣必須先撥號建立一條連線,打完後掛機釋放連線。
  2. 全雙工通訊:即一旦建立了TCP連線,通訊雙方可以在任何時候都能傳送資料。
  3. 可靠的:指的是通過TCP連線傳送的資料,無差錯,不丟失,不重複,並且按序到達。
  4. 面向位元組流:流,指的是流入到程序或從程序流出的字元序列。簡單來說,雖然有時候要傳輸的資料流太大,TCP報文長度有限制,不能一次傳輸完,要把它分為好幾個資料塊,但是由於可靠性保證,接收方可以按順序接收資料塊然後重新組成分塊之前的資料流,所以TCP看起來就像直接互相傳輸位元組流一樣,面向位元組流。
  • TCP建立連線 

必須進行三次握手:若A要與B進行連線,則必須 

  1. 第一次握手:建立連線。客戶端傳送連線請求報文段,將SYN位置為1,Sequence Number為x;然後,客戶端進入SYN_SEND狀態,等待伺服器的確認。即A傳送資訊給B
  2. 第二次握手:伺服器收到客戶端的SYN報文段,需要對這個SYN報文段進行確認。即B收到連線資訊後向A返回確認資訊
  3. 第三次握手:客戶端收到伺服器的(SYN+ACK)報文段,並向伺服器傳送ACK報文段。即A收到確認資訊後再次向B返回確認連線資訊 

此時,A告訴自己上層連線建立;B收到連線資訊後告訴上層連線建立。


這樣就完成TCP三次握手 = 一條TCP連線建立完成 = 可以開始傳送資料

三次握手期間任何一次未收到對面回覆都要重發。
最後一個確認報文段傳送完畢以後,客戶端和伺服器端都進入ESTABLISHED狀態。


為什麼TCP建立連線需要三次握手?

答:

防止伺服器端因為接收了早已失效的連線請求報文從而一直等待客戶端請求,從而浪費資源

  • “已失效的連線請求報文段”的產生在這樣一種情況下:Client發出的第一個連線請求報文段並沒有丟失,而是在某個網路結點長時間的滯留了,以致延誤到連線釋放以後的某個時間才到達server。
  • 這是一個早已失效的報文段。但Server收到此失效的連線請求報文段後,就誤認為是Client再次發出的一個新的連線請求。
  • 於是就向Client發出確認報文段,同意建立連線。
  • 假設不採用“三次握手”:只要Server發出確認,新的連線就建立了。
  • 由於現在Client並沒有發出建立連線的請求,因此不會向Server傳送資料。
  • 但Server卻以為新的運輸連線已經建立,並一直等待Client發來資料。>- 這樣,Server的資源就白白浪費掉了。

採用“三次握手”的辦法可以防止上述現象發生:

  • Client不會向Server的確認發出確認
  • Server由於收不到確認,就知道Client並沒有要求建立連線
  • 所以Server不會等待Client傳送資料,資源就沒有被浪費

 

TCP釋放連線 

  • TCP釋放連線需要四次揮手過程,現在假設A主動釋放連線:(資料傳輸結束後,通訊的雙方都可釋放連線)
  1. 第一次揮手:A傳送釋放資訊到B;(發出去之後,A->B傳送資料這條路徑就斷了)
  2. 第二次揮手:B收到A的釋放資訊之後,回覆確認釋放的資訊:我同意你的釋放連線請求
  3. 第三次揮手:B傳送“請求釋放連線“資訊給A
  4. 第四次揮手:A收到B傳送的資訊後向B傳送確認釋放資訊:我同意你的釋放連線請求

B收到確認資訊後就會正式關閉連線; 
A等待2MSL後依然沒有收到回覆,則證明B端已正常關閉,於是A關閉連線


為什麼TCP釋放連線需要四次揮手?

為了保證雙方都能通知對方“需要釋放連線”,即在釋放連線後都無法接收或傳送訊息給對方

  • 需要明確的是:TCP是全雙工模式,這意味著是雙向都可以傳送、接收的
  • 釋放連線的定義是:雙方都無法接收或傳送訊息給對方,是雙向的
  • 當主機1發出“釋放連線請求”(FIN報文段)時,只是表示主機1已經沒有資料要傳送 / 資料已經全部發送完畢; 

 但是,這個時候主機1還是可以接受來自主機2的資料。

  • 當主機2返回“確認釋放連線”資訊(ACK報文段)時,表示它已經知道主機1沒有資料傳送了 
  • 但此時主機2還是可以傳送資料給主機1
  • 當主機2也傳送了FIN報文段時,即告訴主機1我也沒有資料要傳送了 
  • 此時,主機1和2已經無法進行通訊:主機1無法傳送資料給主機2,主機2也無法傳送資料給主機1,此時,TCP的連線才算釋放

1.5 UDP協議

  • 定義:User Datagram Protocol,即使用者資料報協議,是一種傳輸層通訊協議。

基於UDP的應用層協議有TFTP、SNMP與DNS。

  • 特點:無連線的、不可靠的、面向報文、沒有擁塞控制
  1. 無連線的:和TCP要建立連線不同,UDP傳輸資料不需要建立連線,就像寫信,在信封寫上收信人名稱、地址就可以交給郵局傳送了,至於能不能送到,就要看郵局的送信能力和送信過程的困難程度了。
  2. 不可靠的:因為UDP發出去的資料包發出去就不管了,不管它會不會到達,所以很可能會出現丟包現象,使傳輸的資料出錯。
  3. 面向報文:資料報文,就相當於一個數據包,應用層交給UDP多大的資料包,UDP就照樣傳送,不會像TCP那樣拆分。
  4. 沒有擁塞控制:擁塞,是指到達通訊子網中某一部分的分組數量過多,使得該部分網路來不及處理,以致引起這部分乃至整個網路效能下降的現象,嚴重時甚至會導致網路通訊業務陷入停頓,即出現死鎖現象,就像交通堵塞一樣。TCP建立連線後如果傳送的資料因為通道質量的原因不能到達目的地,它會不斷重發,有可能導致越來越塞,所以需要一個複雜的原理來控制擁塞。而UDP就沒有這個煩惱,發出去就不管了。

應用場景

 很多的實時應用(如IP電話、實時視訊會議、某些多人同時線上遊戲等)要求源主機以很定的速率傳送資料,並且允許在網路發生擁塞時候丟失一些資料,但是要求不能有太大的延時,UDP就剛好適合這種要求。所以說,只有不適合的技術,沒有真正沒用的技術。


 1.6 HTTP協議

詳情請看我寫的另外一篇文章你需要了解的HTTP知識都在這裡了!


2. Socket定義

  • 即套接字,是一個對 TCP / IP協議進行封裝 的程式設計呼叫介面(API)
  1. 即通過Socket,我們才能在Andorid平臺上通過 TCP/IP協議進行開發
  2. Socket不是一種協議,而是一個程式設計呼叫介面(API),屬於傳輸層(主要解決資料如何在網路中傳輸)
  • 成對出現,一對套接字:
  1. Socket ={(IP地址1:PORT埠號),(IP地址2:PORT埠號)}

 3. 原理

Socket的使用型別主要有兩種:

  • 流套接字(streamsocket) :基於 TCP協議,採用 流的方式 提供可靠的位元組流服務
  • 資料報套接字(datagramsocket):基於 UDP協議,採用 資料報文 提供資料打包傳送的服務

具體原理圖如下:

 

4. Socket 與 Http 對比

  • Socket屬於傳輸層,因為 TCP / IP協議屬於傳輸層,解決的是資料如何在網路中傳輸的問題
  • HTTP協議 屬於 應用層,解決的是如何包裝資料

由於二者不屬於同一層面,所以本來是沒有可比性的。但隨著發展,預設的Http裡封裝了下面幾層的使用,所以才會出現Socket & HTTP協議的對比:(主要是工作方式的不同):

  • Http:採用 請求—響應 方式。

即建立網路連線後,當 客戶端 向 伺服器 傳送請求後,伺服器端才能向客戶端返回資料。
可理解為:是客戶端有需要才進行通訊

  • Socket:採用 伺服器主動傳送資料 的方式

即建立網路連線後,伺服器可主動傳送訊息給客戶端,而不需要由客戶端向伺服器傳送請求
可理解為:是伺服器端有需要才進行通訊


5. 使用步驟

  • Socket可基於TCP或者UDP協議,但TCP更加常用
  • 所以下面的使用步驟 & 例項的Socket將基於UDP協議

UDP 使用API介紹

  • – InetAddress(確定是哪個地址)
  • – DatagramSocket(receive,send) (資料的傳送和接受)
  • – DatagramPacket(資料打包)

• 實戰

  • – Client與Server通訊
  • 傳送流程固定:必須先Client傳送一條資料給Server,Server再發送訊息給Client

1-使用UDP(實現客戶端和服務端的通訊)

服務端UdpServer.java的編寫 

主要是接受client發來的訊息和傳送給client的訊息

涉及DatagramSocket、DatagramPacket、InetAddress三個API



import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Scanner;

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/11/26 10:32<p>
 * <p>更改時間:2018/11/26 10:32<p>
 * <p>版本號:1<p>
 */

/***
 * 服務端的程式碼
 */
public class UdpServer {

    private InetAddress mInetAddress;//確定是哪個地址
    private int mPort = 8888;//埠號:確定是哪個應用
    private Scanner mScanner;

    private DatagramSocket mSocket;

    public UdpServer() {
        try {
            mInetAddress = InetAddress.getLocalHost();//基於本機的地址
            System.out.println(mInetAddress);
            mSocket = new DatagramSocket(mPort, mInetAddress);
            mScanner = new Scanner(System.in);//結束輸入資料
            mScanner.useDelimiter("\n");//換行結束

        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }

    public void start() {

        while (true) {//不要server退出
            try {
                /**
                 * Server接受client發來的的資料
                 */
                byte[] buf = new byte[1024];
                //1.準備裝資料的包
                //陣列,陣列長度
                DatagramPacket receivedPacket = new DatagramPacket(buf, buf.length);
                //2.接受資料包(如果沒有資料會一直處於阻塞狀態)
                mSocket.receive(receivedPacket);
                //3.解析資料包
                //拿地址/埠號
                InetAddress address = receivedPacket.getAddress();
                int port = receivedPacket.getPort();
                //byte[]陣列解析成String
                String clientMsg = new String(
                        receivedPacket.getData(),
                        0,
                        receivedPacket.getLength());
                System.out.println("收到Client發來的資料:address=" + address + ",port=" + port + ",msg=" + clientMsg);

                /**
                 * Server傳送資料到client
                 */
                String returnMsg = mScanner.next();
                System.out.println("Server傳送資料給Client:"+returnMsg);
                byte[] returnMsgBytes = returnMsg.getBytes();
                //陣列。陣列長度,攜帶目標地址
                DatagramPacket sendPacket = new DatagramPacket(
                        returnMsgBytes,
                        returnMsgBytes.length,
                        receivedPacket.getSocketAddress());
                mSocket.send(sendPacket);

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

    /**
     * 啟動服務端
     *
     * @param args
     */
    public static void main(String[] args) {
        new UdpServer().start();
    }
}

客戶端UdpClient.java的編寫 


/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/11/26 10:51<p>
 * <p>更改時間:2018/11/26 10:51<p>
 * <p>版本號:1<p>
 */

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

/***
 * 客戶端的編寫
 */
public class UdpClient {
    //Serverip地址
    private String mServerIp = "192.168.31.180";
    private InetAddress mServerAddress;
    //Server的埠號
    private int mServerPort = 8888;
    private DatagramSocket mSocket;
    private Scanner mScanner;

    public UdpClient() {
        try {
            mServerAddress = InetAddress.getByName(mServerIp);
            mSocket = new DatagramSocket();
            mScanner = new Scanner(System.in);
            mScanner.useDelimiter("\n");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void start() {
        while (true) {
            try {
                /**
                 * Client傳送資料給Server
                 */
                String ClientMsg = mScanner.next();
                System.out.println("Client傳送資料給Server" + ClientMsg);
                byte[] clientMsgBytes = ClientMsg.getBytes();
                //byte陣列,長度,server端的地址,server端的埠號
                DatagramPacket ClientPacket = new DatagramPacket(
                        clientMsgBytes
                        , clientMsgBytes.length, mServerAddress, mServerPort);
                mSocket.send(ClientPacket);
                /**
                 * 接收Server的資料
                 */
                byte[] buf = new byte[1024];
                DatagramPacket ServerMsgPacket = new DatagramPacket(buf, buf.length);
                mSocket.receive(ServerMsgPacket);
                //解析資料包
                //byte[]陣列解析成String
                String ServerMsg = new String(
                        ServerMsgPacket.getData(),
                        0,
                        ServerMsgPacket.getLength());
                System.out.println("收到Server發來的 msg= " + ServerMsg);


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

        }
    }

    /**
     * 程式的入口
     *
     * @param args
     */
    public static void main(String[] args) {
        new UdpClient().start();
    }
}

演示:如果Server端一直報空指標,說明埠號被佔用,換一個埠號即可(注意client和server要寫同一個埠號)

run main函式後 Client端:


 

Server端: 

移植客戶端到android中

服務端不變:

第一步:androidmanifest.xml中新增網路許可權

    <uses-permission android:name="android.permission.INTERNET" />

第二步:建biz業務類 實現客戶端的資料傳送和接收

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/11/26 10:51<p>
 * <p>更改時間:2018/11/26 10:51<p>
 * <p>版本號:1<p>
 */

import android.text.TextUtils;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

/***
 * 客戶端的編寫
 */
public class UdpClientBiz {
    //Serverip地址
    private String mServerIp = "192.168.31.180";
    private InetAddress mServerAddress;
    //Server的埠號
    private int mServerPort = 9999;
    private DatagramSocket mSocket;

    public UdpClientBiz() {
        try {
            mServerAddress = InetAddress.getByName(mServerIp);
            mSocket = new DatagramSocket();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 回撥函式
     */
    public interface OnMsgReturnListener {
        void OnMsgReturnListener(String msg);

        void onError(Exception ex);
    }

    /**
     * 提供外接send方法
     *
     * @param msg
     * @param listener
     */
    public void sendMsg(final String msg, final OnMsgReturnListener listener) {
        //校驗
        if (!TextUtils.isEmpty(msg) || listener != null) {
            new Thread() {
                @Override
                public void run() {
                    /**
                     * Client傳送資料給Server
                     */
                    try {
                        System.out.println("Client傳送資料給Server" + msg);
                        byte[] clientMsgBytes = msg.getBytes();
                        //byte陣列,長度,server端的地址,server端的埠號
                        DatagramPacket ClientPacket = new DatagramPacket(
                                clientMsgBytes
                                , clientMsgBytes.length, mServerAddress, mServerPort);
                        mSocket.send(ClientPacket);
                        //receiver msg
                        /**
                         * 接收Server的資料
                         */
                        byte[] buf = new byte[1024];
                        DatagramPacket ServerMsgPacket = new DatagramPacket(buf, buf.length);
                        mSocket.receive(ServerMsgPacket);
                        //解析資料包
                        //byte[]陣列解析成String
                        String ServerMsg = new String(
                                ServerMsgPacket.getData(),
                                0,
                                ServerMsgPacket.getLength());
                        System.out.println("收到Server發來的 msg= " + ServerMsg);
                       ///通過listener回調出去
                        listener.OnMsgReturnListener(ServerMsg);
                    } catch (IOException e) {
                        //非同步回撥exception回調出去
                        listener.onError(e);
                    }
                }
            }.start();


        }
    }

    /**
     * 關閉Socket
     */
    public void onDestroy() {
        if (mSocket != null) {
            mSocket.close();
        }
    }
}

 第三步:MainActivity.java


import android.os.Handler;
import android.os.Looper;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.demo.udpdemo.biz.UdpClientBiz;

public class MainActivity extends AppCompatActivity {

    private Button bt_send;
    private EditText et_msg;
    private TextView tv_content;
    private UdpClientBiz mUdpClientBiz = new UdpClientBiz();
    //UIHandler
    private Handler mUiHandler = new Handler(Looper.getMainLooper());

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

    /**
     * 初始化事件
     */
    private void initEvent() {
        bt_send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String msg = et_msg.getText().toString();
                if (TextUtils.isEmpty(msg)) {
                    return;
                }
                /**
                 * client傳送訊息也要append一下
                 */
                appendMsgToContent("client——>server: " + msg);
                /**
                 * 傳送訊息到server,並且拿server返回的 資訊
                 */
                //傳送完之後清空一下輸入框
                et_msg.setText("");
                mUdpClientBiz.sendMsg(msg, new UdpClientBiz.OnMsgReturnListener() {
                    @Override
                    public void OnMsgReturnListener(final String msg) {
                        //收到server傳送的資料
                        //更新UI,讓它回到主執行緒中更新ui
                        mUiHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                appendMsgToContent("server——>client: " + msg);
                            }
                        });

                    }

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

    /**
     * 顯示內容的拼接方法
     */

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

    }

    /**
     * 初始化檢視
     */
    private void initViews() {
        bt_send = findViewById(R.id.id_bt_send);
        et_msg = findViewById(R.id.id_et_msg);
        tv_content = findViewById(R.id.id_tv_content);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mUdpClientBiz.onDestroy();
    }
}

 完成。

2-通過TCP實現網路連線(簡易聊天室 多個客戶端)

上面的例子使用了UDP進行客戶端和服務端通訊

下面的是使用TCP來實現

  • 多個客戶端可以同時發訊息給server服務端
  • 服務端將訊息同步顯示到每一個客戶端

TCP 注意的API介紹

  • – ServerSocket
  • – Socket

• 實戰

  • – 簡易版本聊天室
  • – 涉及:訊息佇列,多執行緒處理、觀察者模式等;

服務端的編寫:

 大致流程

  • 服務端開啟,一直處於阻塞狀態等待客戶端的到來
  • 一旦有客戶端連線到服務端,便會開啟一個執行緒來讀寫資料,併為客戶端註冊觀察者
  • 如果客戶端傳送了訊息,這條訊息會發送到訊息佇列Queue中,並轉發到其他註冊的觀察者(客戶端中)

佇列有什麼作用?:可以解決併發問題(同一時間多個客戶端發訊息),佇列會只開一個執行緒進行,訊息的處理

假設有多個客戶端同時進行傳送訊息,那麼訊息會分別放入佇列中,FIFO的順序通過一個執行緒進行轉發訊息

  1. 服務端立刻將訊息放入佇列中(如果沒有訊息會訊息佇列處於阻塞狀態)
  2. 服務端(被觀察者)利用觀察者模式通知每一個註冊的觀察者(客戶端要註冊觀察者) 訊息來了
  • 所有的客戶端就可以收到新的訊息(註冊觀察者的回撥方法返回新的資料),資料寫入到客戶端顯示

TcpServer.java


import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/11/26 13:46<p>
 * <p>更改時間:2018/11/26 13:46<p>
 * <p>版本號:1<p>
 */
public class TcpServer {

    public void start() {

        ServerSocket serverSocket = null;
        try {
            //埠自定
            serverSocket = new ServerSocket(9090);
            /**
             * 1起一個Queue起一個執行緒不斷的從訊息佇列中取資料正常情況使處於阻塞狀態
             */
            MsgPool.getInstance().start();

            while (true) {
                /**
                 * 2.等待客戶端的到來 accept是阻塞的方法, socket代表客戶端
                 * 如果沒有執行緒到來accept一直處於阻塞狀態
                 */
                Socket socket = serverSocket.accept();
                System.out.println(" 當前客戶端的資訊:ip:" + socket.getInetAddress().getHostAddress() +
                        " port:" + socket.getPort() + " is online...");
                //針對每一個連線客戶端建立一個Thread執行緒 進行讀取資料
                //然後繼續回到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();
    }

}

MsgPool.java 訊息佇列類 實現觀察者模式

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/11/26 14:02<p>
 * <p>更改時間:2018/11/26 14:02<p>
 * <p>版本號:1<p>
 */

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 這是一個訊息佇列 來解決併發的問題
 * 併發:就是同一時間有多個客戶端進行傳送訊息(就會有很多個執行緒開啟)
 * 不斷的往訊息佇列進行寫資料,只有一個執行緒從這個佇列取資料,和分發資料
 */
public class MsgPool {
    /**
     * 單例
     */
    private static MsgPool sInstance = new MsgPool();
    //阻塞的訊息佇列  可以封裝物件
    private LinkedBlockingQueue<String> mQueue = new LinkedBlockingQueue<>();

    public static MsgPool getInstance() {
        return sInstance;
    }

    private MsgPool() {
    }

    /**
     * 問題:所有客戶端將訊息放到佇列中,那麼佇列取出來的訊息如何給所有的客戶端呢?
     * 使用觀察者模式
     * 1.被觀察者: MsgPool
     * 2.觀察者: 所有的ClientTask
     * 3.觀察者註冊後,當MsgPool收到訊息notify所有的觀察者
     */
    /**
     * 1.觀察者
     */
    public interface MsgComingListener {
        void onMsgComing(String msg);
    }

    /**
     * 2.存放所有觀察者物件的集合
     */
    private List<MsgComingListener> mListeners = new ArrayList<>();

    /**
     * 3.註冊觀察者的方法
     */
    public void addMsgComingListener(MsgComingListener listener) {
        mListeners.add(listener);
    }

    /**
     * 4.通知所有觀察者 有訊息來了
     */
    private void notifyMsgComing(String msg) {
        //遍歷所有的觀察者,執行onMsgComing的方法
        for (MsgComingListener listener : mListeners) {
            listener.onMsgComing(msg);
        }
    }

    /**
     * 開啟執行緒去佇列不斷讀資料
     */

    public void start() {
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        //沒有訊息會阻塞
                        String msg = mQueue.take();
                        //拿到資料後通知所有觀察者 有訊息來了
                        notifyMsgComing(msg);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }


    /**
     * 所有客戶端將訊息放到佇列中
     * 如果有訊息來,進行取資料分發資料
     * 沒有的話阻塞佇列就會阻塞
     * 問題:所有客戶端將訊息放到佇列中,那麼佇列取出來的訊息如何給所有的客戶端呢?
     */

    public void sendMsg(String msg) {
        try {
            mQueue.put(msg);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

當有客戶端連線服務端時,會為該客戶端單獨開啟一個執行緒

ClientTask.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/11/26 13:51<p>
 * <p>更改時間:2018/11/26 13:51<p>
 * <p>版本號:1<p>
 */

/**
 * 每一個連線的客戶端單獨開一個執行緒
 * 1.inputstream 讀資訊
 * 2.outputstream 寫資訊
 */
public class ClientTask extends Thread implements MsgPool.MsgComingListener {
    //傳入Socket物件,可以進行讀寫資料
    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;

        /**讀客戶端寫入的資訊
         * 按行讀/不斷讀的訊息
         */
        try {
            while ((line = br.readLine()) != null) {
                System.out.println("read= " + line);
                //轉發訊息到其他客戶端socket
                MsgPool.getInstance().sendMsg(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 每一個客戶端都是觀察者,訊息來了 notify所有觀察者進行回撥
     *
     * @param msg
     */
    @Override
    public void onMsgComing(String msg) {
        //拿到server新的訊息,寫入資料

        try {
            mOs.write(msg.getBytes());
            mOs.write("\n".getBytes());
            mOs.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客戶端的編寫

TcpClient.java 主要是實現

  • 1.寫入資料到服務端
  • 2.收到服務端訊息佇列返回的資料,進行顯示
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/11/26 14:36<p>
 * <p>更改時間:2018/11/26 14:36<p>
 * <p>版本號:1<p>
 */
public class TcpClient {

    private Scanner mScanner;

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

    public void start() {
        //ip地址/埠號
        try {
            Socket socket = new Socket(InetAddress.getLocalHost().getHostAddress(), 9090);
            InputStream is = socket.getInputStream();//輸入流讀資料
            OutputStream os = socket.getOutputStream();//輸出流寫資料

            final BufferedReader br = new BufferedReader(new InputStreamReader(is));
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
            //開啟一個執行緒讀server的資料
            /**
             * 輸出服務端傳送的資料
             */
            new Thread() {
                @Override
                public void run() {
                    try {
                        String line = null;
                        while ((line = br.readLine()) != null) {
                            System.out.println("收到Server發來的資料" + line);

                        }
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }

                }
            }.start();

            /**
             * 寫資料到服務端
             */
            while (true) {
                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();
    }
}

效果:

執行客戶端和服務端的Main方法

開啟一個服務端,三個客戶端

客戶端依次輸入一個訊息 client * send *

看服務端和每個服務端接收到資料:

server端

client1: 

client2:

client3: 

客戶端向服務端傳送資料的步驟:

  1. 建立客戶端與服務端的連線
  2. 從Socket獲取輸出流的物件
  3. 向輸出流物件寫入資料
  4. 傳送資料到伺服器
  5. 斷開客戶端與服務端的連線

客戶端接收服務端資料的步驟:

  1. 建立客戶端與服務端的連線
  2. 建立獲取輸入流物件
  3. 傳入輸入流
  4. 通過輸入流讀取器物件接收伺服器傳送的資料
  5. 斷開客戶端與服務端的連線

移植客戶端(到android上)

注意:新增網路許可權

import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;

/**
 * <p>檔案描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>建立時間:2018/11/26 14:36<p>
 * <p>更改時間:2018/11/26 14:36<p>
 * <p>版本號:1<p>
 */
public class TcpClientBiz {
    private Socket mSocket;
    private InputStream mIs;
    private OutputStream mOs;
    private Handler mUiHandler = new Handler(Looper.getMainLooper());

    /**
     * 訊息回撥方法
     */
    public interface OnMsgComingListener {
        void onMsgComing(String msg);

        void onError(Exception ex);
    }

    private OnMsgComingListener mListener;

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

    public TcpClientBiz() {
        /**
         * 在子執行緒中進行網路操作
         */
        new Thread() {
            @Override
            public void run() {
                try {
                    //1、建立客戶端Socket,指定伺服器地址和埠
                    mSocket = new Socket("192.168.31.180", 9090);
                    mIs = mSocket.getInputStream();//輸入流讀資料
                    mOs = mSocket.getOutputStream();//輸出流寫資料

                    readServerMsg();//迴圈接收發送的訊息
                } catch (final IOException e) {

                    mUiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            if (mListener != null) {
                                mListener.onError(e);
                                Log.e("TAG-vv",e.getMessage());
                            }
                        }
                    });
                }
            }
        }.start();

    }

    /**
     * 迴圈接收Server傳送的訊息
     *
     * @throws IOException
     */
    private void readServerMsg() throws IOException {
        final BufferedReader br = new BufferedReader(new InputStreamReader(mIs));
        String line = null;
        //迴圈接收server傳送來的資料
        while ((line = br.readLine()) != null) {
            //將回調返回到ui執行緒進行更新ui
            final String finalLine = line;
            mUiHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (mListener != null) {
                        Log.e("TAG-vv", "mUiHandler");
                        mListener.onMsgComing(finalLine);
                    }
                }
            });

        }
    }

    /**
     * 對外提供一個傳送資料的方法
     *
     * @param msg
     */
    public void sendMsg(final String msg) {
        new Thread() {
            @Override
            public void run() {
                try {
                    //直接寫入server端
                    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(mOs));
                    bw.write(msg);
                    bw.newLine();
                    bw.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }.start();
    }

    public void onDestroy() {
        if (mOs != null) {
            try {
                mOs.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (mIs != null) {
            try {
                mIs.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (mSocket != null) {
            try {
                mSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 開啟兩個客戶端。

服務端