关闭

自己的web服务器项目-request请求和response响应处理(一)

标签: web服务器
3273人阅读 评论(0) 收藏 举报
分类:

  我们经常使用别人的服务器进行构建网站,现在我们就自己来写一个自己的服务来使用。

准备工作:下载所需的题材及文档

注:完整项目下载

一、request请求获取

 1、了解request请求

在写服务器之前,我们需要知道客户端发送给我们哪些信息?以及要求我们返回哪些信息?经过测试我们能够知道用户客户端发送的信息有以下几点:

客户端发送到服务器端的请求消息,我们称之为请求(request),其实就是一个按照http协议的规则拼接而成的字符串,Request请求消息包含三部分: 请求行 消息报头 请求正文
        第一部 请求行
            格式:
            Method Request-URI HTTP-Version CRLF
             各部分分别为:
            Method表示请求方法;一般为GET或者POST ;Request-URI是一个统一资源标识符; HTTP-Version表示请求的HTTP协议版本; CRLF表示回车和换行
            例如:
            GET /test.html HTTP/1.1 
        第二部 消息报头 http header
            例如:
            GET /test.html HTTP/1.1
            Host: 127.0.0.1:9999
            User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0
            Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
            Accept-Language: zh-CN,en;q=0.8,zh;q=0.5,en-US;q=0.3
            Accept-Encoding: gzip, deflate
            Connection: keep-alive
        第三部 请求正文 http body
            请求头和请求正文之间是一个空行,这个行非常重要,它表示请求头已经结束,接下来的是请求正文。请求正文中可以包含客户提交的字符串信息
        注意:在第二部分header和第三部分body之间有个空行,除非没有请求正文(如果你想要亲自看到效果,请参考:浏览器中GET和POST的区别),这是因为用户在浏览网页时提交给服务器的信息是不同的

 2、实现

         经过以上分析,我们就能够清楚的知道,客户端发送给服务器的请求,请求信息有使用的协议、请求的方法、请求的资源路径、请求的消息报头、判断请求的内容是否为静态资源、判断请求的内容是否为动态资源、判断是否为空请求,为了使用的方便,我们需要将其封装起来,总不能使用一次读取一次吧,这样做实在是太浪费系统资源与时间了,如下代码,就是一个接口类,用于获取客户端发送过来的属性

package com.sample.http;
import java.util.Map;
    // http协议的请求
