Android實現Thrift服務端與客戶端

david大偉哥發表於2017-08-12

這一篇是將Android和C#實現Thrift服務端和客戶端中Android部分單獨拆分開來的,方便不需要C#的開發者使用。
編寫Thrift檔案
寫個簡單的,有輸入引數,無返回值,檔案命名為 HelloWorld.thrift

service HelloWorldService{
    void SayHello(1:string msg);
}

根據Thrift檔案生成對應的java檔案,生成的java只有一個檔案。

這裡寫圖片描述

可以看到我用的是Thrift 0.10.0版本
Android Thrift服務端和客戶端實現,並實現訊息傳送及接收

先看最後實現的效果
這裡寫圖片描述
首先點選【啟動服務】按鈕,然後填寫正確的Ip、埠號,點選【傳送】即可完成訊息傳送。將Ip改成C#服務端ip(192.168.1.XX)和埠號即可完成與C#服務端之間的通訊。

新建一個Android專案:Thrift_HelloWorld_Android,將生成的Java檔案拷入專案的thrift_hellowrold_android資料夾內,目錄結構如下圖:

這裡寫圖片描述

我使用IDE的Android Studio,新增thrift依賴項,類似eclipse的maven。在Gradle Scripts下的build.gradle(Module.app)檔案中新增:

    compile 'org.apache.thrift:libthrift:0.10.0'
    compile 'org.glassfish.main:javax.annotation:4.0-b33'

這裡寫圖片描述

這裡還有一步比較重要,為thrift生成的java檔案新增包。因為生成的java檔案沒有設定package,而在java中沒有包是個很麻煩的事,我對java不熟,最簡單的方式就是為所有檔案新增package:

package com.david.thrift_helloworld_android;

這裡寫圖片描述

Thrift準備工作完成了,我們開始做Android的介面展示部分。開啟app\res\activity_main.xml檔案,修改為:

<?xml version="1.0" encoding="utf-8"?>
<AbsoluteLayout 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.david.thrift_helloworld_android.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_y="15dp"
        android:text="服務端埠號:"
        />
    <EditText
        android:id="@+id/edtSrvPort"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_x="100dp"
        android:width="60dp"
        android:text="1234"
        />
    <Button
        android:id="@+id/btnStart"
        android:layout_y="0dp"
        android:layout_x="170dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="啟動服務"/>
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_y="55dp"
    android:text="服務端地址"
    />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_y="75dp"
        android:layout_x="40dp"
        android:text="IP:"
        />
    <EditText
        android:id="@+id/edtCliIP"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_y="60dp"
        android:layout_x="100dp"
        android:width="160dp"
        android:text="127.0.0.1"
        />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_y="105dp"
        android:layout_x="40dp"
        android:text="埠號:"
        />
    <EditText
        android:id="@+id/edtCliPort"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_y="90dp"
        android:layout_x="100dp"
        android:width="60dp"
        android:text="1234"
        />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_y="135dp"
        android:text="訊息內容:"
        />
    <EditText
        android:id="@+id/edtMsg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_y="120dp"
        android:layout_x="100dp"
        android:width="260dp"
        android:text="hello"
        />
    <Button
        android:id="@+id/btnSend"
        android:layout_y="160dp"
        android:layout_x="100dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="傳送"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_y="235dp"
        android:text="接收到的訊息:"
        />
    <TextView
        android:id="@+id/tvMsg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:layout_x="0dp"
        android:layout_y="258dp"
        android:width="350dp"/>
</AbsoluteLayout>

此程式碼編寫了一個絕對的佈局,因為我對android開發不熟悉,先搞個最簡單的佈局湊合。佈局中有幾個輸入框、兩個點選按鈕、一個訊息展示控制元件。

首先在MainActivity中定義一個Handler,用於將訊息顯示到展示控制元件中。

    public Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            if (msg.what == 0x8090) {
                String str = (String) msg.getData().get("msg");
                final TextView tvMsg = (TextView) findViewById(R.id.tvMsg);
                tvMsg.setText(str);//Activity上的TextView元素顯示訊息內容
            }
        }
    };

編寫Thrift Client程式碼,完成訊息傳送功能,此為MainActivity內部類。

 public class HelloWorldClient {
        public int Port;
        public String IP;
        public String Msg;

        void CallServer(String ip, int port, String msg) throws TException {
            Port = port;
            IP = ip;
            Msg = msg;

            Runnable simple = new Runnable() {
                public void run() {
                    TTransport transport = new TSocket(IP, Port);
                    try {

                        transport.open();
                        TProtocol protocol = new TBinaryProtocol(transport);
                        HelloWorldService.Client client = new HelloWorldService.Client(protocol);
                        client.SayHello(Msg);
                    } catch (TException ex) {
                        Message msg = new Message();
                        msg.what = 0x8090;
                        msg.getData().putString("msg", ex.getMessage());
                        MainActivity.this.handler.sendMessage(msg);
                    } finally {
                        transport.close();
                    }
                }
            };

            new Thread(simple).start();
        }
    }

