今天终于把老大交代的任务搞完了,感觉收获挺多的,所以就写一篇来记录一下吧,首先还是来看一下,老大们的需求

需求:

希望移动端的用户标识(IMEI)和HTML页面的用户标识(Cookie)连接起来,其中HTML页面可能是用户使用PC访问指定的html页面也有可能是移动端使用浏览器访问html页面

技术解决:

运用移动设备的系统特型,用Apps的Service监听本地的网络端口,当局域网内或本机的Html页面通过请求调用本地IP:Port时触发监听。Apps在将当前Html的Cookie和移动设备标识物发回服务端时,即实现了本次PC/Mobile Html页面的Cookie和移动设备的互相绑定。

具体实现方案之多端通信技术方案



各模块细节

1.Native App(移动端的app):

1).跟随主服务常驻内存,在收到当前网络环境为Wifi事件后,启动HttpServer。

(当前网络切换成3G/2G等网络时关闭HttpServer)。

以上这步我们叫做注册:启动移动端的HttpServer的时候,我们会将移动设备的ip地址(内网地址)发送到指定的后台Server上

 

2).收到从Web来的http请求后,往Server发送绑定请求,这时候就将移动端的IMEI和HTML中的Cookie一起上传到指定的Server上

例如:

http://serverhost/?cookie=123213&imei=31233213123

以上这步我们叫做一次绑定的完成操作

以为我们是在移动端开始一个HttpServer的,所以要考虑一些额外的因素,比如耗电量

风险控制:使用灰度测试规则,并使用开关限制功能。

功耗风险:经过三台设备测试,电量消耗忽略不计(具体CPU使用时间在统计)。

 

2.Web JS:

1).加载页面加载后先从CDN加载一个静态的JS到此页面。

2).静态JS再请求Server 获取一段动态JS用以做本地Ping。

(如果此IP已有N次获取,则考虑>N的节点是一个大型公共网络,则把此公共IP加入黑名单,不再返回动态JS)

3).获得动态JS后直接Ping本地的HttpServer。

(Ping的IP/Port会随着动态JS一起下发)

这里我们是在Html页面中使用JS脚本来处理传递Cookie给移动端的功能。

3.Bridge Server:

1).Sever选型:Node.js

同时考虑到开发效率,性能,和client的JS的契合度。

Node.js单台16核服务器,4节点,QPS经测试能到6k-8k。

较适合短频非阻塞的请求。

 

2).数据存储:Redis

考虑到NativeApp的外网IP时效性,以及对于短频请求的快速应答。

以外网IP为Key,各个IP为Value。每天清理一次外网IP。

存储格式:{外网IP:{NativeApp IP/Port, .........}}



3).移动端所需要的接口

接口一:Native App 对于本外网IP的注册的请求。

接口二:Navtive App的绑定请求。



具体原理图:





上面讲解了一下技术的实现方案,下面我们就来看一下,具体的实现技术

首先来看移动端的,我们需要建立一个HttpServer

我们可能知道在PC端建立一个Server很简单,直接使用Tomcat服务器就可以了,但是我们移动端不可能去搭建一个服务器的,那么我们该怎么办呢?我们可以在网上搜索一下关于如何在手机中搭建一个WebServce,我们可以看到Apache已经帮我们做好了这件事了,他提供了一个jar,有相应的api,同时还有一种技术就是老外有个人写好了一个类:NanoHttpd,我们只需要看懂这个类,然后自己在编写一个类,继承这个类即可,因为这个类是抽象的。这两种搭建的Server原理都是很简单的,就是建立一个Socket连接,这个和Tomcat是一样的,只是他们将这种操作进行优化处理了,那么我们首先来看一下Apache给我们提供的这个HttpServer:

按照上面的技术方案,我们希望这个HttpServer常驻在一个服务中进行监听的,所以我们需要建立一个Service

package com.sohu.tests;

import java.io.IOException;

import org.apache.http.client.ClientProtocolException;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.os.IBinder;
import android.util.Log; public class SocketService extends Service { private int port = Consts.defaultPort; //网络开关状态改变的监听
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)){
AppListYPLog.e("DEMO","网络状态切换...");
//注册接口
new Thread(){
@Override
public void run(){
//开始注册
register();
}
}.start();
}
}
}; @Override
public IBinder onBind(Intent arg0) {
return null;
} @Override
public void onCreate() {
super.onCreate();
Log.v("23233", "Complete"); Utils.init(this); //开启监听端口
HttpServer hs = new HttpServer();
try{
AppListYPLog.i("监听开启...");
for(int i=0;i<Consts.portAry.length;i++){
if(Utils.checkPort(Consts.portAry[i])){
port = Consts.portAry[i];
break;
}
}
hs.execute(port);
register();
}catch(Exception e){
AppListYPLog.e("异常:"+e.getMessage());
} //注册广播
IntentFilter filter = new IntentFilter();
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
registerReceiver(receiver, filter); } /**
* 注册ip地址
* @return
*/
public boolean register(){
//拼接参数
StringBuilder param = new StringBuilder();
param.append("imei=");
param.append(Utils.getImeiInfo());
param.append("&hs_ip=");
param.append(Utils.getLocalHostIp()+":"+port);
param.append("&route_mac=");
param.append(Utils.getRouteMac());
param.append("&route_ssid=");
param.append(Utils.getRouteSSID());
param.append("&timetag=");
param.append(System.currentTimeMillis()+""); boolean flag = false; //上报数据
if(Utils.checkNetWorkState()){
try {
flag = NetUtils.uploadRequest(Consts.registerUrl, param.toString());
if(flag){
AppListYPLog.i("注册操作成功");
//Toast.makeText(getApplicationContext(), "注册成功", Toast.LENGTH_LONG).show();
}else{
AppListYPLog.e("注册操作失败");
//Toast.makeText(getApplicationContext(), "注册失败", Toast.LENGTH_LONG).show();
}
} catch (ClientProtocolException e) {
AppListYPLog.e("异常:"+e.getMessage()+",注册失败");
} catch (IOException e) {
AppListYPLog.e("异常:"+e.getMessage()+",注册失败");
} catch(Exception e){
AppListYPLog.e("异常:"+e.getMessage()+",注册失败");
}
} return flag;
} }

在这个服务中,我们开始的时候就进行注册,上报一些信息:

imei:手机移动的标识

hs_ip:手机的HttpServer的地址和端口号

route_mac:手机连接的路由器的mac地址

route_ssid:手机连接的路由器的ssid

timetag:注册的时间戳

同时我们还注册了一个监听网络变化的广播,这个是为了防止,开始的时候用户没有开启网络,一旦用户开启网路的时候,我们就开启监听并且进行注册。

这里我们使用的监听端口是3个,因为我们知道,端口最大的是655535,0-1024是系统使用的,所以我们就使用了三个备用的端口,在使用这个端口之前要检测一下该端口有没有被占用,这里我们就使用Socket探针技术。

再来看一下最核心的HttpServer的实现:

首先我们需要下载Apache提供的jar包:

http://download.csdn.net/detail/jiangwei0910410003/7431829



代码:

