这里是使用的java实现的,每种语言的实现方式都是一样的,拼凑http的报文信息进行发送,以及连接端口号,接收报文信息解析即可。
一. socket实现http get请求发送数据

服务端先随意写个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 解释 参考链接:.
  • 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();
     }
  1. 使用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的底层原理(参考链接):