探究HTTP报文并原生socket手写实现HTTP请求的request和response

这里是使用的java实现的,每种语言的实现方式都是一样的,拼凑http的报文信息进行发送,以及连接端口号,接收报文信息解析即可。
一. socket实现http get请求发送数据
  1. 服务端先随意写个get请求的接口
@GetMapping("/get")
public Map<String, Object> get(@RequestParam("name") String name){
    System.out.println(name);
    return Map.of("msg","success");
}
  1. 要用原生的socket去调用http接口,就需要按照http的规范去发送报文信息;通过ApiPost工具发送请求,再使用抓包工具可以拿到报文信息(大概长这样):
    在这里插入图片描述

(我们只需要关注其中比较重要的几项即可)
3. 先写一个通用的发送socket数据的方法

private void sendHttp(HttpSendVo vo, String httpBody){
        try {
            System.out.println("**************** Server request ****************");
            System.out.println(httpBody);
            Socket socket = new Socket(vo.getIp(), vo.getPort());
//            socket.setSoTimeout(30000);
            PrintWriter outWriter = new PrintWriter(socket.getOutputStream());
            outWriter.println(httpBody);

            outWriter.flush();
            // socket客户端接收tomcat返回的数据
            BufferedReader inReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 以下是服务器返回的数据
            System.out.println("**************** Server response ****************");
            String tmp = "";
            while ((tmp = inReader.readLine()) != null) {
                // 解析服务器返回的数据,做相应的处理
                System.out.println(tmp);
            }
            outWriter.close();
            inReader.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  1. 使用硬编码方法,将上面的http报文传过去做测试,是否能够正常请求成功(下面的post也可以这样先来进行测试,确保有一份能跑通的报文信息,方便拼接报文时做对比)
直接调用sendHttp(),传入上面报文测试是否通过即可,需要注意content-length不能出错
  1. 然后依据上面的报文拼接代码如下(这里只需要加上主要的几个请求头即可):
public static void sendGet(HttpSendGetVo vo) {
        try {
            StringBuffer httpBody = new StringBuffer(200);
            httpBody.append("GET "+vo.getUrl()+"?"+vo.getParams()+" HTTP/1.1\r\n");
            httpBody.append("Host: "+vo.getIp()+":"+vo.getPort()+"\r\n"); // 这个host参数也是必须的
            httpBody.append("Connection: Close\r\n"); // 这个不加,client不会关闭
            httpBody.append("\r\n");
            sendHttp(vo, httpBody.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

  1. 此处的字符是都不能出错的,否则便发送不成功,所以可以用字符串作比较的方式排查问题:
private void compareStr(StringBuffer sb){
        String s = sendData();
        String s2 = sb.toString();
        for(int i = 0; i < s.length(); i ++){
            char c = s.charAt(i);
            char c2= s2.charAt(i);
            System.out.println(c+"   "+c2+"    "+i);
            if(c != c2){
                System.out.println("不同::  "+c+"   "+c2+"    "+i);
            }
        }
        System.out.println( sendData().toString());
        System.out.println(sb.toString()+ "    "+sb.length());
        System.out.println(sb.toString().equals(sendData().toString()));
    }  

7.发送上面的请求,报文如下:

* GET /file/get?name=111 HTTP/1.1
* Host: 127.0.0.1:8080
* Connection: Close
*
*
* HTTP/1.1 200
* Content-Type: text/plain;charset=UTF-8
* Content-Length: 3
* Date: Sat, 12 Nov 2022 05:01:50 GMT
* Connection: close
二. socket实现http post请求发送数据
get请求还是比较简单的,而post请求有多种传参方式,并且还有文件传输,报文拼接就相对复杂一些。
  1. post中 content-type 解释
    参考链接:https://blog.csdn.net/Eat_a_meal/article/details/124198611.
    • multipart/form-data:就是http请求中的multipart/form-data,它会将表单的数据处理为一条消息,以标签为单元,用分隔符分开。既可以上传键值对,也可以上传文件。当上传的字段是文件时,会有Content-Type来表名文件类型;content-disposition,用来说明字段的一些信息;由于有boundary隔离,所以multipart/form-data既可以上传文件,也可以上传键值对,它采用了键值对的方式,所以可以上传多个文件。
    • application/x-www-form-urlencoded:就是application/x-www-form-urlencoded,会将表单内的数据转换为键值对,比如,username=张三
    • raw:以上传任意格式的文本,可以上传text、json、xml、html等content-type= text/html(HTML 文档);text/plain(纯文本);text/css(CSS 样式表);application/json (json字符串)
    • binary:相当于Content-Type:application/octet-stream,从字面意思得知,只可以上传二进制数据,通常用来上传文件,由于没有键值,所以,一次只能上传一个文件。

  1. 准备一个post的接口,其中包含接收参数和文件
@RequestMapping("/test")
public Map<String, Object> test(
        @RequestParam(name="file",required = false)MultipartFile file, @RequestParam Map param) throws IOException {
    if(null != file){
        file.transferTo(new File("E:\\"+file.getOriginalFilename()));
        System.out.printf("上传文件名:【%s】",file.getOriginalFilename());
    }
    if(null != param){
        System.out.printf("上传参数:【%s】",JSONObject.toJSONString(param));
    }
    return Map.of("msg","success");
}

  1. post使用 x-www-form-urlencoded 发送数据

3.1 报文如下(从中可以看出参数是&拼接的,并且报文信息比较简单):

POST /file/text HTTP/1.1
Host: 127.0.0.1
Content-Length: 22
Content-Type: application/x-www-form-urlencoded
Connection: Close

name=gloomyfish&age=32

3.2 然后依据上面的报文拼接代码如下(这里只需要加上主要的几个请求头即可,此处需要注意Content-Length的长度,必须是发送数据的长度):

public static void sendPost(HttpSendPostVo vo) throws IOException {
       String data = URLEncoder.encode("name", "utf-8") + "="
                + URLEncoder.encode("哈哈哈哈", "utf-8")
                + "&" + URLEncoder.encode("age", "utf-8") + "="
                + URLEncoder.encode("32", "utf-8");
        StringBuffer sb = new StringBuffer();
        sb.append("POST " + vo.getUrl() + " HTTP/1.1\r\n");
        sb.append("Host: " + vo.getIp() + "\r\n");
        sb.append("Content-Length: " + data.length() + "\r\n");
        sb.append("Content-Type: "+ContentTypeEnum.FORM_URLENCODED.getContentType()+"\n");
        sb.append("Connection: Close\r\n");
        sb.append("\r\n");  
        sb.append(data);
        sb.append("\r\n");
        sendHttp(vo, sb.toString());
}

  1. post使用 form-data 发送数据

    • 无文件上传()
      a)报文如下(这里就需要注意拼接和换行了,必须保证符合协议规范):
     POST /file/text HTTP/1.1
     Host: 127.0.0.1
     Content-Type: multipart/form-data; boundary=qyl
     Content-Length: 113
     Connection: Close
    
     --qyl
     Content-Disposition: form-data; name="id"
    
     111
     --qyl
     Content-Disposition: form-data; name="name"
    
     222
     --qyl--
    

    b)发送请求代码如下(,此处需要注意Content-Length的长度,必须是发送数据的长度):

    public static void sendPostFormDataNoFile(HttpSendPostVo vo) throws IOException {
         String boundary = "qyl";
         StringBuffer data = new StringBuffer();
         data.append(getFormItem("id", "11"));
         data.append(getFormItem("name", "11"));
         data.append("--"+boundary+"--");
    
         StringBuffer sb = new StringBuffer();
         sb.append("POST " + vo.getUrl() + " HTTP/1.1\r\n");
         sb.append("Host: " + vo.getIp() + "\r\n");
         sb.append("Content-Type: "+ContentTypeEnum.FORM_DATA.getContentType()+";boundary="+boundary +"\r\n");
         sb.append("Content-Length: " + data.length() + "\r\n");
         sb.append("Connection: Close\r\n");
         sb.append("\r\n");
         sb.append(data);
         sb.append("\r\n");
         sendHttp(vo, sb.toString());
     }
    

    • 使用文本方式(text/plain)进行上传
      a)报文如下(这里需要注意文件的拼接方式,必须保证符合协议规范,此处使用文本类型的文件):
     POST /file/text HTTP/1.1
     Host: 127.0.0.1
     Content-Type: multipart/form-data;boundary=qinyulin
     Content-Length: 248
     Connection: Close
    
     --qinyulin
     Content-Disposition: form-data; name="id"
    
     11
     --qinyulin
     Content-Disposition: form-data; name="name"
    
     11
     --qinyulin
     Content-Disposition: form-data; name="file"; filename="text.txt"
     Content-Type: text/plain
    
     sdfs
     --qinyulin--
    
    

    b)发送请求代码如下(,此处需要注意Content-Length的长度,必须是发送数据的长度):

    public static void sendPostFormDataWithFile(HttpSendPostVo vo) throws Exception {
         String boundary = "qinyulin";
         StringBuffer data = new StringBuffer();
         data.append(getFormItem("\"id\"", "11"));
         data.append(getFormItem("\"name\"", "11"));
         data.append(getFormFileItem("\"file\"", "D:\\text.txt",boundary));
         data.append("--"+boundary+"--");
    
         StringBuffer sb = new StringBuffer();
         sb.append("POST " + vo.getUrl() + " HTTP/1.1\r\n");
         sb.append("Host: " + vo.getIp() + "\r\n");
         sb.append("Content-Type: "+ContentTypeEnum.FORM_DATA.getContentType()+";boundary="+boundary +"\r\n");
         sb.append("Content-Length: " + data.length() + "\r\n");
         sb.append("Connection: Close\r\n");
         sb.append("\r\n");
         sb.append(data);
         sb.append("\r\n");
         sendHttp(vo, sb.toString());
    } 
    

    • 使用二进制流方式(content-type要写具体类型,或者octet-stream) 进行上传
      a)报文如下(这里需要注意文件的拼接方式,必须保证符合协议规范):
     POST /file/text HTTP/1.1
     Host: 127.0.0.1
     Content-Type: multipart/form-data;boundary=qinyulin
     Content-Length: 21239
     accept-encoding: gzip, deflate, br
    
     --qinyulin
     Content-Disposition: form-data; name=name
    
     222
     --qinyulin
     Content-Disposition: form-data; name=id
    
     111
     --qinyulin
     Content-Disposition: form-data; name=file; filename=1668245082534.png
     Content-Type: image/pngConnection: Keep-Alive
    
     �PNG
     
     
     IHDR  �  
     ... 这里都是文件的二进制乱码省略了
    --qinyulin--
    
    

    b)发送请求代码如下(,此处需要注意Content-Length的长度,必须是发送数据的长度),这里拼接的时候,文件是byte类型直接传输的:

     public void sendPostFormDataWithFileStream(HttpSendPostVo vo) throws Exception {
             Socket socket = new Socket(vo.getIp(), vo.getPort()); // 创建socket
             OutputStream out = socket.getOutputStream(); // 创建输出流
             File file = vo.getFile();
             FileInputStream fin = new FileInputStream(file);
             byte[] fileBytes = fin.readAllBytes(); // 得到文件字节码
             StringBuffer fileBefore = new StringBuffer(); // 输出文件字节码之前的表单数据
             vo.getBodyParam().forEach((k, v) ->{ // 表单参数
                 fileBefore.append(getFormItem(k, v.toString()));
             });
             fileBefore.append("--").append(BOUNDARY).append(NEW_LINE); // 包装文件
             fileBefore.append("Content-Disposition: form-data; name=").append(vo.getFileReqName())
                     .append("; filename=").append(file.getName()).append(NEW_LINE);
             fileBefore.append("Content-Type: "+Files.probeContentType(file.toPath()));
             fileBefore.append("Connection: Keep-Alive");
             fileBefore.append(NEW_LINE).append(NEW_LINE);
             String end = getReqEndLine(); // 结尾
             StringBuffer sb = new StringBuffer();
             sb.append("POST ").append(vo.getUrl()).append(" HTTP/1.1").append(NEW_LINE);
             sb.append("Host: ").append(vo.getIp()).append(NEW_LINE);
             sb.append("Content-Type: ").append(ContentTypeEnum.FORM_DATA.getContentType())
                     .append(";boundary=").append(BOUNDARY).append(NEW_LINE);
             int length = fileBefore.toString().getBytes().length
                     + fileBytes.length + end.getBytes().length;
             sb.append("Content-Length: ").append(length).append(NEW_LINE);
     //        sb.append("Content-Length: 21397 \r\n");
     //        sb.append("Accept:text/html, application/xhtml+xml, */*").append("\r\n");
             sb.append("accept-encoding: gzip, deflate, br").append(NEW_LINE).append(NEW_LINE);
             out.write(sb.toString().getBytes());
             // 下面是数据
             out.write(fileBefore.toString().getBytes());
             out.write(fileBytes);
     //        byte b[]=new byte[1024];
     //        int rb = 0;
     //        while((rb = fin.read(b))!=-1){
     //            out.write(b,0,rb);
     //        };
             out.write("\r\n".getBytes(StandardCharsets.UTF_8));
             out.write(end.getBytes(StandardCharsets.UTF_8));
             // socket客户端接收tomcat返回的数据
             BufferedReader inReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             // 以下是服务器返回的数据
             System.out.println("**************** Server response ****************");
             String tmp = "";
             while ((tmp = inReader.readLine()) != null) {
                 // 解析服务器返回的数据,做相应的处理
                 System.out.println(tmp);
             }
             fin.close();
             out.close();
     //        inReader.close();
         }
    

  2. 使用socket服务端接收http请求,代码如下,client发送数据后,用socket监听8080端口,就拿到了所有的报文,然后拿到的数据根据http协议解析就可以了:

