使用Java Socket手撸一个http服务器

简介: 作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomcat的底层是怎么支持http服务的呢?大名鼎鼎的Servlet又是什么东西呢,该怎么使用呢?在初学java时,socket编程是逃不掉的一章;虽然在实际业务项目中,使用这个的可能性基本为0,本篇博文将主要介绍如何使用socket来实现一个简单的http服务器功能,提供常见的get/post请求支持,并再此过程中了解下http协议

作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomcat的底层是怎么支持http服务的呢?大名鼎鼎的Servlet又是什么东西呢,该怎么使用呢?


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


I. Http服务器从0到1



既然我们的目标是借助socket来搭建http服务器,那么我们首先需要确认两点,一是如何使用socket;另一个则是http协议如何,怎么解析数据;下面分别进行说明


1. socket编程基础


我们这里主要是利用ServerSocket来绑定端口,提供tcp服务,基本使用姿势也比较简单,一般套路如下


  • 创建ServerSocket对象,绑定监听端口
  • 通过accept()方法监听客户端请求
  • 连接建立后,通过输入流读取客户端发送的请求信息
  • 通过输出流向客户端发送乡音信息
  • 关闭相关资源


对应的伪代码如下:

ServerSocket serverSocket = new ServerSocket(port, ip)
serverSocket.accept();
// 接收请求数据
socket.getInputStream();
// 返回数据给请求方
out = socket.getOutputStream()
out.print(xxx)
out.flush();;
// 关闭连接
socket.close()
复制代码


2. http协议


我们上面的ServerSocket走的是TCP协议,HTTP协议本身是在TCP协议之上的一层,对于我们创建http服务器而言,最需要关注的无非两点


  • 请求的数据怎么按照http的协议解析出来
  • 如何按照http协议,返回数据

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


请求消息

image.png

响应消息

image.png

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


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


  • 第一行:状态行
  • 第二行到第一个空行:header(请求头/相应头)
  • 剩下所有:正文


3. http服务器设计


接下来开始进入正题,基于socket创建一个http服务器,使用socket基本没啥太大的问题,我们需要额外关注以下几点


  • 对请求数据进行解析
  • 封装返回结果


a. 请求数据解析


我们从socket中拿到所有的数据,然后解析为对应的http请求,我们先定义个Request对象,内部保存一些基本的HTTP信息,接下来重点就是将socket中的所有数据都捞出来,封装为request对象


@Data
public static class Request {
    /**
     * 请求方法 GET/POST/PUT/DELETE/OPTION...
     */
    private String method;
    /**
     * 请求的uri
     */
    private String uri;
    /**
     * http版本
     */
    private String version;
    /**
     * 请求头
     */
    private Map<String, String> headers;
    /**
     * 请求参数相关
     */
    private String message;
}
复制代码


根据前面的http协议介绍,解析过程如下,我们先看请求行的解析过程


请求行,包含三个基本要素:请求方法 + URI + http版本,用空格进行分割,所以解析代码如下


