WebServer代码实现第二版

前言

1.通过注册登录功能完善WebServer

编写注册页面:reg.html

  • 那我们就尝试做一个注册页面,我们真的把这个数据,提交到后台来,我们服务端拿到以后,把这个数据,就是用户的信息,都给你保存起来,是吧,存哪啊,咱们是不是可以存一个文件里,你将来登录时,你输入的用户名,密码,如果我的文件里有这个人,我是不是就可以给你登录成功;没有就等入失败。那关键我怎么能够提交一个信息上来,咱们做一个简单的页面,不讲那么复杂,咱们在webapp下,再新建一个页面,起个名reg.html,然后我们写几个输入框,input标签,这时页面没换行,我们加结尾<br/>换行,还是很不好看,那咱可以做一个表格,html标签table就是表格,tr标签就是表格里的行,然后再在tr里面,td标签就是列,就是一行有几列,写几个td标签,就是有几个列。
  • 另外,为了看出来表格的效果,还要在table标签内加上border属性边框,然后将每个td标签里面,都加上内容和大小,这样就撑起来了,然后input标签内的属性type可以设置为不同的类型,比如秘密password,最后面,我们再加一个提交按钮,提交按钮,它还是input,只不过它的类型,type="submit" ,value是按钮的名称,另外这个提交按钮,我们可以在td标签里用colspan属性,将它所在那一行tr内的两列td合并成一列,在td里,我们还可以加一个属性 align="center",居中,在td这一列上居中显示。打开浏览器看一下,当然我们现在点完注册按钮,还没什么用,因为现在我们还没有表单提交,如果我们想把这些内容提交到服务端,我们还需要一个非常重要的东西,叫做form表单,form也是一个标签,那我把你所有的东西括在form里面就可以了,哪怕你把整个table括进去也行,form本身不体现出来,它只是告诉你,这是我某一个域啊,在这个域里面的所有的输入框都是我表单要提交的内容,所以说它会把你这所有输入框里的东西给它拿过去。
  • 比如我们这将table及其所有的内容括在form标签里,然后form后面有一个属性叫action,action的值就是你要把这东西提交到哪去,后面还有一个常见的叫做method,method就是你表单提交的方式,使用get,还是将来我们用post方式,这地方我们不讲post了,统一都用get,那action,比如咱们写一个叫reg,即action="reg",一会我告诉你这reg拼哪去了啊,咱先不着急,先写个reg,就是注册,好了么,没玩呢,如果你是正常的一个form表单,那大家注意,这些input输入框,都要给名字,因为它都得知道,哪个输入框是什么,现在咱们只是写了4个输入框,但是它们没有名字,那注意,在form表单提交的时候,哪个输入框是谁,谁是谁,就分不清楚了,所以咱们这块要给每个输入框起个名字,那我们在再为每个输入框加一个属性name,见代码reg.html
  • 访问http://localhost:8088/reg.html,这个时候就可以提交了,输入内容后,点击注册,发现跳到没有资源的页面,没有资源正常,这个时候过来的时候,你发现它网址栏里的内容就变了吧,http://localhost:8088/reg?username=zhangsan&password=123&email=123%40sina.cn&nickname=zs,reg问号,然后一大堆,然后里面有一个%40,这是要处理那个@,这个咱们就简单点,不去管它,我们不要邮箱那行了(注释掉reg.html中邮箱那一个tr),再来一遍,发现其中reg其实就是那个action定义的,action就是告诉要提交到哪去,但这个实际上,咱们不应该是,提交到某一个页面去了,因为我不是想返回页面,我是要把这个消息交给你服务端,你去把这个能够给我注册上,那咱们应该是交给java的一个程序去处理了吧,就不是说我只想要你的某一个页面,那咱们是不是原来就一个页面,我拿到那个资源以后,咱就直接载入你这个文件,发给你就完了,但这不是,我不是管你要个页面了。我只是说把我的信息提给你,你去处理就好了。
  • 所以这我们看到一点,提交叫做reg,这是不就不是一个页面了,为什么看到苍老师,没有资源,是因为不认识reg这个东西,所以就没有这个东西,我们就404了,因为咱们没有响应过这个地址,那大家注意,form表单提交以后呢,action指定以后,咱们是不是都在地址栏传的提交的那些内容,那这个是由你method="get"决定的,如果是post,地址栏后面就看不见那一大堆提交的内容,只能看到那reg了,它是以表单提交过去的,那个时候,我们发过去消息的时候,消息行,消息头,消息正文就都有了啊,那是post请求,那个咱们就先不说了,我们就以get为例。
  • 那么在这个地方,http://localhost:8088/reg?username=zhangsan&password=123&email=123%40sina.cn&nickname=zs,问号,大家注意啊,这个问号前面,就相当于你前面的那个部分,请求你什么东西,问号后面,就是表单里的输入框的东西。中间用&隔开了,那就发给我服务端了,那么注意,其实服务端这面,是有没有响应呢,是有的,只不过这回再解析的时候,我们拿到的uri就长了,将来我想把uri这些信息写到文件里面去,那咱们还得把这个uri进行拆分,哪个名字等于什么值,然后再想办法把它保存到文件里保存就好了,就相当于我的服务器接收到你这信息,注册好了,将来你要想登录,你就按照你曾经输入的用户名、密码登录就完了,那是后话。那接下来,咱们来完成一个,注册的功能,把这个注册功能写好,就是我们真的能得到这些信息,我们一点击,能注册,注册完还能给用户显示,注册成功,争取能做到这一点。

在HttpRequest中处理get请求中的参数

  • 那么当get请求的请求行带有表单提交的内容时,就也需要解析这些提交的参数内容。而不是简单的GET /index.html HTTP/1.1,这种形式,是类似于,GET /reg?username=fancq&password=123456&nickname=fanfan HTTP/1.1,这种形式,所以我们在解析请求行时要兼顾这两种形式。所以我们在HttpRequest类中,解析请求行parseRequestLine(InputStream in),这个方法下面再写一个处理uri的方法parseUri(String uri),这个(方法声明)写好以后,先不着急写这个parseUri(String uri)方法,在parseRequestLine方法中,之前处理uri,只是写了this.uri = data[1];,这一句,现在把它注(释)了,我们换成这样parseUri(data[1]);,让parseUri去处理,因为属性分两种情况,是否带有表单提交的参数,如果这么赋值的话不太对。
  • 对于有表单传参的情况,我们还需要把问号后面的表单参数解析出来,问号前面才是我们要的真正的uri。而这些表单参数,我们把啊,它存到一个Map集合里。所以我们在消息报头信息的属性,private Map<String,String> header;,下面单独再定义一个属性,存储客户端传递过来的参数,这里给大家强调一个事情,存储客户端传递过来的参数,这个不是应该在请求行相关的注册上写,这个不是属于请求行里面的信息么。为什么没有写在请求行,也没有写在消息报头,是因为地址栏的这种传参,是一种,如果你们要是用post的方式,那它传参不是用像get方法在问号后面,拼一大堆了,它就不在那个uri里拼了,它就在消息正文里面把那些参数传过来的,post请求和get请求,它发送过来的参数是不在一个地方的,不是都是通过这种地址栏的形式出来。要是post请求的话,你看不见提交过来的参数,它是在那个消息正文,就是那个请求的正文里面。
  • 所以说我们不能把这些参数全都得归纳到请求行里面,所以这就是,为什么单独把private Map<String,String> parameters;(存储客户端传递过来的参数),先写在这个地方。这个做好了以后,咱们往下,到刚才写的那个parseUri(String uri)这个方法里,如果是/index.html,这种直接把它赋给uri这个属性就完了。原来没有考虑post请求时,直接this.uri = data[1];,把解析到的,即/index.html赋给uri属性了,现在考虑到post请求了,所以,要判断是否为post请求,即解析到的data[1]是否含有表单提交的参数,然后我们交给parseUri(String uri)方法处理,如果是post请求,即/reg?username=fancq&password=123456&nickname=fanfan这种,我们还要把问号(?)后面那一堆,放到Map里面去,而放到Map里面的原则就是,每一项都是由key-value组成,key就是参数的名字(如username),value就是值(如fancq)。代码如下:

//存储客户端传递过来的参数
private Map<String,String> parameters;

/**
 * 处理URI
 * @param uri
 */
private void parseUri(String uri) {
	/*
	 * /index.html
	 * /reg?username=fancq&password=123456&nickname=fanfan
	 * 在GET请求中,URI可能会有上面两种情况。
	 * HTTP协议中规定,GET请求中的URI可以传递
	 * 参数,而规则是请求的资源后面以"?"分隔,
	 * 之后则为所有要传递的参数,每个参数由:
	 * name=value的格式保存,每个参数之间使用 "&"分割。
	 * 这里的处理要求:
	 * 将"?"之前的内容保存到属性uri上。
	 * 之后的每个参数都存入属性parameters中
	 * 其中key为参数名,value为参数的值。
	 * 
	 * 1:实例化HashMap用于初始化属性parameters
	 * 
	 */
	//1.先把HashMap初始化出来,否则一调用put方法就会报空指针。
	//因为咱们之前上面只是定义了parameters这个HashMap类型的属性。
	this.parameters = new HashMap<String,String>();
	
	//先分析uri中是否含有"?",有问号就说明它有参。
	int index = -1;
	/*
	 * 下面等于0的情况是不会有的,因为得符合http协议要求,
	 * 但是我们就这么写了,没有关系的。
	 */		
	if((index = uri.indexOf("?"))>=0) {
		//先按照"?"拆分
		this.uri = uri.substring(0,index);
		
		/*
		 * 截取出所有参数,paras的值就应该是如下:
		 * paras:username=fancq&password=123456&nickname=fanfan
		 */			
		String paras = uri.substring(index+1);
		/*
		 * 拆分每一个参数,以&分割,拆完以后,就应该是一个如下数组了。
		 * [username=fancq,password=123456,nickname=fanfan] 
		 */
		String[] paraArray = paras.split("&");
		//遍历每一个参数:遍历这个数组,把每一个参数放入Map。
		for(String para : paraArray){
			//按照"="拆分
			String[] paraData = para.split("=");
			this.parameters.put(paraData[0], paraData[1]);
		}
		//如果uri里面不含有问号,就是直接把uri这个参数,赋给当前对象的uri属性。
	}else {
		this.uri=uri;
	}
}
/*
 * 当测试通过后,这个段测试代码(打桩)就可以删掉或注掉了。
 * 这个构造方法本应该传参的,但是为了测试,
 * 定义一个无参的构造方法,这样能快速创建出来,
 * 调用HttpRequest这个类中所要测试的方法parseUri(String uri)
 */ 	 
