WebServer代码实现第一版

WebServer代码实现第一版:请求和响应信息的简单处理

前言

1.建立Socket连接与接收请求信息代码分析与实现

WebServer解析请求信息

  • 下面我们就开始这个服务器项目,当然将来,你不会使你自己写的WebServer,你会使用人家已经写好的,功能更强壮的,更安全,更好的一个WebServer,叫做Tomcat;而咱们写它的一个简单版,能实现基本功能的简易WebServer。我们先建立一个Maven Project,打包方式为jar包,然后,先在src/main/java中建立一个包,我们起名叫core,在里面建一个类WebServer,下面我开始写程序,先搭好结构,首先在我们Web服务端要有一个ServerSocket用来接受不同客户端连接,private ServerSocket server;,我们这先创建个ServerSocket,然后定义构造方法,public WebServer() { },然后是启动方法start,public void start() {},然后,是main方法。然后在main方法里做启动工作。

public static void main(String[] args) {
		WebServer server = new WebServer();
		server.start();
}

  • 然后来到构造方法WebServer,把这个WebServer初始化出来,server = new ServerSocket(8088);,声明报了异常,try-catch一下,catch到了,我们也处理不了,直接抛出去,让调用者处理下,throw e;

public WebServer() throws IOException {
	try {
		server = new ServerSocket(8088);
	} catch (IOException e) {
		e.printStackTrace();
		throw e; 
	}
}

  • 构造方法抛出了异常,main方法调用了构造方法,也要处理下异常。

public static void main(String[] args) {
	WebServer server;
	try {
		server = new WebServer();
		server.start();
	} catch (IOException e) {
		e.printStackTrace();
		System.out.println("服务端启动失败!");
	}
}

  • 然后呢,在start方法里面,我们来开始干活了!在start方法里要做的事情就是等待客户端连接了,实际上就是调用ServerSocket里面的accept方法,accept方法声明抛了异常,在这也try-catch一下,调用accept以后,咱们就可以得到一个返回值socket,这样,我们就可以等待一个客户端连接上了,那么连上以后,我们就可以就可以读客户端发过来的消息了。然后创建输入流InputStream,然后就可以读发过来的内容,创建InputStream in = socket.getInputStream()

public void start() {
	try {
		Socket socket = server.accept();
		InputStream in = socket.getInputStream();
	} catch (IOException e) {
		e.printStackTrace();
	}
}

  • 那么当咱们读取的时候,InputStream有了,那我们说将来浏览器一访问,就相当于它这么发送的,实际上它一口气就发了这么多东西过来:

GET /index.html HTTP/1.1CRLF 
HOST:doc.tedu.cnCRLF
Content-Length:23CRLFCRLF
... ...

  • 当然比我们上面的东西还要多,就类似于这么多。其实它就是发了这样的东西过来,最后有两个CRLF,就相当于另一个单独的换行。当然这个消息头里面的东西还有好几项,那么咱们读的话,就可以按行读,读到换行符为止。按行读,其实这都是些字符串,我们还可以创建成BufferedReader,这些不都是对字符么,首先我们先可以创建成字符流,然后把它变成BufferedReader,按行读。但是告诉大家,在这真的不能用BufferedReader,因为BufferedReader,它按行读的时候,它是按照不同系统,所在的那个换行符为依据,那就是windows和Linux还不太一样,所以说,你会发现它其实并不是以CRLF作为换行符,这是BufferedReader,你如果去看文档的话,你可以看的到,它不是以CRLF认为这是一个换行符,它是认为\n,就是换行了。你要用BufferedReader读的话,等于它不是以CRLF结尾的话,你一行读完了以后,它可能不是按照你想要的样子,去读下一行,去读下一行。
  • 所以那就不用呗!不用咱们是不是也能自己读字符,咱就原始一点,不一次读一行,而是一个字符,一个字符读,只要读到一个CR和一个LF,大家注意:我这虽然写的是CR和LF,但是CR也是对应的是一个字节的一个编码,就是回车的编码;然后,LF也是一个换行的编码,一个是10(LF),一个是13(CR),这个是,它有这个规定的,就是这都是对应的编码。那也就是说只要读到了CR和LF,我们就认为一行结束了,然后我们可以把这一行,我们之前所读到的所有字符,拼成一个字符串给你,那不也算是读了一行么!
  • 那么我们现在就自己来读,我就字节流就完了,我一次读一个字节,就是一次读一个字符,一个字符,一个字符的读。而且告诉你这块有一个事情啊,咱们要是一次读一个字节,不是一次应该读两个字节么?而且不同的编码,还读的不一样,你要是utf-8编码,应该读3个字节,才能变成一个中文,才能变成一个字符。那么到底读几个字节,怎么读法,不会想那么多,HTTP只传基本的那些英文,所以每个字符一个字节,这是HTTP要求,那是不是HTTP要求在这里面就不能传中文了,理论上是的!如果你要想传中文就很麻烦,你需要自己把那个,按照特定的字节转换成单字节,转成几个字节,然后你在读回来的时候,你再把那几个字节,你再把它翻译,再反转,再那么弄回来,所以你要在地址栏传中文是很恶心的。
  • 所以你们会发现,几乎你们是在地址栏上,没有见到中文的时候,就算见到中文,你也会发现,那都是%啥啥,%啥啥一大堆,其实那个就相当于把你这个中文,拆成了单字节,然后以什么编码写好,将来你再按照那个格式,你再把他都回成中文,那真的是这样的!HTTP就是这样,它就是对传中文很不友好, 这个你在将来在真正学习WebServer,写服务端的时候你会发现地址栏传中文是及其恶心的一件事情。那么那个我们就先不考虑了,现在就踏踏实实传英文,先把这一套弄明白再说,所以那我们就一个字节,一个字节去读就行了。因为在ASCII编码当中,一个字节就能表示一个字符。Unicode的前面的英文也一样,255个就是拉丁文那些东西abcd...,那些全有了,所以就是说它那一个字节就够使了。
  • 所以每个字符都是一个字节的!那在这就单字节读就行了。那咱们怎么读呢?那咱们拿到这个流以后,我们就可以循环读了,读每一个字节就拼成一个字符,一个字符拼,那这就导致一个问题,我怎么拼成字符串啊!我读多少个字节啊?实际上我读多少个字节不一定的,因为我只要读到CRLF为止,但是前面不一定是多长。因为你访问这个页面的名字是不一定的,所以它的长度是不一定的,但我又是一个一个字符读,那我要是回字符串怎么办呀。那我可以做这样一个模式,我创建一个StringBuilder,StringBuilder是不是就是用来修改字符串的,那我就可以,我读一个字符append,读一个字符append,往里拼呗!StringBuilder有了以后,读到一个拼一个,拼拼拼拼拼拼,读到CRLF,读完了,你那字符串里面的东西不就有了么!有了以后把它toString就拿到那个字符串了。这样更方便点吧!
  • 否则的话,你想想我们一次读多少个字节,我们不确定啊!不像原来我们说,我创建一个10K的字节数组,一下读10K,你这读不到10K啊,你都不知道你要读多少个字符,那咱只能一个一个的读了。那你最后要以字符串拿到,咱就拼呗,那只能是自己想办法啊!所以我的模式就是,先创建一个StringBuilder,StringBuilder builder = new StringBuilder();,然后我读到一个字符,我就往StringBuilder里拼一个,读一个拼一个,读一个拼一个,直到你读到CRLF为止,那就把CRLF之前的那段内容,最终就都拼到StringBuilder里面了。都拼好了以后,一toString,就拿到了那段修改好的字符串了。连续读取若干字符,直到连续读取到了CR(13) LF(10)为止。

GET /index.html HTTP/1.1CRLF
GET /index.html HTTP/1.1 1310
注意:CR那个字节,那个编码,如果你把它变成十进制的话,看的是13;然后LF对应的就是10,a不是97么! 就是我们把那个编码当成数字看的话。CR是13,LF是10.

  • 首先,咱们是要循环读,定义int c = -1;,然后呢,我们while循环,然后呢,首先,c=in.read();,就是读一个字节,然后这个字节的值不等于-1,while((c1=in.read())!=-1) { },那么,至少是说明我读到东西了,读到东西以后呢,我把它转成一个字符拼到这个StringBuilder里面去。实际应该类似于这样,builder.append((char)c);,把读到的这个字符转成char值,它实际上读到的,是一个字符么,是不是拼进去就行了。那我什么时候停啊?总得有个if判断,能够break吧,因为说白了,我是不是要读一行,就是当我连续读到了CRLF就应该停了,那么我这么写,if(c==13&&?==10){break;},什么等于10啊?说白了,是不是发现读一个字符好像不行,因为我得连续判断2个,一个是CR,一个是LF,所以我这么干,上面我定义2个,int c1=-1, c2=-1;
  • 首先,读一个字符呢,赋给c1,首先我先读一个字符,如果它不等于-1,说明我们是读到东西了。读到东西以后我们不马上把它拼进去,我们要先看,c1是不是等于10,假设读到的是最后一个字符,我看他是不是10,并且呢,c2是不是等于13;如果等于,我就break,你就理解为c1是本次读到的字符,c2是上次读到的字符,比如读这个 GET /index.html HTTP/1.1 1310,判断if(c1=10&&c2==13){ },如果是就break停掉,如果不是的话,我就调用这个builder.append(c1),我们把c1搁进去,搁进去以后,我就让c2=c1;,如果读到c1后if判断读到了不是CRLF末尾,不是连续符末尾,我就把c1赋给c2,也就是c2就是存的就是我上一次读的字符。
  • 另外用上述方法读取的字符串为GET /index.html HTTP/1.1CR,可以看出在append拼字符时,也把倒数第二个字符CR拼到了StringBuilder里,这里面多了个CR,我应该要的是CRLF之前的内容么!那也就是说这个东西,其实回车(CR),换行(LF)也好,它们都属于空白字符,不管是回车(CR),还是换行(LF)也好,它们都属于空白字符,你要不想要它的话,trim一下就行了,所以说当我们最后退出这个循环以后,由于多出了一个CR,所以我们在得到这个StringBuilder这个字符串的时候,我们调用builder.toString().trim();,这样就行了,出了这个循环等于说我这们这个第一行就完整的读完了。然后出来以后trim,把那个CR给他去掉。然后咱们就输出一下这个字符串,看看它是啥。

//请求中第一行字符串(请求行内容)
StringBuilder builder = new StringBuilder();
//c1是本次读到的字符,c2是上次读到的字符
int c1=-1, c2=-1;
while((c1=in.read())!=-1) {
	if(c1==10&&c2==13) {//c1==LF&&c2==CR
		break;
	}
	builder.append(c1);
	c2 = c1;
}
String line = builder.toString().trim();
System.out.println(line);

  • 那现在呢,我们就把这个服务端给他跑起来,大家注意,是不是一跑起来就卡住了,卡哪了,是不是卡在了accept了。当然现在控制台这,是不是等于什么都看不出来啊!这不理想啊!刚开始不利于我们编程,其实我们知道卡在哪了,最好,我们还是能掌控一下,看它到底是卡哪了,其实咱们在这个accept之前,咱们写一句话,比如说:等待连接...system.out.println("等待连接...");,我们看看是不是到这了。我们再运行一下,发现,诶,等待连接,是不是真的卡这了。好了么!那咱们就来,激动人心的一刻到了,试试。在浏览器网址输入框里,输入http://,这是不是我们正常输入域名的这个的标准的前面的http,这是协议,这是在写这个路径的时候,前面必须要有的,其实你要不写,浏览器也会给你加上。
  • 然后呢,后面就写那个地址了,对吧!那我们写地址什么呀,localhost就行了吧,或者写127.0.0.1,那在这咱们写localhost,然后呢,8088,直接8088,这不能这么写,是吧!我们知道光有IP地址不够,IP地址过来以后,你得有端口号,是不是才能找到我们的程序,那如果你不写,浏览器默认访问80端口,http请求,默认的端口是80,所以将来要是写一个服务端的话,你们默认可以开启80端口,这样的浏览器就不用写端口号。默认就是80端口,但如果我们要是自己定的话就得写端口号,那端口号怎么写呢,就在这个IP地址后面加一个冒号“:”,就行了,然后8088,明白吧,你这么写就可以了,即:http://localhost:8088,这样一执行,大家来看,诶,出来了,控制台上怎么是一堆数字啊,这啥啊这是,但首先,咱们先看它第一点,是不读过来了,是这样的吧!确定我们现在是读过东西来了,只不过这个,看他是一堆数,是吧!

等待连接...
716984324732728484804749464913

  • 那这个咱想办法去解决,先来试试访问一下,看它有没有反映,是一堆数么,那解释解释为什么是一堆数,是因为什么啊,咱们读过来以后,读到的每一个字节,读到的那个二进制,我们是不是以int形式保存的,而你看我这builder.append(c1),我这是不是直接就把这个int值拼进去了,是这道理吗?都看你们的代码,咱们这while循环,咱们读到以后,是咱们判断了一下这个字节,咱们是不是就直接把这个int值拼进去了,那这个不对,我们知道这个int值,实际上表示是一个char字符的吧,所以我们应该干吗啊,我们应该给他强转成char好了,把这先强转成char,builder.append((char)c1);,强转成char以后,重新启动一下服务端,我们再来一遍,回车,诶,这回看到发现是一个GET请求,GET / HTTP/1.1,就出来了。
  • 那这块为什么是个“/”啊?原因在于你那个路径写完了以后,你是不是什么都没加,你要这个,http://localhost:8088/index.html呢,先别按回车,因为我服务器是不是断了,咱服务器现在等于说,接收一次就停了,那我们再来重新启动,输入这个域名http://localhost:8088/index.html,这时你在控制台看到的就是,你访问的是,index.html,即GET /index.html HTTP/1.1,是这样的吧,那就都有了。咱们接着来看,咱们现在是不是只是读了一行,但其实之前咱们说过它给你发过来的内容,除了这个请求行之外,是不是还有消息头,那你怎么看啊,其实是不是还是以CRLF结尾读一行就完了,然后每行每行一读,就能看到都是什么,那就循环读呗。刚才写的while循环就是读一行输出的代码,然后呢,要是想再读怎么办啊,再读,你是不是要在外层再来个循环,干这件事!
  • 而大家会发现,其实咱们这个代码量已经堆了好多了吧!在start里面,你看看,又启动,又获得输入流,又读东西,其实这个就不理想,那么,咱把这个读一行的代码,咱先把它弄出去,因为现在,等于这一个方法里面的代码量,其实干了好几件事了,这已经其实违背了我们之前写代码的一个原则,叫做“高内聚,低耦合”,高内聚其实就是最好是什么啊,一个方法一个功能,不把所有的代码都堆一起,当然咱们可以慢慢的拆,对吧!首先,就算咱们现在不拆,你要是想读好多行,你是不是连这个创建的StringBuilder这一大堆东西,就要写到循环里面干了,你要想读好多行的话。

while(true) {
	//请求中第一行字符串(请求行内容)
	StringBuilder builder = new StringBuilder();
	//c1是本次读到的字符,c2是上次读到的字符
	int c1=-1, c2=-1;
	while((c1=in.read())!=-1) {
		if(c1==10&&c2==13) {//c1==LF&&c2==CR
			break;
		}
		builder.append((char)c1);
		c2 = c1;
	}
	String line = builder.toString().trim();
	System.out.println(line);
	//System.out.println("有没有回车");
}

  • 比如说,我先这么写,我把那些读一行的代码放到while(true){}里,死循环,然后里面创建一个StringBuilder,卡拉卡拉读一堆,读到CRLF结尾,输出一次,然后,再来从循环进来,然后再创建个StringBuilder,在卡拉卡拉读一堆,等于说你读一行要用一个while循环,然后要读好多行,外层又套了一个循环,这代码首先看的就不是那么流畅,首先我们先看看效果吧,咱先以实现为主,重新连接一下,又等待连接了吧!连一下请求,完了以后服务器发给我们这么多行:

 等待连接...
