欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JAVA简易WEB服务器(三)

程序员文章站 2022-05-04 18:17:31
...

在上一篇《JAVA简易WEB服务器(二)》中我们完成了对浏览器请求的解析,这一篇我们继续来实现响应浏览器的请求,同样的,我们还是先来看一下服务端响应给浏览器的数据格式

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Accept-Ranges: bytes
ETag: W/"129-1456125361109"
Last-Modified: Mon, 22 Feb 2016 07:16:01 GMT
Content-Type: text/html
Content-Length: 129
Date: Mon, 22 Feb 2016 08:08:32 GMT

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>test</title>
</head>
<body>this is test page.
</body>
</html>

只要我们响应的数据满足这个格式,浏览器就可以正常解析了,在解析之前还是先来做一些准备工作。
相应请求时可能会出现异常,所以我们先编写一个异常类,和请求的异常类类似。

package com.gujin.server;

/**
 * 响应异常
 * 
 * @author jianggujin
 * 
 */
public class HQResponseException extends RuntimeException
{
   private static final long serialVersionUID = 1L;

   public HQResponseException()
   {
      super();
   }

   public HQResponseException(String message)
   {
      super(message);
   }

   public HQResponseException(String message, Throwable cause)
   {
      super(message, cause);
   }

   public HQResponseException(Throwable cause)
   {
      super(cause);
   }
}

不管是请求还是响应都需要对字符串进行操作,所以将字符串操作的方法抽取出来形成一个字符串工具类。

package com.gujin.server.utils;

/**
 * 字符串工具
 * 
 * @author jianggujin
 * 
 */
public class HQString
{
   /**
    * 判断字符串为空
    * 
    * @param s
    * @return
    */
   public static boolean isEmpty(String s)
   {
      return s == null || s.length() == 0;
   }

   /**
    * 判断字符串是否非空
    * 
    * @param s
    * @return
    */
   public static boolean isNotEmpty(String s)
   {
      return !isEmpty(s);
   }

   /**
    * 判断字符串是空白字符
    * 
    * @param s
    * @return
    */
   public static boolean isBlack(String s)
   {
      return isEmpty(s) || s.matches("\\s*");
   }

   /**
    * 判断字符串是非空白字符F
    * 
    * @param s
    * @return
    */
   public static boolean isNotBlack(String s)
   {
      return !isBlack(s);
   }

   /**
    * 截取字符串
    * 
    * @param s
    * @param flag
    * @return
    */
   public static String subStringBefore(String s, String flag)
   {
      if (isEmpty(s) || isEmpty(flag))
      {
         return s;
      }
      int index = s.indexOf(flag);
      if (index != -1)
      {
         return s.substring(0, index);
      }
      return flag;
   }

   /**
    * 截取字符串
    * 
    * @param s
    * @param flag
    * @return
    */
   public static String subStringAfter(String s, String flag)
   {
      if (isEmpty(s) || isEmpty(flag))
      {
         return s;
      }
      int index = s.indexOf(flag);
      if (index != -1)
      {
         return s.substring(index + flag.length());
      }
      return flag;
   }
}

好了,准备工作已经基本完成了,下面我们来编写响应类

package com.gujin.server;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

import com.gujin.server.utils.HQString;

/**
 * HTTP响应
 * 
 * @author jianggujin
 * 
 */
public class HQResponse
{
   /** 缓冲区大小 **/
   private final int BUFSIZE = 512;
   /** 响应时间格式化 **/
   private final String RESPONSE_DATE_TIME = "EEE, dd MMM yyyy HH:mm:ss 'GMT'";
   /** HTTP版本 **/
   private final String HTTP_VERSION = "HTTP/1.1";
   /** 响应时间格式化 **/
   private final SimpleDateFormat RESPONSE_DATE_FORMAT = new SimpleDateFormat(
         RESPONSE_DATE_TIME, Locale.US);
   /** 缓冲输出流 **/
   private ByteArrayOutputStream bufferStream = null;
   /** Socket输出流 **/
   private OutputStream stream = null;
   /** 响应码 **/
   private int statusCode = 200;
   /** 内容类型 **/
   private String contentType;

   /**
    * 构造方法
    * 
    * @param socket
    * @throws IOException
    */
   public HQResponse(Socket socket) throws IOException
   {
      bufferStream = new ByteArrayOutputStream(BUFSIZE);
      stream = socket.getOutputStream();
   }

   /**
    * 向客户端写数据
    * 
    * @param data
    * @throws IOException
    */
   public void write(byte[] data) throws IOException
   {
      bufferStream.write(data);
   }