編寫Thrift Server服務功能,首先實現服務介面,然後開啟執行緒執行服務。此為MainActivity內部類。

    public class HelloWorldServiceImpl implements HelloWorldService.Iface {
        @Override
        public void SayHello(String str) throws TException {
            Message msg = new Message();
            msg.what = 0x8090;
            msg.getData().putString("msg", str);
            MainActivity.this.handler.sendMessage(msg);
        }
    }

    public class HelloWorldServe {
        public HelloWorldServiceImpl handler;

        public HelloWorldService.Processor processor;

        public void Run() {
            try {
                handler = new HelloWorldServiceImpl();
                processor = new HelloWorldService.Processor(handler);

                Runnable simple = new Runnable() {
                    public void run() {
                        simple(processor);
                    }
                };

                new Thread(simple).start();

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

        public void simple(HelloWorldService.Processor processor) {
            try {

                final EditText etPort = (EditText) findViewById(R.id.edtSrvPort);
                String port = etPort.getText().toString();
                int p = Integer.parseInt(port);

                TServerTransport serverTransport = new TServerSocket(p);
                //TServer server = new TSimpleServer(new TServer.Args(serverTransport).processor(processor));

                // Use this for a multithreaded server
                TServer server = new TThreadPoolServer(new TThreadPoolServer.Args(serverTransport).processor(processor));

                System.out.println("Starting the simple server...");

                Message msg = new Message();
                msg.what = 0x8090;
                msg.getData().putString("msg", "Starting");
                MainActivity.this.handler.sendMessage(msg);
                server.serve();
            } catch (Exception e) {
                Message msg = new Message();
                msg.what = 0x8090;
                msg.getData().putString("msg", e.getMessage());
                e.printStackTrace();
            }
        }
    }

接來下為Button按鈕新增事件,有兩個按鈕,分別為【啟動服務】和【傳送】。程式碼在MainActivity中。

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

        final Button btnServer = (Button) findViewById(R.id.btnStart);
        btnServer.setOnClickListener(new View.OnClickListener() {
                                         @Override
                                         public void onClick(View v) {
                                             HelloWorldServe srv = new HelloWorldServe();
                                             srv.Run();
                                             btnServer.setEnabled(false);
                                         }
                                     }
        );

        final Button btn = (Button) findViewById(R.id.btnSend);
        //設定點選後TextView現實的內容
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub

                try {
                    final EditText cPort = (EditText) findViewById(R.id.edtCliPort);
                    String port = cPort.getText().toString();
                    int p = Integer.parseInt(port);

                    final EditText cIp = (EditText) findViewById(R.id.edtCliIP);
                    String ip = cIp.getText().toString();

                    final EditText cMsg = (EditText) findViewById(R.id.edtMsg);
                    String msg = cMsg.getText().toString();

                    HelloWorldClient cli = new HelloWorldClient();
                    cli.CallServer(ip, p, msg);

                } catch (TException ex) {
                    Toast.makeText(MainActivity.this, ex.getMessage(), Toast.LENGTH_LONG).show();
                }
            }
        });
    }

程式碼結構圖:
這裡寫圖片描述
ok,完成程式碼編寫,接下來說幾個在執行時會發生的異常。

網路許可權
thrift client連線外部網路時需要申請系統網路許可權,設定的地方在AndroidManifest.xml檔案,具體如下圖:

這裡寫圖片描述

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

虛擬機器埠對映
還有一個很重要的地方,因為是在安卓虛擬機器上開發,在執行thrift server時申請的埠號並不是本機PC的埠號,會發現在本機中並沒有開啟此埠號。因為這個問題我搞了好久,一直以為是自己的服務開啟有問題,之後才找到原因是申請的埠號為安卓虛擬機器內埠號。可以參考一下Android筆記:模擬器之間的Socket通訊。簡單的說就是要把安卓虛擬機器內埠號對映為PC機埠號。接下來詳細介紹如何設定安卓虛擬機器埠對映。

1 檢視PC埠號
首先看一下PC埠號1234是否被佔用,因為安卓模擬器上開的是1234埠,PC也用1234埠,當然也可以改成其他埠號。
在PC命令列中輸入 netstat -ano 命令檢視所有被佔用的埠,確認沒有被佔用,如果被佔用則可以換其他埠。
也可以在PC命令列中輸入 netstat -ano | findstr 1234 檢視所有匹配的埠占用情況。
2 開啟虛擬機器
這個就沒什麼好講的了,要設定當然需要先把安卓虛擬機器啟動起來。
3 通過telnet進入虛擬機器
在PC命令列中輸入 telnet 127.0.0.1 5554 進入虛擬機器,5554為虛擬機器的埠號,不要和剛才的那個thrift server埠號搞混了。
可能會出現錯誤

telnet不是內部或外部命令

是因為PC沒有開啟telnet客戶端,參考Win7如何解決telnet不是內部或外部命令的方案!
在輸入telnet 127.0.0.1 5554 後可能會出現

Android Console: Authentication required

這裡寫圖片描述

解決方案:[tsackoverflow]Android Console: Authentication require

根據提示找到“C:\Users\Administrator.emulator_console_auth_token”檔案並開啟:
這裡寫圖片描述
裡面就一串字串,複製該字串,在剛才telnet的命令列中輸入:

auth jSuaJlZp9P74fVh2

這裡寫圖片描述

就完成了認證。若不完成此步,就不能成功設定埠對映。

4 埠對映

redir add tcp:1234:1234

ok,完成了這些。
若第3步沒有完成會提示錯誤:

KO: unknown command, try 'help'

這裡寫圖片描述

完成埠對映後,開啟android thrift服務端,在PC命令列中輸入

netstat -ano | findstr 1234

檢視1234埠是否開啟,若開啟則對映成功。

ADB檢視埠占用
還有一個要說的,就是通過ADB檢視埠是否佔用,這裡有兩個意思,一是開啟服務前檢視需要使用的埠是否已經別佔用,二是在服務開啟後判斷服務是否開啟成功,通過檢視埠是否被佔用即可。
在PC中,通過命令列進入到adb.exe所在資料夾輸入

adb -s emulator-5554 shell

進入虛擬機器,輸入

netstat -apn | grep 1234

檢視埠1234是否被佔用。

這裡寫圖片描述

原始碼地址:
Android上實現的Thrift服務端與客戶端

相關文章