GET /index.html HTTP/1.1
Host: localhost:8088
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8

  • 第一行就是什么呀,请求行,然后下面都是,消息报头,你会发现,消息头的每一条都是一个冒号“:”分隔,然后左边一个,右边一个,Host的右边就是你的路径localhost:8088,当然你看路径里面请求的那个资源还带着那个冒号呢,当然你要不写端口号,就看不见冒号8088,就只能看见这个localhost,然后第二项,Connection,就是说客户端跟你时刻保持连接,keep-alive就是一直活着;然后Cache-Control就是咱们有没有缓存,然后等等一大堆的信息;你看User-Agent就是,我浏览器的内核的那些信息了,比如说这是Mozilla/5.0的,然后什么一大堆,什么内核,你看这个Chrome,我这不就是Chrome浏览器么,它们其实融和了许多的标准,那这个在上面都有所体现;
  • 然后Accept告诉你我想请求的是你的一个页面,text/html这个是我们的一个资源类型,就是告诉你这是一个文本格式的html页面,我想要这个东西,或者我还可以接收这一大堆,image,这我还可以接收图片啊,这一大堆等等这些类型,这它都可以给你发过来。然后再下面是什么呢,Accept-Encoding,我支持的版本,Accept-Language,我支持的语言是什么,它都会告诉你,也就是说,它都会告诉我们服务器,我是什么性能,我是怎么回事,都告诉你,然后你具有这些信息,将来递给我做回应。不过这些玩应,咱就不细说一下了,其实最主要的话,咱就有请求行这一句,就先够了,至少你要知道,你要的什么东西,我给你什么东西,具体的消息头这些玩应,我们先不看了,当然咱们后边,我们在写一些小案例的时候,咱们可能会用到一个到两个细节的一些这个消息头,但是这些东西咱不用一个一个的说了。
  • 但总之,我们http一个标准请求,至少这两部分都有,GET请求,大家可以看到没有消息体(消息正文),是吧,就是这消息正文是没有的,正常来讲请求行后面有一个单独的一个CRLF(根据rfc2616描述请求行后面没有的单独的CRLF的,只有消息头下面会有表示消息头的结束),消息头下面还有一个单独的一个CRLF(换行),只不过这个换行之前,什么都没有,那就什么都没输出了,是吧!你这等于输出一个空字符串,什么都没有。但其实在消息头下面还有一行,然后再往下。
  • 如果你要是POST请求,下面就是一堆字节了,就告诉你我这东西多长,你把它转回来以后,该是图片是图片,该是字符转字符,该是什么转什么,当然那它将来到底给我发的是图片啊,还是什么啊,那它会在那个消息头,给你定义出来,我下面那些数据,是什么类型的,是图片,还是文字,还是什么,你就根据那个东西,你把那些后面传给你的一些字节,理解成你想要的意思,去保存就好了,但那个,咱就不强调POST了,咱就先说这个基本的,至少我们能够看的到,一个请求过来,人家浏览器发了这么多信息给我们了。另外,因为每判断出一次CRLF,就会输出一行,之前说,在消息头后面还有一个空行,我们可以通过打桩,在每输出一行消息之后,再输出System.out.println("有没有回车");,可以看出消息正文与消息头之间是有一个回车空白(CRLF)隔开的,但请求行与消息头之间是没有多出一个CRLF隔开的。
  • 当然,你一访问服务器,这时浏览器都会显示一些内容,但现在什么都没有,服务端没有响应,因为其实咱还没理过客户端呢,就是浏览器你给我发了请求信息,服务器我这控制台都显示一下,我没理过你,咱也没创建过输出流,给客户端回信息么,是吧!所以浏览器那边,现在显示啥,不重要,先就看到它给我们发过来的内容,先完成第一步,至少我能先听到你跟我说的话了。现在都连接成功以后,我们大家来看,这样,这个方法我们读取这个东西是不是就在这都写了一大堆,那这个代码其实看的挺凌乱的了,咱现在整理整理。
  • 其实你看咱们做的事情其实并不多,我们只是连接上了以后,通过InputStream in = socket.getInputStream();,获取了一下输入流,然后就开始读那个请求信息了,只不过请求信息分很多很多行,然后每一行的读法都一样,然后我们都写在这块,start方法里了,我们就不这么干了,现在呢,先把读取一行消息的代码,把它拆出去,怎么拆呢?我们定义一个方法readLine,来干这件事,这样呢,我们这个循环里面,读取行消息的功能,调这个readLine方法,就一行代码就搞定了,就看到是调一个方法,然后呢,具体的细节,怎么读这一行,怎么读到CRLF,把这段逻辑搁到一个方法里,先在这让你这整体的功能,看的清晰一些。
  • 所以呢,现在做一件事情啊,我们在这个start方法下面,我们单独先定义一个方法,private String readLine(InputStream in){},我们定义一个方法叫readLine,然后我们把这个输入流传进来,我们通过这个流来读这个字节,然后,当然这个地方,while((c1=in.read())!=-1) {...},肯定要涉及到异常了,所以我们先把异常抛出去,这块应该是throws IOException,然后把读取一行字符串的代码,挪到这个readLine方法里面去,然后在这个方法里面,String line = builder.toString().trim();,读完这行字符串后,就直接把它return了,return builder.toString().trim();,把你拼好的那个字符串,return回去就行了,因为这个方法不就是读取一行字符串么,当readLine方法写好以后,之前的,在while(true){}循环里面输出一行的代码,就都不要了,就可以直接调readLine方法,然后把这个输入流in,给进去就行了,即String line = readLine(in);

public void start() {
	try {
		System.out.println("等待连接...");
		Socket socket = server.accept();
		InputStream in = socket.getInputStream();			
		while(true) {
			String line = readLine(in);
			System.out.println(line);
			//System.out.println("有没有回车");
		}
	} catch (IOException e) {
		e.printStackTrace();
	}		
}

/**
 * 	根据输入流读取一行字符串
 * 根据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==10&&c2==13) {//c1==LF&&c2==CR
			break;
		}
		builder.append((char)c1);
		c2 = c1;
	}
	return builder.toString().trim();
}

WebServer解析请求代码实现
package core;

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

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

	public void start() {
		try {
			System.out.println("等待连接...");
			Socket socket = server.accept();
			InputStream in = socket.getInputStream();			
			while(true) {
				String line = readLine(in);
				System.out.println(line);
				//System.out.println("有没有回车");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}		
	}
	
	/**
	 * 	根据输入流读取一行字符串
	 * 根据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==10&&c2==13) {//c1==LF&&c2==CR
				break;
			}
			builder.append((char)c1);
			c2 = c1;
		}
		return builder.toString().trim();
	}
	
	public static void main(String[] args) {
		WebServer server;
		try {
			server = new WebServer();
			server.start();
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("服务端启动失败!");
		}
	}
}

代码重构之解析请求信息:WebServer和ClientHandler

  • 上面的都写好了之后,但是现在还是有一个问题,你想想,比如说,我这正在循环接收一个客户端的消息,如果我这第一个人过来了,一连接,它这块就卡卡卡,读着东西呢,假设另一个人也连上呢,一样的吧!因为将来好多个客户端都要连我这个服务器,那么第二个客户端连我的时候,我是不是又重复去调accept了,那你想想啊,我要是再重复去调accept,我怎么还循环读啊,那又是这个问题了,也就是说,我要想处理多客户端的时候,是不是还得靠线程办这件事,怎么靠线程办,是不是可以把,就是读客户端发过来消息这件事搁到一个ClientHandler里去干,然后让accept接收客户端这个地方死循环,拿到一个连接,拿到一个Socket,我是不是就交给线程就完了。
  • 那咱们现在,下面先不再处理逻辑了,反正我们现在已经看到了,正常是不是可以读到一个客户端的消息了,那咱们现在就再把它拆一下,我们把读取请求消息这件事搁到线程里去干。那么咱们不把它new成内部类了,咱们搁到外边去,实际上是不是也应该作为WebServer的内部类,这个ClientHandler,如果别的地方不让用,你可以把它设计成内部类,但是咱们这块呢,把它拆开,每个类的代码量尽量让它少一些,为了逻辑能看的清楚一些,在这呢,我们在这个包core里面,再定义一个类就是ClientHandler,该线程任务用于处理每个客户端的请求。这个类要实现Runnable接口,重写父类run方法,先把结构建立好,首先在客户端处理类ClientHandler里要有一个客户端Socket,将服务端接收到的不同客户端的连接,赋给不同线程里的这个Socket单独处理,private Socket socket;,然后定义构造方法,在初始化构造方法得到WebServer发过来的Socket。

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

  • 在run里面,我要干吗啊,是不是就处理那个客户端的请求了,所以说白了,是不是应该用socket获取输入流然后读那些消息的事,应该放到这干,所以现在我们先把代码挪一挪,在WebServer里的start方法里,得到了浏览器客户端发来的socket以后,处理这个socket这个,就是这个客户端的一件事,是不是就不应该在WebServer里的start方法里干了,所以,我们把这个获取输入流,然后循环读,这件事,咱们Ctrl+X,剪切一下,粘贴到ClientHandler的run方法里来。

public void run() {
	InputStream in = socket.getInputStream();			
	while(true) {
		String line = readLine(in);
		System.out.println(line);
		//System.out.println("有没有回车");
	}
}

  • 报错了吧,谁没有啊,readLine这个方法没有,所以我们把readLine这个方法粘过来,readLine方法还在WebServer里,把readLine整个方法,剪切,Ctrl+X, 在ClientHandler类里的run方法下面Ctrl+V粘过来就行了,然后把两个类都保存一下。那么现在,大家注意,ClientHandler里面应该有两个方法吧,一个run方法,一个readLine方法,然后在run方法中,socket.getInputStream();readLine(in);都声明报了IOException异常,我们把它们一起try-catch一下。

public void run() {
	InputStream in;
	try {
		in = socket.getInputStream();
		while(true) {
			String line = readLine(in);
			System.out.println(line);
			//System.out.println("有没有回车");
		}
	} catch (IOException e) {
		e.printStackTrace();
	}			
}

/**
 * 	根据输入流读取一行字符串
 * 根据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==10&&c2==13) {//c1==LF&&c2==CR
			break;
		}
		builder.append((char)c1);
		c2 = c1;
	}
	return builder.toString().trim();
}

  • 然后,现在回到WebServer,WebServer现在start里面,应该就两句了,等待客户端连接,还有一个server.accept(),是这样的吧,那现在,我们是不是就可以循环接收了,那我们将这两句包含在死循环里面,while(true){}

while(true){
	System.out.println("等待连接...");
	Socket socket = server.accept();
}

  • 那么这个时候,一旦一个客户端连接以后,我们干吗啊,是不是就可以启动一个线程跑了,那我们怎么做,ClientHandler handler = new ClientHandler(socket);,然后呢,把这个socket给进去,然后创建一个线程,Thread t = new Thread(handler);,然后把这个线程handler给进去,然后t.start();,跑起来。那么做完这么一步,等于说,现在咱们代码,就是逻辑没有什么变化。咱们只是把结构调整一下,现在其实也就说,让我们的服务端能够同时接收多个客户端了。咱们现在当然还是一个客户端就行了,咱先跑一下,看看效果。
代码实现
WebServer:
package core;

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

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

	public void start() {
		try {
			while(true) {
				System.out.println("等待连接...");
				Socket socket = server.accept();
				ClientHandler handler = new ClientHandler(socket);
				Thread t = new Thread(handler);
				t.start();
			}
		} 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("服务端启动失败!");
		}
	}
}
ClientHandler:
package core;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
/**
 * 该线程任务用于处理每个客户端的请求。
 * 
 * @author fhy
 *
 */
public class ClientHandler implements Runnable {

	private Socket socket;
	public ClientHandler(Socket socket) {
		this.socket = socket;
	}	
	
	public void run() {	
		try {
			InputStream in = socket.getInputStream();
			while(true) {
				String line = readLine(in);
				System.out.println("ss: "+line);
				//System.out.println("有没有回车");
			} 
			
		} catch (IOException e) {
			e.printStackTrace();
		}			
	}
	
	/**
	 * 	根据输入流读取一行字符串
	 * 根据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==10&&c2==13) {//c1==LF&&c2==CR
				break;
			}
			builder.append((char)c1);
			c2 = c1;
		}
		return builder.toString().trim();
	}
}
为什么会无限回车空白
  • 有这样的解释,说这时会使劲的输出回车空白,知道这是为什么么,为什么现在这时停不下来了,原因在于,实际上客户端给我们断了,客户端看你半天不理我,断!它,其实我们知道,客户端不也是应用一个socket连上么!它把请求都发给你了,然后你不理它,半天不回应,那它就认为服务器没有回应,就和你断开连接了,一旦断开连接,就会出现什么情况啊,我们这不是死循环读呢么!我们在ClientHandler的run方法中不是死循环读呢么! readLine这个方法里已经读不到东西了(每次都把c1和c2的初始值-1拼StringBuilder里,trim去空白之后,实际上什么都没有,在整理完结构后的ClientHandler类run方法里面while(true)里System.out.println(line)输出时,就一直循环回车换行了!),不过没关系,咱先这么写着。
  • 以上括号内的解释,其实是不对的,其他的总体解释还勉强可以接受。实际上,经过对每次输出做了时间延迟之后,代码如下:
WebServer:
ClientHandler:
package core;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.sql.Time;
/**
 * 该线程任务用于处理每个客户端的请求。
 * 
 * @author fhy
 *
 */
public class ClientHandler implements Runnable {

	private Socket socket;
	public ClientHandler(Socket socket) {
		this.socket = socket;
	}	
	