package com.sohu.tests;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Locale; import java.util.Map; import org.apache.http.ConnectionClosedException;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpServerConnection;
import org.apache.http.HttpStatus;
import org.apache.http.MethodNotSupportedException;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpProcessor;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.protocol.HttpRequestHandlerRegistry;
import org.apache.http.protocol.HttpService;
import org.apache.http.protocol.ImmutableHttpProcessor;
import org.apache.http.protocol.ResponseConnControl;
import org.apache.http.protocol.ResponseContent;
import org.apache.http.protocol.ResponseDate;
import org.apache.http.protocol.ResponseServer; public class HttpServer { //获取来访者的ip地址
private static String ipAddress = ""; //开启监听
public void execute(int port) throws Exception{
Thread t = new RequestListenerThread(port);
t.setDaemon(false);
t.start();
} //自定义HttpRequest的处理类,我们需要继承HttpRequestHandler接口
static class WebServiceHandler implements HttpRequestHandler { public WebServiceHandler() {
super();
} //在这个方法中我们就可以处理请求的业务逻辑
public void handle(final HttpRequest request,
final HttpResponse response, final HttpContext context)
throws HttpException, IOException { //获取请求方法
String method = request.getRequestLine().getMethod().toUpperCase(Locale.ENGLISH);
//获取请求的uri
String target = request.getRequestLine().getUri();
Map<String,String> params = Utils.getParam(target);
//拼接参数
StringBuilder param = new StringBuilder();
param.append("cookie=");
if(params.get("cookie") != null){
param.append(params.get("cookie"));
}
param.append("&imei=");
param.append(Utils.getImeiInfo());
param.append("&visitor_ip=");
param.append(ipAddress);
param.append("&local_ip=");
param.append(Utils.getLocalHostIp());
param.append("&route_mac=");
param.append(Utils.getRouteMac());
param.append("&route_ssid=");
param.append(Utils.getRouteSSID());
param.append("&timetag=");
param.append(System.currentTimeMillis()+""); //上报数据
try{
if(Utils.checkNetWorkState()){
boolean flag = NetUtils.uploadRequest(Consts.cookieUrl, param.toString());
if(flag){
AppListYPLog.e("完成操作成功");
}else{
AppListYPLog.e("完成操作失败");
}
}
}catch(Exception e){
AppListYPLog.e("异常:"+e.getMessage()+"完成操作失败");
} //get请求方式(我们这里只处理get方式)
if (method.equals("GET") ) {
response.setStatusCode(HttpStatus.SC_OK);
StringEntity entity = new StringEntity("Request Success!!");
response.setEntity(entity);
} else if (method.equals("POST") ) {
response.setStatusCode(HttpStatus.SC_OK);
StringEntity entity = new StringEntity("Request Success!!");
response.setEntity(entity);
} else {
throw new MethodNotSupportedException(method + " method not supported");
}
} } /**
* 自定一个监听的线程
* @author weijiang204321
*
*/
static class RequestListenerThread extends Thread { private final ServerSocket serversocket;
private final HttpParams params;
private final HttpService httpService; public RequestListenerThread(int port)
throws IOException { /**********************下面就是模板代码了***********************/
this.serversocket = new ServerSocket(port); HttpProcessor httpproc = new ImmutableHttpProcessor(
new HttpResponseInterceptor[] {
new ResponseDate(), new ResponseServer(),
new ResponseContent(), new ResponseConnControl() });
this.params = new BasicHttpParams();
this.params
.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 5000)
.setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE,8 * 1024)
.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false)
.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true)
.setParameter(CoreProtocolPNames.ORIGIN_SERVER,"HttpComponents/1.1"); //这只请求头信息
HttpRequestHandlerRegistry reqistry = new HttpRequestHandlerRegistry();
//WebServiceHandler用来处理webservice请求,这里我们可以注册多个处理器Handler的
reqistry.register("*", new WebServiceHandler()); this.httpService = new HttpService(httpproc,
new DefaultConnectionReuseStrategy(),
new DefaultHttpResponseFactory());
httpService.setParams(this.params);
//为http服务设置注册好的请求处理器。
httpService.setHandlerResolver(reqistry); } @Override
public void run() {
AppListYPLog.i("Listening on port " + this.serversocket.getLocalPort());
AppListYPLog.i("Thread.interrupted = " + Thread.interrupted());
while (!Thread.interrupted()) {
try {
// 建立Http连接
Socket socket = this.serversocket.accept();
DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
ipAddress = socket.getInetAddress().getHostAddress();
AppListYPLog.i("Incoming connection from " + socket.getInetAddress());
conn.bind(socket, this.params);
//开启工作线程
Thread t = new WorkerThread(this.httpService, conn);
t.setDaemon(true);
t.start();
} catch (InterruptedIOException ex) {
AppListYPLog.e("异常:InterruptedIOException");
break;
} catch (IOException e) {
AppListYPLog.e("I/O error initialising connection thread: " + e.getMessage());
break;
}
}
}
} /**
* 后台工作线程
* @author weijiang204321
*
*/
static class WorkerThread extends Thread { private final HttpService httpservice;
private final HttpServerConnection conn; public WorkerThread(final HttpService httpservice,final HttpServerConnection conn) {
super();
this.httpservice = httpservice;
this.conn = conn;
} @Override
public void run() {
System.out.println("New connection thread");
HttpContext context = new BasicHttpContext(null);
try {
while (!Thread.interrupted() && this.conn.isOpen()) {
this.httpservice.handleRequest(this.conn, context);
}
} catch (ConnectionClosedException ex) {
System.err.println("Client closed connection");
} catch (IOException ex) {
System.err.println("I/O error: " + ex.getMessage());
} catch (HttpException ex) {
System.err.println("Unrecoverable HTTP protocol violation: "+ ex.getMessage());
} finally {
try {
this.conn.shutdown();
} catch (IOException ignore) {
}
}
}
} }

我们首先需要定义一个RequestHander用来处理请求的逻辑结果,同时我们需要建立一个HttpConnection,这里我们需要获取来访者的ip地址,所以我们定义了一个全局的变量ipAddress,然后在WebServiceHandler类中的handle方法中进行上报完成的动作,需要上报的参数为:

cookie:来访者携带的cookie值(也就是Html页面中的cookie)

imei:手机的移动标识

visitor_ip:来访者的ip地址

local_ip:本机的ip地址

route_mac:手机连接的路由器的地址

route_ssid:手机连接的路由器的ssid

timetag:上报的时间戳

下面再来看一下辅助类

Utils:

package com.sohu.tests;

import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map; import org.apache.http.conn.util.InetAddressUtils; import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils; /**
* 工具类
*
* @author weijiang204321
*
*/
public class Utils { public static Context mContext; public static void init(Context context) {
mContext = context;
} /**
*
* @param str
* @return 字符串是否为空
*/
public static boolean isNotEmpty(String str) {
if (str != null && !"".equals(str)) {
return true;
}
return false;
} /**
* 获取手机的Mac地址
*
* @return
*/
public static String getMacAddress() {
String result = "";
try {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
result = wifiInfo.getMacAddress();
} catch (Exception e) {
result = "";
}
return result;
} /**
* 获取手机的imei
*
* @return
*/
public static String getImeiInfo() {
try {
TelephonyManager mTm = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
return mTm.getDeviceId();
} catch (Exception e) {
e.printStackTrace();
return "";
} } /**
* 获取手机的imsi
*
* @return
*/
public static String getImsiInfo() {
try {
String imsi = "";
TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
if (telephonyManager != null) {
imsi = telephonyManager.getSubscriberId();
}
if (TextUtils.isEmpty(imsi)) {
imsi = "UNKNOWN";
}
return imsi;
} catch (Exception e) {
return "";
}
} /**
* 获取手机型号
*
* @return
*/
public static String getTypeInfo() {
return android.os.Build.MODEL; // 手机型号
} /**
* 获取手机系统版本
*
* @return
*/
public static String getOsVersion() {
return android.os.Build.VERSION.RELEASE;
} /**
* 获取路由器的SSID
*
* @return
*/
public static String getRouteSSID() {
try {
WifiManager wm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
WifiInfo info = wm.getConnectionInfo();
if(info.getSSID().contains("<")){
return "";
}else{
return info.getSSID().replace("\"","") + "";
}
} catch (Exception e) {
AppListYPLog.e("异常:" + e.getMessage() + ",获取SSID失败!");
return "";
}
} /**
* 获取路由器的Mac地址
*
* @return
*/
public static String getRouteMac() {
try {
WifiManager wm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
WifiInfo info = wm.getConnectionInfo();
if(info.getBSSID() == null){
return "";
}else{
return info.getBSSID() + "";
}
} catch (Exception e) {
AppListYPLog.e("异常:" + e.getMessage() + ",获取Mac地址失败!");
return "";
}
} /**
* 解析uri参数
* @param uri
* @return
*/
public static Map<String,String> getParam(String uri){
Map<String,String> params = new HashMap<String,String>();
try{
if(isNotEmpty(uri)){
String subStr = uri.substring(uri.indexOf("?")+1);
String[] ary = subStr.split("&");
for(int i=0;i<ary.length;i++){
String[] temp = ary[i].split("=");
if(temp.length<2){
params.put(temp[0], "");
}else{
params.put(temp[0], temp[1]);
} }
return params;
}else{
return null;
}
}catch(Exception e){
return null;
} } /**
* 判断网络
* @param ctx
* @return
*/
public static boolean checkNetWorkState() {
boolean isnetwork = false;
ConnectivityManager connManager = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = connManager.getActiveNetworkInfo(); if (info != null && (info.isAvailable() || info.isConnected())) {
isnetwork = true;
}
return isnetwork;
} /**
* 获取本机的ip
* @return
*/
public static String getLocalHostIp(){
String ipaddress = "";
try{
Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
// 遍历所用的网络接口
while (en.hasMoreElements()){
NetworkInterface nif = en.nextElement();// 得到每一个网络接口绑定的所有ip
Enumeration<InetAddress> inet = nif.getInetAddresses();
// 遍历每一个接口绑定的所有ip
while (inet.hasMoreElements()){
InetAddress ip = inet.nextElement();
if (!ip.isLoopbackAddress()&& InetAddressUtils.isIPv4Address(ip.getHostAddress())){
return ip.getHostAddress();
}
}
}
}catch (SocketException e){
AppListYPLog.e("feige", "获取本地ip地址失败");
}
return ipaddress;
} /**
* 检查端口port是否被占用了
* @param port
* @return
*/
public static boolean checkPort(int port){
try{
InetAddress theAddress=InetAddress.getByName("127.0.0.1");
try {
Socket theSocket = new Socket(theAddress,port);
theSocket.close();
theSocket = null;
theAddress = null;
return false;
}catch (IOException e) {
AppListYPLog.e("异常:"+e.getMessage()+"检查端口号是否被占用");
}catch(Exception e){
AppListYPLog.e("异常:"+e.getMessage()+"检查端口号是否被占用");
}
}catch(UnknownHostException e) {
AppListYPLog.e("异常:"+e.getMessage()+"检查端口号是否被占用");
}
return true;
} }

