webserver初体验

1 背景知识--http/1.1

HTTP请求报文解剖 

HTTP请求报文由3部分组成(请求行+请求头+请求体): 

下面是一个实际的请求报文: 

①是请求方法,GET和POST是最常见的HTTP方法,除此以外还包括DELETE、HEAD、OPTIONS、PUT、TRACE。不过,当前的大多数浏览器只支持GET和POST,Spring 3.0提供了一个HiddenHttpMethodFilter,允许你通过“_method”的表单参数指定这些特殊的HTTP方法(实际上还是通过POST提交表单)。服务端配置了HiddenHttpMethodFilter后,Spring会根据_method参数指定的值模拟出相应的HTTP方法,这样,就可以使用这些HTTP方法对处理方法进行映射了。 

②为请求对应的URL地址,它和报文头的Host属性组成完整的请求URL,③是协议名称及版本号。 

④是HTTP的报文头,报文头包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。 

⑤是报文体,它将一个页面表单中的组件值通过param1=value1&param2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求URL也可以通过类似于“/chapter15/user.html? param1=value1&param2=value2”的方式传递请求参数。 

对照上面的请求报文,我们把它进一步分解,你可以看到一幅更详细的结构图: 

HTTP响应报文解剖 

响应报文结构 

HTTP的响应报文也由三部分组成(响应行+响应头+响应体): 

以下是一个实际的HTTP响应报文: 

①报文协议及版本; 
②状态码及状态描述; 
③响应报文头,也是由多个属性组成; 
④响应报文体,即我们真正要的“干货”。 

响应状态码 

和请求报文相比,响应报文多了一个“响应状态码”,它以“清晰明确”的语言告诉客户端本次请求的处理结果。 

HTTP的响应状态码由5段组成: 

  • 1xx 消息,一般是告诉客户端,请求已经收到了,正在处理,别急...
  • 2xx 处理成功,一般表示:请求收悉、我明白你要的、请求已受理、已经处理完成等信息.
  • 3xx 重定向到其它地方。它让客户端再发起一个请求以完成整个处理。
  • 4xx 处理发生错误,责任在客户端,如客户端的请求一个不存在的资源,客户端未被授权,禁止访问等。
  • 5xx 处理发生错误,责任在服务端,如服务端抛出异常,路由出错,HTTP版本不支持等。



以下是几个常见的状态码: 

200 OK 

你最希望看到的,即处理成功! 

303 See Other 

我把你redirect到其它的页面,目标的URL通过响应报文头的Location告诉你。 

引用
悟空:师傅给个桃吧,走了一天了 
唐僧:我哪有桃啊!去王母娘娘那找吧



304 Not Modified 

告诉客户端,你请求的这个资源至你上次取得后,并没有更改,你直接用你本地的缓存吧,我很忙哦,你能不能少来烦我啊! 

404 Not Found 

你最不希望看到的,即找不到页面。如你在google上找到一个页面,点击这个链接返回404,表示这个页面已经被网站删除了,google那边的记录只是美好的回忆。 

500 Internal Server Error 

看到这个错误,你就应该查查服务端的日志了,肯定抛出了一堆异常,别睡了,起来改BUG去吧! 


其它的状态码参见:http://en.wikipedia.org/wiki/List_of_HTTP_status_codes 


有些响应码,Web应用服务器会自动给生成。你可以通过HttpServletResponse的API设置状态码: 

Java代码  
  1. //设置状态码,状态码在HttpServletResponse中通过一系列的常量预定义了,如SC_ACCEPTED,SC_OK  
  2. void    setStatus(int sc)   



常见的HTTP响应报文头属性 

Cache-Control 

响应输出到客户端后,服务端通过该报文头属告诉客户端如何控制响应内容的缓存。 

下面的设置让客户端对响应内容缓存3600秒,也即在3600秒内,如果客户再次访问该资源,直接从客户端的缓存中返回内容给客户,不要再从服务端获取(当然,这个功能是靠客户端实现的,服务端只是通过这个属性提示客户端“应该这么做”,做不做,还是决定于客户端,如果是自己宣称支持HTTP的客户端,则就应该这样实现)。 

