手写简单的web服务器

非 Y 不嫁゛ 提交于 2020-03-02 17:15:23


手写简单的web服务器

一、用到的知识

oop,容器,io,多线程,网络编程,xml解析,反射,HTML,http

1.反射

将Java类中的各种结构映射成一个个Java对象,利用反射对一个类进行解剖,反射是框架设计灵魂

jdk9 用反射创建对象不再用.newInstance()创建对象,而是getConstructor().newInstance();

clz = Class.forName("包名.类名");//创建对象clz.newInstance();//9,不再这样用clz.getConstructor().newInstance();

2.xml解析

XML 可扩展标记语言 树结构

这里用SAX解析

{       //1、获取解析工厂        SAXParserFactory factory=SAXParserFactory.newInstance();        //2、从解析工厂获取解析器        SAXParser parse =factory.newSAXParser();        //3、编写处理器        //4、加载文档 Document 注册处理器        PHandler handler=new PHandler();        //5、解析        parse.parse(Thread.currentThread().getContextClassLoader()        .getResourceAsStream("com/sxt/server/basic/p.xml")        ,handler);}               class PersonHandler extends DefaultHandler{    private List<Person> persons ;    private Person person ;    private String tag; //存储操作标签    @Override    public void startDocument() throws SAXException {        persons = new ArrayList<Person>();    }       @Override    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {        if(null!=qName) {            tag = qName; //存储标签名            if(tag.equals("person")) {                person = new Person();            }        }    }       @Override    public void characters(char[] ch, int start, int length) throws SAXException {        String contents = new String(ch,start,length).trim();        if(null!=tag) { //处理了空            if(tag.equals("name")) {                person.setName(contents);            }else if(tag.equals("age")) {                if(contents.length()>0) {                    person.setAge(Integer.valueOf(contents));                }                       }        }    }       @Override    public void endElement(String uri, String localName, String qName) throws SAXException {        if(null!=qName) {             if(qName.equals("person")) {                persons.add(person);            }        }        tag = null; //tag丢弃了    }    @Override    public void endDocument() throws SAXException {    }    public List<Person> getPersons() {        return persons;    }}​

解析web.xml 文件

文件的解析,用SAX解析,而通过url找到相应的类,就需要创建一个Context类,将集合转化为Map集合,通过键值对的方式找到对应的servlet类

<web-app>  <servlet>      <servlet-name>Login</servlet-name>    <servlet-class>com.yn.server01.LoginServlet</servlet-class>      </servlet>    <servlet-mapping>      <servlet-name>Login</servlet-name>    <url-pattern>/login</url-pattern>    <url-pattern>/login02</url-pattern>      </servlet-mapping>  <servlet>      <servlet-name>index</servlet-name>    <servlet-class>com.yn.server01.IndexServlet</servlet-class>     </servlet>    <servlet-mapping>      <servlet-name>index</servlet-name>       <url-pattern>/index</url-pattern>      </servlet-mapping></web-app>   

封装对应的类

public class Entity {       private String name;    private String clz;    ......}   public class Mapping {      private String name ;        private Set<String > patterns;    public void addPattern(String pattern){        this.patterns.add(pattern);         }    ......  }

创建webContext类,将list集合转化为Map集合

public class WebContext {       private Map<String,String > entityMap=new HashMap<String,String>();     private Map<String,String> mappingMap=new HashMap<String,String>();     private List<Entity> entitys=null;    private List<Mapping> mappings=null;    public WebContext(List<Entity> entitys, List<Mapping> mappings) {               this.entitys = entitys;        this.mappings = mappings;        //将entity集合转化为Map集合        for (Entity entity : entitys) {                     entityMap.put(entity.getName(), entity.getClz());        }        //将mapping集合转化为Map集合                for (Mapping mapping : mappings) {            for (String pattern : mapping.getPatterns()) {                mappingMap.put(pattern, mapping.getName());            }        }    }    /**     * 通过url路径找到对应的class文件     * @param pattern     * @return     */    public String getClz(String pattern){        String name=mappingMap.get(pattern);        return entityMap.get(name);         }}

测试类

public class XmlTest02 {​    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {        //1、获取解析工厂        SAXParserFactory factory=SAXParserFactory.newInstance();        //2、从解析工厂获取解析器        SAXParser parse =factory.newSAXParser();        //3、编写处理器        //4、加载文档 Document 注册处理器        PHandler handler=new PHandler();        //5、解析        parse.parse(Thread.currentThread().getContextClassLoader()        .getResourceAsStream("com/yn/server01/web.xml")        ,handler);          //获取解析后的list集合        List<Entity> entitys = handler.getEntitys();        List<Mapping> mappings = handler.getMappings();        //将list集合转化为Map集合        WebContext wc=new WebContext(entitys, mappings);        //通过url找到对应的class类名        String clz = wc.getClz("/login02");             try {            Class<?> className = Class.forName(clz);            Servlet s = (Servlet) className.newInstance();            s.service();        } catch (ClassNotFoundException e) {            e.printStackTrace();        } catch (InstantiationException e) {            e.printStackTrace();        } catch (IllegalAccessException e) {            e.printStackTrace();        }    }}class WebHandler extends DefaultHandler{    private List<Entity> entitys ;    private  List<Mapping> mappings ;    private Entity entity;    private Mapping mapping;        private boolean isMapping=false  ;    private String tag; //    @Override    public void startDocument() throws SAXException {        entitys =new  ArrayList<Entity>();        mappings =new  ArrayList<Mapping>();    }       @Override    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {        if(null!=qName) {            tag = qName; //            if(tag.equals("servlet")) {                entity = new Entity();                isMapping = false;                          }else if (tag.equals("servlet-mapping")) {                mapping=new Mapping();                isMapping=true;            }        }    }       @Override    public void characters(char[] ch, int start, int length) throws SAXException {        String contents = new String(ch,start,length).trim();        if(null!=tag) { //处理空                   if (isMapping) {//操作servlet-mapping                if(tag.equals("servlet-name")) {                    mapping.setName(contents);                              }else if(tag.equals("url-pattern")) {                    if(contents.length()>0) {                        mapping.addPattern(contents);                    }                           }            }else{                          if(tag.equals("servlet-name")) {                    entity.setName(contents);                                   }else if(tag.equals("servlet-class")) {                    if(contents.length()>0) {                        entity.setClz(contents);                    }                           }            }        }    }       @Override    public void endElement(String uri, String localName, String qName) throws SAXException {        if(null!=qName) {             if(qName.equals("servlet")) {                entitys.add(entity);            }else if(qName.equals("servlet-mapping")) {                mappings.add(mapping);            }        }        tag = null; //tag丢弃了    }    public List<Entity> getEntitys() {        return entitys;    }    public List<Mapping> getMappings() {        return mappings;    }   }

 

3.HTML

超文本标记语言

post和get请求的区别,

post 提交 基于http协议不同,量大,参数不可见,安全

get 默认 获取,基于http协议不同,量小,参数可见,不安全

表单的name属性是提供给后台使用的,id作为前端使用的

4.Http协议

超文本传输协议,所有的www文件都必须遵守这个标准

http是应用层的协议 tcp和udp是传输层的协议

请求协议

  1. 请求行

  2. 请求头

  3. 请求正文

相应协议

1.状态行

  1. 响应头

  2. 响应正文

二、手写服务器

客户端就不用写了,就是浏览器,现在要写的是服务器的内容

1.获取请求协议

使用ServerSocket获取请求协议

/** * 使用ServerSocket建立与浏览器的连接,获取请求协议 * @author student * */public class Server01 {    private ServerSocket ss;    public static void main(String[] args) {        Server01 s=new Server01();        s.start();    }    //启动服务    public void start(){        try {            ss=new ServerSocket(8888);            System.out.println("服务器启动了");            receive();                  } catch (IOException e) {            e.printStackTrace();            System.out.println("服务器启动失败");        }       }    //接受连接    public void receive(){        try {            Socket client = ss.accept();            System.out.println("一个客户端建立了连接");            //获取请求协议            InputStream is = client.getInputStream();            byte[] datas=new byte[1024*1024];            int len = is.read(datas);            String requstInfo=new String(datas,0,len);            System.out.println(requstInfo);                 } catch (IOException e) {            e.printStackTrace();            System.out.println("客户端连接出现错误");        }           }    //停止服务    public void stop(){                    }}​

2. 获取响应协议

​ 动态的添加内容

累加字节数的长度

根据状态码拼接响应头协议 (注意空格和换行)

使用输出流输出

根据状态码同一推送出去

            //获取响应协议            StringBuilder content =new StringBuilder();            content.append("<html>");            content.append("<head>");            content.append("<title>");            content.append("服务器响应成功");            content.append("</title>");            content.append("</head>");            content.append("<body>");            content.append("shsxt server终于回来了。。。。");            content.append("</body>");            content.append("</html>");            int size = content.toString().getBytes().length; //必须获取字节长度            StringBuilder responseInfo =new StringBuilder();            String blank =" ";            String CRLF = "\r\n";            //返回            //1、响应行: HTTP/1.1 200 OK            responseInfo.append("HTTP/1.1").append(blank);            responseInfo.append(200).append(blank);            responseInfo.append("OK").append(CRLF);            //2、响应头(最后一行存在空行):            /*             Date:Mon,31Dec209904:25:57GMT            Server:shsxt Server/0.0.1;charset=GBK            Content-type:text/html            Content-length:39725426             */            responseInfo.append("Date:").append(new Date()).append(CRLF);                       responseInfo.append("Server:").append("shsxt Server/0.0.1;charset=GBK").append(CRLF);                        responseInfo.append("Content-type:text/html").append(CRLF);            responseInfo.append("Content-length:").append(size).append(CRLF);            responseInfo.append(CRLF);            //3、正文            responseInfo.append(content.toString());                        //写出到客户端                        BufferedWriter bw =new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));            bw.write(responseInfo.toString());            bw.flush();

3.封装response

响应分为响应行,响应头,响应体

响应头中要关注状态码200,404,500

响应头不变

响应体自己写

整个Response类整体分四部分

1.初始化

在无参构造器中将协议头和正文初始化,有参构造器可以传入参数客户端或输出流,初始化

BufferedWriter

2. 构建响应行和响应头

`响应行: HTTP/1.1 200 OK`

响应行中要对状态码进行判断,回应不同的信息

响应头正常添加

3. 响应体

响应体要动态添加,同时字节码长度也要添加

4. 将相应推送出去

用BufferedWriter将内容推送出去

public class Response {    private BufferedWriter bw ;    //正文    private  StringBuilder content;    //协议头信息    private StringBuilder headInfo ;        private final String BLANK =" ";    private final String CRLF = "\r\n";     private int len;//正文的字节数    private Response(){             content=new StringBuilder();        headInfo=new StringBuilder();        len=0;          }    public Response(Socket client){        this();        try {            bw =new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));        } catch (IOException e) {            e.printStackTrace();            headInfo=null;        }    }    public Response(OutputStream os){        this();        bw =new BufferedWriter(new OutputStreamWriter(os));    }       //动态添加内容    public Response print(String info){        content.append(info);        len+=info.getBytes().length;        return this;    }    public Response println(String info){        content.append(info).append(CRLF);        len+=(info+CRLF).getBytes().length;        return this;    }       //推送响应信息        public void pushToBrowser(int code ){               if (null==headInfo) {            code=500;        }        creatHeadInfo(code);        try {            bw.append(headInfo);            bw.append(content);                     bw.flush();        } catch (IOException e) {            e.printStackTrace();        }           }       //构建头信息    private void creatHeadInfo(int code){        //1、响应行: HTTP/1.1 200 OK        headInfo.append("HTTP/1.1").append(BLANK);        headInfo.append(code).append(BLANK);        headInfo.append("OK").append(CRLF);         switch (code) {        case 200:            headInfo.append("OK").append(CRLF);            break;        case 404:            headInfo.append("Not Found").append(CRLF);            break;        case 500:            headInfo.append("Server Error").append(CRLF);            break;        }           //2、响应头(最后一行存在空行):        /*         Date:Mon,31Dec209904:25:57GMT        Server:shsxt Server/0.0.1;charset=GBK        Content-type:text/html        Content-length:39725426         */        headInfo.append("Date:").append(new Date()).append(CRLF);       headInfo.append("Server:").append("shsxt Server/0.0.1;charset=GBK").append(CRLF);        headInfo.append("Content-type:text/html").append(CRLF);        headInfo.append("Content-length:").append(len).append(CRLF);        headInfo.append(CRLF);          }}

4.封装request

封装了五个属性 协议信息,请求方式,请求的url,请求参数,存储参数

1.构造器初始化

在构造器中初始化parameterMap ,并获取协议信息,之后分析协议信息

2.解析协议信息

通过字符串的分割获取想要的信息,字符串去空要注意,其中解析参数复杂了一点

参数的提交分为post和get请求

当是get请求的时候只需要分析请求头中的参数列表

当是post请求的时候,参数列表在请求头和请求体中都有

3. 将请求参数信息转化为map 集合

将获取的请求参数字符串分割加入Map集合

在获取参数的时候,参数的值为中文的时候回产生乱码,就需要处理乱码的问题

4.处理乱码

调用java.net.URLDecoder.decode(value,enc)方法来处理乱码

5.代码
public class Request02 {    //协议信息    private String requstInfo;    //请求方式    private String  method;    //请求url    private String url;    //请求参数    private String queryStr;    //存储参数    private Map<String ,List<String>> parameterMap;         private final  String CRLF = "\r\n";    public Request02(InputStream is){        parameterMap =new HashMap<String ,List<String>>();        byte[] datas=new byte[1024*1024];        int len;        try {            len = is.read(datas);            this.requstInfo=new String(datas,0,len);            System.out.println(requstInfo);         } catch (IOException e) {            e.printStackTrace();            return ;        }        //分解字符串        parseRequestInfo();     }       public Request02(Socket client) throws IOException{             this(client.getInputStream());    }       private void parseRequestInfo(){        System.out.println("-----分解-------");        System.out.println("-----1、获取请求方式   开头第一个/ ------");        this.method=this.requstInfo.substring(0, this.requstInfo.indexOf("/")).toUpperCase();           System.out.println("-----1、获取请求url   开头第一个/到HTTP/------");        System.out.println("-----可能包含的请求参数  前面的为url------");        //获取第一个/                int startIdx=this.requstInfo.indexOf("/")+1;        //获取HTTP/的位置        int endIdx=this.requstInfo.indexOf("HTTP/");                //分割字符串        this.url = this.requstInfo.substring(startIdx, endIdx);         //获取?的位置        int queryIdx = this.url.indexOf("?");               if (queryIdx>=0) {//表示存在请求参数            String [] urlArray=this.url.split("\\?");            this.url=urlArray[0];            queryStr=urlArray[1];        }        System.out.println("-----获取请求参数,如果是GET已经获取,如果是post可能在请求体中------");        this.queryStr=this.queryStr.trim();        this.method=this.method.trim();             if (method.equals("POST")) {            String qstr=this.requstInfo.substring(this.requstInfo.lastIndexOf(CRLF)).trim();            System.out.println("---->"+qstr);            if (null==queryStr) {                queryStr=qstr;            }else {                queryStr+="&"+qstr;                }        }           queryStr= null==queryStr?"":queryStr;        //this.queryStr=this.queryStr.trim();        //System.out.println("url :"+this.url+"     method :"+method +"   queryStr :"+queryStr);        System.out.println(queryStr);        //转成Map        convertMap();               }    //处理请求参数为Map    private void convertMap(){        //分割字符串        String[] keyValues=this.queryStr.split("&");        for (String queryStr : keyValues) {            //再次分割字符串 =            String[] kv=queryStr.split("=");            kv = Arrays.copyOf(kv, 2);            //获取key和value            String key=kv[0];            String value=kv[1]==null?null:decode(kv[1], "utf-8");                       //存放到Map中            if (!parameterMap.containsKey(key)) {                parameterMap.put(key, new ArrayList<String>());            }                       parameterMap.get(key).add(value);        }           }       private String decode(String value,String enc){        try {            return java.net.URLDecoder.decode(value,enc);        } catch (UnsupportedEncodingException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }        return null;    }       /**     * 通过name获取对应的多个值     * @param key     * @return     */    public String[] getPatameterValues(String key){             List<String> values = this.parameterMap.get(key);        if (null==values||values.size()<0) {            return null;        }               return values.toArray(new String[0]);           }    /**     * 通过name获取对应的一个值     * @param key     * @return     */    public String getPatameter(String key){             String[] values = getPatameterValues(key);              return values==null?null:values[0];         }    public String getMethod() {        return method;    }    public String getUrl() {        return url;    }    public String getQueryStr() {        return queryStr;    }   }

5.引入Servlet

创建Servlet父类,定义service方法方法需要传入参数Request和Response对象,在子类中的service方法中,写具体的响应内容response.print(),Server类中判断请求地址url,调用对应的servlet,调用service方法,在页面响应

//接受连接    public void receive(){        try {            Socket client = ss.accept();            System.out.println("一个客户端建立了连接");            //获取请求协议            Request request=new Request(client);            //获取响应协议                        Response response=new Response(client);            Servlet servlet = null;            if (request.getUrl().equals("login")) {                servlet=new LoginServlet();            }else if (request.getUrl().equals("index")) {                servlet=new IndexServlet();            }                        servlet.service(request, response);            response.pushToBrowser(200);        } catch (IOException e) {            e.printStackTrace();            System.out.println("客户端连接出现错误");        }           }

6.整和web.xml 文件

将之前的web.xml解析的代码拿过来,创建WebApp 和 WebHandler类 解析配置文件,定义方法用来通过url获取配置文件的servlet,WebHandler类,是用来将解析的内容封装到对应的类

webApp类

public class WebApp {    private static WebContext wc;    static {                SAXParserFactory factory=SAXParserFactory.newInstance();        //2銆佷粠瑙f瀽宸ュ巶鑾峰彇瑙f瀽鍣�        SAXParser parse;        try {            parse = factory.newSAXParser();            //3銆佺紪鍐欏鐞嗗櫒            //4銆佸姞杞芥枃妗B燚ocument聽娉ㄥ唽澶勭悊鍣�            WebHandler handler=new WebHandler();            //5銆佽В鏋�            parse.parse(Thread.currentThread().getContextClassLoader()            .getResourceAsStream("com/yn/server03/web.xml")            ,handler);            //将配置文件的信息转化为Map集合            wc=new WebContext(handler.getEntitys(), handler.getMappings());        } catch (Exception e) {            e.printStackTrace();            System.out.println("解析配置文件错误");        }           }    /**     * 通过url获取配置文件的servlet     * @param url     * @return     */    public  static  Servlet getServletFromUrl(String url){          String clz = wc.getClz("/"+url);                try {            Class<?> className = Class.forName(clz);            Servlet s = (Servlet) className.newInstance();            return s;        } catch (Exception e) {                 e.printStackTrace();            return null;        }    }​
public class WebHandler extends DefaultHandler{    private List<Entity> entitys ;    private  List<Mapping> mappings ;    private Entity entity;    private Mapping mapping;        private boolean isMapping=false  ;    private String tag; //瀛樺偍鎿嶄綔鏍囩    @Override    public void startDocument() throws SAXException {        entitys =new  ArrayList<Entity>();        mappings =new  ArrayList<Mapping>();    }        @Override    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {        if(null!=qName) {            tag = qName; //瀛樺偍鏍囩鍚�            if(tag.equals("servlet")) {                entity = new Entity();                isMapping = false;                            }else if (tag.equals("servlet-mapping")) {                mapping=new Mapping();                isMapping=true;            }        }    }        @Override    public void characters(char[] ch, int start, int length) throws SAXException {        String contents = new String(ch,start,length).trim();        if(null!=tag) { //处理空                        if (isMapping) {//操作servlet-mapping                if(tag.equals("servlet-name")) {                    mapping.setName(contents);                                    }else if(tag.equals("url-pattern")) {                    if(contents.length()>0) {                        mapping.addPattern(contents);                    }                           }            }else{                            if(tag.equals("servlet-name")) {                    entity.setName(contents);                                    }else if(tag.equals("servlet-class")) {                    if(contents.length()>0) {                        entity.setClz(contents);                    }                           }            }        }    }        @Override    public void endElement(String uri, String localName, String qName) throws SAXException {        if(null!=qName) {             if(qName.equals("servlet")) {                entitys.add(entity);            }else if(qName.equals("servlet-mapping")) {                mappings.add(mapping);            }        }        tag = null; //tag丢弃了    }​    public List<Entity> getEntitys() {        return entitys;    }​​    public List<Mapping> getMappings() {        return mappings;    }​}

WebContext类

public class WebContext {            private Map<String,String > entityMap=new HashMap<String,String>();        private Map<String,String> mappingMap=new HashMap<String,String>();        private List<Entity> entitys=null;    private List<Mapping> mappings=null;    public WebContext(List<Entity> entitys, List<Mapping> mappings) {                this.entitys = entitys;        this.mappings = mappings;        //将entity集合转化为Map集合        for (Entity entity : entitys) {                        entityMap.put(entity.getName(), entity.getClz());        }        //将mapping集合转化为Map集合                for (Mapping mapping : mappings) {            for (String pattern : mapping.getPatterns()) {                mappingMap.put(pattern, mapping.getName());            }        }    }    /**     * 通过url路径找到对应的class文件     * @param pattern     * @return     */    public String getClz(String pattern){        String name=mappingMap.get(pattern);        return entityMap.get(name);            }​    

Server02

//接受连接    public void receive(){        try {            Socket client = ss.accept();            System.out.println("一个客户端建立了连接");            //获取请求协议            Request request=new Request(client);            //获取响应协议                        Response response=new Response(client);            Servlet servlet =  WebApp.getServletFromUrl(request.getUrl());            if (servlet!=null) {                servlet.service(request, response);                response.pushToBrowser(200);            }else {                //错误                response.pushToBrowser(404);            }            response.pushToBrowser(200);        } catch (IOException e) {            e.printStackTrace();            System.out.println("客户端连接出现错误");        }           }

7.高效分发器.

使用多线程来处理每次请求都需启动服务器的问题

首先创建一个Dispatcher线程,在构造器中初始化客户端,请求和响应协议,如果异常,关闭客户端资源

在run方法中通过url获取的servlet方法获取对应的servlet,调用service方法,并在不同错误的时候推送对应的状态码,最后的方法是释放资源

package com.yn.server.core;​import java.io.File;import java.io.IOException;import java.io.InputStream;import java.net.Socket;import java.io.IOException;import java.nio.file.*;/** * 分发器:加入状态内容处理  404 505 及首页 *  * @author yn * */public class Dispatcher implements Runnable {    private Socket client;    private Request request;    private Response response ;    public Dispatcher(Socket client) {        this.client = client;        try {            //获取请求协议            //获取响应协议            request =new Request(client);            response =new Response(client);        } catch (IOException e) {            e.printStackTrace();            this.release();        }    }    @Override    public void run() {                 try {            if(null== request.getUrl() || request.getUrl().equals("")) {                                InputStream is =Thread.currentThread().getContextClassLoader().getResourceAsStream("index.html");​                byte b[] = new byte[1024];                         int len = 0;                         int temp = 0; // 所有读取的内容都使用temp接收                          while ((temp = is.read()) != -1) { // 当没有读取完时,继续读取                                b[len] = (byte) temp;                                len++;                         }​                        System.out.println(new String(b, 0, len));​                        response.print((new String(b,0,len)));                         response.pushToBrowser(200);                         is.close();                         return ;                  }                  Servlet servlet= WebApp.getServletFromUrl(request.getUrl());                  if(null!=servlet) {                        servlet.service(request, response);                        //关注了状态码                       response.pushToBrowser(200);                 }else {                //错误....                        InputStream is =Thread.currentThread().getContextClassLoader().getResourceAsStream("error.html");                        byte b[] = new byte[1024];                        int len = 0;                        int temp = 0; // 所有读取的内容都使用temp接收                        while ((temp = is.read()) != -1) { // 当没有读取完时,继续读取                              b[len] = (byte) temp;                              len++;                }​                                          response.print((new String(b,0,len)));                                          int read = is.read();                          response.pushToBrowser(404);                          is.close();                       }                     }catch(Exception e) {                    try {                            response.println("你好我不好,我会马上好");                            response.pushToBrowser(500);                        } catch (IOException e1) {                                e1.printStackTrace();                        }                  }                       release();          }    //释放资源          private void release() {                  try {                        client.close();                  } catch (IOException e1) {                          e1.printStackTrace();                  }    }​}​

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!