网络工具类NetUtils

package com.sohu.tests;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set; import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils; public final class NetUtils { /**
* 上传数据
*
* @param url
* @param param
* @return
*/
public static String postData(String url, HashMap<String, String> param) { HttpPost httpPost = new HttpPost(url);
// 设置HTTP POST请求参数必须用NameValuePair对象
List<NameValuePair> params = new ArrayList<NameValuePair>();
if (param != null) {
Set<Entry<String, String>> set = param.entrySet();
Iterator<Entry<String, String>> iterator = set.iterator();
while (iterator.hasNext()) {
Entry<String, String> tempEntry = iterator.next();
params.add(new BasicNameValuePair(tempEntry.getKey(), tempEntry.getValue()));
}
} else {
AppListYPLog.e("DEMO:", "上传参数错误");
return null;
}
HttpResponse httpResponse = null;
try {
// 设置httpPost请求参数
httpPost.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
HttpClient httpClient = new DefaultHttpClient();
// 请求超时
httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 20000);
// 读取超时
httpClient.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 20000);
httpResponse = httpClient.execute(httpPost);
if (httpResponse.getStatusLine().getStatusCode() == 200) {
// 第三步,使用getEntity方法活得返回结果
String result = EntityUtils.toString(httpResponse.getEntity());
return result;
}
} catch (ClientProtocolException e) {
AppListYPLog.e("异常:" + e.getMessage());
return null;
} catch (IOException e) {
AppListYPLog.e("异常:" + e.getMessage());
return null;
}
return null;
} /**
* @return 通过Request方式曝光
*/
public static boolean uploadRequest(String baseUrl, String param) throws ClientProtocolException, IOException {
HttpGet httpGet = null;
if (param != null && !"".equals(param)) {
httpGet = new HttpGet(baseUrl + "?" + param);
} else {
httpGet = new HttpGet(baseUrl);
}
AppListYPLog.e("URL:"+httpGet.getURI()+"");
HttpParams params = httpGet.getParams();
HttpConnectionParams.setConnectionTimeout(params, 3000);
HttpConnectionParams.setSoTimeout(params, 3000);
HttpClient httpClient = new DefaultHttpClient(params);
HttpResponse response = httpClient.execute(httpGet);
int statusCode = response.getStatusLine().getStatusCode();
AppListYPLog.e("响应码:" + statusCode);
if (statusCode < 300 && statusCode >= 200) {
return true;
}
return false;
} }

常量类:

package com.sohu.tests;

/**
* 常量类
* @author weijiang204321
*
*/
public class Consts { /*cookie等信息上传url*/
public static final String cookieUrl = "http://192.168.1.110:8080/HttpServer/FinishServlets"; /*注册的url*/
public static final String registerUrl = "http://192.168.1.110:8080/HttpServer/RegisterServlets"; /*备用端口*/
public static int[] portAry = new int[]{23021,10034,48990}; /*默认端口*/
public static int defaultPort = 23021; }

打印Log类

package com.sohu.tests;

import android.util.Log;

/**
* 打印Log信息
* @author weijiang204321
*
*/
public class AppListYPLog { private final static String TAG = "APPLIST";
private static boolean SHOW_LOG = true; private AppListYPLog() {
} public static void closeLog() {
SHOW_LOG = false;
} public static void i(String msg) {
if (SHOW_LOG) {
Log.i(TAG, msg);
}
} public static void d(String msg) {
if (SHOW_LOG) {
Log.d(TAG, msg);
}
} public static void w(Exception ex) {
if (SHOW_LOG) {
ex.printStackTrace();
}
} public static void e(String msg) {
if (SHOW_LOG) {
Log.e(TAG, msg);
}
} public static void i(String tag, String msg) {
if (SHOW_LOG) {
Log.i(tag, msg);
}
} public static void d(String tag, String msg) {
if (SHOW_LOG) {
Log.d(tag, msg);
}
} public static void w(String tag, String msg) {
if (SHOW_LOG) {
Log.w(tag, msg);
}
} public static void e(String tag, String msg) {
if (SHOW_LOG) {
Log.e(tag, msg);
}
} }

测试的Demo:

package com.sohu.tests;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle; public class Main extends Activity { @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mainlay);
startService(new Intent(this, SocketService.class));
} }

手机端的HttpServer就搭建完成了,下面我们要想测试的话,就必须在搭建一个Server端,我们这里就简单的建立一个Servlet:

移动端进行注册的接口:

package com.weijiang.httpserver;

import java.io.IOException;
import java.util.Date; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; public class RegisterServlets extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
System.out.println("imei:"+req.getParameter("imei"));
System.out.println("hs_ip:"+req.getParameter("hs_ip"));
System.out.println("route_mac:"+req.getParameter("route_mac"));
System.out.println("route_ssid:"+req.getParameter("route_ssid"));
System.out.println("timetag:"+new Date(Long.parseLong(req.getParameter("timetag"))));
} protected void doPost(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
doGet(req,resp);
} }

移动端完成操作的接口:

package com.weijiang.httpserver;

import java.io.IOException;
import java.util.Date; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; public class FinishServlets extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
System.out.println("cookie:"+req.getParameter("cookie"));
System.out.println("imei:"+req.getParameter("imei"));
System.out.println("visitor_ip:"+req.getParameter("visitor_ip"));
System.out.println("local_ip:"+req.getParameter("local_ip"));
System.out.println("route_mac:"+req.getParameter("route_mac"));
System.out.println("route_ssid:"+req.getParameter("route_ssid"));
System.out.println("timetag:"+new Date(Long.parseLong(req.getParameter("timetag"))));
} protected void doPost(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
doGet(req,resp);
} }

模拟携带有cookie值的Html页面

<body>
<a href='http://192.168.1.109:23021?cookie=123456'>Click</a>
</body>

这里就是为了简单的模拟,所以直接随便写了一个cookie值

我们将PC和手机进行联网,这里不需要连接在一个网段中,只要在一个公网内就可以了,即他们两个可以ping通就可以,我这里在家里面测试的,用的是自己的路由器,所以一定是在一个网段中,我们查看PC的ip地址:

手机端的ip地址:

这样我们下面就可以来进行测试了,首先我们将Android应用部署到手机中,同时我们将Server启动,测试流程很简单:我们尝试改变网络状态,将网路断开,在进行连接,打印Log如下:

这里我们看到注册成功了,而且将手机的HttpServer的ip地址和监听端口上报了

我们在Server查看上报的信息:

我们就可以将手机的的内网地址进行保存,这里我们要注意在前面我们说到,这里还有一个步骤就是要获取公网的ip地址

上面的注册流程就走通了,下面我们看一下,完成操作的测试:

我们在浏览器中访问:

http://localhost:8080/HttpServer/index.jsp

点击Click,我们看到控制台中Log信息