Java代码 
  1. Cache-Control: max-age=3600  



ETag 

一个代表响应服务端资源(如页面)版本的报文头属性,如果某个服务端资源发生变化了,这个ETag就会相应发生变化。它是Cache-Control的有益补充,可以让客户端“更智能”地处理什么时候要从服务端取资源,什么时候可以直接从缓存中返回响应。 

关于ETag的说明,你可以参见:http://en.wikipedia.org/wiki/HTTP_ETag。 
Spring 3.0还专门为此提供了一个org.springframework.web.filter.ShallowEtagHeaderFilter(实现原理很简单,对JSP输出的内容MD5,这样内容有变化ETag就相应变化了),用于生成响应的ETag,因为这东东确实可以帮助减少请求和响应的交互。 

下面是一个ETag: 

Java代码  
  1. ETag: "737060cd8c284d8af7ad3082f209582d"  



Location 

我们在JSP中让页面Redirect到一个某个A页面中,其实是让客户端再发一个请求到A页面,这个需要Redirect到的A页面的URL,其实就是通过响应报文头的Location属性告知客户端的,如下的报文头属性,将使客户端redirect到iteye的首页中: 

Java代码  
  1. Location: http://www.iteye.com  



Set-Cookie 

服务端可以设置客户端的Cookie,其原理就是通过这个响应报文头属性实现的: 

Java代码  
  1. Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1  




其它HTTP响应报文头属性 

更多其它的HTTP响应头报文,参见:http://en.wikipedia.org/wiki/List_of_HTTP_header_fields 


如何写HTTP请求报文头 

在服务端可以通过HttpServletResponse的API写响应报文头的属性: 

Java代码  
  1. //添加一个响应报文头属性  
  2. void    setHeader(String name, String value)   



象Cookie,Location这些响应都是有福之人,HttpServletResponse为它们都提供了VIP版的API: 

Java代码  
    1. //添加Cookie报文头属性  
    2. void addCookie(Cookie cookie)   
    3.   
    4. //不但会设置Location的响应报文头,还会生成303的状态码呢,两者天仙配呢  
    5. void    sendRedirect(String location)  

 下面我已一个具体的案例来实际开发出一个webserver原型(比较简陋):

该项目共分为6个包,分别为:

1 域模型层下主要是用户实体类等,该类只含有属性和set以及get等方法,不含有具体的业务逻辑。我的理解是该类就是一个封装了各种信息的实体类,用于供dao层服务,该类一般实现了序列化接口。

具体的UserInfo类代码如下:

package com.tedu.vo;

import java.io.Serializable;

/**
 * 用户实体类
 * @author Administrator
 *
 */
public class UserInfo implements Serializable{
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	
	private String username;
	private String password;
	private String nickname;
	private String phonenumber;
	
	
	public UserInfo() {
		super();
	}


	public UserInfo(String username, String password, String nickname, String phonenumber) {
		super();
		this.username = username;
		this.password = password;
		this.nickname = nickname;
		this.phonenumber = phonenumber;
	}


	public String getUsername() {
		return username;
	}


	public void setUsername(String username) {
		this.username = username;
	}


	public String getPassword() {
		return password;
	}


	public void setPassword(String password) {
		this.password = password;
	}


	public String getNickname() {
		return nickname;
	}


	public void setNickname(String nickname) {
		this.nickname = nickname;
	}


	public String getPhonenumber() {
		return phonenumber;
	}


	public void setPhonenumber(String phonenumber) {
		this.phonenumber = phonenumber;
	}


	@Override
	public String toString() {
		return "UserInfo [username=" + username + ", password=" + password + ", nickname=" + nickname + ", phonenumber="
				+ phonenumber + "]";
	}
	
	

}

该类非常纯粹,不含有任何逻辑判断。

2 开发数据访问层(dao、dao.impl)

在该层中我并没有使用接口,直接定义了一个具体的实现类,该类直接与数据库等数据源进行交互,一般都离不开基本的CRUD(增删改查)操作,Dao层是直接和数据库交互的,所以Dao层的接口一般都会有增删改查这四种操作的相关方法。

