HTTP学习(5)--demo编写(1)

2023-05-16

一个基于Java的HTTP服务器demo

前面几篇博客,大致介绍了几个方面的例子,例如报文解析、报文组装等。现在打算将这些东西集合起来,编写一个小HTTP服务器demo。期间遇到了很多问题,也解决了很多问题,有些不好或者无法解决的问题,也就妥协了一点,采用硬编码的方式来解决了。哈哈!
代码写的不是很好,毕竟我是正在学习的。这个程序可以保证,在正确的操作下,访问还是正常的。因为HTTP服务器通常是很复杂的,我这里是学习初期,一切都只是能让程序运行起来就行了。(可用性)

HTTP学习(4)–违反协议的错误

基本功能介绍

demo基本功能

我主要期待它具有以下功能:查看所有图片上传图片查看单张图片
上面几个功能正好涉及到了:报文解析报文组装接收请求响应请求的功能

web的路由为:
图片页:/ 和 /index.mmp
上传页:/upload.mmp
上传处理:/upload
web图标:/favicon.ico
查看图片页:/pictures
未知路由:

demo结构

本demo采用基本的Java工程,使用任何Java IDE都可以,无外部依赖jar包。
在这里插入图片描述

效果演示

演示GIF

在这里插入图片描述

demo图片路径

当demo启动后,会读取这个路径中的文件,并使用一个静态 Map 记录其文件名。当用户查找某一个图片时,系统会将图片响应给用户。当用户上传图片后,静态 Map 同样也会记录图片的名称,返回 index 页面时,也会同样显示新上传的图片。
在这里插入图片描述

代码部分

这个demo可以看作是一个HTTP文件服务器了,虽然它很简单,也不够完善。编码过程中有很多的硬编码,或者是不通用的方式。但是它作为我们了解、学习 HTTP 是足够了。

DragonServerBoot类

说明: 启动服务的主类。

package com.dragon.server;

public class DragonServerBoot {
	public static void main(String[] args) {
		int port = 9999;
		DragonServer server = new DragonServer(port);
		server.start();
	}
}

DragonServer类

说明: 提供服务的类。这里通过一个静态的线程安全的 Map 集合来存储数据。 因为我需要的数据量很少,最多几十张图片就足够了,我也没想真用它来搭建服务。所以,只用一个 Map 就可以了,不需要使用数据库这种重量级的软件了。

注意: 这里处理四种类型的图片:.jpg .jpeg .gif .png。并且这里类型是大小写敏感的,这不是关键问题,我也就简单处理了。