我们看到完成操作,将cookie值和imei上报给Server端了,同时记录了来访者的ip地址

我们在Server的控制台中看到打印信息:

这样我们就将手机的imei和html页面的cookie值进行绑定了。。

前面我们说到了搭建移动端的Server的时候还有一种方法就是使用一个老外写的一个简单的类:

NanoHTTPD:

package com.sohu.nanohttpd;  

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.io.PushbackInputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TimeZone; /**
* A simple, tiny, nicely embeddable HTTP server in Java
* <p/>
* <p/>
* NanoHTTPD
* <p></p>Copyright (c) 2012-2013 by Paul S. Hawke, 2001,2005-2013 by Jarno Elonen, 2010 by Konstantinos Togias</p>
* <p/>
* <p/>
* <b>Features + limitations: </b>
* <ul>
* <p/>
* <li>Only one Java file</li>
* <li>Java 5 compatible</li>
* <li>Released as open source, Modified BSD licence</li>
* <li>No fixed config files, logging, authorization etc. (Implement yourself if you need them.)</li>
* <li>Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25)</li>
* <li>Supports both dynamic content and file serving</li>
* <li>Supports file upload (since version 1.2, 2010)</li>
* <li>Supports partial content (streaming)</li>
* <li>Supports ETags</li>
* <li>Never caches anything</li>
* <li>Doesn't limit bandwidth, request time or simultaneous connections</li>
* <li>Default code serves files and shows all HTTP parameters and headers</li>
* <li>File server supports directory listing, index.html and index.htm</li>
* <li>File server supports partial content (streaming)</li>
* <li>File server supports ETags</li>
* <li>File server does the 301 redirection trick for directories without '/'</li>
* <li>File server supports simple skipping for files (continue download)</li>
* <li>File server serves also very long files without memory overhead</li>
* <li>Contains a built-in list of most common mime types</li>
* <li>All header names are converted lowercase so they don't vary between browsers/clients</li>
* <p/>
* </ul>
* <p/>
* <p/>
* <b>How to use: </b>
* <ul>
* <p/>
* <li>Subclass and implement serve() and embed to your own program</li>
* <p/>
* </ul>
* <p/>
* See the separate "LICENSE.md" file for the distribution license (Modified BSD licence)
*/
public abstract class NanoHTTPD {
/**
* Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
* This is required as the Keep-Alive HTTP connections would otherwise
* block the socket reading thread forever (or as long the browser is open).
*/
public static final int SOCKET_READ_TIMEOUT = 5000;
/**
* Common mime type for dynamic content: plain text
*/
public static final String MIME_PLAINTEXT = "text/plain";
/**
* Common mime type for dynamic content: html
*/
public static final String MIME_HTML = "text/html";
/**
* Pseudo-Parameter to use to store the actual query string in the parameters map for later re-processing.
*/
private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
private final String hostname;
private final int myPort;
private ServerSocket myServerSocket;
private Set<Socket> openConnections = new HashSet<Socket>();
private Thread myThread;
/**
* Pluggable strategy for asynchronously executing requests.
*/
private AsyncRunner asyncRunner;
/**
* Pluggable strategy for creating and cleaning up temporary files.
*/
private TempFileManagerFactory tempFileManagerFactory; /**
* Constructs an HTTP server on given port.
*/
public NanoHTTPD(int port) {
this(null, port);
} /**
* Constructs an HTTP server on given hostname and port.
*/
public NanoHTTPD(String hostname, int port) {
this.hostname = hostname;
this.myPort = port;
setTempFileManagerFactory(new DefaultTempFileManagerFactory());
setAsyncRunner(new DefaultAsyncRunner());
} private static final void safeClose(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
}
}
} private static final void safeClose(Socket closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
}
}
} private static final void safeClose(ServerSocket closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
}
}
} /**
* Start the server.
*
* @throws IOException if the socket is in use.
*/
public void start() throws IOException {
myServerSocket = new ServerSocket();
myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort)); myThread = new Thread(new Runnable() {
@Override
public void run() {
do {
try {
final Socket finalAccept = myServerSocket.accept();
registerConnection(finalAccept);
finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT);
final InputStream inputStream = finalAccept.getInputStream();
asyncRunner.exec(new Runnable() {
@Override
public void run() {
OutputStream outputStream = null;
try {
outputStream = finalAccept.getOutputStream();
TempFileManager tempFileManager = tempFileManagerFactory.create();
HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress());
while (!finalAccept.isClosed()) {
session.execute();
}
} catch (Exception e) {
// When the socket is closed by the client, we throw our own SocketException
// to break the "keep alive" loop above.
if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage()))) {
e.printStackTrace();
}
} finally {
safeClose(outputStream);
safeClose(inputStream);
safeClose(finalAccept);
unRegisterConnection(finalAccept);
}
}
});
} catch (IOException e) {
}
} while (!myServerSocket.isClosed());
}
});
myThread.setDaemon(true);
myThread.setName("NanoHttpd Main Listener");
myThread.start();
} /**
* Stop the server.
*/
public void stop() {
try {
safeClose(myServerSocket);
closeAllConnections();
myThread.join();
} catch (Exception e) {
e.printStackTrace();
}
} /**
* Registers that a new connection has been set up.
*
* @param socket
* the {@link Socket} for the connection.
*/
public synchronized void registerConnection(Socket socket) {
openConnections.add(socket);
} /**
* Registers that a connection has been closed
*
* @param socket
* the {@link Socket} for the connection.
*/
public synchronized void unRegisterConnection(Socket socket) {
openConnections.remove(socket);
} /**
* Forcibly closes all connections that are open.
*/
public synchronized void closeAllConnections() {
for (Socket socket : openConnections) {
safeClose(socket);
}
} public final int getListeningPort() {
return myServerSocket == null ? -1 : myServerSocket.getLocalPort();
} public final boolean wasStarted() {
return myServerSocket != null && myThread != null;
} public final boolean isAlive() {
return wasStarted() && !myServerSocket.isClosed() && myThread.isAlive();
} /**
* Override this to customize the server.
* <p/>
* <p/>
* (By default, this delegates to serveFile() and allows directory listing.)
*
* @param uri Percent-decoded URI without parameters, for example "/index.cgi"
* @param method "GET", "POST" etc.
* @param parms Parsed, percent decoded parameters from URI and, in case of POST, data.
* @param headers Header entries, percent decoded
* @return HTTP response, see class Response for details
*/
@Deprecated
public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms,
Map<String, String> files) {
return new Response(Response.Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found");
} /**
* Override this to customize the server.
* <p/>
* <p/>
* (By default, this delegates to serveFile() and allows directory listing.)
*
* @param session The HTTP session
* @return HTTP response, see class Response for details
* 我们需要自己实现这个方法
*/
public Response serve(IHTTPSession session) {
Map<String, String> files = new HashMap<String, String>();
Method method = session.getMethod();
if (Method.PUT.equals(method) || Method.POST.equals(method)) {
try {
session.parseBody(files);
} catch (IOException ioe) {
return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
} catch (ResponseException re) {
return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
}
} Map<String, String> parms = session.getParms();
parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString());
return serve(session.getUri(), method, session.getHeaders(), parms, files);
} /**
* Decode percent encoded <code>String</code> values.
*
* @param str the percent encoded <code>String</code>
* @return expanded form of the input, for example "foo%20bar" becomes "foo bar"
*/
protected String decodePercent(String str) {
String decoded = null;
try {
decoded = URLDecoder.decode(str, "UTF8");
} catch (UnsupportedEncodingException ignored) {
}
return decoded;
} /**
* Decode parameters from a URL, handing the case where a single parameter name might have been
* supplied several times, by return lists of values. In general these lists will contain a single
* element.
*
* @param parms original <b>NanoHttpd</b> parameters values, as passed to the <code>serve()</code> method.
* @return a map of <code>String</code> (parameter name) to <code>List<String></code> (a list of the values supplied).
*/
protected Map<String, List<String>> decodeParameters(Map<String, String> parms) {
return this.decodeParameters(parms.get(QUERY_STRING_PARAMETER));
} /**
* Decode parameters from a URL, handing the case where a single parameter name might have been
* supplied several times, by return lists of values. In general these lists will contain a single
* element.
*
* @param queryString a query string pulled from the URL.
* @return a map of <code>String</code> (parameter name) to <code>List<String></code> (a list of the values supplied).
*/
protected Map<String, List<String>> decodeParameters(String queryString) {
Map<String, List<String>> parms = new HashMap<String, List<String>>();
if (queryString != null) {
StringTokenizer st = new StringTokenizer(queryString, "&");
while (st.hasMoreTokens()) {
String e = st.nextToken();
int sep = e.indexOf('=');
String propertyName = (sep >= 0) ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
if (!parms.containsKey(propertyName)) {
parms.put(propertyName, new ArrayList<String>());
}
String propertyValue = (sep >= 0) ? decodePercent(e.substring(sep + 1)) : null;
if (propertyValue != null) {
parms.get(propertyName).add(propertyValue);
}
}
}
return parms;
} // ------------------------------------------------------------------------------- //
//
// Threading Strategy.
//
// ------------------------------------------------------------------------------- // /**
* Pluggable strategy for asynchronously executing requests.
*
* @param asyncRunner new strategy for handling threads.
*/
public void setAsyncRunner(AsyncRunner asyncRunner) {
this.asyncRunner = asyncRunner;
} // ------------------------------------------------------------------------------- //
//
// Temp file handling strategy.
//
// ------------------------------------------------------------------------------- // /**
* Pluggable strategy for creating and cleaning up temporary files.
*
* @param tempFileManagerFactory new strategy for handling temp files.
*/
public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) {
this.tempFileManagerFactory = tempFileManagerFactory;
} /**
* HTTP Request methods, with the ability to decode a <code>String</code> back to its enum value.
*/
public enum Method {
GET, PUT, POST, DELETE, HEAD, OPTIONS; static Method lookup(String method) {
for (Method m : Method.values()) {
if (m.toString().equalsIgnoreCase(method)) {
return m;
}
}
return null;
}
} /**
* Pluggable strategy for asynchronously executing requests.
*/
public interface AsyncRunner {
void exec(Runnable code);
} /**
* Factory to create temp file managers.
*/
public interface TempFileManagerFactory {
TempFileManager create();
} // ------------------------------------------------------------------------------- // /**
* Temp file manager.
* <p/>
* <p>Temp file managers are created 1-to-1 with incoming requests, to create and cleanup
* temporary files created as a result of handling the request.</p>
*/
public interface TempFileManager {
TempFile createTempFile() throws Exception; void clear();
} /**
* A temp file.
* <p/>
* <p>Temp files are responsible for managing the actual temporary storage and cleaning
* themselves up when no longer needed.</p>
*/
public interface TempFile {
OutputStream open() throws Exception; void delete() throws Exception; String getName();
} /**
* Default threading strategy for NanoHttpd.
* <p/>
* <p>By default, the server spawns a new Thread for every incoming request. These are set
* to <i>daemon</i> status, and named according to the request number. The name is
* useful when profiling the application.</p>
*/
public static class DefaultAsyncRunner implements AsyncRunner {
private long requestCount; @Override
public void exec(Runnable code) {
++requestCount;
Thread t = new Thread(code);
t.setDaemon(true);
t.setName("NanoHttpd Request Processor (#" + requestCount + ")");
t.start();
}
} /**
* Default strategy for creating and cleaning up temporary files.
* <p/>
* <p></p>This class stores its files in the standard location (that is,
* wherever <code>java.io.tmpdir</code> points to). Files are added
* to an internal list, and deleted when no longer needed (that is,
* when <code>clear()</code> is invoked at the end of processing a
* request).</p>
*/
public static class DefaultTempFileManager implements TempFileManager {
private final String tmpdir;
private final List<TempFile> tempFiles; public DefaultTempFileManager() {
tmpdir = System.getProperty("java.io.tmpdir");
tempFiles = new ArrayList<TempFile>();
} @Override
public TempFile createTempFile() throws Exception {
DefaultTempFile tempFile = new DefaultTempFile(tmpdir);
tempFiles.add(tempFile);
return tempFile;
} @Override
public void clear() {
for (TempFile file : tempFiles) {
try {
file.delete();
} catch (Exception ignored) {
}
}
tempFiles.clear();
}
} /**
* Default strategy for creating and cleaning up temporary files.
* <p/>
* <p></p></[>By default, files are created by <code>File.createTempFile()</code> in
* the directory specified.</p>
*/
public static class DefaultTempFile implements TempFile {
private File file;
private OutputStream fstream; public DefaultTempFile(String tempdir) throws IOException {
file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
fstream = new FileOutputStream(file);
} @Override
public OutputStream open() throws Exception {
return fstream;
} @Override
public void delete() throws Exception {
safeClose(fstream);
file.delete();
} @Override
public String getName() {
return file.getAbsolutePath();
}
} /**
* HTTP response. Return one of these from serve().
*/
public static class Response {
/**
* HTTP status code after processing, e.g. "200 OK", HTTP_OK
*/
private Status status;
/**
* MIME type of content, e.g. "text/html"
*/
private String mimeType;
/**
* Data of the response, may be null.
*/
private InputStream data;
/**
* Headers for the HTTP response. Use addHeader() to add lines.
*/
private Map<String, String> header = new HashMap<String, String>();
/**
* The request method that spawned this response.
*/
private Method requestMethod;
/**
* Use chunkedTransfer
*/
private boolean chunkedTransfer; /**
* Default constructor: response = HTTP_OK, mime = MIME_HTML and your supplied message
*/
public Response(String msg) {
this(Status.OK, MIME_HTML, msg);
} /**
* Basic constructor.
*/
public Response(Status status, String mimeType, InputStream data) {
this.status = status;
this.mimeType = mimeType;
this.data = data;
} /**
* Convenience method that makes an InputStream out of given text.
*/
public Response(Status status, String mimeType, String txt) {
this.status = status;
this.mimeType = mimeType;
try {
this.data = txt != null ? new ByteArrayInputStream(txt.getBytes("UTF-8")) : null;
} catch (java.io.UnsupportedEncodingException uee) {
uee.printStackTrace();
}
} /**
* Adds given line to the header.
*/
public void addHeader(String name, String value) {
header.put(name, value);
} /**
* Sends given response to the socket.
*/
private void send(OutputStream outputStream) {
String mime = mimeType;
SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT")); try {
if (status == null) {
throw new Error("sendResponse(): Status can't be null.");
}
PrintWriter pw = new PrintWriter(outputStream);
pw.print("HTTP/1.1 " + status.getDescription() + " \r\n"); if (mime != null) {
pw.print("Content-Type: " + mime + "\r\n");
} if (header == null || header.get("Date") == null) {
pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
} if (header != null) {
for (String key : header.keySet()) {
String value = header.get(key);
pw.print(key + ": " + value + "\r\n");
}
} pw.print("Connection: keep-alive\r\n"); if (requestMethod != Method.HEAD && chunkedTransfer) {
sendAsChunked(outputStream, pw);
} else {
sendAsFixedLength(outputStream, pw);
}
outputStream.flush();
safeClose(data);
} catch (IOException ioe) {
// Couldn't write? No can do.
}
} private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException {
pw.print("Transfer-Encoding: chunked\r\n");
pw.print("\r\n");
pw.flush();
int BUFFER_SIZE = 16 * 1024;
byte[] CRLF = "\r\n".getBytes();
byte[] buff = new byte[BUFFER_SIZE];
int read;
while ((read = data.read(buff)) > 0) {
outputStream.write(String.format("%x\r\n", read).getBytes());
outputStream.write(buff, 0, read);
outputStream.write(CRLF);
}
outputStream.write(String.format("0\r\n\r\n").getBytes());
} private void sendAsFixedLength(OutputStream outputStream, PrintWriter pw) throws IOException {
int pending = data != null ? data.available() : 0; // This is to support partial sends, see serveFile()
pw.print("Content-Length: "+pending+"\r\n"); pw.print("\r\n");
pw.flush(); if (requestMethod != Method.HEAD && data != null) {
int BUFFER_SIZE = 16 * 1024;
byte[] buff = new byte[BUFFER_SIZE];
while (pending > 0) {
int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
if (read <= 0) {
break;
}
outputStream.write(buff, 0, read); pending -= read;
}
}
} public Status getStatus() {
return status;
} public void setStatus(Status status) {
this.status = status;
} public String getMimeType() {
return mimeType;
} public void setMimeType(String mimeType) {
this.mimeType = mimeType;
} public InputStream getData() {
return data;
} public void setData(InputStream data) {
this.data = data;
} public Method getRequestMethod() {
return requestMethod;
} public void setRequestMethod(Method requestMethod) {
this.requestMethod = requestMethod;
} public void setChunkedTransfer(boolean chunkedTransfer) {
this.chunkedTransfer = chunkedTransfer;
} /**
* Some HTTP response status codes
*/
public enum Status {
OK(200, "OK"), CREATED(201, "Created"), ACCEPTED(202, "Accepted"), NO_CONTENT(204, "No Content"), PARTIAL_CONTENT(206, "Partial Content"), REDIRECT(301,
"Moved Permanently"), NOT_MODIFIED(304, "Not Modified"), BAD_REQUEST(400, "Bad Request"), UNAUTHORIZED(401,
"Unauthorized"), FORBIDDEN(403, "Forbidden"), NOT_FOUND(404, "Not Found"), METHOD_NOT_ALLOWED(405, "Method Not Allowed"), RANGE_NOT_SATISFIABLE(416,
"Requested Range Not Satisfiable"), INTERNAL_ERROR(500, "Internal Server Error");
private final int requestStatus;
private final String description; Status(int requestStatus, String description) {
this.requestStatus = requestStatus;
this.description = description;
} public int getRequestStatus() {
return this.requestStatus;
} public String getDescription() {
return "" + this.requestStatus + " " + description;
}
}
} public static final class ResponseException extends Exception { private final Response.Status status; public ResponseException(Response.Status status, String message) {
super(message);
this.status = status;
} public ResponseException(Response.Status status, String message, Exception e) {
super(message, e);
this.status = status;
} public Response.Status getStatus() {
return status;
}
} /**
* Default strategy for creating and cleaning up temporary files.
*/
private class DefaultTempFileManagerFactory implements TempFileManagerFactory {
@Override
public TempFileManager create() {
return new DefaultTempFileManager();
}
} /**
* Handles one session, i.e. parses the HTTP request and returns the response.
*/
public interface IHTTPSession {
void execute() throws IOException; Map<String, String> getParms(); Map<String, String> getHeaders(); /**
* @return the path part of the URL.
*/
String getUri(); String getQueryParameterString(); Method getMethod(); InputStream getInputStream(); CookieHandler getCookies(); /**
* Adds the files in the request body to the files map.
* @arg files - map to modify
*/
void parseBody(Map<String, String> files) throws IOException, ResponseException;
} protected class HTTPSession implements IHTTPSession {
public static final int BUFSIZE = 8192;
private final TempFileManager tempFileManager;
private final OutputStream outputStream;
private PushbackInputStream inputStream;
private int splitbyte;
private int rlen;
private String uri;
private Method method;
private Map<String, String> parms;
private Map<String, String> headers;
private CookieHandler cookies;
private String queryParameterString; public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
this.tempFileManager = tempFileManager;
this.inputStream = new PushbackInputStream(inputStream, BUFSIZE);
this.outputStream = outputStream;
} public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
this.tempFileManager = tempFileManager;
this.inputStream = new PushbackInputStream(inputStream, BUFSIZE);
this.outputStream = outputStream;
String remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
headers = new HashMap<String, String>(); headers.put("remote-addr", remoteIp);
headers.put("http-client-ip", remoteIp);
} @Override
public void execute() throws IOException {
try {
/* Read the first 8192 bytes.
The full header should fit in here.
Apache's default header limit is 8KB.
Do NOT assume that a single read will get the entire header at once!
header的最大长度为8192bytes,所以这里设置BUFSIZE为8192bytes
*/
byte[] buf = new byte[BUFSIZE];
splitbyte = 0;
rlen = 0;//总共的长度
{
int read = -1;//每次读取的长度
try {
read = inputStream.read(buf, 0, BUFSIZE);
} catch (Exception e) {
safeClose(inputStream);
safeClose(outputStream);
throw new SocketException("NanoHttpd Shutdown");
}
if (read == -1) {
// socket was been closed
safeClose(inputStream);
safeClose(outputStream);
throw new SocketException("NanoHttpd Shutdown");
}
while (read > 0) {
rlen += read;
splitbyte = findHeaderEnd(buf, rlen);
if (splitbyte > 0)//如果返回0则说明没有找到
break;
read = inputStream.read(buf, rlen, BUFSIZE - rlen);
}
}
//如果读多了,则将多余的部分重新写到流里面
if (splitbyte < rlen) {
inputStream.unread(buf, splitbyte, rlen - splitbyte);
} parms = new HashMap<String, String>();
if(null == headers) {
headers = new HashMap<String, String>();
} // Create a BufferedReader for parsing the header.
BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen))); // Decode the header into parms and header java properties
Map<String, String> pre = new HashMap<String, String>();
decodeHeader(hin, pre, parms, headers);
/**
* 取出客户端请求的方式:GET/POST
*/
method = Method.lookup(pre.get("method"));
if (method == null) {
throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
}
//请求的连接
uri = pre.get("uri"); cookies = new CookieHandler(headers); /*
* Ok, now do the serve()
* 解析
*/
Response r = serve(this);
if (r == null) {
throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
} else {
cookies.unloadQueue(r);
//响应客户端信息
r.setRequestMethod(method);
r.send(outputStream);
}
} catch (SocketException e) {
// throw it out to close socket object (finalAccept)
throw e;
} catch (SocketTimeoutException ste) {
throw ste;
} catch (IOException ioe) {
Response r = new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
r.send(outputStream);
safeClose(outputStream);
} catch (ResponseException re) {
Response r = new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
r.send(outputStream);
safeClose(outputStream);
} finally {
tempFileManager.clear();
}
} @Override
public void parseBody(Map<String, String> files) throws IOException, ResponseException {
RandomAccessFile randomAccessFile = null;
BufferedReader in = null;
try {
//创建临时文件
randomAccessFile = getTmpBucket(); long size;
if (headers.containsKey("content-length")) {
size = Integer.parseInt(headers.get("content-length"));
} else if (splitbyte < rlen) {
size = rlen - splitbyte;
} else {
size = 0;
} /*
* Now read all the body and write it to f
* 将流的内容写到临时文件中
*/
byte[] buf = new byte[512];
while (rlen >= 0 && size > 0) {
rlen = inputStream.read(buf, 0, (int)Math.min(size, 512));
size -= rlen;
if (rlen > 0) {
randomAccessFile.write(buf, 0, rlen);
}
} // Get the raw body as a byte []
ByteBuffer fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length());
randomAccessFile.seek(0);//重新定位到临时文件的开始位置 // Create a BufferedReader for easily reading it as string.
InputStream bin = new FileInputStream(randomAccessFile.getFD());
in = new BufferedReader(new InputStreamReader(bin)); // If the method is POST, there may be parameters
// in data section, too, read it:
if (Method.POST.equals(method)) {
String contentType = "";
String contentTypeHeader = headers.get("content-type"); StringTokenizer st = null;
if (contentTypeHeader != null) {
st = new StringTokenizer(contentTypeHeader, ",; ");
if (st.hasMoreTokens()) {
contentType = st.nextToken();
}
}
//表单提交,文件上传类型
if ("multipart/form-data".equalsIgnoreCase(contentType)) {
// Handle multipart/form-data
if (!st.hasMoreTokens()) {
throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
} String boundaryStartString = "boundary=";
int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
if (boundary.startsWith("\"") && boundary.endsWith("\"")) {
boundary = boundary.substring(1, boundary.length() - 1);
}
//fbuf里面包含body的内容
decodeMultipartData(boundary, fbuf, in, parms, files);
} else {
// Handle application/x-www-form-urlencoded
String postLine = "";
StringBuilder postLineBuffer = new StringBuilder();
char pbuf[] = new char[512];
int read = in.read(pbuf);
while (read >= 0 && !postLine.endsWith("\r\n")) {
postLine = String.valueOf(pbuf, 0, read);
postLineBuffer.append(postLine);
read = in.read(pbuf);
}
postLine = postLineBuffer.toString().trim();
decodeParms(postLine, parms);
}
} else if (Method.PUT.equals(method)) {
String content = saveTmpFile(fbuf, 0, fbuf.limit());
files.put("content", content);
}
} finally {
safeClose(randomAccessFile);
safeClose(in);
}
} /**
* Decodes the sent headers and loads the data into Key/value pairs
*/
private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers)
throws ResponseException {
try {
// Read the request line GET / HTTP/1.1
String inLine = in.readLine();
if (inLine == null) {
return;
}
//分隔inLine用 \t\n\r\f
StringTokenizer st = new StringTokenizer(inLine);
if (!st.hasMoreTokens()) {
throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
}
String method = st.nextToken();
pre.put("method", method);
if (!st.hasMoreTokens()) {
throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
} String uri = st.nextToken();
/*
* Decode parameters from the URI
* 以"?" 来判断是否有参数
*/
int qmi = uri.indexOf('?');
if (qmi >= 0) {//说明有参数,调用decodeParms将参数解析到parms中
decodeParms(uri.substring(qmi + 1), parms);
uri = decodePercent(uri.substring(0, qmi));
} else {
uri = decodePercent(uri);
}
// If there's another token, it's protocol version,
// followed by HTTP headers. Ignore version but parse headers.
// NOTE: this now forces header names lowercase since they are
// case insensitive and vary by client.
if (st.hasMoreTokens()) {
String line = in.readLine();
while (line != null && line.trim().length() > 0) {
int p = line.indexOf(':');
if (p >= 0)
headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
line = in.readLine();
}
}
pre.put("uri", uri);
} catch (IOException ioe) {
throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
}
} /**
* Decodes the Multipart Body data and put it into Key/Value pairs.
* @param boundary
* @param fbuf
* @param in
* @param parms header头里面的参数
* @param files 支持多文件上传功能
* @throws ResponseException
*/
private void decodeMultipartData(String boundary, ByteBuffer fbuf, BufferedReader in, Map<String, String> parms,
Map<String, String> files) throws ResponseException {
try {
//根据boundary获取分割线与内容的间隔位置
int[] bpositions = getBoundaryPositions(fbuf, boundary.getBytes());
int boundarycount = 2;//源码为1,如果是上传单个文件会有问题
String mpline = in.readLine();
while (mpline != null) {
if (!mpline.contains(boundary)) {
throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html");
}
boundarycount++;
Map<String, String> item = new HashMap<String, String>();
mpline = in.readLine();
while (mpline != null && mpline.trim().length() > 0) {
int p = mpline.indexOf(':');
if (p != -1) {
item.put(mpline.substring(0, p).trim().toLowerCase(Locale.US), mpline.substring(p + 1).trim());
}
mpline = in.readLine();
}
if (mpline != null) {
String contentDisposition = item.get("content-disposition");
if (contentDisposition == null) {
throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html");
}
StringTokenizer st = new StringTokenizer(contentDisposition, "; ");
Map<String, String> disposition = new HashMap<String, String>();
while (st.hasMoreTokens()) {
String token = st.nextToken();
int p = token.indexOf('=');
if (p != -1) {
disposition.put(token.substring(0, p).trim().toLowerCase(Locale.US), token.substring(p + 1).trim());
}
}
String pname = disposition.get("name");
pname = pname.substring(1, pname.length() - 1); String value = "";
if (item.get("content-type") == null) {
while (mpline != null && !mpline.contains(boundary)) {
mpline = in.readLine();
if (mpline != null) {
int d = mpline.indexOf(boundary);
if (d == -1) {
value += mpline;
} else {
value += mpline.substring(0, d - 2);
}
}
}
} else {
if (boundarycount > bpositions.length) {
throw new ResponseException(Response.Status.INTERNAL_ERROR, "Error processing request");
}
int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount - 2]);
//创建一个临时文件,然后保存内容,这个函数我们也需要重写setTempFileManagerFactory
String path = saveTmpFile(fbuf, offset, bpositions[boundarycount - 1] - offset - 4);
files.put(pname, path);
value = disposition.get("filename");
value = value.substring(1, value.length() - 1);
do {
mpline = in.readLine();
} while (mpline != null && !mpline.contains(boundary));
}
parms.put(pname, value);
}
}
} catch (IOException ioe) {
throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
}
} /**
* Find byte index separating header from body. It must be the last byte of the first two sequential new lines.
* 找到head和body的分界点,两个回车换行
*/
private int findHeaderEnd(final byte[] buf, int rlen) {
int splitbyte = 0;
while (splitbyte + 3 < rlen) {
if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
return splitbyte + 4;
}
splitbyte++;
}
return 0;
} /**
* Find the byte positions where multipart boundaries start.
* @param b
* @param boundary
* @return
*/
private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) {
int matchcount = 0;
int matchbyte = -1;
List<Integer> matchbytes = new ArrayList<Integer>();
for (int i = 0; i < b.limit(); i++) {
if (b.get(i) == boundary[matchcount]) {
if (matchcount == 0)
matchbyte = i;
matchcount++;
if (matchcount == boundary.length) {
matchbytes.add(matchbyte);
matchcount = 0;
matchbyte = -1;
}
} else {
i -= matchcount;
matchcount = 0;
matchbyte = -1;
}
}
int[] ret = new int[matchbytes.size()];
for (int i = 0; i < ret.length; i++) {
ret[i] = matchbytes.get(i);
}
return ret;
} /**
* Retrieves the content of a sent file and saves it to a temporary file. The full path to the saved file is returned.
*/
private String saveTmpFile(ByteBuffer b, int offset, int len) {
String path = "";
if (len > 0) {
FileOutputStream fileOutputStream = null;
try {
//这个地方的tempFileManager需要我们setTempFileManagerFactory
TempFile tempFile = tempFileManager.createTempFile();
ByteBuffer src = b.duplicate();
fileOutputStream = new FileOutputStream(tempFile.getName());
FileChannel dest = fileOutputStream.getChannel();
src.position(offset).limit(offset + len);
dest.write(src.slice());
path = tempFile.getName();
} catch (Exception e) { // Catch exception if any
System.err.println("Error: " + e.getMessage());
} finally {
safeClose(fileOutputStream);
}
}
return path;
} private RandomAccessFile getTmpBucket() {
try {
TempFile tempFile = tempFileManager.createTempFile();
return new RandomAccessFile(tempFile.getName(), "rw");
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
return null;
} /**
* It returns the offset separating multipart file headers from the file's data.
*/
private int stripMultipartHeaders(ByteBuffer b, int offset) {
int i;
for (i = offset; i < b.limit(); i++) {
if (b.get(i) == '\r' && b.get(++i) == '\n' && b.get(++i) == '\r' && b.get(++i) == '\n') {
break;
}
}
return i + 1;
} /**
* Decodes parameters in percent-encoded URI-format ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
* adds them to given Map. NOTE: this doesn't support multiple identical keys due to the simplicity of Map.
*/
private void decodeParms(String parms, Map<String, String> p) {
if (parms == null) {
queryParameterString = "";
return;
} queryParameterString = parms;
//使用&来分隔参数
StringTokenizer st = new StringTokenizer(parms, "&");
while (st.hasMoreTokens()) {
String e = st.nextToken();
int sep = e.indexOf('=');
if (sep >= 0) {
/*
* 这里使用decodePercent来解析,防止参数里面带有中文
* 比如“张杰”那么浏览器会写成%E5%BC%A0%E6%9D%B0
*/
p.put(decodePercent(e.substring(0, sep)).trim(),
decodePercent(e.substring(sep + 1)));
} else {
p.put(decodePercent(e).trim(), "");
}
}
} @Override
public final Map<String, String> getParms() {
return parms;
} public String getQueryParameterString() {
return queryParameterString;
} @Override
public final Map<String, String> getHeaders() {
return headers;
} @Override
public final String getUri() {
return uri;
} @Override
public final Method getMethod() {
return method;
} @Override
public final InputStream getInputStream() {
return inputStream;
} @Override
public CookieHandler getCookies() {
return cookies;
}
} public static class Cookie {
private String n, v, e; public Cookie(String name, String value, String expires) {
n = name;
v = value;
e = expires;
} public Cookie(String name, String value) {
this(name, value, 30);
} public Cookie(String name, String value, int numDays) {
n = name;
v = value;
e = getHTTPTime(numDays);
} public String getHTTPHeader() {
String fmt = "%s=%s; expires=%s";
return String.format(fmt, n, v, e);
} public static String getHTTPTime(int days) {
Calendar calendar = Calendar.getInstance();
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
calendar.add(Calendar.DAY_OF_MONTH, days);
return dateFormat.format(calendar.getTime());
}
} /**
* Provides rudimentary support for cookies.
* Doesn't support 'path', 'secure' nor 'httpOnly'.
* Feel free to improve it and/or add unsupported features.
*
* @author LordFokas
*/
public class CookieHandler implements Iterable<String> {
private HashMap<String, String> cookies = new HashMap<String, String>();
private ArrayList<Cookie> queue = new ArrayList<Cookie>(); public CookieHandler(Map<String, String> httpHeaders) {
String raw = httpHeaders.get("cookie");
if (raw != null) {
String[] tokens = raw.split(";");
for (String token : tokens) {
String[] data = token.trim().split("=");
if (data.length == 2) {
cookies.put(data[0], data[1]);
}
}
}
} @Override public Iterator<String> iterator() {
return cookies.keySet().iterator();
} /**
* Read a cookie from the HTTP Headers.
*
* @param name The cookie's name.
* @return The cookie's value if it exists, null otherwise.
*/
public String read(String name) {
return cookies.get(name);
} /**
* Sets a cookie.
*
* @param name The cookie's name.
* @param value The cookie's value.
* @param expires How many days until the cookie expires.
*/
public void set(String name, String value, int expires) {
queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires)));
} public void set(Cookie cookie) {
queue.add(cookie);
} /**
* Set a cookie with an expiration date from a month ago, effectively deleting it on the client side.
*
* @param name The cookie name.
*/
public void delete(String name) {
set(name, "-delete-", -30);
} /**
* Internally used by the webserver to add all queued cookies into the Response's HTTP Headers.
*
* @param response The Response object to which headers the queued cookies will be added.
*/
public void unloadQueue(Response response) {
for (Cookie cookie : queue) {
response.addHeader("Set-Cookie", cookie.getHTTPHeader());
}
}
}
}