类中方法的参数或者返回值一般为用户实体类实例,对于一些保存等操作,会有返回值标识是否保存成功

package com.tedu.dao;

import java.io.RandomAccessFile;
import java.util.Arrays;

import com.tedu.vo.UserInfo;

public class UserInfoDAO {
	
	
	//保存用户信息
	/**
	 * 如果写入成功则返回true 写入失败返回false
	 * @param userinfo
	 * @return
	 */
	public boolean save(UserInfo userinfo) {
		try(RandomAccessFile raf=new RandomAccessFile("user.dat", "rw")){
			
			//先将指针移动到文件末尾
			raf.seek(raf.length());
			
			String username=userinfo.getUsername();
			String password=userinfo.getPassword();
			String nickname=userinfo.getNickname();
			String phonenumber=userinfo.getPhonenumber();
			
			//
			writeString(raf,username,20);
			writeString(raf,password,32);
			writeString(raf,nickname,32);
			writeString(raf,phonenumber,32);
			
			return true;
			
		}catch(Exception e) {
			e.printStackTrace();
		}
		
		return false;
		
	}

	private void writeString(RandomAccessFile raf, String string, int len) {

		try {
			
			byte [] data=string.getBytes("utf-8");
			data=Arrays.copyOf(data	, len);
			
			raf.write(data);
		}catch(Exception e) {
			e.printStackTrace();
		}
		
	}

