1. com.webserver.core
/** * WebServer是模拟Tomcat的一个web容器 * web容器可以同时管理多个网络应用,并且提供了与客户端(通常是浏览器)的网络连接以及传输数据 * 和与客户端的应用层交互(涉及到TCP协议以及HTTP协议)上面的支持。有了web容器,使得程序员更多 * 的精力是放在具体Web应用的业务上。 * * webapp(网络应用):它包含的内容大致有网页,图片,其他静态素材以及java程序代码,就是 * 我们上网时俗称的一个"网站"的全部内容。 * * TCP协议,处于传输层,负责两台计算机之间通过网络传输数据的协议。 * * HTTP协议,处于应用层,规定了双方发送数据的格式,以及交互规则。 */ public class WebServer { private ServerSocket server; private ExecutorService threadPool; /** * 初始化服务端 */ public WebServer() { try { System.out.println("正在启动服务端..."); server = new ServerSocket(8088); threadPool = Executors.newFixedThreadPool(50); System.out.println("服务端启动完毕!"); } catch (Exception e) { e.printStackTrace(); } } /** * 服务端开始工作的方法 */ public void start() { try { while(true) { System.out.println("等待客户端连接..."); Socket socket = server.accept(); System.out.println("一个客户端连接了!"); //启动一个线程处理该客户端交互 ClientHandler handler = new ClientHandler(socket); threadPool.execute(handler); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { WebServer server = new WebServer(); server.start(); } }
/** * 该线程负责处理与指定客户端的交互工作 * 处理过程分为三步: * 1:准备工作 * 2:处理请求 * 3:发送响应 */ public class ClientHandler implements Runnable{ private Socket socket; public ClientHandler(Socket socket) { this.socket = socket; } public void run() { try { //1 准备工作 HttpRequest request = new HttpRequest(socket); HttpResponse response = new HttpResponse(socket); /* * 2 处理请求 * 2.1:通过request获取客户端请求的资源对应的抽象路径requestURI的值 * 2.2:从webapps目录中通过对应的抽象路径寻找该资源 */ String path = request.getRequestURI(); System.out.println("path:"+path); //首先判断该请求是否请求为业务 HttpServlet servlet = ServerContext.getServlet(path); if(servlet!=null) { //处理业务 servlet.service(request,response); }else { File file = new File("./webapps"+path); if(file.exists()) { System.out.println("该资源已找到!"); response.setEntity(file); System.out.println("响应客户端完毕!"); }else { System.out.println("该资源不存在!"); File notFound = new File("./webapps/root/404.html"); //设置状态代码与描述 response.setStatusCode(404); response.setStatusReason("NOT FOUND"); //设置正文文件为404页面 response.setEntity(notFound); } } //3响应客户端 response.flush(); } catch(EmptyRequestException e) { //单独捕获空请求,不做任何处理。 System.out.println("空请求..."); } catch (Exception e) { e.printStackTrace(); } finally { /* * 本次响应完毕后与客户端断开连接这个操作是HTTP1.0的方式。 * 1.1允许建立连接后进行多次请求响应,但是需要额外的消息头和响应头的处理 */ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
/** * 这里保存所有服务端相关的配置信息 */ public class ServerContext { private static Map<String,HttpServlet> servletMapping = new HashMap<>(); static { initServletMapping(); } /** * 初始化请求路径与对应Servlet的关系 */ private static void initServletMapping(){ // servletMapping.put("/myweb/reg", new RegServlet()); // servletMapping.put("/myweb/login", new LoginServlet()); /* * 解析conf/servlets.xml文件 * 将根标签下所有的<servlet>标签获取到并且将每个<servlet>标签中的属性: * path的值作为key * className的值利用反射加载对应的类并实例化,将实例化的对象造型为 * HttpServlet并作为value存入到servletMapping完成初始化。 */ try { SAXReader reader = new SAXReader(); Document doc = reader.read(new File("./conf/servlets.xml")); Element root = doc.getRootElement(); List<Element> list = root.elements(); for(Element servletEle : list) { String path = servletEle.attributeValue("path"); String className = servletEle.attributeValue("className"); Class cls = Class.forName(className); HttpServlet servlet = (HttpServlet)cls.newInstance(); servletMapping.put(path, servlet); } } catch (Exception e) { e.printStackTrace(); } System.out.println(servletMapping.size()); } /** * 根据给定的请求路径获取对应的Servlet * @param path * @return */ public static HttpServlet getServlet(String path) { return servletMapping.get(path); } }
/** * 将user.dat文件中的用户信息显示到控制台 */ public class ShowAllUserDemo { public static void main(String[] args) throws IOException { RandomAccessFile raf = new RandomAccessFile("user.dat","r"); for(int i=0;i<raf.length()/100;i++) { byte[] data = new byte[32]; raf.read(data); String username = new String(data,"UTF-8").trim(); raf.read(data); String password = new String(data,"UTF-8").trim(); raf.read(data); String nickname = new String(data,"UTF-8").trim(); int age = raf.readInt(); System.out.println(username+","+password+","+nickname+","+age); System.out.println("pos:"+raf.getFilePointer()); } raf.close(); } }
2.com.webserver.exception
/** * 空请求异常 * 当HttpRequest解析请求时发现此请求为空请求时抛出该异常。 */ public class EmptyRequestException extends Exception{ private static final long serialVersionUID = 1L; public EmptyRequestException() { super(); } public EmptyRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } public EmptyRequestException(String message, Throwable cause) { super(message, cause); } public EmptyRequestException(String message) { super(message); } public EmptyRequestException(Throwable cause) { super(cause); } }
3.com.webserver.http
/** * HTTP协议规定之内容 */ public class HttpContext { private static Map<String,String> mimeMapping = new HashMap<>(); static { //初始化所有静态属性 initMimeMapping(); } /** * 初始化资源后缀与Content-Type对应值 */ private static void initMimeMapping() { // mimeMapping.put("html", "text/html"); // mimeMapping.put("css", "text/css"); // mimeMapping.put("js", "application/javascript"); // mimeMapping.put("png", "image/png"); // mimeMapping.put("jpg", "image/jpeg"); // mimeMapping.put("gif", "image/gif"); /* * 解析conf/web.xml文件 * 将根标签下所有名为<mime-mapping>的子标签获取出来,并将它下面的: * <extension>标签中的文本作为key * <mime-type>标签中的文本作为value * 初始化mimeMapping这个Map。 * 初始化完毕后,mimeMapping这个Map中应当有1000多个元素 */ try { SAXReader reader = new SAXReader(); Document doc = reader.read(new File("conf/web.xml")); Element root = doc.getRootElement(); List<Element> list = root.elements("mime-mapping"); for(Element e : list) { String key = e.elementTextTrim("extension"); String value = e.elementTextTrim("mime-type"); mimeMapping.put(key, value); } } catch (Exception e) { e.printStackTrace(); } System.out.println(mimeMapping.size()); } /** * 根据给定的资源后缀名获取对应的Content-Type的值 */ public static String getMimeType(String ext) { return mimeMapping.get(ext); } public static void main(String[] args) { String type = getMimeType("js"); System.out.println(type); } }
/** * 请求对象 * 该类的每一个实例用于表示客户端发送过来的一个具体的请求内容 * 一个请求由三部分组成:请求行,消息头,消息正文 */ public class HttpRequest { //请求行相关信息 //请求的方式 private String method; //抽象路径 private String uri; //协议版本 private String protocol; //uri当中请求部分(?左侧内容) private String requestURI; //uri当中参数部分(?右侧内容) private String queryString; //记录参数部分中的具体每一个参数 private Map<String,String> parameters = new HashMap<>(); //消息头相关信息 /* * key:消息头的名字 * value:消息头对应的值 */ private Map<String,String> headers = new HashMap<>(); //消息正文相关信息 //和连接相关的属性 private Socket socket; private InputStream in; /** * 构造方法,用于初始化请求对象 * @throws EmptyRequestException */ public HttpRequest(Socket socket) throws EmptyRequestException { try { this.socket = socket; this.in = socket.getInputStream(); /* * 解析请求分为三步: * 1:解析请求行 * 2:解析消息头 * 3:解析消息正文 */ System.out.println("HttpRequest:开始解析请求..."); parseRequestLine(); parseHeaders(); parseContent(); System.out.println("HttpRequest:解析请求完毕!"); } catch(EmptyRequestException e) { throw e; } catch (Exception e) { e.printStackTrace(); } } /** * 解析请求行 * @throws EmptyRequestException */ private void parseRequestLine() throws EmptyRequestException { System.out.println("HttpRequest:开始解析请求行"); try { /* * 通过输入流读取第一行字符串。 * 一个请求中的第一行字符串就是请求行的内容。 * 读取到以后,按照" "(空格)拆分为三部分然后将这三部分内容分别设置到请求行对应 * 的属性method,uri,protocol上 * http://localhost:8088/index.html */ String line = readLine(); //判断该请求是否为空请求 if("".equals(line)) { throw new EmptyRequestException(); } String[] data = line.split("\\s"); method = data[0]; uri = data[1]; protocol = data[2]; parseURI();//进一步解析uri System.out.println("method:"+method);// GET System.out.println("uri:"+uri);// /index.html System.out.println("protocol:"+protocol);// HTTP/1.1 } catch(EmptyRequestException e) { throw e; } catch (Exception e) { e.printStackTrace(); } System.out.println("HttpRequest:解析请求行完毕"); } /** * 进一步解析uri */ private void parseURI() { System.out.println("HttpRequest:进一步解析uri..."); /* * 首先在解析之前,要先判别当前uri是否含有参数,判断的依据是看uri当中 * 是否含有"?",有则说明含有参数,否则就是没有参数。 * 如果没有参数,那么直接将uri的值赋值给requestURI即可。 * * 若有参数,则应当先按照"?"将uri拆分然后将"?"左侧内容赋值给requestURI, * 将"?"右侧内容赋值给queryString,接着将参数部分再按照"&"拆分为每一组参 * 数,每组参数再按照"="拆分为参数名与参数值,并将名字作为key,值作为value * 保存到parameters这个Map中完成解析 * /myweb/reg?username=%E8%8C%8CXXXXXX */ if(uri.indexOf("?")!=-1) { String[] data = uri.split("\\?"); requestURI = data[0]; if(data.length>1) { queryString = data[1]; //对queryString中包含的%XX进行转码 try { queryString = URLDecoder.decode(queryString, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } parseParameters(queryString); } }else { requestURI = uri; } System.out.println("requestURI:"+requestURI); System.out.println("queryString:"+queryString); System.out.println("parameters:"+parameters); System.out.println("HttpRequest:解析uri完毕!"); } /** * 解析参数,参数是一个字符串. * 格式为:name=value&name=value... * 解析出来的参数会放在parameters这个Map中 */ private void parseParameters(String line) { String[] data = line.split("&"); for(String str : data) { String[] para = str.split("="); if(para.length>1) { parameters.put(para[0], para[1]); }else { parameters.put(para[0], null); } } } /** * 解析消息头 */ private void parseHeaders() { System.out.println("HttpRequest:开始解析消息头"); try { /* * 循环调用readLine方法读取每一行字符串,每一行就是一个消息头, * 如果readLine方法返回的字符串是一个空字符串时就应当停止循环读取 * 操作了(因为单独读取到了CRLF) * * 读取到每一个消息头后,我们可以按照": "(即:冒号空格)来进行拆分,将 * 消息头的名字作为key,消息头的值作为value保存到headers这个Map中完成消息 * 头的解析工作 */ while(true) { String line = readLine(); if("".equals(line)) { break; } String[] data = line.split(":\\s"); headers.put(data[0], data[1]); } System.out.println("headers:"+headers); } catch (Exception e) { e.printStackTrace(); } System.out.println("HttpRequest:解析消息头完毕"); } /** * 解析消息正文 */ private void parseContent() { System.out.println("HttpRequest:开始解析消息正文"); /* * 根据消息头中的Content-Length来判定 * 当前请求是否含有消息正文 */ if(headers.containsKey("Content-Length")) { //首先得到正文的长度(字节量) int length = Integer.parseInt( headers.get("Content-Length") ); try { //读取正文中所有的字节 byte[] data = new byte[length]; in.read(data); //根据Content-Type指定的正文类型来处理 String type = headers.get("Content-Type"); //是否为页面form表单提交的用户输入的信息 if("application/x-www-form-urlencoded".equals(type)) { /* * 正文内容就是一个字符串,原GET请求中 * url里"?"右侧内容 */ String line = new String(data,"ISO8859-1"); line = URLDecoder.decode(line,"UTF-8"); parseParameters(line); //将来可以再添加分支判断其他类型 } } catch (Exception e) { e.printStackTrace(); } } System.out.println("HttpRequest:解析消息正文完毕"); } private String readLine() throws IOException { int d = -1; char c1='a',c2='a'; StringBuilder builder = new StringBuilder(); while((d = in.read())!=-1) { c2 = (char)d; if(c1==13&&c2==10) { break; } builder.append(c2); c1 = c2; } return builder.toString().trim(); } public String getMethod() { return method; } public String getUri() { return uri; } public String getProtocol() { return protocol; } public String getHeader(String name) { return headers.get(name); } public String getRequestURI() { return requestURI; } public String getQueryString() { return queryString; } /** * 根据参数名获取参数值 * @param name * @return */ public String getParameter(String name) { return parameters.get(name); } }
/** * 响应对象 * 该类的每一个实例用于表示发送给客户端的一个具体的响应内容。 * 每个响应包含三部分内容: * 状态行,响应头,响应正文 */ public class HttpResponse { //状态行相关信息 private int statusCode = 200; private String statusReason = "OK"; //响应头相关信息 private Map<String,String> headers = new HashMap<>(); //响应正文相关信息 //响应正文对应的实体文件 private File entity; //与连接相关的信息 private Socket socket; private OutputStream out; public HttpResponse(Socket socket) { try { this.socket = socket; this.out = socket.getOutputStream(); } catch (Exception e) { e.printStackTrace(); } } /** * 将当前响应对象内容以一个标准的响应格式发送给客户端 */ public void flush() { /* * 1发送状态行 * 2发送响应头 * 3发送响应正文 */ sendStatusLine(); sendHeaders(); sendContent(); } /** * 发送状态行 */ private void sendStatusLine() { System.out.println("HttpResponse:开始发送状态行..."); try { //发送状态行 String line = "HTTP/1.1"+" "+statusCode+" "+statusReason; System.out.println("状态行:"+line); out.write(line.getBytes("ISO8859-1")); out.write(13);//written CR out.write(10);//written LF } catch (Exception e) { e.printStackTrace(); } System.out.println("HttpResponse:状态行发送完毕!"); } /** * 发送响应头 */ private void sendHeaders() { System.out.println("HttpResponse:开始发送响应头..."); try { /* * 遍历headers,将所有响应头发送 */ Set<Entry<String,String>> entrySet = headers.entrySet(); for(Entry<String,String> header : entrySet) { String name = header.getKey(); String value = header.getValue(); String line = name+": "+value; System.out.println("响应头:"+line); out.write(line.getBytes("ISO8859-1")); out.write(13); out.write(10); } //单独发送CRLF表示响应头发送完毕 out.write(13);//written CR out.write(10);//written LF } catch (Exception e) { e.printStackTrace(); } System.out.println("HttpResponse:响应头发送完毕!"); } /** * 发送响应正文 */ private void sendContent() { System.out.println("HttpResponse:开始发送响应正文..."); if(entity!=null) { try( FileInputStream fis = new FileInputStream(entity); ){ //发送响应正文 int len = -1; byte[] data = new byte[1024*10]; while((len = fis.read(data))!=-1) { out.write(data,0,len); } } catch (Exception e) { e.printStackTrace(); } } System.out.println("HttpResponse:响应正文发送完毕!"); } public File getEntity() { return entity; } /** * 设置响应正文实体文件 * 设置的同时会自动添加两个响应头: * Content-Type与Content-Length */ public void setEntity(File entity) { this.entity = entity; String fileName = entity.getName(); System.out.println("资源名:"+fileName); //获取该资源后缀名 String ext = fileName.substring( fileName.lastIndexOf(".")+1 ).toLowerCase(); System.out.println("资源后缀名:"+ext); String type = HttpContext.getMimeType(ext); putHeader("Content-Type", type); putHeader("Content-Length", entity.length()+""); } public int getStatusCode() { return statusCode; } public void setStatusCode(int statusCode) { this.statusCode = statusCode; } public String getStatusReason() { return statusReason; } public void setStatusReason(String statusReason) { this.statusReason = statusReason; } /** * 向当前响应对象中添加新的响应头 * @param name 响应头的名字 * @param value 响应头的值 */ public void putHeader(String name,String value) { this.headers.put(name, value); } }
4 com.webserver.servlet
/** * 所有Servlet的超类 */ public abstract class HttpServlet { public abstract void service(HttpRequest request,HttpResponse response); /** * 设置response跳转指定的页面 * @param path * @param request * @param response */ public void forward(String path,HttpRequest request,HttpResponse response) { File file = new File("./webapps"+path); response.setEntity(file); } }
/** * 登录业务 */ public class LoginServlet extends HttpServlet{ public void service(HttpRequest request,HttpResponse response) { //1获取登录信息 String username = request.getParameter("username"); String password = request.getParameter("password"); //2验证登录 try ( RandomAccessFile raf = new RandomAccessFile("user.dat","r"); ){ for(int i=0;i<raf.length()/100;i++) { raf.seek(i*100); //读取用户名 byte[] data = new byte[32]; raf.read(data); String name = new String(data,"UTF-8").trim(); if(name.equals(username)) { //比密码 raf.read(data); String pwd = new String(data,"UTF-8").trim(); if(pwd.equals(password)) { //登录成功 forward("/myweb/login_success.html", request, response); return; } break; } }//for循环结束 //如果走到这里,统一设置登录失败 forward("/myweb/login_fail.html",request,response); } catch (Exception e) { e.printStackTrace(); } } }
/** * 用于处理注册业务 */ public class RegServlet extends HttpServlet{ public void service(HttpRequest request,HttpResponse response) { System.out.println("RegServlet:开始处理注册..."); /* * 1:通过request获取用户提交的注册信息 * 2:将信息写入user.dat文件保存 * 3:设置response响应注册结果页面 */ String username = request.getParameter("username"); String password = request.getParameter("password"); String nickname = request.getParameter("nickname"); int age = Integer.parseInt(request.getParameter("age")); System.out.println("username:"+username); System.out.println("password:"+password); System.out.println("nickname:"+nickname); System.out.println("age:"+age); /* * 每个用户占用100字节 * 其中用户名,密码,昵称为字符串各32字节 * 年龄是int值固定的4字节。 */ try( RandomAccessFile raf = new RandomAccessFile("user.dat","rw"); ) { //要先检查该用户是否已经存在了 for(int i=0;i<raf.length()/100;i++) { raf.seek(i*100); byte[] data = new byte[32]; raf.read(data); String name = new String(data,"UTF-8").trim(); if(name.equals(username)) { //重复用户 forward("/myweb/have_user.html", request, response); return; } } raf.seek(raf.length()); //写入用户名 byte[] data = username.getBytes("UTF-8"); //扩容数组到32字节 data = Arrays.copyOf(data, 32); raf.write(data); //写密码 data = password.getBytes("UTF-8"); data = Arrays.copyOf(data, 32); raf.write(data); //写昵称 data = nickname.getBytes("UTF-8"); data = Arrays.copyOf(data, 32); raf.write(data); //写年龄 raf.writeInt(age); //响应注册成功的页面给客户端 forward("/myweb/reg_success.html", request, response); } catch (Exception e) { e.printStackTrace(); } System.out.println("RegServlet:处理注册完毕!"); } }
sevlet.xml
<?xml version="1.0" encoding="UTF-8"?> <servlets> <servlet path="/myweb/reg" className="com.webserver.servlet.RegServlet" /> <servlet path="/myweb/login" className="com.webserver.servlet.LoginServlet" /> </servlets>