我们只实现一个静态页面服务器。
一、创建xml配置文件
配置文件中有对端口、访问根路径、实际路径的配置。
1.dtd约束文件
<!ELEMENT contain (listener+)>
<!ELEMENT listener (context,path) >
<!ELEMENT context (#PCDATA) >
<!ELEMENT path (#PCDATA) >
<!ATTLIST listener port ID #CDATA >
2.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE contain SYSTEM ".\web-setting.dtd">
<contain>
<listener port="60000">
<context>/</context>
<path>.</path>
</listener>
<listener port="50000">
<context>/</context>
<path>.</path>
</listener>
</contain>
二、监听器类,监听端口
1.抽象类Environment
用于存储运行环境变量
public abstract class Environment {
private static String classpath;
public static String getClasspath() {
return classpath;
}
public static void setClasspath(String classpath) {
Environment.classpath = classpath;
}
}
2.实体类Listener
package xyz.syyrjx.enitty;
public class Listener extends Environment{
private Integer port;
private String context;
private String path;
private Thread runThread;//运行线程
private Integer failCount = 0;//启动线程失败次数
public Listener() {
}
public Listener(Integer port, String context, String path) {
this.port = port;
this.context = context;
this.path = path;
}
public Integer getFailCount() {
return failCount;
}
public void setFailCount(Integer failCount) {
this.failCount = failCount;
}
public Thread getRunThread() {
return runThread;
}
public void setRunThread(Thread runThread) {
this.runThread = runThread;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public String getContext() {
return context;
}
public void setContext(String context) {
this.context = context;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return "Listener{" +
"port='" + port + '\'' +
", context='" + context + '\'' +
", path='" + path + '\'' +
'}';
}
}
三、请求对象和响应对象
1.请求对象
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
public interface Request {
int READ_SIZE = 1024;
static <T> T parse(Class<T> clazz,InputStream inputStream) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (Objects.equals(method.getName(),"parse")){
method.setAccessible(true);
return (T)method.invoke(null,inputStream);
}
}
throw new NoSuchMethodException();
}
String getParameter(String key);
String getUri();
String getMethod();
}
package xyz.syyrjx.enitty;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public class HttpRequest implements Request{
private String uri;
private String method;
private Map<String,String> parameters = new HashMap<>();
private HttpRequest(){}
/**
* 解析输入流的内容
* @param inputStream socket输入流
* @return 返回HttpRequest请求对象
* @throws IOException
*/
public static HttpRequest parse(InputStream inputStream) throws IOException {
int readCount = 0;
byte[] bytes = new byte[Request.READ_SIZE];
StringBuilder builder = new StringBuilder();
// //问题:在inputStream内容被读完后会卡死在read方法
// while ((readCount = inputStream.read(bytes)) != -1){
// builder.append(new String(bytes,0,readCount));
// }
while ((readCount = inputStream.read(bytes)) > 0){
builder.append(new String(bytes,0,readCount));
if (readCount < Request.READ_SIZE){
break;
}
}
String requestPackage = builder.toString();
return parseRequestPackage(requestPackage);
}
/**
* 解析请求包
* @param requestPackage 请求包字符串
* @return 返回HttpRequest对象
*/
private static HttpRequest parseRequestPackage(String requestPackage){
// System.out.println(requestPackage);
HttpRequest res = new HttpRequest();
int requestMethodSpace = requestPackage.indexOf(' ');
int contextSpace = requestPackage.indexOf(' ',requestMethodSpace + 1);
String requestMethod = requestPackage.substring(0,requestMethodSpace);
String context = requestPackage.substring(requestMethodSpace + 1,contextSpace);
String uri = context;
int questionMarkIndex = context.indexOf('?');
if (questionMarkIndex != -1){
String parametersString = context.substring(questionMarkIndex + 1);
uri = context.substring(0,questionMarkIndex);
String[] pairs = parametersString.split("&");
for (String pair : pairs) {
String[] kv = pair.split("=");
if (kv.length == 2){
res.parameters.put(kv[0],kv[1]);
}else{
res.parameters.put(kv[0],"");
}
}
}
res.uri = uri;
res.method = requestMethod;
return res;
}
@Override
public String getParameter(String key) {
return parameters.get(key);
}
@Override
public String getUri() {
return this.uri;
}
@Override
public String getMethod() {
return this.method;
}
}
2.响应对象
import java.io.IOException;
public interface Response {
void response() throws IOException;
void notFound() throws IOException;
}
import java.io.IOException;
import java.io.OutputStream;
public class HttpResponse implements Response {
private String code = "200";
private String content;
private String contentType = "text/html";
private OutputStream outputStream;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public HttpResponse(OutputStream outputStream) {
this.outputStream = outputStream;
}
@Override
public void response() throws IOException {
outputStream.write(this.toString().getBytes());
}
@Override
public void notFound() throws IOException {
this.content = "404 Not Found";
response();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("HTTP /1.1 ")
.append(code)
.append("\r\n")
.append("Content-type: ")
.append(contentType)
.append("\r\n")
.append("Content-length: ")
.append(content.length())
.append("\r\n")
.append("\r\n")
.append(content);
return builder.toString();
}
}
四、主类
在主类中完成Listener对象的创建,以及保证每个监听器对象都由一个线程在运行。
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import xyz.syyrjx.enitty.Environment;
import xyz.syyrjx.enitty.Listener;
import xyz.syyrjx.exception.SettingFileException;
import xyz.syyrjx.exception.SettingFileNotFoundException;
import xyz.syyrjx.listener.ListenerThread;
import java.io.*;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
public class Start {
static List<Listener> listenerList = new ArrayList<>();
/**
* 解析listener
*/
static {
try {
Environment.setClasspath(URLDecoder.decode(Start.class.getClassLoader().getResource("").toString().substring(6),"utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
File setting = new File(Environment.getClasspath(),"setting.xml");
if (!setting.exists()){
throw new SettingFileNotFoundException("没有找到配置文件,请确认配置文件路径");
}
try {
Document contains = Jsoup.parse(setting);
Elements listeners = contains.getElementsByTag("listener");
for (Element elementListener : listeners) {
Integer port = null;
try {
port = Integer.valueOf(elementListener.attr("port"));
} catch (NumberFormatException e) {
throw new SettingFileException("请检查listener标签的port属性,以确保该属性为数字类型",e);
}
String context = elementListener.getElementsByTag("context").text();
String path = elementListener.getElementsByTag("path").text();
listenerList.add(new Listener(port,context,path));
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
while (true){
for (Listener listener : listenerList){
if (listener.getFailCount() < 5 && //失败次数小于5
(listener.getRunThread() == null || !listener.getRunThread().isAlive())){ //线程未创建或线程不存活
try {
Thread t = new Thread(new ListenerThread(listener));
t.setName(String.valueOf(listener.getPort()));
listener.setRunThread(t);
t.start();
listener.setFailCount(0);
} catch (Exception e) {
e.printStackTrace();
listener.setFailCount(listener.getFailCount() + 1);
}
}
}
}
}
}
五、监听器工作线程类
解析获得请求对象、创建响应对象、调用服务类的service()方法
import xyz.syyrjx.enitty.HttpRequest;
import xyz.syyrjx.enitty.HttpResponse;
import xyz.syyrjx.enitty.Listener;
import xyz.syyrjx.enitty.Request;
import xyz.syyrjx.exception.StarterException;
import xyz.syyrjx.service.RunnableService;
import xyz.syyrjx.service.Service;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.ServerSocket;
import java.net.Socket;
public class ListenerThread implements Runnable{
private Listener listener;
public ListenerThread() {
}
public ListenerThread(Listener listener) {
this.listener = listener;
}
@Override
public void run() {
ServerSocket serverSocket = null;
Socket socket = null;
try {
serverSocket = new ServerSocket(listener.getPort());
while (true){
//获取socket连接
socket = serverSocket.accept();
//输入流
InputStream inputStream = socket.getInputStream();
HttpRequest request = Request.parse(HttpRequest.class,inputStream);
//输出流
OutputStream outputStream = socket.getOutputStream();
HttpResponse response = new HttpResponse(outputStream);
//服务调用
if (!request.getUri().contains(listener.getContext())){
response.notFound();
}
Thread t = new Thread(new RunnableService(new Service(request,response,listener)));
t.setName(listener.getRunThread() + listener.getContext());
t.start();
}
}catch (IOException e) {
throw new StarterException("应用启动异常,请检查端口是否被占用",e);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
} finally {
if (null != socket){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != serverSocket){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
六、服务方法
1.服务类
import xyz.syyrjx.enitty.HttpResponse;
import xyz.syyrjx.enitty.Listener;
import xyz.syyrjx.enitty.Request;
import xyz.syyrjx.enitty.Response;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class Service {
static final int READ_SIZE = 1024;
private Request request;
private Response response;
private Listener listener;
public Service() {
}
public Service(Request request, Response response,Listener listener) {
this.request = request;
this.response = response;
this.listener = listener;
}
public void service() throws IOException {
String uri = request.getUri();
String path = listener.getPath();
if (path.equals(".") || path.equals("./") || path.equals(".\\")){
path = Listener.getClasspath();
}
if (uri.equals("/")){
uri = "/index.html";
}
File file = new File(path,uri);
if (!file.exists()){
response.notFound();
}else{
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bytes = new byte[Service.READ_SIZE];
int readCount = 0;
StringBuilder builder = new StringBuilder();
while ((readCount = fileInputStream.read(bytes)) != -1){
builder.append(new String(bytes,0,readCount));
}
((HttpResponse)response).setContent(builder.toString());
response.response();
}
}
}
2.适配器类,将Service类适配为Runnable类
import java.io.IOException;
public class RunnableService implements Runnable{
private Service service;
public RunnableService(Service service) {
this.service = service;
}
@Override
public void run() {
try {
service.service();
} catch (IOException e) {
e.printStackTrace();
}
}
}
七、启动工程,发送请求到60000端口
1.创建静态页面
欢迎页index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
index
</body>
</html>
测试页test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
111111111
</body>
</html>
2.启动工程
3.访问