需求:
简单实现http服务器功能,服务器运行之后,可以自定义servlet,完成指定功能,浏览器访问,后台可以处理请求,并返回相应内容
写在前面
http协议基于TCP/IP协议,本例是用socket做底层实现的
socket相关看这篇(https://www.jianshu.com/p/651fd7718450)
1.简易Server端构建
socket构建server端,浏览器不同方式访问,查看不同的请求
public class Server2 { private static final String CRLF="\r\n"; private ServerSocket serverSocket; public static void main(String[] args) { Server2 server = new Server2(); server.start(); } /** * 服务器启动方法 * @throws IOException */ public void start(){ try { serverSocket = new ServerSocket(8888); this.recive(); } catch (Exception e) { e.printStackTrace(); //关闭server } } /** * 服务器接收客户端方法 */ public void recive() { try { Socket client = serverSocket.accept(); //得到客户端 StringBuilder sb =new StringBuilder(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream())); String mString = null; while ((mString = bufferedReader.readLine()).length()>0) { sb.append(mString); sb.append(CRLF); if (mString==null) { break; } } System.out.println(sb.toString()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * 关闭服务器方法 */ public void stop(){ //CloseUtils.closeSocket(server); } }
GET请求:
GET /index?name=123&psw=fdskf HTTP/1.1 Host: localhost:8888 Connection: keep-alive Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9
POST请求:
POST /index HTTP/1.1 Host: localhost:8888 Connection: keep-alive Content-Length: 34 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: null Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 username=fdsfgsdfg&pwd=fdsfadsfasd
对两种不同的方式的请求信息解析
2.Request封装
先解析下请求:
第一行: 请求方式 请求资源 HTTP协议版本
后面几行是一些协议,客户端支持的数据格式
如果是POST请求,请求参数会放在最后一行,和上面一行有空行间隔,如果是GET方式,请求参数会放在第一行
2.1 得到浏览器的请求信息
通过构造方法将socket的输入流传入,读取解析
2.2 得到请求方式与请求资源
解析请求信息,字符串截取第一行,然后截取方法,根据方法判断,如果是GET,那么请求资源与请求参数在第一行,如果是POST,第一行是请求资源,最后一行是请求参数,然后解析请求资源
public Request(InputStream inputStream) { this(); this.inputStream = inputStream; //从输入流中取出请求信息 try { byte[] data = new byte[20480]; int len = inputStream.read(data); requestInfo=new String(data, 0, len); //解析请求信息 parseRequestInfo(); } catch (Exception e) { return; } } /** * 解析请求信息 * @param requestInfo2 */ private void parseRequestInfo() { if(requestInfo==null || requestInfo.trim().equals("")) { return; } String paramentString="";//保存请求参数 //得到请求第一行数据 String firstLine = requestInfo.substring(0, requestInfo.indexOf(CRLF)); //第一个/的位置 int index = firstLine.indexOf("/"); this.method = firstLine.substring(0,index).trim(); String urlString = firstLine.substring(index,firstLine.indexOf("HTTP/")).trim(); //判断请求方式 if (method.equalsIgnoreCase("post")) { url = urlString; //最后一行就是参数 paramentString = requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim(); }else if (method.equalsIgnoreCase("get")) { if (!urlString.contains("?")) { this.url = urlString; }else { //分割url String[] urlArray = urlString.split("\\?"); this.url = urlArray[0]; paramentString = urlArray[1]; } } if (paramentString!=null&&!paramentString.trim().equals("")) { //解析请求参数 parseParament(paramentString); } } /** * 解决中文乱码 * @param value * @param code * @return */ private String decode(String value,String code) { try { return URLDecoder.decode(value, code); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } /** * 解析请求参数,放在数组里面 * name=12&age=13&fav=1&fav=2 */ private void parseParament(String paramentString) { String[] paramentsArray = paramentString.split("&"); for(String string:paramentsArray) { //某个键值对的数组 String[] paramentArray = string.split("="); //如果该键没有值,设值为null if (paramentArray.length==1) { paramentArray = Arrays.copyOf(paramentArray, 2); paramentArray[1]=null; } String key = paramentArray[0]; String value = paramentArray[1]==null?null:decode(paramentArray[1].trim(), "utf8"); //分拣法 if (!paramentMap.containsKey(key)) { paramentMap.put(key,new ArrayList<String>()); } //设值 ArrayList<String> values = paramentMap.get(key); values.add(value); } }
2.3 根据name得到请求参数的值
上一步解析完数据之后,会把请求参数放在Map中,key为请求参数name,value为请求参数值,根据key得到值
/** * 根据key得到多个值 */ public String[] getParamenters(String name) { ArrayList<String> values =null; if ((values=paramentMap.get(name))==null) { return null; }else { return values.toArray(new String[0]); } } /** * 根据key得到值 */ public String getParamenter(String name) { if ((paramentMap.get(name))==null) { return null; }else { return getParamenters(name)[0]; } }
2.4 解决中文乱码
前台提交数据时候,中文有时候会乱码,
/** * 解决中文乱码 * @param value * @param code * @return */ private String decode(String value,String code) { try { return URLDecoder.decode(value, code); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; }
3. 封装response
当得到浏览器请求之后,需要给浏览器响应
响应格式:
响应:
HTTP协议版本,状态码
响应头
响应正文
3.1 得到服务器的输出流
构造方法传入socket的输出流
3.2 构造响应头
常见响应码:200 404 500
根据状态码,构建不同的响应头,
3.3 构建方法,外界传入响应正文
暴露一个方法,用于外界传入响应值,与状态码
3.4 构建响应正文
根据不同响应码构建响应正文,如404,返回一个NOT FOUNF页面,
500返回一个SERVER ERROR 页面,如果是200,就返回正常界面,
3.5 构建推送数据到客户端的方法
将响应推送到客户端
4. 封装servlet
将响应与请求封装在一个servlet类中,主要是实现业务逻辑,不做其他事情 , 在server端直接new 一个servlet类,调用业务方法
public class Servlet { public void service(Request request,Response response){ response.print("<html>\r\n" + "<head>\r\n" + " <META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\r\n" + "</head>\r\n" + "<body>\r\n" + "欢迎你\r\n" + request.getParamenter("username")+ "</form>\r\n" + "</body>\r\n" + "</html>"); } }
5. 处理不同请求的server
想让server可以处理不同请求,/login 是做登录请求 /reg 是做注册请求
需要多线程,当有请求过来之后,创建一个线程处理相关的请求与响应,每个请求的线程互不影响,
5.1 创建一个转发器
每有一个客户端连接,就会创建一个线程,处理改客户端的请求与响应
public class Dispatcher implements Runnable{ private Request request; private Response response; private Socket client; private int code=200; public Dispatcher(Socket client) { try { client = client; request = new Request(client.getInputStream()); response = new Response(client.getOutputStream()); } catch (IOException e) { code=500; return; } } @Override public void run() { Servlet servlet = new Servlet(); servlet.service(request, response); response.pushToclient(code); CloseUtils.closeSocket(client); } }
5.2 创建上下文对象,存放servlet与对应的mapping
使用工厂模式,得到不同url得到不同的servlet,
public class ServletContext { /* * 有LoginServlet 设值别名 login * 访问login /login /log */ //存放servlet的别名 private Map<String, Servlet> servletMap; //存放url对应的别名 private Map<String, String> mappingMap; public ServletContext() { servletMap = new HashMap<>(); mappingMap = new HashMap<>(); }
public class WebApp { private static ServletContext context; static { context = new ServletContext(); //存放servlet 和其对应的别名 Map<String, Servlet> servletMap = context.getServletMap(); servletMap.put("login", new LoginServlet()); servletMap.put("register", new RegisterServlet()); Map<String, String> mappingMap = context.getMappingMap(); mappingMap.put("/login", "login"); mappingMap.put("/", "login"); mappingMap.put("/register", "register"); mappingMap.put("/reg", "register"); } public static Servlet getServlet(String url) { if (url==null || url.trim().equals("")) { return null; }else { return context.getServletMap().get(context.getMappingMap().get(url)); } } }
6.反射获取servlet对象
根据请求的url, 在mappingmap中找到servlet的别名,根据servlet的别名在servletmap中得到servlet对象,map存对象过于耗费内存,并且,每次添加一个servlet,都要更改这个文件,所以讲servlet的配置,卸载xml文件中,读取xml文件
<?xml version="1.0" encoding="UTF-8"?> <web-app> <servlet> <servlet-name>login</servlet-name>别名 <servlet-class>jk.zmn.server.demo4.LoginServlet</servlet-class>类的全路径 </servlet> <servlet-mapping>配置映射 <servlet-name>login</servlet-name>别名 <url-pattern>/login</url-pattern> 访问路径 <url-pattern>/</url-pattern> 访问路径 </servlet-mapping> <servlet> <servlet-name>reg</servlet-name> <servlet-class>jk.zmn.server.demo4.RegisterServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>reg</servlet-name> <url-pattern>/reg</url-pattern> </servlet-mapping> </web-app>
6.1 解析xml文件
首先要先解析xml配置文件,得到servlet及其映射,
6.2 根据解析到的数据,动态添加到map中
//获取解析工厂 try { SAXParserFactory factory =SAXParserFactory.newInstance(); //获取解析器 SAXParser sax =factory.newSAXParser(); //指定xml+处理器 WebHandler web = new WebHandler(); sax.parse(Thread.currentThread().getContextClassLoader() .getResourceAsStream("jk/zmn/server/demo4/web.xml") ,web); //得到所有的servlet 和别名 List<ServletEntity> entityList = web.getEntityList(); List<MappingEntity> mappingList = web.getMappingList(); context = new ServletContext(); //存放servlet 和其对应的别名 Map<String, String> servletMap = context.getServletMap(); for(ServletEntity servletEntity: entityList) { servletMap.put(servletEntity.getName(),servletEntity.getClz()); } //存放urlpatten和servlet别名 Map<String, String> mappingMap = context.getMappingMap(); for(MappingEntity mappingEntity:mappingList) { List<String> urlPattern = mappingEntity.getUrlPattern(); for(String url:urlPattern) { mappingMap.put(url, mappingEntity.getName()); } }
不用每次都直接修改这个文件,直接在配置文件中配置就行
##########################################################
最后:我已将文件抽好,
image.png
servlet包,是用户自定义包,新建的servlet必须要继承servlet类,
web.xml必须在src目录下,配置servlet也需按照格式配置,
运行core java application, 浏览器访问你的项目,就可以正常运行了,
效果:
image.png
image.png
image.png
本例存在诸多bug,请多多指教,在读取请求信息时候,我是直接读了20480个字节,实际上应该一个一个字节的读,但是写不出来,希望大佬们帮帮忙
源码:https://gitee.com/zhangqiye/httpserver
作者:z七夜
链接:https://www.jianshu.com/p/2c608b1baccf