前面介绍了GET方式的HTTP调用,该方式主要用于向服务器索取数据,不管是字符串形式的应答报文,还是二进制形式的网络文件,都属于服务器提供的信息。当然调用方也可以向服务地址传送请求参数,除了通过连接对象设置的HTTP参数,还能给url地址添加形如“?参数A名称=A参数值&参数B名称=B参数值”这样的业务参数,服务地址根据url后面的业务参数,再返回符合条件的应答数据。倘若服务器不仅仅作为信息提供方,还想成为信息接收方,例如保存调用方提交的表单数据,或者保存调用方待上传的文件,那便要求调用方的程序能够传送复杂的数据信息。通过GET方式固然也能在url后方填写简单的请求参数,但是这并非信息传送的可靠手段,原因有三:
1、往URL末尾添加的请求参数,全为明文传输,不利于数据的保密措施;
2、URL格式的请求串只支持键值对形式的参数,难以表达复杂的结构化数据,譬如数组形式的参数;
3、URL本身是个字符串,Query部分的请求参数也只能是字符串,这叫二进制形式的文件上传如何是好?
鉴于种种不可避免的困难,GET方式实在不适合向服务器提交数据,必须采用POST方式提交数据才行。POST方式同样需要服务器给个调用地址,但该方式的业务参数没放到URL末尾,而是放在了请求报文当中。所谓的请求报文与应答报文相对应,应答报文要从连接对象的输入流中获取,而请求报文要写入连接对象的输出流。编码实现POST请求的时候,除了调用setRequestMethod要将请求方式设置为POST,还需留意连接对象的下列几种方法:
setRequestProperty:设置请求属性。该方法可设置特定名称的属性值。
setDoOutput:准备让连接执行输出操作。默认为false(GET方式),POST方式需要设置为true。
setDoInput:准备让连接执行输入操作。默认为true,通常无需特意调用该方法。
getOutputStream:从连接对象中获取输出流,后续会把请求报文写入输出流。
getHeaderField:获取应答报文头部指定名称的字段值。该方法可得到特定名称的参数值,例如getHeaderField("Content-Length")返回的是应答报文的长度,getHeaderField("Content-Type")返回的是应答报文的内容类型,conn.getHeaderField("Content-Encoding")返回的是应答报文的压缩方式。
上述几种方法中尤为值得注意的是setRequestProperty,依据不同的请求属性名称,该方法将会设置各式各样的属性值,以此提醒服务器做好相应的准备工作。其中常见的属性名称及其属性值罗列如下:
Content-Type:请求报文的内容类型。如果请求报文采取形如“参数A名称=A参数值&参数B名称=B参数值”的url参数格式,则内容类型应设置为“application/x-www-form-urlencoded”;如果请求报文是json格式,则内容类型应设置为“application/json”;如果请求报文是xml格式,则内容类型应设置为“application/xml”;如果请求报文是分段传输的文件数据,则内容类型应设置为“multipart/form-data;boundary=***”。
Connection:指定连接的保持方式。如果是文件上传,则必须设置为“Keep-Alive”,表示建议服务器保留连接,以便能够持续发送文件的分段数据。
User-Agent:指定调用方的浏览器类型。
Accept:指定可接受的应答报文类型。如果不设置则默认“*/*”,表示允许返回任何类型的应答报文;如果设置为“image/png”,则表示只接受返回png图片。
Accept-Language:指定可接受的应答报文语言。通常无需设置,如果只接受中文则可设置为“zh-cn”。
Accept-Encoding:指定可接受的应答报文压缩方式。如果不设置则默认identity,表示不允许应答报文使用压缩;如果设置为gzip,则表示允许应答报文采用GZIP压缩,此时服务器可能返回gzip压缩的应答数据,也可能返回未压缩的应答数据。
接下来举个请求报文是json串的HTTP接口例子,采用POST方式的调用方法代码如下所示:

// 对指定url发起POST调用
	private static void testCallPost(String callUrl, String body) {
		try {
			URL url = new URL(callUrl); // 根据网址字符串构建URL对象
			// 打开URL对象的网络连接,并返回HttpURLConnection连接对象
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setRequestMethod("POST"); // 设置请求方式为POST调用
			conn.setRequestProperty("Content-Type", "application/json"); // 请求报文为json格式
			conn.setDoOutput(true); // 准备让连接执行输出操作。默认为false,POST方式需要设置为true
			conn.connect(); // 开始连接
			OutputStream os = conn.getOutputStream(); // 从连接对象中获取输出流
			os.write(body.getBytes()); // 往输出流写入请求报文
			// 打印HTTP调用的应答内容长度、内容类型、压缩方式
			System.out.println( String.format("应答内容长度=%s, 内容类型=%s, 压缩方式=%s", 
					conn.getHeaderField("Content-Length"), conn.getHeaderField("Content-Type"),
					conn.getHeaderField("Content-Encoding")) );
			// 对输入流中的数据进行解压和字符编码,得到原始的应答字符串
			String content = StreamUtil.getUnzipString(conn);
			// 打印HTTP调用的应答状态码和应答报文
			System.out.println( String.format("应答状态码=%d, 应答报文=%s", 
					conn.getResponseCode(), content) );
			conn.disconnect(); // 断开连接
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

 

然后由外部在调用testCallPost时输入服务地址和请求报文,具体代码示例如下:

testCallPost("http://localhost:8080/NetServer/checkUpdate", "{\"package_list\":[{\"package_name\":\"com.qiyi.video\"}]}");

 

运行上述的POST代码,从以下的接口日志可知POST方式正确发送了请求报文,且正常收到了应答报文。

请求报文={"package_list":[{"package_name":"com.qiyi.video"}]}
应答内容长度=152, 内容类型=text/plain;charset=utf-8, 压缩方式=null
应答状态码=200, 应答报文={"package_list":[{"package_name":"com.qiyi.video","download_url":"https://3g.lenovomm.com/w3g/yydownload/com.qiyi.video/60020","new_version":"10.2.0"}]}

通过HTTP接口上传文件也要采用POST方式,只是文件上传还需遵守一定的数据规则,除了内容类型设置为“multipart/form-data;boundary=***”(***处要替换成边界字符串),请求报文也得依顺序填入报文头、报文体和报文尾,详细的上传过程代码如下所示:

// 把本地文件上传给指定url
	private static void testUpload(String filePath, String uploadUrl) {
		// 从本地文件路径获取文件名
		String fileName = filePath.substring(filePath.lastIndexOf("/"));
		String end = "\r\n"; // 结束字符串
		String hyphens = "--"; // 连接字符串
		String boundary = "WUm4580jbtwfJhNp7zi1djFEO3wNNm"; // 边界字符串
		try (FileInputStream fis = new FileInputStream(filePath)) {
			URL url = new URL(uploadUrl); // 根据网址字符串构建URL对象
			// 打开URL对象的网络连接,并返回HttpURLConnection连接对象
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setDoOutput(true); // 准备让连接执行输出操作。默认为false,POST方式都要设置为true
			conn.setRequestMethod("POST"); // 设置请求方式为POST调用
			// 连接过程要保持活跃
			conn.setRequestProperty("Connection", "Keep-Alive");
			// 请求报文要求分段传输,并且各段之间以边界字符串隔开
			conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
			// 根据连接对象的输出流构建数据输出流
			DataOutputStream ds = new DataOutputStream(conn.getOutputStream());
			// 以下写入请求报文的头部
			ds.writeBytes(hyphens + boundary + end);
			ds.writeBytes("Content-Disposition: form-data; "
					+ "name=\"file\";filename=\"" + fileName + "\"" + end);
			ds.writeBytes(end);
			// 以下写入请求报文的主体
			byte[] buffer = new byte[1024];
			int length;
			// 先将文件数据写入到缓冲区,再将缓冲数据写入输出流
			while ((length = fis.read(buffer)) != -1) {
				ds.write(buffer, 0, length);
			}
			ds.writeBytes(end);
			// 以下写入请求报文的尾部
			ds.writeBytes(hyphens + boundary + hyphens + end);
			ds.close(); // 关闭数据输出流
			// 对输入流中的数据进行解压和字符编码,得到原始的应答字符串
			String content = StreamUtil.getUnzipString(conn);
			// 打印HTTP上传的应答状态码和应答报文
			System.out.println( String.format("应答状态码=%d, 应答报文=%s", 
					conn.getResponseCode(), content) );
			conn.disconnect(); // 断开连接
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

 

然后由外部在调用testUpload方法时输入上传地址和待上传的文件路径,具体代码示例如下:

testUpload("E:/bliss.jpg", "http://localhost/NetServer/uploadServlet");

 

运行上述的上传代码,从以下的上传日志可知文件已经成功上传至服务器。

应答状态码=200, 应答报文=文件上传成功,文件大小为1912K

  

更多Java技术文章参见《Java开发笔记(序)章节目录》