Android7.1 與 PC端 套接字 socket Tcp/Ip 通訊(多線程)

最近有點閒,打算經過 socket 實現 PC 調用手機攝像頭,實現實時視頻傳輸。搞到後來發現搞不了, java 和 c++ 的基本數據類型就有差異,要通過轉換才行,天然兩者的套接字也是不能直接傳數據的。我在手機上運行本身寫的客戶端,PC 上運行C++開發的服務端,能鏈接上可是傳輸不了數據(不能直接傳輸)。因此不搞了,太麻煩。。。是我太菜哈哈哈哈哈,,,,,java

不過仍是把最後結果在這裏分享一下,由於這個過程那叫個坎坷哦,網上的確能找到不少這方面的博客什麼的,可是沒有一個能跑的通的代碼,不知道是否是Android版本不一樣致使的(參考的那些帖也沒有給出開發環境),反正我手機上就是運行不了(確切說是能運行,但發不了數據,總是崩潰), 花了  ≈Android小白 的我好幾天時間才發現問題所在,沒錯!是的!!線程的問題!!!android

因爲參考了不知道多少個博客和帖子,因此這裏不一一列出了,說實話我本身都記不清看(抄)了多少代碼了。。。那些代碼大部分都是開了一共兩個線程,即一個主線程用於UI顯示,一個子線程用於創建 socket 鏈接並傳送數據。但我在我手機上運行時老是崩潰,具體表現爲:UI能正常顯示;能經過socket創建鏈接;不能發送數據,一點擊發送按鈕,程序就閃退。開始我覺得是我哪裏設置的不對,後來搞了好幾天,把代碼改的面目全非,結構都變了,仍是不行,並且不知道是客戶端的問題仍是服務器的問題。c++

後來呢,柳暗花明,機智(愚蠢)的我終於仍是弄清了緣由:服務器

(1). 客戶端的問題;多線程

(2). 一個線程中不能創建鏈接的同時還發送數據。併發

那怎麼辦呢?改客戶端代碼唄,子線程再建子線程用於發送數據。app

下面進入正題,首先給出我開發環境:socket

IDE: Android Studio 2.3.3tcp

API: android-25(Android 7.1)ide

JDK: 1.8.0

創建 AS 工程時,我設置的最低SDK支持版本是 24(Android 7.0),個人手機是基於 Android 7.1 的系統,是直接用手機鏈接電腦調試的,沒有用虛擬設備。

原始代碼是從網上某個博主哪裏找來的,不過客戶端已經被我改的面目全非了,除了按鈕的名字,差很少看不到原來的樣子了。。服務端卻是沒怎麼改。

多線程我是用線程池實現的,所以不瞭解線程池的小夥伴本身先去了解一下,相比於普通的多線程,線程池有不少優勢(避免重複開闢新線程,節省資源啥的...)。多線程有不少坑,好比某些東西不能放在某些地方,我也是踩了不少,一遍遍試過來的,程序裏面我也註釋了一點。

1. 程序結構、功能、界面

程序開始運行,設置UI界面,此時運行的是主線程。

而後,主線程新建一個線程,用於創建 socket 鏈接。鏈接成功後最上面的地方會顯示服務器端發送的消息(相似於「服務器/192.168.0.101加入;此時總鏈接:1(服務器發送)」),若是顯示的是「TCP-IP」(即項目名稱),就說明沒連上。

而後在新線程中監聽按鈕,當按鈕被按下時,再新開子線程用於發送數據。

界面佈局以下:

2. 代碼

一共在 AS 下創建了兩個工程,用兩個 AS 窗口分別顯示。

一個工程名爲 Tcp-Ip,這個工程爲客戶端程序,運行在手機端,是一個正常的 Android 工程。工程目錄如圖:

另外一個工程名爲 Tcp-Ip-Server,這個工程爲服務端程序,運行在PC端,是一個空的 Android 工程,裏面添加了名爲 TCPServer 的模塊(Java Library型的module),運行的時候也是隻運行這個模塊(詳情本身百度,就是在 Android Studio 下開發普通java程序)。工程目錄如圖:

 

客戶端

 AndroidManifest.xml,能夠參考一下,主要是添加權限:

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

    <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=".SocketActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>

