利用Java手寫簡單的httpserver


前言:

在看完尚學堂JAVA300中講解如何實現一個最簡單的httpserver部分的視頻之后,

一、前置知識

1.HTTP協議

  當前互聯網網頁訪問主要采用了B/S的模式,既一個瀏覽器,一個服務器,瀏覽器向服務器請求資源,服務器回應請求,瀏覽器再將接收到的回應解析出來展現給用戶。這一問一答的過程可以抽象成瀏覽器向服務器發送一個Request然后服務器返回一個Response的過程
  其中Request和Reponse在HTTP中有有具體的格式要求

  • 一個Request的例子
Method Path-to-resource Http/Version-number
User-agent 瀏覽器的類型Accept-charset 用戶首選的編碼字符集……
Accept 瀏覽器接受的MIME類型
Accept language 用戶選擇的接受語言
Accept-charset 用戶首選的編碼字符集
空行
Option Request Body

如表格所示,第一行首先是請求方式,后跟請求資源路徑(url)如果請求方式是GET,則請求參數跟在請求路徑里,以?分開,然后一個空格,后跟HTTP版本。后幾行為固定格式內容。如果請求方式為POST,則隔一個空行后,跟的請求體的內容,里面有請求參數。

  • 一個Response內容
Http/Version-number Statuscode message
Server 服務器的類型信息
Content-type 響應的MIME類型信息
Content-length 被包含在相應類型中的字符數量
空行
Option Response Body

和Request類似,同樣包含響應頭和響應體兩部分。第一行的Statuscode標識了狀態參數,404表示請求資源沒有找到,500表示服務器錯誤,200表示成功。響應體里面包含的是響應內容

該部分具體可以參考博文:

2.JAVA網絡編程

在Java中提供了兩種網絡傳輸方式的實現,面向數據的UDP傳輸方式和面向連接的TCP傳輸方式,這里選用TCP方式。
在TCP方式中,服務端的編寫主要依靠類Socket和類ServerSocket,通過這兩個類可以建立一個TCP連接。

具體方法是:

  1. 首先新建一個ServerSocket對象server,指明端口號信息
  2. 然后使用server.accept()函數監聽端口,監聽到連接以后返回一個Socket對象
  3. 通過這個Socket對象,以及里面的輸入流和輸出流,我們就可以獲得傳過來的信息以及返回信息

二、具體實現

1.服務器類

首先我們需要建立一個服務器類,負責不斷監聽端口,獲得Socket,然后再利用獲得Socket新建一個分發器(Dispatcher),該分發器支持多線程,所以一個分發器專門負責處理一個連接,而服務器只負責不斷接收連接,建立分發器。
具體代碼如下:

package top.dlkkill.httpserver;

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



public class Server {
	
	private boolean flag=false;
	private ServerSocket server;
	
	public static void main(String[] args) throws IOException {
		Server myserver=new Server(8888);
		myserver.start();
	}
	
	public Server(int port) {
		try {
			server=new ServerSocket(port);
		} catch (IOException e) {
			this.stop();
		}
	}
	
	public void start() throws IOException {
		this.flag=true;
		this.revice(server);
	}
	
	public void revice(ServerSocket server) throws IOException {
			while(flag) {
				Socket client=server.accept();
				new Thread(new Dispatcher(client)).start();
			}
	}
	
	public void stop() {
		flag=false;
		
	}
}

2.封裝Request和Response

為了方便解析Request和返回Response,我們需要抽象出兩個對象(Request類和Response對象)。

首先封裝Request

  Request對象的作用是解析請求信息,將請求方式,請求資源路徑,請求參數解析分離出來,構建一個Request對象我們需要傳入一個參數------>輸入流。這樣我們就可以從輸入流中讀入請求信息然后開始解析。

package top.dlkkill.httpserver;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;


public class Request {
    private String url;
    private String method;
    
	private String info;
    private Map<String,List<String>> paramterMapValues;
    private InputStream is;
    
    
    