	public void run() {	
		try {
			InputStream in = socket.getInputStream();
			while(true) {
				//请求中第一行字符串(请求行内容)
				StringBuilder builder = new StringBuilder();
				//c1是本次读到的字符,c2是上次读到的字符
				int c1=-1, c2=-1;
				if((c1=in.read())==-1) {
					System.out.println(builder);
					System.out.println("文件末尾-1");
//					break;
				} else {
				while((c1=in.read())!=-1) {
					//System.out.println("???: "+c1);
					if(c1==10&&c2==13) {//c1==LF&&c2==CR
						break;
					}
					builder.append((char)c1);
					c2 = c1;
				}
				}
				String line = builder.toString().trim();
				long time1= System.currentTimeMillis();
				int n=100;
				for(int i=1; i<n; i++) {
					long time2 = System.currentTimeMillis();
					if((time2-time1)>1000) {
						n=i;
					}else {
						i=1;
					}
				}
				System.out.println("ss: "+line);
				//System.out.println("有没有回车");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}			
	}
	
	/**
	 * 	根据输入流读取一行字符串
	 * 根据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==10&&c2==13) {//c1==LF&&c2==CR
				break;
			}
			builder.append((char)c1);
			c2 = c1;
		}
		return builder.toString().trim();
	}
}
输出结果:
等待连接...
等待连接...
等待连接...
ss: ET /index.html HTTP/1.1
ss: ost: localhost:8088
ss: onnection: keep-alive
ss: ache-Control: max-age=0
ss: pgrade-Insecure-Requests: 1

文件末尾-1
ss: ser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36
ss: 

文件末尾-1
ss: ccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
ss: 

文件末尾-1
ss: ec-Fetch-Site: none
ss: 

文件末尾-1
ss: ec-Fetch-Mode: navigate
ss: 

文件末尾-1
ss: ec-Fetch-User: ?1
ss: 

文件末尾-1
ss: ec-Fetch-Dest: document
ss: 

文件末尾-1
ss: ccept-Encoding: gzip, deflate, br
ss: 

文件末尾-1
ss: ccept-Language: zh-CN,zh;q=0.9
ss: 

文件末尾-1
ss: 

文件末尾-1
ss: 

文件末尾-1
ss: 
... ... 
分析结论:
  • 由以上运行结果,可以知道,等待连接...,输出了3次,说明浏览器其先后发起了两次请求,通过输出的情况可以看出,第一次发起的是正常的真正要发送的请求,第二次,发出的是一个空的请求,服务器端直接读到了-1。经过推断,是这样的,浏览器先后发起了这两个请求,服务器也随即开启了两个线程去运行。第一个是正常的HTTP请求,服务器端正常接收读取了请求信息,在接收数据后正常结束;而第二个浏览器发送了空的请求,并非HTTP协议请求,而服务器没有对浏览器做出任何响应,浏览器便主动断开了客户端的Socket,而一旦客户端Socket断开后,服务端Socket还是能读取到数据的,且每次读取到的数据都是-1
  • 因为在每次读取时都创建了一个StringBuilder,这个StringBuilder在死循环内,在第一个正常的HTTP请求的线程正常结束后,实际上第二个线程中的空请求,发现了服务端没有反应,就自动断开了,所以每次读取的结果,都是返回了-1,从而导致,无限的创建StringBuilder,并无限的输出,而所有的线程在控制台的打印,都是在同一个屏幕上输出的。这也就是为什么出现了无限回车空白的情况。

2.代码重构之连接池:WebServer

  • HTTP是无状态的连接方式,它不是长连接,什么叫长连接,就是一直连着,就叫长连接。咱们现在所谓一个短连接,那么这就导致了一个问题,我们这怎么做的,一个客户端和我连上,交给一个ClientHandler,把线程跑起来,它这是不是这就去处理了,将来处理它发过来个请求,把我这响应给你,实际上是不是咱俩就断了,一旦断了,我们这ClientHandler工作都干完,意味着这个线程就死掉了。我再来一个请求呢,你又启动一个线程 ,给你一个响应,又死掉了。这个可和聊天室不一样,聊天室是我跟你连上以后,咱俩是不半天不会断,我跟你聊,聊个几分钟,聊多久,是不是不一定了,你那线程是不是活很久!但是在这呢,可能一秒钟,如果将来我们带宽快,一秒钟都用不了,线程创建,马上线程就销毁了,如果用户比较多,那线程在频繁的创建和销毁,这不理想,对吧!
  • 那这个时候我们就不该自己去new线程了,我们应该用线程池去维护了,我创建了一批线程,用完了给我,一会谁来用,再拿出去用,我不要让这些线程,来回来去的创建销毁,这样的话,是会给系统带来不少负担的,那这里先把线程池创建好,那么我们在我这个WebServer当中,我们除了创建ServerSocket之外,我们再建立一个属性,就是线程池,private ExecutorService threadPool;,比如说,我们同时处理并发100个,这个当然我们是可调的,咱先写死了,那么怎么做呢,咱先还是在构造方法里,把它初始化出来,threadPool = Executors.newFixedThreadPool(100);,那这个new出来以后,那下面呢,在WebServer类start方法里面,咱们是不是就不用自己new线程了,那我们怎么做啊,任务创建好了,怎么交给线程池,调用线程池的一个方法吧,这个方法叫什么啊,叫excute,是这样的吧!我们把这个handler交给它,threadPool.execute(handler);,剩下的工作,是不就它干了,我们就不管了。

代码实现WebServer

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();
				ClientHandler handler = new ClientHandler(socket);
				threadPool.execute(handler);
			}
		} 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("服务端启动失败!");
		}
	}
}

3.处理请求信息:HttpRequest

HttpRequest代码结构

  • 也就是说,现在当一个请求过来,咱是不是就交给了一个ClientHandler的run方法,用它来去处理客户端请求。那一会要做的事情是什么,我们将读取到的请求信息,首先分为3个部分,请求行部分,消息头部分和消息正文部分。那么第一件事是解析请求行的请求资源,由于后面还要对响应信息作处理,所以这里可以这样做,对于请求行,消息头,消息正文,这些都是请求,可以单独构建一个类,就是请求类,让这个请求类封装所有请求信息。把所有请求信息都封装到一个对象里,然后我们在那个对象里面,先把请求那些东西都拿到以后,该什么地方,搁什么地方。你将来用的时候,你想GET哪部分,我就直接把哪部分给你。
  • 那么咱们不在这个核心的包core中干这件事了,这个核心包就是做服务端的工作,再建一个包,起名为http,我们把这些内容都放到这,因为这都属于http的东西,然后,在这包里面新建一个类,HttpRequest,在这个类里面,我们来做这件事情,那首先呢,咱们定义一个构造方法,public HttpRequest(InputStream in){},我们在new的同时,再做一些解析工作,然后我们把这个输入流传进来,因为我们都是从socket那个输入流读的么,所以我们把那个输入流传进来,对于一个请求而言,包含这么几个部分,有请求行,包括请求方法,资源与协议和版本,剩下的就是一堆消息头里的东西,而消息头是不是就两部分组成,冒号隔开。

public class HttpRequest {
	/*
	 * 请求行相关信息
	 */
	//请求方法
	private String method;
	//请求资源URI	 (统一资源定位)
	private String uri;
	//请求协议版本
	private String protocol;
	
	//消息报头信息
	private Map<String,String> header;
		
	public HttpRequest(InputStream in) {
			//1解析请求行
			//2解析消息头	
			//3解析消息正文(略)
	}
}

  • 那么在HttpRequest这里面,我们怎么存这么一大堆信息,首先它们都是字符串啊,于是我这么设计,首先有,请求的方法,然后呢,请求的资源路径,还有协议,这3项分别就是请求行里的东西,private String method;private String uri;private String protocol;然后呢,关键是还有消息头那么一大堆,你会发现里面每一项是不是就是一个名字对应一个值,那咱能把它放一个map里,map不就是key-value形式的,所以咱再定义一个map,private Map<String,String> header;
  • 现在呢,我们就把请求里面的内容拆开了,对于咱们现在来讲,请求里面就是这些信息,但其实还有一个,咱没用到,就是消息正文,但是对于get请求,没有消息正文,将来有的话,我们再加上就好了,对于拆分这几部分,咱们是不要从输入流里面给它读回来,我们之前在ClientHandler里直接就读了,第一行请求行,第2行,什么什么,还输出了吧,现在,我们不仅要把它们读出来,我们还要把它们都保存好,那将来你要用的时候,你是不是就get拆分好的这些信息就行了,就把你想要的东西,就都获取到了。
  • 现在,我们就把请求的那一大堆信息,按行都读出来,然后该放哪呢,咱就把它们放哪,保存上,那这样这个Request实例,它就可以表示客户端发过来的请求里面所有的信息,就都有了,那咱们就开始在构造方法中开始解析工作,解析工作其实就分为3步走,第一步解析请求行,第2步解析消息头,其实将来还有第3步解析消息正文,只不过这里面咱们暂时先略了啊,因为我们没有涉及到那么复杂的东西,但是前两步肯定要有的啊,这个地方因为要涉及读写啊,所以我们try-catch把这个异常捕获一下,catch里应该把异常抛出去啊,throw e;,因为我们要拿到这个东西以后,得解析清楚,解析如果失败的话,要告诉外边,我们这个东西不对,那下面的操作就不用干了。

public HttpRequest(InputStream in) throws Exception {
	try {
		//1解析请求行
		parseRequestLine(in);
		//2解析消息头
		parseRequestHeader(in);
		//3解析消息正文(略)
	} catch (Exception e) {
		throw e;
	}
}

  • Java的Exception有分为两类:IOException(非运行时异常)和RuntimeException(运行时异常)非运行时异常属于可检查异常,这种异常必须要在编译时强制处理,在try-catch时必须可以检查到才行,否则会报错:Unreachable catch block for IOException. This exception is never thrown from the try statement body,但运行时异常则无需如此,即使在try-cath时没有检查到此类异常也不会编译报错,Exception是它们的父类,所以和运行时异常一样,不会报错,所以这里虽然知道是IOException,但因为后面抛出IOException的解析方法,还没有实现,为了方便,直接写Exception进行处理。
  • 那这里呢,其实解析的代码也很多的,所我们不在HttpRequest构造方法这干,只是告诉你我这有这么3件事,每一步咱都写成一个方法,给它写分开,所以我们先定义第一个,解析请求行,我们要在下面单独定义一个方法,这个方法不需要让外界知道,所以是一个private,这是我们自己干的事,private void parseRequestLine(InputStream in){},拿到这个输入流以后是不是先读第一行,读完第一行以后呢,按空格拆,第一个赋给属性method,第2个赋给uri,第3个赋给protocol,写完以后,在上面构造方法里第一步,解析请求行只需调用parseRequest(in)这个方法即可,来处理这件事;这时把输入流传入构造方法里后,解析完请求行,method,uri,protocol,这个3个属性就应该有值了;
  • 而第2步解析消息头,通过输入流再解析消息头,之后,上面的消息头属性Map里面,东西就全了。将来你这个post请求,还有第3步消息正文,将来你再解析消息正文,然后再定义一个属性保存上,因为咱们这还没涉及到消息正文,咱就先不干了。解析消息头,我们再来个方法private void parseRequestHeader(InputStream in){},然后,我们也给它放在构造方法里,第2步去调用它parseRequestHeader(in),具体先不写了。代码如下:

package http;

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

public class HttpRequest {
	/*
	 * 请求行相关信息
	 */
	//请求方法
	private String method;
	//请求资源URI	 (统一资源定位)
	private String uri;
	//请求协议版本
	private String protocol;
	
	//消息报头信息
	private Map<String,String> header;
	
	public HttpRequest(InputStream in) {
		try {
			//1解析请求行
			parseRequestLine(in);
			//2解析消息头
			parseRequestHeader(in);
			//3解析消息正文(略)
		} catch (Exception e) {
			throw e;
		}
	}
	
	private void parseRequestLine(InputStream in) {
	}	

	private void parseRequestHeader(InputStream in) {
	}
}

重构代码之分离请求信息:ClientHandler和HttpRequest

  • 咱们先回到ClientHandler这个类里面,就是咱们处理一个客户端这件事,原来咱们连上一个客户端,咱就启动一个线程,然后这个线程调用run方法就跑起来了,然后就通过socket获取输入流了么,InputStream in = socket.getInputStream();,在获取输入流之后,就直接读请求行,然后消息头,后面一大堆东西吧。

while(true) {
	String line = readLine(in);
	System.out.println("ss: "+line);
	//System.out.println("有没有回车");
} 

  • 那么这些事是不是就不应该在这干了,咱就是因为这个才定义HttpRequest,所以呢,这一大堆不用写了,之前写过的吧,就不需要了,因为我们没必要把东西都堆在这个地方,咱们把这些信息呢,都交给HttpRequest里面去干就好了。而ClientHandler这块,以后就直接从HttpRequest类这个类,拿到我这个请求的所有信息,就完了。那么在这我们干嘛呢,创建好输入流以后,我们来创建对应的请求对象,HttpRequest request = new HttpRequest(in),把这个in传进去就好了。这里之前catch到的是IOException,因为我们在HttpRequest构造方法上抛出的是Exception,所以这里改为catch (Exception e) {...}。代码如下:

public void run() {	
	try {
		InputStream in = socket.getInputStream();
		//创建对应的请求对象
		HttpRequest  request = new HttpRequest(in);
	} catch (Exception e) {
		e.printStackTrace();
	}			
}

  • 写到这先走一下思路,当我们服务端启动起来以后,start方法一调用,就开始循环等待客户端连接,那这个时候,当一个客户端,当它访问我们的地址,这个时候,一访问过来,accept就接收到了,接收到以后就连接上了,之后呢,我们就new一个CliendHandler,把这个连接放到线程池里面跑起来了,线程池就分配一个线程执行这个任务,一旦这个任务被执行,那么,它的run方法就被执行了,run方法一执行以后,我们是不是已经把这个socket传给ClientHandler里了,我们通过之前在WebServer类里面new ClientHandler时就把socket传进去了,ClientHandler handler = new ClientHandler(socket);,是吧,然后它一跑起来,它这run方法一执行,执行以后,就通过传过来的这个socket获取输入流,是不是就准备读那个客户端浏览器发过来的那个请求信息了。
  • 那我们说了,因为请求信息里面包含了大量的信息,所以我们不在ClientHandlet这个类里面做解析了,全交给请求对象HttpRequest去干,那就是说,HttpRequest的每一个实例,是不是就表示一个客户端发过来的整个请求的所有内容,然后,那我们是不是就把这个in,这个输入流,传给这个HttpRequest的一个实例进行解析,把想要的信息都设置好,我将来从这个request对象上,取得我想要的信息就完了,就把请求行的信息啊,消息头的信息啊,我就从request对象里去拿,那当然我这一new HttpRequest,怎么干的呢,那你那个构造方法,构造方法里干嘛了,第一件事,解析请求行,就相当于我拿到这个输入流以后,我准备先读请求行,然后呢,第2步解析消息头,把消息头里的一堆东西就都读出来了,就都分别设置好了,那这样的话,我这个request就有你这客户端发给我的请求信息里面的所有内容了。
  • 那将来我用这个请求信息的时候,我是不是就从request里面取就完了,调get方法,一会我们给那些属性都加好get方法,一个一个获取就完了。那既然是这样的话,我们一调HttpRequest它的构造方法,进到HttpRequest的构造方法里,执行解析请求行,调用parseRequestLine(in),就来到private void parseRequestLine(InputStream in){...},这个方法里了,那解析怎么做,输入流也给你了,就读取第一行呗,GET /index.html HTTP/1.1,读完了以后呢,按空格拆呗,拆完3项呢,GET设置到method上,/index.html设置到uri上,HTTP/1.1设置到protocol,是不是就干完了,干这3件事,那怎么读一行啊,之前不是写好了么,那个readLine方法,你不还用那个方法从那个流里读一行么,对吧,只不过又发现一件事,我们那readLine方法,我们之前从最早从WebServer挪到ClientHandler里去了,现在这个解析工作又不在ClientHandler里干了,所以我们还得从ClientHandler里,又把这方法挪一下,挪到HttpRequest里面来,是吧。
  • 所以,再ClientHandler往下找,是不就是找到这个方法readLine,ctrl+X,剪切,从ClientHandler当中再剪切一下,粘哪去啊,就粘HttpRequest里面呗,是吧,我们在HttpRequest下面,我们找一个空白,这个地方,就在我们那个parseRequestHeader,在这个方法下面,我们把它粘上,粘过来了以后,这些类该保存的都保存一下。
代码实现ClientHandler
package core;

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

import http.HttpRequest;

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

	private Socket socket;
	public ClientHandler(Socket socket) {
		this.socket = socket;
	}	
	
	public void run() {	
		try {
			InputStream in = socket.getInputStream();
			//创建对应的请求对象
			HttpRequest  request = new HttpRequest(in);
		} catch (Exception e) {
			e.printStackTrace();
		}			
	}
}
代码实现HttpRequest
package http;

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

public class HttpRequest {
	/*
	 * 请求行相关信息
	 */
	//请求方法
	private String method;
	//请求资源URI	 (统一资源定位)
	private String uri;
	//请求协议版本
	private String protocol;
	
	//消息报头信息
	private Map<String,String> header;
	
	public HttpRequest(InputStream in) throws Exception {
		try {
			//1解析请求行
			parseRequestLine(in);
			//2解析消息头
			parseRequestHeader(in);
			//3解析消息正文(略)
		} catch (Exception e) {
			throw e;
		}
	}
	
	private void parseRequestLine(InputStream in) {
	}	

	private void parseRequestHeader(InputStream in) {
	}
	