SocketActivity.java,主程序:

package com.example.gao.tcp_ip;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SocketActivity extends AppCompatActivity {

    public TextView tv_msg = null;
    public EditText ed_msg = null;
    public Button btn_send = null;
    public Button btn_break = null;
    public String content = "";
    public String temp = "";
    private ExecutorService SocketService = Executors.newSingleThreadExecutor(); //建立單線程池,用於套接字鏈接

    // 接收線程發送過來信息,並用TextView顯示
    public Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            tv_msg.setText(content);
        }
    };

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

        tv_msg = (TextView) findViewById(R.id.tv_contents);
        ed_msg = (EditText) findViewById(R.id.et_content);
        btn_send = (Button) findViewById(R.id.bt_sender);
        btn_break = (Button) findViewById(R.id.bt_break);

        SocketService.execute(socketService);  // 在線程池中開啓新線程socketService
    }

    /// 定義Runnable接口變量socketService完成的功能
    Runnable socketService = new Runnable(){
        // Socket變量不能定義在run中,不然不能在按鈕響應函數中調用,BufferedReader、PrintWriter好像能夠
        private Socket socket = null;
        private BufferedReader in = null;
        private PrintWriter out = null;
        private static final String HOST = "192.168.0.100";
        private static final int PORT = 6666;
        private String msg;
        private ExecutorService SendService = Executors.newSingleThreadExecutor(); //建立單線程池,用於發送數據

        @Override
        public void run(){
            try {
                //創建套接字鏈接,獲取輸入輸出流
                socket = new Socket(HOST, PORT);
                in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);

                while (true) {
                    // 經過輸入流從服務器獲取數據併發送給主線程
                    if (!socket.isClosed() && socket.isConnected() && !socket.isInputShutdown()) {
                        if ((temp = in.readLine()) != null) {
                            content += temp + "\n";
                            mHandler.sendMessage(mHandler.obtainMessage());
                        }
                    }

                    //獲取編輯框內容並開啓新線程發送
                    btn_send.setOnClickListener( new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            msg = ed_msg.getText().toString();              //獲取發送數據
                            ed_msg.getText().clear();
                            if (socket.isConnected() && !socket.isOutputShutdown()) {
                                if(out != null) {
                                    SendService.execute(sendService);       //開啓發送數據線程
                                }
                            }
                        }
                    });

                    // 開啓新線程發送"exit"信號
                    btn_break.setOnClickListener( new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            msg = "exit";                                   //定義發送數據
                            if (socket.isConnected() && !socket.isOutputShutdown()) {
                                if(out != null) {
                                    SendService.execute(sendService);       //開啓發送數據線程
                                }
                            }
                        }
                    });
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //定義發送消息接口
        Runnable sendService = new Runnable(){
            @Override
            public void run() {
                out.println(msg);   //經過輸出流發送數據
            }
        };
    };

    public void exit(View view){
        finish();
    }
}

activity_socket.xml,佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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="com.example.gao.tcp_ip.SocketActivity">

    <TextView
        android:id="@+id/tv_contents"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="@string/app_name"
        tools:layout_constraintTop_creator="1"
        tools:layout_constraintRight_creator="1"
        tools:layout_constraintBottom_creator="1"
        app:layout_constraintBottom_toTopOf="@+id/et_content"
        android:layout_marginStart="2dp"
        android:layout_marginEnd="2dp"
        app:layout_constraintRight_toRightOf="parent"
        tools:layout_constraintLeft_creator="1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0" />

    <EditText
        android:id="@+id/et_content"
        android:layout_width="0dp"
        android:layout_height="54dp"
        android:inputType="text"
        tools:layout_constraintTop_creator="1"
        tools:layout_constraintRight_creator="1"
        android:layout_marginStart="2dp"
        android:layout_marginEnd="2dp"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginTop="194dp"
        tools:layout_constraintLeft_creator="1"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        tools:layout_constraintTop_creator="1"
        tools:layout_constraintLeft_creator="1"
        app:layout_constraintLeft_toLeftOf="@+id/tv_contents"
        app:layout_constraintTop_toTopOf="parent"
        android:id="@+id/linearLayout">

    </LinearLayout>

    <Button
        android:id="@+id/bt_sender"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="發送"
        android:layout_marginStart="23dp"
        tools:layout_constraintTop_creator="1"
        app:layout_constraintTop_toBottomOf="@+id/et_content"
        tools:layout_constraintLeft_creator="1"
        app:layout_constraintLeft_toLeftOf="@+id/et_content" />

    <Button
        android:id="@+id/bt_break"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="斷開鏈接"
        android:layout_marginStart="35dp"
        tools:layout_constraintTop_creator="1"
        app:layout_constraintTop_toBottomOf="@+id/et_content"
        tools:layout_constraintLeft_creator="1"
        app:layout_constraintLeft_toRightOf="@+id/bt_sender" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="退出"
        android:onClick="exit"
        android:layout_marginEnd="13dp"
        tools:layout_constraintTop_creator="1"
        tools:layout_constraintRight_creator="1"
        app:layout_constraintRight_toRightOf="@+id/et_content"
        app:layout_constraintTop_toBottomOf="@+id/et_content" />

