第12次文章:网络编程——httpserver服务器的搭建

  • 2019 年 10 月 8 日
  • 筆記

这周的任务太多了,服务器只学习了一半,先更新出来吧!下周继续!fighting!

这周的主要的学习内容是httpserver服务器的搭建,需要一些简单的HTML语言语法的了解。

一、HTML语法简介:

下面是一个网页的源码:

<!doctype html>  <html lang="en">   <head>    <meta charset="UTF-8">    <meta name="Generator" content="EditPlus®">    <meta name="Author" content="">    <meta name="Keywords" content="">    <meta name="Description" content="">    <title>第一个表单</title>   </head>   <body>    <pre>    method:请求方式 get/post      get:默认方式,数据量小,安全性不高      post:量大,安全性相对高    action:请求的服务器路径    id:编号,前端区分唯一性,js中使用    name:名称,后端(服务器)区分唯一性,获取值    只要提交数据给后台,必须存在name    </pre>    <form method="get" action="http://localhost:8888/index.html">    用户名:<input type = "text" name="uname" id="uname"/>    密码:<input type="password" name="pwd" id="pwd"/>    兴趣:<input type="checkbox" name="fav" value="0">篮球    <input type="checkbox" name="fav" value="1">足球    <input type="checkbox" name="fav" value="2">乒乓球    <input type="submit" value="登录"/>    </form>   </body>  </html>

使用浏览器打开之后,显示的内容如下所示:

下面是两种不同的请求方式,服务器返回的响应信息:

1、post方式:

POST /index.html HTTP/1.1  Host: localhost:8888  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8  Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2  Accept-Encoding: gzip, deflate  Content-Type: application/x-www-form-urlencoded  Content-Length: 38  Connection: keep-alive  Upgrade-Insecure-Requests: 1

2、get方式:

GET /index.html?uname=dasdsai&pwd=dsadfa&fav=0&fav=1&fav=2 HTTP/1.1  Host: localhost:8888  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8  Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2  Accept-Encoding: gzip, deflate  Connection: keep-alive  Upgrade-Insecure-Requests: 1

二、服务器的封装

http服务器的构建。在网络中的通信过程中,就是客户端和服务器端进行相互请求和应答方式的交互。客户端在自己这边输入服务器地址,请求获取服务器的资源信息。当服务器接收到请求信息的时候,根据已有的资源,进行响应的答复。所以在服务器的搭建过程中,主要就是将请求信息进行封装,并且进行分析,然后根据分析结果,把应答信息发送出去即可。

1、对响应信息进行封装

在对响应信息进行封装的过程中,主要的思想就是利用输出流,按照html语法格式,对应答信息进行包装,然后输出包装之后的信息块。