	/**
	 * 	根据输入流读取一行字符串
	 * 根据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==10&&c2==13) {//c1==LF&&c2==CR
				break;
			}
			builder.append((char)c1);
			c2 = c1;
		}
		return builder.toString().trim();
	}
}

调试代码之打桩

  • 把请求信息的处理分离到HttpRequest以后,将代码保存好,先不写代码,将来工作中也是,又干了一大堆,比如说将来我们自己有这种想法,结构又设计好了,是吧,那么,马上干嘛呢,不写代码,先打桩,怎么打桩啊,比如HttpRequest这个类,咱们按理来讲,是不是ClientHandler里的run方法一执行,这socket拿到输入流以后,InputStream in = socket.getInputStream();,是不是就调这个HttpRequest这个构造方法了,HttpRequest request = new HttpRequest(in);,那是不是按理来讲,应该就能调到我们这个HttpRequest的构造方法,一旦进来,这就是不是要执行这两步,解析请求行和消息头,那比如到 parseRequestLine(in);,在这句话,在它之前输出一句话,比如输出什么呢,http构造方法,System.out.println("http构造方法!");,那也就是,这句话一输出,是不是就说明我们调到了HttpRequest的这个构造方法了。
  • 然后呢,HttpRequest构造方法调用完了以后,是不是就到它里面的parseRequest(in);,是吧,那应该是不是就进到,private void parseRequestLine(...),这个方法里面来,那我在这里面也输出一句话啊,比如说输入什么呢,解析请求行,System.out.println("解析请求行!");,那这句话要是输出,就说明parseRequest(InputStream in),这个方法就调了;那下面是不还有一个,解析什么啊,消息头的,private void parseRequestHeader(InputStream in) {},在这里面我们也输出一句话,什么呢,解析消息头,是吧,private void parseRequestHeader(InputStream in) { System.out.println("解析消息头!"); }

public HttpRequest(InputStream in) throws Exception {
	try {
		System.out.println("http构造方法!");
		//1解析请求行
		parseRequestLine(in);
		//2解析消息头
		parseRequestHeader(in);
		//3解析消息正文(略)
	} catch (Exception e) {
		throw e;
	}
}

private void parseRequestLine(InputStream in) {
	System.out.println("解析请求行!");
}	

private void parseRequestHeader(InputStream in) {
	System.out.println("解析消息头!");
}

  • 我把这个几个方法都写好了以后啊,然后呢,我们现在就来测试一下,比如说,我现在调一下WebServer,把这个程序启动起来,我们来访问一下,http://localhost:8088/index.html,页面会显示"无法访问此网站,localhost拒绝了我们的连接请求什么的,执行,来看控制台,这时你发现它什么啊,http构造方法,调了一下,请求行和消息头是不是都调了。

等待连接...
等待连接...
等待连接...
http构造方法!
解析请求行!
解析消息头!
http构造方法!
解析请求行!
解析消息头!

  • 有人会问怎么出来了两次啊,这个http构造方法,请求行和消息头,这是不是出来了两次,对么,这是一个很坑爹的事情,不同的浏览器不一样,有的浏览器一次,有的浏览器两次,为什么两次,实际上,我告诉你们,就是你们有见到浏览器有时候会有这种情况,就是浏览器页眉的这个地方,会有一个小logo,有时候会看到一个网站的logo,logo都知道吧,就是小图标啊,实际上它在访问一个网站的时候,它一般会请求两次,第一次就请求你需要的东西,第2次管你要那个logo,你把这图标给我,给你显示在这,没有就算了,就拉倒了,但是它会请求两次,明白吧,将来我们去看那个uri,你会发现它那个资源位置不一样,第一次是你真正想请求的这个页面,第2次你发现它就写一个杠logo点什么,png的图什么,它就是要你那个logo图啊,这个没有也无所谓了,但是你得知道,它这两次是为啥啊,在这给解释一下。

  • 不过没关系,至少发现,现在这个东西是不是调用到了,我们正常就应该去初始化那个请求了,是吧,该解析的一会咱都给它解析出来,都打个桩,看看能不能到这块,先到这个HttpRequest构造方法这,上面先输出一句话,就是System.out.println("http构造方法!");,调用了这个,然后在那两个解析请求行和消息头的方法里面都分别输出一下,System.out.println("解析请求行!");System.out.println("解析消息头!");,输出以后,先启动一下WebServer,用浏览器访问一下,看这3句话输出了吗,得有类似这3句的输出啊,输出了两遍,什么两三遍都没关系啊,就是得有,别什么访问过来以后,什么都没有,那说明可能写的有地方不对啊。

  • 有地方不对,怎么解决啊,为什么我这一输出什么都没有,先看看你的等待连接有没有,等待连接都没有,说明你服务器没启动,因为你正常启动起来以后,start方法一调,这句话是不是肯定就输出了,对吧,那这输出了,我下面没有怎么办啊,就逐行打印呗,是吧,排查,怎么排查啊,那你比如说等待连接有了,那这个执行完了以后,accept得到连接,Socket socket = server.accept();,这句话以后,咱们是不是可以写几句话,比如一个客户端连接了,看看这句话输出了没有,对吧。

    System.out.println(“等待连接…”);
    Socket socket = server.accept();
    System.out.println(“一个客户端连接了!”);

  • 你浏览器访问过,然后是否输出了这句话,一个客户端连接了,如果输出了,说明到System.out.println("一个客户端连接了!");,没有问题,然后你new那个ClientHandler的时候,你在ClientHandler handler = new ClientHandler(socket);,这句话的下面,是不是可以再输出一下,ClientHandler创建了,System.out.println("ClientHandler创建了");,这句话输出完了以后,再下一句,threadPool.execute(handler);的后面,我已经把ClientHandler交给线程池了,System.out.println("已经把ClientHandler交给线程池!");,明白么,都输出,然后你看它到哪句没走呗,都按行打印,哪怕说不想打那么多字,那你写1,2,3,4,你看走到几,不是也行么,明白么,都这么查错啊,那如果假设我这个地方threadPool.execute(handler);后面输出了,假设System.out.println("已经把ClientHandler交给线程池!");,我这个东西也输出了,假设啊,这句话如果也输出了的话,那说明threadPool.execute(handler);,这句话是不是走了,我们把这个任务是不是已经交给线程池了。

  • 然后再从哪输出去啊,线程池里输出去,我们线程池连看都看不见,再说线程池也不是你写的啊,但是我们知道,我只要把任务交给线程池,它是不是会分,只要有线程,一定会分配线程来运行我这个任务,一旦运行任务,我这个ClientHandler的run方法是不是就要执行了,那是不是就到了ClientHandler类中的run方法了,是不是这样的啊,在这run方法里面,在这句话InputStream in = socket.getInputStream();前面,也输出一下啊,输出完了以后,在HttpRequest request = new HttpRequest(in);,这句话前面也输出一下啊,输出完了,在这句代码后面再输出一下,你看你在这句话InputStream in = socket.getInputStream();,获取的这个in对不对啊,你socket也输出一下,看有没有null啊,明白么,这就是你调错的方式,我们刚开始学习,都这么调错,这个你得会啊,打桩必须得会,否则你将来,没人给你看代码,你怎么看呀,明白么,别懒啊,把这些事搞好,如果你这个都出来了,就可以开始写HttpRequest的这个第一个方法parseRequestLine了。

代码实现Webserver
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("服务端启动失败!");
		}
	}
}
代码实现ClientHandler
package core;

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

import http.HttpRequest;

/**
 * 该线程任务用于处理每个客户端的请求。
 * @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);
		} catch (Exception e) {
			e.printStackTrace();
		}			
	}
}
代码实现HttpRequest
package http;

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

public class HttpRequest {
	/*
	 * 请求行相关信息
	 */
	//请求方法
	private String method;
	//请求资源URI	 (统一资源定位)
	private String uri;
	//请求协议版本
	private String protocol;
	
	//消息报头信息
	private Map<String,String> header;
	
	public HttpRequest(InputStream in) throws Exception {
		try {
			System.out.println("http构造方法!");
			//1解析请求行
			parseRequestLine(in);
			//2解析消息头
			parseRequestHeader(in);
			//3解析消息正文(略)
		} catch (Exception e) {
			throw e;
		}
	}
	
	private void parseRequestLine(InputStream in) {
		System.out.println("解析请求行!");
	}	

	private void parseRequestHeader(InputStream in) {
		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==10&&c2==13) {//c1==LF&&c2==CR
				break;
			}
			builder.append((char)c1);
			c2 = c1;
		}
		return builder.toString().trim();
	}
}

HttpRequest之parseRequestLine()方法

  • 现在开始着手写HttpRequest类的这个第一个方法parseRequestLine。在这个方法里,把这3项解析出来了,请求方式method,资源路径uri和协议版本protocol。解析这个请求行的时候,你应该拿到是不是类似于这样的,比如说我在浏览器地址栏访问这个,http://localhost:8088/index.html,那你这个地方parseRequestLine方法里,取到的这个第一行,这就应该是GET /index.html HTTP/1.1, 其中GET,/index.htmlHTTP/1.1,应该是这3项,把GET设置到method那个属性上,把那个/index.html呢,设置到uri这个属性上,把那HTTP/1.1设置到protocol这个属性上,这不就是那个请求行的3个相关信息么,设置到这3项上,设置上以后,干件什么事啊,打个桩,把这3项给我输出一下,就你在这个parseRequestLine方法里,看你拿到的对不对,开始就干这件事。
  • 还有根据空格拆分这是\s,那个正则表达式的split方法,是不是根据正则表达式拆的么,我们之前说过,正则表达式里面,\s表示是一个空白字符,\d是一个数字,\w是个单词字符,那这写\s,实际上得写成\\s,就这个。那在parseRequestLine方法里面第一步干嘛呀,直接就调用readLine(in)String line = readLine(in);,把这输入流给进去,就这readLine读一行就行了,这个地方是不是就会抛异常,我们把这异常给它抛出去,当然了,捕获是没问题的么,你应该是自己先捕获,是吧,我们说了,自己先捕获的话,有一个好处是什么呀,就是你知道是在你这,出的问题,是吧,你将来在这可以写日志,然后,当然这个错误不归我们管,我们就接着往外,给它抛出去就行了,告诉外界,咱们这个解析请求行,这个失败了,那下边就都不用干了。

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);
	} catch (IOException e) {
		e.printStackTrace();
		throw e;
	}
}	

  • String line = readLine(in);,读完这一行以后,不要马上就开始按着空格拆,我的建议是什么呀,不着急,读到这一行以后,马上就先输出一下line,System.out.println(line);,先看看对不对,对的话,再往下写啊,那所以就说,我写完这句就不写了,然后呢,运行一下,控制台上,等待连接…,然后我们浏览器刷新一下,我们看看有没有,发现有了,GET /index.html HTTP/1.1,就是这一行,是这样的吧,这一行有了(把无关打桩注释掉),那既然这行有了,然后我是不是可以再接着往下写了,是吧。

等待连接...
一个客户端连接了!
等待连接...
一个客户端连接了!
等待连接...
http构造方法!
解析请求行!
requestLine: GET /index.html HTTP/1.1
解析消息头!
http构造方法!
解析请求行!
requestLine: 
解析消息头!

关于Chrome浏览器第二次空请求的处理
  • 那我这块输出的这个"http构造方法",一下连续跳了两次,这是怎么回事啊,因为你得知道,浏览器是不是给你过来了两个请求,但这两个请求是并发的,因为咱们现在不都是靠线程操作的么,并发的话,就无所谓什么问题,你看我这是构造方法,解析请求行,消息头,是不是这3个就都执行一遍,然后再来一个构造方法什么什么的,但这不一定啊,因为要是两个线程同时运行,怎么先后顺序执行,无所谓,清楚吧,所以这个不重要。
  • 但是我告诉你,还有一个恶心的问题,就是一解析,就是一拆分,用split一拆分,数组越界,是吧,这是为什么,这是浏览器另一个恶心的地方,它发一个连接,发给你,没理我,给你发一个空串,它连你了,它又连上你了,它不给你发HTTP请求,它相当就,恩,意思意思,连一下,其实它什么都没给你发,明白么,它就连上了,看你有没有反应啊(难道这个空串就是请求那个logo图标?),那我告诉你,所以你看,像第2次,我这是不是一输出,得到是空行,看到了么,我们在System.out.println(line);,打印输出这句话的时候,在里面标记了一下,标记什么字符都行,就证明我在这输出过东西,比如System.out.println("requestLine: "+line);,如果这么输出的话,就能清楚里看到这有一个空行,那就是在这块,数组越界的,这就是浏览器恶心的地方。
  • 而且我告诉你啊,将来你们学web端的时候,你会发现浏览器,不同浏览器,它连上,正常都应该给你发http请求,明白吧,但是不同浏览器,它在连完以后,你有没有反应什么的,它的处理机制,可不一样,这个其实也是很恶心的事,这就是为什么将来你们都会使Tomcat,基本不会自己写个WebServer,就是因为底下还要处理很多很恶心的事情,那在这,我们怎么办啊,如果你给我发一个空行过来,我是不是这个request对象,就不创建了,明白么,所以就是说,既然咱已经遇到这个问题了,所以首先,是不是先读到了第一行,String line = readLine(in);,对吧,请求行,我们读到了,然后呢,我们在这要做一件什么事情呢,是不是可以做一个必要的判断工作,对不对,判断一下这个line.length(),如果大于零,是不是我们才干下一件事,那也就是如果它要是等于0呢,就说明这个字符串里,啥也没有,是不是啊,啥也没有,干嘛呢,直接就return;

String line = readLine(in);
if(line.length()==0) {
	return;
}
System.out.println(line);

  • 或者你给它抛个异常,都行,告诉你,你这个没东西,明白吧,就是这次请求什么都没有,就相当于是一个空啊,这是一个空请求,那我们都可以这样做,那么总之,我们再有这么一个处理工作啊,如果是这样的话,我们就不再处理了,比如说在这呢,我们抛异常,我new一个RuntimeException("空的请求行!");,比如说空的请求行啊,咱们抛一个异常,就相当这一行,什么都没有,那是不是以下操作就都不干了啊,如果有的话呢,我们再开始,进行相应的解析工作。现在我告诉你啊,将来你使用不同的浏览器,这发一个请求过来以后,各种乱七八糟的东西都有,大家不用大惊小怪啊,这个不同浏览器,它们除了支持这个http标准协议之外,它竟给你玩一些这个,出其不意,知道吧,那各种不一样啊。

String line = readLine(in);
if(line.length()==0) {
	throw new RuntimeException("空的请求行!");
}
System.out.println(line);

  • 那么这个OK了,如果要是正常的情况下,咱们就可以干嘛呢,按照空格拆分了吧,是吧,怎么做啊,第2步,我们拿到一个数组是不是,这个数组等于什么呢,这个line.split("\\s");,这是按照什么拆啊,\\s,是不是按照空格拆分,String[] data = line.split("\\s");,是这样的吧,但其实呢,也建议大家,在这个验证之前,输出这个line,就是现在我读到这个第一行请求行以后,咱就把这个requestLine,我们每次都输出一下,看看将来你换不同浏览器,还会出现什么恶心的事情,咱们再完善我们这个判断逻辑,就这个刚开始,咱们都可以这样做啊。

String line = readLine(in);
System.out.println("requestLine:"+line);
if(line.length()==0) {...}

  • 现在我告诉你,这代码又可以细分了,其实,为什么又可以细分了,比如你看啊,在这parseRequestLine(InputStream in)方法里面,我是不是就分为3步走,那你看,你读完第一行以后,是不是要有一个必要的验证,但其实验证,对于咱们这个来讲,是不是只验证了一个空的行,if(line.length()==0){...} ,那么它可能还有别的验证,你是不是会写一堆,if-elseif-else,那你是不是把那个一堆if-else,可以又搁到一个方法里,它这就统一调这个方法,验证一下,明白么,这样的话,你这块代码是不是还要走流程,现在就大概3步,还没有开始做呢,还有一些细碎的东西,再调方法,再去干,清楚吧,你将来写代码,都这么做,但是咱们现在先就这一个验证了,if(line.length()==0),咱就忍了,就放这了,但是将来你发现你这有一堆的if,if,你就最好拿到一个方法里去,清楚了吧,你这个判断,一个方法里搞定,在这里就只看,这是判断工作,至于怎么判断的呢,那将来在方法里再去看啊。
  • 那好了,比如说咱这个,第一步拿到这个请求行以后,做一些必要的判断,第2步是不是就可以拆分了,String[] data = line.split("\\s");,拆分以后呢,是不是就可以进行相应的设置工作了,比如第一个,this.method = data[0];,是不是就设置到我们method这个属性上了,它等于什么啊,就是那data的第几项,第一项是吧,然后,this.uri这个是什么啊,data[1]this.uri = data[1];,data的第2项,然后呢,this.protocol,这个呢,就是我们第3项,this.protocol = data[2];,是这样的吧,OK,那这样的话,我们就解析请求行完毕。

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];
	this.uri = 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;
}

  • 然后在这你们是不是在这也,就是这3项干完以后,是不是打了个桩,再发点东西,对啊,那这个做好了以后呢,看一下啊,一执行,然后呢,走。

等待连接...
一个客户端连接了!
等待连接...
一个客户端连接了!
等待连接...
http构造方法!
解析请求行开始!
http构造方法!
解析请求行开始!
requestLine: GET /index.html HTTP/1.1
mothod: GET,uri: /index.html,protocol: HTTP/1.1
解析请求行结束!
解析消息头!
requestLine: 
java.lang.RuntimeException: 空的请求行!
	at http.HttpRequest.parseRequestLine(HttpRequest.java:55)
	at http.HttpRequest.<init>(HttpRequest.java:25)
	at core.ClientHandler.run(ClientHandler.java:27)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.lang.Thread.run(Unknown Source)

  • 这样发现,这三项是不就都拿到了,是这样的吧,那一会还会报错么?这是不是报了一个错,那个空请求行,是这样的吧,那空请求行,这个拿到了以后,实际上咱们是不是把它catch到了,catch到了以后,输出一下错误,等于说,那个请求就什么都没干啊,没关系,你要知道将来我们这一行就catch到以后,不用输出它,你就看不到这个错了,就给它pass了,忽略它就行了,都是我这样的吧,OK了啊。