我们测试的时候需要自己定义的类:

package com.sohu.nanohttpd;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log; public class MainActivity extends Activity { @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); NanoHTTPD nanoHttpd = new MyNanoHTTPD("127.0.0.1",23456);
try{
nanoHttpd.start();
}catch(Exception e){
e.printStackTrace();
} Log.e("DEMO",nanoHttpd.isAlive()+""); } } class MyNanoHTTPD extends NanoHTTPD{ public MyNanoHTTPD(int port) {
super(port);
} public MyNanoHTTPD(String hostName,int port){
super(hostName,port);
} public Response serve(IHTTPSession session) {
Method method = session.getMethod();
Log.e("DEMO","Method:"+method.toString());
if(NanoHTTPD.Method.GET.equals(method)){
//get方式
String queryParams = session.getQueryParameterString();
Log.e("DEMO","params:"+queryParams);
}else if(NanoHTTPD.Method.POST.equals(method)){
//post方式
}
return super.serve(session);
} }

这里只是做了一下拓展知识,两种方式搭建都可以的,但是明显Apache的功能强大一点。。

测试项目的下载地址:

Android测试项目HttpServer下载地址

http://download.csdn.net/detail/jiangwei0910410003/7432533

PC端的Server测试项目HttpServer下载地址

http://download.csdn.net/detail/jiangwei0910410003/7432537

