http.md 14.6 KB
Newer Older
沉默王二's avatar
沉默王二 已提交
1 2 3 4 5 6 7 8 9 10 11
---
title: 牛逼,用Java Socket手撸了一个HTTP服务器
shortTitle: 用Socket撸一个HTTP服务器
description: 作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomcat的底层是怎么支持http服务的呢?大名鼎鼎的Servlet又是什么东西呢,该怎么使用呢? 在初学java时,socket编程是逃不掉的一章;虽然在实际业务项目中,使用这个的可能性基本为0,本篇博文将主要介绍如何使用socket来实现一个简单的http服务器
category:
  - Java核心
tag:
  - Java网络编程
head:
  - - meta
    - name: keywords
沉默王二's avatar
socket  
沉默王二 已提交
12
      content: Java,Java SE,Java基础,Java教程,Java程序员进阶之路,Java进阶之路,Java入门,教程,JavaSocket,java网络编程,网络编程,http,socket http,http 服务器
沉默王二's avatar
沉默王二 已提交
13 14
---

沉默王二's avatar
socket  
沉默王二 已提交
15
# 11.3 用Socket撸一个HTTP服务器
沉默王二's avatar
沉默王二 已提交
16

沉默王二's avatar
socket  
沉默王二 已提交
17
作为一个 Java 后端,提供 HTTP 服务可以说是基本技能之一了,但是你真的了解 HTTP 协议么?你知道知道如何手撸一个 HTTP 服务器么?tomcat 的底层是怎么支持 HTTP 服务的呢?大名鼎鼎的 Servlet 又是什么东西呢,该怎么使用呢?
沉默王二's avatar
沉默王二 已提交
18

沉默王二's avatar
socket  
沉默王二 已提交
19
在初学 Java 时,Socket 编程是逃不掉的一章;虽然在实际业务项目中,使用这个的可能性基本为 0,本篇博文将主要介绍如何使用 Socket 来实现一个简单的 HTTP 服务器功能,提供常见的 get/post 请求支持,并再此过程中了解下 HTTP 协议
沉默王二's avatar
沉默王二 已提交
20

沉默王二's avatar
socket  
沉默王二 已提交
21
## I. HTTP 服务器从 0 到 1
沉默王二's avatar
沉默王二 已提交
22

沉默王二's avatar
socket  
沉默王二 已提交
23
既然我们的目标是借助 Socket 来搭建 HTTP 服务器,那么我们首先需要确认两点,一是如何使用 Socket;另一个则是 HTTP 协议如何,怎么解析数据;下面分别进行说明
沉默王二's avatar
沉默王二 已提交
24

沉默王二's avatar
socket  
沉默王二 已提交
25
### 1\. Socket 编程基础
沉默王二's avatar
沉默王二 已提交
26

沉默王二's avatar
socket  
沉默王二 已提交
27
我们这里主要是利用 ServerSocket 来绑定端口,提供 tcp 服务,基本使用姿势也比较简单,一般套路如下
沉默王二's avatar
沉默王二 已提交
28

沉默王二's avatar
socket  
沉默王二 已提交
29 30 31 32 33
- 创建 ServerSocket 对象,绑定监听端口
- 通过 accept()方法监听客户端请求
- 连接建立后,通过输入流读取客户端发送的请求信息
- 通过输出流向客户端发送乡音信息
- 关闭相关资源
沉默王二's avatar
沉默王二 已提交
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

对应的伪代码如下:

```java
ServerSocket serverSocket = new ServerSocket(port, ip)
serverSocket.accept();
// 接收请求数据
socket.getInputStream();

// 返回数据给请求方
out = socket.getOutputStream()
out.print(xxx)
out.flush();;

// 关闭连接
socket.close()
```

沉默王二's avatar
socket  
沉默王二 已提交
52
### 2\. HTTP 协议
沉默王二's avatar
沉默王二 已提交
53

沉默王二's avatar
socket  
沉默王二 已提交
54
我们上面的 ServerSocket 走的是 TCP 协议,HTTP 协议本身是在 TCP 协议之上的一层,对于我们创建 HTTP 服务器而言,最需要关注的无非两点
沉默王二's avatar
沉默王二 已提交
55

沉默王二's avatar
socket  
沉默王二 已提交
56 57
- 请求的数据怎么按照 HTTP 的协议解析出来
- 如何按照 HTTP 协议,返回数据
沉默王二's avatar
沉默王二 已提交
58 59 60 61 62

所以我们需要知道数据格式的规范了

**请求消息**

沉默王二's avatar
沉默王二 已提交
63
![request headers](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/socket/http-22e0f678-b83a-4514-b2ae-44980ad845d0.jpg)
沉默王二's avatar
沉默王二 已提交
64 65 66

