网络编程——解剖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的通信。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CrazyMo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值