這篇要來聊在Android中實現Wifi通訊之中的TCP通訊

我的Blog有寫了關於TCP/UDP通訊的相關單元

在這裡

->碼農日常-『Android studio』在Android上實現TCP/UDP區域網路通訊上篇-UDP伺服器端與客戶端實現

->碼農日常-『Android studio』在Android上實現TCP/UDP區域網路通訊中篇-TCP伺服器端實現

->碼農日常-『Android studio』在Android上實現TCP/UDP區域網路通訊下篇-TCP客戶端實現

開發上若有TCP/UDP的需求歡迎抄走參考我的程式哦(・ωー)~☆

 

在上篇我有提到關於TCP/UDP的一些參考資源,這次我就不再花時間講廢話了

上篇內容提到的是關於如何以UDP模式在同一區域網路內達成不同手機之間的文字通訊

在那篇,我們演示了如何在UDP上面寫監聽端以及發送端

這篇我們改用TCP來實現相似的功能

不過因為TCP要同時介紹客戶端(Client)跟伺服器(Sever)端文章篇幅會太大

所以我TCP的部分我便把他切成中篇跟下篇方便介紹( ゚Д゚)b

總之,先來看範例

 

以及Github

->https://github.com/thumbb13555/UDP_TCP_ConnectDemo

 

開始吧!(-‿◦)


 

1. 介紹&加入權限

 

1.1 介紹專案結構

本次專案為了節省我Github的空間,所以我直接把UDP跟TCP的功能都放在一個APP

怎麼樣?很有誠意對吧(-‿◦)

來看一下專案結構吧

截圖 2021-01-02 下午4.39.38

 

身為一個二合一的APP,所以主畫面(MainActivity.java)便是選擇UDP或是TCP的頁面而已

Screenshot_1609576851

 

當然,這次的文章我們是選擇UDP得這個畫面

 

而在檔案中還有一個CommendFun.java

截圖 2021-01-02 下午4.43.08

 

裡面只放了一個用來取得手機所連線到之WIfi之IP而已

我直接PO一下吧

 

CommendFun.java

public class CommendFun {

    @SuppressLint("DefaultLocale")
    public static String getLocalIP(Context context) {
        WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(WIFI_SERVICE);
        assert wifiManager != null;
        WifiInfo info = wifiManager.getConnectionInfo();
        int ipAddress = info.getIpAddress();
        return String.format("%d.%d.%d.%d"
                , ipAddress & 0xff
                , ipAddress >> 8 & 0xff
                , ipAddress >> 16 & 0xff
                , ipAddress >> 24 & 0xff);
    }
}

 

本篇的內容是要講關於TCP通訊的Server端

也就是下圖紅匡內TCP的資料夾內的TCPActivity.javaTCPServer.java兩個檔案

如果要知道UDP的話,麻煩電梯上樓

->碼農日常-『Android studio』在Android上實現TCP/UDP區域網路通訊上篇-UDP伺服器端與客戶端實現

想知道TCP通訊的客戶端,請電梯下樓

->碼農日常-『Android studio』在Android上實現TCP/UDP區域網路通訊下篇-TCP客戶端實現

截圖 2021-01-09 下午3.40.25

1.2 加入權限

 

本次的專案因為涉及到網路使用以及Wifi使用

因此必須加入相關權限

 

請至AndroidManifest.xml的部分

加入以下粉底白字兩行

 

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jetec.udp_tcp_connectdemo">

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".TCP.TCPActivity"></activity>
        <activity android:name=".UDP.UDPActivity" />
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

網路的話不用特別問使用者,因此準備工作到這篇囉

接下來畫介面

 


 

2. 畫介面

 

這是這次介面的內容

Screenshot_1610178577

 

請仔細看一下本介面右上角有個切換元件

由於一個IP不會同時存在Server端與Client端

因此我們必須在介面上加以互鎖以防止閃退

效果如下

 

來吧,上介面

 

activity_t_c_p.xml