public HttpRequest() {
}
//测试代码
public static void main(String[] args) {
	HttpRequest r = new HttpRequest();
	
	 // 为了测试方便,我们在parseUri(String uri)方法的参数里,
	 // 给一个死值,那这个方法就可以测试成功,因为这个方法里面,
	 // 会把这uri和保存表单参数的Map都初始化完毕了么,然后,
	 // 调完这个方法以后,我们下面输出一下初始化的uri和Map对不对,
	 // 如果对,就说明我们这个方法写的没错。
	 
	r.parseUri("/reg?username=fancq&password=123456&nickname=fanfan");
	System.out.println("uri:"+r.uri);
	System.out.println("param:"+r.parameters);
}

  • 那么放好了以后,咱做一个测试,怎么做测试呢,我们在最下面写个main方法,具体内容见HttpRequest类中的源代码。这里有一个问题,Map里面的东西我输出了,System.out.println("param:"+r.parameters);,在HttpRequest的parseUri(String uri)方法中,parameters指代的就是在HttpRequest中定义的成员属性,private Map<String,String> parameters;,在parseUri(String uri)中的parameters,是没有歧义的,加不加不加this都无所谓(这里加上了this),this.parameters = new HashMap<String,String>();,但是uri是哪,那看你怎么写的,你是直接写的uri等于你那项么,你的uri那,是你这个private void parseUri(String uri)的参数,你得赋到那,该uri这个属性上,就是你这个parseUri方法的参数uri,与该HttpRequest对象中成员属性,private String uri;,这个uri同名了,这时候在这个parseUri方法中,如果请求行没有参数时,就需要直接将parseUri这个方法的参数uri赋值给HttpRequest对象的成员变量uri,这个this.uri = uri;,你得加个this.,才是赋到那个对象的属性上去。

  • 写完了以后,你可以试试,咱们之前在这个ClientHandler类的run方法当中,原来拿到那个uri以后,String uri = request.getUri();,就直接new成了一个File,File file = new File("webapp"+uri);,是不是看这里文件存不存在,if(file.exists()) {},那显然,你要是在HttpRequest类的parseUri(String uri)方法里解析出来的uri就变成/reg了,那么这个文件肯定是不存在的,对吧,如果它不是个文件,那你是不是可以在if(file.exists()) {...}后面加一个else if(){},你判断下如果uri是/reg,那你就可以想办法从request当中,把那用户名啊,密码啊,映衬的写出来,写文件里,写一行就行,然后用逗号隔开,就相当于,这写的就类似于,那个文件是一个.txt文本文件就行,比如说fanchuanqi,123456,fanfan,类似这样一行。

  • 那我们来到HttpRequest类中,写好parseUri(String uri)方法,具体内容见代码,写完之后,运行,看看效果。就发现出来了,数据也都对,如下:

    uri:/reg
    param:{password=123456, nickname=fanfan, username=fancq}

  • 那这样的话,这个方法就写好了,写好了以后,我们这个main方法和那个没用的构造方法就可以注掉了,或删掉了,不过留着也可以,将来,再测试其他代码时会用到,不过现在就不会用了。那咱们现在再回到ClientHandler类的run方法里面来,我们在第一个if里判断uri是不是一个直接的资源,如果是,那么,是不是用file.exist()能找到它,如果不是的话,那咱们就在第2个if里,可以看看它是不是想请求一个功能了,如果再不是的话,那就else,说明我们确实没有,再给它返回404。也就是说我们在它这run方法当中加了一个,判断它是不是请求我们一个功能的判断。先看它请求是不是一个注册,else if("/reg".equals(uri)),如果要是一个注册的话,我们得想办法把这用户输入的这3项,给它写文件里,那这要涉及哪些操作啊,打开个流,往文件里写,写的就是你那个request里面那个,得到的那3项,用户名,密码,昵称。

  • 那想想,在这个if里写,代码堆在这,那也不好,但是我还可以提成一个方法,比如说我下面专门写一个方法,就是做注册的。然后在这个if里直接调那个方法就好了,对么,那将来我们要做登录呢,那我怎么知道你是登录啊,就是我怎么知道你这个请求,是一个登入还是一个注册啊,实际上,大家注意啊,之前我给大家讲的是什么呢,我们页面写的时候,我们页面上有好多的输入框,是吧,这些输入框都包含在一个form的标签里面,对吧,而form就是说明,它就是相当于,就是知道了这些输入框都是我这个表单要提交给服务器的东西,这就想当于什么啊,你有一张纸,这个输入框,想当于纸上的每一行,是吧,那你这每一行都写好了以后,你就把整个纸拿回去,提交就行了,这个表单就有点那个感觉啊。

  • 而表单里的属性,action的意思就是,我要提交到哪里去,action="reg",你这要写reg的话,你会发现我们这请求这,拼完了以后,你就会发现,http://localhost:8088/reg?username=fancq&password=123456&nickname=fanfan,这里拼了个reg,那将来我这要写的是,比如我要写的是login呢,即action="login",那大家注意,这时这个请求拼完了以后,原来reg的地方就变成了login,如下,http://localhost:8088/login?username=fancq&password=123456&nickname=fanfan,明白这意思么,那注意啊,form表单就决定这uri前面那部分,action写啥,前面就拼啥,清楚吧,咱们现在写的reg,所以就拼的是reg,那后面再写登录,那是不是还得有一个表单是专门提交登录的,那也就是说我们ClientHandler类的run方法里,将来还会有else if{},判断是不是登录的,我将来要,还要有其他功能呢,再写,再写,写一大堆是吧。

程序设计原则:高内聚,低耦合

  • 那咱们说这个是不是一个可能将来要解决的问题,可能将来要写一大堆else if{},咱先不考虑这些问题,还要考虑一个问题是什么呢,你说是不是要处理一个业务功能,业务功能将来可能会很复杂,不是说简简单单一个方法,几句话,就搞定了,那这个时候,我们要做一个拆分,为什么要拆分,大家注意啊,ClientHandler类,它只是用于什么呢?将来我们把它只是用于,作为一个分发,就是说不要把所有的活都让它一个人干,这个是我们将来设计一个程序的原则,就是什么呢,高内聚,什么叫高内聚呢,就是说,所谓高内聚就是说,最好能做到,不可再拆分,就是功能不可再拆分,就是每一个人它那干的活,就很单一就好了,不要让它一个人什么活都干。
  • 你看啊,咱们这个ClientHandler现在干的活是什么呀,拿到客户端的请求过来以后,帮我们把request对象,response对象创建好了,是吧,然后如果是页面,我就是直接给你转回去就好了,对吧,如果要是处理业务,业务应该归我管么,不管,它就相当于是一个公司的前台,明白么,咱们让他干的一个活就相当于前台,就相当于什么呢,它就相当于直接就是对客户端的,就好了啊,比如说这就是一个客户端,我现在把程序分成3层,分成3层是什么呢?客户端直接和ClientHandler交互,如果是简单的这种,直接请求资源,那它是不是就很方便的,直接给你回就好了。
  • 那如果要是,你请求的是我的某一个业务,注册业务,那怎么办呢,我把这事交给谁办呢,交给后面专门一个类干,比如说这个类是专门处理什么呢,RegUser的,那将来比如说,我要是想处理这个登录呢,我是不是可以再提供一个类,它是不是可以专门处理登录的,可以理解么,我们这么干,把这程序分成这样子做,啊,那么虽然我们说,结构好像复杂了,但是这样的话,是不是就是ClientHandler只是负责一个什么啊,请求,中间一个流转的工作,负责帮你转发的这么一个工作啊,也就是说真正干活的是我们让后面的类去干啊,我们尽可能把功能都给它拆开,不要让一个人什么都干,让他又管什么呀,又管响应给用户普通页面,又管处理用户业务,那将来我们的业务多,那你是不是正常上一个网页,业务多的呢,是这样的吧。
  • 那所以就是说,你要都让它一个人干,它的代码非常多,这个不利于你将来程序特殊的业务,这个维护工作,其实大家注意,你会发现,你管理你这个软件的结构,跟一个公司管理人是一样的,你会发现,你将来去公司以后,你发现什么呢,你干的事,说我一个人顶8个人干,是吧,公司都这么用人,但是你得知道,你的功能是单一的,只能说你的工作量繁忙,是吧,但不会说,让你一个人跨好几个事干,比如说什么呀,你说你是程序猿,你去公司说白了,你就管开发,是吧,你说你一个人当8个人使,这只能说你并发好,是吧,这个能干的事效率高,但是你会发现,在功能上来讲,你是单一的,你只负责开发,对么,不会让你去负责什么呀,这个客服,是吧,不会让你管财务,不会让你管人事,这些是不是都不会让你管,对么,你只管你那一摊活,就完了啊,你会发现,公司为什么要弄这么多人,要弄这么多部门,为什么不是说,那就一个人干,挺好,对么。
  • 公司,那大家注意,人是越少越好么?肯定不是的啊,为什么这么说,你们想,如果以我为例,假如说公司,就是达内,让我,就是我现在的工作其实只是负责教课,对吧,我不负责别的,但你想想假设啊,我又管招生,又管讲课,还管你的日常生活,还管班级维护,是吧,我还管你们将来就业,是吧,我还管其他的事,那大家注意,这个时候,我是不是管的事太多了,那这个时候我在公司就是个重量级,是吧,所谓重量级,其实并不是好事,在计算机里,我们指重量级,一般都不是一件好事,为什么呢,你太过重量级的话,就相当于你是绝对的核心,是吧,那就出现一个问题,你不干了,怎么办,是这道理吧,因为我们总是将来要考虑这种问题的,如果让你干的活特别多,你什么事你都要处理一下,有一天,你不能满足我需求的时候,我要不要把你换掉,换掉你以后,我的代价有多大。
  • 就相当于,比如刚才说了,既然我能干这么多活,我得跟韩总聊聊呗,对不对,你看既然我在公司挺卖力的,你离开我不行了,是吧,那你看看一个月给我长个百八十万的,是吧,那这时候韩总只会说一句话,滚,是不是,为什么啊,无所谓,对吧,你走了,我顶多就是你这块没人讲,我是不是可以再换一个老师来,对不对,那这样的话,那代价是不是还是比较小的,但是如果你走了,这公司就摊了,什么都运转不了了,那就很麻烦,对不对,所以你会发现公司在组织人员结构的时候,也是高内聚,低耦合的,一个人你就管你那摊活,你的工作不能再拆分了,那就好,你就管这个就行了,像我们老师就管讲课,别的都不用管,是吧,那这个时候就行了,其实我们说所有老师讲课,还能拆分,几个老师,一人一块,是吧,那就做到最小,高内聚,你就做这点,然后几个老师共同去干。
  • 而你,大家注意,团队协作之间呢,我认识王克晶,我认识李洪鹤,我认识苍老师,是吧,我只是把他们当同事看,当老师看,我只知道有第一阶段的老师,第2阶段的老师,第3阶段的老师,是吧,这样呢,之间呢,咱们配合之间呢,还得是依靠接口的,就是我只把你当成老师看,咱俩都是老师,只不过咱俩是两个老师的实现类,我调你,是调老师的方法,你调我是调老师的方法,咱俩之间本质上也没有必然耦合关系,就是我离开克晶就受不了了,那这个不行是吧,明白这意思吧,我们俩之间也是靠接口入座的,是吧,我只是把他当做老师看,她也把我当做老师看,明白么,将来比如说,第一个阶段换一个,或者说我这阶段换一个,是不是之间还依然可以配合,对吧,这就是低耦合关系,就是咱俩没有一个必然关系,就是比如说,苍老师让我们去讲课,苍老师只是让老师去讲课,我们只是,都是老师的实现类,明白吧。
  • 所以你最终将来这个东西,慢慢去体会,现在刚开始一时说,可能也没有太多概念,最终你会发现,等你将来写的项目越来越多,重构的越来越多,你自然就理解高内聚,低耦合是什么概念,它不是说光为了考虑开发问题,还有,就是你后期维护问题,明白吧,它有这些问题,所以你会发现将来程序,没有人说把所有的东西都写在一个类里,那样是看的很简单,就一个类,你不用让我记,那么多个类,是吧,但带来的问题是,有一天呢,其中有一个功能不行,我是不是这个类都要换掉,一换所有的东西都不能用,明白么,这就是为什么说公司也不是一个人干,人越多越好,当然成本,那个是现实问题了,是吧,当然我说类越多越好,也是这个意思,你这类越多越好,也不要出现什么呢,过多的冗余,是吧,那这个没有必要,你要有过多的冗余,你就抽象出来,往上提一层,明白吗,最终你会发现,都是这么干,这时候,你的程序就变的非常健壮,这个程序就非常的合理。