/**
 * 根据标准的http协议,解析请求行
 *
 * @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, 具体实现如下


/**
 * 根据标准http协议,解析请求头
 *
 * @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);
}
复制代码


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


先看具体实现如下


/**
 * 根据标注http协议,解析正文
 *
 * @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[]来读取流中所有数据,如果我们的数组比实际的小,则读不完;如果大,则数组中会有一些空的数据;


最后将上面的几个解析封装一下,完成request解析

/**
 * http的请求可以分为三部分
 *
 * 第一行为请求行: 即 方法 + 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;
}
复制代码


b. 请求任务HttpTask


每个请求,单独分配一个任务来干这个事情,就是为了支持并发,对于ServerSocket而言,接收到了一个请求,那就创建一个HttpTask任务来实现http通信


那么这个httptask干啥呢?


  • 从请求中捞数据
  • 响应请求
  • 封装结果并返回
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();
            }
        }
    }
}
复制代码


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


@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());
}
复制代码


c. http服务搭建


前面的基本上把该干的事情都干了,剩下的就简单了,创建ServerSocket,绑定端口接收请求,我们在线程池中跑这个http服务


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();
                    }
                }
            }
        }
    }
}
复制代码


到这里,一个基于socket实现的http服务器基本上就搭建完了,接下来就可以进行测试了


4. 测试


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


一个完成的post请求如下

image.png

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

image.png


相关文章
|
6月前
|
C# 图形学 开发者
Unity开发中使用UnityWebRequest从HTTP服务器下载资源。
总之,UnityWebRequest就是游戏开发者手中的万能钓鱼竿,既可以获取文本数据,也能钓上图片资源,甚至是那声音的涟漪。使用UnityWebRequest的时候,你需要精心准备,比如确定URL、配置请求类型和头信息;发起请求;巧妙处理钓获的数据;还需要机智面对网络波澜,处理各种可能出现的错误。按照这样的过程,数据的钓取将会是一次既轻松愉快也效率高效的编程钓鱼之旅。
323 18
|
6月前
|
应用服务中间件 网络安全 数据安全/隐私保护
网关服务器配置指南:实现自动DHCP地址分配、HTTP服务和SSH无密码登录。
哇哈哈,道具都准备好了,咱们的魔术秀就要开始了。现在,你的网关服务器已经魔法满满,自动分配IP,提供网页服务,SSH登录如入无人之境。而整个世界,只会知道效果,不会知道是你在幕后操控一切。这就是真正的数字世界魔法师,随手拈来,手到擒来。
367 14
|
5月前
|
JSON 前端开发 Go
Go语言实战:创建一个简单的 HTTP 服务器
本篇是《Go语言101实战》系列之一,讲解如何使用Go构建基础HTTP服务器。涵盖Go语言并发优势、HTTP服务搭建、路由处理、日志记录及测试方法,助你掌握高性能Web服务开发核心技能。
|
5月前
|
Go
如何在Go语言的HTTP请求中设置使用代理服务器
当使用特定的代理时,在某些情况下可能需要认证信息,认证信息可以在代理URL中提供,格式通常是:
428 0
|
5月前
|
存储 人工智能 Java
java之通过Http下载文件
本文介绍了使用Java实现通过文件链接下载文件到本地的方法,主要涉及URL、HttpURLConnection及输入输出流的操作。
366 0
|
7月前
|
存储 数据库 Python
使用HTTP POST协议将本地压缩数据发送到服务器
总的来说,使用HTTP POST协议将本地压缩数据发送到服务器是一个涉及多个步骤的过程,包括创建压缩文件,设置HTTP客户端,发送POST请求,以及服务器端的处理。虽然这个过程可能看起来复杂,但一旦你理解了每个步骤,就会变得相对简单。
282 19
|
7月前
|
存储 安全 数据安全/隐私保护
HFS-快速创建HTTP服务器
鉴于HFS的操作简便和方便快捷,它在满足快速,临时的文件分享和传输需求上,能够发挥出巨大的作用。只要明确了以上的安全警告,并做好了必需的安全设置,HFS将是一款实用的HTTP服务器工具。
670 9
|
Web App开发 前端开发 Apache
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html><head><meta http-equiv="Cont
修改的ranger ui的admin用户登录密码时,需要在ranger的配置里把admin_password改成一样的,否则hdfs的namenode在使用admin时启动不起来,异常如下: Traceback (mos...
1141 0
|
Web App开发 前端开发 Android开发
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html><head><meta http-equiv="Cont
使用MAT分析内存泄露 对于大型服务端应用程序来说,有些内存泄露问题很难在测试阶段发现,此时就需要分析JVM Heap Dump文件来找出问题。
929 0
|
Web App开发 监控 前端开发
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html><head><meta http-equiv="Cont
系统的升级涉及各个架构组件,细节很多。常年累月的修修补补使老系统积累了很多问题。 系统升级则意味着需要repair之前埋下的雷,那为何还要升级,可以考虑以下几个方面 成熟老系统常见问题: 1. 缺乏文档(这应该是大小公司都存在的问题。
712 0