本人尚在java 学习阶段,不是技术大咖, 自认是技术宅, 有一点写东西的能力,因最近学习了java网络编程,决定手写一个web服务器,不喜勿喷,大神也请高抬贵手,不足之处还望指点一二,不胜感激!
项目文件目录,是基于Maven的标准文件夹目录,src内包含7个类和一个接口,user包下的类为使用类。一个config文件夹用于存放服务器配置文件,webapps文件夹为服务器站点目录。
此类为本服务器的main线程类,负责启动服务器,不断接收客户端连接,使用ServerSocket对象实现基于tcp的网络对接,因为
并发线程会很多,所以使用线程池来不断接收客户端的连接,也避免了频繁创建线程的效率低下问题。这里的端口号与线程池的大
小都写入了config配置文件中,便于后期修改配置。接收到的soket对象放入ClientHandler线程类中使用。
构造函数中初始化ServerSocket对象与线程池对象。
public WebServer() {
try {
server = new ServerSocket(ServerContext.Port);
threadPool = Executors.newFixedThreadPool(ServerContext.MaxThread);
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器启动失败!");
}
}
}
public void start() {
try {
while (true) {
System.out.println("等待客户端连接");
Socket socket = server.accept();
threadPool.execute(new ClientHandler(socket));
}
} catch (Exception e) {
}
}
在main函数中启动服务端程序。
public static void main(String[] args) {
WebServer server = new WebServer();
server.start();
}
此类为客户端线程类,实现Runnable接口,是http连接的核心类,负责接收浏览器端发来的请求并返回响应消息,中转站的作用
。
run方法首先获取输入输出流并使用HttpRequest对象解析请求。
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
HttpRequset req = new HttpRequset(in);
boolean isPost = "POST".equals(req.getMethod());
HttpResponse res = new HttpResponse(out);
//把消息分为静态内容和动态内容
//--动态内容--
//是否为用户数据处理接口(动态内容)
if (ActionListen.isAction(req.getUri(), isPost)) {
ActionListen.doAction(req, res, req.getUri(), isPost);
return;
}
//--静态内容--
//获取请求类型
String type = req.getUri().substring(req.getUri().lastIndexOf(".") + 1);
//设置响应头
res.setHeader("Content-Type", ServerContext.getType(type));
//获取静态文件
File file = new File(ServerContext.WebRoot + req.getUri());
if (!file.exists()) {
//404 请求内容找不到
res.setStatus(404);
file = new File(ServerContext.WebRoot + "/" + ServerContext.NotFoundPage);
} else {
res.setStatus(200);
}
//响应静态内容
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
byte[] bys = new byte[(int) file.length()];
bis.read(bys);
res.write(bys);
bis.close();
解析http请求消息,存储资源标识符,请求类型和协议等,对请求头进行封装,保存所有的消息头序列,封装请求参数。写此类
前必须先了解http协议格式。
在analysis方法中首先解析uri、method、protocol属性,
String line = buffer.readLine();
if (line != null && line.length() > 0) {
String[] temp = line.split("\\s");
this.method = temp[0];
this.uri = temp[1];
if (this.uri.indexOf("?") != -1) {
String str = this.uri.substring(this.uri.indexOf("?") + 1);
genarenal(str, false);
this.uri = this.uri.substring(0, this.uri.indexOf("?"));
}
if (this.uri.endsWith("/")) {
this.uri += "index.html";
}
this.protocol = temp[2];
}
然后解析header信息,这里要注意的一个问题点是使用buffer.readLine时读取到末尾并不会自动退出,会一直阻塞,因为浏览器
端发送消息后或一直等待服务端的响应直到请求超时,所以在等待期间输入流会一直打开,而服务端使用buffer.readLine读取时,读
取到最后时因为浏览器端输入流没关闭,所以会一直阻塞,造成了两端在相互等待的情况,形成死锁。所以在读取到空行时就break
出while循环。这里保存的Content-Length的值是为了根据这个值的大小去读取post请求的内容。
String l = null;
int len = 0;
while ((l = buffer.readLine()) != null) {
if ("".equals(l)) {
break;
}
String k = l.substring(0, l.indexOf(":")).trim();
String v = l.substring(l.indexOf(":") + 1).trim();
this.Header.put(k, v);
if (l.indexOf("Content-Length") != -1) {
len = Integer.parseInt(l.substring(l.indexOf(":") + 1).trim());
}
}
如果请求的为post请求,则根据Content-Length读取消息体
if (method != null && method.toUpperCase().equals("POST")) {
char[] bys = new char[len];
buffer.read(bys);
String paraStr = new String(bys);
genarenal(paraStr, true);
}
/**
* 对请求字符串解析成参数对象
* @param str
* @param isPost
*/
private void genarenal(String str, boolean isPost) {
String[] arr = str.split("&");
for (String s : arr) {
String[] temp = s.split("=");
if (isPost) {
this.Form.put(temp[0], temp[1]);
} else {
this.QueryString.put(temp[0], temp[1]);
}
this.Parameter.put(temp[0], temp[1]);
}
}
4个属性 :Status存储所有消息响应码与对应的内容,Header则是存储用于响应的所有消息头,整型status用于设置本次响应码
out流为响应流。
初始化属性:
public HttpResponse(OutputStream out) {
this.out = out;
Header = new LinkedHashMap();
Status = new HashMap();
Status.put(HttpContext.STATUS_CODE_OK, HttpContext.STATUS_REASON_OK);
Status.put(HttpContext.STATUS_CODE_NOT_FOUND, HttpContext.STATUS_REASON_NOT_FOUND);
Status.put(HttpContext.STATUS_CODE_ERROR, HttpContext.STATUS_REASON_ERROR);
Header.put("Content-Type", "text/plain;charset=utf-8");
Header.put("Date", new Date().toString());
status = 200;
}
两个核心重载方法分别用于发送字节流和字符串
/**
* 响应方法,发送字符串
* @param bys
*/
public void write(String str) {
Header.put("Content-Length", Integer.toString(str.length()));
PrintStream ps = new PrintStream(out);
printHeader(ps);
ps.println(str);
ps.flush();
}
/**
* 打印头信息
* @param ps
*/
private void printHeader(PrintStream ps) {
ps.println(ServerContext.Protocol + " " + status + " " + Status.get(status));
Set> set = Header.entrySet();
for (Entry entry : set) {
String k = entry.getKey();
String v = entry.getValue();
ps.println(k + ":" + v);
}
ps.println("");
}
静态变量类,目前用于存放响应状态码和状态信息。
public class HttpContext {
public final static int STATUS_CODE_OK = 200;
public final static String STATUS_REASON_OK = "OK";
public final static int STATUS_CODE_NOT_FOUND = 404;
public final static String STATUS_REASON_NOT_FOUND = "Not Found";
public final static int STATUS_CODE_ERROR = 500;
public final static String STATUS_REASON_ERROR = "Internal Server Error";
}
读取配置文件类,首先在config文件夹中创建一个xml文件,存放配置信息
这里使用dom4j来读取xml文件
private static void init() {
Types = new HashMap();
try {
SAXReader reader = new SAXReader();
Document doc = reader.read("config/config.xml");
Element root = doc.getRootElement();
Element service = root.element("service");
Element connector = service.element("connector");
Protocol = connector.attributeValue("protocol");
Port = Integer.parseInt(connector.attributeValue("port"));
MaxThread = Integer.parseInt(connector.attributeValue("max-thread"));
WebRoot = service.elementText("webroot");
NotFoundPage = service.elementText("not-found-page");
@SuppressWarnings("unchecked")
List typeMappings = root.element("type-mappings").elements("type-mapping");
for (Element e : typeMappings) {
Types.put(e.attributeValue("ext"), e.attributeValue("type"));
}
} catch (Exception e) {
e.printStackTrace();
}
}
-----到此,本服务器大致已经搭建完成,可以请求响应静态内容,如html、js、css、图片等内容,接下来,我们开始动态内容的搭建
首先,写一个action.xml存放与config目录下,基本结构如下,主要放请求url,使用的请求方法和处理此请求的类方法。
再写一个接口用于规范处理数据类,
public interface Action {
void bridge(HttpRequset req, HttpResponse res);
}
用于读取action.xml中的信息使用的也是dom4j来读取。
private static Map GetMethod;
private static Map PostMethod;
static {
loadListenes();
}
private static void loadListenes() {
GetMethod = new HashMap();
PostMethod = new HashMap();
try {
SAXReader reader = new SAXReader();
File file = new File("config/action.xml");
Document doc = reader.read(file);
Element root = doc.getRootElement();
@SuppressWarnings("unchecked")
List list = root.elements("listen");
for (Element e : list) {
Element action = e.element("action");
if ("POST".equals(action.attributeValue("method").toUpperCase())) {
PostMethod.put(action.getText(), e.elementText("target"));
} else {
GetMethod.put(action.getText(), e.elementText("target"));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
使用反射执行xml里的处理类方法:
public static void doAction(HttpRequset req, HttpResponse res, String methodurl, boolean isPost) {
try {
String target = null;
if (isPost) {
target = PostMethod.get(methodurl);
} else {
target = GetMethod.get(methodurl);
}
Class extends Object> cls = Class.forName(target);
Object obj = cls.newInstance();
Method[] methods = cls.getDeclaredMethods();
for (Method method : methods) {
if ("bridge".equals(method.getName())) {
method.invoke(obj, req, res);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
到此,本服务器所有架构搭建完毕,接下来我们写一个测试用例,来测试服务器:
首先在webapps文件夹里写一个login.html文件
登录
这里我用ajax请求模拟登录操作
在user包中创建Lodin类,如下:
public class Login implements Action{
public void bridge(HttpRequset req, HttpResponse res) {
String user = req.Form("user");
String pwd = req.Form("password");
String rel = user + "--" + pwd;
res.write(rel);
}
}
测试:
测试成功!