MVC模式简单说明

  • 所以你会发现像咱们这个ClientHandler类的run方法里,这个地方又拆出一层,也就是说,我现在就相当于,这个ClientHandler就相当于控制的,就相当于给人派活的,就相当于那客户端(客户)来一句啊,那个我想干点什么事,ClientHandler说,啊,你等着,我给你找人,哎,干注册功能,找RegUser干,你干,那想干登录的事,好,找LoginUser,好,你干,那是不是就它干,这样的话,也就是说,ClientHandler,它大部分情况下,是不是就是起到了一个中间控制扭转的工作了,指派让谁干活了,当然我们说简单的活,就比如说,我们直接就返回这一个静态的资源,是吧,页面啊,图片啊,它就直接流转了啊,其实那些事,咱们是不是也可以拆出去啊,那当然这个比较简单了,咱就不拆了,但是我们说真正处理业务,你肯定还是要拆出单独一个层,而这个东西我们拆完以后,就已经非常接近一个模式,叫做MVC,Model View Control,就是所谓的模型层,视图层和控制层。
  • 那么视图层就是,我们要给用户显示的页面,就是我们视图层的东西,控制层,就是相当于ClientHandler,什么啊,一个中间的流转工作的,那它这控制相当于什么啊,我是一个调度,我其实自己不干活啊,所有活来了以后,我统一一个规划,让谁干,让谁干,让谁干,明白吧,包括你说你把你那个结构给我以后,我让谁去回应客户端,他就起一个中间流转的作用,而M,其实就是我们所谓的业务层, Model模型层其实就包括我们的这些业务逻辑,就是真正我们干活,给用户提供这些,其实你这么看的话,那么现实生活中有没有案例,MVC模式,常有,为什么这么说啊,你看那个中国移动打电话,来了以后,不是先要找到一个专门的接话器么,然后说先生你有什么事,他要是能给你直接解决,就给你解决了,有的,它一看解决不了,他怎么办,他说你稍等,我给你转到谁谁谁那,有专门的技术人员,他给你转到后面那个人,让那个人给你解答,对吧,其实都是类似于这么个意思。
  • 所以呢,咱们现在呢,也是,当然我们还没有写,但是我们,要把它们都拿到另一个类里去干,我们不全都写在ClientHandler里头了啊,这样的话,将来不好做拆分,所以呢,先说明下,如果将来你在ClientHandler里面,查看请求一个功能里面,即else if{}里,已经写了什么一个输入流什么,一大堆,将来我会再给它拆到别的类里去,明白吧,说那我到底拆类,还是拆方法,是吧,这个慢慢的去体会,在这呢,先告诉大家,按功能的话,都应该拆到不同的类里面去,如果说有同样的一个功能,比如说在这类里,他们的功能差不多,那咱们就可以定义成一个方法,是吧,介于方法之间去调,当然按功能来讲,都应该给它拆成不同的类啊,差不多就是这样一个原则,这个东西慢慢去体会,我们后面还有好多的项目,是吧,自己慢慢去总结,去归纳。

创建RegServlet并在ClientHandler里调用

  • 那么,但是至少是知道,用户要做一个注册功能了,所以咱们定义一个类专门做它这个注册这件事啊,我们呢,在这个src/main/java里面我们再新建一个包啊,包名叫com.tedu.service,然后在里面我们定义一个类,叫做RegServlet,这是用来完成用户注册功能。Ok以后,咱们在这块呢,再定义一个方法吧,我叫做service,我们用这个方法来完成这个业务处理啊,那先写到这,先看看这个类,看这个ClientHandler类,然后看我这个地方啊,我们再ClientHandler这个地方,run方法中,查看是否请求一个功能的else if{}里面,我们就调刚才那个service方法,让它去处理,对吧,那在这个else if{}里,我们怎么做啊,首先第一件事,是不是得先实例化出来,比如说我们先创建出一个RegServlet,是吧,RegServlet servlet = new RegServlet();,然后呢,调用它的service方法就行了。
  • 那注意啊,你要想让它能注册,它得有用户提交的那些东西,对么,那当然我们这些东西都封装在哪了,是不是在request对象里头,对不对,所以这个地方,我是不是得想办法把这个request对象给它,对吧,而且你们想想啊,它处理完了这个用户的请求以后,是不是还得响应客户端,是这意思吧,也就是它是不是还得知道,将来我要给客户端访问什么页面,能不能去看到我这(响应的)东西对不对,所以说在这个地方干嘛呢,我们也把response给你,就是请求和响应都在这个service方法里面,你要用哪个,你就用哪个,你要需要获取到用户的请求信息,就从request这里面拿,你要将来直接给客户端响应,你是不是直接拿response就行了,service,它要有这两个对象,是不是它既可以接收客户端的请求,也可以去回给客户端了,能理解吧,就把这请求对象和响应对象,交给service方法里了,这是不是就完全归你管了,明白吧,就类似于这个意思啊。
  • 那既然是这样的话,那也就是说,我们service方法,应该至少也能接收两个参数,是这意思吧,就是request和response这两个参数,com.tedu.service这个包中的RegServlet这个类中,我们接收这两个参数,public void service(HttpRequest request,HttpResponse response),Ok,把这两个接收一下,然后导一下包。那写到这呢,咱们在RegServlet类里的service方法里打个桩,就是说开始处理注册,System.out.println("开始处理注册!");,如果这句话输出了,就说明到这了,然后呢,我现在就来试试,那我们先启动服务端,比如说访问注册页面,localhost:8088/reg.html,输入信息后,点击注册,这个时候网页上没有响应是对的,因为我们确实没响应呢,但是也就是说,看看控制台,已经来到了开始注册了,只不过,我们来到开始注册时候,这个时候,咱们并没有再通过response给客户端响应了,所以客户端是不是什么都没有了,对吧。
  • 那也就是说,我们应该把这块写好,就没有问题了,如果没问题的话了,现在我们在RegServlet类的service方法中,开始处理业务了,做这件事,将用户的注册信息按行写入到userinfo.txt文件中,见源代码中。也就是说应该能做到这一点啊,比如说,当我们网页上注册信息输入完了以后,一点注册,那我们这个时候,在你的项目的webapp下,就是不是应该有一个新的文件,叫userInfo.txt,明白么,那在这里的话应有一行,就好比说打开这个userInfo.txt以后,里面就有一行谁了,类似于像这样的,fanchuanqi,123456,fanfan,用户名,密码和昵称,如果一会再有一个人也注册一下呢,那就应该在下面,再来一行,比如说,再接着来一行,liucangsong,222333,cangcang,清楚吧,再来一个呢,就这么写,能理解我的意思吧,就一行一条,每条3项,清楚么,就这么干。

获取请求信息的安全性问题

  • 不过注意啊,这块做完了,是不是依然没有给客户端响应,是吧,咱们再响应一下客户端就完了,所以咱们不要求客户端这边,一点完注册就必须有反应,当然至少这应该点完了以后,你这后面控制台处理完了以后,那个userInfo.txt文件应该有了,明白吧,文件应该有了,而且里面都有数据了。然后呢,再给补充一点,我在这直接写上,这会遇到的一个问题,在这个RegServlet的service方法里想处理的话,那么首先,先通过request把用户输入的数据(密码,用户名和昵称)拿到,这3项现在在哪搁着呢,是不是在HttpRequest类中的Map parameters,这个集合里面呢,我们之前不是打过桩么,我们已经把uri处理的那些内容,放到存储客户端传递过来的参数,private Map<String,String> parameters;,这个Map里了,对吧。
  • 这个Map parameters由于是我们的一个私有属性,是不是我们并没有给它定义get方法,那等于说外界是看不见的,所以咱们也可以先把这个方法,先给它加上,而且加上的话,没有必要把这个Map全暴露出去,因为外面没有这个,对它进行put的权限,外面只是获取它的值,你不能给我随便乱改,因为请求是,包含的是人家用户信息,是吧,所以外边咱们只提供一个什么呢,这个get,不对,提供get,你不是也把parameters整个参数返回了么,你把它返回了,那就能调它的put方法,那怎么办啊,这Map里面存的是不是用户传递给你的参数,所以这块是不是不能随便乱给它改,想给它put啥,就put啥,那是不是不行,对么,那也就是说,我不允许对Map进行put操作,你只能获取里面用户给你的,因为这个表示人家用户的请求信息,是吧,你要改了的话,就不能表示人家用户的请求信息了么,虽然我们不会这么干,但是我们应该也避免这件事。
  • 那想想如果你提供一个get方法,把这个Map返回了,那外面拿到这个Map,怎么不能调用这个Map的put方法呀,还能调remove,删了你怎么弄,对不对,所以既然大家只是想获取信息,说白了,它只是想获取Map里面的信息,我们这要提供一个get,只不过你想要啥,你跟我说,我自己从Map里拿出来给你啊,所以在这呢,我们提供这么一个方法,在这个HttpRequest下面,我们再定义一个方法,这里面不是有好多方法了么,是这样的吧,咱们再加一个啊,都加在下面吧,加在下面比较清楚,下面是不是有一个我们专门做测试的东西,main方法,加在这个测试代码的上面,根据参数名获取参数值,public String getParameter(String name) {return parameters.get(name);},我来解释一下,想一想这个道理,在这里,你要想要我那个参数的值,说白了,你不就想要这个么,paras:username=fancq&password=123456&nickname=fanfan,这个地方就是想要这个。
  • 比如说我想要用户名的值,密码的值这些东西,但是想要这些信息的话,那注意,我不要把这个Map直接给你,如果我在这getParameter(String name)方法里,直接return的是类似于这个Map,那就有一个什么不好的事情出现呢,你把这个Map如果返回了,将来外面拿到这个Map以后,它是可以根据key来获取对应的value,但是它是不是还可以往里面put新东西,往你这Map里,是不是还可以remove你的元素,那你这样的话,它要给你remove的话,或者把你这东西改了,你这个还能是代表人家用户提交上来的给你的请求信息么,就不对了,对么,我为了不让你改我这个Map,怎么办呢,我就不能直接把我这个Map给你,你不是想要那个值么,那你把key给我,我自己从这个Map里面提出来,给你,这样的话,你就可以拿到值了,这样的话,你是不是直接看不到我的Map,那你是不是就能对我的Map做操作,拿你要想要信息,我是不是依然还可以给你,你把这个对应的key,比如说你把username给我,传进来了,那我是不是就从这个Map当中,用username作为一个key,我就把这个username的值返回给你就完了。
  • 这就好比说,我身上有一个钱包,你想管我要钱,我是给你钱啊,还是我把这个钱包给你,你自己拿啊,那我给你钱,不就完了,多少钱,你跟我说,我是不是拿出来给你,你不能直接对我的钱包做操作,对吧,如果你说,来,给我钱包,啊,给你,那你这样的话,你想怎么拿,怎么拿,你想怎么搞,怎么搞,那肯定是不行的,那不就乱了套了么,清楚这个意思吧,就是我在这不直接把Map返回给它啊,你要想要什么值,我从Map里拿出来给你,你不能直接对我的Map做操作,就类似于这个道理,所以在这个地方呢,我们不直接把Map返回了,那样显示方便,但是这样的话,会存在一些风险,就是外边可能把那值给你改了,给你输入一个新元素,然后给你干点啥,清楚吧,就是你想要啥,我从Map里拿出来给你就完了。

