在上一篇博客中《JAVA简易WEB服务器(一)》我们了解了浏览器与服务端之间的通信数据的格式。这一篇博客开始,我们会一步一步的完成一个简易的WEB服务器的开发,需要注意的是,这里介绍的只是一种思路,毕竟开发一个服务器的工作量是很大的,而且需要考虑的事情很多,这里面我们只是简单的实现其部分功能,有兴趣可以自己进行扩展,深入研究。
言归正传,这一篇博客我们需要实现的是对浏览器请求的解析。
首先,为了我们调试的方便,我们先来创建一个日志接口,用于输出日志内容:
package com.gujin.server;
import java.util.logging.Logger;
/**
* 日志
*
* @author jianggujin
*
*/
public interface HQHttpServerLog
{
/**
* 日志对象
*/
public Logger LOG = Logger.getLogger(HQHttpServerLog.class.getName());
}
我们知道,浏览器请求的方法有POST、GET、PUT等等,这里我们将其抽取出来,用枚举表示:
package com.gujin.server;
/**
* 请求方法
*
* @author jianggujin
*
*/
public enum HQMethod
{
/** GET **/
GET,
/** POST **/
POST
}
在进行网络通信的时候,我们需要对套接字等进行关闭、释放的操作,所以我们可以为其编写一个工具类,用于执行其关闭方法:
package com.gujin.server.utils;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Level;
import com.gujin.server.HQHttpServerLog;
/**
* 关闭工具
*
* @author jianggujin
*
*/
public class HQClose implements HQHttpServerLog
{
/**
* 安全关闭
*
* @param closeable
*/
public static final void safeClose(Object closeable)
{
try
{
if (closeable != null)
{
if (closeable instanceof Closeable)
{
((Closeable) closeable).close();
}
else if (closeable instanceof Socket)
{
((Socket) closeable).close();
}
else if (closeable instanceof ServerSocket)
{
((ServerSocket) closeable).close();
}
else
{
Class<?> clazz = closeable.getClass().getClass();
Method method;
try
{
method = clazz.getMethod("close");
try
{
method.invoke(closeable);
return;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
catch (Exception e)
{
}
throw new IllegalArgumentException("Unknown object to close");
}
}
}
catch (IOException e)
{
LOG.log(Level.SEVERE, "Could not close", e);
}
}
}
好了,我们的准备工作已经完成,后续需要的话,我们再继续添加,现在我们来完成对请求数据的解析工作,我们再来看一下请求的数据格式:
POST /?test=123 HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Content-Length: 8
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: null
User-Agent: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8
name=bob
我们要解析请求的数据,那么我们需要解决的问题就是什么时候内容结束,通过观察GET方式和POST方式请求的数据我们可以知道,当浏览器以GET方式进行请求时,最后一行头信息会紧跟上两个\r\n
,这个标记就代表了数据结束,当为POST方式请求数据时,在头信息中会有Content-Length
信息,代表请求内容的长度,当我们解析到一个空行后再继续读取相应长度的数据即为客户端请求的完整数据。
下面,我们来编写请求的解析类,并为其提供一些公用的方法:
package com.gujin.server;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* HTTP请求
*
* @author jianggujin
*
*/
public class HQRequest
{
/** 缓冲区大小 **/
private final int BUFSIZE = 512;
/** 字节输出流 **/
private ByteArrayOutputStream outputStream = null;
/** Socket输入流 **/
private InputStream stream = null;
/** 请求方法 **/
private HQMethod method;
/** 请求路径 **/
private String url;
/** 查询字符串 **/
private String queryString;
/** HTTP版本 **/
private String version;
/** 请求头 **/
private Map<String, List<String>> headers = null;
/** 请求参数 **/
private Map<String, List<String>> params = null;
/** 请求编码 **/
private String charset = null;
/** 套接字 **/
private Socket socket = null;
/**
* 构造方法
*
* @param socket
* @throws IOException
*/
public HQRequest(Socket socket) throws IOException
{
this.socket = socket;
outputStream = new ByteArrayOutputStream(512);
headers = new HashMap<String, List<String>>();
params = new HashMap<String, List<String>>();
this.stream = socket.getInputStream();
}
/**
* 执行解析
*
* @throws IOException
*/
public void execute() throws IOException
{
parseFirstLine();
parseHeaders();
parseBody();
}
/**
* 解析请求第一行,请求方法、请求地址、HTTP版本
*
* @throws IOException
*/
private void parseFirstLine() throws IOException
{
String line = readLine();
try
{
int index = line.indexOf(' ');
int lastIndex = line.lastIndexOf(' ');
this.method = HQMethod.valueOf(line.substring(0, index).toUpperCase());
String fullUrl = line.substring(index + 1, lastIndex);
int tmp = fullUrl.indexOf('?');
if (tmp > 0)
{
this.url = fullUrl.substring(0, tmp);
this.queryString = fullUrl.substring(tmp + 1);
dealParamString(queryString);
}
else
{
this.url = fullUrl;
}
this.version = line.substring(lastIndex + 1).toUpperCase();
}
catch (Exception e)
{
e.printStackTrace();
throw new HQRequestException("Request format unqualified.");
}
}
/**
* 解析请求头
*/
private void parseHeaders() throws IOException
{
String line = null;
while ((line = readLine()) != null)
{
// 分隔符位置
if (line.length() == 0)
{
break;
}
addHeader(line);
}
}
/**
* 添加请求头信息
*
* @param line
*/
private void addHeader(String line)
{
int index = line.indexOf(": ");
String name = line.substring(0, index).toUpperCase();
String value = line.substring(index + 2);
List<String> list = this.headers.get(name);
if (list == null)
{
list = new ArrayList<String>(1);
this.headers.put(name, list);
}
list.add(value);
}
/**
* 处理查询参数
*
* @param queryString
*/
private void dealParamString(String queryString)
{
if (!isEmpty(queryString))
{
String[] params = queryString.split("&");
if (params != null)
{
for (String param : params)
{
addParameter(param);
}
}
}
}
/**
* 添加参数
*
* @param name
* @param value
*/
private void addParameter(String line)
{
int index = line.indexOf("=");
String name = line.substring(0, index);
String value = line.substring(index + 1);
List<String> values = params.get(name);
if (values == null)
{
values = new ArrayList<String>();
params.put(name, values);
}
values.add(value);
}
/**
* 是否为空
*
* @param msg
* @return
*/
private boolean isEmpty(String msg)
{
return msg == null || msg.length() == 0;
}
/**
* 获得请求参数
*
* @param name
* @return
*/
public String getParameter(String name)
{
List<String> values = params.get(name);
if (values != null && !values.isEmpty())
{
if (isEmpty(charset))
{
return values.get(0);
}
else
{
try
{
return URLDecoder.decode(values.get(0), charset);
}
catch (UnsupportedEncodingException e)
{
}
}
}
return null;
}
/**
* 获得请求参数名称
*
* @return
*/
public Iterator<String> getParameterNames()
{
return params.keySet().iterator();
}
/**
* 获得请求参数对应值
*
* @param name
* @return
*/
public String[] getParameterValues(String name)
{
List<String> values = params.get(name);
if (values != null)
{
if (isEmpty(charset))
{
return values.toArray(new String[0]);
}
else
{
int len = values.size();
String[] v = new String[len];
try
{
for (int i = 0; i < len; i++)
{
v[i] = URLDecoder.decode(values.get(i), charset);
}
return v;
}
catch (UnsupportedEncodingException e)
{
}
}
}
return null;
}
/**
* 解析请求体
*
* @throws IOException
*/
private void parseBody() throws IOException
{
if (HQMethod.POST == method)
{
int len = getContentLength() - outputStream.size();
if (len > 0)
{
byte[] buffer = new byte[BUFSIZE];
long total = 0;
int readLen = -1;
do
{
long left = len - total;
if (left <= 0)
{
break;
}
if (left >= BUFSIZE)
{
readLen = stream.read(buffer);
}
else
{
readLen = stream.read(buffer, 0, (int) left);
}
if (readLen < 0)
{
break;
}
outputStream.write(buffer, 0, readLen);
total += readLen;
} while (total < len);
outputStream.flush();
if (isEmpty(charset))
{
dealParamString(outputStream.toString());
}
else
{
dealParamString(outputStream.toString(charset));
}
}
}
}
/**
* 从输入流中读取一行
*
* @return
* @throws IOException
*/
private String readLine() throws IOException
{
String line = null;
int i = -1;
while ((i = stream.read()) != -1)
{
if (i == '\r')
{
outputStream.flush();
line = outputStream.toString();
outputStream.reset();
i = stream.read();
if (i != '\n')
{
outputStream.write(i);
}
break;
}
else if (i == '\n')
{
outputStream.flush();
line = outputStream.toString();
outputStream.reset();
break;
}
outputStream.write(i);
}
return line;
}
/**
* 获得请求方法
*
* @return
*/
public HQMethod getMethod()
{
return method;
}
/**
* 获得请求路径
*
* @return
*/
public String getUrl()
{
return url;
}
/**
* 获得HTTP版本
*
* @return
*/
public String getVersion()
{
return version;
}
/**
* 获得请求头信息
*
* @param name
* @return
*/
public String getHeader(String name)
{
List<String> values = headers.get(name.toUpperCase());
if (values != null && !values.isEmpty())
{
return values.get(0);
}
return null;
}
/**
* 获得请求头名称
*
* @return
*/
public Iterator<String> getHeaderNames()
{
return headers.keySet().iterator();
}
/**
* 获得请求头对应值
*
* @param name
* @return
*/
public List<String> getHeaderValues(String name)
{
return headers.get(name);
}
/**
* 获得内容长度
*
* @return
*/
public int getContentLength()
{
String contentLength = getHeader("Content-Length");
if (contentLength != null && contentLength.matches("\\d+"))
{
return Integer.parseInt(contentLength);
}
return -1;
}
/**
* 获得内容类型
*
* @return
*/
public String getContentType()
{
return getHeader("Content-Type");
}
/**
* 获得编码
*
* @return
*/
public String getCharset()
{
return charset;
}
/**
* 设置编码
*
* @param charset
*/
public void setCharset(String charset)
{
this.charset = charset;
}
/**
* 获得查询字符串
*
* @return
*/
public String getQueryString()
{
return queryString;
}
/**
* 获得远程主机地址
*
* @return
*/
public String getRemoteAddr()
{
return socket.getInetAddress().getHostAddress();
}
/**
* 获得远程主机名
*
* @return
*/
public String getRemoteHost()
{
return socket.getInetAddress().getHostName();
}
/**
* 获得远程主机端口
*
* @return
*/
public int getRemotePort()
{
return socket.getPort();
}
/**
* 获得本地主机名称
*
* @return
*/
public String getLocalName()
{
return socket.getLocalAddress().getHostName();
}
/**
* 获得本地主机地址
*
* @return
*/
public String getLocalAddr()
{
return socket.getLocalAddress().getHostAddress();
}
/**
* 获得本地主机端口
*
* @return
*/
public int getLocalPort()
{
return socket.getLocalPort();
}
/**
* 获得输入流
*
* @return
*/
public InputStream getInputStream()
{
return new ByteArrayInputStream(outputStream.toByteArray());
}
}
解析异常的类如下:
package com.gujin.server;
/**
* 请求异常
*
* @author jianggujin
*
*/
public class HQRequestException extends RuntimeException
{
private static final long serialVersionUID = 1L;
public HQRequestException()
{
super();
}
public HQRequestException(String message)
{
super(message);
}
public HQRequestException(String message, Throwable cause)
{
super(message, cause);
}
public HQRequestException(Throwable cause)
{
super(cause);
}
}
现在,我们已经完成了请求解析的实现,有点小激动啊,下面我们来编写Server,用于启动、处理浏览器请求,此处我们接收到请求后进打印出请求的头信息
package com.gujin.server;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
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();
Iterator<String> iterator = request.getHeaderNames();
while (iterator.hasNext())
{
String name = iterator.next();
System.err.printf("%s: %s\n", name, request.getHeader(name));
}
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
,我们可以看到控制台输出如下信息:
2016-2-23 18:32:19 com.gujin.server.HQHttpServer start
信息: server init success.
ACCEPT-ENCODING: gzip,deflate,sdch
HOST: 127.0.0.1
USER-AGENT: Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
CONNECTION: keep-alive
ACCEPT: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
ACCEPT-LANGUAGE: zh-CN,zh;q=0.8
至此,我们对浏览器的请求的解析已经基本完成了,后面我们会继续对其进行完善。