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¶m2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求URL也可以通过类似于“/chapter15/user.html? param1=value1¶m2=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设置状态码:
- //设置状态码,状态码在HttpServletResponse中通过一系列的常量预定义了,如SC_ACCEPTED,SC_OK
- void setStatus(int sc)
常见的HTTP响应报文头属性
Cache-Control
响应输出到客户端后,服务端通过该报文头属告诉客户端如何控制响应内容的缓存。
下面的设置让客户端对响应内容缓存3600秒,也即在3600秒内,如果客户再次访问该资源,直接从客户端的缓存中返回内容给客户,不要再从服务端获取(当然,这个功能是靠客户端实现的,服务端只是通过这个属性提示客户端“应该这么做”,做不做,还是决定于客户端,如果是自己宣称支持HTTP的客户端,则就应该这样实现)。
- 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:
- ETag: "737060cd8c284d8af7ad3082f209582d"
Location
我们在JSP中让页面Redirect到一个某个A页面中,其实是让客户端再发一个请求到A页面,这个需要Redirect到的A页面的URL,其实就是通过响应报文头的Location属性告知客户端的,如下的报文头属性,将使客户端redirect到iteye的首页中:
- Location: http://www.iteye.com
Set-Cookie
服务端可以设置客户端的Cookie,其原理就是通过这个响应报文头属性实现的:
- Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1
其它HTTP响应报文头属性
更多其它的HTTP响应头报文,参见:http://en.wikipedia.org/wiki/List_of_HTTP_header_fields
如何写HTTP请求报文头
在服务端可以通过HttpServletResponse的API写响应报文头的属性:
- //添加一个响应报文头属性
- void setHeader(String name, String value)
象Cookie,Location这些响应都是有福之人,HttpServletResponse为它们都提供了VIP版的API:
- //添加Cookie报文头属性
- void addCookie(Cookie cookie)
- //不但会设置Location的响应报文头,还会生成303的状态码呢,两者天仙配呢
- 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类的名称要和表的名称一致或者对应。