红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Java编程笔记
  4. 正文

Java编程笔记26:HTTP

2022年11月1日 191点热度 0人点赞 0条评论

image-20221101145143893

图源:

Fotor懒设计

HTTP协议实际上是基于TCP的应用层协议,规定了请求报文和响应报文等。参考HTTP协议的规定,我们可以实现一个简单的HTTP Server。

前文Java编程笔记25:TCP - 红茶的个人站点 (icexmoon.cn)中我介绍了如何实现简单的TCP服务端和客户端,本文将在之前示例的基础上实现一个简单的HTTP服务。

HTTP的基本概念推荐阅读图解HTTP (豆瓣) (douban.com),或者Web基础 - 廖雪峰的官方网站 (liaoxuefeng.com)。

请求报文

创建一个类负责解析并结构化请求报文:

package cn.icexmoon.java.note.ch25;
// ...
@Getter
public class HttpRequest {
    private String headLine;
    private Map<String, List<String>> headers = new LinkedHashMap<>();
    private String body;
    private String httpMethod;
    private String url;
    private String schema;
​
    public HttpRequest(BufferedReader br) throws IOException {
        //处理首行
        headLine = br.readLine();
        String[] hlPart = headLine.split(" ");
        if (hlPart.length < 3) {
            throw new RuntimeException("请求报文首行格式错误");
        }
        httpMethod = hlPart[0];
        url = hlPart[1];
        schema = hlPart[2];
        //处理报文头
        boolean requestReadEnd = false;
        do {
            String header = br.readLine();
            if (header == null) {
                //报文读取结束
                requestReadEnd = true;
                break;
            }
            if (header.isEmpty()) {
                //报文头结束
                break;
            }
            int separatorIdx = header.indexOf(":");
            if (separatorIdx <= 0) {
                throw new RuntimeException("报文头格式错误,缺少分隔符");
            }
            String headerKey = header.substring(0, separatorIdx);
            String headerValues;
            try {
                headerValues = header.substring(separatorIdx + 1);
                headerValues = headerValues.trim();
            } catch (IndexOutOfBoundsException e) {
                e.printStackTrace();
                throw new RuntimeException("报文头格式错误,缺少值");
            }
            if (headerValues == null || headerValues.isEmpty()) {
                this.headers.put(headerKey, null);
            }
            else{
                String[] values = headerValues.split(",");
                List<String> valuesList = Arrays.stream(values).map(v -> v.trim()).collect(Collectors.toList());
                this.headers.put(headerKey, valuesList);
            }
        }
        while (true);
        if (requestReadEnd) {
            this.body = "";
            return;
        }
        //处理报文体
        String contentLength = getFirstHeaderVal("Content-Length");
        if (contentLength != null) {
            if (Integer.valueOf(contentLength).compareTo(0) <= 0) {
                this.body = "";
                return;
            }
        }
        StringBuilder sb = new StringBuilder();
        final int SIZE = Integer.valueOf(contentLength);
        char[] bodyBf = new char[SIZE];
        br.read(bodyBf);
        this.body = new String(bodyBf);
    }
​
    /**
     * 获取第一个报文头的值
     *
     * @param headerKey 报文头的key
     * @return 如果缺少报文头或者报文头没有值,返回null
     */
    private String getFirstHeaderVal(String headerKey) {
        if (!headers.containsKey(headerKey)) {
            return null;
        }
        if (headers.get(headerKey) == null) {
            return null;
        }
        return headers.get(headerKey).get(0);
    }
}

HttpRequest的构造器接受一个输入流,并从流中读取请求报文并解析。

需要注意的是,报文体最后没有换行符,所以不能通过readLine方法读取。这是有意义的,因为除了常见的通过报文体传递字符串外,二进制文件也可以通过报文体传输,所以报文体必须按照字节方式读取,字节长度由报文头Content-Length指定。

响应报文

一次完整的HTTP请求由请求和响应构成,所以这里同样需要构建响应报文:

package cn.icexmoon.java.note.ch25;
// ...
@Accessors(chain = true)
@Setter
public class HttpResponse {
    private String schema;
    private Integer code;
    private String desc;
    private Map<String, List<String>> headers = new LinkedHashMap<>();
    private String body;
​
    public HttpResponse setCode(Integer code) {
        this.code = code;
        if (code.equals(200)) {
            desc = "ok";
        }
        return this;
    }
​
    /**
     * 添加响应报文头,同一个报文头可以调用多次添加多个值
     * @param key
     * @param value
     */
    public void addHeader(String key, String value){
        if (!headers.containsKey(key)){
            headers.put(key, new LinkedList<>());
        }
        List<String> values = headers.get(key);
        values.add(value);
    }
​
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(schema).append(" ").append(code).append(" ").append(desc).append("\n");
        if (headers != null) {
            headers.forEach((key, values) -> {
                sb.append(key).append(":");
                values.forEach(v -> {
                    sb.append(v).append(",");
                });
                if (sb.charAt(sb.length() - 1) == ',') {
                    sb.deleteCharAt(sb.length() - 1);
                }
                sb.append("\n");
            });
        }
        sb.append("\n");
        if (body != null && !body.isEmpty()) {
            sb.append(body);
        }
        return sb.toString();
    }
