1.手写一个HTTP服务器
2.手写一个博客系统运行在HTTP服务器上
Windows10+IDEA+JDK1.8+Maven+MySQL
1.Http请求解析
2.Http响应
3.静态Controller处理
4.自定义类加载器
5.开启服务器Server类
6.博客代码
博客提交页面
博客列表页面
博客文章详情
首先编写代码进行解析请求
请求包含以下部分
请求行 GET /aaa.html?id=1&password=ppp&name=10 HTTP/1.1
请求头 Host: www.aaa.com Accept:text/html等信息
请求体 请求体(get方法没有请求体)
特殊格式的请求 “GET /hello?target=%E4%B8%AD%E5%9B%BD HTTP/1.0”
解析请求行
请求的方法method
请求的参数urlall,请求的参数
请求的版本号protocol
解析请求头
请求头Host:网址
请求头Accept:text/html
请求的编码
1.首先定义请求类保存请求行请求头参数
2.在解析请求头的时候判断是post还是get方法
3.进行分步解析
package com.github7.request;
import java.io.*;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
public class Request {
String url;
String protocol;
String method;
Map headers = new HashMap<>();
Map requestParam = new HashMap<>();
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public void setHeaders(String key, String value) {
this.headers.put(key, value);
}
public void setRequestParam(String key, String value) {
this.requestParam.put(key, value);
}
public void setMethod(String method) throws IOException {
this.method = method.toUpperCase();
if (this.method.equals("POST") || this.method.equals("GET")) {
return;
}
throw new IOException("不支持的方法");
}
public void setUrl(String url) throws UnsupportedEncodingException {
this.url = URLDecoder.decode(url, "UTF-8");
}
public String getUrl() {
return url;
}
public String getProtocol() {
return protocol;
}
public String getMethod() {
return method;
}
public Map getHeaders() {
return headers;
}
public Map getRequestParam() {
return requestParam;
}
//请求方法
public static Request parse(InputStream inputStream) throws IOException {
//传入浏览器请求的字节流,Server解析
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
Request request = new Request();
//设置请求行参数
praseRequestLine(bufferedReader, request);
//设置请求头参数
praseRequestHeader(bufferedReader, request);
return request;
}
//请求第一行 请求行:方法 请求URL HTTP版本号
//GET /aaa.html?id=1&password=ppp&name=10 HTTP/1.1
private static void praseRequestLine(BufferedReader bufferedReader, Request request) throws IOException {
String firstLine = bufferedReader.readLine();
//第一行参数是以" "进行分割。然后得到请求方法,请求参数,请求版本号
//比如请求行为GET /aaa.html?id=1&name=10 HTTP/1.1
String[] fragments = firstLine.split(" ");
//fragmens至少存在三个参数
if (fragments.length < 3) {
throw new IOException("错误的请求行,参数不够");
}
//第一个参数是请求方式
String method = fragments[0];
request.setMethod(method);
//第二个参数是总的url请求参数
String urlAll = fragments[1];
//第三个参数是版本号
String protocol = fragments[2];
request.setProtocol(protocol);
//获取url请求页面,是urlAll里面的
String[] qFragmens = urlAll.split("\\?");
request.setUrl(qFragmens[0]);
//获取请求的参数 qFragmens里面的第二个参数
//id=1 & name=10
if (qFragmens.length > 1) {
String[] mess = qFragmens[1].split("&");
for (int i = 0; i < mess.length; i++) {
String[] keyvalue = mess[i].split("=");
String key = keyvalue[0];
String value = "";
if (keyvalue.length > 1) {
value = URLDecoder.decode(keyvalue[1], "utf-8");
}
//将获取到的键值对添加进去
request.setRequestParam(key, value);
}
}
}
//请求第二部分,直到遇到空行结束
// 请求头有很多信息如下:
// Host:www.baidu.com
// Accept:text/html
private static void praseRequestHeader(BufferedReader bufferedReader, Request request) throws IOException {
String secondLine;
//将请求头的信息全部保存到HashMap中。
while ((secondLine = bufferedReader.readLine()) != null && secondLine.trim().length() != 0) {
String[] keyValue = secondLine.split(":");
//分割之后可能会出现空格。所以要去掉空格
String key = keyValue[0];
String value = "";
if (keyValue.length > 1) {
value = keyValue[1].trim();
}
//将信息头保存到请求头里面
request.setHeaders(key, value);
}
}
@Override
public String toString() {
return "Request{" +
"请求页面='" + url + '\'' +
", 版本号='" + protocol + '\'' +
", 请求方法='" + method + '\'' +
", 请求头=" + headers +
", 请求行参数=" + requestParam +
'}';
}
}
响应主要包括三部分 1.响应行 2.响应头 3.响应体
第一行版本信息和状态码:HTTP/1.1 200 OK
第二行响应头里面有:
Server HTTP 服务器编号
Date Wed, 07 Aug 2019 10:14:53 GMT 指定格式的响应时间说明
Content-Type 响应体的格式说明
Content-Length 响应体的长度说明
常见状态码
200 OK
302 Temporarily Moved
400 Bad Request
404 Not Found
405 Method Not Allowed
500 Internal Server Error
编码
1.首先我们创建一个输出流数组,保存所有的信息
2.创建一个枚举类保存状态码
3.设置我们的响应服务器,类型等信息。
4.设置好之后,将响应行,响应头,响应体写到字节数组中去
5.将字节数组中的内容拷贝到输出流中,并设置长度。
package com.github7.response;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.*;
public class Response {
public static Response start(OutputStream outputStream) throws IOException {
Response response = new Response();
response.setServer();
response.setOutputStream(outputStream);
response.setContentType("text/html");
response.setDate();
return response;
}
//保存输出流
private byte[] resByteArray = new byte[8192];
private OutputStream outputStream;
private void setOutputStream(OutputStream outputStream) {
this.outputStream = outputStream;
}
private int contentLength = 0;
//保存以下信息Content-Type:Content-Lenth:Data:Server:
private Map headers = new HashMap<>();
//将默认状态置为200 ok
private State state = State.OK;
public void setState(State state) {
this.state = state;
}
private void setServer() {
headers.put("Server", "LR/1.0");
}
public void setContentType(String contentType) {
headers.put("Content-Type", contentType + ";charset=utf-8");
}
private void setDate() {
SimpleDateFormat dateFormat = new SimpleDateFormat("E,dd MMM yyyy HH:mm:ss z");
headers.put("Date", dateFormat.format(new Date()));
}
//将信息写入到响应输出流中的方法。
public void write(byte[] bytes, int off, int len) throws IOException {
if (contentLength + len > 8192) {
throw new IOException("超过请求头最大长度");
}
//将写入的东西先保存在自己的byte数组中
System.arraycopy(bytes, off, resByteArray, contentLength, len);
contentLength += len;
}
public void write(byte[] bytes, int len) throws IOException {
write(bytes, 0, len);
}
public void write(byte[] bytes) throws IOException {
write(bytes, bytes.length);
}
//格式化响应
public void print(String string, Object... args) throws IOException {
write(new Formatter().format(string, args).toString().getBytes("utf-8"));
}
public void println(Object o) throws IOException {
print("%s%n", o.toString());
}
public void flush() throws IOException {
headers.put("Content-Length", String.valueOf(contentLength));
sendResponseLine();
sendResponseHeads();
outputStream.write(resByteArray, 0, contentLength);
}
//将响应行加入到输出流中
public void sendResponseLine() throws IOException {
String responseLine = String.format("HTTP/1.0 %d %s \r\n", state.getCode(), state.getReason());
outputStream.write(responseLine.getBytes());
}
//将响应头加入到输出流中
public void sendResponseHeads() throws IOException {
for (Map.Entry entry : headers.entrySet()) {
String header = String.format("%s:%s\r\n", entry.getKey(), entry.getValue());
outputStream.write(header.getBytes("utf-8"));
}
outputStream.write("\r\n".getBytes("UTF-8"));
}
@Override
public String toString() {
return "Response{" +
" headers=" + headers +
", state=" + state +
'}';
}
}
package com.github7.response;
public enum State {
OK(200, "OK"),
Temporarily_Moved(302, "Temporarily Moved"),
BAD_REQUEST(400, "BAD_REQUEST"),
NOT_FOUND(404, "NOT_FOUND"),
METHOD_NOT_ALLOWED(405,"Not_Allowed"),
INTERNAL_SERVER_ERROR(500, "INTERNAL SERVER ERROR");
private int code;
private String reason;
public int getCode() {
return code;
}
public String getReason() {
return reason;
}
State(int code, String reason) {
this.code = code;
this.reason = reason;
}
}
该类有两个方法默认执行的doGet方法
package com.github7.controller;
import com.github7.request.Request;
import com.github7.response.Response;
import com.github7.response.State;
import java.io.IOException;
public class Controller {
public void doGet(Request request, Response response) throws IOException {
if (request.getProtocol().endsWith("1.0")) {
response.setState(State.METHOD_NOT_ALLOWED);
response.println("版本号不支持,仅支持1.1");
} else {
response.setState(State.BAD_REQUEST);
response.println("请求错误,仅支持HTTP/1.1");
}
}
public void doPost(Request request, Response response) throws IOException {
this.doGet(request, response);
}
}
StaticContraller继承自Controller覆写其方法
编码
1.首先我们根据url获取要链接的静态页面。
2.如果要获取的页面是当前路径/就加载指定静态页面
3.读取文件内容,将文件写到byte数组中
4.根据文件后缀比如.html设置ContentType
package com.github7.controller;
import com.github7.request.Request;
import com.github7.response.Response;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public class StaticController extends Controller {
private static final String HOME = System.getenv("LR_HOME");
//保存ContentType 支持三种类型用HashMap先保存起来
private final Map Content_TYPE = new HashMap() {
{
put("html", "text/html");
}
};
@Override
public void doPost(Request request, Response response) throws IOException {
this.doGet(request, response);
}
@Override
public void doGet(Request request, Response response) throws IOException {
//1.获取文件地址 request.getUrl()得到的是/hh.html
String filename = getFileName(request.getUrl());
//2.开始读取文件内容
System.out.println(filename);
InputStream inputStream = new FileInputStream(filename);
//缓冲流 一次读取1024字节内容
byte[] buf = new byte[1024];
int len;
while ((len = inputStream.read(buf)) != -1) {
response.write(buf, 0, len);
}
//3.获取类型content-type,根据文件名的后缀
String suffix = getSuffix(filename);
String contype = getContentType(suffix);
//然后设置响应头里面的contenttype
response.setContentType(contype);
}
private String getContentType(String suffix) {
String contentType = Content_TYPE.get(suffix);
if (contentType == null) {
//说明没有这种类型的文件
contentType = "text/html";
}
return contentType;
}
private String getSuffix(String filename) {
//找到最后一个点,点后面的就是文件类型
int index = filename.lastIndexOf('.');
if (index == -1) {
return null;
}
//返回文件名后缀
return filename.substring(index + 1);
}
private String getFileName(String url) {
if (url.equals("/")) {
url = "/index.html";
}
return "F:\\\\httpProject\\webapps\\"+ url.replace("/", File.separator);
}
}
首先要定义一个类加载器,我们如果在静态页面找不到页面。就找动态的类进行加载
1.根据传进来的url进行解析得到我们的文件
2.通过继承ClassLoader找到我们所需要的文件.class文件。
3.读取内容返回生成Class对象
4.通过返回的Class对象创建controller实例
package com.github7.classloader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
@Override
public Class> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
//1.根据类名称找到name对应的.class文件
//String fileLocation = "F:\\\\LRhttp\\webapps\\WEB-INF\\classes\\"+name.replace("/","")+".class";
String fileLocation = "F:\\\\httpProject\\target\\classes\\" + name.replace("/", "") + ".class";
File file = new File(fileLocation);
if (file.exists()) {
//2.读取文件内容。
byte[] buf = new byte[8192];
int len = 0;
try {
len = new FileInputStream(fileLocation).read(buf);
} catch (IOException e1) {
throw new ClassNotFoundException("文件出错",e);
}
//3.调用defineClass 转为Class>
System.out.println("类名" + name.replaceAll("/", ""));
return defineClass(name.replaceAll("/", ""), buf, 0, len);
} else {
throw new ClassNotFoundException("类没找到");
}
}
}
}
1.创建ServerSocket监听端口
创建Socket从网络中读取和写入数据,不同计算机上的两个应用可以通过连接发送和接受字节流。
2.分别创建Request和Response引用指向socket的输入输出流
3.获取url判断调用静态页面还是加载动态类
4.创建实例调用doGet/doPost方法
5.调用flush方法,更新数据
package com.github7.server;
import com.github7.classloader.MyClassLoader;
import com.github7.controller.Controller;
import com.github7.controller.StaticController;
import com.github7.request.Request;
import com.github7.response.Response;
import com.github7.response.State;
import org.dom4j.DocumentException;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server {
public final Controller staticController = new StaticController();
public Server() throws DocumentException {
}
public static void main(String[] args) throws IOException, DocumentException {
Server server = new Server();
server.run(1234);
}
public void run(int port) throws IOException {
ExecutorService pool = Executors.newFixedThreadPool(20);
//监听端口
ServerSocket serverSocket = new ServerSocket(port);
// 查看端口netstat -ano
while (true) {
Socket socket = serverSocket.accept();
pool.execute(new Runnable() {
@Override
public void run() {
try {
//初始化
Request request = Request.parse(socket.getInputStream());
Response response = Response.start(socket.getOutputStream());
//查找请求url是否存在
String filelocation = request.getUrl();
String filename = "";
if (filelocation.equals("/")) {
filename = "F:\\\\httpProject\\webapps\\index.html";
}
filename = "F:\\\\httpProject\\webapps\\" + filelocation.replace("/", File.separator);
//根据 URL 的不同,用不用的 controller 去处理
File file = new File(filename);
Controller controller = null;
if (file.exists()) {
System.out.println("静态controller");
controller = staticController;
} else {
System.out.println("动态controller");
Class> cla = null;
String name = request.getUrl().replace("/", "");
if (name.equals("article")) {
cla = new MyClassLoader().loadClass("ArticleController");
} else if (name.equals("postArticle")) {
cla = new MyClassLoader().loadClass("PostController");
} else {
cla = new MyClassLoader().loadClass(name);
}
if (cla != null) {
System.out.println(cla.getName());
controller = (Controller) cla.newInstance();
}
}
//动态也找不到 返回错误
if (controller == null) {
response.setState(State.NOT_FOUND);
response.println("" + State.NOT_FOUND.getCode() + " " + State.NOT_FOUND.getReason() + " 页面没有找到
");
} else {
if (request.getMethod().equals("GET")) {
controller.doGet(request, response);
} else if (request.getMethod().equals("POST")) {
controller.doPost(request, response);
} else {
//不支持的方法
response.setState(State.METHOD_NOT_ALLOWED);
response.println(State.METHOD_NOT_ALLOWED.getReason());
}
}
response.flush();
System.out.println(request.toString());
System.out.println(response.toString());
System.out.println();
socket.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
});
}
}
}
博客主要由以下部分构成
post.html博客提交页面
PostController博客提交:获取到html表单内容,往数据库中写入数据
ListController博客列表页面:获取数据库中的所有标题已经作者
ArticleController博客文章详情:点击超链接,连接数据库获取数据信息。
post.html
发表文章
发表文章
首页
获取html表单内容,发表文章
/*
Author:linrui
Date:2019/8/26
Content:
*/
import com.github7.controller.Controller;
import com.github7.request.Request;
import com.github7.response.Response;
import com.github7.response.State;
import java.io.IOException;
import java.sql.*;
import java.util.UUID;
public class PostController extends Controller {
@Override
public void doGet(Request request, Response response) throws IOException {
this.doPost(request, response);
}
@Override
public void doPost(Request request, Response response) throws IOException {
//获取url的id 和内容
String id = UUID.randomUUID().toString();//随机ID
String title = request.getRequestParam().get("title");
String author = request.getRequestParam().get("author");
String content = request.getRequestParam().get("content");
if(title.length()==0){
response.println("");
response.println("输入标题");
response.println("点击返回发表文章 ");
response.println("");
return;
}
if(author.length()==0){
response.println("");
response.println("对不起,未输入作者姓名");
response.println("点击返回发表文章 ");
response.println("/");
return;
}
if(content.length()==0){
response.println("");
response.println("未输入内容");
response.println("点击返回发表文章 ");
response.println(" ");
return;
}
//然后将所有内容存储到数据库中
//连接到数据库
Connection connection = null;
Statement statement = null;
PreparedStatement preparedStatement = null;
String url = "jdbc:mysql://localhost:3307/blogs?useSSL=false";
String sql = "insert into blog values(?,?,?,?)";
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection(url, "root", "aaaaaa");
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, id);
preparedStatement.setString(2, title);
preparedStatement.setString(3, author);
preparedStatement.setString(4, content);
preparedStatement.executeUpdate();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
//重定向首页
response.setState(State.Temporarily_Moved);
System.out.println("进入到PostController");
response.setHeaders("Location", "/article?id=" + id);
}
}
文章 title
import com.github7.controller.Controller;
import com.github7.request.Request;
import com.github7.response.Response;
import java.io.IOException;
import java.sql.*;
public class ListController extends Controller {
@Override
public void doGet(Request request, Response response) throws IOException {
this.doPost(request, response);
}
@Override
public void doPost(Request request, Response response) throws IOException {
response.println("所有文章
");
response.println("发表文章 ");
Connection connection = null;
Statement statement = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
String url = "jdbc:mysql://localhost:3307/blogs?useSSL=false";
String sql = "select article_id,article_title,article_author from blog";
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection(url, "root", "aaaaaa");
preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
String id = resultSet.getString(1);
String title = resultSet.getString(2);
String author=resultSet.getString(3);
response.println("" + title + ""+" 作者:"+author+" ");
}
} catch (ClassNotFoundException e) {
System.out.println(e.getMessage());
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
resultSet.close();
preparedStatement.close();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
文章详情页面
import com.github7.controller.Controller;
import com.github7.request.Request;
import com.github7.response.Response;
import java.io.IOException;
import java.sql.*;
public class ArticleController extends Controller {
@Override
public void doGet(Request request, Response response) throws IOException {
this.doPost(request, response);
}
@Override
public void doPost(Request request, Response response) throws IOException {
//从url 获取id
String id = request.getRequestParam().get("id");
Connection connection = null;
Statement statement = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
String url = "jdbc:mysql://localhost:3307/blogs?useSSL=false";
String sql = "select article_title,article_author,article_content from blog where article_id='" + id + "'";
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection(url, "root", "aaaaaa");
preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();
response.println("回到首页");
while (resultSet.next()) {
String title = resultSet.getString(1);
String author = resultSet.getString(2);
String content = resultSet.getString(3);
response.println("" + title + "
");
response.println(" " +
"作者:" + author + "
");
response.println(content);
}
} catch (ClassNotFoundException e) {
System.out.println(e.getMessage());
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
resultSet.close();
preparedStatement.close();
connection.close();
} catch (SQLException e) {
response.println(e.getMessage());
}
}
}
}