Android客户端app由3个activity组成,如下图
目录结构如图
menifest.xml文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ouling.Ex_AcontrolPC_A"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" >
</uses-permission>
<application
android:allowBackup="true"
android:icon="@drawable/icon"
android:label="@string/app_name" >
<activity
android:name=".ScanActivity"
android:theme="@android:style/Theme.Dialog" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ReScanActivity"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:theme="@android:style/Theme.Dialog" >
</activity>
<activity
android:name=".MainActivity"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:theme="@android:style/Theme.Dialog" >
</activity>
</application>
</manifest>
可以看出,首先进到的是ScanActivity,这个activity是用来扫描当前连接的所在网段内所有设备,看是否有开启30000端口的服务。核心代码如下
<span style="white-space:pre"> </span>class ScanIPThread extends Thread{
@Override
public void run() {
serverIP = myWifi.getServerIp();
int t = serverIP.lastIndexOf(".")+1;
resultIP = serverIP.substring(0, t);
boolean flag = false;
for(int i=1;i<255;i++){
try {
Socket socket = new Socket();
InetSocketAddress s = new InetSocketAddress(resultIP+i, 30000);
socket.connect(s,50);
Message message = new Message();
message.what=1000;
message.obj=resultIP+i;
handler.sendMessage(message);
flag =true;
socket.close();
break;
} catch (IOException e) {
handler.sendEmptyMessage(i);
}
}
if(!flag){
handler.sendEmptyMessage(2000);
}
super.run();
}
<span style="white-space:pre"> </span>}
这个线程就是用来扫描网段内哪些IP开启了30000端口的。为什么要开个子线程?因为主线程是用于图形界面交互的,而这里的操作是阻塞的,如果占用主线程会挂掉。android4.0以上的版本都不允许在主线程中添加耗时操作了。
在Socket中有个方法connect(SocketAddress endpoint,int timeout),endpoint是服务器地址,timeout是超时值,在建立连接或超时之前,连接一直处于阻塞状态。因为要扫描的地址不多,所以可以用for来遍历,如果扫描的地址过多,可能得另想办法了。这里采用的思路是如果连接上了,就把当前服务器的地址记录下来并结束扫描,当连接超时或者连接出异常则继续尝试连接下个地址。看起来貌似没有问题,但是,那么问题来了,怎么知道要扫描那个网段呢??
这就要用到android中的WiFi服务了。WifiManager可以get到DhcpInfo的实例,DhcpInfo里面有个serverAddress保存着连接主机的ip地址,但是这个ip地址是int型的,需要用以下方法解析成*.*.*.*格式
public String FormatString(int value){
String strValue="";
byte[] ary = intToByteArray(value);
for(int i=ary.length-1;i>=0;i--){
strValue+=(ary[i]&0xFF);
if(i>0){
strValue+=".";
}
}
return strValue;
}
public byte[] intToByteArray(int value){
byte[] b=new byte[4];
for(int i=0;i<4;i++){
int offset = (b.length-1-i)*8;
b[i]=(byte) ((value>>>offset)&0xFF);
}
return b;
}
当所以地址都扫描完后还没有找到可连接的地址,则跳到ReScanActivity,可以重新扫描或者退出,上面的图中可以看到。如果找到了就用handler机制将IP发送回来,再用intent传递IP跳转到MainActivity。
<span style="white-space:pre"> </span>case 1000:
<span style="white-space:pre"> </span>Toast.makeText(ScanActivity.this, "找到可连接PC", Toast.LENGTH_SHORT).show();
<span style="white-space:pre"> </span>Intent intent = new Intent(ScanActivity.this,MainActivity.class);
<span style="white-space:pre"> </span>//将可以连接的ip发过去
<span style="white-space:pre"> </span>intent.putExtra("ip", (String)msg.obj);
<span style="white-space:pre"> </span>startActivity(intent);
<span style="white-space:pre"> </span>finish();
break;
在MainActivity这边接收传过来的IP
Intent intent = getIntent();
connIP = intent.getStringExtra("ip");
有了这个IP,就可以和服务端建立连接了。
开一个线程,与服务端建立连接,然后就一直循环来维持与服务端的持续通信。循环时,首先拿到客户端的输入输出流,当data_putput和发送的内容都不为空时,就将内容写出去,然后用data_input获取服务端发过来的信息,当服务端发来的信息不为空时,就将信息发送回UI主线程,并提示给用户
try {
client_socket = new Socket(ip, port);
while(true){
data_output = new DataOutputStream(client_socket.getOutputStream());
data_input = new DataInputStream(client_socket.getInputStream());
String msg="";
if ((data_output != null) && (!content.equals(""))) {
data_output.writeUTF(content);
}
msg = data_input.readUTF();
System.out.println(msg);
if(msg!=null&&!"".equals(msg)){
Message message = new Message();
message.what=1;
message.obj=msg;
handler.sendMessage(message);
}
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
if(data_output!=null){
data_output.close();
}
if(data_input!=null){
data_input.close();
}
if(client_socket!=null){
client_socket=null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
super.run();
在handleMessage(Message msg)中的操作,用于提示给用户
switch (msg.what) {
case 1:
Toast.makeText(MainActivity.this, (String)msg.obj, Toast.LENGTH_SHORT).show();
break;
default:
break;
}
super.handleMessage(msg);
至于发送给服务端的内容,在判断获取用户点击的按钮时就可以设置了
case R.id.btnShutdown:
final String shutdown = "shutdown";
if(connThread!=null){
connThread.interrupt();
}
connThread = new ConnThread(connIP,30000,shutdown);
connThread.start();
break;
到这里,客户端与服务端就可以连通了,但是还有许多需要优化的地方,就先这样吧,希望能给大家带来些灵感。
有几点需要注意的:
1.android模拟器启动应用会报错,用真机测试
2.有时会出现,明明开了服务端,但是客户端扫描不到,原因可能是网络阻塞,可以把扫描时的延时时间调大点,但这样也就增加了扫描耗时