文章大纲
引言
前几篇文章
- 网络编程——网络分层模型及一些你应该知道的TCP等网络基础常识
- 网络编程——TCP连接的三次握手和“四次”挥手小结
- 网络编程——关于最常用的网络通信HTTP协议你应该知道的一些常识
- 网络编程——使用原生Socket 完成HTTP简单通信
总结了网络分层模型、TCP协议、HTTP协议等一些最基本网络知识,如果你对于HTTP协议还没有了解的话,跟着文章敲一遍,细细体味每个流程,或许对于所谓的协议有自己的理解,接下来就直接进入代码的世界吧。
一、抽象构造自定义的HttpURL对象
此处我们使用的是HTTP协议进行通信,所以得根据HTTP协议中约定的URL的基本语法
形如: schemal " : //" “internet adress” [:port] “/resource_file_name” 形式来构造HttpURL,这也是HTTP请求头的数据来源。
package com.crazymo.cruzrv281;
import java.net.MalformedURLException;
import java.net.URL;
/**
* @author : Crazy.Mo
*/
public class HttpUrl {
String protocol;
String host;
String path;
int port;
/**
* scheme://host:port/path?query#fragment
* @param path 传入完整的请求 比如http://restapi.amap.com/v3/weather/weatherInfo?city=上海&key=ccbf1d251595efa936df0ba784346902
* @throws MalformedURLException
*/
public HttpUrl(String path) throws MalformedURLException{
URL url=new URL(path);
host=url.getHost();
this.path =url.getFile();
this.path =(this.path ==null || this.path.length() ==0)? "/" : this.path;
protocol=url.getProtocol();
port=url.getPort();
port=port== -1 ? url.getDefaultPort() :port;
}
public String getProtocol() {
return protocol;
}
public String getHost() {
return host;
}
public String getPath() {
return path;
}
public int getPort() {
return port;
}
}
二、Http报文解析
在Java 中网络通信的本质都是I/O操作,所以所谓的报文解析就是根据Http协议定义的报文格式去解析,因为Http报文本身就是高度结构化的字符串,严格定义了报文中各部分内容的规则。
package com.crazymo.cruzrv281;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
/**
* Http报文解析类
* @author : Crazy.Mo
*/
public class HttpCodec {
ByteBuffer byteBuffer;
public HttpCodec(){
//申请足够大的内存记录读取的数据 (一行)
byteBuffer = ByteBuffer.allocate(10 * 1024);
}
/**
*
* @param inputStream
* @return
* @throws IOException
*/
public String readLine(InputStream inputStream) throws IOException {
try {
byte b;
boolean isMabeyEofLine = false;
//标记
byteBuffer.clear();
byteBuffer.mark();
while ((b = (byte) inputStream.read()) != -1) {
byteBuffer.put(b);
// 读取到/r则记录,判断下一个字节是否为/n
if (b == HttpConst.CR) {
isMabeyEofLine = true;
} else if (isMabeyEofLine) {
//上一个字节是/r 并且本次读取到/n
if (b == HttpConst.LF) {
//获得目前读取的所有字节
byte[] lineBytes = new byte[byteBuffer.position()];
//返回标记位置
byteBuffer.reset();
byteBuffer.get(lineBytes);
//清空所有index并重新标记
byteBuffer.clear();
byteBuffer.mark();
return new String(lineBytes);
}
isMabeyEofLine = false;
}
}
} catch (Exception e) {
throw new IOException(e);
}
throw new IOException("Response Read Line.");
}
/**
* 用于解析头部
* @param inputStream
* @return
* @throws IOException
*/
public Map<String,String> readHeaders(InputStream inputStream) throws IOException {
HashMap<String, String> headers = new HashMap<>();
while (true) {
String line = readLine(inputStream);
//读取到空行 则下面的为body
if (isEmptyLine(line)) {
break;
}
int index = line.indexOf(":");
if (index > 0) {
String name = line.substring(0, index);
// ": "移动两位到 总长度减去两个("\r\n")
String value = line.substring(index + 2, line.length() - 2);
headers.put(name, value);
}
}
return headers;
}
public byte[] readBytes(InputStream inputStream, int length) throws IOException {
byte[] bytes=new byte[length];
int readNum=0;
while (true){
readNum +=inputStream.read(bytes,readNum,length-readNum);
//读取完毕
if(readNum==length){
return bytes;
}
}
}
private boolean isEmptyLine(String line) {
return line==null || line.equals("\r\n");
}
/**
* 解析分块编码形式的响应体
* @param inputStream
* @return
* @throws IOException
*/
public String readChunked(InputStream inputStream) throws IOException {
int len=-1;
boolean isEmptyData=false;
StringBuffer buffer=new StringBuffer();
while (true){
if(len<0){
String line= readLine(inputStream);
//减掉\r\n
line=line.substring(0,line.length()-2);
//Chunked 编码最后一段数据为0 \r\n\r\n
len=Integer.valueOf(line,16);
isEmptyData=len==0;
}else {
//块的长度不包括\r\n 所以加2 将\r\n读走
byte[] bytes=readBytes(inputStream,len+2);
buffer.append(new String(bytes));
len=-1;
if(isEmptyData){
return buffer.toString();
}
}
}
}
}
定义一个工具常量类(建议以后对于一些用于存储常量的类,都使用枚举类或者接口来替代,因为接口已经是抽象的,并且是一种单例,因此它们的字段默认情况下都是 static final类型的)
package com.crazymo.cruzrv281;
/**
* @author : Crazy.Mo
*/
public interface HttpConst {
//回车和换行
String CRLF = "\r\n";
//"/r"
int CR = 13;
int LF = 10;
String GET="GET ";
String POST="POST ";
String HOST="Host: ";
}
三、使用Socket 进行Http通信
使用Socket 进行Http通信一般来说有以下几个步骤:
1、创建HttpURL并拼接请求报文
所谓创建请求报文就是严格根据前面文章的报文格式,拼接成对应的字符串。
final HttpUrl url=new HttpUrl("http://restapi.amap.com/v3/weather/weatherInfo?city=上海&key=ccbf1d251595efa936df0ba784346902");
/**
* 创建Http请求报文
* @param url
* @return
*/
private StringBuffer createRequestPacket(HttpUrl url) {
StringBuffer buffer=new StringBuffer();
//构造请求行
buffer.append(HttpConst.GET);
buffer.append(url.getPath());
buffer.append(" ");
buffer.append("HTTP/1.1");
buffer.append(HttpConst.CRLF);
//请求头
buffer.append(HttpConst.HOST);
buffer.append(url.getHost());
buffer.append(HttpConst.CRLF);
//请求体,这个请求可以为空所以。。。
buffer.append(HttpConst.CRLF);
return buffer;
}
2、创建Socket并通过connect方法进行连接
创建Socket很简单直接使用JDK给我们提供的Socket的构造方法即可。
Socket socket=new Socket();
//通过端口号与指定Host的主机建立了连接
socket.connect(new InetSocketAddress(url.getHost(),url.getPort()),5000);
3、获取Socket 对应的输入和输出流
建立了Socket 连接之后就可以通过对应的方法获取输入输出流,其中输入流是用于读取服务器的响应数据;而输出流则是客服端发送数据给服务器的
final OutputStream outputStream=socket.getOutputStream();
final InputStream inputStream=socket.getInputStream();
4、发送Http请求
发送Http请求就是向输出流写入字节数据。
/**
* 发送Http请求
* @param buffer
* @param outputStream 输出流则是客服端发送数据给服务器的
* @throws IOException
*/
private void sendRequest(StringBuffer buffer, OutputStream outputStream) throws IOException {
outputStream.write(buffer.toString().getBytes());
outputStream.flush();
}
5、解析Http响应
new Thread(new Runnable() {
@Override
public void run() {
HttpCodec httpCodec=new HttpCodec();
try{
//解析响应行
String responseLine=httpCodec.readLine(inputStream);
System.out.println("响应行:"+responseLine);
System.out.println("响应头:");
//解析响应头
Map<String,String> headers=httpCodec.readHeaders(inputStream);
for (Map.Entry<String,String> entry :headers.entrySet()){
System.out.println(entry.getKey() +":"+entry.getValue());
}
//解析Content-Length响应体
if(headers.containsKey("Content-Length")){
int length=Integer.valueOf(headers.get("Content-Length"));
byte[] bytes=httpCodec.readBytes(inputStream,length);
System.out.println("\n响应体:"+new String(bytes));
}else{
//分块编码
String response=httpCodec.readChunked(inputStream);
System.out.println("分块响应体:"+response);
}
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
在解析Http响应体的时候,有可能不同的api采用的方式不同,有的可能还会采用分块编码的形式,如下图:
最终完整测试代码
void httpTest() throws MalformedURLException {
//高德地图获取天气api 响应体使用Content-Length
final HttpUrl url=new HttpUrl("http://restapi.amap.com/v3/weather/weatherInfo?city=上海&key=ccbf1d2515xxxxxx");
//快递100查询 api 响应体使用分块编码的api
//final HttpUrl url=new HttpUrl("http://www.kuaidi100.com/query?type=shunfeng&postid=8989");
System.out.println("host:"+url.host);
System.out.println("protocol:"+url.getProtocol());
System.out.println("port:"+url.getPort());
System.out.println("path:"+url.getPath());
StringBuffer buffer=createRequestPacket(url);
Socket socket=new Socket();
try {
//通过端口号与指定Host的主机建立了连接
socket.connect(new InetSocketAddress(url.getHost(),url.getPort()),5000);
//建立了Sockect 连接之后就可以通过对应的方法获取输入输出流,其中输入流是用于读取服务器的响应数据;而输出流则是客服端发送数据给服务器的
final OutputStream outputStream=socket.getOutputStream();
final InputStream inputStream=socket.getInputStream();
System.out.println("开始发送报文... \n"+buffer);
sendRequest(buffer, outputStream);
new Thread(new Runnable() {
@Override
public void run() {
HttpCodec httpCodec=new HttpCodec();
try{
//解析响应行
String responseLine=httpCodec.readLine(inputStream);
System.out.println("响应行:"+responseLine);
System.out.println("响应头:");
//解析响应头
Map<String,String> headers=httpCodec.readHeaders(inputStream);
for (Map.Entry<String,String> entry :headers.entrySet()){
System.out.println(entry.getKey() +":"+entry.getValue());
}
//解析Content-Length响应体
if(headers.containsKey("Content-Length")){
int length=Integer.valueOf(headers.get("Content-Length"));
byte[] bytes=httpCodec.readBytes(inputStream,length);
System.out.println("\n响应体:"+new String(bytes));
}else{
//分块编码
String response=httpCodec.readChunked(inputStream);
System.out.println("分块响应体:"+response);
}
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
while (true){
Thread.sleep(1000*10);
}
} catch (IOException e) {
e.printStackTrace();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
高德查询天气api的运行结果:
快递100查询api的结果:
ps:这里为了方便我直接在单元测试中进行测试,也不考虑健壮性等一些细节的设计,仅仅是为了演示原始Socket实现HTTP通信,项目中需要重新设计逻辑。
所谓的通信协议其实也就那样吧,定义规范,通信双方必须遵守,下一篇继续HTTPS的通信。