自己实现简易版本的Http服务器
程序员文章站
2022-05-04 18:21:08
...
1.模块设计:
Http服务器需要解析来自浏览器端的数据,因此我们需要设计两个类。
(1)Request类,负责保存浏览器端的信息;
(2) HttpServer 类,负责处理浏览器端的信息
2.详细设计
主要解析三部分的请求数据:
(1)请求行
(2)请求报头
(3)请求正文
其中请求行包括三部分:请求方法,url,版本号。
请求报头可以解析到哈希表中
请求正文也需要解析到哈希表中
这里我们设置端口号是9999;
服务器端的 url 设置了五种,分别为:
(1)login.html:是一个登录页面。用户需要输入账号和密码。
(2)login:用户登录成功后跳转的页面。登录成功后,服务器端会保存用户信息,并且生成一个 sessionId,并把该 sessionId 返回给浏览器端,此后浏览器再访问服务器都会携带该 sessionId
(3)302:此处我们重定向到 www.baidu.com
(4)getCookie:浏览器端向服务器端请求时会携带之前的sessionId,此时服务器会检验此 sessionId 有没有与之对应的用户名,如果存在,那么此用户就可以进入此网站;否则不可以进入
(5)setCookie:可以根据原来的 sessionId 找到对应的用户,并且为该用户添加一个新的 sessionId
参考代码如下:
** HttpServer 类:解析客户端信息并响应相关操作**
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HttpServer {
// 端口号
private static final int PORT = 9999;
// 获取处理器核数
private static final int COUNT = Runtime.getRuntime().availableProcessors();
// 处理的任务量和线程数量、CPU、内存等资源都相关
// 一般推荐使用跟处理器核数数量相等的线程数
private static final ExecutorService EXE =
Executors.newFixedThreadPool(COUNT);
public static Map<String, String> SESSION = new HashMap<>();
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(PORT);
// 一个 socket 对象代表一个客户端
while (true) {
// 获取客户端请求 socket 对象:阻塞方法
Socket socket = server.accept();
EXE.submit(new HttpTask(socket));
}
}
}
// Http请求任务处理类
class HttpTask implements Runnable {
private Socket socket;
// 构造方法
public HttpTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
// 获取客户端请求数据:输入流
InputStream is = null;
InputStreamReader isr = null;
BufferedReader br = null;
// 获取客户端输出流,返回响应数据
OutputStream os = null;
PrintWriter pw = null;
try {
try {
is = socket.getInputStream();
// 响应输出数据
// 将原生的字节流转换成字符流,方便处理
isr = new InputStreamReader(is, StandardCharsets.UTF_8);
// 转换流放在缓冲流中读取效率比较高
br = new BufferedReader(isr);
Request request = new Request();
// 请求数据的解析:http协议报的解包
// 1.解析请求行(第一行): Method URL Version
String requestLine = br.readLine();
// 根据 " " 把请求行分成三部分
String[] requestLines = requestLine.split(" ");
// 添加 method
request.setMethod(requestLines[0]);
// 可能为 http://localhost:9999/xxx?uername=stu&passord=123
String url = requestLines[1];
// 当 method 为 get 时可能需要解析请求正文
if (url.contains("?")) {
String parameters = url.substring(url.indexOf("?") + 1);
request.parseParamaters(parameters);
url = url.substring(0, url.indexOf("?"));
}
// 添加 url
request.setUrl(url);
// 添加 version
request.setVersion(requestLines[2]);
// 2.解析请求头
// key : value 每个换行,以空行作为结尾
String header;
while ((header = br.readLine()) != null && header.length() != 0) {
// 第一个条件保证请求头还没有结束,
// 第二个条件保证读取到的元素不是空行
String key = header.substring(0, header.indexOf(":"));
String value = header.substring(header.indexOf(":") + 1);
request.addHeader(key, value.trim());
}
// POST请求,需要根据请求头 Content-Length 获取请求体的长度
if ("POST".equals(request.getMethod())) {
String len = request.getHeader("Content-Length");
if (len != null) {
// 将 String 类型转换为 int 类型
int l = Integer.parseInt(len);
char[] chars = new char[l];
br.read(chars, 0, l);
// 请求参数格式:username=stu&password=123
String s = new String(chars);
//String requestBody = Arrays.toString(chars);
//System.out.println(requestBody + "hehe");
//request.parseParamaters(requestBody);
request.parseParamaters(s);
//String[] ps = s.split("&");
// for (String p : ps) {
// String[] array = p.split("=");
// request.addParameter(array[0], array[1]);
// System.out.println(array[0] + "### " + array[1]);
// }
//System.out.println("req body==="+requestBody);
}
}
System.out.println(request);
os = socket.getOutputStream();
pw = new PrintWriter(os, true);
// http://localhost:9999/302/111
if ("/302".equals(request.getUrl())) {
pw.println("HTTP/1.1 302 重定向");
pw.println("Content-Type: text/html;charset=utf-8");
pw.println("Location: https://www.baidu.com");
} else if ("/login".equals(request.getUrl())) {
pw.println("HTTP/1.1 200 OK");
String username = request.getParameter("username");
String sessionId = UUID.randomUUID().toString();
// 在服务器保存生成的 sessionId 和 username 对应的哈希表
HttpServer.SESSION.put(sessionId, username);
pw.println("Set-Cookie: SESSIONID=" + sessionId);
pw.println("Content-Type: text/html; charset=utf-8");
String content= "<h2>欢迎用户[" + request.getParameter("username") + "]登录</h2>";
pw.println("Content-Length: "+content.getBytes().length);
pw.println();
pw.println(content);
} else if ("/setCookie".equals(request.getUrl())) {
pw.println("HTTP/1.1 200 OK");
String sessionId = UUID.randomUUID().toString();
// 根据原来的 Cookie 值找到对应的 value 即 username
String oldCookie = request.getHeader("Cookie");
String[] arr = oldCookie.split("=");
String old = arr[1];
String name = HttpServer.SESSION.get(old);
HttpServer.SESSION.put(sessionId, name);
pw.println("Set-Cookie: SESSIONID=" + sessionId);
pw.println("Content-Type: text/html; charset=utf-8");
String content = "<h2>sseion重置成功!</h2>";
pw.println("Content-Length: "+content.getBytes().length);
pw.println();
pw.println(content);
} else if ("/getCookie".equals(request.getUrl())) {
String cookie = request.getHeader("Cookie");
String[] cookies = cookie.split(";");
for (String s : cookies) {
String[] s2 = s.split("=");
String key = s2[0].trim();
String value = s2[1].trim();
if (key.equals("SESSIONID") && HttpServer.SESSION.containsKey(value)) {
pw.println("HTTP/1.1 200 OK");
pw.println("Content-Type: text/html; charset=utf-8");
pw.println();
pw.println("<h2>用户" + HttpServer.SESSION.get(value) + "能够访问</h2>");
return;
}
}
pw.println("HTTP/1.1 403 Forbidden");
pw.println("Content-Type: text/html; charset=utf-8");
pw.println();
pw.println("没有访问权限");
} else {
// 访问 /login.html,转化为访问./login.html
// 其中 ./ 表示当前目录下的文件
InputStream htmlIs = HttpServer.class.getClassLoader()
.getResourceAsStream("." + request.getUrl());
if (htmlIs != null) {
pw.println("HTTP/1.1 200 OK");
pw.println("Content-Type: text/html; charset=utf-8");
pw.println();
// 返回 webapp 下的静态资源文件内容
InputStreamReader htmlIsr = new InputStreamReader(htmlIs);
BufferedReader htmlBr = new BufferedReader(htmlIsr);
String content;
while ((content = htmlBr.readLine()) != null) {
pw.println(content);
}
} else {
// 返回 404
pw.println("HTTP/1.1 404 Not Found");
pw.println("Content-Type: text/html; charset=utf-8");
pw.println();
pw.println("<h2>找不到资源</h2>");
}
}
} catch (IOException e) {
e.printStackTrace();
}
finally {
// 关闭流:反向关闭
if (br != null) {
br.close();
}
if (isr != null) {
isr.close();
}
if (is != null) {
is.close();
}
if (pw != null) {
pw.close();
}
if (os != null) {
os.close();
}
// 关闭客户端连接(短连接)
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
用户登录界面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<h3>登录页面</h3>
<form method="post" action="login">
用户名:<input type="text" name="username" />
<br> <!-- br代表换行符 -->
密码:<input type="password" name="password"/>
<input type="submit" value="登录" />
</form>
</body>
</html>
Request 类:保存,获取用户相关信息
import java.util.HashMap;
import java.util.Map;
public class Request {
// 请求方法
private String method;
// 请求路径
private String url;
// http版本号
private String version;
// 请求头
private Map<String, String> heads = new HashMap<>();
// 请求参数
private Map<String, String> parameters = new HashMap<>();
// 添加请求头
public void addHeader(String key, String value) {
heads.put(key, value);
}
// 获取某个请求头
public String getHeader(String key) {
return heads.get(key);
}
// 解析请求参数:key1=value1&key2=value2
public void parseParamaters(String parameters) {
String[] ps = parameters.split("&");
for (String p : ps) {
String[] array = p.split("=");
addParameter(array[0], array[1]);
}
// addParameter(ps[0],ps[1]);
}
// 添加请求参数
public void addParameter(String key, String value) {
parameters.put(key, value);
}
// 获取请求参数
public String getParameter(String key) {
return parameters.get(key);
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public Map<String, String> getHeads() {
return heads;
}
public void setHeads(Map<String, String> heads) {
this.heads = heads;
}
public Map<String, String> getParameters() {
return parameters;
}
public void setParameters(Map<String, String> parameters) {
this.parameters = parameters;
}
@Override
public String toString() {
return "Request{" +
"\n method='" + method + '\'' +
", \n url='" + url + '\'' +
", \n version='" + version + '\'' +
", \n heads=" + heads +
", \n parameters=" + parameters +
'}';
}
}
结果展示
(1)login.html :用户登录
(2)login:用户进入
(3)302:重定向
(4)getCookie:用户是否有权限进入
(5)setCookie:给用户添加一个新的 sessionId
推荐阅读
-
在自己的服务器上部署 GitLab 社区版及使用 博客分类: 开发(版本)环境工具 gitlabgit
-
Spring Boot实现Undertow服务器同时支持HTTP2、HTTPS的方法
-
Spring Boot实现Undertow服务器同时支持HTTP2、HTTPS的方法
-
PHP实现基于Swoole简单的HTTP服务器
-
基于HTTP长连接的"服务器推"技术的php 简易聊天室
-
基于HTTP长连接的"服务器推"技术的php 简易聊天室_php实例
-
[服务器] 用Servlet搭建自己的HTTP服务|后台向前端传输文件|Java文件传输
-
基于C#实现一个最简单的HTTP服务器实例
-
基于C#实现一个最简单的HTTP服务器实例
-
NodeJS搭建HTTP服务器的实现步骤