实现注册功能RegServlet

  • 总之,你也能要到你想要的一些信息,这个根据参数名获取参数名的方法,public String getParameter(String name) {return parameters.get(name);},写好了之后,那咱们在这个RegServlet类,在service方法里注册的时候,咱们怎么取用户名啊,比如说我想获取用户名,我是不是通过request.getParameter("username");,给它传什么呢,username是不是就完了,对吧,当你这传username以后,那也就是说,这username就相当于是传到HttpRequest类中的getParameter(String name)这个方法了,那这个方法里的,return parameters.get(name);中的name就相当于是传的实参,这就是相当于return parameters.get("username");,这个地方传的是username,那能不能获取用户名的值啊,能,因为我这个Map里面,存的不就是那个,key就是username那个参数名,value就是那个参数对应的值么,是不是啊,就用户传递过来的那个。
  • 那这样的话,我们就能获得那个用户名所对应的值了,就类似于这么干啊,最后响应一个页面,给用户回个页面,注册成功。那下面,在RegServlet类的service方法中,我来写这个注册,看看这个怎么做,我们最终这个目的就是,我们要把用户输入的那几项,我们以行为单位写到目录(文件)里面去,那么想写的话,首先,我们要分为几步啊。第一步,我们获取所有注册信息,包括用户输入的所有信息;然后呢,第2步,打开一个流,往文件里写是吧,我创建一个输出流;第3步呢,我将数据写进去,是吧,最后把流关了。
  • 那么,第1步获取用户名,密码和昵称,try-catch是必须的了,然后首先在try外面,创建一个PrintWriter,PrintWriter pw = null;,在try里面去使用它就好了,在catch里,如果有异常的话,把这个异常输出一下,最后finally,finally里面是什么啊,就是if,如果pw!=null,就pw.close();,即if(pw!=null) {pw.close();},然后,就是try里面了,先实例化,pw = new PrintWriter(new FileOutputStream("webapp"+File.separator+"userinfo.txt"));,然后在PrintWriter里面new FileOutputStream(),然后在这里面呢,是那个,"webapp"+File.separator+"userinfo.txt",这个文件,那写到这,发现一个事,注册一个完,没事,写进去了,再注册一个,把第一个给抹了,是吧,原因在于什么啊,咱们默认情况下创建这个文件输出流是覆盖,它就是会把原来的内容删掉,那这个不行,咱们不能每次一写,都先把原来的内容干掉,也就是说,我应该追加写的模式就行了。
  • 所以就是说,在FileOutputStream()里面,传入那个文件后面,我们再加一个参数true,pw = new PrintWriter(new FileOutputStream("webapp"+File.separator+"userinfo.txt",true));,就是说这个文件输出流,第2个参数是一个布尔值,如果是true,那也就是说,这回打开流,就可以接着之前的最后去写了,是吧,要这么去干。那然后呢,pw.println(username+","+password+","+nickname); pw.flush();,然后这就写出去了,写完了以后,我们输出一句话,注册成功,System.out.println("注册成功!");

public class RegServlet {
	public void service(HttpRequest request,HttpResponse response) {
		System.out.println("开始处理注册!");//打桩
		//获取用户名,密码,昵称
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		String nickname = request.getParameter("nickname");
		PrintWriter pw = null;			
		try {
			//实例化PrintWriter,传入文件输出流,布尔值设置为true,可以追加字符。
			pw = new PrintWriter(new FileOutputStream("webapp"+File.separator+"userinfo.txt",true));
			//将用户信息写入文件userinfo.txt
			pw.println(username+","+password+","+nickname);
			//如果在这里不写这个flush,当下面close的时候也会有flush.
			pw.flush();
			//这就写出去了,写完之后,我们输出一句话"注册成功"
			System.out.println("注册成功!");
		} catch (Exception e) {
			//如果有异常的话,将异常输出一下。
			e.printStackTrace();
		} finally {
			if(pw!=null) {
				pw.close();
			}
		}
	}
}

  • 那我们来试一试,开启服务器,打开浏览器,localhost:8088/reg.html,输入信息,点击注册后,控制台会出现,注册成功,这句话了。那么在我们webapp目录下,就会出现一个文件userInfo.txt,打开看看,是不是里面就有东西了,是吧。那怎么这么多个相同的信息,那你看,在控制台输出的,注册成功,成了好几次,删掉userInfo.txt里的信息,再重新来一遍注册试试啊,比如http://localhost:8088/reg?username=zhangsan&password=123&nickname=zs,就一条信息了,我告诉你们啊,真正浏览器在发给你请求的时候,其实还有好多好多这样的问题,你们将来后期会考虑的,比如还有缓存问题啊,这么一大堆事啊,总之现在咱们写完这个文件以后,进去了,就可以了啊!现在咱们注册的地方啊,所有输入的东西,先不要有中文,因为有中文的话,你在地址栏传中文的话,会有问题啊。那注意,这个逻辑其实不完美,为什么呢,正常情况下,你们上网的时候会发现一个问题,用户名一般是不让你输重复的,你要输重了,怎么提示用户,那是不是有重复的,把用户名给截出来,说挺对的,怎么做啊,怎么判断啊。
  • 首先做第一步,你先得把这个数据注册完,成功把它写进去了,就这样的吧,那就应该尝试着,这样吧,咱先把正常流程先走完,咱现在不等于说还没回客户端呢么,是这样的吧,那这个都干完了,咱是不是得回复客户端了,怎么回复客户端啊,是不是就用response,把那个页面回过去,是这道理吧,那咱先做一个注册成功的页面出来,创建个新页面,比如叫reg_ok.html,见代码。接下来我们会到RegServlet类的service方法中,刚才我们写到输出注册成功,System.out.println("注册成功!");,这句代码,在这之后,我们来写响应注册成功的页面给客户端。
  • 那么,响应注册成功的页面给客户端,这个地方,我们先创建一个File对象,将响应的页面,reg_ok.html,放到这个File对象里面,然后,第一个,咱们是不是得先设置好这个状态代码,就是响应的是成功的,OK,response.setStatus(HttpContext.STATUS_CODE_OK);,然后呢,是响应头吧,响应头里面,我设置两个,第一个是setContentType,那值呢,值应该是这个text/html,是它吧,你是不是响应一个页面回去么!咱们想要那个注册成功reg_ok.html,那个页面么,所以应该响应这种格式啊。但是关键的是我要这么去写的话,不是不可以啊,就是说等于说你这定死在这了,是吧,那当然,能不能换,根据这个文件名的后缀也可以换,就这样的吧。
  • 就是你可以用哪个,在ClientHandler当中是不是出了类似的这么个事,就在responseFile这个方法里,里面这个response.setContentType(getContentTypeByFile(file));的时候,在这我们是不是用到了一个,就是getContentTypeByFile(file),那在这个方法里,是不是也是这样获取用户名啊,然后截取后缀,然后再调用HttpContext.contentTypeMapping.get(ext);的方法,根据这个contentTypeMapping,把那个后缀拿过来,然后获取到那个格式,我们就是这么写的吧。但是大家注意,我们回到RegServlet类中,service方法中的设置响应头,response.setContentType("text/html");,这句代码这里,你在这如果响应确实是个页面,你在这写死,其实也没什么问题,因为你也不会,返回个图片啥的啊,就是返回个页面,你在这写死了也行。
  • 或者你换成之前那么写,也可以,当然那么写的话,那问题就是在于,如果你要是不同种类的,一般我们会根据后缀去区分,如果你确保你只是,肯定就是返回一个页面,那你这写死了也没问题,将来,你要实际开发,也是这样的,我也可能就是写死的,就是这么种情况啊,但前提你得确保你真的是返回页面,而不是别的种类的那些资源。 然后呢,再response.setContentLength(),值呢,是不是就是这文件大小file.length(),那这样我就设置好了,然后呢,设置响应正文,response.setEntity(file);,然后是不是把你的文件给进去就行了,最后呢,response.flush(),是不是就发回去了。

public class RegServlet {
	/*
	 * 我们用这个方法来完成业务处理
	 */
	public void service(HttpRequest request,HttpResponse response) {
		//此时重启服务器,客户端访问注册页面就可以在控制台显示“开始处理注册!”
		System.out.println("开始处理注册!");//打桩
		/*
		 * 将用户的注册信息按行写入到
		 * userinfo.txt文件中。
		 * 每行为一条用户的信息,格式:
		 * username,password,nickname
		 * 例如:
		 * fanchuanqi,123456,fanfan
		 * liucangsong,222333,cangcang
		 * 
		 * userinfo.txt放在webapp目录下
		 */
		//获取用户名,密码,昵称
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		String nickname = request.getParameter("nickname");
		//拿到用户名,密码,昵称信息之后,就应该往文件里写入这些信息
		//在try-catch之外,首先创建一个PrintWriter。
		PrintWriter pw = null;
		//try-catch是必须的了。
		try {
			//实例化PrintWriter,传入文件输出流,
			//布尔值设置为true,可以追加字符。
			pw = new PrintWriter(new FileOutputStream("webapp"+File.separator+"userinfo.txt",true));
			//将用户信息写入文件userinfo.txt
			pw.println(username+","+password+","+nickname);
			//如果在这里不写这个flush,当下面close的时候也会有flush.
			pw.flush();
			//这就写出去了,写完之后,我们输出一句话"注册成功"
			System.out.println("注册成功!");
			
			//要想访问一个页面回去,我们先创建一个File表示那个页面
			File file = new File("webapp"+File.separator+"reg_ok.html");
			//要想给用户回应,要用response回应,要干这几件事。
			//1.用response回应的第一件事,设置状态行
			response.setStatus(HttpContext.STATUS_CODE_OK);
			//2.设置响应头:
			//1) 因为要响应一个页面,reg_ok.html,这个页面是.html格式,所以设置为"text/html"
			 //下面也可以不写死,但如果的确定返回的是一个.html页面,写死了也没有关系,将来实际开发中也是这样。(详解见笔记)
			response.setContentType("text/html");
			//2) 设置响应页面的大小,其值就是这个文件的大小。
			//下面的设置的ContentLength是一个int值,而file.length()返回的是一个long值,所以必须强转。
			response.setContentLength((int)file.length());
			//3.设置响应正文
			//1) 把响应的文件给到这个响应对象里
			response.setEntity(file);
			//2) 自动行刷新
			response.flush();

		} catch (Exception e) {
			//如果有异常的话,将异常输出一下。
			e.printStackTrace();
		}finally {
			if(pw!=null) {
				pw.close();
			}
		}		
	}
}

