接着上次没有进行完毕的话题继续下去,csdn的贴图实在是非常不方便。就该用刚刚从手上滑过的两个小工程项目来讲述一下,不过实际项目开发过程中远没有理论上阐述的那么清晰,因为总是会夹杂不少其他的概念。
项目一
业务需求:
某工厂的员工,经常使用工作用手机或者pad通过链接到非指定ssid进行上网活动,厂方希望能够开发一套软件能够阻止此类事情的发生。
需求分析:
根据厂方提出的需求我们可以通过以下三种方式来达到目的。
方案 | 优点 | 缺点 | 是否采用 |
对上游厂商提出定制需求,直接写SSID和密码到固件中 | 1. 直接杜绝了用户使用其他SSID进行上网的可能 2. 用户必须通过刷机才能更改上网设置。 | 1. 用户可以伪造相同的SSID和密码骗取通道上网。 2. 更改SSID和密码代价高,需要上游厂商配合刷机 | 否 |
提供一个有root权限的APP,固话SSID和密码到APP中 | 1. 直接杜绝了用户使用其他SSID进行上网的可能 2. 用户必须root卸载掉该APP才能更改上网设置 | 1. 用户可以伪造相同的SSID和密码骗取通道上网 2. 更改SSID和密码代价高,需要专业人员root更改程序 | 否 |
提供一个普通app,采用联网方式进行人工阻止方式 | 1. 不需要对系统进行定制和root 2. 增加了用户更改SSID进行上网的成本(5秒钟检查) 3. 开发部署成本低 | 1. 用户可以轻易卸载 | 是 |
术语定义:
终端:手机或pad
需求确认:
1. 服务第一次启动应该手工输入ssid,ssid密码和注册服务器地址,并将这些信息设置为默认信息。
2. 终端定时检测是否链接在指定的ssid上面,如果不是则更改wiffi设置,强制链接到指定的ssid上。
3. 终端定时向指定服务器注册,上送包括终端MAC地址在内的一些身份标识信息,并根据服务器回复的信息对终端重新进行设定。(二次附加需求)
4. 终端保存服务器回复的ssid、ssid密码,服务器地址等值为默认值。(二次附加需求)
5. 管理员可以通过浏览器查看所有终端最近一次的注册时间,并对超过时限未进行注册的终端优先显示。(二次附加需求)
6. 后台运行,不影响终端的操作功能。
7. 开机自启动,能防止各种管家杀死服务。
实现思路:
这个需求非常简单,经过二次整理表达后依然是一个小微工程(在基础设施得当的情况下,整个工程自撰写代码不超过2000行),而且用例场景都比较简单,就采用面向过程基于对象的思考方式来实现这个系统。
面向过程的思考方式:
a. 一个初始化界面提供给最终用户,用户首次使用能够输入需要连接的SSID和密码以及注册服务器的地址。
b. 一个后台服务来轮训检测用户是否更改了上网方式或者通道,如果有更改则自动设置为默认上网方式。
c. 一个后台服务定时向注册服务器发送注册信息,注册信息包括终端MAC在内的一些身份标识信息,并根据注册服务器的返回报文对终端的默认上网配置能进行更改。
d. 注册服务器为一个HTTP服务器。
e. 注册服务器提供一个GET URL请求链接提供按照注册时序注册的终端设备列表。
f. 注册服务器提供一个POST URL请求链接接收各个终端的注册请求,更新注册信息,并返回默认上网配置信息。
g. 注册服务器提供一个POST URL请求允许管理员批量修改所有注册终端的上网配置信息。
h. 提供一个展示页面,轮询更新设备注册信息。
i. 提供一个提交页面,更新终端上网方式。
终端软件基于对象(安卓下)的实现方式:
a. 扩展一个基于标准Activity的包含有三个输入框一个确定按钮的界面类
b. 自定义一个wiffi管理类,实现对wiffi的开、关、切换、以及网络信息收集的工作。
c. 自定义一个上网方式配置管理类,实现对上网方式配置信息的存取及持久化动作。
d. 扩展一个基于标准Service的自定义服务,实现循环服务启动及服务工作线程启动。
e. 扩展一个基于标准BroadcastReceiver的自定义服务,监听系统开机广播,实现自定义服务自启动。
f. 实现一个Runnable接口类,提供定时注册及上网方式定时检测、更改功能。
服务器端采用nodejs+Express+EXTJS方案:
a. 给Express的app对象添加post information路由收集wiffi配置信息。
b. 给Express的app对象添加post register路由收集终端注册信息,并回复wiffi配置信息。
c. 给Express的app对象添加get retister路由发送终端注册信息列表。
d. 给Express的app对象添加get information路由发送当前的wiffi配置信息。
e. 页面采用EXTJS给定的MVC模式进行中规中矩的方法添加,因为界面很简单且和思考方式一一对应,在此不在叙述。
总结:
因为这是一个实际工程,所以把前文中的分层、面向过程、基于对象等思想揉合起来进行了开发。其中比较明显的分层为终端MVC、页面MVC。服务器不明显那是因为在整个工程结构中给服务器的定位就是一个数据处理服务器,不进行任何展示和控制功能。
各个部分的代码也共享一下,能用的朋友拿去用吧
安卓部分
public class MainActivity extends Activity implements OnClickListener {
private ServiceInformation information;
protected void update_ui(boolean saved){
EditText ssidEdit = (EditText)findViewById(R.id.editText1);
EditText passEdit = (EditText)findViewById(R.id.editText2);
EditText urlEdit = (EditText)findViewById(R.id.editText3);
if(saved){
information.ssid = ssidEdit.getText().toString();
information.pass = passEdit.getText().toString();
information.url = urlEdit.getText().toString();
}else{
ssidEdit.setText(information.ssid);
passEdit.setText(information.pass);
urlEdit.setText(information.url);
}
}
@Override
public void onClick(View arg0){
update_ui(true);
if(information.ssid.isEmpty() || information.url.isEmpty()){
Toast.makeText(this,String.format(Locale.CHINESE, "输入信息错误,请检查输入信息"),Toast.LENGTH_LONG).show();
return;
}
information.synInformation();
startService(new Intent(this, MonitorService.class));
finish();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button submit = (Button)findViewById(R.id.button1);
submit.setOnClickListener(this);
information = new ServiceInformation(this);
update_ui(false);
}
}
public class MonitorService extends Service {
private Thread worker = null;
@Override
public IBinder onBind(Intent arg0) {
// TODO Auto-generated method stub
return null;
}
//此处添加启动服务要执行的操作代码
private void registerIntentReceiver(){
if(worker != null){
AutoStartService.showContext("worker is started ... \n");
return ;
}
try{
worker = new Thread( new SSIDRegister(this));
worker.start();
AutoStartService.showContext("worker is starting ... \n");
}catch(Exception e){
AutoStartService.showContext(e.getMessage());
worker = null;
}
}
@Override
public void onCreate() {
super.onCreate();
registerIntentReceiver();
AutoStartService.showContext("wiffi monitor servcie start...\n");
}
@Override
public void onDestroy() {
super.onDestroy();
AutoStartService.showContext("wiffi monitor servcie stopped...\n");
startService(new Intent(this, MonitorService.class));
}
@Override
public int onStartCommand(Intent intent, int flags, int startId){
super.onStartCommand(intent, flags, startId);
registerIntentReceiver();
return START_STICKY;
}
}
public class AutoStartService extends BroadcastReceiver{
static final String ACTION = "android.intent.action.BOOT_COMPLETED";
//写/mnt/sdcard/
public static void showContext(final String msg){
String path = Environment.getExternalStorageDirectory().getPath() + "/ssid_monitor.txt";
try {
FileOutputStream fout = new FileOutputStream(path,true);
fout.write(msg.getBytes());
fout.close();
}catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(ACTION)){
context.startService(new Intent(context, MonitorService.class));
}
}
}
public class SSIDRegister implements Runnable {
private static final String APPLICATION_JSON = "application/json";
private ServiceInformation information;
private WifiAdmin wifiAdmin ;
private HttpClient httpClient = new DefaultHttpClient();
public SSIDRegister(Context context){
information = new ServiceInformation(context);
wifiAdmin = new WifiAdmin(context);
}
protected void processHttpResponse(HttpEntity entity){
StringBuilder builder = new StringBuilder();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent()));
for (String s = reader.readLine(); s != null; s = reader.readLine()) {
builder.append(s);
}
reader.close();
} catch (IOException e1) {
AutoStartService.showContext(e1.getMessage());
return ;
}
String jsonString = builder.toString();
AutoStartService.showContext(jsonString);
try {
information.update(new JSONObject(jsonString));
} catch (JSONException e) {
AutoStartService.showContext(e.getMessage());
}
}
@Override
public void run() {
for (; true ;information.reloadInformation()) {
// 轮询服务器,获取最新的服务地址
try {
wifiAdmin.openWifi(information.ssid,information.pass,information.monopoly);
Thread.sleep(information.sleepInterval);
HttpResponse response = httpClient.execute(new HttpPost(information.url){
{
setEntity(new StringEntity(wifiAdmin.toString(),HTTP.UTF_8){
{
setContentType(APPLICATION_JSON);
}
});
}
});
if(response.getStatusLine().getStatusCode() != 200){
AutoStartService.showContext(response.toString());
}else{
processHttpResponse(response.getEntity());
}
} catch (Exception e) {
AutoStartService.showContext(e.getMessage());
}
}
}
}
public class WifiAdmin {
// 定义WifiManager对象
private WifiManager mWifiManager;
// 构造器
public WifiAdmin(Context context) {
// 取得WifiManager对象
mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
}
//查找设置ssid是否在搜索列表中
protected void searchSSIDUntilFind(String ssid,boolean forbidOther) throws InterruptedException{
mWifiManager.setWifiEnabled(false);
for(; true ;Thread.sleep(100)){
if(!mWifiManager.isWifiEnabled()){
mWifiManager.setWifiEnabled(true);
continue;
}
if(!mWifiManager.startScan()){
continue;
}
List<ScanResult> scanResultList = mWifiManager.getScanResults();
for (final ScanResult scanResult : scanResultList) {
if(scanResult.SSID.equalsIgnoreCase(ssid)){
return ;
}
}
}
}
//当前使用ssid是否是指定ssid
protected boolean ssidIsUsing(String ssid) {
if(!mWifiManager.isWifiEnabled() || mWifiManager.getWifiState() != WifiManager.WIFI_STATE_ENABLED){
return false;
}
WifiInfo info = mWifiManager.getConnectionInfo();
return info == null ? false : info.getSSID().equalsIgnoreCase(ssid);
}
// 打开WIFI
public boolean openWifi(final String ssid,final String pass,boolean forbidOther) throws InterruptedException{
if(ssidIsUsing(ssid)){
return true;
}
searchSSIDUntilFind(ssid,forbidOther);
int netID = mWifiManager.addNetwork(new WifiConfiguration() {
private void setWifiConfig(final String ssid,final String pass){
SSID = String.format(Locale.ENGLISH, "\"%s\"", ssid);
if(pass.isEmpty()){
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
return;
}
preSharedKey = String.format(Locale.ENGLISH, "\"%s\"", pass);
hiddenSSID = true;
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
allowedProtocols.set(WifiConfiguration.Protocol.WPA);
status = WifiConfiguration.Status.ENABLED;
}
{
setWifiConfig(ssid, pass);
}
});
return mWifiManager.enableNetwork(netID, true);
}
/*
* info.getBSSID(); 获取BSSID地址。 info.getSSID(); 获取SSID地址。 需要连接网络的ID
* info.getIpAddress(); 获取IP地址。4字节Int, XXX.XXX.XXX.XXX 每个XXX为一个字节
* info.getMacAddress(); 获取MAC地址。 info.getNetworkId(); 获取网络ID。
* info.getLinkSpeed(); 获取连接速度,可以让用户获知这一信息。 info.getRssi();
* 获取RSSI,RSSI就是接受信号强度指示
*/
public String toString(){
final WifiInfo info = mWifiManager.getConnectionInfo();
try {
return new JSONObject(){
{
put("bssid", info.getBSSID());
put("ssid", info.getSSID());
put("ip", info.getIpAddress());
put("mac", info.getMacAddress());
put("id", info.getNetworkId());
put("mac", info.getMacAddress());
put("speed", info.getLinkSpeed());
put("rssi", info.getRssi());
put("all", info.toString());
}
}.toString();
} catch (JSONException e) {
return String.format(Locale.CHINESE, "{\"error_msg\":\"%s\"}", e.getMessage());
}
}
}
public class ServiceInformation {
public String ssid = "TP-LINK_9D8A78";
public String pass = "";
public String url = "http://172.17.138.33:3721/registe";
public int sleepInterval = 5 * 60 * 1000;
public boolean monopoly = true;
private Context context;
public ServiceInformation(Context c){
context = c;
reloadInformation();
}
public void synInformation(){
SharedPreferences.Editor shareData = context.getSharedPreferences("ssid_monitor", 0).edit();
shareData.putString("ssid",ssid);
shareData.putString("pass",pass);
shareData.putString("url",url);
shareData.putInt("sleep", sleepInterval);
shareData.putBoolean("monopoly", monopoly);
shareData.commit();
}
public void update(JSONObject object){
try {
ssid = object.getString("ssid");
pass = object.getString("pass");
url = object.getString("url");
sleepInterval = object.getInt("sleepInterval");
monopoly = object.getBoolean("monopoly");
synInformation();
} catch (JSONException e) {
AutoStartService.showContext(e.getMessage());
}
}
public void reloadInformation(){
SharedPreferences shareData = context.getSharedPreferences("ssid_monitor", 0);
ssid = shareData.getString("ssid", ssid);
pass = shareData.getString("pass", pass);
url = shareData.getString("url", url);
sleepInterval = shareData.getInt("sleep", sleepInterval);
monopoly = shareData.getBoolean("monopoly", monopoly);
}
}
NODEJS部分
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var app = express();
// uncomment after placing your favicon in /public
//app.use(favicon(__dirname + '/public/favicon.ico'));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public/build/production/dmanager')));
var contentType={
"css": "text/css",
"gif": "image/gif",
"html": "text/html",
"ico": "image/x-icon",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"js": "text/javascript",
"json": "application/json",
"pdf": "application/pdf",
"png": "image/png",
"svg": "image/svg+xml",
"swf": "application/x-shockwave-flash",
"tiff": "image/tiff",
"txt": "text/plain",
"wav": "audio/x-wav",
"wma": "audio/x-ms-wma",
"wmv": "video/x-ms-wmv",
"xml": "text/xml"
};
app.checkSendFile = function(filename,req,rsp){
fs.stat(filename,function(e,s){
if(e){
rsp.send(400,filename);
console.log(e);
return;
}
console.log(s);
var lastTime = s.mtime.toUTCString();
if(req.header('If-Modified-Since') == lastTime){
rsp.send(304,'file no changed');
return;
}
var ext = path.extname(req.path);
ext = ext ? ext.slice(1) : 'unknown';
console.log(req.path,path.extname(req.path),contentType[ext]);
var isGz = filename.match(/\bgz\b$/ig);
var exp = new Date();
exp.setTime(exp.getTime() + 10000 * 1000);
rsp.writeHead(200,'OK',{
'Content-Encoding': isGz ? 'gzip' : 'UTF-8',
'Content-Type': contentType[ext],
'Expires' : exp.toUTCString(),
'Cache-Control' : 'max-age=10000',
'Last-Modified' : lastTime
});
console.log('send file : ',filename,isGz);
if(isGz){
fs.createReadStream(filename,'binary').pipe(rsp);
}else{
rsp.sendfile(filename);
}
});
};
app.get('/app.js',function(req,rsp){
var acp = req.header('accept-encoding');
var filename = 'app.js';
if(acp.match(/\bgzip\b/)){
filename = 'app.js.gz';
}
app.checkSendFile(filename,req,rsp);
});
app.get('/resources/dmanager-all.css',function(req,rsp){
var acp = req.header('accept-encoding');
var filename = './resources/dmanager-all.css';
if(acp.match(/\bgzip\b/)){
filename = './resources/dmanager-all.css.gz';
}
app.checkSendFile(filename,req,rsp);
});
app.get("/",function(req,rsp){
rsp.sendfile("index.html");
});
app.device_list = {
aa :{
mac : "aaa",
ip : 'aaaa',
last_time : new Date()
},
bb :{
mac : "bbb",
ip : 'bbb',
last_time : new Date()
}
};
app.information = {
ssid : 'TDC',
pass : 'Password',
url : 'http://172.17.138.33:3721/registe',
sleepInterval : 5 * 60 * 1000,
monopoly : true
};
app.post("/information",function(req,rsp){
console.log(req.body);
while(true){
req.body.sleepInterval = req.body.sleepInterval * 1;
req.body.monopoly = (req.body.monopoly == 'true');
app.information = req.body || app.information;
rsp.send({
success : true,
msg : '服务器已更改,设备下次注册后会生效'
});
console.log(app.information);
return;
}
rsp.send({
success : false,
msg : '输入值错误,请修正后重新提交'
});
});
app.get("/information",function(req,rsp){
rsp.send({
success : true,
data : app.information
})
});
app.get("/registers",function(req,rsp){
var registers = [];
for(var n in app.device_list){
registers.push({
mac : app.device_list[n].mac || "Unknown MAC",
ip : app.device_list[n].mac || "Unknown Ip",
last_time : app.device_list[n].last_time || "Unknown time",
others : JSON.stringify(app.device_list[n])
});
}
rsp.send( {
success : true,
DeviceList : registers
} );
});
app.post("/registe",function(req,rsp){
try{
var obj = req.body || {mac : 'null'};
obj.last_time = new Date();
app.device_list[obj.mac] = obj;
rsp.send(app.information);
}catch(e){
rsp.send(e);
}
});
module.exports = app;
EXTJS部分
Ext.define('dmanager.view.Main', {
extend: 'Ext.container.Container',
requires:[
'Ext.tab.Panel',
'Ext.layout.container.Border',
'dmanager.view.SSIDSettor',
'dmanager.store.DeviceList'
],
xtype: 'app-main',
layout: {
type: 'border'
},
items: [{
region: 'center',
xtype: 'panel',
title: '设备列表',
tools : [{
type : 'gear',
handler : function(){
Ext.create('dmanager.view.SSIDSettor').show();
}
}],
items :[{
xtype: 'gridpanel',
defaults:{
flex : 1
},
columns: [
{ text: 'mac地址', dataIndex: 'mac' },
{ text: 'ip地址', dataIndex: 'ip'},
{ text: '注册时间', dataIndex: 'last_time' },
{ text: '其他信息', dataIndex: 'others', flex: 3 }
],
store: Ext.data.StoreManager.lookup('DeviceList') || Ext.create('dmanager.store.DeviceList')
}]
}]
},function(){
var queryDevice = function (){
var store = Ext.data.StoreManager.lookup('DeviceList');
if(store){
store.load();
}else{
console.log('look up DeviceList failed')
}
setTimeout(queryDevice,1000 );
};
setTimeout(queryDevice,1000 );
});
Ext.define('dmanager.view.SSIDSettor', {
extend: 'Ext.window.Window',
title : '重新设置SSID相关信息',
layout: 'fit',
width : 480,
height : 320,
items :[{
xtype : 'form',
url : 'information',
layout: {
pack: 'center'
},
defaults: {
allowBlank: false,
region: "center",
margin :'10 5 5 10',
xtype : 'textfield'
},
items :[{
fieldLabel: 'SSID名称',
name: 'ssid'
},{
fieldLabel: 'SSID密码',
allowBlank: true,
name: 'pass'
},{
fieldLabel: '注册URL',
name: 'url'
},{
xtype: 'numberfield',
fieldLabel: '注册间隔时间',
name:'sleepInterval'
},{
xtype: 'combobox',
fieldLabel: '自动屏蔽其他信号源',
name: 'monopoly',
queryMode: 'local',
valueField: 'abbr',
displayField: 'name',
store: {
fields: ['abbr', 'name'],
data : [
{"abbr":false, "name":"不禁止其他信号源"},
{"abbr":true, "name":"禁止其他信号源"}
]
}
}],
buttonAlign : 'center',
buttons : [{
text: '提交',
handler: function() {
var form = this.up('form').getForm();
if (!form.isValid()) {
return;
}
form.submit({
self : this,
success: function(form, action) {
Ext.Msg.alert('提交成功,立即生效', action.result.msg);
},
failure: function(form, action) {
Ext.Msg.alert('提交失败,稍后重试', action.result.msg);
}
});
}
},{
text: '取消',
handler: function() {
this.up('window').close();
}
},{
text: '重置',
handler: function() {
this.up('form').getForm().reset();
}
}]
}]
});
Ext.define('dmanager.store.DeviceList', {
extend: 'Ext.data.Store',
model: 'dmanager.model.Device',
id : 'DeviceList',
sorters: [{
property: 'last_time',
direction: 'ASC'
}],
proxy: {
type: 'ajax',
url: 'registers',
reader: {
type: 'json',
root: 'DeviceList'
}
},
autoLoad: false
});
Ext.define('dmanager.model.Device', {
extend: 'Ext.data.Model',
fields: [
{name: 'mac', type: 'string'},
{name: 'ip', type: 'string'},
{name: 'last_time', type: 'string'},
{name: 'others', type: 'string'}
]
});
Ext.define('dmanager.model.Setting', {
extend: 'Ext.data.Model',
fields: [
{name: 'ssid', type: 'string'},
{name: 'pass', type: 'string'},
{name: 'url', type: 'string'},
{name: 'sleepInterval', type: 'number'},
{name: 'monopoly', type: 'boolean'}
]
});