记一次使用Socket模拟HTTP请求

Posted jellybean961

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记一次使用Socket模拟HTTP请求相关的知识,希望对你有一定的参考价值。

直接进入主题。

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议,HTTP是基于TCP/IP通信协议来传递数据。

Java中的正好有一个类可以实现TCP的传输与接收,那就是Socket。

首先要先起一个SpringBoot项目用于接收HTTP请求,Controller很简单,接收请求,并返回请求的内容

@RequestMapping("response")
@Controller
public class MainController {

    private static Logger LOG = LoggerFactory.getLogger(MainController .class);
 

    @RequestMapping("index.html")
    @ResponseBody
    public String index(HttpServletRequest request){
        LOG.info("请求内容:"+requestContent(request));
        return "请求内容:"+requestContent(request);
    }
     public String requestContent(HttpServletRequest request){

        Map<String,String[]> map = request.getParameterMap();

        Iterator<String> iterator = map.keySet().iterator();

        StringBuilder sb = new StringBuilder();

        while(iterator.hasNext()){
            String key = iterator.next();
            String[] args = map.get(key);

            String str = "";
            for(String s:args){
                str += s+",";
            }
            if(key.equals("sign")) {
                str = str.replaceAll(" ", "+");
            }
            sb.append(key+"="+(str.length() > 0 ? str.substring(0,str.length() - 1) : ""));
            sb.append("&");
        }


        if(sb.length() > 0) {
            return sb.substring(0, sb.length() - 1);
        }

        return sb.toString();

    }    
}

在发送报文前先了解一下HTTP协议的报文结构。

技术图片

一个完整的GET请求的请求报文是这样的: (来源Chrome)

GET /response/index.html HTTP/1.1

Host: 127.0.0.1:8080

Connection: keep-alive

Cache-Control: max-age=0

Upgrade-Insecure-Requests: 1

User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36

Sec-Fetch-Dest: document 

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9

接下来就是使用Socket来发送这样结构的报文,模拟一次HTTP请求。

 public static void main(String[] args) {
        
        try {
            // 新建一个socket套接字,地址跟端口都指向springboot项目
            Socket socket = new Socket("127.0.0.1", 8080);
            
            // 拼接http请求报文
            StringBuilder sb = new StringBuilder();
            // 拼接请求行
            sb.append("GET http://127.0.0.1:8080/response/index.html?a=1 HTTP/1.1
");
            // 开始拼接请求头
            sb.append("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
");
            sb.append("Connection: keep-alive
");
            // Content-Length 跟 Content-Type 在POST请求时起很重要的作用,没有这两个请求头,就算你发送的请求实体,服务端也是解析不到请求实体的。
            sb.append("Content-Length: 7
");
            sb.append("Content-Type: application/x-www-form-urlencoded
");
            sb.append("Host: 127.0.0.1:8080
");
            sb.append("User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
");
            // 结束拼接请求头,然后拼接一个空行
            sb.append("
");
            // 这里是请求实体,用于POST测试
            sb.append("b=2&c=3");
            
            // 将拼接好的报文发送至服务端
            socket.getOutputStream().write(sb.toString().getBytes());
            
            System.out.println("发送数据");
            System.out.println(sb.toString());

            // 开启一个线程去接收服务端返回的数据
            // 开线程的目的是服务端需要时间进行响应,所以直接获取inputstream是拿不到服务端返回的数据的,因为服务端还没返回,你就直接去获取,这样是拿不到的
            new Thread(new Runnable() {
                
                @Override
                public void run() {
                    while(true) {
                        byte[] b = new byte[1024];
                        int off = 0;try {
                            while(socket.getInputStream().read(b, off, b.length) != -1) {
                                System.out.println("返回数据:"+new String(b,"UTF-8"));
                            }
                            
                        } catch (IOException e1) {
                            // TODO Auto-generated catch block
                            e1.printStackTrace();
                        }
                        
                    }
                }
            }).start();

            socket.shutdownOutput();
            
        } catch (IOException e) {
            e.printStackTrace();
        } 
        
        
        
    }

将上面的代码运行后得到

发送数据
GET http://127.0.0.1:8080/response/index.html?a=1 HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Connection: keep-alive
Content-Length: 7
Content-Type: application/x-www-form-urlencoded
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36

b=2&c=3
返回数据:HTTP/1.1 200 
Content-Type: text/html;charset=UTF-8
Content-Length: 16
Date: Mon, 09 Mar 2020 04:13:41 GMT

请求内容:a=1

会发现一个问题,请求实体b=2&c=3是一起发送过去的,但是后端解析的时候没能解析出来,因为GET请求是不包含请求实体的。

把GET换成POST后就得到

发送数据
POST http://127.0.0.1:8080/response/index.html?a=1 HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Connection: keep-alive
Content-Length: 7
Content-Type: application/x-www-form-urlencoded
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36

b=2&c=3
返回数据:HTTP/1.1 200 
Content-Type: text/html;charset=UTF-8
Content-Length: 24
Date: Mon, 09 Mar 2020 04:17:21 GMT

请求内容:a=1&b=2&c=3

后端能够正确的获取到了请求实体。

在服务端返回的数据,结构跟请求报文差不多。第一行是状态行,第一行后的

Content-Type: text/html;charset=UTF-8
Content-Length: 24
Date: Mon, 09 Mar 2020 04:17:21 GMT

这三行是消息的报头,表示了内容的类型,内容的长度跟服务器的响应时间。

消息报头后有一行空行,然后就是响应的内容了,也就是服务端返回的数据。

 

以上是关于记一次使用Socket模拟HTTP请求的主要内容,如果未能解决你的问题,请参考以下文章

java socket模拟http请求

记一次重构:并行化调用接口实践

使用PHP Socket 编程模拟Http post和get请求

C#怎么用Socket模拟 HTTP请求

记一次Api请求

记一次HTTP POST请求变成GET请求