实现登录功能Loginservlet

  • 那这样,你看在HttpResponse类的flush方法里,先发送状态行,发送响应头,响应正文就都回过去了,那这个先写好以后呢,咱们先试一把,看看成不成功。发现就可以注册成功后,浏览器就可以返回一个注册成功的页面了。那注意,现在首先第1点,你是不是可以参考我们注册页面,我解释一下注册页面啊,如果大家想做登录的话,只需要用用户名和密码,两个框了,也就是说可以少一行,改的话可以这么做,我复制一下注册页面,我在做一个登录页面,叫login.html,见代码。写完后,我们可以先访问一下登录页面,看看对不对,http://localhost:8088/login.html,页面就出来了,我们输入注册好的,用户名和密码,一点登录,又会转到 没有资源 那个页面,为什么呀,因为这块你看,http://localhost:8088/login?username=fancq&password=123456,到时候你form表单的action是login了,就可以看到是/login了,是这样的吧。
  • 但大家注意,由于我们之前写的比较通用了,就会见到一个什么好处啊,就在ClientHandler当中,你就对应了,就是之前你要是一解析,你实际上,你是长这样子,/login,它其实是那个getParameters里面也已经有东西了,因为你不是还要解析那个uri么,发现是有问号,传参了么,那等于说这个请求里面的getParameter是不是应该也是有东西的,只不过这时,在查看是否请求一个功能时,else-if条件里的路径,不是/reg,应该是/login的了,那多了一个login的话,那你就要再写一个else-if分支,在分支体里再去写一个LoginServlet的service方法,service方法里面怎么办,是不是还是通过request,先拿到用户输入的用户名和密码,然后去文件里把所有的用户读出来以后,比一比,是这样的吧,对了,跳一个登录成功的页面,不对,跳一个页面,登录失败,是不是就可以了,能理解整个流程大概怎么干么。
  • 再说一遍啊,怎么干啊,第一步,先写个登录页面login.html,然后呢,form表单,提交的比如叫login,你不叫这个,你叫一个什么都行,你自己记得住的uri的名字就行了,然后第二步干嘛啊,在ClientHandler当中,我们是不是再加一个分支,是吧,分支的if判断里写什么啊,就是你那form表单里那个,是不是多一个杠/,是吧,然后呢,你再写一个Servlet,调它的Service方法,把这个request,response,传进去就完了,在这service方法里面,获取它的用户名和密码,输入的用户名和密码,然后呢,跟我这个useInfo.txt里面的每一行的比一比,如果有哪个是与输入的一样的,用户名也对,密码也对,然后这时就登录成功,你就输出一句话,登录成功;失败的话,就输出一个登录失败。
  • 然后注册的时候,如果用户名已经有了,用户已经存在了,那就跳个页面,写上该用户已存在,首先呢,搞一个login.html登录页面,然后我们得action写一个login,见代码。那么现在我们输入完信息,点击登录,会跳出没有资源那个页面,这是因为,我是不是并没有处理我这个/login的请求,是吧,那也就是说那想处理这个/login的话,首先,这跟我之前写的那个reg一样,我们要在com.tedu.service这个包中,再新建一个类,叫LoginServlet,然后它里面也有一个service方法,也是用来处理逻辑的啊。然后呢,在这个ClientHandler当中再加一个else-if,这个else-if判断的是login的,是吧,如果这地址是/login的形式,那我们这里面走的是LoginServlet,见代码,这样的话,一调这个代码,就能到我们这个LoginServlet类里面来了。
  • 那在这个LoginServlet类里面我们怎么去处理啊,首先是不是可以先获取用户,输入的那个用户名和密码,这个还是从request里面取,只不过是登录页面发送过来的表单参数,只用用户名和密码,那注意啊,你在request.getParameter("username");里的参数,就是你html页面里面input输入框里的name属性的的值,其实因为你那form表单提交的时候,我们那地址栏里面,它也是用这个name属性的名字,等于你那个输入框里的值,所以你跟这块对应上就可以了,那么比如说,那我们拿到用户名,密码以后呢,先打个桩,看看对不对啊,打个桩,看看到没到登录来,以及是否输出用户名和密码,System.out.println("开始登录..."); System.out.println(username+","+password);,登录一下,控制台就输出了打桩的内容,也就是说到目前为止这些都没错,那就接着写,那拿到用户名,密码以后呢,我们要做什么啊,是不是得看看userInfo.txt文件里面,你输的对不对,那我就读取userinfo.txt文件中的所有用户,并且逐个比较。
  • 在LoginServlet的service方法里,先完成基本功能,然后再想别的事,先把基础的工作都写好,try-catch,finally块,关流,然后再写具体业务代码,后面如果登入成功后,我们还要写一个login_ok.html,登入成功的页面,见代码。登录失败也类似于登入成功的形式编写。另外当输入的用户名或密码不正确,我们也可以在登录失败页面中,加一对标签<a>, a是超链接,<a href="login.html">重新登录</a>,href的值是你重新要回到哪个页面去。这时在登录失败的页面就多出了一个重新登录的超链接。另外,在这也会发现,是不是重用代码特别多了,你看啊,你的跳转页面的代码,设置状态行,响应头和响应正文,是不是代码结构都是类似的。那之前最早在那个ClientHandler当中,如果响应静态页面的话,是不是都有这么一个方法,responseFile了,是吧。
  • 那这样的话,你看啊,如果我说我这在else-if里,注册功能,注册完毕以后,你得到的那个RegServlet注册里面,service方法里,注册成功,打桩的那句代码以后的响应注册成功的页面给客户端的那一大段代码。

//要想访问一个页面回去,我们先创建一个File表示那个页面
File file = new File("webapp"+File.separator+"reg_ok.html");
//要想给用户回应,要用response回应,要干这几件事。
//1.用response回应的第一件事,设置状态行
response.setStatus(HttpContext.STATUS_CODE_OK);
//2.设置响应头:
//1) 因为要响应一个页面,reg_ok.html,这个页面是.html格式,所以设置为"text/html"
 //下面也可以不写死,但如果的确定返回的是一个.html页面,写死了也没有关系,将来实际开发中也是这样。(详解见笔记)
response.setContentType("text/html");
//2) 设置响应页面的大小,其值就是这个文件的大小。
//下面的设置的ContentLength是一个int值,而file.length()返回的是一个long值,所以必须强转。
response.setContentLength((int)file.length());
//3.设置响应正文
//1) 把响应的文件给到这个响应对象里
response.setEntity(file);
//2) 自动行刷新
response.flush();

  • 我不写这一大段,我直接在ClientHandler类里的那个对应的else-if里面调用service方法之后,我直接responseFile();,即responseFile(HttpContext.STATUS_CODE_OK,file,response);,直接用ClientHandler类里的这个方法,响应这个页面,不就完了么,去那个注册成功那个页面,用这个方法实现,写在else-if里,那这样的话,一句话不就搞定了么,对么,那告诉大家啊,我们不会这么干,为什么呢,因为将来实际上,你这一个service,比如说里面处理完了,不同情况下,我会返回不同页面,说不就是一个成功,一个失败么。那我这个service方法,返回一个true和false,返回true就登录成功,返回false就失败;那我成功去哪个页面,失败去哪个页面,但将来也不一定,有可能你成功,还有很多种情况,你到底是VIP啊,还是普通用户啊,我也给你跳不同页面,那这个时候,你怎么办,是这样的吧,这都是你将来实际要考虑的问题。
  • 所以我们不会在这,这么去做,我们都是让它里面service,自己处理完了以后,你要跳哪你自己就跳哪去,就好了,明白吧。我们不会在ClientHandler类里去给它弄好,但是,你会发现一个问题,代码它有重复,对不对,怎么办,抽成一个方法,咱们是不是也可以在Servlet里面抽成一个方法啊,对不对啊,那你想一个,还有一个事,你看我这了啊,抽成一个方法没关系,那这个注册的LoginServlet,它这个Servlet处理完了注册请求以后,它会写一大段,响应注册成功的页面给客户端的业务代码,对吧,那么我刚才还让大家写什么啊,如果它的用户名存在了,是不是还要跳那个用户名存在的那个页面去,那也就是说,你这段响应页面给客户端的业务代码,这个结构的代码,就会写不同的地方,包括你看刚才写的登录是不是也有这段东西,也是根据成功与否,是不是访问不同的页面,是吧。
  • 那也就说,你们想一个问题,那这样的话,会有一个什么事啊,我们在RegServlet里面,我们把响应页面给客户端的业务代码,提出去,怎么提出去呢,这样,首先我在RegServlet下面就可以定义一个方法,比如说我起名叫forward,public void forward(String uri,HttpResponse response){},比如说我做一个跳转,然后呢,我在这里面就可以写响应页面给客户端的业务的那一大段代码。只不过你跳转的是不是不同的页面啊,那我在,这个forward方法干嘛呢,在它的参数列表里面传一个参数进来,String uri,所以在里面访问的文件 拼成uri就行了,那我们在这个方法体里,还用到了response,所以我们也把HttpResponse response,也作为参数传进来,是不是就可以了,对吧,然后方法体里面的flush,要求我们抛异常,咱把异常给它接着往外抛了啊,那这样的话,这个forward方法就写好了,而这个方法写好以后,干嘛呢,在LoginServlet里面也能用,是这道理吧。