System.out.println("start...");
//创建服务器端对象
ServerSocket server = new ServerSocket(PORT);
while(true) {
    try {
        Socket socket = server.accept();
        //创建文件输出流和网络输入流
        BufferedReader inReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//                String res = inReader.readLine();
        // 以下是服务器返回的数据
        System.out.println("**************** Server request ****************");
        String tmp = "";
        while ((tmp = inReader.readLine()) != null) {
            // 解析服务器返回的数据,做相应的处理
//                    res += tmp;
            System.out.println(tmp);
//                    if(tmp.startsWith("Connection")){
//                        break;
//                    }

        }
//                System.out.println("拿到的数据:"+res);
        PrintWriter outWriter = new PrintWriter(socket.getOutputStream());
        outWriter.println("success");
        outWriter.flush();
        outWriter.close();
        inReader.close();
    }catch (Exception e){
        e.printStackTrace();
    }
}

代码位置:https://gitee.com/ClumsyBird/learn-demo/tree/master/file-upload
(代码比较粗糙,在com.qyl.http_upload.finally_code包下)

三. 总结
  1. 通过对于http的实现,能够了解其本质,只是对socket进行了规范制定
  2. 能够更清楚每种传参方式的优劣,以及什么样的传参对应什么样的接收方式
  3. 在一些性能比较有影响的地方,在熟悉http之后,能够做一些优化和更高效的使用
  4. 在一些特殊场景中,能够去自定义私有协议进行传输
  5. 通过抓包、解析的过程,能够提升爬虫的能力,以及对于传输安全性措施的提升
  6. java socket的底层原理(参考链接):https://blog.csdn.net/weixin_43719752/article/details/126751394
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
使用 WorkStealingPool 实现 HTTP 请求并返回报文的示例代码如下: ```java import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class WorkStealingPoolExample { public static void main(String[] args) { // 创建 WorkStealingPool 线程池 ExecutorService executor = Executors.newWorkStealingPool(); // 提交 HTTP 请求任务 executor.submit(() -> { try { String url = "https://example.com"; // 替换为你要请求的 URL URL obj = new URL(url); HttpURLConnection con = (HttpURLConnection) obj.openConnection(); con.setRequestMethod("GET"); // 发送请求 int responseCode = con.getResponseCode(); System.out.println("Response Code: " + responseCode); // 读取响应内容 BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); String inputLine; StringBuilder response = new StringBuilder(); while ((inputLine = in.readLine()) != null) { response.append(inputLine); } in.close(); // 输出响应内容 System.out.println("Response: " + response.toString()); } catch (Exception e) { e.printStackTrace(); } }); // 关闭线程池 executor.shutdown(); try { if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); } } } ``` 上述代码中,使用 WorkStealingPool 创建了一个线程池,然后通过 `executor.submit()` 方法提交了一个 HTTP 请求的任务。在任务中,使用 HttpURLConnection 发送 GET 请求,并读取响应内容。最后,输出响应码和响应内容。 需要注意的是,这里只提交了一个任务作为示例,你可以根据实际需求提交多个任务。同时,记得在任务执行完毕后关闭线程池。 希望这个示例对你有帮助!如果还有其他问题,请继续提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值