截圖 2021-01-09 下午4.02.01

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".UDP.UDPActivity">

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.1" />

    <TextView
        android:id="@+id/textView_IP"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:text="本機IP: "
        android:textColor="@android:color/background_dark"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/guideline2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ToggleButton
        android:id="@+id/toggleButton_Server"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textOff="開啟Server"
        android:textOn="關閉Server"
        app:layout_constraintBottom_toTopOf="@+id/guideline6"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/editText_Port"
        app:layout_constraintTop_toTopOf="@+id/guideline2" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.4" />

    <EditText
        android:id="@+id/editText_ReceiveMessage"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:focusable="false"
        android:focusableInTouchMode="false"
        android:gravity="top"
        android:minLines="20"
        android:inputType="textMultiLine"
        app:layout_constraintBottom_toTopOf="@+id/guideline4"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView3"
        app:layout_constraintVertical_bias="0.0" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.9" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="收到的消息"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline3" />

    <EditText
        android:id="@+id/editText_Input"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:ems="10"
        android:hint="請輸入內容"
        android:inputType="textPersonName"
        android:text="Hello! I'm samsung"
        app:layout_constraintBottom_toBottomOf="@+id/button_Send"
        app:layout_constraintEnd_toStartOf="@+id/button_clear"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/button_Send" />

    <EditText
        android:id="@+id/editText_Port"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:ems="10"
        android:hint="Port"
        android:inputType="number"
        android:text="8888"
        app:layout_constraintBottom_toTopOf="@+id/guideline6"
        app:layout_constraintEnd_toStartOf="@+id/guideline5"
        app:layout_constraintStart_toEndOf="@+id/textView4"
        app:layout_constraintTop_toTopOf="@+id/guideline2" />

    <Button
        android:id="@+id/button_Send"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="發送"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline4" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.7" />

    <EditText
        android:id="@+id/editText_RemoteIp"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:ems="10"
        android:inputType="numberDecimal"
        android:text="192.168.50.5"
        app:layout_constraintBottom_toTopOf="@+id/toggleButton_ClientConnect"
        app:layout_constraintEnd_toStartOf="@+id/guideline5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView5" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline6"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.17" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:text="本機Port: "
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/guideline6"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline2" />

    <TextView
        android:id="@+id/textView5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="Remote Ip:"
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline6" />

    <TextView
        android:id="@+id/textView6"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="Remote Port: "
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        app:layout_constraintStart_toStartOf="@+id/guideline5"
        app:layout_constraintTop_toTopOf="@+id/guideline6" />

    <EditText
        android:id="@+id/editText_RemotePort"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:ems="10"
        android:inputType="textPersonName"
        android:text="8888"
        app:layout_constraintBottom_toTopOf="@+id/toggleButton_ClientConnect"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/guideline5"
        app:layout_constraintTop_toBottomOf="@+id/textView6" />

    <ToggleButton
        android:id="@+id/toggleButton_ClientConnect"
        android:layout_width="0dp"
        android:textOff="連線至指定Server"
        android:textOn="斷線"
        android:enabled="false"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toTopOf="@+id/guideline3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/button_clear"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="清除"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/button_Send"
        app:layout_constraintTop_toTopOf="@+id/guideline4" />

    <Switch
        android:id="@+id/switch_ModeChange"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Server"
        app:layout_constraintBottom_toTopOf="@+id/guideline2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/guideline5"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="UseSwitchCompatOrMaterialXml" />
</androidx.constraintlayout.widget.ConstraintLayout>

 


 

3. 撰寫TCP-Server端的主要功能

 

接下來開始編寫主要程式部分

這一次的範例是撰寫TCP-Server的部分

也就是TCPServer.java的這個檔案

截圖 2021-01-09 下午4.08.02

 

