这一篇是将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服务端与客户端