public void forward(String uri,HttpResponse response) throws Exception {
	//要想访问一个页面回去,我们先创建一个File表示那个页面
	File file = new File("webapp"+File.separator+"reg_ok.html");
	//要想给用户回应,要用response回应,要干这几件事。
	//1.用response回应的第一件事,设置状态行
	response.setStatus(HttpContext.STATUS_CODE_OK);
	//2.设置响应头:
	//1) 因为要响应一个页面,reg_ok.html,这个页面是.html格式,所以设置为"text/html"
	 //下面也可以不写死,但如果的确定返回的是一个.html页面,写死了也没有关系,将来实际开发中也是这样。(详解见笔记)
	response.setContentType("text/html");
	//2) 设置响应页面的大小,其值就是这个文件的大小。
	//下面的设置的ContentLength是一个int值,而file.length()返回的是一个long值,所以必须强转。
	response.setContentLength((int)file.length());
	//3.设置响应正文
	//1) 把响应的文件给到这个响应对象里
	response.setEntity(file);
	//2) 自动行刷新
	response.flush();
}

  • 那LoginServlet里面也一样,我在下面,定义出来forward这么一个方法,那定义出来这个方法,然后呢,上面怎么办,上面响应页面给客户端的业务,都不用写这么多了,我们把登录成功那,改成一句话,forward("/login_ok.html",response);,在里面参数里面,传它所要页面的路径就行了。下面登录失败也改为一句forward("/login_error.html", response);即可。在RegServlet注册成功的地方也是一样的,可以改为forward("/reg_ok.html", response);,那这样呢,拼好了以后,咱们再来测一下,看看成不成功啊,因为每一次改动,都要做一次测试,发现我们的改动是没问题的了。
  • 那么紧接着,那这个时候,我们RegServlet里面写了forward这样一个方法,LoginServlet里面也写了这样一个forward方法,那将来你要,还要处理其他的业务逻辑呢,那首先你是不是要在ClientHandler当中要再加一个else-if,再写一个Servlet,再调它的service方法,那个方法里面是不是也会跳转,是吧,那你会发现,是不是好多类都有个forward这个方法,那不理想,怎么办,大家注意这个时候,我们是不是就可以往上再抽象一层,为了让多个类共享同一个方法的话,我是不是可以统一定义成一个父类,你们都继承它这个forward方法,是不是子类就不用再去写一遍了,是不是应该抽个父类出来啊,因为你看呢,现在是RegServlet有这个forward方法,LoginServlet里面,是不是也有这个方法,对么,那我不想这样做怎么办啊,那是不是就可以,它们两之间,这两个类都有一个一模一样的方法,我将来要是再建个Servlet,是不是还有forward方法,我再建一个,还有,就是它们都有跳转这个问题吧。
  • 那怎么办啊?我不让你们每个类里面,都有个一模一样的方法,你们几个都继承统一的一个父类。我在父类里面就一次,所有子类不就都有这个方法了吗,是这个道理么,这就是咱们为什么要继承啊,继承不也是为了复用代码么,什么时候要继承啊,就是好几个类都要用这个方法,而不是说就是你这一个类里面要用,你这一个类里,好几段代码都用,你可以抽成一个方法,对么,好几个类都需要它,那就抽一个父类出来,明白这道理吧,OK啊。先不着急写了,改吧,把你的那个RegServlet里跳转页面的东西,挪到这个forward方法里面,改完以后,看看能使么,能使的话,登录就先不干了,因为登录里面,其实也是还要加这个forward这个玩应,对吧。所以咱们现在统一的做一件事情啊,我们定义一个父类出来,在这个core,这个包里面,在这个包里定义一个类,叫做什么呢,HttpServlet,OK了。
  • 那然后呢,我们把那个RegService类的那个forward方法,粘贴到这里面来,Ctrl+X,粘过来以后,我们要做的事情是什么,是不是让那个RegServlet,继承一下就行了,来都回到那个RegServlet,现在本身是不是报错了,因为里面的forward,没有这个方法,是吧,怎么办啊,咱们在上面,我们让那个RegServlet extends HttpServlet,继承自HttpServlet,重新跑一下注册,看看对么还,那既然这样的话,我们这个方法还可以复用到登录那个LoginServlet了。那这个时候,我们在定义这个LoginServlet的时候,让它也干嘛啊,也继承自HttpServlet,是吧,一旦它继承了以后,那它下面的那个forward方法是不是就也不用写了,对么,不用写了应该也是不会报错的了,那这样的话,就没有问题了。
  • 知道什么时候,应该抽象了么,什么时候应该在一个类里面抽象一个方法出来啊,当你发现你这个类里面,好几个地方都要用到同一段代码,你是不是可以把这个代码,抽象出一个方法,然后是那几段代码都调这个方法就完了。但你现在发现,你不是只在这一个类里面要用到这段代码,好几个类都要用到这段代码,怎么办啊,往父类上抽象,在父类里定义这个方法,所有子类一继承,是不是都有了。就是跨类共用方法的时候啊,就往父类上抽象。其实我告诉大家现在咱们写这点代码,光重构,咱还能再写一天,就是代码不加新的了,光重构,还能写一天,那不能再延了啊,延不了啊,不过没关系啊,咱这里面其实有很多设计上的不足,等后面一使用Tomcat以后,你才会发现,喔,它比咱之前写的可爽多了,人家都写好了,服务端不用你干了,什么怎么跳转,跳转怎么写的forward,这都不用你写了啊,你就直接,将来我告诉你,你们将来用Tomcat的时候,你只需要干嘛了,页面写好了,只需要去写Servlet就行了,一重写service,干业务逻辑就行了,剩下怎么流转,怎么流转,这都是Tomcat帮你干了,啊,这些活都不用你写。
  • 所以就是说,咱们这写的代码,在Tomcat里面,几乎百分之八十,九十,全给你干了,不过,自己写了一遍也挺好,你知道知道,它里面大概怎么玩,是吧。那么接下来,还有一点需要注意,你看啊,比如说,在这我还有一些事情是可以干的,正常情况下,咱们是不是访问任何一个网站的时候,上来我们是不是什么都不敲,只敲一个www.baidu.com,那这样的话,是不是一般就是去首页了,是吧,但你看,我们这一访问过来的时候,它这出来是什么玩应,是不是在控制台,资源路径就一个杠/了,什么都没有了,是不是啊,那如果是一个杠/的话,怎么办,那这样的话,咱们是不是可以加有个默认首页的功能,是吧。
  • 怎么再加一个默认的首页功能,你看啊,当我们这个ClientHandler类里的run方法,进来以后,拿到那个请求对象以后,我们在之后先获取了uri,之后咱们是不是直接先把它变成了一个文件,用这个File,是这意思吧,然后就继续访问到,它如果要是一个杠/的话,咱们就可以先看看是不是要去首页啊,所以就是说,咱们也可以先做一个功能是什么呢,是这样,比如说啊,我们把这个下面if这个地方,将它往后去放一放,比如说,上来,我先看什么呢?if("/".equals(uri)) {//去首页File file = new File("webapp/index.html");responseFile(HttpContext.STATUS_CODE_OK,file,response);},当然,将来new File("webapp/index.html");,这个东西咱们现在是写死的,那其实这个东西,你是不是可以后期,再给它变成配置文件,包括你看这个里面的webapp,是不是在好多地方都出现过了,那这个东西,其实就应该把它单独拎出去,是吧。
  • 所以我们之前说过,还有好多东西需要,重构的么,也就是说咱们先写("webapp/index.html"),是不是就去访问这个页面,然后responseFile,如果这项是一个杠/就走这了,否则呢,否则什么条件就走下面的那个文件啊,如果是个文件,怎么看它是个文件啊,就是else吧,是不是啊,else再去看以前的那些判断了,else里面再是以前的那一大套了。也就是以前的那些单独都在这个else里面了。然后看它里面是不是一个文件,是走这个文件的内容,不是的话,看它是不是请求一个功能,当我这么写完了以后,我们再去启动服务端,看如果我直接去访问域名的话,你看一访问首页的话,就直接进到首页页面,当然咱这个当时写的是假的,是这样的吧,当然我们在这个index.html首页页面上,是不是也可以加点东西了,我在代码里加两个超链接,可以吧,login.html去登录和reg.html去注册,这个连上了,是吧,连上不好看,那怎么办,加上空格是这意思吧,这回是不是就是分开了,然后你可以在登录页面再写一个回首页,这都是你们正常在网上都见过的这些超连接。

2.WebServer代码实现第二版

  • 那这样是不是就比较完整的网站了,以互相访问了,网址上的localhost的那个IP改为输入你要访问的IP地址,其实注意啊,标准的话,我们在html中,有专门的空格符,这不简单的敲一个空格,这low,这个等真正用html再聊这事了,咱就这么写吧。写完后,你们就可以把你的服务器启动起来,就可以访问网站了。WebServer这个服务端的工作其实东西并不多。除此之外,如果在登录注册时用户输入为空,我们在HttpRequest中对其默认赋值为nothing字符串,在注册时如果判断有任何一个获取的请求数据为nothing,就返回注册失败页面。在登录或注册,比较用户名和密码的逻辑,以及在finally中的关流操作,我们也给它们分别挪到单独的方法中实现,但需要注意的是,我们需要把IO相关的流实例化到方法(stack)之外,存于类(heap)中,用以在finally内的方法也可以作为方法的参数关闭流。见LoginServlet和RegServlet源码。

1.WebServer/src/main/java/common/HttpContext.java

package common;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

/**
 * HTTP协议相关信息定义
 * @author soft01
 *
 */
public class HttpContext {
	public static final int CR = 13;
	public static final int LF = 10;
	
	/*
	 * 状态代码定义
	 */
	//状态码-接受成功
	public static final int STATUS_CODE_OK = 200;
	//状态描述-接受成功
	public static final String STATUS_REASON_OK = "OK";
	
	//状态码-资源未发现
	public static final int STATUS_CODE_NOTFOUND =404;
	//装态描述-资源未发现
	public static final String STATUS_REASON_NOTFOUND = "Not Found";
	
	//状态码-服务器发生未知错误
	public static final int STATUS_CODE_ERROR = 500;
	//状态描述-服务器发生未知错误
	public static final String STATUS_REASON_ERROR = "Internal Server Error";
	
	/*
	 * 状态码-状态描述 对应的Map
	 */
	public static final Map<Integer,String> statusMap = new HashMap<Integer,String>();
	
	/*
	 * Content-Type映射Map
	 * key:资源类型(资源文件的后缀名)
	 * value:对应该资源在HTTP协议中规定的ContentType
	 * 
	 * 例如:index.html
	 * 那么这个文件在个该map中对应key应当是html
	 * value对应的值就是text/html
	 */
	/*
	 * 下面的Map实际上就是之前在response里写的200,OK;
	 * 现在把上面定义好的状态码和和状态描述放到这个Map里面。
	 * 通过static静态块对Map进行初始化。 
     */	
	public static final Map<String,String> contentTypeMapping = new HashMap<String,String>();
	
	static {
		/*
		 * 根据配置文件初始化相关信息
		 * /conf/web.xml
		 */
		//1初始化ContentType映射
		//在这里去调initContentTypeMapping()方法,将测试代码注释掉。
		initContentTypeMapping();
		
		//2初始化状态码-状态描述
		initStatus();
	}
	
	private static void initStatus() {
		//在这个方法里面将状态码和描述一个一个放入Map中
		statusMap.put(STATUS_CODE_OK, STATUS_REASON_OK);
		statusMap.put(STATUS_CODE_NOTFOUND, STATUS_REASON_NOTFOUND);
		statusMap.put(STATUS_CODE_ERROR, STATUS_REASON_ERROR);
	}
	
	private static void initContentTypeMapping() {
		/*
		 * 将web.xml配置文件中<type-mappings>中
		 * 的每一个<type-mapping>进行解析,将
		 * 其中属性ext的值作为key,将type属性的
		 * 值作为value存入到contentTypeMapping
		 * 这个Map中。
		 */
		System.out.println("初始化ContentType");
		try {
			//使用dom解析xml的第一件事是先创建SAXReader
			SAXReader reader = new SAXReader();
			/*
			 * 第二步就是去读那个xml文档,可以调用read方法,
			 * 方法参数可以new一个流,或者new一个文件,
			 * 因为它支持很多种构造方法,如果new一个文件的话,
			 * 路径如下,最好用File.separator代替斜杠,
			 * 然后把读到的内容放到一个Document对象里。
             */			
			Document doc = reader.read(new File("conf"+File.separator+"web.xml"));
			//然后就可以解析web.xml里面的东西了。先获取根元素 web标签,
			Element root = doc.getRootElement();
			// 再获取根里面的子标签<type-mappings>.
			Element mappingsEle = root.element("type-mappings");
			//再获取<type-mappings>它下面的所有子标签.
			List<Element> mappingList =mappingsEle.elements();
			//然后从mappingList中遍历每一个mapping
			for(Element mapping : mappingList) {
				//获取mapping的两个属性ext和type.
				String ext = mapping.attribute("ext").getValue();
				String type = mapping.attribute("type").getValue();
				//把着两个属性放到contentTypeMapping里。
				contentTypeMapping.put(ext, type);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/*//测试代码
	public static void main(String[] args) {
		//测试initContentTypeMapping()方法是否执行
		initContentTypeMapping();
		//输出一下contentTypeMapping
		System.out.println(contentTypeMapping);
	}*/
}

2.WebServer/src/main/java/core/ClientHandler.java

package core;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

import common.HttpContext;
import http.HttpRequest;
import http.HttpResponse;
import service.LoginServlet;
import service.RegServlet;

/**
 * 该线程任务用于处理每个客户端的请求。
 * @author fhy
 *
 */
public class ClientHandler implements Runnable {

	private Socket socket;
	public ClientHandler(Socket socket) {
		this.socket = socket;
	}	
	
	public void run() {	
		try {
			//System.out.println("进入到run方法!");
			InputStream in = socket.getInputStream();
			//创建对应的请求对象
			//System.out.println("已经从socket中获取到输入流in:"+in);
			HttpRequest  request = new HttpRequest(in);
			
			OutputStream out = socket.getOutputStream();
			//创建对应的响应对象
			HttpResponse response = new HttpResponse(out);

			/*
			 * 处理用户请求
			 * 0.获取用户请求资源路径:/index.html
			 */
			String uri = request.getUri();
			System.out.println("uri:webapp"+uri);
			//正常我们访问一个网站,只敲一个域名(不需要写资源路径),
			//比如www.baidu.com,它就会自动跳转到首页,我们也这样做。
			if("/".equals(uri)) {
				//去首页
				File file = new File("webapp/index.html");
				responseFile(HttpContext.STATUS_CODE_OK,file,response);
			}else {
				File file = new File("webapp"+uri); 
				if(file.exists()) {
					System.out.println("找到了相应资源"+file.length());
					/*
					 * 响应页面要向用户发送的内容:
					 * HTTP/1.1 200 OKCRLF
					 * Content-Type:text/htmlCRLF
					 * Content-Type:273CRLF
					 * CRLF
					 * 1010100101001011010101010111110010010(index.html数据)
					 */
					responseFile(HttpContext.STATUS_CODE_OK,file,response);
				    //查看是否请求一个功能,先看它请求是不是一个注册if("/reg".equals(uri)),如果要是注册的话就要先办法把用户注册信息写到文件里。
				} else if("/reg".equals(uri)) {
					//调RegServlet类中的service方法,让它去处理这个请求。首先第一件事,得先实例化出来RegServlet这个类的对象。
					RegServlet servlet = new RegServlet();
					//然后调用它的service方法,并把request对象和请求对象response传给这个方法作为参数。
					servlet.service(request,response);
				} else if("/login".equals(uri)) {
					//调LoginServlet类中的service方法,让它去处理这个请求。首先第一件事,得先实例化出来LoginServlet这个类的对象。
					LoginServlet servlet = new LoginServlet();
					//然后调用它的service方法,并把request对象和请求对象response传给这个方法作为参数。
					servlet.service(request,response);
				} else {
					System.out.println("没有资源:404");
					file = new File("webapp/404.html");
					responseFile(HttpContext.STATUS_CODE_NOTFOUND,file,response);
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				//如果不关流logo图片将出不来。
				socket.close();
			}catch (Exception e) {
				e.printStackTrace();
			}
		}			
	}
	
	/**
	 * 根据给定的文件分析其名字后缀以获取对应的ContentType
	 * @param file
	 * @return
	 */
	private String getContentTypeByFile(File file) {
		//获取文件名
		String name = file.getName();
		System.out.println("文件名:"+name);//打桩
		//截取后缀
		String ext = name.substring(name.lastIndexOf(".")+1);
		System.out.println("后缀:"+ext);//打桩
	    //获取对应的ContentType
		String contentType = HttpContext.contentTypeMapping
				.get(ext);
		System.out.println("contentType:"+contentType);
		return contentType;
	}

	/**
	 * 相应客户端指定资源
	 * @param status 响应状态码
	 * @param file 要响应的资源
	 * @throws Exception 
	 */
	private void responseFile(int status, File file,HttpResponse response) throws Exception {
		try {
			//1 设置状态行信息
			response.setStatus(status);
			//2 设置响应头信息
			//分析该文件后缀,根据后缀获取对应的ContentType
			response.setContentType(getContentTypeByFile(file));
			response.setContentLength((int)file.length());
			//3 设置响应正文
			response.setEntity(file);
			//4 响应客户端
			response.flush();
		} catch (Exception e) {
			throw e;
		}
	}
}

3.WebServer/src/main/java/core/HttpServlet.java

package core;

import java.io.File;

import common.HttpContext;
import http.HttpResponse;

/**
 * 用来处理Http的功能请求
 * @author fhy
 *
 */
public class HttpServlet {
	public void forward(String uri,HttpResponse response) throws Exception {
		//要想访问一个页面回去,我们先创建一个File表示那个页面
		File file = new File("webapp"+File.separator+uri);
		//要想给用户回应,要用response回应,要干这几件事。
		//1.用response回应的第一件事,设置状态行
		response.setStatus(HttpContext.STATUS_CODE_OK);
		//2.设置响应头:
		//1) 因为要响应一个页面,reg_ok.html,这个页面是.html格式,所以设置为"text/html"
		 //下面也可以不写死,但如果的确定返回的是一个.html页面,写死了也没有关系,将来实际开发中也是这样。(详解见笔记)
		response.setContentType("text/html");
		//2) 设置响应页面的大小,其值就是这个文件的大小。
		//下面的设置的ContentLength是一个int值,而file.length()返回的是一个long值,所以必须强转。
		response.setContentLength((int)file.length());
		//3.设置响应正文
		//1) 把响应的文件给到这个响应对象里
		response.setEntity(file);
		//2) 自动行刷新
		response.flush();
	}
}

4.WebServer/src/main/java/core/WebServer.java

package core;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Web服务器
 * @author fhy
 *
 */
public class WebServer {
	private ServerSocket server;
	private ExecutorService threadPool;
	
	public WebServer() throws IOException {
		try {
			server = new ServerSocket(8088);
			threadPool = Executors.newFixedThreadPool(100);
		} catch (IOException e) {
			e.printStackTrace();
			throw e; 
		}
	}

	public void start() {
		try {
			while(true) {
				System.out.println("等待连接...");
				Socket socket = server.accept();
				System.out.println("一个客户端连接了!");
				ClientHandler handler = new ClientHandler(socket);
				//System.out.println("ClientHandler创建了!");
				threadPool.execute(handler);
				//System.out.println("已经把ClientHandler交给线程池!");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}		
	}	
	
	public static void main(String[] args) {
		WebServer server;
		try {
			server = new WebServer();
			server.start();
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("服务端启动失败!");
		}
	}
}

5.WebServer/src/main/java/http/HttpRequest.java

package http;

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

import common.HttpContext;


public class HttpRequest {
	/*
	 * 请求行相关信息
	 */
	//请求方法
	private String method;
	//请求资源URI	 (统一资源定位)
	private String uri;
	//请求协议版本
	private String protocol;
	
	//消息报头信息
	private Map<String,String> header;
	
	//存储客户端传递过来的参数
	private Map<String,String> parameters;
	
	public HttpRequest(InputStream in) throws Exception {
		try {
			System.out.println("http构造方法!");
			//1解析请求行
			parseRequestLine(in);
			//2解析消息头
			parseRequestHeader(in);
			//3解析消息正文(略)
		} catch (Exception e) {
			throw e;
		}
	}
	/**
	 * 解析请求行
	 * @param in
	 * @throws IOException
	 */
	private void parseRequestLine(InputStream in) throws IOException {
		/*
		 * 实现步骤:
		 * 1:先读取一行字符串(CRLF结尾)
		 * 2:根据空格拆分(\s)
		 * 3:将请求行中三项内容设置到HttpRequest对应属性上
		 * 
		 * GET /index.html HTTP/1.1
		 */
		try {
			System.out.println("解析请求行开始!");
			String line = readLine(in);
			System.out.println("requestLine: "+line);
			/*
			 * 1.对请求行格式做一些必要验证
			 */
			if(line.length()==0) {
				throw new RuntimeException("空的请求行!");
			}
			
			//2.根据空格拆分请求行 
			String[] data = line.split("\\s");
			
			//3.将拆分的结果赋给相应属性上
			this.method = data[0];
			parseUri(data[1]);
			this.protocol = data[2];
			System.out.println("mothod: "+method+",uri: "+uri+",protocol: "+protocol);
			System.out.println("解析请求行结束!");
		} catch (IOException e) {
			e.printStackTrace();
			throw e;
		}
	}	

	/**
	 * 处理URI
	 * @param uri
	 */
	private void parseUri(String uri) {
		/*
		 * /index.html
		 * /reg?username=fancq&password=123456&nickname=fanfan
		 * 在GET请求中,URI可能会有上面两种情况。
		 * HTTP协议中规定,GET请求中的URI可以传递
		 * 参数,而规则是请求的资源后面以"?"分隔,
		 * 之后则为所有要传递的参数,每个参数由:
		 * name=value的格式保存,每个参数之间使用 "&"分割。
		 * 这里的处理要求:
		 * 将"?"之前的内容保存到属性uri上。
		 * 之后的每个参数都存入属性parameters中
		 * 其中key为参数名,value为参数的值。
		 * 
		 * 1:实例化HashMap用于初始化属性parameters
		 * 
		 */
		//1.先把HashMap初始化出来,否则一调用put方法就会报空指针。
		//因为咱们之前上面只是定义了parameters这个HashMap类型的属性。
		this.parameters = new HashMap<String,String>();
		
		//先分析uri中是否含有"?",有问号就说明它有参。
		int index = -1;
		/*
		 * 下面等于0的情况是不会有的,因为得符合http协议要求,
		 * 但是我们就这么写了,没有关系的。
		 */		
		if((index = uri.indexOf("?"))>=0) {
			//先按照"?"拆分
			this.uri = uri.substring(0,index);
			
			/*
			 * 截取出所有参数,paras的值就应该是如下:
			 * paras:username=fancq&password=123456&nickname=fanfan
			 */			
			String paras = uri.substring(index+1);
			/*
			 * 拆分每一个参数,以&分割,拆完以后,就应该是一个如下数组了。
			 * [username=fancq,password=123456,nickname=fanfan] 
			 */
			String[] paraArray = paras.split("&");
			//遍历每一个参数:遍历这个数组,把每一个参数放入Map。
			for(String para : paraArray){
				//按照"="拆分
				String[] paraData = para.split("=");
				if(paraData.length<2) {
					System.out.println("用户名或密码为空!");
					this.parameters.put(paraData[0], "nothing");
				} else {
					this.parameters.put(paraData[0], paraData[1]);
				}
			}
			//如果uri里面不含有问号,就是直接把uri这个参数,赋给当前对象的uri属性。
		}else {
			this.uri=uri;
		}
	}	
	
	
	/**
	 * 解析消息头
	 * @param in
	 * @throws IOException
	 */
	private void parseRequestHeader(InputStream in) throws IOException {
		/*
		 * 消息头由若干行组成
		 * 每行格式:
		 * name:valueCRLF
		 * 当所有消息头全部发送过来后,浏览器会单独发送一个CRLF结束
		 * 
		 * 实现思路:
		 * 1:死循环下面步骤
		 * 2:读取一行字符串
		 * 3:判断该字符串是否为空字符串
		 * 	若是空字符串说明读到最后单独的CRLF
		 * 	那么就可以停止循环,不用再解析消息头的信息
		 * 4:若不是空串,则按照“:”截取出名字与对应的值并存入header这个Map中
		 */
		try {
			System.out.println("解析消息头开始!");
			header = new HashMap<String,String>();
			String line = null;
			while(true){
				line = readLine(in);
				if(line.length()==0) {
					break;
				}
				int index = line.indexOf(":");
				String name = line.substring(0,index);
				String value = line.substring(index+1).trim();
				header.put(name,value); 
			}
		} catch (IOException e) {
			e.printStackTrace();
			throw e;
		}
		System.out.println("header:"+header);
		System.out.println("解析消息头完毕!");
	}
	
	/**
	 * 	根据输入流读取一行字符串
	 * 根据HTTP协议读取请求中的一行内容,
	 * @param in
	 * @return
	 * @throws IOException
	 */
	private String readLine(InputStream in) throws IOException {
		//请求中第一行字符串(请求行内容)
		StringBuilder builder = new StringBuilder();
		//c1是本次读到的字符,c2是上次读到的字符
		int c1=-1, c2=-1;
		while((c1=in.read())!=-1) {
			if(c1==HttpContext.LF&&c2==HttpContext.CR) {//c1==LF&&c2==CR
				break;
			}
			builder.append((char)c1);
			c2 = c1;
		}
		return builder.toString().trim();
	}
	
	public String getMethod() {
		return method;
	}
	public String getUri() {
		return uri;
	}
	public String getProtocol() {
		return protocol;
	}
	public Map<String, String> getHeader() {
		return header;
	}

	/**
	 * 根据参数名获取参数值
	 * @param name
	 * @return
	 */
	public String getParameter(String name) {
		return parameters.get(name);
	}
		
	/*
	 * 当测试通过后,这个段测试代码(打桩)就可以删掉或注掉了。
	 * 这个构造方法本应该传参的,但是为了测试,
	 * 定义一个无参的构造方法,这样能快速创建出来,
	 * 调用HttpRequest这个类中所要测试的方法parseUri(String uri)
	 */ 	 
/*	public HttpRequest() {
	}*/
	//测试代码
/*	public static void main(String[] args) {
		HttpRequest r = new HttpRequest();
		
		 // 为了测试方便,我们在parseUri(String uri)方法的参数里,
		 // 给一个死值,那这个方法就可以测试成功,因为这个方法里面,
		 // 会把这uri和保存表单参数的Map都初始化完毕了么,然后,
		 // 调完这个方法以后,我们下面输出一下初始化的uri和Map对不对,
		 // 如果对,就说明我们这个方法写的没错。
		 
		r.parseUri("/reg?username=fancq&password=123456&nickname=fanfan");
		System.out.println("uri:"+r.uri);
		System.out.println("param:"+r.parameters);
	}	*/	
}

6.WebServer/src/main/java/http/HttpResponse.java

package http;

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

import common.HttpContext;


/**
 * 表示一个HTTP的响应  
 * @author fhy
 *
 */
public class HttpResponse {
	private OutputStream out;
	
	//状态代码
	private int status;
	
	//响应头-ContentType
	private String contentType;
	
	//响应头-ContentLength
	private int contentLength = -1;
	
	//响应实体-文件
	private File  entity;
	
	public HttpResponse(OutputStream out) {
		this.out = out;
	}

	public int getStatus() {
		return status;
	}

	public void setStatus(int status) {
		this.status = status;
	}

	public String getContentType() {
		return contentType;
	}

	public void setContentType(String contentType) {
		this.contentType = contentType;
	}

	public int getContentLength() {
		return contentLength;
	}

	public void setContentLength(int contentLength) {
		this.contentLength = contentLength;
	}

	public File getEntity() {
		return entity;
	}

	public void setEntity(File entity) {
		this.entity = entity;
	}

	/**
	 * 将响应信息发送给客户端
	 * @throws IOException 
	 */
	public void flush() throws IOException {
		try {
			/*
			 * 响应页面要向用户发送的内容:
			 * HTTP/1.1 200 OKCRLF
			 * Content-Type:text/htmlCRLF
			 * Content-Type:273CRLF
			 * CRLF
			 * 1010100101001011010101010111110010010(index.html数据)
			 */
			System.out.println("发送响应信息");
			//1发送状态行
			responseStatusLine();
			//2发送响应头
			responseHeader();
			//3响应正文
			responseEntity();
		} catch (IOException e) {
			System.out.println("响应客户端失败!");
			throw e;
		}
	}

	/**
	 * 向客户端发送一行字符串,以CRLF结尾(CRLF自动追加)
	 * @param line
	 * @throws IOException 
	 */
	private void println(String line) throws IOException {
		try {
			out.write(line.getBytes("ISO8859-1"));
			out.write(HttpContext.CR);//CR
			out.write(HttpContext.LF);//LF	
		} catch (IOException e) {
			e.printStackTrace();
			throw e;
		}
	}
	
	/**
	 * 响应状态行
	 * @throws IOException 
	 */
	private void responseStatusLine() throws IOException {
		try {
			 String line = "HTTP/1.1"+status+" "+HttpContext.statusMap.get(status);
			 println(line);
		} catch (IOException e) {
			throw e;
		}
	}
	
	/**
	 * 响应头
	 * @throws IOException 
	 */
	private void responseHeader() throws IOException {
		try {
			String line = null;
			if(contentType !=null) {
				line= "Content-Type:"+contentType;
				println(line);
			}
			if(contentLength >=0) {
				line = "Content-Length:"+contentLength;
				System.out.println("长度:"+line);
				println(line);
			}
			
			//单独发送CRLF表示响应头信息完毕
			println("");
			
		} catch (IOException e) {
			throw e;
		}
	}
	
	/**
	 * 响应正文
	 * @throws IOException 
	 */
	private void responseEntity() throws IOException {
		BufferedInputStream bis = null;//移到try外面,方便关流。
		try {
			/*
			 * 将entity文件中所有字节发送给客户端
			 */
			bis = new BufferedInputStream(new FileInputStream(entity));
			BufferedOutputStream bos = new BufferedOutputStream(out);
			int d = -1;
			while((d = bis.read())!=-1) {
				out.write(d);
			}
			bos.flush();
		} catch (IOException e) {  
			throw e;
		} finally {
			if(bis != null) {
				try {
					bis.close();
				} catch (IOException e2) {
					e2.printStackTrace();
				}
			}
		}
	}
}

7.WebServer/src/main/java/service/LoginServlet.java

package service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

import core.HttpServlet;
import http.HttpRequest;
import http.HttpResponse;

public class LoginServlet extends HttpServlet{
public void service(HttpRequest request,HttpResponse response) {
System.out.println(“开始登录…”);//打桩
//获取用户名,密码
String username = request.getParameter(“username”);
String password = request.getParameter(“password”);
System.out.println(username+","+password);//打桩

	/*
	 * 读取userinfo.txt文件中的所有用户,并且逐个比较。
	 * 先现实功能,然后在考虑其他事,直截了当的话,首先你这读文件了,try-catch捕获异常是在所难免的。
	 */
	//先创建一个BufferedReader,按行读就可以了,当然,这个BufferedReader用完了得关掉,所以把它放到外面定义了。
	BufferedReader br = null;
	try {
		//基础工作做好后,在这里初始化br,new转换流,new字节流,传入要读取得文件。
		br = new BufferedReader(new InputStreamReader(new FileInputStream("webapp"+File.separator+"userinfo.txt")));
		Boolean flag = comparedNameAndPwd(br,username,password);
		//flag的值判断完成后,我们就可以在这根据它的值是否为true来决定它是否登录成功。
		if(flag) {
			System.out.println("登录成功!");//打桩
			/*
			 * 响应登录成功的页面给客户端(拷贝上面代码,登录失败也一样)
			 */
			forward("/login_ok.html",response);
		} else {
			System.out.println("登录失败!");
			forward("/login_error.html", response);
		}
	} catch (Exception e) {
		e.printStackTrace();
	}finally {
		closeBr(br);
	}
}

private void closeBr(BufferedReader br) {
	//先把基础的工作都写好,在这里把BufferedReader关掉。
	if(br!=null) {
		try {
			//关流时,它要求捕获异常,我们就捕获异常。
			br.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}		
}

private Boolean comparedNameAndPwd(BufferedReader br, String username, String password) throws IOException {
	//然后,按行读取文件的内容,先从文件里读取一行,如果读取到的内容不为空,就说明我读到一行了。
	String line = null;
	boolean flag = false;//登录是否成功
	while((line = br.readLine())!=null) {
		System.out.println("line:"+line);//打桩
		//按照","拆分出用户名和秘密
		String[] userInfos = line.split(",");
		/*
		 * 判断用户名和秘密是否一致	
		 */
		if("".equals(username.trim())||"".equals(password.trim())) {
			flag=false;
		}
		if(username.equals(userInfos[0])&&password.equals(userInfos[1])) {
			//登录成功! 将flag改成true,即这样逐行判断,只要有一个对了,就将flag改成true,之后就break,后面的就不用再读取判断了。
			flag = true;
		}
	}
	return flag;
}

}

8.WebServer/src/main/java/service/RegServlet.java

package service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;

import common.HttpContext;
import core.HttpServlet;
import http.HttpRequest;
import http.HttpResponse;

/**
 * 用来完成用户注册功能
 * @author fhy
 *
 */
public class RegServlet extends HttpServlet {
	/*
	 * 我们用这个方法来完成业务处理
	 */
	public void service(HttpRequest request,HttpResponse response) {
		//此时重启服务器,客户端访问注册页面就可以在控制台显示“开始处理注册!”
		System.out.println("开始处理注册!");//打桩
		/*
		 * 将用户的注册信息按行写入到
		 * userinfo.txt文件中。
		 * 每行为一条用户的信息,格式:
		 * username,password,nickname
		 * 例如:
		 * fanchuanqi,123456,fanfan
		 * liucangsong,222333,cangcang
		 * 
		 * userinfo.txt放在webapp目录下
		 */
		//获取用户名,密码,昵称
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		String nickname = request.getParameter("nickname");
		//拿到用户名,密码,昵称信息之后,就应该往文件里写入这些信息
		//在try-catch之外,首先创建一个PrintWriter。
		BufferedReader br = null;
		PrintWriter pw = null;
		//try-catch是必须的了。
		try {
			//基础工作做好后,在这里初始化br,new转换流,new字节流,传入要读取得文件。
			br = new BufferedReader(new InputStreamReader(new FileInputStream("webapp"+File.separator+"userinfo.txt")));
			//实例化PrintWriter,传入文件输出流,布尔值设置为true,可以追加字符。在if-else外部实例化,方便finally中关流。
			pw = new PrintWriter(new FileOutputStream("webapp"+File.separator+"userinfo.txt",true));
			Boolean isNamed = comparedName(br, username);
			if(isNamed||"nothing".equals(username)||"nothing".equals(password)||"nothing".equals(nickname)) {
				System.out.println("用户名已存在,或输入为空!");
				forward("/reg_info.html", response);
			} else {
				//将用户信息写入文件userinfo.txt
				pw.println(username+","+password+","+nickname);
				//如果在这里不写这个flush,当下面close的时候也会有flush.
				pw.flush();
				//这就写出去了,写完之后,我们输出一句话"注册成功"
				System.out.println("注册成功!");
				/*
				 * 响应注册成功的页面给客户端
				 */
				forward("/reg_ok.html", response);
			}
		} catch (Exception e) {
			//如果有异常的话,将异常输出一下。
			e.printStackTrace();
		}finally {
			closeBrAndPw(br,pw);
		}
	}
	
	private void closeBrAndPw(BufferedReader br, PrintWriter pw) {
		if(br!=null) {
			try {
				br.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if(pw!=null) {
			pw.close();
		}
	}

	private Boolean comparedName(BufferedReader br, String username) throws IOException {
		//然后,按行读取文件的内容,先从文件里读取一行,如果读取到的内容不为空,就说明我读到一行了。
		String line = null;
		boolean flag = false;//用户名可用
		while((line = br.readLine())!=null) {
			System.out.println("line:"+line);//打桩
			//按照","拆分出用户名
			String[] userInfos = line.split(",");
			/*
			 * 判断用户名和秘密是否一致	
			 */
			if(username.equals(userInfos[0])) {
				//用户名已存在
				flag = true;
			}
		}
		return flag;
	}
}

9.WebServer/conf/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web>
	<!-- Content-Type mapping -->
	<type-mappings>
		<type-mapping ext="html" type="text/html"/>
		<type-mapping ext="xml" type="text/xml"/>
		<type-mapping ext="png" type="image/png"/>
		<type-mapping ext="icon" type="image/icon"/>
		<type-mapping ext="jpeg" type="image/jpeg"/>
		<type-mapping ext="gif" type="image/gif"/>
		<type-mapping ext="css" type="text/css"/>
		<type-mapping ext="js" type="text/javascript"/>
	</type-mappings>
</web>

10.WebServer/webapp/

index.html
<html>
	<head>
		<meta charset="UTF-8">
		<title>百度一下,你就知道!</title>
	</head>
	<body>
		<center>
			<!-- <h1>百度一下,你就知道!</h1> 将这个几个字换成图片-->
			<img alt="tmooc" src="/logo.png"><br/>
			<!-- <img alt="百度" src="logo.png"><br/> 也可以-->
			<input type="text" size="30"/>
			<input type="button" value="百度一下"/><br/>
			<a href="login.html">去登录</a>    <a href="reg.html">去注册</a>
		</center>
	</body>
</html>
404.html
<html>
	<head>
		<meta charset="UTF-8">
		<title>404</title>
	</head>
	<body>
		<center>
			<img alt="" src="404.png"><br/>
			<h1>没有这个资源!</h1>	
		</center>
	</body>
</html>
login.html
<html>
	<head>
		<meta charset="UTF-8">
		<title>用户登录</title>
	</head>
	<body>
		<center>
			<h1>欢迎登录</h1>
			<form action="login" method="get">
				<table border="1">
					<tr>
						<td>用户名:</td>
						<td><input name="username" type="text" size="30"/></td>
					</tr>
					<tr>
						<td>密  码:</td>
						<td><input name="password" type="password" size="30"/></td>
					</tr>
					<tr>
						<td colspan="2" align="center"><input type="submit" value="登录"></td>
					</tr>
				</table>
			</form>
		</center>
	</body>
</html>
login_ok.html
<html>
	<head>
		<meta charset="UTF-8">
		<title>成功</title>
	</head>
	<body>
		<center>
			<h1>登录成功!</h1>	
		</center>
	</body>
</html>
login_error.html
<html>
	<head>
		<meta charset="UTF-8">
		<title>失败</title>
	</head>
	<body>
		<center>
			<h1>登录失败,用户名或密码不正确</h1>	
			<a href="login.html">重新登录</a>
		</center>
	</body>
</html>
reg.html
<html>
	<head>
		<meta charset="UTF-8">
		<title>注册用户</title>
	</head>
	<body>
		<center>
			<h1>欢迎注册</h1>
			<form action="reg" method="get">
				<table border="1">
					<tr>
						<td>用户名:</td>
						<td><input name="username" type="text" size="30"/></td>
					</tr>
					<tr>
						<td>密码:</td>
						<td><input name="password" type="password" size="30"/></td>
					</tr>
					<!-- <tr>
						<td>邮箱:</td>
						<td><input name="email" type="text" size="30"/></td>
					</tr> -->
					<tr>
						<td>昵称:</td>
						<td><input name="nickname" type="text" size="30"/></td>
					</tr>
					<tr>
						<td colspan="2" align="center"><input type="submit" value="注册"></td>
					</tr>
				</table>
			</form>
		</center>
	</body>
</html>
reg_ok.html
<html>
	<head>
		<meta charset="UTF-8">
		<title>成功</title>
	</head>
	<body>
		<center>
			<h1>恭喜你!注册成功!</h1>	
		</center>
	</body>
</html>
reg_info.html
<html>
	<head>
		<meta charset="UTF-8">
		<title>已存在</title>
	</head>
	<body>
		<center>
			<h1>注册失败!用户名已存在或错误的输入!</h1>	
		</center>
	</body>
</html>

11.WebServlet/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>cn.fhy</groupId>
  <artifactId>WebServer</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  
  <dependencies>
  	<dependency>
  		<groupId>dom4j</groupId>
  		<artifactId>dom4j</artifactId>
  		<version>1.6.1</version>
  	</dependency>
  </dependencies>
</project>

写在后面

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值