package com.dragon.server;

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DragonServer {
	private int port;
	private static ServerSocket server;
	public static Map<String, String> picMap = Collections.synchronizedMap(new HashMap<>());  //线程安全的map
	
	//使用线程安全的 map 来存储图片的路径
	static {
		//存放图片的路径
		Path path = Paths.get("D:", "DragonFile", "ServerImg");
		File picDir = path.toFile();
		File[] files = picDir.listFiles((dir, f)->f.contains(".jpg") || f.contains(".jpeg") || f.contains(".gif") || f.contains(".png"));
		for (File file : files) {
			String filename = file.getName();
			picMap.put(filename, filename);
		}
	}
	
	public DragonServer(int port) {
		this.port = port;
	}
	
	/**
	 * 启动服务
	 * */
	public void start() {
	    try {
	    	server = new ServerSocket(port);
	    	System.out.println("服务启动成功...");
	    	this.receiveRequest();
	    } catch (IOException e) {
	    	e.printStackTrace();
	    	System.out.println("服务启动失败!");
	    }
	}
	
	/**
	 * 接收请求
	 * */
	public void receiveRequest() {
		ExecutorService pool = Executors.newFixedThreadPool(10);
		while (true) {
			try {
				Socket client = server.accept();
				System.out.println("用户"+client.getInetAddress().toString()+"建立连接" + client.toString());
				pool.submit(new Connection(client));  //使用线程处理每一个请求。
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * 停止服务
	 * 注:一般是不需要关闭服务的。
	 * */
	public void stop() {
		try {
			if (server != null) {
				server.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("服务器关闭失败!");
		}
	}
}

Request类

说明: Request类用于解析用户请求报文,Response类用于组装响应报文,并发送给用户。
前面的博客里面已经介绍过了这两个类的大致功能。

前面的博客里面已经介绍了 Request 类的大致功能,这里只是把它拿出来作为一个单独的类进行使用。并且多了一个方法 private void splitHeader(String header) 来解析具体的信息,这里主要来说一下它的功能:
首先请求报文的第一行是请求行,它的结构和下面的头部是不同的,所以使用一个标志位 flag 来单独处理。
请求行的结构:
GET /pictures/d18d2a1d-051e-4692-83f4-8d6b8deb239c.png HTTP/1.1
POST /upload HTTP/1.1

第一个参数是请求方法,第二个参数是请求路径和请求参数(GET方式),第三个是协议的版本。

注意:GET方式请求路径和请求参数是在 一起的,POST方式请求路径在第一行,请求参数是在报文体部分。

说明: 这里的处理很是麻烦,因为我也是第一次这样做。我这个demo的功能很简单,所以只有查看图片的GET方式是带有请求参数的,其它的GET方式是没有请求参数,所以他们的请求路径和参数就是请求路径了。(/ 和 /index.mmp /upload.mmp 方式
所以我就对查看图片的方式进行特殊处理,如果路径中含有 /pictures,那么这个路径就是查看图片的了,并且假定它的格式一定为 /pictures/jsdjfksl.jpg 这种形式,否则程序也是处理不了的。(因为我确实是没有接触过这些东西的处理方式,所以这里是一种妥协的处理方式,只要系统运行起来就行了,哈哈!)

if (flag) {       //请求行
	String[] requestLine = header.split("\\s");  //以空格来分割字符串
	this.method = requestLine[0];    //不考虑大小写了
	if  ("POST".equalsIgnoreCase(this.method)) {
		this.path = requestLine[1];
	} else {
		//解析get方式的请求行和请求参数  get方式包含请求路径和请求参数
		if (requestLine[1].contains("/pictures")) {
			int pos = requestLine[1].lastIndexOf("/");
			this.path = requestLine[1].substring(0, pos);
			paramsMap.put("filename", requestLine[1].substring(pos+1));
		}
		else {
			// 路径 / /index.mmp /upload.mmp 都没有参数
			this.path = requestLine[1];
		}
	}
package com.dragon.server;

import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * 处理请求类
 * */
public class Request {
	private InputStream in; //输入流
	private boolean flag = true;   // 设置一个标志位,如果 flag 为 true,代表读取的是请求行,否则是请求头的其它部分。
	private String method;  //请求方法
	private String path;    //请求路径
	private Map<String, String> paramsMap = new HashMap<>();   //请求参数
	
	public Request(InputStream in) {
		this.in = new BufferedInputStream(in);
	}
	
	/**
	 * 解析报文,主要关注一下几点,其它不考虑:
	 * method 
	 * path
	 * params
	 * Content-Type
	 * Content-Length
	 * 
	 * 报文的类型也简化一下,只有 GET 方式请求 html 和 picture 
	 * 和 POST 方式上传文件
	 * */
	public void resolveRequest() {
		try {
			/**
			 * 当取出来的行为空时,意味着已经取出了所有的头部。
			 * 下面就是数据部分了(这里考虑的是简单的报文,非Multip-part类型的报文)
			 * */
			String header = null;
			do {
				header = this.getHeader();  //读取一行请求头
				if ("".equals(header)) break; 
				this.splitHeader(header);   //分割请求头
				System.out.println("header:-----> " + header);
			} while (true);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	//封装一个 getHeader 方法,每次取一个头部行,参考 getLine 方法。
	private String getHeader() throws IOException {
		StringBuilder header = new StringBuilder();
		while (true) {
			int c = in.read();
			if (c == '\n' || c == -1) break;
			header.append((char)c);
		}
		return header.toString().trim();  //取出空格字符
	}
	
	private void splitHeader(String header) {
		System.out.println();
		if (flag) {       //请求行
			String[] requestLine = header.split("\\s");  //以空格来分割字符串
			this.method = requestLine[0];    //不考虑大小写了
			if  ("POST".equalsIgnoreCase(this.method)) {
				this.path = requestLine[1];
			} else {
				//解析get方式的请求行和请求参数  get方式包含请求路径和请求参数
				if (requestLine[1].contains("/pictures")) {
					int pos = requestLine[1].lastIndexOf("/");
					this.path = requestLine[1].substring(0, pos);
					paramsMap.put("filename", requestLine[1].substring(pos+1));
				}
				else {
					// 路径 / /index.mmp /upload.mmp 都没有参数
					this.path = requestLine[1];
				}
			}
			flag = false;  //下次解析就不是请求行
		} else {
			String[] headLine = header.split(":", 2);
			paramsMap.put(headLine[0].trim(), headLine[1].trim());
		}	
	}
	
	public String getMethod() {
		return this.method;
	}
	public String getPath() {
		return this.path;
	}
	//根据特定的键取出请求参数
	public String getParam(String param) {
		return paramsMap.get(param);
	}
	public InputStream getInputStream() {
		return this.in;
	}
}

Response类

这个类主要是用于组装报文的,然后使用对象的 out 属性,发送响应报文 。前面的博客已经大致介绍了它的功能,这里只是把它作为一个单独的类拿来使用了。

package com.dragon.server;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

/**
 * 处理响应
 * */
public class Response {
	private static final String BLANK = " ";     //空白符 拼接报文使用
	private static final String CRLF = "\r\n";   //换行回车符 拼接报文使用
	private OutputStream out;                    //输出响应体的输出流
	
	public Response(OutputStream out) {
		this.out = out;
	}

	/**
	 * 只使用字节流来输出响应体。
	 * */
	public OutputStream getOutputStream() {
		return this.out;
	}
	
	/**
	 * 响应头
	 * 这里同样是简化处理,对于 contentType 来说,只有文本和图片两类。
	 * */
	public byte[] getHeader(String contentType, long length, int statusCode, String phrase) {
		return new StringBuilder()
				.append("HTTP/1.0").append(BLANK).append(statusCode).append(BLANK).append(phrase).append(CRLF)  // 响应头部
				.append("Server:"+"CrazyDragon").append(CRLF)
				.append("Date:").append(BLANK).append(this.getDate()).append(CRLF)
				.append("Content-Type:").append(BLANK).append(contentType).append(CRLF) //文件的 Content-Type 可通过Java获取。
				.append("Content-Length:").append(BLANK).append(length).append(CRLF).append(CRLF)
				.toString()
				.getBytes(Charset.forName("UTF-8"));
	}
	
	//获取时间
	private String getDate() {
		Date date = new Date();
		SimpleDateFormat format = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
		format.setTimeZone(TimeZone.getTimeZone("GMT")); // 设置时区为GMT  
		return format.format(date);
	}
	
	//关闭响应:关闭连接和关闭流的效果是一样的
	public void close() {
		try {
			if (out != null) {
				out.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

Connection类

处理路由

在用户请求到来时,会首先使用Request类来解析请求报文的头部,剩下数据部分等待进一步处理(如果请求包含数据部分的话)。
不同的用户请求对应着不同的处理方式,相信具有web经验的人都是了解的。所以这里需要一个路由方式来处理用户的请求。 我这里就考虑使用简单的switch来完成这个功能了。(毕竟我的功能其实很简单,太复杂了我也不知道是怎么做的了。)

package com.dragon.server;

import java.io.IOException;
import java.net.Socket;

public class Connection implements Runnable {
	private Socket client;
	
	public Connection(Socket client) {
		this.client = client;
	}
	
	@Override
	public void run() {
		//请求分发:/ /index.mmp /upload.mmp /img/pic.jpg /unknown
		try {
			Request request = new Request(client.getInputStream());  //创建请求对象,获取请求信息
			request.resolveRequest();   //解析请求,不然程序会无法继续进行
			Response response = new Response(client.getOutputStream()); //创建响应对象,执行响应信息
			System.out.println("请求路径: " + request.getPath());
			switch (request.getPath()) {
			case "/":                         
			case "/index.mmp":                 //根路径
				Index index = new Index(request, response);
				index.doGet();
				break;
			case "/favicon.ico": 
				Favicon favicon = new Favicon(request, response);
				favicon.doGet();
				break;
			case "/upload.mmp":           //上传页面 不是上传处理类
				UploadPage uploadPage = new UploadPage(request, response);
				uploadPage.doGet();
				break;
			case "/upload":                //上传处理类 不是 上传页面
				PicUpload picUpload = new PicUpload(request, response);
				picUpload.doGet();
				break;         
			case "/pictures":
				Pictures pictures = new Pictures(request, response);
				pictures.doGet();
				break;           //查看图片页面
			default: 
				NotFound notFound = new NotFound(request, response);
				notFound.doGet();
				break;                    //非法请求路径,返回404
			}
		} catch(IOException e) {
			e.printStackTrace();
		}
	}
}

说明

前面介绍了基本的几个类的作用,下一篇博客会介绍具体实现功能的类。如果都写着一起的话,太长了,估计很多人都看不下去,所以我再写一篇博客继续上面未完的内容。

HTTP学习(5)–demo编写(2)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

HTTP学习(5)--demo编写(1) 的相关文章

  • 使用PHP获取http url参数而不自动解码

    我有一个像这样的网址 test php x hello world y 00h 00e 00l 00l 00o 当我将它写入文件时 file put contents x txt GET x gt hello world file put
  • 摆脱浏览器控制台中的 401(未经授权)ajax 错误

    我正在使用 javascript 通过 api 调用jQuery ajax http api jquery com jQuery ajax 称呼 如果用户未经过身份验证 API 会响应 401 并且我只想针对此调用忽略此错误 我已经尝试了
  • TRESTRequest:是否可以在 POST 请求中使用自定义媒体类型?

    例如 我们有一个 API 需要我们自己的供应商特定内容类型application vnd xxxx custom custom data json但查看 REST Client 的源代码 它似乎总是默认为 REST Types 中的 Con
  • 把自己限制在HTTP1.0有什么用吗?

    我负责构建一些工具来帮助最终用户测试为什么他们的浏览器可能无法与网站配合使用 我被告知它可能不起作用的原因之一是 需要 HTTP1 1 这一行 我浏览了大多数浏览器选项 只浏览了 IE 版本 6 及更高版本 even 9 允许您禁用 HTT
  • Express.js在控制器中获取http方法

    我正在构建一个注册表单 本地护照作为身份验证 表单作为表单助手 因为注册只知道 GET 和 POST 我想在一个函数中完成整个处理 换句话说 我正在寻找类似的东西 exports register function req res if r
  • 从开放的 HTTP 流中读取数据

    我正在尝试使用 NET WebRequest WebResponse 类来访问 Twitter 流 API 此处 http stream twitter com spritzer json 我需要能够打开连接并从打开的连接中增量读取数据 目
  • C# - 如何进行 HTTP 调用

    我想对网站进行 HTTP 调用 我只需要点击 URL 不想上传或下载任何数据 最简单 最快的方法是什么 我尝试了下面的代码 但它很慢 并且在第二次重复请求后 它只是超时 59 秒 然后恢复 WebRequest webRequest Web
  • 为什么使用 HTTP 动词?

    因为动词的目标是像 server domain getallrecords 或 server domain delete1record 或类似的 URL 而getallrecords delete1record都是专门为特定目的而设计的 为
  • 是否有用于通过 HTTP、HTTP 隧道发送二进制数据的 Java 库?

    我想通过 HTTP 以二进制格式发送相当大的数据块 也称为HTTP 隧道 http en wikipedia org wiki HTTP tunnel 我想通过 Java 将这种技术用于一些 Java Swing 应用程序 也可能是 And
  • 减少1000张图片的HTTP请求?

    我知道这个问题可能听起来有点疯狂 但我想也许有人会想出一个聪明的主意 假设您在一个 HTML 页面上有 1000 个缩略图 图像大小约为5 10 kb 有没有办法在单个请求中加载所有图像 以某种方式将所有图像压缩到一个文件中 或者您对该主题
  • asp.net core http 如果没有内容类型标头,则删除 `FromBody` 忽略

    我在 http 中使用 bodyDELETE要求 我知道目前删除主体是非标准的 但是允许的 使用时出现问题HttpClient它不允许删除请求的正文 我知道我可以使用SendAsync 但我宁愿让我的 API 更加灵活 我希望这个机构是可选
  • Go中如何自定义http.Client或http.Transport超时重试?

    我想实现一个自定义http Transport对于标准http Client 如果客户端超时 它将自动重试 附 由于某种原因 习俗http Transport is a 一定有 我已经查过了hashcorp go retryablehttp
  • 通过 HTTPS 加载页面但请求不安全的 XMLHttpRequest 端点

    我有一个页面 上面有一些 D3 javascript 该页面位于 HTTPS 网站内 但证书是自签名的 当我加载页面时 我的 D3 可视化效果不显示 并且出现错误 混合内容 页面位于 https integration jsite com
  • 按照约定应返回哪些 REST PUT/POST/DELETE 调用?

    根据 REST 意识形态 PUT POST DELETE 请求的响应正文中应该包含什么 返回码呢 是HTTP OK enough 如果有的话 这种约定的原因是什么 我发现了一篇描述 POST PUT 差异的好文章 发布与放置 http ww
  • iOS 上的多个 HTTP 请求与单个 TCP 连接

    我正在开发一个 iPhone 应用程序 它使用我控制的基于 Web 的 API 连接到持续打开的 TCP 端口并通过 TCP API 发出请求 或者为我想要获取的所有数据发出新的 HTTP 请求 会更快或更高效吗 我认为差异可以忽略不计 但
  • 防止 ASP.Net 中的表单重新提交(不重定向到我自己)

    我有一个带有表单元素的母版页
  • ASP.NET Core URL 重写

    我正在尝试将我的网站从 www 重定向到非 www 规则以及 http 到 https https example com https example com 在中间件中 我曾经在 web config 中进行这些重定向更改 例如
  • RestSharp RestClient的默认超时值是多少?

    任何人都知道默认超时值休息锐利 https github com restsharp 休息客户端 RestSharp 在底层使用 HttpWebRequest 它有一个默认超时 https msdn microsoft com en us
  • Android - API 请求

    我开发了一个应用程序 它也在 iPhone 上 问题出在 api 请求上 我为所有请求设置了超时 有时会出现 30 60 秒的中断 看起来这个应用程序执行了几个请求 然后就中断了 一直超时 大约 45 秒后一切正常 不知道是服务器问题还是安
  • 诸如用于测试 HTTP 请求的虚拟 REST 服务器之类的东西? [关闭]

    Closed 此问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我一直在四处寻找 但找不到任何这样的网站 我想知道是否有一些虚拟服务器可以响应测试 GET 请求并返回

随机推荐