这里使用IntelliJ IDEA
,Maven WebApp
项目,不过这里我们不会使用/启动Tomcat服务器
。
本文的目的就是使用Socket实现一个服务器;此服务器是一个Servlet容器
,我们需要遵循Servlet接口规范,即javax.servlet.*
。
这里由于我们使用的是Maven项目,所以这里引入servlet api 依赖,servlet api的版本为3.1
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
dependency>
我们查看Servlet接口,发现其接口方法只有5个
public interface Servlet {
public void init(ServletConfig config) throws ServletException;
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
public void destroy();
public ServletConfig getServletConfig();
public String getServletInfo();
}
其中init,service,destroy
是3个与Servlet生命周期相关的函数。getServletConfig(),getServletInfo
从方法名可以看出,它们与Servlet的信息相关。
当实例化某个Servlet类后
,servlet会调用其init方法来对servlet进行初始化
,servlet容器只会调用该方法一次
,调用后就可以执行service方法
来处理请求相应逻辑。在servlet接收任何请求之前,必须是经过正确初始化的
,初始化在servlet的生命周期中只会执行一次
,所以我们可以在init方法中做一些初始化操作,如初始化默认值,载入数据库驱动等。。一般情况,我们可以将init方法留空,什么也不做。
当客户端的请求到达时,servlet容器就会响应相应servlet的service方法
,并传入javax.servlet.ServletReqeust
,javax.servlet.ServletResponse
到service方法,其中ServletReqeust,ServletResponse分别包含HTTP请求响应的相关信息。service方法会在客户端每次请求时反复地被调用。
在Servlet实例服务移除前,servlet容器会调用servlet实例的destroy
方法,一般当servlet容器关闭或者要释放内存时,才会将servlet实例移除。当且仅当servlet实例的service方法中所有线程都退出或执行超时后,才会调用destroy方法。
所以,现在我们总结以下,Servlet容器的处理流程:
1.
当客户端请求服务器时,第一次调用某个servlet时,要先加载该servlet类,并调用其init方法(仅此一次)
2.
针对于每个请求,都会创建一个 javax.servlet.ServletReqeust
实例和一个javax.servlet.ServletResponse
实例3.
调用该servlet的service方法,刚创建的ServletReqeust,ServletResponse作为service方法的参数。
4.
当关闭该servlet类时,调用其destroy方法,并卸载该servlet类。所以现在我们可以简单地实现一个自己的Servlet类,我们称之为PrimitiveServlet
,它继承自servlet接口,所以我们必须要重写servlet的5个方法:
public class PrimitiveServlet implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
System.out.println("Init...");
}
@Override
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
System.out.println("From service");
PrintWriter out = response.getWriter();
// 头部信息
out.write("HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html\r\n" +
"\r\n" );
out.println("Hello "
+ this.getClass().getSimpleName() + " ");
}
@Override
public void destroy() {
System.out.println("Destroy...");
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public String getServletInfo() {
return null;
}
}
我们看到,这里的servlet的service方法,我们简单地返回一个html文本,其内容是
Hello ${ServletName}
。同时,我们暂时只对servlet的3个生命周期函数做一个简单实现。
我们的任务就是根据不同请求的URL地址,来做出不同的响应,这里我们做的很简单,假定用户只会按照下面的格式来访问我们的服务器。
http://localhost:8080/staticResource
,此类Url访问,我们当作静态资源的请求http://localhost:8080/servlet/servletName
,此类Url访问,我们当作一个Servlet请求http://localhost:8080/SHUTDOWN
,关闭服务器。这里我们定义一个HTTP Server服务器的的主启动类
,名为TomHttpServer1
,它主要用来创建服务端Socket,并处理Socket请求。
public class TomHttpServer1 {
Logger logger = LoggerFactory.getLogger(TomHttpServer1.class);
public static final String SHUTDOWN = "/SHUTDOWN";
public static final int PORT = 8080;
private boolean isShutDown = false;
public static void main(String[] args) {
TomHttpServer1 httpServer = new TomHttpServer1();
httpServer.await();
}
public void await() {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(PORT, 1, InetAddress.getByName("localhost"));
} catch (Exception e){
e.printStackTrace();
System.exit(1);
}
while(!isShutDown){
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();
// 创建ServletRequest,ServletResponse
TomServletRequest servletRequest = new TomServletRequest(input);
TomServletResponse servletResponse = new TomServletResponse(output, servletRequest);
// process
if(servletRequest.getUri() != null && servletRequest.getUri().startsWith("/servlet/")){
// 如果是请求Servlet
ServletProcessor1 sp1 = new ServletProcessor1();
sp1.process(servletRequest, servletResponse);
} else {
// 否则,我们认为它是请求静态资源
StaticResourceProcessor srp = new StaticResourceProcessor();
srp.process(servletRequest, servletResponse);
}
socket.close();
isShutDown = servletRequest.getUri().equals(SHUTDOWN);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
我们看到main函数,await方法为主要的逻辑所在。await方法首先初始化一个监听在8080端口的ServerSocket,然后根据Servlet 规范,我们创建ServletRequest,ServletResponse两个接口的实现类TomServletRequest,TomServletResponse实例
,然后根据请求的URI来判断是请求静态资源还是请求servlet
servletRequest.getUri().startsWith("/servlet/")
如果是Servlet请求,我们就交由ServletProcessor1
去处理,否则交由StaticResourceProcessor
去处理。
这里的sp1.process(servletRequest, servletResponse);
和srp.process(servletRequest, servletResponse);
暂时可以简单地理解为service(servletRequest,servletResponse)
方法。
然后isShutDown = servletRequest.getUri().equals(SHUTDOWN);
表示如果用户输入如下URL,整个服务器就停止运行。
http://localhost:8080/SHUTDOWN
上面代码的4个类TomServletRequest,TomServletResponse,StaticResourceProcessor ,StaticResourceProcessor
我们现在仍然没有实现,接下来,会一一实现。
public class TomServletRequest implements ServletRequest {
private Logger logger = LoggerFactory.getLogger(TomServletRequest.class);
private InputStream input;
private String uri;
public TomServletRequest(InputStream input) {
this.input = input;
this.parse();
}
private void parse(){
StringBuffer requestStringBuffer = new StringBuffer(2048);
int len = -1;
byte[] buffer = new byte[2048];
try{
len = input.read(buffer);
} catch (Exception e){
e.printStackTrace();
}
for(int j = 0 ; j < len ; j ++){
requestStringBuffer.append((char) buffer[j]);
}
logger.info("request string: \n{}", requestStringBuffer);
uri = parseUri(requestStringBuffer.toString());
logger.info("parse uri: \n{}", uri);
}
public String getUri() {
return uri;
}
/**
* The first line of the http header is just like this :
* GET /servlet/tomservlet HTTP/1.1
* @param requestString
* @return
*/
private String parseUri(String requestString){
int index1, index2;
index1 = requestString.indexOf(' ');
if(index1 != -1 ){
index2 = requestString.indexOf(' ', index1 + 1);
if(index2 > index1){
return requestString.substring(index1 + 1, index2);
}
}
return null;
}
/**
* 下面是部分重写的代码,代码实现为空
* 后面我们将一一实现,这里暂时省略,一面占篇幅
*/
Socket接收到请求后,就获取socket对应的InputStream,并传入TomServletRequest类的构造函数TomServletRequest(InputStream input)以创建一个ServletRequest类。
这里我们注意到一个parse()方法,它能够解析出HTTP头信息的请求URI地址,核心代码就是parseUri
函数。parseUri函数
很容易理解,我们可以参考下面的HTTP头信息的第一行理解一下:我们只需要找到第一个空格
和第二个空格的索引
即可,两个索引之间的字符就是HTTP请求的URI地址。
GET /servlet/tomservlet HTTP/1.1
定义常量类,定义当前maven web的根目录。
public class Constants {
public static final String WEB_ROOT = new File("").getAbsoluteFile().getPath()
+ "\\src\\main\\webapp";
public class TomServletResponse implements ServletResponse {
private static final int BUFFER_SIZE = 1024;
private OutputStream out;
private TomServletRequest servletRequest;
private PrintWriter writer;
public TomServletResponse(OutputStream out, TomServletRequest servletRequest) {
this.out = out;
this.servletRequest = servletRequest;
}
public void sendStaticResources() throws IOException {
FileInputStream fis = null;
try{
File file = new File(Constants.WEB_ROOT, servletRequest.getUri());
fis = new FileInputStream(file);
// 头部信息
out.write(
("HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html\r\n" +
"\r\n" ).getBytes());
byte[] bytes = new byte[BUFFER_SIZE];
int ch = -1;
while ( (ch = fis.read(bytes, 0, BUFFER_SIZE) )!=-1) {
out.write(bytes, 0, ch);
}
} catch (FileNotFoundException e) {
String notFoundMessage =
"HTTP/1.1 404 FILE NOT FOUND\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: 28\r\n" +
"\r\n" +
"404: FILE NOT FOUND
";
out.write(notFoundMessage.getBytes());
} catch (Exception e){
e.printStackTrace();
} finally{
if(fis != null){
fis.close();
}
}
}
@Override
public PrintWriter getWriter() throws IOException {
// true stand for autoFlush
writer = new PrintWriter(out, true);
return writer;
}
/**
* 下面是重写ServletResponse的方法
* 之后我们会实现它们
*/
这里sendStaticResources
为核心方法,意思是发送静态资源。通过之前request的parse解析出URI,那么现在我们已经知道了客户端请求的URI是什么了,所以我们可以根据URI来定位到具体的静态文件,并发送给客户端。
上面的代码很容易理解,就是读取静态资源文件然后返回给客户端,我们假定请求的静态资源都是html文档。
这里要注意头部信息要带上,不然浏览器输入URL时,浏览器会报错ERR_INVALID_HTTP_RESPONSE
,得不到响应。
StaticResourceProcessor,静态资源处理器。我们直接使用TomServletResponse实现的方法sendStaticResources即可。
public class StaticResourceProcessor {
public void process(TomServletRequest servletRequest, TomServletResponse servletResponse){
try{
servletResponse.sendStaticResources();
} catch (Exception e){
e.printStackTrace();
}
}
}
静态资源的处理,我们已经解决了,就是通过请求的URI来定位到Web根目录下的文件,并读取放回给客户端。
但是,我们如何通过servlet字符串来定位到Servlet类,并且交由Servlet类来处理此Servlet请求呢?
我们先上源码,然后再细细分析。
public class ServletProcessor1 {
public void process(TomServletRequest servletRequest, TomServletResponse servletResponse){
String uri = servletRequest.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
URLClassLoader loader = null;
try{
// 我们的请求中URL只有一个,所以实例化一个大小为1的URL数组
URL[] urls = new URL[1];
URLStreamHandler urlStreamHandler = null;
File classPath = new File(Constants.WEB_ROOT);
// repository,从此URL目录("仓库")来加载类
String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString();
urls[0] = new URL(null, repository, urlStreamHandler);
// 从urls指定的url来加载类
loader = new URLClassLoader(urls);
} catch (Exception e){
e.printStackTrace();
}
Class<?> clazz = null;
try{
clazz = loader.loadClass(servletName);
} catch (Exception e){
e.printStackTrace();
}
Servlet servlet = null;
try{
servlet = (Servlet) clazz.newInstance();
servlet.service(servletRequest, servletResponse);
} catch (Exception e){
e.printStackTrace();
}
}
}
首先我们要介绍的一个类就是URLClassLoader类,它是ClassLoader类的直接子类,它的构造函数函数URLClassLoader(URL[])
接收一个URL数组,意思是,从这些URL目录下去加载Servlet类。
下面是核心代码,通过Class类来newInstance,并调用其service方法来处理Servlet请求
。
// 实例化类加载器
URLClassLoader loader = new URLClassLoader(urls);
// 使用类加载器来加载Servlet类
Class<?> clazz = loader.loadClass(servletName);
// 实例化Servlet类
Servlet servlet = (Servlet) clazz.newInstance();
// 调用其service方法
servlet.service(servletRequest, servletResponse);
现在我们可以开始访问了。