Android测试项目NanoHTTPD下载地址

http://download.csdn.net/detail/jiangwei0910410003/7432595

总结:上面就是将PC端的Cookie和移动端的imei进行绑定的操作,这种操作的价值是很大的,未来如果用户在家里面进行上网的话,我们就可以将PC端和移动端进行紧密耦合在一起,相当于一个整体。。这样我们对用户的行为就更加了解了。。

Android中如何搭建一个WebServer的更多相关文章

  1. 在Android中动画移动一个View的位置,采用Scroller类实现Android动画之 View移动

    在Android中动画移动一个View的位置,采用Scroller类实现 今天说最近自己遇到的一个问题,就是要用动画效果来移动一个VIew的位置. 这个具体的情况是,需要做一个SlidingMenu的 ...

  2. Android中Service的一个Demo例子

    Android中Service的一个Demo例子  Service组件是Android系统重要的一部分,网上看了代码,很简单,但要想熟练使用还是需要Coding.  本文,主要贴代码,不对Servic ...

  3. Android在如何建立一个WebServer

    今天老板交待任务最终完成了,感觉收获颇多,所以写一个关于它的记录,首先,看一下.老板的需求 需求: 希望移动端的用户标识(IMEI)和HTML页面的用户标识(Cookie)连接起来,当中HTML页面可 ...

  4. Android中的第一个NDK的例子

    前几天研究了JNI技术后,想在Android上试一试研究结果,查阅了很多资料后,总结如下步骤: 首先来看一下什么是NDK NDK 提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动 ...

  5. 浅谈android中只使用一个TextView实现高仿京东,淘宝各种倒计时

    今天给大家带来的是只使用一个TextView实现一个高仿京东.淘宝.唯品会等各种电商APP的活动倒计时.近期公司一直加班也没来得及时间去整理,今天难得歇息想把这个分享给大家.只求共同学习,以及自己兴许 ...

  6. Android中自己定义一个shade.xml

    自己定义一个shade: <shape> <!-- 实心 --> <solid android:color="#ff9d77"/> <!- ...

  7. Android 中如何从一个App启动另外一个App(如启动支付界面、启动地图界面、应用商场下载App等场景)

    假定两个App,分别是A和B,当A运行某个功能需要启动B,一种是启动B应用,一种直接进入B的某个Activity.搜了很多资料,没有一个完整的.下面就A--Android5.1.1.B--Androi ...

  8. Android中调用另一个Activity并返回结果-以模拟选择头像功能为例

    场景 Android中点击按钮启动另一个Activity以及Activity之间传值: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/detail ...

  9. iOS 10中如何搭建一个语音转文字框架

    在2016WWDC大会上,Apple公司介绍了一个很好的语音识别的API,那就是Speech framework.事实上,这个Speech Kit就是Siri用来做语音识别的框架.如今已经有一些可用的 ...