HttpRequest之parseRequestHeader()方法

  • 那个这个写完了以后,也就是我们HttpRequest的构造方法中的第一步,就是这个解析请求行啊,parseRequestLine(in);,这件事干好了,然后,第2步,我们要做的是什么啊,解析消息头,是吧,那消息头怎么解析,找冒号呗,是吧,找冒号,然后呢,找到冒号,是不是把冒号前面的解析出来,把冒号后面的解析出来,是不是就行了,但是由于它是不是有好多行,在这个地方,解析消息头这个地方,是不是得循环干了,对吧,所以咱们来到这个parseRequestHeader,这个方法里面,我在这把那个思路写一写,解析消息头,因为其实请求行是不是就一行, 然后往下解析消息头,多少行是不是就不一定了,那是不是就可以循环解析啊,那消息头是这样的啊。

/*
 * 消息头由若干行组成
 * 每行格式:
 * name:valueCRLF
 * 当所有消息头全部发送过来后,浏览器会单独发送一个CRLF结束
 * 
 * 实现思路:
 * 1:死循环下面步骤
 * 2:读取一行字符串
 * 3:判断该字符串是否为空字符串,若是空字符串说明读到最后单独的CRLF
 * 	那么就可以停止循环,不用再解析消息头的信息
 * 4:若不是空串,则按照“:”截取出名字与对应的值并存入header这个Map中
 */

  • 消息头大致内容如下:

Host: localhost:8088CRLF
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
... ...
Accept-Encoding: gzip, deflate, brCRLF
Accept-Language: zh-CN,zh;q=0.9CRLF
CRLF

  • 想当于就这样是吧,当然咱这块简化了,那其实它现在有好几行,就相当于上面这样的结构,像这样的东西会发好几个,然后最后会单独再发一个什么呀,CRLF,就类似于上面这样,那你们想想,基于这个模式,那我们应该怎么办?是不是相当于我要循环干,读一行,先看看是不是空串,因为像这一行,你要调像readLine方法读,读完了以后,是不是就只剩下一个空串了,什么都没有了,那就说明,肯定是读到头了啊,就是这个消息头其实就读完了,否则的话你要是,正常你,读了一行东西以后呢,那是不是就应该按照,冒号拆开,是吧,拆开以后呢,左边一项,右边一项,就这么干啊。
  • 那首先,我们刚才看到,它那个给我们的格式,是不是就类似于刚才上面这样的一大堆,但其实,如果你们去发现的话,消息头它实际上,这个冒号后面,是不是都有一个空格的,这个没关系,这是消息头里面的规定,就是它那个格式里面的要求,就是冒号后面会有一个空格,那所以就说,我们其实在解析的时候,冒号后面截出来以后,trim一下,这个空格就没了,这都没关系啊,这都问题不大,然后,那我都给它解析出来,我直接输出不也能看到效果么,为什么非要放Map里,放Map里的目的是,比如说将来我根据消息头的这个名字,是不是能获取到后面对应的值,我要去用它的,你要是光输出来的话,程序怎么去用它啊,我现在想看哪一项的值是啥,哪一项的值是啥,我得需要它。
  • 消息头其实有很多信息的啊,记录的是你客户端的信息,给我发过来的信息很多东西,我将来要通过消息头要去得到,所以要把它都保留起来,所以我们才要放到Map里面去,因为你放到Map里面,就等于存上了么,将来我用的话,我根据Key是不是也可以取对应的value,你要光是输出到控制台,输出去一下,输出了,我再想得到,怎么办呀,就得不到了,就是这么个问题,那么在消息头这个地方,在解析之前,首先我们要先干嘛呀,先得实例化一个header,现在不就是一个空Map么,private Map<String,String> header;,是吧,先实例化出一个,这个Map啊,在这个HttpRequest类的parseRequestHeader方法里,首先第一点,header = new HashMap<String,String>();
  • 然后呢,我们是不是就可以循环干了,怎么循环干呀,while循环,比如说,while(true){},比如说这,我们先定义一个,String line = null;,那么,我们是不是开始循环的时候,我就干嘛呀,每一次读一行,是吧,比如说,我开始读第一行,比如说line,首先等于什么呀,这个read,是不是还是调那个readLine方法,line = readLine(in);,是这样的吧,调这个readLine方法,然后先读一行,读完一行以后呢,我们要做什么事情,那这个地方咱们还是把这个异常抛出去也没错啊,throw e;,不过咱们还是先捕获一下,目的是我们能感知到,将来我们可以自己去记日志,这样的,留这么一个,这么处理的一个手段啊。

try {
	System.out.println("解析消息头开始!");
	header = new HashMap<String,String>();
	String line = null;
	while(true){
		line = readLine(in);
	}
} catch (IOException e) {
	e.printStackTrace();
	throw e;
}

  • 那么咱们先读一行,读完一行以后,正常的话,就相当于这个吧,Host: localhost:8088CRLF,就读到这一行的内容,那当然,你说最后一行,你要读的话,那CRLF之前是不是什么都没有,这就是一个空字符串啊,那么为什么是空字符串啊,因为你看咱们调用的这个readLine方法,我们readLine方法,上来咱们怎么做的啊,重新分析一下啊,咱们之前写的readLine方法是不是先创建了一个StringBuilder,你默认情况下,创建一个StringBuilder,我之前说过,我说默认创建出一个StringBuilder,内部是一个空字符串,什么东西都没有的,是这样的吧,但是如果你要连续拼了一个CRLF,等于这个StringBuilder里是不是就是什么都没有,那不就是一个空串么,return builder.toString().trim();,那它也就是返回值,它返回这个builder.toString().trim();,它trim完就还是那个空串,什么都没有,是吧,咱们这个地方不会返回成null。
  • 所以就是说,咱们在读完这一行字符串的时候,我首先要,先做一个判断,判断的是什么呢,看看这个line,它的点length是不是等于0的,如果是零就说明,单独的你就读到了一个换行,那等于说这是不是就是一个空行,什么都没有,那我说空行是不是就意味着消息头所有的内容就都完了,因为正常消息头的话,每一个CRLF之前,都有一行内容,但是它最后一行还有一个单独的CRLF,比如说,Accept-Language: zh-CN,zh;q=0.9CRLF,这一行,完事以后,下面没了,它会再单独给你发一个CRLF,是吧,就是单独发一个空行,所以就是说,我单独如果读到一个空行,就标志着什么呢,消息头就解析完毕了,那在这我们干嘛呢,就break,是不是停下就可以了,对吧。

if(line.length()==0) {
	break;
}

  • 如果不是的话,如果它length不等于零的话,那是不是说明,我们读到的是一行内容了,然后呢,找冒号位置吧,怎么找冒号位置啊,int index = line.indexOf(":");,是吧,找到以后呢,name怎么截的,是不是String name = line.substring(0,index);,是不是就到index就行了,然后呢,value呢,是不是String value = line.substring(index+1);,是不是就index+1,一直到结尾就完了,然后呢,是这样的吧,当然注意这个地方,是不是可以String value = line.substring(index+1).trim();,trim一下,因为我们说这冒号,冒号后面有一个空格么,那么index+2,不就完了么,是不是也行,对吧,那咱就加一了,咱就按着它那个标准模式,就按着冒号截,然后咱把两边空白去了就行了,那么这个时候,这个两项是不是就都做好了,做好了以后呢,我是不是就往这header.put(name,value);,是吧,我就把它put进去,然后这个是name,后边是value,是不是这样,一项一项就都解析完,放里面去了。

int index = line.indexOf(":");
String name = line.substring(0,index);
String value = line.substring(index+1).trim();
header.put(name,value); 

  • 这个完事了以后,咱们这底下,写在try-catch外面,是不是就输出这个解析消息头,这个事完毕了,是吧,就把System.out.println("解析消息头!");,改为 System.out.println("解析消息头完毕!");,然后呢,在它之前,你是不是也可以先打个桩,这个就是输出一下消息头啊,System.out.println("header:"+header);,那它其实有很多项,当然你要愿意的话,遍历Map,是不是可以一个一个,Key:value对,一个一个遍历出来,也没问题,你用Entry遍历也行,用哪种自己去想办法,遍历key,遍历value,都可以啊,在这我就直接输出了,在这就打个桩,看看效果啊。