**响应消息**

沉默王二's avatar
沉默王二 已提交
67
![respones headers](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/socket/http-8213eb7a-14d9-4db9-b843-d9d816a1fdec.jpg)
沉默王二's avatar
沉默王二 已提交
68 69 70 71 72

上面两张图,先有个直观映象,接下来开始抓重点

不管是请求消息还是相应消息,都可以划分为三部分,这就为我们后面的处理简化了很多

沉默王二's avatar
socket  
沉默王二 已提交
73 74 75
- 第一行:状态行
- 第二行到第一个空行:header(请求头/相应头)
- 剩下所有:正文
沉默王二's avatar
沉默王二 已提交
76

沉默王二's avatar
socket  
沉默王二 已提交
77
### 3\. HTTP 服务器设计
沉默王二's avatar
沉默王二 已提交
78

沉默王二's avatar
socket  
沉默王二 已提交
79
接下来开始进入正题,基于 Socket 创建一个 HTTP 服务器,使用 Socket 基本没啥太大的问题,我们需要额外关注以下几点
沉默王二's avatar
沉默王二 已提交
80

沉默王二's avatar
socket  
沉默王二 已提交
81 82
- 对请求数据进行解析
- 封装返回结果
沉默王二's avatar
沉默王二 已提交
83 84 85

#### a. 请求数据解析

沉默王二's avatar
socket  
沉默王二 已提交
86
我们从 Socket 中拿到所有的数据,然后解析为对应的 HTTP 请求,我们先定义个 Request 对象,内部保存一些基本的 HTTP 信息,接下来重点就是将 Socket 中的所有数据都捞出来,封装为 request 对象
沉默王二's avatar
沉默王二 已提交
87 88 89 90 91 92 93 94 95 96 97 98 99

```java
@Data
public static class Request {
    /**
     * 请求方法 GET/POST/PUT/DELETE/OPTION...
     */
    private String method;
    /**
     * 请求的uri
     */
    private String uri;
    /**
沉默王二's avatar
socket  
沉默王二 已提交
100
     * HTTP版本
沉默王二's avatar
沉默王二 已提交
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
     */
    private String version;

    /**
     * 请求头
     */
    private Map<String, String> headers;

    /**
     * 请求参数相关
     */
    private String message;
}
```

沉默王二's avatar
socket  
沉默王二 已提交
116
根据前面的 HTTP 协议介绍,解析过程如下,我们先看请求行的解析过程
沉默王二's avatar
沉默王二 已提交
117

沉默王二's avatar
socket  
沉默王二 已提交
118
**请求行**,包含三个基本要素:请求方法 + URI + HTTP 版本,用空格进行分割,所以解析代码如下
沉默王二's avatar
沉默王二 已提交
119 120 121

```java
/**
沉默王二's avatar
socket  
沉默王二 已提交
122
 * 根据标准的HTTP协议,解析请求行
沉默王二's avatar
沉默王二 已提交
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
 *
 * @param reader
 * @param request
 */
private static void decodeRequestLine(BufferedReader reader, Request request) throws IOException {
    String[] strs = StringUtils.split(reader.readLine(), " ");
    assert strs.length == 3;
    request.setMethod(strs[0]);
    request.setUri(strs[1]);
    request.setVersion(strs[2]);
}
```

**请求头的解析**,从第二行,到第一个空白行之间的所有数据,都是请求头;请求头的格式也比较清晰, 形如 `key:value`, 具体实现如下

```java
/**
沉默王二's avatar
socket  
沉默王二 已提交
140
 * 根据标准HTTP协议,解析请求头
沉默王二's avatar
沉默王二 已提交
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
 *
 * @param reader
 * @param request
 * @throws IOException
 */
private static void decodeRequestHeader(BufferedReader reader, Request request) throws IOException {
    Map<String, String> headers = new HashMap<>(16);
    String line = reader.readLine();
    String[] kv;
    while (!"".equals(line)) {
        kv = StringUtils.split(line, ":");
        assert kv.length == 2;
        headers.put(kv[0].trim(), kv[1].trim());
        line = reader.readLine();
    }

    request.setHeaders(headers);
}
```

**最后就是正文的解析了**,这一块需要注意一点,正文可能为空,也可能有数据;有数据时,我们要如何把所有的数据都取出来呢?

先看具体实现如下

```java
/**
沉默王二's avatar
socket  
沉默王二 已提交
167
 * 根据标注HTTP协议,解析正文
沉默王二's avatar
沉默王二 已提交
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
 *
 * @param reader
 * @param request
 * @throws IOException
 */
private static void decodeRequestMessage(BufferedReader reader, Request request) throws IOException {
    int contentLen = Integer.parseInt(request.getHeaders().getOrDefault("Content-Length", "0"));
    if (contentLen == 0) {
        // 表示没有message,直接返回
        // 如get/options请求就没有message
        return;
    }

    char[] message = new char[contentLen];
    reader.read(message);
    request.setMessage(new String(message));
}
```

