Android的网络编程

一、HTTP协议原理

HTTP简介
HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展

HTTP协议的主要特点
支持C/S(客户/服务器)模式

简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST,每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快

灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记

无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间

无状态:HTTP协议是无状态协议,无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快

HTTP URL 的格式如下
Android的网络编程
http表示要通过HTTP协议来定位网络资源
host表示合法的Internet主机域名或者IP地址
port指定一个端口号,为空则使用默认端口80
abs_path指定请求资源的URI

基于Http协议
一般是发送请求到某个应用服务器。此时需要用到HttpURLConnection
先取得HttpURLConnection urlConn = new URL(“http://www.google.com”).openConnection();
设置标志 
urlConn.setDoOutput(true);
urlConn.setDoInput(true);
//post的情况下需要设置DoOutput为true

urlConn.setRequestMethod(“POST”);
urlConn.setUseCache(false);//设置是否用缓存
urlConn.setRequestProperty(“Content-type”,“application/x-www-form-urlencoded”);
//设置content-type获得输出流,便于想服务器发送信息。

DataOutputStream dos = new DataOutputStream(urlConn.getOutputStream());
望流里面写请求参数 dos.writeBytes(“name=”+URLEncoder.encode(“chenmouren”,“gb2312”);
dos.flush();dos.close();
//发送完后马上关闭

获得输入流,取数据
BufferReader reader = new BufferedReader(new InputStreamReader(urlConn.getInputStream()));
reader.readLine();//用 !=null来判断是否结束
reader.close();
读完了记得关闭connection 
urlConn.disconnect();

HTTP有两种报文分别是请求报文和响应报文

HTTP的请求报文
请求报文的一般格式:
Android的网络编程
HTTP请求报文由请求行、请求报头、空行、和请求数据4个部分组成

请求行
请求行由请求方法,URL字段和HTTP协议的版本组成,格式如下:
Android的网络编程
其中 Method表示请求方法
Request-URI是一个统一资源标识符
HTTP-Version表示请求的HTTP协议版本
CRLF表示回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符)

HTTP请求方法有8种,分别是GET、POST、DELETE、PUT、HEAD、TRACE、CONNECT 、OPTIONS。其中PUT、DELETE、POST、GET分别对应着增删改查,对于移动开发最常用的就是POST和GET了。

GET:请求获取Request-URI所标识的资源
POST:在Request-URI所标识的资源后附加新的数据
HEAD:请求获取由Request-URI所标识的资源的响应消息报头
PUT: 请求服务器存储一个资源,并用Request-URI作为其标识
DELETE :请求服务器删除Request-URI所标识的资源
TRACE : 请求服务器回送收到的请求信息,主要用于测试或诊断
CONNECT: HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
OPTIONS :请求查询服务器的性能,或者查询与资源相关的选项和需求

请求报头
在请求行之后会有0个或者多个请求报头,每个请求报头都包含一个名字和一个值,它们之间用“:”分割。请求头部会以一个空行,发送回车符和换行符,通知服务器以下不会有请求头

请求数据
请求数据不在GET方法中使用,而是在POST方法中使用。POST方法适用于需要客户填写表单的场合,与请求数据相关的最常用的请求头是Content-Type和Content-Length

HTTP的响应报文
响应报文的一般格式:
Android的网络编程
HTTP的响应报文由状态行、消息报头、空行、响应正文组成。响应报头后面会讲到,响应正文是服务器返回的资源的内容
状态行
状态行格式如下:
Android的网络编程
其中,HTTP-Version表示服务器HTTP协议的版本
Status-Code表示服务器发回的响应状态代码
Reason-Phrase表示状态代码的文本描述
状态代码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值:

100~199:指示信息,表示请求已接收,继续处理
200~299:请求成功,表示请求已被成功接收、理解、接受
300~399:重定向,要完成请求必须进行更进一步的操作
400~499:客户端错误,请求有语法错误或请求无法实现
500~599:服务器端错误,服务器未能实现合法的请求

常见的状态码如下:
200 OK:客户端请求成功
400 Bad Request:客户端请求有语法错误,不能被服务器所理解
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden:服务器收到请求,但是拒绝提供服务
500 Internal Server Error:服务器发生不可预期的错误
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常

HTTP的消息报头
消息报头分为通用报头、请求报头、响应报头、实体报头等。消息头由键值对组成,每行一对,关键字和值用英文冒号“:”分隔。

通用报头
既可以出现在请求报头,也可以出现在响应报头中

Date:表示消息产生的日期和时间
Connection:允许发送指定连接的选项,例如指定连接是连续的,或者指定“close”选项,通知服务器,在响应完成后,关闭连接
Cache-Control:用于指定缓存指令,缓存指令是单向的(响应中出现的缓存指令在请求中未必会出现),且是独立的(一个消息的缓存指令不会影响另一个消息处理的缓存机制)

请求报头
请求报头通知服务器关于客户端求求的信息,典型的请求头有:

Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机
User-Agent:发送请求的浏览器类型、操作系统等信息
Accept:客户端可识别的内容类型列表,用于指定客户端接收那些类型的信息
Accept-Encoding:客户端可识别的数据编码
Accept-Language:表示浏览器所支持的语言类型
Connection:允许客户端和服务器指定与请求/响应连接有关的选项,例如这是为Keep-Alive则表示保持连接。
Transfer-Encoding:告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式。

响应报头
用于服务器传递自身信息的响应,常见的响应报头:

Location:用于重定向接受者到一个新的位置,常用在更换域名的时候
Server:包含可服务器用来处理请求的系统信息,与User-Agent请求报头是相对应的
实体报头
实体报头用来定于被传送资源的信息,既可以用于请求也可用于响应。请求和响应消息都可以传送一个实体,常见的实体报头为:

Content-Type:发送给接收者的实体正文的媒体类型
Content-Lenght:实体正文的长度
Content-Language:描述资源所用的自然语言,没有设置则该选项则认为实体内容将提供给所有的语言阅读
Content-Encoding:实体报头被用作媒体类型的修饰符,它的值指示了已经被应用到实体正文的附加内容的编码,因而要获得Content-Type报头域中所引用的媒体类型,必须采用相应的解码机制。
Last-Modified:实体报头用于指示资源的最后修改日期和时间
Expires:实体报头给出响应过期的日期和时间

关于http协议get和post的区别:

1.get是从服务器上获取数据,post是向服务器传送数据。
2.get是把参数数据队列加到提交表单的 ACTION属性所指的URL中,值和表单内各个字段一一对应,在URL中可以看到。
post是通过HTTPpost机制,将表单内各个字段与其内容放置在HTML HEADER内一起传送到ACTION属性所指的URL地址。用户看不到这个过程
3.对于get方式,服务器端用 Request.QueryString获取变量的值,对于post方式,服务器端用Request.Form获取提交的数据
4.get 传送的数据量较小,不能大于2KB。post传送的数据量较大,一般被默认为不受限制。但理论上,IIS4中最大量为80KB,IIS5中为100KB
5.get安全性非常低,post安全性较高
android的网络编程分为2种:基于socket的,和基于http协议的

基于socket的用法

服务器端:

先启动一个服务器端的socket
  ServerSocket svr = new ServerSocket(8989);

开始侦听请求        
  Socket s = svr.accept();

取得输入和输出        
 DataInputStream dis =newDataInputStream(s.getInputStream());

DataOutputStream dos = new DataOutputStream(s.getOutputStream());

Socket 的交互通过流来完成,即是说传送的字节流,因此任何文件都可以在上面传送,不过打开的一定要关上

用DataInputStream/DataOutputStream来进行包装是因为我们想要他们对基本数据类型的读写功能readInt(),writeInt(),readUTF(),writeUTF()等等

服务器端编程步骤:
1: 创建服务器端套接字并绑定到一个端口上
2: 套接字设置监听模式等待连接请求
3: 接受连接请求后进行通信
4: 返回,等待赢一个连接请求

客户端:
 发起一个socket连接    
 Socket s = new Socket(“192.168.1.200”,8989);

取得输入和输出  
 DataInputStream dis = new DataInputStream(s.getInputStream());

DataOutputStream dos = new DataOutputStream(s.getOutputStream());

客户端编程步骤

1: 创建客户端套接字(指定服务器端IP地址与端口号)
2: 连接(Android 创建Socket时会自动连接)
3: 与服务器端进行通信
4: 关闭套接字

Android Socket 通信原理注意:
1: 中间的管道连接是通过InputStream/OutputStream流实现的。
2: 一旦管道建立起来可进行通信
3: 关闭管道的同时意味着关闭Socket
4: 当对同一个Socket创建重复管道时会异常
5: 通信过程中顺序很重要:服务器端首先得到输入流,然后将输入流信息输出到其各个客户端 。

客户端先建立连接后先写入输出流,然后再获得输入流。不然活有EOFException的异常,之后就可以相互通信了,打开的记得要关上

二、HttpClient与HttpURLConnection
在相应的module下的build.gradle中加入:
android {
useLibrary ‘org.apache.http.legacy’
}

HttpClient的GET请求
//创建HttpClient
private HttpClient createHttpClient() {
HttpParams mDefaultHttpParams = new BasicHttpParams();
//设置连接超时
HttpConnectionParams.setConnectionTimeout(mDefaultHttpParams, 15000);
//设置请求超时
HttpConnectionParams.setSoTimeout(mDefaultHttpParams, 15000);
HttpConnectionParams.setTcpNoDelay(mDefaultHttpParams, true);
HttpProtocolParams.setVersion(mDefaultHttpParams, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(mDefaultHttpParams, HTTP.UTF_8);
//持续握手
HttpProtocolParams.setUseExpectContinue(mDefaultHttpParams, true);
HttpClient mHttpClient = new DefaultHttpClient(mDefaultHttpParams);
return mHttpClient;
}

HttpClient的POST请求

private void useHttpClientPost(String url) {
HttpPost mHttpPost = new HttpPost(url);
mHttpPost.addHeader(“Connection”, “Keep-Alive”);
try {
HttpClient mHttpClient = createHttpClient();
List postParams = new ArrayList<>();
//要传递的参数
postParams.add(new BasicNameValuePair(“username”, “moon”));
postParams.add(new BasicNameValuePair(“password”, “123”));
mHttpPost.setEntity(new UrlEncodedFormEntity(postParams));
HttpResponse mHttpResponse = mHttpClient.execute(mHttpPost);
HttpEntity mHttpEntity = mHttpResponse.getEntity();
int code = mHttpResponse.getStatusLine().getStatusCode();
if (null != mHttpEntity) {
InputStream mInputStream = mHttpEntity.getContent();
String respose = converStreamToString(mInputStream);
Log.i(“wangshu”, “请求状态码:” + code + “\n请求结果:\n” + respose);
mInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

HttpURLConnection的POST请求
因为HttpURLConnection的POST请求和GET请求是一样的,所以我这里只举出POST的例子
首先我们创建一个UrlConnManager类,然后里面提供getHttpURLConnection()方法用于配置默认的参数并返回HttpURLConnection:

public static HttpURLConnection getHttpURLConnection(String url){
HttpURLConnection mHttpURLConnection=null;
try {
URL mUrl=new URL(url);
mHttpURLConnection=(HttpURLConnection)mUrl.openConnection();
//设置链接超时时间
mHttpURLConnection.setConnectTimeout(15000);
//设置读取超时时间
mHttpURLConnection.setReadTimeout(15000);
//设置请求参数
mHttpURLConnection.setRequestMethod(“POST”);
//添加Header
mHttpURLConnection.setRequestProperty(“Connection”,“Keep-Alive”);
//接收输入流
mHttpURLConnection.setDoInput(true);
//传递参数时需要开启
mHttpURLConnection.setDoOutput(true);
} catch (IOException e) {
e.printStackTrace();
}
return mHttpURLConnection ;
}
因为我们要发送POST请求,所以在UrlConnManager类中再写一个postParams()方法用来组织一下请求参数并将请求参数写入到输出流中:

public static void postParams(OutputStream output,ListparamsList) throws IOException{
StringBuilder mStringBuilder=new StringBuilder();
for (NameValuePair pair:paramsList){
if(!TextUtils.isEmpty(mStringBuilder)){
mStringBuilder.append("&");
}
mStringBuilder.append(URLEncoder.encode(pair.getName(),“UTF-8”));
mStringBuilder.append("=");
mStringBuilder.append(URLEncoder.encode(pair.getValue(),“UTF-8”));
}
BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(output,“UTF-8”));
writer.write(mStringBuilder.toString());
writer.flush();
writer.close();
}
接下来添加请求参数,调用postParams()方法将请求的参数组织好传给HttpURLConnection的输出流,请求连接并处理返回的结果:

private void useHttpUrlConnectionPost(String url) {
InputStream mInputStream = null;
HttpURLConnection mHttpURLConnection = UrlConnManager.getHttpURLConnection(url);
try {
List postParams = new ArrayList<>();
//要传递的参数
postParams.add(new BasicNameValuePair(“username”, “moon”));
postParams.add(new BasicNameValuePair(“password”, “123”));
UrlConnManager.postParams(mHttpURLConnection.getOutputStream(), postParams);
mHttpURLConnection.connect();
mInputStream = mHttpURLConnection.getInputStream();
int code = mHttpURLConnection.getResponseCode();
String respose = converStreamToString(mInputStream);
Log.i(“wangshu”, “请求状态码:” + code + “\n请求结果:\n” + respose);
mInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
最后开启线程请求网络:

private void useHttpUrlConnectionGetThread() {
new Thread(new Runnable() {
@Override
public void run() {
useHttpUrlConnectionPost(“http://www.baidu.com”);
}
}).start();
}

OkHttp3网络通讯的编码步骤
1.加载OkHttp和FastJson的依赖包
在app的build.gradle中增加依赖
implementation ‘com.squareup.okhttp3:okhttp:4.2.1’ implementation ‘com.alibaba:fastjson:1.1.71.android’

  1. 创建请求数据的对象
    根据url网址的请求返回的响应报文的json字符串创建对象,如访问淘宝ip的网址
    { “code”: 0,
    “data”: {
    “ip”: “221.226.155.10”,
    “country”: “中国”,
    “area”: “”,
    “region”: “江苏”,
    “city”: “南京”,
    “county”: “XX”,
    “isp”: “电信”,
    “country_id”: “CN”,
    “area_id”: “”,
    “region_id”: “320000”,
    “city_id”: “320100”,
    “county_id”: “xx”,
    “isp_id”: “100017” } }

根据JSON字符串的关键字,创建对应的实体对象
public class Ip implements Serializable {
private int code; private IpData data;
// 省略getter/setter方法 }

JSON字符串的命名不符合Java的命名规范,可以使用@JSONField注解进行标注,指定name属性,确 保name的值与JSON的关键字一致。注意点:每个实体类务必有一个无参的构造方法
public class IpData implements Serializable {
private String ip;
private String country;
private String city;
private String area;
private String region;
private String county;
private String isp;
@JSONField(name = “country_id”)
private String countryId;
@JSONField(name = “area_id”)
private String areaId;
@JSONField(name = “region_id”)
private String regionId;
@JSONField(name = “city_id”)
private String cityId;
@JSONField(name = “county_id”)
private String countyId;
@JSONField(name = “isp_id”)
private String ispId;
// 省略getter/setter方法
}
3. 编写请求响应的代码
在 OkHttpActivity 类中加入各个按钮的事件监听,分别处理get方式、post方式、上传文件和下载文 件的功能。OkHttp的网络请求都是在子线程中执行的,需要使用 Handler 、 AsyncTask 或 runOnUiThread 等方式更新Activity上的控件的数据。 3.1 get方式请求数据
get方式请求是将请求字符串加在请求url的后面,如:“http://ip.taobao.com/service/getIpInfo.php?i p=221.226.155.14”,问号后面的键值对就是请求字符串,多个键值对之间用"&"分隔。

private void get(String url) {
// 1. 构造Request Request request = new Request.Builder() .url(url)
.header(“user-agent”, "Mozilla/5.0 (Windows NT 6.3; WOW64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " + “Chrome/51.0.2704.7 Safari/537.36”) .addHeader(“Accept”, “application/json”)
.build();

// 2. 发送请求,并处理回调
OkHttpClient client = HttpsUtil.handleSSLHandshakeByOkHttp(); client.newCall(request).enqueue(new Callback() { @Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.e(“OkHttpActivity”, e.getMessage()); }
@Override
public void onResponse(@NotNull Call call, @NotNull Response response)
throws IOException {
if(response.isSuccessful()) {
// 1. 获取响应主体的json字符串
String json = response.body().string();
// 2. 使用FastJson库解析json字符串
final Ip ip = JSON.parseObject(json, Ip.class);
// 3. 回到UI线程显示获取的数据
runOnUiThread(new Runnable() {
@Override
public void run() {
// 3.1 根据返回的code判断获取是否成功 if(ip.getCode() != 0) {
tvResult.setText(“未获得数据”);
} else {
// 3.2 解析数据
IpData data = ip.getData(); tvResult.setText(data.getIp()+", "+data.getCity());
}
}
});
}
}
});
}

3.2 post方式请求数据
post方式请求是将请求字符串放在请求主体中,在请求url后面是不可见的。 get方式与post方式的区别在于:get方式只能有小于4k的请求参数,而post方式不仅可以隐藏请求参数 的信息,还能发送大数据量的信息。上传文件、图片等只能采用post方式

在发送请求之前,需要将请求参数组装为RequestBody的格式
private RequestBody setRequestBody(Map<String, String> params) {
FormBody.Builder builder = new FormBody.Builder();
for (String key : params.keySet()) {
builder.add(key, params.get(key)); }
return builder.build(); }

OkHttp的post方式的请求与get方式的请求类似,只需要修改Request对象的请求方式,响应报文的处 理与get方式完全相同。

private void post(String url, Map<String, String> params) {
// 1. 构建RequestBody
RequestBody body = setRequestBody(params);
// 2. 创建Request对象
Request request = new Request.Builder()
.url(url)
.post(body)
.header(“user-agent”, "Mozilla/5.0 (Windows NT 6.3; WOW64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " + “Chrome/51.0.2704.7 Safari/537.36”) .addHeader(“Accept”, “application/json”)
.build();
// 3. 创建OkHttpClient对象,发送请求,并处理回调 // 以下处理与get()方法相同,直接拷贝即可
}
3.3 上传文件
OkHttp上传文件,需要将文件转化为相应MediaType格式的 RequestBody 对象,通过 post() 方法发 送请求,并根据响应报文的内容进行处理
// 指定MIME类型 public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse(“text/xmarkdown;charset=utf-8”); private void uploadFile(String url, final String fileName) {
Request request = new Request.Builder()
.url(url)
.post(RequestBody.create(new File(fileName), MEDIA_TYPE_MARKDOWN))
.build();
OkHttpClient client = HttpsUtil.handleSSLHandshakeByOkHttp(); client.newCall(request).enqueue(new Callback() { @Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.e(“OkHttpActivity”, e.getMessage()); tvResult.post(new Runnable() {
@Override
public void run() {
tvResult.setText(fileName + “上传失败”);
}
});
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response)
throws IOException {
if (response.isSuccessful()) {
final String str = response.body().string(); runOnUiThread(new Runnable() {
@Override
public void run() {
tvResult.setText(“上传成功,” + str); } });
} else {
Log.d(“OkHttpActivity”, response.body().string()); } } }); }

3.4 上传图片
上传图片的媒体类型有: image/jpeg 和 image/png ,根据图片类型进行选择。图片作为表单的元素上 传时,表单的其它元素也需要上传,因此,在使用OkHttp上传包含图片的表单元素时,除了使用 create() 方法上传图片文件,同时使用 addFormDataPart() 方法添加表单元素和图片,共同组成请 求主体RequestBody

创建OkHttpClient对象和上传后返回的Response结果的处理与上传文件相同

3.5 下载文件
下载文件的功能,主要的难点是对请求返回的响应数据的处理。OkHttp通过 byteStream() 方法获取 响应数据的字节流对象,可以通过Java的文件流将数据存储到手机上

public static void writeFile(InputStream is, String path, String fileName) throws IOException {
// 1. 根据path创建目录对象,并检查path是否存在,不存在则创建
File directory = new File(path);
if (!directory.exists()) { directory.mkdirs();
}
// 2. 根据path和fileName创建文件对象,如果文件存在则删除
File file = new File(path, fileName);
if (file.exists()) { file.delete(); }
// 3. 创建文件输出流对象,根据输入流创建缓冲输入流对象
FileOutputStream fos = new FileOutputStream(file); BufferedInputStream bis = new BufferedInputStream(is);
// 4. 以每次1024个字节写入输出流对象
byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) != -1) {
fos.write(buffer, 0, len); }
fos.flush();
// 5. 关闭输入流、输出流对象
fos.close();
bis.close(); }