private void parseRequestHeader(InputStream in) throws IOException {
	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构造方法!
解析请求行开始!
requestLine: GET /index.html HTTP/1.1
http构造方法!
解析请求行开始!
mothod: GET,uri: /index.html,protocol: HTTP/1.1
解析请求行结束!
解析消息头开始!
header:{Cache-Control=max-age=0, Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9, Upgrade-Insecure-Requests=1, Connection=keep-alive, User-Agent=Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36, Sec-Fetch-Site=cross-site, Sec-Fetch-Dest=document, Host=localhost:8088, Accept-Encoding=gzip, deflate, br, Accept-Language=zh-CN,zh;q=0.9, Sec-Fetch-Mode=navigate}
解析消息头完毕!
requestLine: 
java.lang.RuntimeException: 空的请求行!
	at http.HttpRequest.parseRequestLine(HttpRequest.java:56)
	at http.HttpRequest.<init>(HttpRequest.java:26)
	at core.ClientHandler.run(ClientHandler.java:27)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.lang.Thread.run(Unknown Source)

  • 解析的这个,每一个是什么东西,其实是一对一对的,就都有了,是吧,Upgrade-Insecure-Requests=1Connection=keep-alive,这些就都有了,它第2次发过来一个空的请求行异常,这咱们知道,这是第2次发过来那个,是吧,那也就是说现在请求,我们正常可以接收了,那么再往下解析那个消息正文,这事呢,咱们就先不管了啊,就是请求行和消息头都没问题,也就是ClientHandler里的HttpRequest对象,我们是不是可以完整的创建出来,对吧,然后呢,我们下边就要做的事情是什么,在ClientHandler里创建完请求对象以后,HttpRequest request = new HttpRequest(in);,之后,比如咱们开始给用户进行响应工作了吧,是吧,比如说,用户不是给了我们一个要求,访问我们的index.html么,是吧,那咱现在就搞这么个页面出来。

模拟页面index.html

  • 然后,我们先把这个页面做好,然后咱们想办法把这个页面回给用户,争取在浏览器上,我们能够,啪,一访问这,看到一个,这个hello world!就算了,有点low,咱能出来一个简单的一个页面吧,比如咱做一个百度页面,是吧,那个没那么花哨啊,就简单写一个就行了啊,那么比如说,现在在那个WebServer这个项目的根目录下,我们新建一个这个目录啊,然后起个名,就是webapp,那暂时就都搁到这里面,我们在这里面,咱再新建一个文件,起个名,叫做index.html,那在这个文件里呢,写个页面啊,一个基本的几个html,当然不做那么绚丽,就是意思意思点到为止。
    在这里插入图片描述
  • html是超文本标记语言,说白了,就是网页,比如你在浏览器输入一个网页如,www.baidu.com,你看的是一个页面,实际上服务器给你发的是一堆代码,标签,html就是一些标签,它就是使用一些标签,来描述这个图形化界面的,也就说你这个图形化界面都是靠这些标签组成的,只不过它这些标签,都是有一些意义的,它这些标签的名字,不能随便乱写,不同于写xml,那个自己是不是随便乱写名,list,emp啊,什么都行,但这块每一个标签都有具体含义的,每个标签的名字,它会翻译成一个特定的东西,比如超链接那个标签叫什么,什么图片的标签叫什么,它都有专门的含义的,在这说几个基本的啊。
  • 首先在html这个里面呢,必须要包含的是这么几部分,首先根标签叫html,里面会包含两个子标签,一个叫做haed,一个叫做body,这两个部分是必须要有的,head,这个头信息里面呢,就是你这页面当中一些隐含的内容,就是我们将这个一些配置的信息什么的,都是在head里,包括什么js,这个脚本语言啊,那些东西,包括你的样式啊,很多包括你的界面的一些配置信息,都写在这上面啊,用于让浏览器理解的,这个是你看不见的,是你在浏览器页面看不见的,那说这个页面哪里来的,在body里面定义的,body就是你这页面上可以完整让用户看到的东西,都是在body里定义出来的,那么head的里就相当于我们自己去定义的一些属性,一些隐藏的信息,那些东西都是在这定义的,先在body这里定义一个叫meta的标签,然后呢,这里面写个charset标签,我们统一的一个字符集啊,UTF-8,都用同一个字符集了,这相当于就告诉你这该咱们写的这个页面,使用的字符集是什么。
  • 而且我告诉你,还告诉你一件事,html它不像xml规定的要求那么死,就是你这甚至,你这不写这个杠,<meta charset="UTF-8"/>写成这样<meta charset="UTF-8">,你光写一个前标签,可以,但是你要说,你要愿意的话,你也可以把它加上,就表示这个关闭标签,你把它加上,这些都没问题,但是你不加也没事啊,就是它不像xml要求那么死,xml它是不是要求必须成对出现,单独写一个的话,也给我加关闭标签,那个斜杠,你也得给我有,是吧,慢慢这个去理解这个东西啊,总之这个charset一项加上的话,它的作用就是用来指定告诉你浏览器,你在解析我这个页面的时候,上面我所有写的字符都是由utf-8编码的,如果你这不加的话,咱们发过去的,有可能是发gbk的,有可能发utf-8的,你就读吧,你浏览器一读出来是中文,就卡卡卡,一堆乱码啊,所以都统一好字符集,就这项<meta charset="UTF-8">,都给它加上。
  • 还有这是title标签,title就是什么啊,就是标题,就是这网站,这个页眉上,你看这个,就是那个logo,然后后面这块是不是写了一个,比如,百度一下,你就知道,是吧。
    在这里插入图片描述
  • 那这个是哪呢,就这个title里面的内容,就是title里面写的字,将来就在浏览器页眉上面出现,这咱页先写一个,比如说,我们写一个,百度一下,你就知道!,当然了你要是愿意写点什么个性的,可以自己写啊,无所谓了。这个写好了,其实咱们可以看一下了,然后在你写的那个html文件,鼠标右键,open看到没,其中有一项叫open with,就是用什么打开,就用在这个Web Browser,就是那Web浏览器,用那个打开看一下,那说怎么空白,啥也没有,谁说啥也没有,你看页眉那,百度一下,你就知道,至少这个title已经有了。
  • 然后接着写,开始在body里面写,那么在这个body里面可以开始敲东西了,在这个里面比如说有标签<h1>h1h6,这是标题,让这个标题标签里面的内容居中的话,还有一个标签可以用,就center,把标题标签写在center标签里面,就可以居中了,当然我们后面还有很多的标签可以用,比如说在html5出来了以后,其实好多标签被弃掉了,就不建议去用了,看这个center标签上是不是还给你出来了一个警告,那没关系,咱现在先不说那么复杂的,你看当我加一个center,center的意思就是将内容居中,当我加完了以后,就居中了,在h1里面,咱们写上,百度一下,你就知道!这时页面就出来百度一下,你就知道!这个几个字了,这Ok了啊。
  • 那比如说我现在想在下一行,做一个输入框,边上写个按钮,百度是吧,这时候怎么办啊,输入框的标签是这么写的,它这个标签叫做input,然后type,type决定了你这个东西到底长什么样子,type如果你写成text,那就是一个文本框,文本输入框啊,那么,你看当我写完这个以后,长什么样啊,说这个输入框怎么那么丑,怎么想给它搞的好看一点,后面会通过样式,就可以调各种花边啊,说那还丑,再丑那就是美工的事了,跟后端就没有关系了,咱们只要能,能看懂就行了,将来这个页面长什么样,不听我们的,听UI的,人家学艺术的,人家是搞那个美感的东西,只不过我们赋予它灵魂,这话是有道理的,他们只有外在,没有灵魂,明白么,我们赋予它灵魂,它才变得很智能,能给我们解决各种问题,否则就是个空页面,啥也干不了,是吧,所以我们是,不吹了啊。
  • 那这个输入框的长度其实也是可调的,比如说我们给它调一调长度啊,比如说input标签里面还可以加什么呢,size,它还有一些属性啊,比如说size,我写成30,那这个时候我们这一执行,你看是不是就长了,就是这里面最多,同时能显示30个字符,不是说,只能输入30个,就是能看到的是30个字符,你再输的话,它就哇哇哇,往里砌,就这个意思,那比如说啊,这是一个输入框出来了,是吧,我边上还想再来个按钮,怎么办啊,在输入框下面还是input,只不过这个input这块,type类型不一样,是吧,类型叫button,button注意,这就是个按钮,我们看一下效果,发现出现个按钮,还能点,关键是没字啊,那怎么办,加字很简单,再加一个属性,value=“百度一下”,是吧,再过来执行,这回挺像了,是吧。
  • 另外啊,这个两个input明明都换行了,那它怎么这两个input还显示在一行上啊,我告诉你们啊,如果你写的是这种标题,<h1>这种标题,你h1后边,不在h1里面的东西,都会自然再往下换一行啊,这是那规定,那你要想让两个input标签之间换行,你在input之外换行是没有用的,它都是认为这都是在一行上的,你要是想换行,我们专门有换行的标签,叫做<br>,如果你加一个br标签,我们注意,我们再来执行,你看这就换行了,明白吧,你要不加,它们是在一行上的,这个不重要啊,咱就先写,简单写个页面吧,把这个都搞成我这个样子啊。
代码实现index.html

<html>
	<head>
		<meta charset="UTF-8">
		<title>百度一下,你就知道!</title>
	</head>
	<body>
		<center>
			<h1>百度一下,你就知道!</h1>
			<input type="text" size="30"/>
			<input type="button" value="百度一下"/>
		</center>
	</body>
</html>

4.处理响应信息:HttpResponse

  • 那咱现在就想办法啊,现在这么写的话就不是很理想,就是我们这块直接打开个页面看,是吧,而真正我们在浏览器是想真正访问我们这个网站,能给我们在浏览器上面看,这个意思,那这就相当于我们真的是访问这个地址了,那么注意,那你要想给我们这个用户,给我们这个客户端回应,那大家注意,我们怎么做啊,首先,你要想给客户端回应,我们是不是相当于要通过ClientHandler客户端处理程序的socket,我们创建一输出流,我们之前说了,浏览器和我们连接了以后,它是不是创建了一个输出流,然后我们创建了一个输入流,咱们是不是还把这个输入流变成了有个request对象,是不是就把这个用户所有这个http请求的那个request信息,我们全能读到以后,顺便解析成一个request对象,将来你根据这个Request对象,是不是就可以获取用户想要干的那些事情,对吧。
  • 然后呢,我们要给用户回的话,怎么办,我们回什么,首先,我给你回,我们这块是不是要创建输出流,用户是不是就创建输入流,它这个输入流咱不用管了,这就是浏览器干的事了,是吧,那我们创建输出流,我们是不是就是给用户回它想要的东西,对吗,那注意,我们给用户回的时候,那么这个回的这些内容也得符合http协议要求,那么,回的这是什么呢,你就得遵循http的响应的格式,咱们之前这个发给你的是不是都是request请求格式,分别什么,请求行,消息头和消息正文是吧,同样响应也分外3部分,你要想给用户回的话,也得把这个格式写好啊,在这呢,我们先来把响应的格式呢,我们给它记一下,咱们之前是不是写了有个http.txt文件,咱们之前是不是写了一个,这个正文格式什么的,然后呢,在下面呢,咱们再空一行,说一说http响应的格式规格啊。

HTTP协议的响应格式规则

  • 这些东西不用背啊,这玩应是有文档可以查的,给大家写的这都是中文的,这是我自己弄的一些东西,你要是真正看,在哪看呢,http1.0和http1.1的那个两个协议标准,RFC1945这个东西是有中文版的,就是你可以去看这个http1.0里面的东西,当然1.1是最新的,咱们刚才看的默认都是1.1了,可惜的是1.1是纯英文的啊,你要是有能力可以看,其实在1.0上就是很多东西也都有了啊,包括它这告诉你说,这个http这个里面的很多这些信息,这里面都有,包括什么http消息,请求request和request分为什么方法,uri,这不就是咱们刚才解析的那个get,然后那个地址,什么什么的信息,这是不是都有给你写,然后响应,它这叫回应,响应分为什么啊,状态行,状态值即状态代码,可以找到具体内容,200表示啥意思,500表示啥意思,这有好多,等等,可以自己看一下。
  • 比如状态代码1xx,这个是保留的,将来会用,就是说现在还没用上,2xx就是以2开头的,就是说明这个事肯定是成功了,但是成功还细分成很多种,就是得到你这个请求后,我办了,但是我还分为几种不同的形式,正常2xx表示你的请求,它是得到了,而且能帮你处理了,然后3xx是重定向,重定向这个后边会说,什么叫重定向,然后呢,这个4xx就说明出错了,然后5xx就是服务器端这边出错;4xx是客户端你这边给的东西不对,你的语法不对,我这没法解释,5xx呢是说明,你那个请求对,但是我做不了,明白吧,就是说你看,服务器无法实现你这个合法的请求,就是你这个请求对,是没错,对的,但是我干不了,包括哪写报错了,或者怎么样。
  • 其实你们见过,你们上网时,有时一运行运行500,404也见到过吧,404就说明,我告诉你们啊,404,就是你给的东西不对,就相当于你给我访问的地方没有,我没这页面,500就相当于什么呀,我找到了,但是我这后边报错了,是吧,我给你发一个500,告诉你不对,明白了吧,其实它那个500,404,就都是这个状态码得到的,你要想细看它们的解释,在这个RFC文件里都有详细的解释,你要是特别对这玩应感兴趣,你可以过来看一下,其实在真正写那个WebServer的时候,状态码有非常多,那咱就别搞那么多了,是吧,咱就别那么搞那么费劲了,咱就200,就是你的正常请求来了,我能给你返回个200,错误的话,能给你返回个404,返回个500,咱就用这3个就行了。反正将来你写的这个Web服务器,将来你也不使,你使的都是Tomcat,Tomcat比如这些玩应就都有了,清楚了吧,但是咱们这个基本的,咱们有一个就行了。
  • 这个东西你愿意的话可以去参阅,这些咱们就不写了,然后这些响应头什么的,这个RFC上也有定义,回头一会,比如给你说一个,Content-Type,在这你可以搜一搜,它这就能找得到,告诉你这是内容类型,content-type是什么意思,它这说,比如常见的啊,text/html什么意思,你看关于具体的这个,什么叫介质类型啊,你这玩应就翻译的问题是吧,这就是你,相当于我回给用户,因为你得知道用户一个请求过来,它可不一定看页面,它是不是有时是看图,它要的是css的样式表,要一个你的js的什么的,这个脚本的这个文件,它这个要的东西不一样,那你这个content-type给的值就不一样,你总得告诉用户,我给你的那堆数据,它是什么玩应,因为我发给用户的都是字节,是吧,那我总得知道这些字节是什么东西吧,到底是字符串啊,还是一张表,或图片啊,它其实就通过content-type来回的。
  • 那这些东西是写在响应头里的,我会告诉你,我这个响应最后响应给你的信息,那注意啊,状态行,我只是告诉你,我的请求,你有没有正常接收到,我回给你一个正常的东西,至于我发给你什么,那我要在这个响应头里面写清楚,我发给你的是什么东西啊,然后你知道以后呢,你来接收这段具体的数据,我再把这个具体的数据,以响应正文的形式发给你,它会分为这么3部分,所以这部分状态行只是告诉你,告诉客户端,我有没有接收到这个请求,正不正确,那么它会先根据你这个状态值,比如200,ok,我给你的请求你是正常了,是吧,那它紧接着下面就会对第2部分,就是什么呀,响应头,响应头里面就会包含着什么呀,你那个响应当中的数据到底都是什么信息啊,它会里边有很多项啊,咱们描述清楚。
  • 然后呢,最终呢,开始根据你那响应头里面的信息的描述,它去读那个响应信息里边,就相当于真正的响应的那些数据,读过来,这才为真正我们响应的数据,就是在它的响应正文里,这些都是纯二进制字节的那些东西,当然那些二进制是什么意思,你先在响应头里面告诉人家清楚,那些东西是个啥玩应啊,就是类似这么个意思,那我们说这个状态行的格式,这个有了以后呢,然后就是响应头了啊,具体见http.txt,其实你发现,响应和请求差不多,都是由3部分组成,就是只不过我们请求是分为请求行,请求的这个消息头和消息正文,然后响应分为3部分,状态行,然后响应头和响应正文,状态行是告诉客户端,我们的这个服务端是不是有正常的响应,然后呢,如果是正常响应你的话,那你就可以看我这个响应头里面,告诉你我真正要发给你的信息,真正是什么什么东西,你知道了这些,然后你再去读取我正常回给你的那些数据。

HttpResponse代码分析

  • 咱们来写一个,比方说吧,请求咱是不是已经给它封装成一个对象了,而你看响应,我是不是又要给你发一大堆数据过去,那它其实也应该是不是包含一个对象,就是把我这个对象给设置成,我要回给你的信息,是不是都给搁到这个里面,是这样的吧,在这,咱们应该也去定义一个呢,对应着HttpRequest,是不是可以再定义一个响应的对象HttpResponse,然后呢,我们把这个输出流给进去,那就是用这个响应对象表示所有的响应,其实就跟HttpRequest道理是一样的,因为request是不是相当于从输入流里,读到客户端发给我们的么,是吧,那咱们创建一个响应的这个对象,用它来表示什么啊,就是我们服务器准备回给客户端的那一大段信息啊,咱们就全都放到一块回应了。
  • 那我们也是不写在这个ClientHandler里了,要是写在这的话,在这还得卡卡卡,写一大堆代码,然后咱再一层层的往往外搬,咱们就遵循这request原则,咱们在来搞一个response,就来写一写这些东西,所以我们在这个http包下面呢,我们再新建一个类叫HttpResponse,然后呢,还是先定义一个构造方法啊,public HttpResponse(OutputStream out) { this.out = out; },还有一个输出流属性,private OutputStream out;,然后,咱们还是把这个输出流保存一下,一会我们要用,这个输出流呢,给这个用户进行响应,然后注意啊,那在这个响应信息当中,我们是不是最少得分为3块,什么呀,状态行,然后呢,还有所有的响应头,以及响应正文,是这样的吧,分为这3部分啊,所以在这呢,我们先来定义这个状态行的信息,先得声明个状态行属性,private int status;
  • 那状态行,HTTP-Version Status-Code Reason-PhraseCRLF,这个也分为3部分信息,而且我告诉你啊,HTTP/1.1 200 OKCRLF,这里的状态码和OK状态描述,都是定义死的,就是像状态码有几种,能写几啊,参考RFC文档,可以发现状态码和状态描述都是定义死的,那么这些东西我们就不用都敲了,那将来我们就,比如说我输入200,那么我们就自动的就把后面的状态描述给它拼上,比如说我输入一个200,那后面就自动拼一个OK,我输入201,后面就自动拼这个Created,那这个东西就导致一个问题,既然都是标准的来,那我们是不是只要记住前面的,后面的东西不用记了,咱们就想办法让程序将来一个一个的对应上就行了,但是你要这全对应上,这得是个Map吧,这就相当于200是那个key,后面OK,是那个value,根据我输入一个200,你把后边的那个value取出来给我拼一起,作为状态行的那些信息,是吧。
  • 那这东西定义起来挺多啊,咱就先写死一两个,然后后面的这东西,再慢慢想办法,比如先就写一个200的吧,先把这个状态信息,先把它保存好,由于这个状态的信息,就是这个状态值和状态描述,是两部分,所以还是用一个Map,先给它存上,将来你设置哪个,我就给你把对应的,那个后面的解释也给你就拼好了,我们就不再这个一个一个去记了,所以咱们在这呢,定义一个Map,private Map<Integer,String> statusMap;。然后,先在就在构造方法里初始化这个Map,首先new一个HashMap,statusMap = new HashMap<Integer,String>();,往里面手动敲一个,这个东西咱们后来可以在配置文件里干,现在先敲一个,statusMap.put(200, "OK");,写死一个啊。

public HttpResponse(OutputStream out) {
	this.out = out;
	statusMap = new HashMap<Integer,String>();
	statusMap.put(200, "OK");
}

  • 将来还会有什么201啊,404啊,500啊,什么都可以有啊,那个是我们说状态代码以及所对应的描述,就放到一个Map里啊,对吧,那么比如说,我们一会在回应的时候呢,我们就直接让,就写的时候,我们就直接回应状态值就行了,我们就根据咱们这个写的这个状态值,比如200,我们来找到对应的OK,给它拼成一个状态行,所以呢,在这个地方,我们再保存一个信息啊,上面再加一个属性,就是什么呢,专门这个状态,我只保存这个状态代码,状态的描述,咱们根据状态代码为key,将来去取它对应的描述就行了,那比如说咱们有了这个状态代码,那么状态行至少我们可以给用户返回了,那咱们怎么返回呢。

ClientHandler之处理响应输出流

  • 咱们现在回到这个ClientHandler来,我们来看,之前我们刚开始处理这个用户的时候,我们首先是不是处理了这个请求,是吧,然后呢,咱们顺便呢,就把这个响应对象也给它创建出来啊,那我就创建个OutputStream,它等于socket.getOutputStream(),即OutputStream out = socket.getOutputStream();,我们用它来封装一个,这个响应对象啊,HttpResponse response = new HttpResponse(out);,比如说响应对象我们也创建好了,那也就是说,所有的用户将来回应是不是都是通过它,HttpResponse response,因为它不是把输出流给HttpResponse response了么,new HttpResponse(out);,将来是不是通过这个对象,给用户写那些状态行啊,这个响应正文啊,那个响应头和响应正文,是不是就发回去了,就相当于一次来回就过去了,那也就是说,所有写的那些一大堆的响应格式呢,全让HttpResponse response干就完了,我们在这ClientHandler里就不干了。

OutputStream out = socket.getOutputStream();
//创建对应的响应对象
HttpResponse response = new HttpResponse(out);

  • 但是在这ClientHandler里呢,我们下面要做的事情是什么呀,既然我要给用户响应,那我总得知道用户要干什么吧,对吧,现在请求对象有了,响应对象有了,那么实际上,咱先得通过请求对象,先得看用户想干嘛,是这道理吧,那也就是说,咱们这两个都创建好了以后,下面我们就来干嘛呢,开始处理那个用户请求,那么想处理用户请求的话,那注意啊,首先呢,咱们要做的事情是什么呢,用户一个请求过来以后,咱们之前知道了,它输出的是没有了,咱们是不是把它都封装成一个request对象了,request对象里面是不是能看到你想看啥,也就是说,现在最主要的就是,就是想要请求行里的资源路径,比如你想看我这个index.html这个页面,那咱们先拿到它要干什么,比如咱们先获取到这个路径,找到用户想看我什么东西,所以,咱们通过这个request,先拿到这个请求资源啊,在这HttpRequest里,看不见那些方法,我想获取那个uri,怎么办呀,没有。

HttpRequest之添加get方法

  • 但实际上,我们request对象里面有没有啊,有这些属性,只不过,我们发现一件事,它们都是private,外边看不到,对不对,那也就是说,我们要让外边想要获取这些信息,我们至少是不是得提供get方法,set方法不用提供了,因为set方法是我们解析完了以后,根据解析结果自己设置的,那个不需要提供,那我是不是可以提供相应的get方法,那回到HttpRequest当中来,那么现在呢,我们给它填上相应的get方法啊,我把它们都定义在最下面那,自动生成,鼠标右键选Sources,不要选select All啊,我们select Getters,我们只提供get方法啊,对么,你只需要获取那些信息吧,点上了么, 然后OK,把这几个都生成出来。

public String getMethod() {
	return method;
}
public String getUri() {
	return uri;
}
public String getProtocol() {
	return protocol;
}
public Map<String, String> getHeader() {
	return header;
}

ClientHandeler之获取请求信息

  • 那生成以后,咱们就回到ClientHandler这边来,这个时候我们request点就能得到了,String uri = request.getUri();,那么咱们做到这一步以后,那注意,是不是就首先通过用户给的这个路径,它其实,咱们现在知道,对于我们这块来讲,咱这个路径现在是什么啊,是不是那个/index.html,我们是不是得先访问那个页面,但其实这个页面对于我们来讲是不是就是个文件,我们是不是要把这个文件中的数据,就是这个文件中的所有数据读出来,读出来以后,通过网络是不是发给浏览器,浏览器是不是就能读到index.html文件这些东西,它就能把它展现成一个页面了,是吧,咱想办法把这些东西读回去,那我要想读回去的前提是什么啊,咱得先能读到这些数据吧,是不是才能把这些数据发回去,通过这个输出流,OutputStream out,相当于我们将来通过这个response,因为我们是不是把输出流生成到这个response对象里去了么,这样是不是能通过它response,把这个文件里的数据都发回过去,是这样的吧。
  • 那首先咱们要做的第一件事情是干嘛的呢,咱得先看看这个文件存不存在吧,你不能说你随便输个网页,我就能给你看见,你至少你那网页得,我得有,对不对,你访问这个资源得有啊,那注意,我们怎么判断有没有,那先不做这个优化了,咱这个代码开始又发现了,在这个run方法当中处理好request和response,处理用户请求,这块又开始写一大堆的逻辑了,其实不应该这么干,还是应该拆开的,那现在先不那么复杂了,咱先判断一下这个uri啊,拿到它,那我怎么看看它所访问的这个文件存不存在呢,我是不是先可以定义一个file,对吧,然后我把这个uri这个路径,传进去,然后传进去以后呢,我是不是想看看我这文件存不存在呢,我是不是可以判断,如果file.exists()存在,咱就可以输出什么,比如说找到了相应的资源,咱们先打个桩,否则的话呢,咱们输出没有资源,实际上没有资源的话呢,我们应该给用户回一个404吧,这先不管了,总之先看看,能不能把这个东西得到。

/*
 * 处理用户请求
 * 1.获取用户请求资源路径:/index.html
 */
String uri = request.getUri();
System.out.println(uri);
File file = new File(uri);
if(file.exists()) {
	System.out.println("找到了相应资源");
}else {
	System.out.println("没有资源:404");
}

  • 得到了以后,将来我们是不是通过response,想办法给你回就完了,我们现在就执行一下,看看效果,控制台上,它说是没有资源404。

等待连接...
... ...
解析消息头完毕!
/index.html
没有资源:404

  • 这是为什么呢,为什么没有找到呢,怎么解决这个问题,首先第一步,干嘛呢,得到uri以后,先输出一下吧,输出看看这uri是啥,对么,那么呢,打个桩啊,System.out.println("uri:"+uri);,这样一步一步的解决,看问题啊,执行,等待连接…,浏览器访问,再回到控制台,它说什么啊,uri这位置发现就是/index.html是吧,那我们就想一个问题啊,既然这是index.html,就相当于我这new的时候,File file = new File(uri);,这地方写的是什么呀,就是File file = new File("/index.html");,是不是就类似于这个意思,对么,那我们想想啊,杠,我们说了如果你刚开始写这个杠,这是不是当前目录,这不就相当于是/index.html,对吧,那当前目录是哪啊,我们说过在咱们的项目当中,当前目录是不是我们的根目录,对么,那如果这么去看的话,我项目根目录下有index.html么,确实没有,我们在哪呢,是在webapp这里头呢,是这样的,所以既然咱们所有页面都放在这个webapp里面。
  • 那也就是说,我们应该把这个根定义到这个webapp里,所以说我们前面是不是可以写死一个叫做什么,这个比如刚开始我们前面写一个,"webapp"加/,是不是,你不用加杠了,因为uri里头是不是含了个杠,那也就是说我这么写是不是应该就可以了,File file = new File("webapp"+uri);,其实说,能理解是,大概问题是不是出在这,是这道理吧,那如果我们拼这uri的话,你看如果我们拼的话,我们也拼成这个样子,我们把这也拼到这个输出uri的那个打桩上来,System.out.println("uri:webapp"+uri);,这时我们来看看对不对了啊。

String uri = request.getUri();
System.out.println("uri:webapp"+uri);
File file = new File("webapp"+uri); 
if(file.exists()) {
	System.out.println("找到了相应资源");
}else {
	System.out.println("没有资源:404");
}

  • 那么我们再来啊,执行啊,试试,走,太好了,这回是不是告诉我找到对应资源了,是不是知道这个页面找到了。

等待连接...
... ...
解析消息头完毕!
uri:webapp/index.html
找到了相应资源

ClientHandler之设置响应信息

  • 那我们只要想办法,把这个页面里的数据读出来,回给客户端就完了,是这道理吧,OK啊,这块没有问题,找到资源了,换一个呢,就告诉你没这个资源了吧,实际上咱现在把这响应对象简单的封装起来了,但其实响应对象,我们还没干呢,是吧,正常,咱还得给用户回应呢,就只是先做到这,请求来了,封装好了,拿到uri,看下有没有,有的话,再想办法,把整个响应的东西回过去,上来我要给用户发什么东西,实际上,当我们找到了资源以后,咱们是不是要把这个页面回过去,比如说响应页面啊,首先咱们要第一行包含什么呀,3项肯定有啊,就是响应里面的那3项都得有,什么呀,状态行,响应头,还有什么呀,响应正文都得有,是吧,咱们比如说要回的时候,咱们呢,将来要回复这3行啊,第一行是什么啊,HTTP/1.1,然后呢 200 OKCRLF,这是第一行要回过去的。
  • 第二行要回什么啊,响应头,响应头里面应该要包含这么几项,你得告诉用户,它要的这个资源是个什么东西,因为它是不是给你写了个index.html,那么,在这你得告诉它,在我这index.html是一个什么类型的资源啊,是一个页面啊,还是一张图片啊,还是个什么什么东西啊,明白么,那我怎么告诉用户,我回给你这个响应,这个数据到底是什么类型的数据,那我告诉大家响应头里面通过这几个区分的,第一个,这个是咱们在这个回应的时候,基本都要有的就是content-type,它就是告诉你,我回给你的这个资源是什么类型的,那说白了,后面这写的是什么啊,这个在http协议当中都有规定,如果是个页面,就是html页面,它的格式就写成,text/html,那要是张图的话,这就是image/,然后后面是什么png的啊,jpg的啊,那一大堆那个图啊,它都会给你回不同的格式,但总之如果我们回页面的话,你要告诉客户端,我回给你的这个资源的类型是一个html页面,清楚吧,你光告诉它html页面不够,为什么呀,因为我最终是要把这些html内容都要回给你,都要发给你,通过字节发给你,对吧。
  • 那这个时候就带来一个问题,客户端要读多少个字节啊,你这文件多大啊,不同的页面是不是文件长度不一样的,那我用户读多少个字节啊,我才算是这个文件读完了啊,我才完全接收到了啊,你让用户判断等于-1,那不是这样的,我告诉大家,实际上,在响应当中,我们是这样做的,告诉你这个玩应还会有一个常见的,叫做Content-Length,它的后面就是指定的什么呢,我这个文件多大,比如说咱这个文件多大啊,我输出一下吧,这不找到资源了么,那咱输出一下这个长度啊,恩,file.length(),即,System.out.println("找到了相应资源"+file.length());,我看一眼啊,控制台输出出来的是273个字节,是吧,那也就以为着,我这个地方还要写一个Content-Length: 273CRLF,清楚吧,那注意,那这每一项后边其实还都要有什么啊,CRLF,但这两个有了以后,至少客户端知道,我回给你的是一个页面,然后告诉你这个页面总共有273个字节,当然你可能还会再给用户回一些其他的消息头,就是这个请求头的信息啊,这个咱们就不说了,最主要的就这两项,清楚吧。
  • 那么然后呢,下面是什么啊,再单独发一个CRLF,再往后是什么东西,就是消息正文了,消息正文是什么呀,你就开始对那个html文件,把这个文件里面的每一个字节,它都跟你换成二进制了,明白吗,那这二进制多少字节啊,总共273个,就相当于把这个文件里的每一个字节按顺序发给客户端了,发完就完了,那么客户端开始堆的时候,它也是先读一行(状态行)发现,哦,没问题,然后读到Content-Type:text/html,知道这是有个页面,然后再读273个字节,哦,知道了,你后边要给我发273个字节,然后当它读完单独的CRLF,这一个CRLF,单独写完以后,就认为好,响应头的信息完了,然后我要对响应正文了,响应正文多大,由Content-Length决定,明白么,273个字节,那你就会发现,它就会创建一个273个字节的字节数组,卡,一口气读回来了,明白么,字节数组读完了以后呢,它就把它转换成字符,因为你告诉它,这个是什么呀,一个页面,它就把它转换成字符,然后开始理解里面html代码,在页面上,卡,给你显示出来,百度一下,输入框,按钮,完了,就出来了,清楚了么。
  • 那比如说,将来用户,咱们这块给用户的不是一个页面,比如说是一个logo.jpg,那比如说我发这个东西怎么办啊,那我们注意,那我们来了以后,Content-Type: text/html,这个东西,你还能回text/html,不能,要回成Content-Type: image/jpgCRLF,明白吧,告诉你,这是一张图,然后它是jpg格式的,然后呢,这张图多大,Content-Length这写多少个字节,然后呢,我再把文件里面的那些字节发给你,那它读到以后,那它首先200,OK,找到这个资源了,发现这是一张图,OK,读这么多字节,然后把这些字节理解成一张图片,给你显示在上面,它就出一张图,明白了吗,这个就是响应里头的标准格式。
  • 但是你们要考虑这么几个问题,用户这是不是访问过来的是一个index.html,那我们知道,这个资源是一个html页面吧,那我们发现Content-Type是不是就不一样,如果是html的,那我是不是回成text/html,如果发现是一张图片,我是不是应该给他回image/jpg,或这image/png,那么Content-Type就是由你发过来请求当中,资源的后缀决定,后缀是什么,我这是不是给你拼什么,那也相当于咱们将来是不是要把后缀那个png,jpg,html是不是可以作为key,后面可以对应的就是text/htmlimage/jpg这些玩应,你是不是也得搞一套,因为要是不同的,我是不是这Content-Type要给你拼不同的东西,对吗,这是一个,然后呢,我们还得拼这个Content-Length,这个length好拼吧,因为咱这个文件是不是已经找到了,我只要把这个文件这个length输出出来,搁到这就完了。
  • 然后响应实体这个难么,这也不难,为什么啊,咱们打开一个文件流呗,打开一个文件流,把每一个字节读出来,放到response里头,咱相当于这个输出流OutputStream out = socket.getOutputStream();,它就跟复制一样,我从文件流中把每个字节读出来,通过socket获取这个输出流,写出去,这不就相当于复制么,读一个字节写一个,读一个字节写一个,只不过这回写的,不是写在另一个文件里,而是写给对方的计算机了,它就都收到了,明白么,这就是大致的工作,这个工作干完了,咱们WebServer的基本工作就干完了,大概的过程是这样的。那么,先告诉你们格式,就是这样的一个格式,对吧,你可以先不用这个response,反正这个输出流不是有了吗,你能不能用输出流,自己把这些东西写出去,是吧,那你看看能不能做到,做不到的话,就算了,做不到就先理解理解这是什么意思,先把这段注释写下来,这个是我们下面要给用户响应的内容就是这个,我这101011100...,告诉你表示这是文件数据了啊。

/*
 * 响应页面要向用户发送的内容:
 * HTTP/1.1 200 OKCRLF
 * Content-Type:text/htmlCRLF
 * Content-Type:273CRLF
 * CRLF
 * 1010100101001011010101010111110010010(index.html数据)
 * 移到HttpResponse中flush()方法内...
*/

  • 处理完请求信息,找到了请求资源,这些都没问题,那就应该处理想应信息了。 那这些响应信息的处理是不是都应该放在HttpResponse里啊,这个ClientHander只是做一个数据的转移工作,把请求对应的处理结果数据传到HttpRequest里,实际上就调一个方法即可,但现在我们只处理一个200的响应状态,先写在ClientHandler看看效果,然后没问题的话,再去调整把代码放到HttpResponse里干。首先我们在ClientHandler里面,把要向用户发送的响应信息需要干的几件事写一下。第一件事是设置状态行信息,第二件事是设置响应头信息,第三件事是设置响应正文,这是响应包含的3个部分,最后第四件事,响应客户端,即把响应信息的3个部分全部发送给客户端浏览器。

//1 设置状态行信息
response.setStatus(200);
//2 设置响应头信息
response.setContentType("text/html");
response.setContentLength((int)file.length());
//3 设置响应正文
response.setEntity(file);
//4 响应客户端
response.flush();

HttpResponse之处理响应信息

  • 我们现在只定义了方法,现在这些方法没有,都报错了,这些事情实际我们都是应该放到HttpResponse里干的,但我们先在ClientHandler里实现,把需要调用的HttpResponse里的几个方法写出来,这些方法都是在HttpResponse里实现的,暂时先写在ClientHandler,看看一会的实现效果。其中设置响应头信息,我们这里只设置ContentType和ContentLength,这两个就足够我们实现HTTP协议基本的响应功能呢。
  • 那么现在,我们就回到HttpResponse里,将这些方法实现出来。在HttpResponse里,我们之前已经定义好了属性状态代码,private int status;,现在我们还有对应ClientHandler响应信息写的那几个方法,再定义3个属性,分别是响应头ContentType,响应头ContentLength和响应实体文件entity。

//状态代码
private int status;
//响应头-ContentType
private String contentType;
//响应头-ContentLength
private int contentLength = -1;
//响应实体-文件
private File  entity;

  • 然后在HttpResponse的构造方法后面,为这4个属性生成相应的get/set方法,通过对请求信息的解析为响应信息,通过set方法为响应信息设置相应的数据,并可以通过get方法查看到这些数据设置的是否正确。

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;
}

  • 这时会发现,在ClientHandler里还有一个方法,将响应信息发给客户端,response.flush();,这个方法依然报错,下面我们在HttpResponse里把这个方法也实现了,这个flush方法的作用就是发送响应信息,先打个桩,System.out.println("发送响应信息");,发送响应信息,这个方法里可以分3部分干,第一部分发送状态行,第二步发送响应头,第3部分发送响应正文,结构先写好。