我先PO全部給你,再來一部分一部分解釋(;´д`)ゞ

TCPServer.java

class TCPServer implements Runnable {
    public static final String TAG = "MyTCP";
    public static final String RECEIVE_ACTION = "GetTCPReceive";
    public static final String RECEIVE_STRING = "ReceiveString";
    public static final String RECEIVE_BYTES = "ReceiveBytes";
    private int port;
    private boolean isOpen;
    private Context context;
    public ArrayList<ServerSocketThread> SST = new ArrayList<>();
    /**建立建構子*/
    public TCPServer(int port,Context context){
        this.port = port;
        isOpen = true;
        this.context = context;
    }
    //取得開啟狀態
    public boolean getStatus(){
        return isOpen;
    }
    //關閉伺服器
    public void closeServer(){
        isOpen = false;
        //找出所有正在連線的裝置執行緒,並一一清除、斷線
        for (ServerSocketThread s : SST){
            s.isRun = false;
        }
        SST.clear();
    }
    /**取得Socket許可(握手)*/
    private Socket getSocket(ServerSocket serverSocket){
        try {
            return serverSocket.accept();
        } catch (IOException e) {
            e.printStackTrace();
            Log.e(TAG, "更新Server狀態");
            return null;
        }
    }

    @Override
    public void run() {
        try {
            /**在本機的Port上開啟伺服器*/
            ServerSocket serverSocket = new ServerSocket(port);
            //設置Timeout,以便更新裝置連進來的狀況
            serverSocket.setSoTimeout(2000);
            while (isOpen){
                Log.e(TAG, "監測裝置輸入...");
                if (!isOpen) break;
                Socket socket = getSocket(serverSocket);
                if (socket != null){
                    //如果Socket不為null,表示有裝置連入了
                    new ServerSocketThread(socket,context);
                }
            }

            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**監聽裝置連入與收發狀態之執行緒*/
    public class ServerSocketThread extends Thread{
        private Socket socket;
        private PrintWriter pw;
        private InputStream is;
        private boolean isRun = true;
        private Context context;

        ServerSocketThread(Socket socket, Context context){
            this.socket = socket;
            this.context = context;
            String ip = socket.getInetAddress().toString();
            Log.d(TAG, "檢測到新的裝置,Ip: " + ip);

            try {
                socket.setSoTimeout(2000);
                OutputStream os = socket.getOutputStream();
                is = socket.getInputStream();
                pw = new PrintWriter(os,true);
                start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public void sendData(String msg){
            pw.print(msg);
            pw.flush();
        }

        @Override
        public void run() {
            byte[] buff = new byte[100];
            SST.add(this);
            while (isRun && !socket.isClosed() && !socket.isInputShutdown()){
                try {
                    //監聽訊息是否有送過來
                    int rcvLen;
                    if ((rcvLen = is.read(buff)) != -1 ){
                        String string = new String(buff, 0, rcvLen);
                        Log.d(TAG, "收到訊息: " + string);
                        /**收到訊息後,以廣播的方式回傳到Activity*/
                        Intent intent = new Intent();
                        intent.setAction(RECEIVE_ACTION);
                        intent.putExtra(RECEIVE_STRING,string);
                        intent.putExtra(RECEIVE_BYTES, buff);
                        context.sendBroadcast(intent);

                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            //跳出While迴圈即為斷開連線
            try {
                socket.close();
                SST.clear();
                Log.e(TAG, "關閉Server");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 

以上就是重點程式的部分

可以注意到我有標注到粉底白字的部分

那部分也是個重點,不過請容我一部份一部份解釋

首先是在本類別中提供外部接口的部分

 

//取得開啟狀態
public boolean getStatus(){
    return isOpen;
}
//關閉伺服器
public void closeServer(){
    isOpen = false;
    //找出所有正在連線的裝置執行緒,並一一清除、斷線
    for (ServerSocketThread s : SST){
        s.isRun = false;
    }
    SST.clear();
}

 

再來是重點部分

@Override
public void run() {
    try {
        /**在本機的Port上開啟伺服器*/
        ServerSocket serverSocket = new ServerSocket(port);
        //設置Timeout,以便更新裝置連進來的狀況
        serverSocket.setSoTimeout(2000);
        while (isOpen){
            Log.e(TAG, "監測裝置輸入...");
            if (!isOpen) break;
            Socket socket = getSocket(serverSocket);
            if (socket != null){
                //如果Socket不為null,表示有裝置連入了
                new ServerSocketThread(socket,context);
            }
        }

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

這個部分是利用while迴圈持續監視是否有裝置連入

而TCP連線特殊的地方是,他必須設置粉底白字的Timeout的部分

而每次達到TimeOut時,While迴圈會重新跑一次

這時候就會重新監測裝置的連入

我已經在程式中寫好Log了,看著Log就可以了解到其中意義

 

藍底白字的部分則是取得在迴圈時Socket的連線狀態

其對應的副程式在這邊

 

/**取得Socket許可(握手)*/
private Socket getSocket(ServerSocket serverSocket){
    try {
        return serverSocket.accept();
    } catch (IOException e) {
        e.printStackTrace();
        Log.e(TAG, "更新Server狀態");
        return null;
    }
}

如果在Timeout前有裝置連入,getSocket便會回傳serverSocket.accept()

而只要有拿到Socket,便會進入到下面ServerSocketThread取得資訊並建立監聽

 

再來是ServerSocketThread的部分

以及其對應的ArrayList<ServerSocketThread> SST = new ArrayList<>();

 

這個陣列是用來儲存連線中的裝置資訊

在這個Thread中可以獲得連入裝置的資訊(主要是ip)

也因此,如果去檢測一下陣列SST的長度的話,就可以知道有多少裝置在線囉

 

那麼,來PO一下內容

/**監聽裝置連入與收發狀態之執行緒*/
public class ServerSocketThread extends Thread{
    private Socket socket;
    private PrintWriter pw;
    private InputStream is;
    private boolean isRun = true;
    private Context context;

    ServerSocketThread(Socket socket, Context context){
        this.socket = socket;
        this.context = context;
        String ip = socket.getInetAddress().toString();
        Log.d(TAG, "檢測到新的裝置,Ip: " + ip);

        try {
            socket.setSoTimeout(2000);
            OutputStream os = socket.getOutputStream();
            is = socket.getInputStream();
            pw = new PrintWriter(os,true);
            start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void sendData(String msg){
        pw.print(msg);
        pw.flush();
    }

    @Override
    public void run() {
        byte[] buff = new byte[100];
        SST.add(this);
        while (isRun && !socket.isClosed() && !socket.isInputShutdown()){
            try {
                //監聽訊息是否有送過來
                int rcvLen;
                if ((rcvLen = is.read(buff)) != -1 ){
                    String string = new String(buff, 0, rcvLen);
                    Log.d(TAG, "收到訊息: " + string);
                    /**收到訊息後,以廣播的方式回傳到Activity*/
                    Intent intent = new Intent();
                    intent.setAction(RECEIVE_ACTION);
                    intent.putExtra(RECEIVE_STRING,string);
                    intent.putExtra(RECEIVE_BYTES, buff);
                    context.sendBroadcast(intent);

                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //跳出While迴圈即為斷開連線
        try {
            socket.close();
            SST.clear();
            Log.e(TAG, "關閉Server");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

所以,這時候再回去看一下關閉Server的程式

public void closeServer(){
    isOpen = false;
    //找出所有正在連線的裝置執行緒,並一一清除、斷線
    for (ServerSocketThread s : SST){
        s.isRun = false;
    }
    SST.clear();
}

 

就能稍微理解一二了(・ω・)b

最後是取得資訊並把資料傳回Activity

使用以下這個部分

@Override
public void run() {
    byte[] buff = new byte[100];
    SST.add(this);
    while (isRun && !socket.isClosed() && !socket.isInputShutdown()){
        try {
            //監聽訊息是否有送過來
            int rcvLen;
            if ((rcvLen = is.read(buff)) != -1 ){
                String string = new String(buff, 0, rcvLen);
                Log.d(TAG, "收到訊息: " + string);
                /**收到訊息後,以廣播的方式回傳到Activity*/
                Intent intent = new Intent();
                intent.setAction(RECEIVE_ACTION);
                intent.putExtra(RECEIVE_STRING,string);
                intent.putExtra(RECEIVE_BYTES, buff);
                context.sendBroadcast(intent);

            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //跳出While迴圈即為斷開連線
    try {
        socket.close();
        SST.clear();
        Log.e(TAG, "關閉Server");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

一樣使用廣播(粉底白字)的方式,就能將收到的訊息傳到Activity囉!

 


 

4. 撰寫主要程式

 

接下來就是TCP的Activity的部分

也就是TCPActivity.java這個檔案

這次我先不PO全部,因為會混雜到下篇要聊到的TCPClient的程式

最後我當然會PO全部

 

Server端+UI設置的程式如下

public class TCPActivity extends AppCompatActivity {

    EditText edRemoteIp, edLocalPort, edReceiveMessage, edInputBox, edRemotePort;
    ToggleButton btClientConnect, btOpenServer, btServer;
    @SuppressLint("UseSwitchCompatOrMaterialCode")
    Switch swFunction;
    ExecutorService exec = Executors.newCachedThreadPool();
    MyBroadcast myBroadcast = new MyBroadcast();
    StringBuffer stringBuffer = new StringBuffer();
    TCPServer tcpServer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_t_c_p);
        //設置基本UI
        setBaseUI();
        //設置TCP伺服器功能
        setServerSwitch();
        //設置發送資料功能(伺服器)
        setSendFunction();
        //註冊廣播,接收來自對方設備回傳的資料
        IntentFilter intentFilter = new IntentFilter(TCPServer.RECEIVE_ACTION);
        registerReceiver(myBroadcast, intentFilter);
    }

    private void setBaseUI() {
        TextView tvLocalIp = findViewById(R.id.textView_IP);
        tvLocalIp.setText("本機IP: " + getLocalIP(this));
        Button btClear = findViewById(R.id.button_clear);
        btClear.setOnClickListener(v -> {
            stringBuffer.delete(0, stringBuffer.length());
            edReceiveMessage.setText(stringBuffer);
        });
        btOpenServer = findViewById(R.id.toggleButton_Server);
        btClientConnect = findViewById(R.id.toggleButton_ClientConnect);
        edRemoteIp = findViewById(R.id.editText_RemoteIp);
        edRemotePort = findViewById(R.id.editText_RemotePort);
        edLocalPort = findViewById(R.id.editText_Port);
        edReceiveMessage = findViewById(R.id.editText_ReceiveMessage);
        edInputBox = findViewById(R.id.editText_Input);
        swFunction = findViewById(R.id.switch_ModeChange);
        swFunction.setChecked(false);
        //設置模式切換之Switch
        swFunction.setOnCheckedChangeListener((buttonView, isChecked) -> {
            btOpenServer.setEnabled(!isChecked);
            btClientConnect.setEnabled(isChecked);
            if (!isChecked) {
                swFunction.setText("Server");
            } else {
                if (tcpServer != null && tcpServer.getStatus()) {
                    tcpServer.closeServer();
                    btServer.setChecked(false);
                }
                swFunction.setText("Client");
            }
        });
    }
    /**設置TCP伺服器功能*/
    private void setServerSwitch() {
        btServer = findViewById(R.id.toggleButton_Server);
        ExecutorService exec = Executors.newCachedThreadPool();
        btServer.setOnCheckedChangeListener((buttonView, isChecked) -> {
            if (isChecked) {
                int port = Integer.parseInt(edLocalPort.getText().toString());
                tcpServer = new TCPServer(port, this);
                exec.execute(tcpServer);
                edLocalPort.setEnabled(false);
            } else {
                tcpServer.closeServer();
                edLocalPort.setEnabled(true);
            }
        });

    }
    
    /**設置發送資料功能(伺服器)*/
    private void setSendFunction() {
        Button btSend = findViewById(R.id.button_Send);
        btSend.setOnClickListener(v -> {
            String text = edInputBox.getText().toString();
          
                //切換開關在Server模式時
                if (tcpServer == null)return;
                if (text.length() == 0 || !tcpServer.getStatus()) return;
                //此處Lambda表達式相等於下方註解部分
                if (tcpServer.SST.size() == 0) return;
                exec.execute(() -> tcpServer.SST.get(0).sendData(text));
//           exec.execute(new Runnable() {
//               @Override
//               public void run() {
//                   tcpServer.SST.get(0).sendData(text);
//               }
//           });
            
        });
    }
    private class MyBroadcast extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String mAction = intent.getAction();
            assert mAction != null;
            /**接收來自TCP回傳之訊息*/
            switch (mAction) {
                case TCPServer.RECEIVE_ACTION:
                    String msg = intent.getStringExtra(TCPServer.RECEIVE_STRING);
                    byte[] bytes = intent.getByteArrayExtra(TCPServer.RECEIVE_BYTES);
                    stringBuffer.append("收到: ").append(msg).append("\n");
                    edReceiveMessage.setText(stringBuffer);
                    break;

            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //取消監聽廣播
        unregisterReceiver(myBroadcast);
    }
}

 

在我的標注中有深紫紅土黃這五個顏色的底

 

深紫紅是剛才我們上面TCP-Server的主要部分

則是開啟Server

是斷開連線

是對連線過來的裝置發送訊息

土黃則是接收客戶端傳送資訊的部分

 

最後,PO一下包含下篇會用到的Client端程式全部PO出來

 

TCPActvity.java

public class TCPActivity extends AppCompatActivity {

    EditText edRemoteIp, edLocalPort, edReceiveMessage, edInputBox, edRemotePort;
    ToggleButton btClientConnect, btOpenServer, btServer;
    @SuppressLint("UseSwitchCompatOrMaterialCode")
    Switch swFunction;
    ExecutorService exec = Executors.newCachedThreadPool();
    MyBroadcast myBroadcast = new MyBroadcast();
    StringBuffer stringBuffer = new StringBuffer();
    TCPServer tcpServer;
    TCPClient tcpClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_t_c_p);
        //設置基本UI
        setBaseUI();
        //設置TCP伺服器功能
        setServerSwitch();
        //設置TCP客戶端功能
        setClientSwitch();
        //設置發送資料功能(含伺服器/客戶端)
        setSendFunction();
        //註冊廣播,接收來自對方設備回傳的資料
        IntentFilter intentFilter = new IntentFilter(TCPServer.RECEIVE_ACTION);
        registerReceiver(myBroadcast, intentFilter);
    }

    private void setBaseUI() {
        TextView tvLocalIp = findViewById(R.id.textView_IP);
        tvLocalIp.setText("本機IP: " + getLocalIP(this));
        Button btClear = findViewById(R.id.button_clear);
        btClear.setOnClickListener(v -> {
            stringBuffer.delete(0, stringBuffer.length());
            edReceiveMessage.setText(stringBuffer);
        });
        btOpenServer = findViewById(R.id.toggleButton_Server);
        btClientConnect = findViewById(R.id.toggleButton_ClientConnect);
        edRemoteIp = findViewById(R.id.editText_RemoteIp);
        edRemotePort = findViewById(R.id.editText_RemotePort);
        edLocalPort = findViewById(R.id.editText_Port);
        edReceiveMessage = findViewById(R.id.editText_ReceiveMessage);
        edInputBox = findViewById(R.id.editText_Input);
        swFunction = findViewById(R.id.switch_ModeChange);
        swFunction.setChecked(false);
        //設置模式切換之Switch
        swFunction.setOnCheckedChangeListener((buttonView, isChecked) -> {
            btOpenServer.setEnabled(!isChecked);
            btClientConnect.setEnabled(isChecked);
            if (!isChecked) {
                if (tcpClient != null && tcpClient.getStatus()) {
                    tcpClient.closeClient();
                    btClientConnect.setChecked(false);
                }
                swFunction.setText("Server");
            } else {
                if (tcpServer != null && tcpServer.getStatus()) {
                    tcpServer.closeServer();
                    btServer.setChecked(false);
                }
                swFunction.setText("Client");
            }
        });
    }
    /**設置TCP伺服器功能*/
    private void setServerSwitch() {
        btServer = findViewById(R.id.toggleButton_Server);
        ExecutorService exec = Executors.newCachedThreadPool();
        btServer.setOnCheckedChangeListener((buttonView, isChecked) -> {
            if (isChecked) {
                int port = Integer.parseInt(edLocalPort.getText().toString());
                tcpServer = new TCPServer(port, this);
                exec.execute(tcpServer);
                edLocalPort.setEnabled(false);
            } else {
                tcpServer.closeServer();
                edLocalPort.setEnabled(true);
            }
        });

    }
    /**設置TCP客戶端功能*/
    private void setClientSwitch() {
        String remoteIp = edRemoteIp.getText().toString();
        int remotePort = Integer.parseInt(edRemotePort.getText().toString());
        btClientConnect.setOnCheckedChangeListener((buttonView, isChecked) -> {
            if (isChecked) {
                tcpClient = new TCPClient(remoteIp, remotePort, this);
                exec.execute(tcpClient);
                edRemoteIp.setEnabled(false);
                edRemotePort.setEnabled(false);

            } else {
                tcpClient.closeClient();
                edRemoteIp.setEnabled(true);
                edRemotePort.setEnabled(true);
            }
        });


    }
    /**設置發送資料功能(含伺服器/客戶端)*/
    private void setSendFunction() {
        Button btSend = findViewById(R.id.button_Send);
        btSend.setOnClickListener(v -> {
            String text = edInputBox.getText().toString();
            if (swFunction.isChecked()) {
                //切換開關在Client模式時
                if (tcpClient == null)return;
                if (text.length() == 0 || !tcpClient.getStatus()) return;
                exec.execute(() -> tcpClient.send(text));
            } else {
                //切換開關在Server模式時
                if (tcpServer == null)return;
                if (text.length() == 0 || !tcpServer.getStatus()) return;
                //此處Lambda表達式相等於下方註解部分
                if (tcpServer.SST.size() == 0) return;
                exec.execute(() -> tcpServer.SST.get(0).sendData(text));
//           exec.execute(new Runnable() {
//               @Override
//               public void run() {
//                   tcpServer.SST.get(0).sendData(text);
//               }
//           });
            }
        });
    }
    private class MyBroadcast extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String mAction = intent.getAction();
            assert mAction != null;
            /**接收來自UDP回傳之訊息*/
            switch (mAction) {
                case TCPServer.RECEIVE_ACTION:
                    String msg = intent.getStringExtra(TCPServer.RECEIVE_STRING);
                    byte[] bytes = intent.getByteArrayExtra(TCPServer.RECEIVE_BYTES);
                    stringBuffer.append("收到: ").append(msg).append("\n");
                    edReceiveMessage.setText(stringBuffer);
                    break;

            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //取消監聽廣播
        unregisterReceiver(myBroadcast);
    }
}

 


 

到此為止完成了TCP的伺服器端了

其實TCP伺服器的原理是廣泛被應用在所有的線上遊戲中

我們今天做的內容相當於是遊戲運營商在開啟伺服器的行為

也就是說,遊戲運營商為何能夠抓到玩家IP國籍等等資訊,跟這次的內容大有相關

 

好喔,那今天的文章到此,希望本文能對你有幫助

TK

arrow
arrow

    碼農日常 發表在 痞客邦 留言(16) 人氣()