    public static final String CRLF="\r\n";
	public static final String BANK=" ";
	
	private Request() {
		url=null;
		method=null;
		is=null;
		info=null;
		paramterMapValues=new HashMap<String,List<String>>();
		
	}
	
	public Request(InputStream is) {
		this();
		this.is=is;
		try {
			create();
		} catch (IOException e) {
			this.is=null;
			System.out.println("Request 創建錯誤");
			return;
		}
		
	}
	
	public String getMethod() {
		return this.method;
	}
	
	
	
	public String getUrl() {
		return url;
	}

	private void create() throws IOException{
		getInfo();
		getUrlAndParamter();
		
	}
	
	/**
	 * 根據頁面name獲取對應所有值
	 * @return String[]
	 */
	public String[] getparamterValues(String name) {
		List<String> paramterValues=null;
		if((paramterValues=paramterMapValues.get(name))==null) {
			return null;
		}else {
			return paramterValues.toArray(new String[0]);
		}
	}
	
	/**
	 * 根據頁面name獲取單個值
	 * @return String[]
	 */
	public String getparamterValue(String name) {
		String values[]=getparamterValues(name);
		if(values==null)
			return null;
		else
			return values[0];
	}
	/**
	 * 得到請求信息
	 * @throws IOException
	 */
	private void getInfo() throws IOException {
		byte bytes[]=new byte[20480];
		int len=is.read(bytes);
		info=new String(bytes,0,len);
	}
	/**
	 * 處理得到url資源請求路徑和請求參數值
	 * @return
	 */
	private void getUrlAndParamter(){
		String firstline=info.substring(0,info.indexOf(CRLF));
		//System.out.println("FirstLine:  "+firstline);
		String paramter="";
		this.method=firstline.substring(0,firstline.indexOf("/")).trim();
		String tempurl=
				firstline.substring(firstline.indexOf("/"),firstline.indexOf("HTTP/")).trim();
		System.out.println("tempurl:  "+tempurl);
		if(this.method.equalsIgnoreCase("post")) {
			this.url=tempurl;
			paramter=info.substring(info.lastIndexOf(CRLF)).trim();
		}else {
			if(tempurl.contains("?")) {
				//split函數里面的參數實際上需要的是正則表達式,普通字符串還好,?號是特殊字符
				String[] urlarry=tempurl.split("\\?");
				this.url=urlarry[0];
				paramter=urlarry[1];
			}else {
				this.url=tempurl;
			}
		}
		//解析參數
		parseParmter(paramter);
		return;
		//System.out.println(this.url);
		//System.out.println(paramter);
	}
	/**
	 * 解析請求參數,轉換成鍵值對形式
	 * @param str
	 */
	private void parseParmter(String str) {
		if(str==null||str.equals("")||str.trim().equals(""))
			return;
		StringTokenizer st=new StringTokenizer(str,"&");
		while(st.hasMoreTokens()) {
			String temp=st.nextToken();
			String[] KeyAndValues=temp.split("=");
			if(KeyAndValues.length==1) {
				KeyAndValues=Arrays.copyOf(KeyAndValues,2);
				KeyAndValues[1]=null;
			}
			String key=KeyAndValues[0].trim();
			String value=KeyAndValues[1]==null?null:KeyAndValues[1].trim();
			if(!paramterMapValues.containsKey(KeyAndValues[0])){
				paramterMapValues.put(key,new ArrayList<String>());
			}
			paramterMapValues.get(key).add(decode(value, "gbk"));
		}
	}
	/**
	 * 解決中文編碼問題
	 * @param value
	 * @param code
	 * @return
	 */
	private String decode(String value,String code) {
		try {
			return java.net.URLDecoder.decode(value, code);
		} catch (UnsupportedEncodingException e) {
			
		}
		return null;
	}
}

然后我們進行封裝Response.
  Response主要分為兩部分(響應頭和響應體)
  構建Response需要傳入通過Socket獲得的輸出流,利用這個輸出流我們才可以寫回信息