public void flush() {
	/*
	 * 响应页面要向用户发送的内容:
	 * HTTP/1.1 200 OKCRLF
	 * Content-Type:text/htmlCRLF
	 * Content-Type:273CRLF
	 * CRLF
	 * 1010100101001011010101010111110010010(index.html数据)
	 */
	//1发送状态行
	//2发送响应头
	//3响应正文
}

  • 状态行,根据http协议的规定,我们这样拼,String line = "HTTP/1.1"+status+" "+statusMap.get(status);,输出一下,System.out.println(line);,到这我们需要把状态行line写入输出流,但这里需要注意的,毕竟计算机起源于美国,所以浏览器的标准编解码都是英文的,是以ISO8859-1为标准的,默认不支持中文,我们写的代码都是用的是utf-8,但utf8和iso8859-1在英文部分是一样的,所以我们将状态行line写入输出流的时候,需要先将其按照iso8859-1进行编码,到时浏览器端再自动以iso8859-1解码,保持编解码一致(其实即便是用UTF-8或者GBK进行编码,浏览器也能识别,可能它们的英文大部分都一样吧,谁知道呢?)。
  • 所以我们这编码为ISO8859-1out.write(line.getBytes("ISO8859-1"));,这里声明报了这两个异常,throws UnsupportedEncodingException, IOException,而UnsupportedEncodingException extends IOException,所以,我们还是先try-catch,统一catch(IOException e),然后给句提示System.out.println("响应客户端失败!");,再把异常抛出去,throw e;。最后我们还有一个CRLF需要写入输出流,那这个也说过,CR就是数字13,LF就是数字10,所以,out.write(13);out.write(10);,分别将它们也写入输出流。

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发送状态行
		String line = "HTTP/1.1"+status+" "+statusMap.get(status);
		System.out.println(line);
		out.write(line.getBytes("ISO8859-1"));
		out.write(13);//CR
		out.write(10);//LF	
		//2发送响应头
		//3响应正文
	} catch (IOException e) {
		System.out.println("响应客户端失败!");
		e.printStackTrace();
		throw e;
	}	
}

  • 接下来,我们再发送响应头,首先,根据响应头的格式,发送的是Content-Type。

line= "Content-Type:"+contentType;
System.out.println(line);
out.write(line.getBytes("ISO8859-1"));
out.write(13);//CR
out.write(10);//LF	

  • 这时我们发现最后写入输出流的3句话重复了,是吧,而且后面还有,Content-Length,还有响应实体,都要用到这3句,那这时,我们就应该将它封装成一个方法了吧,以便于代码复用。 所以我们在flush方法下面再定义一个方法println方法,将需要写入的数据作为这个方法的参数传入进去,然后进行try-catch捕获下,将异常抛出去。然后通过println(line);来代替这3句话,之后在响应头里还有一个单独的CRLF,表示响应头信息的结束,我们也写进去println("");。最后发送响应实体,是一个文件,对文件的读写我们需要用到文件流,增加读写效率,我们在文流外面再套个缓存流,见文件写入socket的输出流里。