	/**
	 * 在数据库中通过名字查找
	 * 如果没找到,则返回false 
	 * 找到该用户,则返回该用户
	 * @param username
	 * @return
	 */
	public UserInfo findUserByUsername(String username) {
		try(RandomAccessFile raf=new RandomAccessFile("user.dat", "rw")){
			
			for (int i = 0; i < raf.length()/116; i++) {
				raf.seek(i*116);
				String name=readString(raf, 20);
				if(name.equals(username)) {
					//找到了
					String pwd=readString(raf, 32);
					String nickname=readString(raf, 32);
					String phonenumber=readString(raf, 32);
					
					return new UserInfo(name, pwd, nickname, phonenumber);
				}
				
			}
			
		}catch(Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	
	private String readString(RandomAccessFile raf,int len) {
		try {
			byte [] data=new byte[len];
			raf.read(data);
			String str=new String(data, "utf-8").trim();
			
			return str;
		}catch(Exception e) {
			e.printStackTrace();
		}
		return null;
		
	}
	
	public boolean update(UserInfo userinfo) {
		try(RandomAccessFile raf=new RandomAccessFile("user.dat", "rw")){
			
			
			String username=userinfo.getUsername();
			String password=userinfo.getPassword();
			String nickname=userinfo.getNickname();
			String phonenumber=userinfo.getPhonenumber();
			
			//先查找到要修改的记录位置
			for (int i = 0; i < raf.length()/116; i++) {
				raf.seek(i*116);
				String name=readString(raf, 20);
				if(name.equals(username)) {
					writeString(raf, password, 32);
					writeString(raf, nickname, 32);
					writeString(raf, phonenumber, 32);
					
					
				}
				
			}
			
			
			return true;
			
		}catch(Exception e) {
			e.printStackTrace();
		}
		
		return false;
		
	}

}

3 开发web层

开发注册功能 该层会抽象出HttpServlet,所有Servlet继承于它。

从该层开始与浏览器request和response发生关系,可以使用request从请求端获取用户信息,并使用这些信息进行业务逻辑。

package com.tedu.servlet;

import com.tedu.dao.UserInfoDAO;
import com.tedu.http.HttpRequest;
import com.tedu.http.HttpResponse;
import com.tedu.vo.UserInfo;

public class RegServlet extends HttpServlet{

	@Override
	public void service(HttpRequest request, HttpResponse response) {

		try {
			//从请求端获取用户信息
			String username=request.getParameter("username");
			String password=request.getParameter("password");
			String nickname=request.getParameter("nickname");
			String phonenumber=request.getParameter("phonenumber");
			
			UserInfoDAO dao=new UserInfoDAO();
			
			if(dao.findUserByUsername(username)==null) {
				
				//新用户
				UserInfo newuser=new UserInfo(username, password, nickname, phonenumber);
				dao.save(newuser);
				forward("/myweb/reg_success.html",request,response);
				
			}else {
				forward("/myweb/reg_fail.html", request, response);
			}
			
		}catch(Exception e) {
			e.printStackTrace();
		}
	}

	
	
	

}

RegServlet担任着以下几个职责:

1、接收客户端提交到服务端的表单数据。通过request对象取得表单数据。

2、校验表单数据的合法性,如果校验失败回显错误信息。

3、如果校验通过,调用dao层向数据库中注册用户。

4 开发ClientHandler,通过解析requestURI来判断是何种业务,然后调用具体的servlet来完成功能;如果是文件资源,则直接调用response直接写回给浏览器。

package com.tedu;

import java.io.File;
import java.net.Socket;

import com.tedu.context.HttpContext;
import com.tedu.http.HttpRequest;
import com.tedu.http.HttpResponse;
import com.tedu.servlet.LoginServlet;
import com.tedu.servlet.RegServlet;
import com.tedu.servlet.UpdateServlet;

public class ClientHandler implements Runnable{
	private Socket socket;

	public ClientHandler(Socket socket) {
		super();
		this.socket = socket;
	}

	@Override
	public void run() {
		try {
			
		
			HttpRequest request=new HttpRequest(socket.getInputStream());
			
			
			HttpResponse response=new HttpResponse(socket.getOutputStream());
			

			//获取请求路径
			String requestURI=request.getRequestURI();
			
			//判断是否为注册业务
			if("/myweb/reg".equals(requestURI)) {
				RegServlet reg=new RegServlet();
				reg.service(request, response);
			}else if("/myweb/log_in".equals(requestURI)) {
				LoginServlet log=new LoginServlet();
				log.service(request, response);
			}else if("/myweb/update".equals(requestURI)) {
				UpdateServlet update=new UpdateServlet();
				update.service(request, response);
			}
			
			
			//需要判断请求的文件是否存在?
			File file=new File("webapps"+request.getRequestURI());
			
			
			
			
			if(file.exists()) {
				//解析出后缀名   
				String fileName=file.getName();
				int index=fileName.lastIndexOf('.');
				String extension=fileName.substring(index+1);
				//通过解析出的后缀名设置介质类型
				response.setContentType(HttpContext.getMimeType(extension));
				response.setContentLength(file.length());
				
				
				System.out.println("请求的文件存在");
				response.setEntity(file);
				
				response.flush();
			}else {
				System.out.println("请求的文件不存在");
			}
			
		}catch(Exception e) {
			e.printStackTrace();
		}finally {
			try {
				
				socket.close();
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
		
		
	}
	

	
	

}

5 HttpRequest类和HttpResponse类是较为底层的类,其完成的主要工作是解析浏览器的请求和发送数据给浏览器。

package com.tedu.http;

import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

//解析请求
public class HttpRequest {
	private InputStream in;
	
	private String method;
	
	//url临时保存地址
	private String url;
	private String protocol;
	
	//进一步解析     前后按?隔开
	private String requestURI;
	private String queryString;
	
	//存储所有参数 ?后面的
	private Map<String, String> parameters=new HashMap<>();
	
	
	//存储请求头
	private Map<String, String> headers=new HashMap<>();
	
	public HttpRequest(InputStream in) {
		super();
		this.in = in;
		
		parseRequestLine();
		
		//进一步解析url 分成requestURI queryRequest 
		parseUrl();
		
		parseHeaders();
	}


	private void parseHeaders() {
 
		String header;
		while(!"".equals(header=readLine())) {
			int index=header.indexOf(':');
			headers.put(header.substring(0, index).trim(),
						header.substring(index+1).trim());

		}
		
	}


	//解析请求行
	private void parseRequestLine() {
		
		//先读一行
		String requestLine=readLine();
		String [] arrays=requestLine.split("\\s");
		
		
		this.method=arrays[0];
		this.url=arrays[1];
		this.protocol=arrays[2];
		
		parseUrl();
		
	}


	private String readLine() {
		try {
			StringBuilder builder=new StringBuilder("");
			
			int d=-1;
			char c2='a';
			char c1='a';
			while(true) {
				d=in.read();
				c2=(char)d;
				if(c1==13&&c2==10) {
					break;
				}
				
				builder.append(c2);
				
				c1=c2;

			}
			return builder.toString().trim();
			
		}catch(Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	//详细解析URL部分
	private void parseUrl() {
		if(url.contains("?")) {
			String[] array=url.split("\\?");
			requestURI=array[0];
			queryString=array[1];
			
			/*
			 * 拆分所有参数
			 * 1 按照&拆分出每个参数
			 */
			String [] paraArray=queryString.split("[&]");
			for (String string : paraArray) {
				//2  按照=拆分出每个键值对
				/*
				 * 如果=右边没有值 则赋值为空字符串
				 */
				String [] arrays=string.split("=");
				if(arrays.length==2) {
					this.parameters.put(arrays[0], arrays[1]);
				}else {
					this.parameters.put(arrays[0], "");
				}
				
			}
			
			
		}else {
			requestURI=url;
		}
		
	}


	public String getRequestURI() {
		return requestURI;
	}
	
	//通过username password nickname phonenumber返回具体的值
	public String getParameter(String name) {
		return this.parameters.get(name);
	}

	
	
	


	
	

}
package com.tedu.http;
//回复响应

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import com.tedu.context.HttpContext;

public class HttpResponse {
    
    private OutputStream out;
    
    private File entity;
    

    //用于存放响应头   HttpResponse内部维护了一个headers
    private Map<String, String> headers=new HashMap<>();
    
    public HttpResponse(OutputStream out) {
        super();
        this.out = out;
    }
    

    public void flush() {
        try {
            
            //写状态行
            printLn("http/1.1 200 ok");
            
            //写响应头
            for (Entry<String, String> e : headers.entrySet()) {
                printLn(e.getKey()+":"+e.getValue());
            }
            //响应头结束
            printLn("");
            
            //写响应正文
            writeContent();
        }catch(Exception e) {
            e.printStackTrace();
        }
        
    }
    
    private void writeContent() throws Exception{

        //写响应正文
        FileInputStream fis=new FileInputStream(entity);
        BufferedInputStream br=new BufferedInputStream(fis);
        byte [] buf=new byte[1024*10];
        int len=-1;
        while((len=br.read(buf))!=-1) {
            out.write(buf, 0, len);
        }


        br.close();

        
    }


    private void printLn(String string) throws Exception{
        
        out.write(string.getBytes("iso8859-1"));
        
        out.write('\r');
        out.write('\n'); 
    }
    
    

    


    

    
    
    public void setEntity(File entity) {
        this.entity = entity;
    }
    
    public void setContentType(String contentType) {
        this.headers.put(HttpContext.HEADER_CONTENT_TYPE, contentType);
        
    }
    public void setContentLength(long length) {
        this.headers.put(HttpContext.HEADER_CONTENT_LENGTH, length+"");
        
    }

}

 

通过这个小例子,可以了解到mvc分层架构的项目搭建,在平时的项目开发中,也都是按照如下的顺序来进行开发的:

  1、搭建开发环境

    1.1 创建web项目

    1.2 导入项目所需的开发包

    1.3 创建程序的包名,在java中是以包来体现项目的分层架构的

  2、开发domain

  把一张要操作的表当成一个VO类(VO类只定义属性以及属性对应的get和set方法,没有涉及到具体业务的操作方法),VO表示的是值对象,通俗地说,就是把表中的每一条记录当成一个对象,表中的每一个字段就作为这个对象的属性。每往表中插入一条记录,就相当于是把一个VO类的实例对象插入到数据表中,对数据表进行操作时,都是直接把一个VO类的对象写入到表中,一个VO类对象就是一条记录。每一个VO对象可以表示一张表中的一行记录,VO类的名称要和表的名称一致或者对应。

  

  

  

 

 

posted @ 2017-12-30 20:55  樱吹雪  阅读(267)  评论(0编辑  收藏  举报