響應頭格式較為固定,所有我們在Response中應該有一個私有方法根據響應碼自動構建響應頭信息。
  然后我們還需要一個共有方法void println(String msg),其他類利用該方法寫入對應的響應體部分。
最后需要有一個send()方法,自動整合響應體和響應體內容,並將所有內容發送出去。

package top.dlkkill.httpserver;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Date;

public class Response {
	
	public static final String CRLF="\r\n";
	public static final String BANK=" ";
	
	private StringBuilder headerinfo;
	private StringBuilder content;
	private BufferedWriter wr;
	private int len;
	
	public Response() {
		// TODO Auto-generated constructor stub
		headerinfo=new StringBuilder();
		content=new StringBuilder();
		if(content==null)
			System.out.println("error");
		len=0;
	}
	
	public Response(OutputStream os){
		this();
		wr=new BufferedWriter(new OutputStreamWriter(os));
	}
	
	public void createHeaderinfo(int code) {
		System.out.println("code is "+code);
		headerinfo.append("HTTP/1.1").append(BANK);
		switch (code) {
		case 200:
			headerinfo.append(code).append(BANK).append("OK");
			break;
		case 404:
			headerinfo.append(code).append(BANK).append("404 not found");
			break;
		case 500:
			headerinfo.append(code).append(BANK).append("error");
			break;
		default:
			headerinfo.append(code).append(BANK).append("error");
			break;
		}
		headerinfo.append(CRLF);
		headerinfo.append("Server:dlkkill server/0.1").append(CRLF);
		headerinfo.append("Date:").append(new Date()).append(CRLF);
		headerinfo.append("Content-type:text/html;charset=GBK").append(CRLF);
		//正文長度,字節長度
		headerinfo.append("Content-Length:").append(len).append(CRLF);
		//空行分隔符
		headerinfo.append(CRLF);
		//System.out.println(headerinfo.toString());
	}
	
	public Response println(String msg) {
		//System.out.println(msg);
		if(content==null)
			System.out.println(msg);
		content.append(msg);
		len+=msg.getBytes().length;
		return this;
	}
	
	public void pushToClient(int code) throws IOException {
		if(wr==null) {
			code=500;
		}
		createHeaderinfo(code);
		wr.write(headerinfo.toString());
		wr.write(content.toString());
		wr.flush();
	}
}

3.創建分發器

  我們需要有一個類專門一對一處理一個連接,並且該類要支持多線程。所以我們抽象出來一個分發器類。該類負責專門一對一處理一個連接
  該類擁有一個私有屬性Socket client。利用該屬性,該類可以創建一個Request和一個Response,然后該類再根據請求的url,利用Webapp類(該類用於生成處理不同請求的不同的類)獲得對應的類,啟動該類進行處理。
最后該類再調用Response提供的pushToClient方法將所有信息推送給瀏覽器,然后關閉連接。

具體代碼如下:

package top.dlkkill.httpserver;

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

public class Dispatcher implements Runnable {
	private Socket client;
	private Request req;
	private Response rep;
	private int code=200;
	
	public Dispatcher(Socket client) {
		this.client=client;
		try {
			req=new Request(client.getInputStream());
			rep=new Response(client.getOutputStream());
		} catch (IOException e) {
			code=500;
		}
	}
	
	@Override
	public void run() {
		System.out.println(req.getUrl()+"   ***");
		Servlet servlet=Webapp.getServlet(req.getUrl());
		if(servlet!=null)
			servlet.service(req, rep);
		else
			code=404;
		try {
			rep.pushToClient(code);
		} catch (IOException e) {
			code=500;
		}
		try {
			rep.pushToClient(code);
		} catch (IOException e) {
			
		}
		CloseUtil.closeAll(client);
	}
}

4.抽象處理類Servlet

首先我們將該處理類抽象成一個abstract Servlet類,該類負責根據不同的請求進行處理
該抽象類提供多個抽象方法doGet、doPost方法等分別處理不同的請求,傳入參數為(Request,Response)這兩個參數,在該方法內進行處理。
提供一個service方法根據不同的請求調用不同的方法
具體代碼:

package top.dlkkill.httpserver;

import java.net.Socket;

public abstract class Servlet {
	
	
	public Servlet() {
		
	}
	
	public void service(Request req,Response rep) {
		if(req.getMethod().equalsIgnoreCase("get")) {
			this.doGet(req, rep);
		}else {
			this.doPost(req, rep);
		}
	}
	
	public abstract void doGet(Request req,Response rep);
	
	public abstract void doPost(Request req,Response rep);
}

一個實例:

package top.dlkkill.httpserver;

public class loginServlet extends Servlet {

	
	@Override
	public void doGet(Request req, Response rep) {
		rep.println("<head>" + 
				"    <title>test</title>" + 
				"</head>" + 
				"<body>" + 
				"<p>hellow</p>"+
				"<form action=\"http://localhost:8888/index\" method=\"POST\">" + 
				"name: <input type=\"text\" name=\"name\">" + 
				"password: <input type=\"password\" name=\"pwd\">" + 
				"<input type=\"submit\" value=\"submit\">" + 
				"</form>" + 
				"</body>");
	}

	@Override
	public void doPost(Request req, Response rep) {
		this.doGet(req, rep);
	}

}

5.處理類生成工廠

為了編程的靈活性,我們將該httpserver寫出可以根據一個xml配置文件知道有多少種分別處理什么url請求的類,該xml就負責記錄這種映射關系
首先需要一個ServletContext類,該類有兩個屬性private Map<String,String> servlet和private Map<String,String> map,分別用來記錄名稱到類存儲地址之間的映射和url到名稱之間的映射

package top.dlkkill.httpserver;

import java.util.HashMap;
import java.util.Map;

public class ServletContext {
	
	//名稱到類存儲地址之間的映射
	private Map<String,String> servlet;
	//url到名稱之間的映射
	private Map<String,String> map;
	
	
	public void setServlet(Map<String, String> servlet) {
		this.servlet = servlet;
	}

	public void setMap(Map<String, String> map) {
		this.map = map;
	}
	public ServletContext() {
		servlet=new HashMap<String, String>();
		map=new HashMap<String, String>();
	}

	public Map<String, String> getServlet() {
		return servlet;
	}

	public Map<String, String> getMap() {
		return map;
	}
	
}

然后需要一個Webapp類
該類負責讀入xml文件並且進行解析,根據xml文件配置的內容,為分發器生成不同的servlet處理類。
生成不同的類利用的Java的類加載機制,可以在代碼中獲取class信息然后new一個類出來
解析xml文件我們使用的是SAXParser解析器,為了利用該解析器,我們還需要實現一個繼承於DefaultHandler的類

實現代碼:

package top.dlkkill.httpserver;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.SAXException;

public class Webapp {
	private static ServletContext servletcontext;
	static{
		servletcontext=new ServletContext();
		Map<String,String> servlet=servletcontext.getServlet();
		Map<String,String> map=servletcontext.getMap();
//		servlet.put("index", "top.dlkkill.httpserver.indexServlet");
//		servlet.put("login", "top.dlkkill.httpserver.loginServlet");
//		map.put("/login", "login");
//		map.put("/index", "index");
		SAXParserFactory parserfactor=SAXParserFactory.newInstance();
		WebHandler hd=new WebHandler();
		SAXParser parser;
		try {
			parser=parserfactor.newSAXParser();
			if(null==Thread.currentThread().getContextClassLoader().getResourceAsStream("top/dlkkill/httpserver/web.xml"))
				System.out.println("error");
			parser.parse(
					Thread.currentThread().getContextClassLoader()
					.getResourceAsStream("top/dlkkill/httpserver/web.xml"), 
					hd);
			List<Entity> entityList=hd.getEntityList();
			List<Mapping> mappingList=hd.getMappingList();
			for (Mapping mapping : mappingList) {
				String name=mapping.getName();
				List<String> urlList=mapping.getUrl();
				for (String url:urlList) {
					map.put(url, name);
				}
			}
			for (Entity entity:entityList) {
				String servletname=entity.getName();
				String clz=entity.getClz();
				servlet.put(servletname, clz);
			}
		} catch (ParserConfigurationException | SAXException |IOException e) {
			
		}
		
		
	}
	public static Servlet getServlet(String url) {
		Map<String,String> servlet=servletcontext.getServlet();
		Map<String,String> map=servletcontext.getMap();
		String className=servlet.get(map.get(url));
		Servlet temp=null;
		Class<?> clz=null;
		try {
			System.out.println("classname:"+className);
			if(className!=null)
			clz=Class.forName(className);
		} catch (ClassNotFoundException e) {
			return null;
		}
		try {
			if(clz!=null)
			temp=(Servlet)clz.newInstance();
		} catch (InstantiationException e) {
			return null;
		} catch (IllegalAccessException e) {
			return null;
		}
		return temp;
	}
}