注意下上面我的使用姿势,首先是根据请求头中的`Content-Type`的值,来获得正文的数据大小,因此我们获取的方式是创建一个这么大的`char[]`来读取流中所有数据,如果我们的数组比实际的小,则读不完;如果大,则数组中会有一些空的数据;

沉默王二's avatar
socket  
沉默王二 已提交
189
**最后将上面的几个解析封装一下**,完成 request 解析
沉默王二's avatar
沉默王二 已提交
190 191 192

```java
/**
沉默王二's avatar
socket  
沉默王二 已提交
193
 * HTTP的请求可以分为三部分
沉默王二's avatar
沉默王二 已提交
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
 *
 * 第一行为请求行: 即 方法 + URI + 版本
 * 第二部分到一个空行为止,表示请求头
 * 空行
 * 第三部分为接下来所有的,表示发送的内容,message-body;其长度由请求头中的 Content-Length 决定
 *
 * 几个实例如下
 *
 * @param reqStream
 * @return
 */
public static Request parse2request(InputStream reqStream) throws IOException {
    BufferedReader httpReader = new BufferedReader(new InputStreamReader(reqStream, "UTF-8"));
    Request httpRequest = new Request();
    decodeRequestLine(httpReader, httpRequest);
    decodeRequestHeader(httpReader, httpRequest);
    decodeRequestMessage(httpReader, httpRequest);
    return httpRequest;
}
```

沉默王二's avatar
socket  
沉默王二 已提交
215
#### b. 请求任务 HttpTask
沉默王二's avatar
沉默王二 已提交
216

沉默王二's avatar
socket  
沉默王二 已提交
217
每个请求,单独分配一个任务来干这个事情,就是为了支持并发,对于 ServerSocket 而言,接收到了一个请求,那就创建一个 HttpTask 任务来实现 HTTP 通信
沉默王二's avatar
沉默王二 已提交
218

沉默王二's avatar
socket  
沉默王二 已提交
219
那么这个 httptask 干啥呢?
沉默王二's avatar
沉默王二 已提交
220

沉默王二's avatar
socket  
沉默王二 已提交
221 222 223
- 从请求中捞数据
- 响应请求
- 封装结果并返回
沉默王二's avatar
沉默王二 已提交
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318

```java
public class HttpTask implements Runnable {
    private Socket socket;

    public HttpTask(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        if (socket == null) {
            throw new IllegalArgumentException("socket can't be null.");
        }

        try {
            OutputStream outputStream = socket.getOutputStream();
            PrintWriter out = new PrintWriter(outputStream);

            HttpMessageParser.Request httpRequest = HttpMessageParser.parse2request(socket.getInputStream());
            try {
                // 根据请求结果进行响应,省略返回
                String result = ...;
                String httpRes = HttpMessageParser.buildResponse(httpRequest, result);
                out.print(httpRes);
            } catch (Exception e) {
                String httpRes = HttpMessageParser.buildResponse(httpRequest, e.toString());
                out.print(httpRes);
            }
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
```

对于请求结果的封装,给一个简单的进行演示

```java
@Data
public static class Response {
    private String version;
    private int code;
    private String status;

    private Map<String, String> headers;

    private String message;
}

public static String buildResponse(Request request, String response) {
    Response httpResponse = new Response();
    httpResponse.setCode(200);
    httpResponse.setStatus("ok");
    httpResponse.setVersion(request.getVersion());

    Map<String, String> headers = new HashMap<>();
    headers.put("Content-Type", "application/json");
    headers.put("Content-Length", String.valueOf(response.getBytes().length));
    httpResponse.setHeaders(headers);

    httpResponse.setMessage(response);

    StringBuilder builder = new StringBuilder();
    buildResponseLine(httpResponse, builder);
    buildResponseHeaders(httpResponse, builder);
    buildResponseMessage(httpResponse, builder);
    return builder.toString();
}


private static void buildResponseLine(Response response, StringBuilder stringBuilder) {
    stringBuilder.append(response.getVersion()).append(" ").append(response.getCode()).append(" ")
            .append(response.getStatus()).append("\n");
}

private static void buildResponseHeaders(Response response, StringBuilder stringBuilder) {
    for (Map.Entry<String, String> entry : response.getHeaders().entrySet()) {
        stringBuilder.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
    }
    stringBuilder.append("\n");
}

private static void buildResponseMessage(Response response, StringBuilder stringBuilder) {
    stringBuilder.append(response.getMessage());
}
```