</android.support.constraint.ConstraintLayout>

 

服務端

相比於客戶端,服務器端的代碼我沒有怎麼改(由於它能直接用),就是差很少直接用的別人的博客裏的,具體哪一個忘了。。。。

因爲運行的是 java 模塊(MainActivity和activity_main.xml壓根就沒用),因此我只貼模塊的代碼。

TCPServer.java:

package com.example;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TCPServer {
    private static final int PORT = 6666;
    private List<Socket> mList = new ArrayList<>();
    private ServerSocket server = null;
    private ExecutorService mExecutorService = null; // 建立一個線程池

    public static void main(String[] args) {
        new TCPServer();
    }

    public TCPServer() {
        try {
            server = new ServerSocket(PORT);
            mExecutorService = Executors.newCachedThreadPool(); // 實例化一個線程池
            System.out.println("服務器已啓動,等待加入...");
            Socket client = null;
            while (true) {
                client = server.accept();
                // 把客戶端放入客戶端集合中
                mList.add(client);
                mExecutorService.execute(new Service(client)); // 開啓一個新線程
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /// 擴展Runnable接口
    class Service implements Runnable {
        private Socket socket;
        private BufferedReader in = null;
        private String msg = " ";

        public Service(Socket socket) {
            this.socket = socket;
            try {
                in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                // 客戶端只要一連到服務器,便向客戶端發送下面的信息。
                msg = "服務器" + this.socket.getInetAddress() + "加入;此時總鏈接:" + mList.size() + "(服務器發送)";
                this.sendmsg();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            try {
                while (true) {
                    if ((msg = in.readLine()) != null) {
                        // 當客戶端發送的信息爲:exit時,關閉鏈接
                        if (msg.trim().equals("exit")) {
                            System.out.println("GAME OVER");
                            mList.remove(socket);
                            in.close();
                            msg = "服務器" + socket.getInetAddress() + "退出;此時總鏈接:" + mList.size() + "(服務器發送)";
                            socket.close();
                            this.sendmsg();
                            break;
                        } else {
                            // 接收客戶端發過來的信息msg,而後發送給客戶端。
                            msg = socket.getInetAddress() + "" + msg + "(服務器發送)";
                            this.sendmsg();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        /**
         * 循環遍歷客戶端集合,給每一個客戶端都發送信息。
         */
        public void sendmsg() {
            // 在服務器上打印
            System.out.println(msg);
            // 遍歷打印到每一個客戶端上
            int num = mList.size();
            for (int i = 0; i < num; i++) {
                Socket mSocket = mList.get(i);

                PrintWriter out = null;
                try {
                    out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream())), true);
                    out.println(msg);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

當客戶端鏈接併發送數據時,運行效果以下:

 

最後的最後,再來愉快(juewang)地吐槽一下吧,用 Qt 或者 C++ 庫很簡單就能實現的 socket,爲啥 Android 就這麼費勁呢???這麼一個簡單的功能,要搞這麼多線程,讓整個程序看起來很複雜。。。。哎

目前看來,c++ 的 socket 客戶端只能跟 c++ 的服務端通訊,java 的 socket 客戶端只能跟 java 的服務端通訊,跨越語言的通訊,,,,,我是不行了,。。。

相關文章
相關標籤/搜索