package com.peng.server.demo01;    import java.io.BufferedWriter;  import java.io.IOException;  import java.io.OutputStream;  import java.io.OutputStreamWriter;  import java.net.Socket;  import java.util.Date;    import com.peng.server.util.CloseUtil;    /**   * 封装响应信息   *   */  public class Response {      //两个常量    private static String CRLF = "rn";//回车换行    private static String BLANK = " ";//空格    //流    BufferedWriter bw ;    //正文    private StringBuilder content;    //存储头信息    private StringBuilder headInfo;    //存储正文长度,正文长度是字节长度    private int len = 0;      //构造器    public Response() {      headInfo = new StringBuilder();      content = new StringBuilder();      len = 0;    }    public Response(Socket client)  {      this();      try {        bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));      } catch (IOException e) {        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;    }      /**     * 构建响应头     * 私有方法,仅供该类内部使用     */    private void createHeadInfo(int code) {        //1) HTTP协议版本、代码状态、描述      headInfo.append("HTTP/1.1").append(BLANK).append(code).append(BLANK);      switch(code) {      case 200:        headInfo.append("OK");        break;      case 404:        headInfo.append("NOT FOUND");        break;      case 500:        headInfo.append("SERVER ERROR");        break;      };      headInfo.append(CRLF);        //2) 响应头(Response Head)      headInfo.append("Server:bjsxt Server/0.0.1").append(CRLF);      headInfo.append("Date:").append(new Date()).append(CRLF);      headInfo.append("Content-type:text/html;charset=GBK:").append(CRLF);      //长度:字节长度      headInfo.append("Content-Length:").append(len).append(CRLF);      headInfo.append(CRLF);//分隔符      }      //推送到客户端    void pushToClient(int code) throws IOException {      if(null == headInfo) {        code = 500;      }      createHeadInfo(code);      //头信息+分隔符      bw.append(headInfo.toString());      //正文      bw.append(content.toString());      bw.flush();      }      public void close() {      CloseUtil.closeAll(bw);    }    }

tips:

(1)、在html信息中,经常使用到换行和空格,所以我们预定义两个静态常量,使用的时候,直接进行调用。

(2)、在描述代码状态的时候,一般采用200代表正常状态,404代表服务器没有寻找到相关资源,500代表服务器报错。这些状态码属于一种常规操作,在编写服务器的时候,尽量使用常规定义,这样的话,在后续检查代码以及修正代码的时候,便于我们自己快速定位错误地方。

2、封装请求信息

package com.peng.server.demo01;    import java.io.IOException;  import java.io.InputStream;  import java.io.UnsupportedEncodingException;  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 method;    //请求资源    private String url;    //请求参数    private Map<String,List<String>> parameterMapValues;      //内部    public static final String CRLF = "rn";    private InputStream is ;    private String requestInfo;//请求信息      //构造器    public Request() {      method = "";      url = "";      parameterMapValues = new HashMap<String,List<String>>();    }    public Request(InputStream is) {      this();      this.is = is;      try {        byte[] data = new byte[20480];        int len;        len = is.read(data);        requestInfo = new String(data,0,len);      } catch (IOException e) {        return;      }      //分析请求信息      parseRequestInfo();    }      /**     * 分析请求信息     */    public void parseRequestInfo() {      if(null==requestInfo || ( requestInfo.trim().equals(" "))) {        return;      }        /**       * ===================================       * 从信息的首行分解出:请求方式 请求路径 请求参数(get可能存在)       * 如:GBT/index.html?name123&pwd=5467 HTTP/1.1       *       * 如果为post方式,请求参数可能在最后正文中       *       * ===================================       */      //接收请求参数      String paramString ="";        //1、获取请求方式      String firstLine = requestInfo.substring(0, requestInfo.indexOf(CRLF));      int idx =requestInfo.indexOf("/");//   /的位置      this.method = firstLine.substring(0, idx).trim();//获取请求的方式      String urlStr = firstLine.substring(idx,firstLine.indexOf("HTTP/")).trim();      if(this.method.equalsIgnoreCase("post")) {        this.url = urlStr;        paramString = requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();        }else if(this.method.equalsIgnoreCase("get")) {        if(urlStr.contains("?")) {//是否存在参数          String[] urlArray = urlStr.split("\?");          this.url = urlArray[0];          paramString = urlArray[1];//接收请求参数        }else {          this.url = urlStr;        }      }        //不存在请求参数      if(paramString.equals("")) {        return;      }        //2、将请求参数封装到MAP中      parseParams(paramString);    }      /**     * 将请求封装到MAP中     * @param paramString     */    private void parseParams(String paramString) {      //分割 将字符串转成数组      StringTokenizer token = new StringTokenizer(paramString,"&");      while(token.hasMoreTokens()) {        String keyValue = token.nextToken();        String[] keyValues = keyValue.split("=");        if(keyValues.length==1) {          keyValues = Arrays.copyOf(keyValues, 2);          keyValues[1] = null;        }        String key = keyValues[0].trim();        String value = null==keyValues[1]?null:decode(keyValues[1].trim(),"gbk");        //转换成Map  分拣        if(!parameterMapValues.containsKey(key)) {          parameterMapValues.put(key, new ArrayList<String>());        }        List<String> values = parameterMapValues.get(key);        values.add(value);      }    }      /**     * 解决中文问题     * @param value     * @param code     * @return     */    private String decode(String value,String code) {        try {        return java.net.URLDecoder.decode(value, code);      } catch (UnsupportedEncodingException e) {        // TODO Auto-generated catch block        e.printStackTrace();      }      return null;      }      /**     * 根据页面的name获取对应的多个值     * @param name     * @return     */    public String[] getParameterValues(String name) {      List<String> values = null;      if((values=parameterMapValues.get(name))==null) {        return null;      }else {        return values.toArray(new String[0]);      }      }      /**     * 根据页面的name获取对应的单个值     * @param name     * @return     */    public String getParameter(String name) {      String[] values = getParameterValues(name);      if(null==values) {        return null;      }      return values[0];    }      //获取Url的方法    public String getUrl() {      return url;    }    }

tips:

(1)在进行获取相关值的时候,我们将其显示在网页上,将会涉及到不同编码集和解码集的转换。所以,为了避免解码集和编码集不同而导致的乱码问题,我们自定义一个指定相同解码集与编码集的方法。使用此方法,避免我们在显示时出现的乱码问题。

(2)在管理用户名和参数的问题上时,由于一个用户名可以对应有多个参数,比如密码,喜好等等,所以我们使用了MAP进行存储管理用户信息。键值对,键只有一个,我们使用用户名作为键,然后多个参数,我们使用链表进行存储,这样就可以很好的解决一个键对应多个值的问题。

3、服务器构建

package com.peng.server.demo01;    import java.io.IOException;  import java.net.ServerSocket;  import java.net.Socket;  /**   * 创建服务器并启动   * 请求并响应   * @author 赵晓鹏   *   */  public class Server4 {      private ServerSocket server;    private static String CRLF = "rn";//回车换行    private static String BLANK = " ";//空格      public static void main(String[] args) {      new Server4().start();      }      /**     * 启动方法     */    public void start() {      try {        server = new ServerSocket(8888);        this.receive();      } catch (IOException e) {        // TODO Auto-generated catch block        e.printStackTrace();      }      }      /**     * 接收客户端     */    private void receive() {      try {        Socket client = server.accept();          //请求        Request req = new Request(client.getInputStream());          //响应        Response rep = new Response(client.getOutputStream());        //响应信息的构建        rep.println("<html><head><title>HTTP响应示例</title>");        rep.println("</head><body>");        rep.println("欢迎").println(req.getParameter("uname")).println("回来");        rep.println("</body></html>");        rep.pushToClient(200);        } catch (IOException e) {        // TODO Auto-generated catch block        e.printStackTrace();      }      }  }

tips:经过相应的封装后,我们在建立服务器的时候,只需要进行建立响应和请求类别就可以了,大大简化服务器建立时的代码。