沉默王二's avatar
socket  
沉默王二 已提交
319
#### c. HTTP 服务搭建
沉默王二's avatar
沉默王二 已提交
320

沉默王二's avatar
socket  
沉默王二 已提交
321
前面的基本上把该干的事情都干了,剩下的就简单了,创建`ServerSocket`,绑定端口接收请求,我们在线程池中跑这个 HTTP 服务
沉默王二's avatar
沉默王二 已提交
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381

```java
public class BasicHttpServer {
    private static ExecutorService bootstrapExecutor = Executors.newSingleThreadExecutor();
    private static ExecutorService taskExecutor;
    private static int PORT = 8999;

    static void startHttpServer() {
        int nThreads = Runtime.getRuntime().availableProcessors();
        taskExecutor =
                new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100),
                        new ThreadPoolExecutor.DiscardPolicy());

        while (true) {
            try {
                ServerSocket serverSocket = new ServerSocket(PORT);
                bootstrapExecutor.submit(new ServerThread(serverSocket));
                break;
            } catch (Exception e) {
                try {
                    //重试
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }

        bootstrapExecutor.shutdown();
    }

    private static class ServerThread implements Runnable {

        private ServerSocket serverSocket;

        public ServerThread(ServerSocket s) throws IOException {
            this.serverSocket = s;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Socket socket = this.serverSocket.accept();
                    HttpTask eventTask = new HttpTask(socket);
                    taskExecutor.submit(eventTask);
                } catch (Exception e) {
                    e.printStackTrace();
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    }
}
```

沉默王二's avatar
socket  
沉默王二 已提交
382
到这里,一个基于 Socket 实现的 HTTP 服务器基本上就搭建完了,接下来就可以进行测试了
沉默王二's avatar
沉默王二 已提交
383 384 385 386 387

### 4\. 测试

做这个服务器,主要是基于项目 [quick-fix](https://github.com/liuyueyi/quick-fix) 产生的,这个项目主要是为了解决应用内部服务访问与数据订正,我们在这个项目的基础上进行测试

沉默王二's avatar
socket  
沉默王二 已提交
388
一个完成的 post 请求如下
沉默王二's avatar
沉默王二 已提交
389

沉默王二's avatar
沉默王二 已提交
390
![2.gif](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/socket/http-f314ade3-9006-4caa-b905-5726121826c4.gif)
沉默王二's avatar
沉默王二 已提交
391 392 393

接下来我们看下打印出返回头的情况

沉默王二's avatar
沉默王二 已提交
394
![2.gif](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/socket/http-59db6211-792a-494f-b01a-9d5848eceed1.gif)
沉默王二's avatar
沉默王二 已提交
395 396 397 398 399

## II. 其他

### 0\. 项目源码

沉默王二's avatar
socket  
沉默王二 已提交
400 401 402 403 404
- [quick-fix](https://github.com/liuyueyi/quick-fix)
- 相关代码:
- com.git.hui.fix.core.endpoint.BasicHttpServer
- com.git.hui.fix.core.endpoint.HttpMessageParser
- com.git.hui.fix.core.endpoint.HttpTask
沉默王二's avatar
沉默王二 已提交
405

沉默王二's avatar
socket  
沉默王二 已提交
406
> 参考链接:[https://liuyueyi.github.io/hexblog/2018/12/30/181230-%E4%BD%BF%E7%94%A8Java-Socket%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAhttp%E6%9C%8D%E5%8A%A1%E5%99%A8/](https://liuyueyi.github.io/hexblog/2018/12/30/181230-%E4%BD%BF%E7%94%A8Java-Socket%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAhttp%E6%9C%8D%E5%8A%A1%E5%99%A8/),整理:沉默王二
沉默王二's avatar
沉默王二 已提交
407

沉默王二's avatar
socket  
沉默王二 已提交
408
---
沉默王二's avatar
沉默王二 已提交
409

沉默王二's avatar
socket  
沉默王二 已提交
410
最近整理了一份牛逼的学习资料,包括但不限于 Java 基础部分(JVM、Java 集合框架、多线程),还囊括了 **数据库、计算机网络、算法与数据结构、设计模式、框架类 Spring、Netty、微服务(Dubbo,消息队列) 网关** 等等等等……详情戳:[可以说是 2022 年全网最全的学习和找工作的 PDF 资源了](https://tobebetterjavaer.com/pdf/programmer-111.html)
沉默王二's avatar
沉默王二 已提交
411

沉默王二's avatar
沉默王二 已提交
412
微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **111** 即可免费领取。
沉默王二's avatar
沉默王二 已提交
413

沉默王二's avatar
socket  
沉默王二 已提交
414
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png)