一.开发初衷:最近项目中需要用到版本升级这一块,需要用到一些基本的数据请求与文件下载功能。之前做项目都是用别人的网络框架,类似retrofit 、 okhttp、 fresco等框架,用的多了,发现这几个网络请求框架,无非都是
按解决以下几个问题为导向的:
1.怎么发请求?
2.Cookie的问题。
3.如何停止请求(好像上面提到的几个框架没有停止请求的概念,因为停止请求常用用SOcket长连接协议中,
而http是短连接,只要触发了请求,就失去了控制一样。)
4.请求的并发?
5.如何管理请求的优先级(类似http这种协议请求,几乎可以忽略,请求的优先级常用于socket协议中)
一直都想写一个自己的网络请求框架,借此项目机会,刚好用上了,现将设计思路与源码贡献出来与各位一起交流学习,如有写的不好,请各位大神,批评指正,谢谢。
先从回调接口说起:这个框架中主要有两类回调
第一类为普通的字符串请求(类似json都可以视为一种字符串,只是一种特殊的格式封装的数据)
第二类为文件类的byte流数据。
package com.example.lxb.hellohttp.httpclient.listener; /** * 回调基类接口 * Created by lxb on 2017/4/12. */ public abstract class BaseRequestListener<T> { public abstract void onSuccess(T result); public abstract void onFailure(T result); public void onExcetion(T e) { } public void Excetion(String e){ } public void onLoading(long total,long curProgress){ } }
在这个类中将回调共有的的方法封装出来,如果没有特殊的回调行为可以直接用这个基类作为回调,否则可以自己去扩展。
这里我还写了一个文件的监听器:
package com.example.lxb.hellohttp.httpclient.listener; /** * 文件监听器 * * Created by lxb on 2017/4/14. */ public class FileListener<T> extends BaseRequestListener<T> { @Override public void onSuccess(T result) { } @Override public void onFailure(T result) { } public void Excetion(String e){ } public void onLoading(long total,long curProgress){ } }
二。因网络请求本身就是一种I/O操作,并且是一种阻塞式的请求。如果直接放在主线程中进行很明显会影响主线程的运行,且android系统中不允许这样干。
鉴于此,本框架中的所有请求均在一个新的线程中进行。
先来看普通字符串的请求线程:
package com.example.lxb.hellohttp.httpclient.client; import com.example.lxb.hellohttp.httpclient.HttpClient; import com.example.lxb.hellohttp.httpclient.handler.MsgHandler; import com.example.lxb.hellohttp.httpclient.listener.BaseRequestListener; import com.example.lxb.hellohttp.httpclient.request.RequestParams; /** * 请求线程 * * Created by lxb on 2017/4/12. */ public class RequestThread extends Thread { private HttpClient httpClient; private MsgHandler response; private RequestParams requestParams; public RequestParams getRequestParams() { return requestParams; } public void setRequestParams(RequestParams requestParams) { this.requestParams = requestParams; } public HttpClient getHttpClient() { return httpClient; } public void setHttpClient(HttpClient httpClient) { this.httpClient = httpClient; } public MsgHandler getResponse() { return response; } public void setResponse(MsgHandler response) { this.response = response; } @Override public void run() { super.run(); this.httpClient.execute(requestParams,response); } }
文件请求线程:
package com.example.lxb.hellohttp.httpclient.client; import com.example.lxb.hellohttp.httpclient.HttpClient; import com.example.lxb.hellohttp.httpclient.handler.MsgHandler; import com.example.lxb.hellohttp.httpclient.request.FileRequest; /** * 文件网络请求线程 * * Created by lxb on 2017/4/13. */ public class FileReuqestThread extends Thread { private HttpClient httpClient; private MsgHandler response; private FileRequest fileRequest; public FileRequest getFileRequest() { return fileRequest; } public void setFileRequest(FileRequest fileRequest) { this.fileRequest = fileRequest; } public HttpClient getHttpClient() { return httpClient; } public void setHttpClient(HttpClient httpClient) { this.httpClient = httpClient; } public MsgHandler getResponse() { return response; } public void setResponse(MsgHandler response) { this.response = response; } @Override public void run() { super.run(); this.httpClient.execute(fileRequest,response); } }
这里完全可以使用一个请求线程,但为区分不同的请求类型还是写了两个类,
两个类的行为完全一致:
1.保持真正干活类实例对象的引用(后面会讲,别急^_^)
2.绑定请求对象
3.消息分发句柄(主要用来解决android线程的通信问题,后面会讲)
既然说到这时直接把请求对象都给出来吧:
普通字符 串请求对象,如果是进行这类的请求,需要在这里进行设置参数:
package com.example.lxb.hellohttp.httpclient.request; /** * 请求基类 * Created by lxb on 2017/4/13. */ public class Request { private String URL; private String method; public String getURL() { return URL; } public void setURL(String URL) { this.URL = URL; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } }
不好意思,上面是一个请求基类,
下面才给出真正的普通字符串请求对象:
package com.example.lxb.hellohttp.httpclient.request; import android.text.TextUtils; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; /** * Created by lxb on 2017/4/11. */ public class RequestParams extends Request{ private String defaultMethod = "POST"; private Map<String,String> params = new HashMap<>(); public Map<String, String> getParams() { return params; } public void setParams(Map<String, String> params) { this.params = params; } }
文件请求对象来喽:
package com.example.lxb.hellohttp.httpclient.request; /** * Created by lxb on 2017/4/13. */ public class FileRequest extends Request{ private String filePath; public String getFilePath() { return filePath; } public void setFilePath(String filePath) { this.filePath = filePath; } }
三。再来看看直正的干活类:
方法分发接口,主要为了颗粒度:
/** * 开始执行普通 数据网络请求操作 * * @param requestParams */ public void execute(RequestParams requestParams, MsgHandler response) { this.method = requestParams.getMethod(); this.URL = requestParams.getURL(); Map<String, String> params = requestParams.getParams(); if (!params.isEmpty()) { paramsData = new JSONObject(); Iterator iter = params.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = (Map.Entry) iter.next(); try { paramsData.put(entry.getKey().toString(), entry.getValue().toString()); } catch (JSONException e) { e.printStackTrace(); } } } request(this.method, this.URL, paramsData.toString(), response); }
/** * 默认请求方法采用POST * * @param method * @param url * @param param * @return */ public void request(String method, String url, String param, MsgHandler response) { if (method.equals(MsgCode.POST)) { doPost(url, param, response); } else if (method.equals(MsgCode.GET)) { doPost(url, param, response); } }
四。http请求方式:Post 与 Get (这两种方式的区别这里暂且不提)两种方式,主要是先设置一些请求格式,然后将http中的输入/输出流给拿出来进行相应的操作就行了。
http相对socket协议来说简单很多,它的数据包格式都已经封装好了,也就是它有固定的消息头
但如果采用socket协议的话,消息头就需要自己去定义了,不然很容易出现粘包的情况。
post 方式:代码中有注释,不详细解释了
/** * 向指定 URL 发送POST方法的请求 * * @param url 发送请求的 URL * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 * @return 所代表远程资源的响应结果 * @throws Exception */ public void doPost(String url, String param, MsgHandler response) { PrintWriter out = null; BufferedReader in = null; String result = ""; try { URL realUrl = new URL(url); HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection(); conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setRequestProperty("charset", "utf-8"); conn.setUseCaches(false); conn.setDoOutput(true); conn.setDoInput(true); conn.setReadTimeout(TIMEOUT_IN_MILLIONS); conn.setConnectTimeout(TIMEOUT_IN_MILLIONS); if (param != null && !param.trim().equals("")) { out = new PrintWriter(conn.getOutputStream()); out.print(param); out.flush(); } if (conn.getResponseCode() == 200) { in = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } else { Map<String, String> Failure = new HashMap<>(); Failure.put(MsgCode.Failure_Key, conn.getResponseCode() + ""); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Failure, Failure); } } catch (Exception e) { e.printStackTrace(); Map<String, String> Exception = new HashMap<>(); Exception.put(MsgCode.Exection_Key, e.toString()); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Exection, Exception); } finally { try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (IOException ex) { ex.printStackTrace(); Map<String, String> Exception = new HashMap<>(); Exception.put(MsgCode.Exection_Key, ex.toString()); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Exection, Exception); } } Map<String, String> success = new HashMap<>(); success.put(MsgCode.Success_Key, result); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Success, success); //return result; }
get 方式:
/** * Get请求,获得返回数据 * * @param urlStr * @return * @throws Exception */ public void doGet(String urlStr, String param, MsgHandler response) { PrintWriter out = null; URL url = null; HttpURLConnection conn = null; InputStream is = null; ByteArrayOutputStream baos = null; try { url = new URL(urlStr); conn = (HttpURLConnection) url.openConnection(); conn.setUseCaches(false); conn.setDoOutput(true); conn.setDoInput(true); conn.setReadTimeout(TIMEOUT_IN_MILLIONS); conn.setConnectTimeout(TIMEOUT_IN_MILLIONS); conn.setRequestMethod("GET"); conn.setRequestProperty("accept", "*/*"); conn.setRequestProperty("connection", "Keep-Alive"); if (param != null && !param.trim().equals("")) { out = new PrintWriter(conn.getOutputStream()); out.print(param); out.flush(); } if (conn.getResponseCode() == 200) { is = conn.getInputStream(); baos = new ByteArrayOutputStream(); int len = -1; byte[] buf = new byte[1024]; while ((len = is.read(buf)) != -1) { baos.write(buf, 0, len); } baos.flush(); Map<String, String> success = new HashMap<>(); success.put(MsgCode.Success_Key, baos.toString()); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Success, success); //return baos.toString(); } else { Map<String, String> Failure = new HashMap<>(); Failure.put(MsgCode.Failure_Key, conn.getResponseCode() + ""); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Failure, Failure); //throw new RuntimeException(" responseCode is not 200 ... "); } } catch (Exception e) { //e.printStackTrace(); Map<String, String> Exception = new HashMap<>(); Exception.put(MsgCode.Exection_Key, e.toString()); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Exection, Exception); } finally { try { if (is != null) is.close(); } catch (IOException e) { Map<String, String> Exception = new HashMap<>(); Exception.put(MsgCode.Exection_Key, e.toString()); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Exection, Exception); } try { if (baos != null) baos.close(); } catch (IOException e) { Map<String, String> Exception = new HashMap<>(); Exception.put(MsgCode.Exection_Key, e.toString()); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Exection, Exception); } conn.disconnect(); } //return null; }
上传文件方法:主要是注意文件请求头的格式,其他没什么了
/** * 上传文件时,注意http协议上传文件的协议头部格式 * <p> * 示例:例如向主机192.168.1.8上传图片格式如下: * <p> * POST/logsys/home/uploadIspeedLog!doDefault.html HTTP/1.1 * Accept: text/plain, * Accept-Language: zh-cn * Host: 192.168.1.8 * Content-Type:multipart/form-data;boundary=-----------------------------7db372eb000e2 // step 1 * User-Agent: WinHttpClient * Content-Length: 3693 * Connection: Keep-Alive * <p> * -------------------------------7db372eb000e2 //step 2 * Content-Disposition: form-data; name="file"; filename="kn.jpg" //step 3 * Content-Type: image/jpeg * (此处省略jpeg文件二进制数据...) * -------------------------------7db372eb000e2-- //step 4 * * @param RequestURL * @param path * @param response * @return */ public boolean uploadFile(String RequestURL, String path, MsgHandler response) { long uploadCount = 0; long totalSize = 0; Map<String, String> Uploadprogress = new HashMap<>(); Uploadprogress.put(MsgCode.Loading_Total_Key, totalSize + ""); File uploadFile = new File(path); if (!uploadFile.exists()) return false; totalSize = uploadFile.length(); String filePostfix = path.substring(path.lastIndexOf("/") + 1, path.length()); String lineEnd = "\r\n"; //严格遵循http协议包含换行 String twoHyphens = "--"; //边界前后必须用--声明 String boundary = "*****"; //边界声明,可自定义 try { URL url = new URL(RequestURL); HttpURLConnection con = (HttpURLConnection) url.openConnection(); /** * 允许Input、Output,不使用Cache */ con.setDoInput(true); con.setDoOutput(true); con.setUseCaches(false); con.setReadTimeout(TIMEOUT_IN_MILLIONS); con.setConnectTimeout(TIMEOUT_IN_MILLIONS); /** * 设置http连接属性,这个是参照传输文件的头部信息来写的 */ con.setRequestMethod("POST"); con.setRequestProperty("Connection", "Keep-Alive"); con.setRequestProperty("Charset", "UTF-8"); con.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary); // step1 DataOutputStream dataOutputStream = new DataOutputStream(con.getOutputStream()); //获取输出流通过此处进行文件上传 /** * 注意格式 即:头+边界+尾,它们的分隔可以自定义,方便分隔一张图片是否已经上传完毕 */ dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd); //step 2 dataOutputStream.writeBytes("Content-Disposition: form-data; " + "name=\"file\";filename=\"" + filePostfix + "\"" + lineEnd); //step3 dataOutputStream.writeBytes(lineEnd); //添加换行,每一句格式都须严格遵守 /** * 处理文件 */ FileInputStream fStream = new FileInputStream(path); int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; int length = -1; while ((length = fStream.read(buffer)) != -1) { dataOutputStream.write(buffer, 0, length); uploadCount += bufferSize; Uploadprogress.put(MsgCode.Loading_CurProgress_Key, ((uploadCount * 100) / totalSize) + ""); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Loading, Uploadprogress); //System.out.println("369-----------------已上传:"+((uploadCount * 100) / totalSize)); } /** * 写入文件流时,再写入一遍文件尾,告诉服务器我文件流上传完成可以读取了 */ dataOutputStream.writeBytes(lineEnd); dataOutputStream.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd); fStream.close(); dataOutputStream.flush(); /* 取得Response内容 */ /* InputStream is = con.getInputStream(); int ch; StringBuffer b = new StringBuffer(); while ((ch = is.read()) != -1) { b.append((char) ch); }*/ dataOutputStream.close(); int nResponseCode = con.getResponseCode(); if (nResponseCode == 200) { Map<String, String> success = new HashMap<>(); success.put(MsgCode.Success_Key, path); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Success, success); return true; } else { return false; } } catch (Exception e) { System.out.println("上传失败:" + e); Map<String, String> Exception = new HashMap<>(); Exception.put(MsgCode.Exection_Key, e.toString()); ThreadCrossHandler.sendMsgByHandler(response, MsgCode.Exection, Exception); return false; } }
下载文件:
/** * 下载文件,并把文件存储在制定目录(-1:下载失败,0:下载成功,1:文件已存在) * * @param urlStr * @param path * @param fileName * @param response * @return */ public int downloadFiles(String urlStr, String path, String fileName, MsgHandler response) { try { FileUtils fileUtils = FileUtils.getInstance(); if (fileUtils.isFileExist(fileName, path)) return 1; else { InputStream inputStream = getInputStreamFromUrl(urlStr); int fileSize = getInputStreamSizeFromUrl(urlStr); File resultFile = fileUtils.write2SDFromInput(fileSize, fileName, path, inputStream, response); if (resultFile == null) return -1; } } catch (Exception e) { System.out.println("读写数据异常:" + e); return -1; } return 0; }
/** * 通过url获取输入流 * * @param urlStr * @return * @throws IOException */ public InputStream getInputStreamFromUrl(String urlStr) throws IOException { URL url = new URL(urlStr); HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); InputStream inputStream = urlConn.getInputStream(); return inputStream; } /** * 获取输入流中文件大小 * * @param urlStr * @return * @throws IOException */ private int getInputStreamSizeFromUrl(String urlStr) throws IOException { URL url = new URL(urlStr); HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); return urlConn.getContentLength(); }
请求代理类:
package com.example.lxb.hellohttp.httpclient; import com.example.lxb.hellohttp.httpclient.client.FileReuqestThread; import com.example.lxb.hellohttp.httpclient.client.RequestThread; import com.example.lxb.hellohttp.httpclient.handler.MsgHandler; import com.example.lxb.hellohttp.httpclient.request.FileRequest; import com.example.lxb.hellohttp.httpclient.request.RequestParams; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; /** * 网编请求的代理类 * <p> * Created by lxb on 2017/4/11. */ public class HelloHttp extends HttpClient { private static HelloHttp helloHttp; private RequestParams requestParams; private FileRequest fileRequest; private MsgHandler response; private RequestThread requestThread; private FileReuqestThread fileReuqestThread; private BlockingQueue<RequestThread> requestQueue = new LinkedBlockingQueue<>(10); //请求队列,默认请求大小为10 private BlockingQueue<FileReuqestThread> fileRequestQueue = new LinkedBlockingQueue<>(10); //请求队列,默认请求大小为10 /** * 单例方式构建请求实例对象 * * @return */ public static HelloHttp build() { if (helloHttp == null) { synchronized (HelloHttp.class) { if (helloHttp == null) { helloHttp = new HelloHttp(); } } } return helloHttp; } /** * 设置普通字符串请求 * * @param requestParams * @return */ public HelloHttp setRequestParams(RequestParams requestParams) { this.requestParams = requestParams; return helloHttp; } /** * 设置文件请求 * * @param fileRequest * @return */ public HelloHttp setFileRequest(FileRequest fileRequest){ this.fileRequest = fileRequest; return helloHttp; } /** * 设置消息转发句柄 * * @param response * @return */ public HelloHttp setResponse(MsgHandler response) { this.response = response; return helloHttp; } public HelloHttp() { } /** * 执行请求逻辑 */ public HelloHttp execute() { requestThread = new RequestThread(); requestThread.setRequestParams(this.requestParams); requestThread.setHttpClient(helloHttp); requestThread.setResponse(this.response); requestQueue.add(requestThread); requestThread = requestQueue.poll(); requestThread.start(); return helloHttp; } public HelloHttp executeFile(){ fileReuqestThread = new FileReuqestThread(); fileReuqestThread.setFileRequest(fileRequest); fileReuqestThread.setResponse(this.response); fileReuqestThread.setHttpClient(helloHttp); fileRequestQueue.add(fileReuqestThread); fileReuqestThread = fileRequestQueue.poll(); fileReuqestThread.start(); return helloHttp; } }
使用方法:
package com.example.lxb.microhttp; import android.os.Environment; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.example.lxb.hellohttp.httpclient.HelloHttp; import com.example.lxb.hellohttp.httpclient.HttpClient; import com.example.lxb.hellohttp.httpclient.handler.MsgCode; import com.example.lxb.hellohttp.httpclient.handler.MsgHandler; import com.example.lxb.hellohttp.httpclient.listener.BaseRequestListener; import com.example.lxb.hellohttp.httpclient.request.FileRequest; import com.example.lxb.hellohttp.httpclient.request.RequestParams; import java.io.File; import java.util.HashMap; import java.util.Map; public class MainActivity extends AppCompatActivity { private String TestUrl = "http://192.168.1.8:8080/netTest/index.jsp"; private HelloHttp helloHttp; private TextView txtResult; private String uploadURL = "http://192.168.1.8:8080/netTest/servlet/UploadHandleServlet"; public static String APKPATH = Environment.getExternalStorageDirectory() + "/arapp/智视.apk"; public static String downLoadUrl = "http://192.168.1.8:8080/netTest/head/123.apk"; public static String downPath = "/aaa"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private void initView() { EventClick eventClick = new EventClick(); txtResult = (TextView) findViewById(R.id.txt_result); Button link = (Button) findViewById(R.id.btn_link); link.setOnClickListener(eventClick); Button upload = (Button) findViewById(R.id.btn_upload); upload.setOnClickListener(eventClick); Button download = (Button) findViewById(R.id.btn_download); download.setOnClickListener(eventClick); } private class EventClick implements View.OnClickListener { @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_link: Map<String, String> pamrasMap = new HashMap<>(); pamrasMap.put("username", "liu"); pamrasMap.put("pwd", "123456"); RequestParams params = new RequestParams(); params.setURL(TestUrl); params.setMethod("POST"); params.setParams(pamrasMap); helloHttp = HelloHttp.build() .setRequestParams(params) .setResponse(new MsgHandler().setProxyListener(new StringRequest())) .execute(); break; case R.id.btn_upload: FileRequest fileRequest = new FileRequest(); fileRequest.setURL(uploadURL); fileRequest.setFilePath(APKPATH); fileRequest.setMethod(MsgCode.UPLOADFILE); helloHttp = HelloHttp.build() .setFileRequest(fileRequest) .setResponse(new MsgHandler().setProxyListener(new UploadListener())) .executeFile(); break; case R.id.btn_download: FileRequest downRequest = new FileRequest(); downRequest.setURL(downLoadUrl); downRequest.setFilePath(downPath); downRequest.setMethod(MsgCode.DOWNLOADFILE); helloHttp = HelloHttp.build() .setFileRequest(downRequest) .setResponse(new MsgHandler().setProxyListener(new DownLoadListener())) .executeFile(); break; } } } /** * 请求的回调 */ private class StringRequest extends BaseRequestListener<String> { @Override public void onSuccess(String result) { txtResult.setText(""); txtResult.setText(result); System.out.println("70-------------------:" + result); } @Override public void onFailure(String result) { System.out.println("78-------------------:" + result); } @Override public void onExcetion(String e) { super.onExcetion(e); System.out.println("81-------------------:" + e); } } /** * 上传文件的监听器 */ private class UploadListener extends BaseRequestListener<String> { @Override public void onSuccess(String filePath) { System.out.println("150----------------上传成功本地文件路径为:"+filePath); } @Override public void onFailure(String result) { } @Override public void onExcetion(String e) { super.onExcetion(e); } @Override public void onLoading(long total, long curProgress) { super.onLoading(total, curProgress); txtResult.setText(""); txtResult.setText(curProgress + ""); //System.out.println("152-------------------:" + curProgress); } } /** * 下载的监听器 * */ private class DownLoadListener extends BaseRequestListener<String>{ @Override public void onSuccess(String filePath) { System.out.println("184----------------下载成功本地文件路径为:"+filePath); } @Override public void onFailure(String result) { } @Override public void onExcetion(String e) { super.onExcetion(e); } @Override public void onLoading(long total, long curProgress) { super.onLoading(total, curProgress); txtResult.setText(""); txtResult.setText(curProgress + ""); System.out.println("199-------------------:" + curProgress); } } }