使用Socket发送HTTP请求

在java开发中有不少发送Http请求的需求,比较常见的工具类有HttpURLConnection,HttpClient和Spring中的RestTemplate,这几个类封装程度一个比一个高,越来越方便开发,更符合面向对象的编程思想.但是久而久之,也渐渐地忽略了它们背后的HTTP协议和Socket开发.

HTTP

HTTP全称是超文本传输协议(Hyper Text Transfer Protocol),是一个基于 TCP/IP协议,使用请求-响应模型的应用层协议,本质是一种数据格式的约定,它严格规定了方法,头部,数据体等等的位置,具体报文格式如下
请求报文
响应报文

  • 从请求行的格式可以看出,请求行用空格作为分隔符,这里就解释了为什么URL需要将空格编码成%20,不然就会解析异常
  • 从请求头的格式可以看出,请求头是用\r\n作为分隔符,所以请求头里面用空格是不需要编码的,请求头的结束标志为\r\n\r\n
  • 请求报文的结束位置,是需要看请求头Content-Length,单位是字节,如果大于0,则读取对应长度的字节作为请求体
  • 请求报文没有指定ip和端口,因为它是应用层协议,并不是运输层协议,请求头Host虽然不可缺少,但是乱填也是可以的

Socket

前面说到HTTP协议是基于TCP/IP协议的,实际开发中我们并不需要直接执行TCP/IP协议,而是使用Socket,Socket封装了TCP协议中三次握手,四次挥手的复杂过程,对外抽象为connect方法,close方法和stream操作.
先定义请求类SocketHttpRequest和响应类SocketHttpResponse,如下:

@Data
public class SocketHttpRequest {
    private String method;
    private String host;
    private Integer port;
    private String url;
    private String body;
    private MultiValueMap<String, String> headList =new LinkedMultiValueMap<>();
}
@Data
public class SocketHttpResponse {
    private String status;
    private MultiValueMap<String, String> headList;
    private String body;
}

TCP连接是长链接,一次连接的建立,可以多次发送HTTP请求,所以并不能以关闭连接为报文的结束点,而是以HTTP报文格式规定作为开始和结束,其中关键是对\r\n的插入和解析,在Socket中,数据都是要转成Byte传输的,\r\n对应的字节分别是13和10,所以发送HTTP报文的代码如下:

public class SocketHttpClient {
    Socket socket;
    public SocketHttpClient(String host, int port) throws IOException {
        socket = new Socket();
        socket.connect(new InetSocketAddress(host, port), 2000);
    }
    public SocketHttpResponse sendRequest(SocketHttpRequest socketHttpRequest) throws IOException {
        //写请求
        BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        StringBuffer sb = new StringBuffer();
        //添加地址行
        sb.append(socketHttpRequest.getMethod()).append(" ").append(socketHttpRequest.getUrl()).append(" ").append("HTTP/1.1").append("\r\n");

        //添加请求头
        MultiValueMap<String, String> headMapList = new LinkedMultiValueMap<>();
        if (socketHttpRequest.getHeadList() != null) {
            headMapList.addAll(socketHttpRequest.getHeadList());
        }
        headMapList.add(HttpHeaders.HOST, socketHttpRequest.getHost());
        if (Strings.isNotBlank(socketHttpRequest.getBody())) {
            headMapList.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(socketHttpRequest.getBody().getBytes().length));
        }
        headMapList.forEach((x, y) -> {
            y.forEach(m -> {
                sb.append(x + ":" + m + "\r\n");
            });
        });
        sb.append("\r\n"); //代表请求头已经结束

        //添加body
        if (Strings.isNotBlank(socketHttpRequest.getBody())) {
            sb.append(socketHttpRequest.getBody());
        }
        out.write(sb.toString());
        out.flush();

        //读取响应
        InputStream is = socket.getInputStream();
        List<Byte> statusByteList = new ArrayList<>();
        List<Byte> headByteList = new ArrayList<>();
        List<Byte> bodyByteList = new ArrayList<>();
        Integer contentLength = 0;
        MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
        int type = 1;
        while (true) {
            byte b = (byte) is.read();
            if (type == 1) {
                statusByteList.add(b);
                int size = statusByteList.size();
                //是否已经读取完状态行
                if (b == 10 && size >= 2 && statusByteList.get(size - 2) == 13) {
                    type = 2;
                }
            } else if (type == 2) {
                headByteList.add(b);
                int size = headByteList.size();
                //是否已经将请求头读完
                if (b == 10 && size >= 4 && headByteList.get(size - 4) == 13 && headByteList.get(size - 3) == 10 && headByteList.get(size - 2) == 13) {
                    String head = new String(Bytes.toArray(headByteList));
                    String[] headArray = head.split("\r\n");
                    for (String item : headArray) {
                        String[] itemArray = item.split(":");
                        multiValueMap.add(itemArray[0].trim(), itemArray[1].trim());
                    }
                    type = 3;
                    Map.Entry<String, List<String>> contentLengthHeadEntry = multiValueMap.entrySet().stream().filter(x -> x.getKey().startsWith(HttpHeaders.CONTENT_LENGTH)).findFirst().orElse(null);
                    if (contentLengthHeadEntry != null) {
                        contentLength = Integer.parseInt(contentLengthHeadEntry.getValue().get(0));
                        if (contentLength == 0) {
                            break;
                        }
                    }
                }
            } else if (type == 3) {
                bodyByteList.add(b);
                if (bodyByteList.size() == contentLength) {
                    break;
                }
            }
        }
        String status = new String(Bytes.toArray(statusByteList.subList(0, statusByteList.size() - 2)));
        String body = new String(Bytes.toArray(bodyByteList));
        SocketHttpResponse socketHttpResponse = new SocketHttpResponse();
        socketHttpResponse.setStatus(status);
        socketHttpResponse.setHeadList(multiValueMap);
        socketHttpResponse.setBody(body);
        return socketHttpResponse;
    }

    public void close() throws IOException {
        socket.close();
    }
}

笔记

  • 上面代码是实验性质的,并不能用于生产环境,只处理Content-Length决定报文结束位置的情况,其实还有响应头为Transfer-Encoding:chunked的情况
  • 前面说到,空格是分隔符,需要编码成%20, 事实上不只有空格,URL中的字符需要符合RFC3986规范,编码采用ASCII码,而且表中128个字符只有82个可用,具体代码可以参考一下Tomcat的HttpParser
  • 如果请求头中带中文,也要先进行URL编码,然后接收端做解码
  • 请求体中可以带中文而不需要任何编码,那是因为发送端在Content-Type中指定了charset=UTF-8,所以接收端能正确地将Byte转成字符串
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值