/**
 * 将响应信息发送给客户端
 * @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发送状态行
		String line = "HTTP/1.1"+status+" "+statusMap.get(status);
		System.out.println(line);
		println(line);
		//2发送响应头
		line= "Content-Type:"+contentType;
		System.out.println(line);
		println(line);
		line = "Content-Length:"+contentLength;
		System.out.println(line);
		println(line);
		//单独发送CRLF表示响应头信息完毕
		println("");
		
		//3响应正文
		/*
		 * 将entity文件中所有字节发送给客户端
		 */
		BufferedInputStream 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) {
		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(13);//CR
		out.write(10);//LF	
	} catch (IOException e) {
		e.printStackTrace();
		throw e;
	}
}

  • 编译器提示了,在对响应实体写入文件时的输入流bis没有被关闭,我们一会处理,但现在会发现,这里是不是有很多代码了,现在,我们需要将代码查分出去,把状态行,响应头和响应实体分别拆分到一个方法里,所以我们再在下面写3个方法,responseStatusLine()responseHeader()responseEntity(),其实在设置响应头时,我们这里需要做下验证,因为如果我们set进来的contentType是空的,或者contentLength等于0,就没有必要再对它们进行写入socket的输出流操作了,那我们在这就做个判断,我们先将String line = null;,如果contentType不等于null,对其赋值,如果contentLength大于0,对其赋值,最后在写入响应实体时,因为缓冲输入流,是无论如何都要关闭的,所以放到finally中关闭。
HttpResponse代码实现

/**
 * 将响应信息发送给客户端
 * @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(13);//CR
		out.write(10);//LF	
	} catch (IOException e) {
		e.printStackTrace();
		throw e;
	}
}

/**
 * 响应状态行
 * @throws IOException 
 */
private void responseStatusLine() throws IOException {
	try {
		 String line = "HTTP/1.1"+status+" "+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;
			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();
			}
		}
	}
}

代码重构之HttpContext.java和web.xml

  • 这时我们WebServer最基本的,已经可以成功响应一个客户端的请求了,不过,现在我们可以发现代码中有很多的常量,比如CR(13),LF(10),比如状态码200,状态描述OK,这些数字啊,单词啊,并不直观,记忆和阅读起来都很不理想;而且状态码和状态描述也不止一个200和OK,还有很多很多,我们最后给它们放到一个集合里保存,然后根据实际的请求返回响应的状态。所以我们把这些常量和一些特殊意义的变量啊,专门定义到了一个类里,这个类供整个项目使用,我们叫做HttpContext。还有客户端请求时的访问的请求资源,我们是根据资源的后缀去判断的资源类型,在http协议里,每个资源类型都对应的一个特定的规定好的书写格式,以供客户端和服务器之间的交互和识别,比如index.html后缀为html,对应的固定格式为text/html,我们也把它放到集合里保存,并且为了方便以后对新格式的补充,我们将其定义在配置文件中,这个配置文件我们叫web.xml,然后在这个HttpContext中读取,并存到集合中。
  • 那我们在项目下,src/main/java这个目录下,再新建一个包,叫common,然后在这个包下创建一个类,HttpContext;然后我们在WebServer这个项目目录下,再创建一个目录,目录名叫conf,里面创建这个配置文件web.xml。在配置文件web.xml中我们定义相应的访问资源类型的映射数据,Content-Type Mapping;在HttpContext类中,我们定义一些项目中公共的常量信息(CRLF),一些特殊的变量,响应的状态信息及其映射,并读取资源类型并存入相应的集合当中,因为类似响应状态信息的映射,以及资源类型信息的映射,都是要供整个项目中随时取用的,所以应该提前将封装好的这些信息的集合初始化,所以将它们放到static静态块中执行,然后我们要做的就是把项目中所有在HttpContext配置好的数据都替换掉。
  • 我们先来到ClientHanler这个类中,在run方法中,我们已经写了好多代码,为了让代码结构更加清晰,我们把设置响应信息这几句代码封装到一个方法里ResponseFile,private void responseFile(int status, File file,HttpResponse response){... },并将响应状态码,要响应的资源和响应对象,当作参数分别传入方法里,再者,响应头的资源的类型映射已经在HttpContext中进行了初始化,我们再写一个资源类型的解析类,getContentTypeByFile,根据给定的文件分析其名字后缀以获取对应的ContentType,并在ResponseFile中调用,利用ResonseFile的参数File,作为getContentByFile的参数进行解析。同时将所有代码中在HttpContext类中声明过的常量或变量进行替换。因为在HttpContext中解析配置文件web.xml时,需要用到dom4j,所以也要在pom.xml中导入dom4j依赖包。

5.WebServer服务器代码实现第一版及相关解析

1.WebServer/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>

2.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>

3.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);
	}*/
}

4.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;
				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();
				}
			}
		}
	}
}

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;
	
	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];
			this.uri = 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;
		}
	}	

	/**
	 * 解析消息头
	 * @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;
	}
}

6.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;

/**
 * 该线程任务用于处理每个客户端的请求。
 * @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);
			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);
			}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;
		}
	}
}

7.WebServer/src/main/java/core/WebServer:

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("服务端启动失败!");
		}
	}
}

8.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="百度一下"/>
		</center>
	</body>
</html>

9.WebServer/webapp/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>

10.WebServer/webapp/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>

11.关于以上代码做一些解释

Content-type为什么写在web.xml中
  • 为什么要把Content-type写到web.xml中解析一下。因为客户端将来给这个想要的东西,服务端给它回的时候,不同的资源是不一样的。比如,有些人想拷贝下一个网页的页面,另存为,然后存的这个文档看不见相应的内容。因为在文件源码中,除了这些html之外,它还要加载很多东西,比如说,图片,css样式,js脚本,那么实际上你这个页面,html页面请求回去以后,它会发现这个页面上还要求有那些图片,css样式,js脚本等东西,它会再发请求过来,管我们要这些东西,这个时候你要的那些资源,我们都没有,这都会有问题,或者有,但是都是html代码,这也不对,因为图片就不是html,这就是为什么咱们要先把Content-Type给解决了,就是将来客户端访问我们不同资源的时候,会根据你名字的后缀,然后对应的拿到不同的值。
HttpContext之利用dom4j解析xml文件
  • 那么我们在web.xml文件中写一些基本的文件类型和后缀, 那我们就需要读这些文件了,在读之前,我们需要先把dom4j的jar包导进来,dom4j dom4j/1.6.1[jar],在HttpContext类种中始去读这些文件,见HttpContext类。如果访问的Maven服务器有索引,我们就可以直接点击项目下的pom.xml/Dependencies/Add,在这个里面搜索dom4j,选择dom4j dom4j/1.6.1 [jar],这样我们就可以使用这个包的内容了。然后我们在HttpContext类中的initContentTypeMapping()中进行初始化ContentType的工作,这部分工作要在try-catch中完成(先注释掉,一会在main方法中测试后,再解注)。从先创建SAXReader,到两个属性放到contentTypeMapping里之后,这些搞定完了以后,我们可以在main方法中做一个测试,可以输出一下contentTypeMapping。我们运行一下,先看对不对。发现输出了,初始化ContentType{css=text/css, gif=image/gif, xml=text/xml, png=image/png, jpeg=image/jpeg, js=text/javascript, html=text/html}
通过HttpContext重构ClientHandler中的参数
  • 没有问题之后,现在就不用这个main方法调用测试了,先注释掉,之后把静态块中写的initContentTypeMapping();,解注(解开注释),用这个去调initContentTypeMapping()这个方法就好了。那这个就是咱们之前说的那个状态码和描述信息要放到的ContentTypeMapping,然后,之前上面还有一个状态码-状态描述对应的Map,而状态码,其实上面定义的那些状态信息都是死的,所以咱们就把它,自己一个一个初始化就好了,这里面没有什么动态信息,也可以看到,我们都把它定义为final常量了,我们把它构造到Map里面去,我们将来用的时候就不用一次写两个,就根据Map中的key去获取它的对应的值就好了。Map<String,String>,这个东西其实就是咱们之前在HttpResponse的构造方法里写的那个200和那个OK,只不过我们把它放到这个地方来了。
  • 然后再写一个initStatus()方法,然后再在static静态块中调用这个initStatus(),初始化状态码-状态描述。initStatus()方法中的内容见源代码HttpContext,然后这个OK以后,咱们就可以去使用这个HttpContext类的内容了。现在回到这个ClientHandler里,但我们在浏览器输入http//:localhost:8088/index.html时,我们把这个网址拿过来以后,启动一个线程,就是这个ClientHandler,然后把从socket中得到的输入流in,把这个输入流in变成一个request对象,把这个请求的所有内容都设置到这个对象里面,设置完以后,拿到请求行的uri,String uri = request.getUri();,取到这个值,/index.html,取到它以后,我们就创建成了一个文件,File file = new File("webapp"+uri);,创建以后,我们就响应,把这个文件给用户,设置状态行信息,response.setStatus(200);,注意的是这里的200也不写成200了,因我们刚在HttpContext类中规定了这个值了,所以这个地方我们换成response.setStatus(HttpContext.STATUS_CODE_OK);
  • 那设置响应头呢,response.setContentType("text/html");,这个地方,是不是就不应该写死成这text/html,那你得看用户访问的文件(如这里是/index.html),是什么后缀,那注意了,这个地方,我们首先拿到uri以后,如/index.html,咱们得先截取它的后缀,如.html,然后想办法根据这个后缀去HttpContext中,根据对web.xml文件解析出来的Map,把这个后缀作为key,就可以获取它对应的那个value了。这时,我们了拿到uri中的后缀,我们又得写一套逻辑,去解析出这个后缀来,所以在设置响应头ContentType之前,我们要先分析,用户获取资源的那个后缀,分析该文件后缀,根据后缀获取对应的ContentType。
  • 那我们现在就根据这个文件名,在ClientHandler类的run方法下面,再单独写一个方法,private String getContentTypeByFile(File file){},这里面的文件file是谁啊,咱们在run方法中,已经new出了,File file = new File("webapp"+uri);,已经可以表示这个文件了,那么我们也能拿到这个文件的名字,getName(),拿到名字以后,就想办法获取后缀,具体代码见ClientHandler类getContentTypeByFile(File file)方法内。接下来,我们就可以在设置消息头的时候,通过调用getContentTypeByFile(file),这个方法得到contentType,再将contentType(即getContentTypeByFile(file))作为参数,response.setContentType(getContentTypeByFile(file));,这样的话,text/html这个值,原来咱们写死的,现在就不用写死了。
通过HttpContext重构HttpResponse中的参数
  • 另外,在设置状态行的时候,在HttpResponse构造方法里,之前的存储状态行状态码的Map,是不是直接写在咱们这个HttpResponse构造方法里了,我是不是把它摘出去,放到这个HttpContext类里面去了吧,我们在HttpContext类里面把那些状态码以及描述都定义成HttpContext的属性了,而且,我们的Map也在HttpContext类里初始化了,是这样的吧,也就是说我们用户从这个HttpContext的Map中去取状态码及描述就行了,也就是说,我们在HttpResponse里的那个存储状态码及其描述的Map就没有用了,这个HttpResponse里定义的Map属性,是当时咱们临时要用的,当时,在初始化的时候,这个Map里就搞了一个200的状态码,其他的如404什么的都没有放进去。
  • 所以说在HttpResponse构造方法里面的那个Map与put,我们已经不再需要了,以及它上面定义的Map statusMap这个属性都可以删掉了,干掉以后,我们在HttpResponse类的下面responseStatusLine(),这个方法中,有一个问题就是,你在响应这个状态值的时候,也就是这个状态代码,String line = "HTTP/1.1"+status+" "+statusMap.get(status);,这个地方我们就不应该这么去写了,而用的是HttpContext类里面的Map,即String line = "HTTP/1.1"+status+" "+HttpContext.statusMap.get(status);,就是用它的.getMap方法就可以了,这样的话,就相当于把我们之前自己写的那个Map,在这个里面就不要了。
在HTML中加载图片的问题
  • 重启一下服务器,执行,就会看到控制台输出contentType:text/html,这个打桩语句。此时我们的web服务器就也可以解析图片了,之前我们写了一个index.html网页,我把上面的<h1>标题,换成一张图,在这个,我把我的图片跟我这个index.html,放到同一个目录下,怎么换,要用一个新的标签<img alt="百度" src="/logo.png">,这个标签里面有两个属性,alt和src,alt属性是当你鼠标放到图片上,图片会弹出一个小气泡,显示一些内容(这个属性不明原因无效,暂不讨论)。src就是告诉你我这张图片的路径。我们在index.html,写好这个图片标签,再请求index.html的时候,可以在控制台看到了uri:webapp/logo.png,你看它是不是管我要这张图片了。但是图片并没有显示出来,这个解析一下。
  • 实际是这样的,因为浏览器发送一个请求,要index.html这个页面,服务器就把这个页面回过去了,然后浏览器就显示这个页面,但它发现这个页面要张图,实际上浏览器会再次跟我发起连接,连上以后,它去要那张图logo.png,然后服务器找到这张图以后,再次把这张图,回过去给你,那这其实已经算是第2次连接了吧,咱们之前做了一件事情是什么呢,它在第一次建立连接的时候,它要了这个页面,我把这个东西回给它,回给它以后,实际上我们服务器并没有和客户端断开,如果看ClientHandler的run方法,就是处理这个请求,我们获取输入流,获取输出流,到响应把找到的文件,发回给客户端,但是发完了以后,实际上那个流并没有关,如果流并没有关,实际上浏览器还认为,它还跟我连着呢,它并没有断开,这个和聊天室不一样,聊天室它可以一直连接着,它可以一直问一直回,但是在HTTP是这样的,它一次问,一次回,就断了,就应该主动跟它断掉,run方法的最后,加上finally块,在这里面把socket断开连接。
    在这里插入图片描述
拷贝浏览器中的网页
  • 复制网页,点开网页,右键另存为,新建一个文件目录a,将网页另存到这个目录a中,将其中的js文件,css文件,和图片分别放到服务器项目下的webapp目录下的自建的js目录,css目录和images目录中;然后打开网页的源代码,找到移到webapp的那些文件的位置,将它们的超链接对应的路径,修改为当前在webapp下的路径,然后将网页.html文件也移到webapp下,这样一个网页就基本复制过来了,可以在客户端,可以输入自己的服务器的资源路径去访问那个复制的网页,当然配置文件web.xml中也要对相应的css,js等文件键值对加入到contentTypeMapping中,当然网页可能无法下载全部样式文件,所以与原网页在美观性上会差很多。
favicon.ico图标
  • 另外,控制台还可能看到uri:webapp/favicon.ico,浏览器会自动访问这个资源,这个就是你网页页眉上的logo图标,一般在网址栏中,输入 http://www.it211.com.cn/favicon.ico,类似于这样的域名+favicon.ico,就会拿到网页的那个logo图标的图片。把这个logo图标,放到我们自己项目webapp的根目录下,和我们的.html文件在同一目录下,这样我们访问我们的.html页面时自然也就会带上这个logo了。另外,如果web.xml文件中没有将这个图的后缀与contentType存入Map中,虽然网页可以显示,但控制台观察到的这张图的类型就是null,如,后缀:ico; contentType:null;,所以需要把它也夹入Map中。
在ClientHandler中重构404响应
  • 上面写好之后,我们再做一个页面,当我们访问我们服务器下,一个没有的页面时,服务端就没有反应了,这不友好,其实因为服务端没找到资源,什么都没理我们,一个响应都没有,当访问一个没有的页面时,会出一个表情,没有这个页面,这就不错。那现在就做一个这样的404页面出来,当然咱不做的那么花哨了,是那个意思就行了。在webapp下再新建一个404.html,见源码。之前在ClientHandler类里的run方法中,如果有我们要访问的页面,就把正常页面返回去,如果没有就显示404没有这个资源,现在依据显示正常页面的代码,如果没有请求的页面,就把这个写好的404.html页面返回给客户端。
重构ClientHandler之responseFile方法
  • 实际上,我们会发现,这时的代码又出现重复了,那么我们就再在下面创建一个新的方法,然后去调用,不过,我们要注意,这个重复代码中相同和不同的部分,不一样的地方,就是那个状态码不一样,剩下的是不是都一样,最终响应一个文件,我们在ClientHandler类里面,再建立一个方法,responseFile(int status, File file,HttpResponse response),我们给两个参数,一个是那个状态码,一个是响应的那个文件,将设置响应信息的代码复制到这个方法里后,发现还少一个参数没传,HttpResponse response,因为最终我们要把那个参数,设置到response里面,具体代码见源码。这些都写好了后,我们进入我们写的index.html页面发现,在输入框输入写内容,点完了一下没有反应。也就是说我们是不是应该正常的,把这个数据交给服务器,服务器处理以后,给我一个结果,最常见的到网上干嘛啊,登录注册吧。

写在后面

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值