​
    public static HttpResponse buildSuccessResponse() {
        return new HttpResponse()
                .setCode(200)
                .setDesc("ok")
                .setSchema("HTTP/1.1")
                .setBody("");
    }
}

响应报文相对简单,这里没什么需要过多说明的。

HTTP服务

修改前文的TCP服务端代码,分析请求报文并解析为HttpRequest对象:

package cn.icexmoon.java.note.ch25;
// ...
public class Main {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(6666);
        System.out.println("server is starting...");
        ExecutorService es = Executors.newCachedThreadPool();
        while (true) {
            Socket client = ss.accept();
            final Socket finalClient = client;
            es.execute(new Runnable() {
​
                @Override
                public void run() {
                    try {
                        System.out.println("get connection from " + finalClient.getRemoteSocketAddress());
                        dealRequest(finalClient);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
​
    private static void dealRequest(Socket clientSocket) throws IOException {
        String addr = clientSocket.getRemoteSocketAddress().toString();
        try {
            InputStream is = clientSocket.getInputStream();
            OutputStream os = clientSocket.getOutputStream();
            InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
            BufferedReader br = new BufferedReader(isr);
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8));
            HttpRequest httpRequest = new HttpRequest(br);
            System.out.println("=============http request=========");
            System.out.println("http method:" + httpRequest.getHttpMethod());
            System.out.println("url:" + httpRequest.getUrl());
            System.out.println("schema:" + httpRequest.getSchema());
            System.out.println("============header==========");
            httpRequest.getHeaders().forEach((key, values) -> {
                System.out.println(key + ":" + values);
            });
            System.out.println("==========body============");
            System.out.println(httpRequest.getBody());
            //返回响应报文
            String bodyStr = "{\"code\":401,\"data\":null,\"msg\":\"你好\"}";
            int length = bodyStr.getBytes(StandardCharsets.UTF_8).length;
            HttpResponse httpResponse = HttpResponse.buildSuccessResponse();
            httpResponse.addHeader("Content-Type", "application/json;charset=utf-8");
            httpResponse.addHeader("Content-Length", Integer.valueOf(length).toString());
            httpResponse.addHeader("Keep-Alive", "timeout=60");
            httpResponse.setBody(bodyStr);
            bw.write(httpResponse.toString());
            bw.flush();
            System.out.println("========response=============");
            System.out.println(httpResponse.toString());
        } catch (Exception e) {
            System.out.println(String.format("[%s]connect is closed by IOException.", addr));
            e.printStackTrace();
        } finally {
            clientSocket.close();
            System.out.println(String.format("[]connect is closed normally.", addr));
        }
    }
​
    private static void printWholeRequest(BufferedReader br) throws IOException {
        do {
            String line = br.readLine();
            if (line == null) {
                break;
            }
            System.out.println(line);
        }
        while (true);
    }
}

这里没有根据不同请求进行进一步处理,仅是将请求报文解析后在服务端控制台简单打印,并最终返回一个简单的JSON格式响应。

现在可以将程序打包并运行了,客户端使用浏览器或者接口调试工具都可。

  • 如果使用mvn打包时出现编码GBK的不可映射字符之类的错误,可以检查是否在pom中指定Java编译时的源码编码<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>,具体可以参考使用Maven编译项目遇到——“maven编码gbk的不可映射字符”解决办法 - 孤傲苍狼 - 博客园 (cnblogs.com)。

  • 如果控制台或接口调用工具输出乱码,可以检查是否在字节流和字符流转换部分指定编码,比如:new InputStreamReader(is, StandardCharsets.UTF_8)。

最后,附上我的测试结果:

image-20221101144643146

谢谢阅读。

本文的最终完整示例可以从java-notebook/ch26 (github.com)获取。

参考资料

  • Web基础 - 廖雪峰的官方网站 (liaoxuefeng.com)

  • 使用Maven编译项目遇到——“maven编码gbk的不可映射字符”解决办法 - 孤傲苍狼 - 博客园 (cnblogs.com)

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: http java
最后更新:2022年11月1日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号