public interface HttpRequest {
    //获得请求的协议
    public String getProtocol();
    //获得请求的方法
    public String getRequestMethod();
    //获得请求的路径
    public String getRequestPath();
    //获得请求的消息报头
    public Map<String,String> getRequestHeader();
    //根据参数的名字获得请求带过来的参数值
    public String getParameter(String parameterName);
    //判断当前请求的否是静态资源
    public boolean isStaticResource();
    //判断当前请求的否是动态资源
    public boolean isDynamicResource();
    //判断当前请求的否是为空请求(有些浏览器会自动发送空请求)
    public boolean isNullRequest();
}
有了接口类之后,我们就可以创建类进行实现,下面就是实现类,用于对各个方法进行处理:
package com.sample.http;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class HttpRequestImpl  implements HttpRequest{
	//客户端的Socket
	private Socket s;
	private InputStream is=null;//输入流
	private BufferedReader br=null;
	private HashMap<String,String> hmHeader=new HashMap<String,String>();//消息报头
	private HashMap<String,String> hmparameters=new HashMap<String, String>();//参数集合
	private boolean isNullRequest=false;//是否为空请求,默认false
	private String protocol=null;//请求协议
	private String requestMethod=null;//请求方法
	private String requestPath=null;//请求路径
	public HttpRequestImpl(Socket s) {
		this.s=s;
		getInfos();//调用方法
	 }
	private void getInfos()//定义一个方法,用于处理获取的客户端信息
	{
		try {
			is=s.getInputStream();
			br=new BufferedReader(new InputStreamReader(is));
			//解析第一行
			String str;
			str=br.readLine();//readLine使用回车换行判断一行是否结束
			if(str==null)
			{
				isNullRequest=true;
				return;
			}
			parseRequestMethodPathProtocol(str);//调用自己创建在本类里边的方法处理第一行信息,方法在后面
		//解析第二行---第八行
			String header=null;                      
                       //判断是否结束,如果结束就退出,这里的判断按较为饶人
                     //首先应该明确br.readLine()的内容,当为true是对应的情况
                     //也就是说当“”(中间没有空格)与br.readLine()相等时,就进入到while中
                        while(!"".equals((header=br.readLine()))){
			parseRequestHeader(header);
			}
		//post和get
		if(br.ready())//post//POST提交方式判断,如果还有下一行就继续读取信息
		{
			char[] buf=new char[1024];
			int len=br.read(buf);//使用字节进行读取,因为这一行没有回车换行,readLine无法判断是否结束
			String parameter=new String(buf,0,len);
			parseRequestParamByPost(parameter);//调用自己创建在本类里边的方法处理POST方式提交的正文信息	
		}
		else
		{//get,get参数的位置在第一行连接处
			parseRequestParamByGet(requestPath);//调用自己创建在本类里边的方法处理GET方式提交的正文信息	
		}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
         //GET方法处理
        private void parseRequestParamByGet(String requestPath2) {
		String []str1=requestPath2.split("[?]");//使用“?”分割字符串
		if(str1.length==2)
		{
				parseRequestParamByPost(str1[1]);
		}
		this.requestPath=str1[0];
	}
        //POST方法处理
        private void parseRequestParamByPost(String parameter)  {
		 String[] strs=parameter.split("&");
		 if(strs.length>=1)
		 {
			 for(String str:strs)
			 {
				 String [] sp=str.split("=");
                 hmparameters.put(sp[0],sp[1]); 
			 }
		 }
	}
	//解析第二行到第八行
	private void parseRequestHeader(String header)  throws Exception{
		String[]  headHost=header.split(": ");
		 if(headHost.length!=2)
	         {
			 throw new Exception("消息报头异常,请重新提交");		 
		 }
		 hmHeader.put(headHost[0],headHost[1]);
	}
	//解析第一行
	private void parseRequestMethodPathProtocol(String str) throws Exception {
		 String[] protocolMethodPath=new String[3];//由于第一行含有三个内容,分割后需要三个String存储
		 protocolMethodPath=str.split(" ");//使用空格分割
		 if(protocolMethodPath.length==3)
		 {
		 requestMethod=protocolMethodPath[0];
		 requestPath=protocolMethodPath[1];
		 protocol=protocolMethodPath[2];
		 }
		 else
		 {
			 throw new Exception("首行参数不合适,请重新提交");
		 }
	}
	//获得请求的协议
	public String getProtocol()
	{
		return protocol;
	}
	//获得请求的方法
	public String getRequestMethod(){
		return requestMethod;
	}
	//获得请求的路径
	public String getRequestPath(){
		return requestPath;
	}
	//获得请求的消息报头
	public Map<String,String> getRequestHeader(){
		return this.hmHeader;
	}
	//根据参数的名字获得请求带过来的参数值
	public String getParameter(String parameterName){
		return hmparameters.get(parameterName);
	}
	//判断当前请求的否是静态资源
	public boolean isStaticResource(){
		return true;
	}
	//判断当前请求的否是动态资源
	public boolean isDynamicResource(){
		return true;
	}
	//判断当前请求的否是为空请求(有些浏览器会自动发送空请求)
	public boolean isNullRequest(){
  return isNullRequest;
	}
}

以上内容是对客户端(浏览器)请求内容的处理,即如何进行包装客户端请求的信息,并且将其包装成一个统一的整体,既然能够获取浏览器的内容,那么,我们就必须采取一定的措施告诉浏览器我们找到了你想要的文件,并且将返回给客户端,下面我们就来实现如何返回给客户端想要的信息

二、response响应处理

 1、了解response响应
    服务器在接收和解释客户端的请求消息后,服务器会返回给客户端一个HTTP响应消息,我们称之为响应(response)。其实也是一个按照http协议的规则拼接而成的一个字符串
    HTTP响应也是由三个部分组成,分别是: 响应状态行、消息报头、响应正文
    第一部分 响应状态行
        格式如下:

        HTTP-Version Status-Code Reason-Phrase CRLF

例如:
        HTTP/1.1 200 OK

        各部分分别为:
        HTTP-Version表示服务器HTTP协议的版本;
        Status-Code表示服务器发回的响应状态代码; 
        Reason-Phrase表示状态代码的文本描述。

        CRLF表示回车和换行

    第二部分 消息报头

        HTTP消息报头包括普通报头、请求报头、响应报头、实体报头这四大类。
  每一个 报头域 都是由 名字+冒号+空格+值 组成,消息报头域的名字不区分大小写。它们的作用是描述 客户端或者服务器 的属性
                 1.普通报头:即可用于请求,也可用于响应,是作为一个整体而不是特定资源与事务相关联。
                 2.请求报头:允许客户端传递关于自身信息和希望的响应形式。
                 3.响应报头:允许服务器传递关于自身信息的响应。
                 4.实体报头:定义被传送资源的信息。即可用于请求,也可用于响应。
        什么是 MIME Type?

        首先,我们要了解浏览器是如何处理内容的。在浏览器中显示的内容有 HTML、有 XML、有 GIF、还有 Flash ……那么,浏览器是如何区分它们,决定什么内容用什么形式来显示呢?答案是 MIME Type,也就是该资源的媒体类型。媒体类型通常是通过 HTTP 协议,由 Web 服务器告知浏览器的,更准确地说,是通过响应的消息报头里面的属性 Content-Type 来表示的,例如:Content-Type: text/HTML表示内容是 text/HTML 类型,也就是超文本文件。为什么是“text/HTML”而不是“HTML/text”或者别的什么?MIME Type 不是个人指定的,是经过 ietf 组织协商,以 RFC 的形式作为建议的标准发布在网上的,大多数的 Web 服务器和用户代理都会支持这个规范 (顺便说一句,Email 附件的类型也是通过 MIME Type 指定的)。

      通常只有一些在互联网上获得广泛应用的格式才会获得一个 MIME Type,如果是某个客户端自己定义的格式,一般只能以 application/x- 开头。XHTML 正是一个获得广泛应用的格式,因此,在 RFC 3236 中,说明了 XHTML 格式文件的 MIME Type 应该是 application/xHTML+XML。当然,处理本地的文件,在没有人告诉浏览器某个文件的 MIME Type 的情况下,浏览器也会做一些默认的处理,这可能和你在操作系统中给文件配置的 MIME Type 有关。比如在 Windows 下,打开注册表的“HKEY_LOCAL_MACHINESOFTWAREClassesMIMEDatabaseContent Type”主键,你可以看到所有 MIME Type 的配置信息

每个MIME类型由两部分组成,前面是数据的大类别,例如声音audio、图象image等,后面定义具体的种类。

常见的MIME类型

超文本标记语言文本 .html,.html text/html
普通文本 .txt text/plain
RTF文本 .rtf application/rtf
GIF图形 .gif image/gif
JPEG图形 .ipeg,.jpg image/jpeg
au声音文件 .au audio/basic
MIDI音乐文件 mid,.midi audio/midi,audio/x-midi
RealAudio音乐文件 .ra, .ram audio/x-pn-realaudio
MPEG文件 .mpg,.mpeg video/mpeg
AVI文件 .avi video/x-msvideo
GZIP文件 .gz application/x-gzip
TAR文件 .tar application/x-tar

    第三部分 响应正文

        响应正文就是服务器返回的资源的内容 

 2、实现

      首先,我们需要进行抽象,即将浏览器想要的信息,即如下内容包装起来,如下所示,我们将其包装成一个接口,在抽象时我们必须认识到用户可能会犯的错误,所以尽量使用重载的方法进行避免,在下面的接口中,使用了重载进行处理部分方法:

package com.sample.http;
import java.io.OutputStream;
import java.io.PrintWriter;
//http协议的响应
public interface HttpResponse {
	//获得一个指向客户端的字节流
	public OutputStream getOutputStream()throws Exception;
	//获得一个指向客户端的字符流
	public PrintWriter getPrintWriter()throws Exception;
	//设置响应的状态行 参数为String类型
	public void setStatusLine(String statusCode);
	//设置响应的状态行 参数为int类型
	public void setStatusLine(int statusCode);
	//设置响应消息报头
	public void setResponseHeader(String hName,String hValue);
	//设置响应消息报头中Content-Type属性
	public void setContentType(String contentType);
	//设置响应消息报头中Content-Type属性 并且同时设置编码
	public void setContentType(String contentType,String charsetName);
	//设置CRLF 回车换行  \r\n
	public void setCRLF();
	//把设置好的响应状态行、响应消息报头、固定空行这三部分写给浏览器
	public void printResponseHeader();
	//把响应正文写给浏览器
	public void printResponseContent(String requestPath);		
}
在接口中,我们能够看到详细的解释,下面我们就来实现接口中的方法:
package com.sample.http;
<pre name="code" class="java">import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;

import com.sample.utils.ConfigUtils;
import com.sample.utils.StatusCodeUtils;
//http协议的响应
public class HttpResponseImpl  implements  HttpResponse {
	 //声明初始变量
	Socket s;//客户端socket
	OutputStream os;//输出端字节流
	BufferedWriter bw;//输出端字符流
	PrintWriter pw;
	StringBuffer sbuffer;//放状态行,\r\n ,
	FileInputStream fis;
	File file;
	//构造器
	public HttpResponseImpl(Socket s) {
                    this.s=s;
                    System.out.println("HttpRequestImpl(Socket s)");
                    os=null;
                    bw=null;
                    pw=null;
                    sbuffer=new StringBuffer();//初始化,记得,否则以下的操作会遇见空指针异常
                    fis=null;
                    file=null;   			
                    getInfos();
	}
	  
	private void getInfos() {
			try {
				os=s.getOutputStream();
				bw=new BufferedWriter(new OutputStreamWriter(os,"GBK"));
				pw=new PrintWriter(bw);
			} catch (Exception e) {
				e.printStackTrace();
			}
	}
	//获得一个指向客户端的字节流
	public OutputStream getOutputStream()throws Exception
	{
		return os;
	}
	//获得一个指向客户端的字符流
	public PrintWriter getPrintWriter()throws Exception
	{
		return pw;
	}
	//设置响应的状态行 参数为String类型
	public void setStatusLine(String statusCode)
	{
		String str=StatusCodeUtils.getStatusCodeValue(statusCode);
		//System.out.println(str+"------"+str.length());
		sbuffer.append("HTTP/1.1 "+statusCode+" "+str);
		setCRLF();
	}
	//设置响应的状态行 参数为int类型
	public void setStatusLine(int statusCode)
	{
		setStatusLine(statusCode+"");//将int类型转化为String类型
	}
	//设置响应消息报头
	public void setResponseHeader(String hName,String hValue)
	{
		sbuffer.append(hName+": "+hValue);
		setCRLF();
	}
	//设置响应消息报头中Content-Type属性
	public void setContentType(String contentType)
	{
		setResponseHeader("Content-Type",contentType);
	}
	//设置响应消息报头中Content-Type属性 并且同时设置编码
	public void setContentType(String contentType,String charsetName)
	{//text/html;charset=utf-8
		setContentType(";charset= "+charsetName);
	}
	//设置CRLF 回车换行  \r\n
	public void setCRLF()
	{
		sbuffer.append("\r\n");
	}
	//把设置好的响应状态行、响应消息报头、固定空行这三部分写给浏览器
	public void printResponseHeader()
	{
		//设置setResponseLine,setResponseHeader,setResponseType
		String res=sbuffer.toString();
		pw.print(res);
		pw.flush();
	}
	//把响应正文写给浏览器
	public void printResponseContent(String requestPath)
	{
		//响应正文
		String getPath= requestPath;//客户请求的地址
		String webHome=(new ConfigUtils()).getConfigValue("rootPath");	
		System.out.println("配置文件中目录:"+webHome);//输出从配置文件中获取的地址
		file=new File(webHome+getPath);
		if(file.exists())//如果文件存在就执行
		{
		try {
			fis=new FileInputStream(file);
			byte[] buf=new byte[1024];
			int len=-1;
			while((len=fis.read(buf))!=-1)
			{
				//String str=buf.toString();
				//bw.write(str);//字符流写过去是一个地址,因为写过去之后需要浏览器解析,如果是图片或者其他(图片或视频是字节流)的该怎么解析呢?
				//System.out.println(str);
				os.write(buf, 0, len);
			}
			bw.flush();
			os.flush();//os要不要关???
		} catch (IOException e) {
			e.printStackTrace();
		}finally
		{
			try {
				if(bw!=null)
				bw.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		}
	}
}

在Eclipse中写完以上代码我们会发现,其中有多处错误信息,其原因是我们没有进行创建以上代码所要求的类,现在我们进行创建,其使用方法请参见:java.util 类 Properties    ,使用参考中的方法,我们能够进行对所需要的信息进行配置,在以上代码中使用的地方有两处,分别是【注意:这样做的好处是增减了项目的灵活性,用户能够在不查看代码的情况下随时更改配置文件等一些文件的信息,】:

(1)设置状态行处

//设置响应的状态行 参数为String类型
	public void setStatusLine(String statusCode)
	{
		String str=StatusCodeUtils.getStatusCodeValue(statusCode);
		//System.out.println(str+"------"+str.length());
		sbuffer.append("HTTP/1.1 "+statusCode+" "+str);
		setCRLF();
	}

其中StatusCodeUtils类创建如下所示,而对于status_code.properties文件存放在下载的准备文件中的/webservlet/project/中,直接复制到项目中即可:

package com.sample.utils;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class StatusCodeUtils {
	private static Properties p;
	static 
	{
		InputStream in=null;
		p=new Properties();
		try {
			//读了xx.properties文件
			in=StatusCodeUtils.class.getResourceAsStream("status_code.properties");
			//放置到p中,即放properties文件中的key,value
			p.load(in);
		} catch (IOException e) {
			e.printStackTrace();
		}
		finally
		{
			if(in!=null)
				try {
					in.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
		}
	}
	public static String getStatusCodeValue(String status)
	{ 
		return p.getProperty(status);
	}
	public static String getStatusCodeValue(int status)
	{ 
		return getStatusCodeValue(status+"");//没有空格
	}
	/*public static void main(String[] args) {//输出测试
	//Properties p=new Properties();
	// p.setProperty("rootPath","ddd");
	//System.out.println(p.get("rootPath"));
		System.out.println(getStatusCodeValue("304"));
		System.out.println(getStatusCodeValue("200"));
	}*/
}
(2)响应正文处:

//响应正文
		String getPath= requestPath;//客户请求的地址
		String webHome=(new ConfigUtils()).getConfigValue("rootPath");	
		System.out.println("配置文件中目录:"+webHome);//输出从配置文件中获取的地址
		file=new File(webHome+getPath);

在响应正文中使用了ConfigUtils类进行了项目路径的获取,代码如下所示,对于config.properties(注意:此文件中文件路径应该注意,我使用的是Linux系统,文件结构是/home/***,而对于windows系统,目录结构为:"C://webapps/*****,最好在地址栏复制地址,写到配置中")文件也在准备文件中,请自行下载,然后复制到项目中:就是下面这个东西,路径配置合适,然后你就可以将自己的项目放在webapps目录下,让自己的电脑作为服务器供其他人访问自己的网站了

ConfigUtils路径配置类,用于获取项目文件目录位置

package com.sample.utils;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Properties;
public class ConfigUtils {
	private static Properties p;
	static 
	{
		InputStream in=null;
		OutputStream on=null;
		p=new Properties();
		try {
			//读了xx.properties文件
			in=ConfigUtils.class.getResourceAsStream("config.properties");
			//放置到p中,即放properties文件中的key,value
			p.load(in);
		} catch (IOException e) {
			e.printStackTrace();
		}
		finally
		{
			if(in!=null)
				try {
					in.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
		}
	}
	public static String getConfigValue(String config)
	{ 
		return p.getProperty(config);
	}
	public static void main(String[] args) {//输出测试
	//	Properties p=new Properties();
	//	p.setProperty("rootPath","ddd");
	//	System.out.println(p.get("rootPath"));
		System.out.println(getConfigValue("rootPath"));
	}
}

到此为止,我们已经实现了服务器的主要任务,接受请求和处理请求,下面我们进行测试:

写一个测试类如下:

package com.sample.http;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class ServerTest {
	public static void main(String[] args) {
		//声明变量
                ServerSocket ss=null;
                Socket s=null;
                boolean flag=true;
                try {
                	int port=10002;
                	System.out.println("Server Port:"+port);
					ss=new ServerSocket(port);
					//while(flag)
					{
						//接受客户端发送过来的Socket
						s=ss.accept();
						HttpRequestImpl request=new HttpRequestImpl(s);							
						//	用于测试收到的信息
						System.out.println("获取的路径:"+request.getRequestPath());
						System.out.println("获取的:"+request.getProtocol());
						System.out.println("获取的:"+request.getRequestMethod());
						System.out.println(request.getParameter("name"));
						System.out.println(request.getParameter("id"));					
						Map<String,String>  m=request.getRequestHeader();
						Set<Entry<String,String>> set=m.entrySet();
						Iterator it=set.iterator();
						while(it.hasNext())
						{
							Entry entry=(Entry<String, String> )it.next();
							System.out.println(entry.getKey()+"----"+entry.getValue());
						}
					        //写响应给浏览器
						/*
						 * 封装:
						 * */
						//输出流
						HttpResponseImpl response=new HttpResponseImpl(s);												
						response.setStatusLine(200);
						response.setContentType("text/html");
						response.printResponseHeader();
						response.setCRLF();
						response.printResponseHeader();
						response.printResponseContent(request.getRequestPath());
						//用于输出信息
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
	}
}
在浏览器中输入地址回车:http://127.0.0.1:10002/test.html?id=1212&name=suguniang ,能够看到浏览器解析后的界面,当其他电脑访问时(其他电脑指的是同一个域内的),只要将127.0.0.1修改为本地的ip地址即可

此时控制台上也输出相应的信息:


自己的web服务器项目-静态请求和动态请求处理(二)




2
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:356668次
    • 积分:5771
    • 等级:
    • 排名:第4943名
    • 原创:221篇
    • 转载:106篇
    • 译文:0篇
    • 评论:53条
    博客专栏
    最新评论