WebServer代码实现第一版:请求和响应信息的简单处理
- 前言
- 1.建立Socket连接与接收请求信息代码分析与实现
- 2.代码重构之连接池:WebServer
- 3.处理请求信息:HttpRequest
- 4.处理响应信息:HttpResponse
- 5.WebServer服务器代码实现第一版及相关解析
- 1.WebServer/pom.xml:
- 2.WebServer/conf/web.xml:
- 3.WebServer/src/main/java/common/HttpContext.java:
- 4.WebServer/src/main/java/http/HttpResponse.java:
- 5.WebServer/src/main/java/http/HttpRequest.java:
- 6.WebServer/src/main/java/core/ClientHandler.java:
- 7.WebServer/src/main/java/core/WebServer:
- 8.WebServer/webapp/index.html:
- 9.WebServer/webapp/404.html:
- 10.WebServer/webapp/reg.html:
- 11.关于以上代码做一些解释
- 写在后面
前言
- 返回 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.html
和HTTP/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-else
,if-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=1
,Connection=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>
,h1
到h6
,这是标题,让这个标题标签里面的内容居中的话,还有一个标签可以用,就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/html
,image/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-1
,out.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
页面发现,在输入框输入写内容,点完了一下没有反应。也就是说我们是不是应该正常的,把这个数据交给服务器,服务器处理以后,给我一个结果,最常见的到网上干嘛啊,登录注册吧。