package top.dlkkill.httpserver;

import java.util.ArrayList;
import java.util.List;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class WebHandler extends DefaultHandler {
	
	private List<Entity> entityList;
	

	private List<Mapping> mappingList;
	
	private Entity entity;
	private Mapping mapping;
	
	private String tag;
	private boolean isMap;
	
	public WebHandler() {
		
	}
	public List<Entity> getEntityList() {
			return entityList;
		}
	
	public List<Mapping> getMappingList() {
			return mappingList;
		}
	@Override
	public void startDocument() throws SAXException {
		entityList=new ArrayList<Entity>();
		mappingList=new ArrayList<Mapping>();
	}

	@Override
	public void endDocument() throws SAXException {
//		for (Mapping mapping : mappingList) {
//			if(mapping==null)
//				continue;
//			String name;
//			if(mapping.getName()!=null)
//				name=mapping.getName();
//			else
//				name="null";
//			List<String> urlList=mapping.getUrl();
//			for (String url:urlList) {
//				System.out.println(name+"---->"+url);
//			}
//		}
//		for (Entity entity:entityList) {
//			String servletname=entity.getName();
//			String clz=entity.getClz();
//			System.out.println(servletname+"---->"+clz);
//		}
	}

	@Override
	public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
		//System.out.println("開始處理"+"--->"+qName);
		if(null!=qName) {
			if(qName.equals("servlet")) {
				isMap=false;
				entity=new Entity();
			}else if(qName.equals("servlet-mapping")){
				isMap=true;
				mapping=new Mapping();
			}
		}
		tag=qName;
	}

	@Override
	public void endElement(String uri, String localName, String qName) throws SAXException {
		//System.out.println("結束處理"+"--->"+qName);
		if(null!=qName) {
			if(qName.equals("servlet")) {
				entityList.add(entity);
			}else if(qName.equals("servlet-mapping")){
				mappingList.add(mapping);
			}
		}
		tag=null;
	}

	@Override
	public void characters(char[] ch, int start, int length) throws SAXException {
		String str=new String(ch, start, length);
		//System.out.println("處理中"+"--->"+str);
		if(tag!=null&&str!=null&&!str.trim().equals("")) {
			if(!isMap) {
				if(tag.equals("servlet-name"))
					entity.setName(str);
				else if(tag.equals("servlet-class"))
					entity.setClz(str);
			}else {
				if(tag.equals("servlet-name"))
					mapping.setName(str);
				else if(tag.equals("url"))
					mapping.getUrl().add(str);
			}
		}
	}

}

6.一個工具類

該類負責關閉連接,連接關閉了那與該連接有關的流也就關閉了

package top.dlkkill.httpserver;

import java.io.Closeable;

public class CloseUtil {
	public static void closeAll(Closeable ...io) {
		for (Closeable closeable : io) {
			try {
				if(closeable!=null)
					closeable.close();
			}catch (Exception e) {
				// TODO: handle exception
				e.printStackTrace();
			}
		}
	}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM