使用javasocket手撸一个http服务器(代码片段)

一灰灰Blog 一灰灰Blog     2022-11-24     296

关键词:

原文连接:使用Java Socket手撸一个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协议,返回数据

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

请求消息

技术分享图片

响应消息

技术分享图片

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

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

  • 第一行:状态行
  • 第二行到第一个空行: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("
");


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("
");
    
    stringBuilder.append("
");


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请求如下

技术分享图片

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

技术分享图片

II. 其他

0. 项目源码

  • quick-fix
  • 相关代码:
    • com.git.hui.fix.core.endpoint.BasicHttpServer
    • com.git.hui.fix.core.endpoint.HttpMessageParser
    • com.git.hui.fix.core.endpoint.HttpTask

1. 一灰灰Blog: https://liuyueyi.github.io/hexblog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

一灰灰blog

技术分享图片

知识星球

技术分享图片

使用node.js手撸一个建静态web服务器,内部cv指南(代码片段)

...说这个键盘真漂亮~~文章目录使用Node.js手撸一个建静态Web服务器一、动静态服务器的概念1.1静态Web服务器概念1.2静态Web服务器的优点1.3快速搭建的途径二、手撸指南2.1框架搭建2.2Ctrl+C/V2.3启动服务器2.4部署服务器(简单过... 查看详情

手撸一个orm使用说明(代码片段)

传送门【手撸一个ORM】第一步、约定和实体描述【手撸一个ORM】第二步、封装实体描述和实体属性描述【手撸一个ORM】第三步、SQL语句构造器和SqlParameter封装【手撸一个ORM】第四步、Expression(表达式目录树)扩展【手撸一个ORM... 查看详情

轻而易举的手撸了一个http图片资源服务器!

...开发一直是行业热门技术,而要做web程序就离不开http服务器,现在主流的http服务器用的最广的如tomcat,apache。还有商用版本的各式各样的http服务器,而再行业类各种微服务,各种web开发技术层出不穷,都... 查看详情

javasocket编程

申明:本文摘自:http://www.cnblogs.com/rocomp/p/4790340.htmlJava最初是作为网络编程语言出现的,其对网络提供了高度的支持,使得客户端和服务器的沟通变成了现实,而在网络编程中,使用最多的就是Socket。像大家熟悉的QQ、MSN都使用... 查看详情

javasocket编程简单的web服务器

简单的WEB服务器一个简单的WEB服务器将由列表9.2这样构建.当然,还必须要对方法和回应事件进行改进.简单的服务器不会分析和存储请求头.新的WEB服务器将分析和存储请求,为以后的处理作准备.为了达到这个目的,你必须有一个包... 查看详情

javasocket实现简单在线聊天

出处:http://blog.csdn.net/tuzongxun最近的项目有一个在线网页交流的需求,由于很久以前做过的demo已经忘记的差不多了,因此便重新学习一下。 我计划的大致实现步骤分这样几大步:1、使用awt组件和socket实现简单的单客户端向... 查看详情

javasocket编程----通信是这样炼成的

http://www.cnblogs.com/rocomp/p/4790340.htmlJava最初是作为网络编程语言出现的,其对网络提供了高度的支持,使得客户端和服务器的沟通变成了现实,而在网络编程中,使用最多的就是Socket。像大家熟悉的QQ、MSN都使用了Socket相关的技术... 查看详情

javasocket基本使用

首先编写Client程序:1importjava.io.IOException;2importjava.net.Socket;3importjava.util.Scanner;45publicclassClient{6publicstaticvoidmain(String[]args)throwsIOException{7/**8*创建Socket9*接收服务器端发送数据10*/11Sockets 查看详情

手撸一个mvc框架有多简单(代码片段)

文章目录手撸一个mvc框架有多简单手撸一个mvc框架有多简单在web配置类中定义一个处理前端请求的servletweb.xml配置<?xmlversion="1.0"encoding="UTF-8"?><web-appxmlns="http://xmlns.jcp.org/xml/ns/javaee"xmln 查看详情

javasocket+多线程实现控制台聊天室

转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/5827212.html  聊天室程序的结构图: 架构解释:  Server服务器相当于一个中转站,Client客户端程序传送信息到服务器,服务器再把信息分发到其他客户端上,实现即... 查看详情

javasocket编程重复和并发服务器

重复和并发服务器这个应用程序被当作一个重复的服务器.因为它只有在处理完一个进程以后才会接受另一个连接.更多的复杂服务器是并发的.它为每一个请求分配一个线程,而不是来一个处理一个.所以看起来它在同时处理多人请... 查看详情

javasocket怎么与websocket对接

...如何开发一个websocket协议的服务端程序与js前端对接!因为javasocket不是一种协议,他是tcp的实现,而websocket则是一种协议(类似与HTTP,但与其不完全相同).至于实现方式,在tomcat7中的自带的javax打头的jar包中,就有对于websocket的支持,如果... 查看详情

javasocket编程学习笔记

 1.Socket通信简介及模型  JavaSocket可实现客户端--服务器间的双向实时通信。java.net包中定义的两个类socket和ServerSocket,分别用来实现双向连接的client和server端。2.Socket通信实现方法 2.1 服务器端(非多线程)   用... 查看详情

javasocket编程学习笔记

在上一篇中,使用了javaSocket+Tcp/IP 协议来实现应用程序或客户端--服务器间的实时双向通信,本篇中,将使用UDP协议来实现Socket的通信。1.关于UDP  UDP协议(用户数据报协议)是无连接的、不可靠的、无序的,速度快,进行... 查看详情

javasocket通讯

本来是打算验证javasocket是不是单线程操作,也就是一次只能处理一个请求,处理完之后才能继续处理下一个请求。但是在其中又发现了许多问题,在编程的时候需要十分注意,今天就拿出来跟大家分享一下。首先先建立一个服... 查看详情

javasocket一对多通信如何实现?

现在有一个服务器端,多个客户端,每当一个客户端接入服务器端的时候,服务器端就建立一个线程。现在问题,当服务器端发送消息的时候,只有一个客户端接受到了信息,其他连接到的客户端没有反映。参考技术A【server端... 查看详情

你只会用javasockets?推荐11个开源的javasocket框架

来源:csdn.net/xiaojin21cen/article/details/78587425ZeroCICE的Java版,Netty2作者的后续之作ApacheMINA,Crmky的Cindy之外,还有个超简单的QuickServer,让你专心编写自己的业务代码,不用编写一行TCP代码。1、QuickServer一个免... 查看详情

关于javasocket的消息推送问题

...这些线程在后台管理员添加消息的时候去推送消息呢关于javasocket的消息推送问题,首先:开启服务端,并暴露出端口。然后通过一个while的死循环去不停的接收来自客户端的socket,并且通过一个ArrayList来维护。并且通过子线程去... 查看详情