随机推荐

  1. boost multi index

    Boost.MultiIndex makes it possible to define containers that support an arbitrary number of interfac ...

  2. shell倒计时下班时间

    #!/bin/sh offWorkTime="19:00:00" offWorkHour=${offWorkTime::} offWorkMinute=${offWorkTime: ...

  3. [CSP模拟测试43、44]题解

    状态极差的两场.感觉现在自己的思维方式很是有问题. (但愿今天考试开始的一刻我不会看到H I J) A 考场上打了最短路+贪心,水了60. 然而正解其实比那30分贪心好想多了. 进行n次乘法后的结果一 ...

  4. Linux桌面与命令行切换

    1.首先在安装Linux的时候是选则Desktop桌面方式安装 2.切换命令 2.1快捷键:Ctrl+Alt+F1    切换到桌面模式 Ctrl+Alt+F3    切换到命令行模式

  5. 2019牛客第八场多校 E_Explorer 可撤销并查集(栈)+线段树

    目录 题意: 分析: @(2019牛客暑期多校训练营(第八场)E_Explorer) 题意: 链接 题目类似:CF366D,Gym101652T 本题给你\(n(100000)\)个点\(m(1000 ...

  6. cs224d 作业 problem set2 (二) TensorFlow 实现命名实体识别

    神经网络在命名实体识别中的应用 所有的这些包括之前的两篇都可以通过tensorflow 模型的托管部署到 google cloud 上面,发布成restful接口,从而与任何的ERP,CRM系统集成. ...

  7. javascript实现继承的六种方式

    ​ /*实现继承的六种方式*/ /*1.扩展原型对象的方法实现继承*/ function Foo1(){} //在Foo1函数上扩展一个fn1方法,由构造函数创建的对象都具有fn1这个方法 Foo1. ...

  8. Java DOM解析器 - 修改XML文档

    这是我们需要修改的输入XML文件: 1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="UTF-8&q ...

  9. @RequestParam 引发的编译问题

    在使用SpringMVC绑定基本类型(如String,Integer等)参数时,应通过@RequestParam注解指定具体的参数名称,否则,当源代码在非debug模式下编译后,运行时会引发Handl ...

  10. 深浅拷贝, for循环小知识点 str操作 list的删除问题,类型转换

    深浅拷⻉  : lst1 = ["⾦⽑狮王", "紫衫⻰王", "⽩眉鹰王", "⻘翼蝠王"] lst2 = lst1 ...