   /**
    * 向客户端写数据
    * 
    * @param data
    * @param start
    * @param len
    */
   public void write(byte[] data, int start, int len)
   {
      bufferStream.write(data, start, len);
   }

   /**
    * 向客户端发送头信息
    * 
    * @throws IOException
    */
   private void writeHeader() throws IOException
   {
      stream.write(MessageFormat.format("{0} {1} {2}\r\n", HTTP_VERSION,
            statusCode, "OK").getBytes());
      stream.write(MessageFormat.format("Date: {0}\r\n",
            RESPONSE_DATE_FORMAT.format(new Date())).getBytes());
      stream.write("Server: HQHttpServer 1.0".getBytes());
      if (HQString.isNotEmpty(contentType))
      {
         stream.write(MessageFormat
               .format("Content-Type: {0}\r\n", contentType).getBytes());
      }
      stream.write(MessageFormat.format("Content-Length: {0}\r\n",
            bufferStream.size()).getBytes());
   }

   /**
    * 实际响应
    * 
    * @throws IOException
    */
   public void response() throws IOException
   {
      writeHeader();

      // 换一行
      stream.write("\r\n".getBytes());
      bufferStream.writeTo(stream);
      bufferStream.flush();
      stream.flush();
   }

   /**
    * 获得内容类型
    * 
    * @return
    */
   public String getContentType()
   {
      return contentType;
   }

   /**
    * 设置内容类型
    * 
    * @param contentType
    */
   public void setContentType(String contentType)
   {
      this.contentType = contentType;
   }
}

最后我们对上一篇博客中的HQHttpServer类中的handleRequest方法进行改造,将浏览器请求的头信息响应给浏览器,完成一次交互,完整代码如下:

package com.gujin.server;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.logging.Level;

import com.gujin.server.utils.HQClose;

/**
 * 服务端
 * 
 * @author jianggujin
 * 
 */
public class HQHttpServer implements HQHttpServerLog
{
   /** 端口号 **/
   private int port = 80;
   /** 服务套接字 **/
   private ServerSocket serverSocket = null;

   /**
    * 默认构造方法
    */
   public HQHttpServer()
   {
   }

   /**
    * 构造方法
    * 
    * @param port
    */
   public HQHttpServer(int port)
   {
      this.port = port;
   }

   /**
    * 启动服务器
    */
   public synchronized void start()
   {
      try
      {
         serverSocket = new ServerSocket(port);
         LOG.info("server init success.");
      }
      catch (IOException e)
      {
         LOG.log(Level.SEVERE, e.getMessage(), e);
      }
      new Thread()
      {
         public void run()
         {
            while (!isStop())
            {
               Socket socket;
               try
               {
                  socket = serverSocket.accept();
                  handleRequest(socket);
               }
               catch (IOException e)
               {
                  LOG.log(Level.SEVERE, e.getMessage(), e);
               }
            }
         };
      }.start();
   }

   /**
    * 处理请求
    * 
    * @param socket
    * @throws IOException
    */
   public void handleRequest(Socket socket) throws IOException
   {
      HQRequest request = new HQRequest(socket);
      request.execute();
      HQResponse response = new HQResponse(socket);
      response.setContentType("text/plain");

      Iterator<String> iterator = request.getHeaderNames();
      while (iterator.hasNext())
      {
         String name = iterator.next();
         response.write(MessageFormat.format("{0}: {1}", name,
               request.getHeader(name)).getBytes());
      }
      response.response();
      socket.close();
   }

   /**
    * 是否停止
    * 
    * @return
    */
   public boolean isStop()
   {
      return serverSocket == null || serverSocket.isClosed();
   }

   /**
    * 停止服务器
    */
   public synchronized void stop()
   {
      if (!isStop())
      {
         HQClose.safeClose(serverSocket);
         serverSocket = null;
      }
   }

   public static void main(String[] args)
   {
      new HQHttpServer().start();
   }
}

运行程序,在浏览器输入:http://127.0.0.1,我们可以看到浏览器已经可以正常的接收服务端相应的数据了,页面显示内容如下:
ACCEPT-ENCODING: gzip,deflate,sdchHOST: 127.0.0.1USER-AGENT: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36CONNECTION: keep-aliveACCEPT: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8ACCEPT-LANGUAGE: zh-CN,zh;q=0.8

打开浏览器的开发者工具观察服务端响应的数据信息
JAVA简易WEB服务器(三)

我们可以看到浏览器接收到的头信心与我们相应的数据是一致的。
到这里,一个最简单的WEB服务器已经完成了与浏览器的一次完成整的交互,接下来我们要继续对其优化。