分布式计算 lab3 HTTP服务器
题目:
实验三:实现一个基本的Web服务器程序
【实验目的及要求】
采用Socket API知识和对HTTP协议,CGI的理解,实现一个基本的WEB服务器程序,要求服务器能成功响应客户程序发来的GET命令(传送文件),进一步实现响应POST和GET命令的CGI程序调用请求。
要求:要求独立完成。
【实验原理和步骤】
1.实验原理
(1)服务器主要监听来至客户浏览器或是客户端程序的连接请求,并且接收到客户请求后对客户请求作出响应。如果请求是静态的文本或是网页则将内容发送给客户。如果是CGI程序则服务器调用请求的CGI程序,并发送结果给客户。
(2)HTTP协议是基于TCP/IP协议之上的协议,是Web浏览器和Web服务器之间的应用层协议,是通用的、无状态的、面向对象的协议。
(3)HTTP的请求一般是GET或POST命令(POST用于FORM参数的传递)。GET命令的格式为GET 路径/文件名 HTTP/1.0 文件名指出所访问的文件,HTTP/1.0指出Web浏览器使用的HTTP版本。
(4)Web浏览器提交请求后,通过HTTP协议传送给Web服务器。Web服务器接到后,进行事务处理,处理结果又通过HTTP传回给Web浏览器,从而在Web浏览器上显示出所请求的页面。
在发送内容之前Web服务器首先传送一些HTTP头信息:
HTTP 1.0 200 OK
WEBServer:1.0 // 服务器类型
content_type:类型
content_length:长度值
(5)响应POST和GET命令的CGI程序调用请求需要服务器执行外部程序,Java执行外部可执行程序的方法是:首先通过Runtime run = Runtime.getRuntime()返回与当前Java 应用程序相关的运行时对象;然后调用Process CGI = run.exec(ProgramName)另启一个进程来执行一个外部可执行程序。
2. Web服务器的实现步骤:
(1) 创建ServerSocket类对象,监听端口8080。这是为了区别于HTTP的标准TCP/IP端口80而取的;
(2) 等待、接受客户机连接到端口8080,得到与客户机连接的socket;
(3) 创建与socket字相关联的输入流和输出流
(4) 从与socket关联的输入流instream中读取一行客户机提交的请求信息,请求信息的格式为:GET 路径/文件名 HTTP/1.0
(5) 从请求信息中获取请求类型。如果请求类型是GET,则从请求信息中获取所访问的文件名。没有HTML文件名时,则以index.html作为文件名;
(6) 如果请求文件是CGI程序存则调用它,并把结果通过socket传回给Web浏览器,(此处只能是静态的CGI程序,因为本设计不涉及传递环境变量)然后关闭文件。否则发送错误信息给Web浏览器;
(7) 关闭与相应Web浏览器连接的socket字。
一开始确实不知道这个实验怎么下手。要用Java编写?服务器与客户端都用Java编写?客户端直接用浏览器?浏览器发送的请求是怎么样的?浏览器那的地址栏要怎么写?......
诸如此类的一大堆问题,让这个实验貌似不那么可爱..其实一个个解决下来的话,发现这个实验还是挺有趣的。
其实有时候一下子就能做出来的东西反而没那么有趣了,男人不都喜欢“犹抱琵琶半遮面”嘛....还是有点儿道理的。
解决问题还是从外到内比较好,层层深入。
一
第一个问题,客户端与服务端的角色各是由什么来扮演?我想服务器端铁定是Java没跑的,关键是客户端。要求里说的web浏览器貌似暗示我用浏览器作为客户端,可是是否可以用java模拟一个简陋版的浏览器呢?...事实证明我想多了.....
二
既然客户端是浏览器,那么要把整个过程弄清楚:
我理解的过程是:我们在浏览器那里输入地址,按下回车之后浏览器进程就往相应地址的服务器的相应端口上发送请求。至于怎么把浏览器网址栏里的信息转化成HTTP请求,那是浏览器的事儿了。然后我自己用Java编写的服务器只需提前在对应的端口那监听着,接收web浏览器发过来的请求。再根据请求内容作相应的逻辑处理,把web浏览器请求的文件通过Socket对象的流发送过去即可。(做这个实验的时候才真正理解了HTTP协议原来只针对文件传输的这一句话)
三
上面的过程弄懂后,我就写了个demo,复用了之前lab1文件传输的代码,把所需传输的文件指定(如String filePath = "D:\\temp\\index.html";)然后开浏览器(实验中用的360浏览器),输入本机地址以及端口号(127.0.0.1:8080)。激动人心的时候到了...= =回车..看到了自己写的那个巨丑的html文件,嗯。验证了猜测是正确的。
四
接下来要实现在浏览器地址那指定获取的页面(如127.0.0.1:8080/aotherpage.html)。要实现这个,服务器首先要从Socket流里把web浏览器的请求给读出来,然后从请求中把文件的路径给提取出来,再转化为特定的格式。然后用lab1中文件流读取的方法,把文件传给web浏览器。首先是读请求,这个很简单,一个readline()就可以了(实验原因,请求头下面的请求信息不予读取,以简化程序,若需读取,while循环里readline()读完即可)。读出来的是类似“GET /index.html HTTP/1.1” 之类的字符串。接下来要把文件路径给提取出来,也就是“/index.html”。我写了个简单的函数,思想就是用flag标记两个' '字符,然后取子字符串,一个substring就可以解决。最后是转换了,要把路径里的/都转换成\\,因为windows里的路径都是用\,加上转义符就是\\。同样写了个函数,解决了。
注:substring(a,b)是截取下标a到b-1
然后再一次在浏览器那输入127.0.0.1:8080/index.html 额,激动人心的时候又到了...= =回车..再次看到那巨丑的html文件,一切顺利。
五
接着要弄CGI了,关于CGI,在做这个实验前其实我没有真正的弄懂,就表面上理解而已。书上说:CGI协议在信息服务器和外部进程之间,提供了一个接口或网关。好吧,书上的定义通常都是越看越不清楚的。不过这里有个很重要的信息,就是CGI是一个协议。而实现了这个协议的程序叫CGI程序或CGI应用。也就是说,浏览器那里请求一个CGI的话,是请求我们的服务器运行CGI程序(exe之类的),再把那个程序的输出流赋值给文件传输的输入流,具体代码如下:
cgiIn = new DataInputStream(CGI.getInputStream());
cgiOut = new DataOutputStream(myDataSocket.getOutputStream());
其中CGI是CGI应用进程的句柄。至于为什么CGI调用的是getInputStream(),这需要理解Input在这里是相对于什么input。可以拿Socket的缓冲流类比,Socket的getInputStream()是从流读到程序内存中。所以这个input是对运行中的程序来说的。于是CGI调用的getInputStream()就是把CGI的输出作为程序的input。
关于CGI的另外一行重要代码如下:
Process CGI=run.exec(wholeFilePath.replaceAll(".cgi", ".exe"));
这里的重点是吧请求里的文件路径的后缀改了。(这一行代码在工程中应该放在上面那两行的前面,由于这样比较符合思维的走势,就放在这里了)
好了激动人心的时刻终于又要来一次了...浏览器那输入127.0.0.1:8080/test.cgi 回车....OK.看到了exe里面的输出(其实就是一些Html语句)显示在浏览器上了。
六
接下来实现post请求CGI。要浏览器发出POST请求,我们首先要弄一个带表单的html页面,我自己的是:
1 <html>
2 <head>
3 <title>Post Page</title>
4 <head>
5
6 <body>
7 <h1>This is the post page.</h1>
8 <form method="post" action="test.cgi">
9 <hr>
10 <p>Press <input type="submit" value="here"> to test for the post method.</p>
11 </form>
12 </body>
13 </html>
这样,在这个页面中,点击here这个button就会向服务器发出一个“POST /postpage.cgi HTTP1.1”请求。
然后实验,点button,再次看到了exe程序里的输出,同上面那个“激动人心的时刻”,不过上面那个是GET请求。
至此实验做完鸟。
还有一些问题:
1. 用Chrome浏览器浏览器的时候,除了我们所预想的请求外,还会发一个对favicon.ico的请求,不确定这个到底有什么用。
2. 360浏览器有时候请求到了页面,可是在自己编写的服务器那里没有请求被接收的记录。这证明浏览器实际上没有请求,估计360弄了个cache,把一些最近请求的页面放进去了,以便快速恢复。可是如果此页面更新频繁怎么办,我们都需要的是最新的页面?cache此时反而变成了个累赘....
实验代码:
1 import java.io.BufferedReader; 2 import java.io.DataInputStream; 3 import java.io.DataOutputStream; 4 import java.io.File; 5 import java.io.FileInputStream; 6 import java.io.IOException; 7 import java.io.InputStream; 8 import java.io.InputStreamReader; 9 import java.io.ObjectInputStream.GetField; 10 import java.net.ServerSocket; 11 import java.net.Socket; 12 13 14 public class MyServer { 15 16 public static void main(String[] args){ 17 //定义服务器html,cgi文件存放目录 18 String filePath = "D:\\workspace\\lab3\\htmlfiles"; 19 20 //定义服务器端口 21 int port = 8080; 22 23 try { 24 25 ServerSocket myConSocket = new ServerSocket(port); 26 27 while(true){ 28 //等待客户连接 29 System.out.println("Waiting to be connected."); 30 Socket myDataSocket = myConSocket.accept(); 31 System.out.println("Already get the apply."); 32 33 //定义输入输出流对象 34 InputStream is = myDataSocket.getInputStream(); 35 BufferedReader br = new BufferedReader(new InputStreamReader(is)); 36 DataInputStream cgiIn = null; 37 DataOutputStream cgiOut = null; 38 DataInputStream fileIn = null; 39 DataOutputStream fileOut = null; 40 41 //分析请求中的路径名,如无指定则返回index.html 42 String temp = br.readLine(); 43 44 System.out.println("The request is: " + temp); 45 String judgefornull = getPathSlashOK(getFilePath(temp)); 46 System.out.println("The real sub-path is: " + judgefornull); 47 if(judgefornull.equalsIgnoreCase("\\")) 48 judgefornull = "\\index.html"; 49 50 //判断页面类型 51 String fileType; 52 int length = judgefornull.length(); 53 if(length > 4){ 54 String testForHtml = judgefornull.substring(length-4, length); 55 String testForCgi = judgefornull.substring(length-3, length); 56 if(testForHtml.equalsIgnoreCase("html")) 57 fileType = "html"; 58 else if(testForCgi.equalsIgnoreCase("cgi")) 59 fileType = "cgi"; 60 else 61 fileType = "unknown"; 62 } 63 else 64 fileType = "unknown"; 65 66 67 68 69 //得到输出总路径 70 String wholeFilePath = filePath + judgefornull; 71 System.out.println("whole path is: " + wholeFilePath); 72 73 if(fileType.equals("cgi")){ 74 //CGI 75 System.out.println("I should use a cgi."); 76 Runtime run = Runtime.getRuntime(); 77 78 try { 79 Process CGI=run.exec(wholeFilePath.replaceAll(".cgi", ".exe")); 80 81 cgiIn = new DataInputStream(CGI.getInputStream()); 82 cgiOut = new DataOutputStream(myDataSocket.getOutputStream()); 83 84 //定义缓冲区 85 int bufferSize = 8192; 86 byte[] buff = new byte[bufferSize]; 87 88 //从文件流读入byte数组,再输出到Data Socket的缓冲流里 89 while(true) 90 { 91 int read = 0; 92 if(cgiIn != null) 93 read = cgiIn.read(buff); 94 95 if(read == -1) 96 break; 97 98 cgiOut.write(buff,0,read); 99 } 100 101 //冲刷缓冲区,使数据全部写入缓冲区,以防Socket的意外关闭 102 cgiOut.flush(); 103 104 cgiIn.close(); 105 cgiOut.close(); 106 107 108 } catch (IOException e) { 109 //处理当被请求的cgi程序不存在时的状况,返回notfound.html 110 wholeFilePath = "D:\\workspace\\lab3\\htmlfiles\\notfound.html"; 111 fileIn = new DataInputStream(new FileInputStream(wholeFilePath)); 112 fileOut = new DataOutputStream(myDataSocket.getOutputStream()); 113 114 //创建File变量,获取文件长度 115 File fi = new File(wholeFilePath); 116 System.out.println("文件长度: " + (int)fi.length()); 117 118 //定义缓冲区 119 int bufferSize = 8192; 120 byte[] buff = new byte[bufferSize]; 121 122 //从文件流读入byte数组,再输出到Data Socket的缓冲流里 123 while(true) 124 { 125 int read = 0; 126 if(fileIn != null) 127 read = fileIn.read(buff); 128 129 if(read == -1) 130 break; 131 132 fileOut.write(buff,0,read); 133 } 134 135 //冲刷缓冲区,使数据全部写入缓冲区,以防Socket的意外关闭 136 fileOut.flush(); 137 138 fileIn.close(); 139 fileOut.close(); 140 } 141 } 142 else if(fileType.equals("html")){ 143 //html 144 145 //创建File变量 146 File fi = new File(wholeFilePath); 147 148 //若所请求的html文件不存在,则返回notfound.html文件 149 if(!fi.exists()) 150 wholeFilePath = "D:\\workspace\\lab3\\htmlfiles\\notfound.html"; 151 152 fi = new File(wholeFilePath); 153 System.out.println("文件长度: " + (int)fi.length()); 154 155 fileIn = new DataInputStream(new FileInputStream(wholeFilePath)); 156 fileOut = new DataOutputStream(myDataSocket.getOutputStream()); 157 158 //定义缓冲区 159 int bufferSize = 8192; 160 byte[] buff = new byte[bufferSize]; 161 162 //从文件流读入byte数组,再输出到Data Socket的缓冲流里 163 while(true) 164 { 165 int read = 0; 166 if(fileIn != null) 167 read = fileIn.read(buff); 168 169 if(read == -1) 170 break; 171 172 fileOut.write(buff,0,read); 173 } 174 175 //冲刷缓冲区,使数据全部写入缓冲区,以防Socket的意外关闭 176 fileOut.flush(); 177 178 fileIn.close(); 179 fileOut.close(); 180 181 } 182 else{ 183 //other pages 184 185 wholeFilePath = "D:\\workspace\\lab3\\htmlfiles\\notfound.html"; 186 fileIn = new DataInputStream(new FileInputStream(wholeFilePath)); 187 fileOut = new DataOutputStream(myDataSocket.getOutputStream()); 188 189 //创建File变量,获取文件长度,这个有问题,等会位置要换一下 190 File fi = new File(wholeFilePath); 191 System.out.println("文件长度: " + (int)fi.length()); 192 193 //定义缓冲区 194 int bufferSize = 8192; 195 byte[] buff = new byte[bufferSize]; 196 197 //从文件流读入byte数组,再输出到Data Socket的缓冲流里 198 while(true) 199 { 200 int read = 0; 201 if(fileIn != null) 202 read = fileIn.read(buff); 203 204 if(read == -1) 205 break; 206 207 fileOut.write(buff,0,read); 208 } 209 210 //冲刷缓冲区,使数据全部写入缓冲区,以防Socket的意外关闭 211 fileOut.flush(); 212 213 fileIn.close(); 214 fileOut.close(); 215 } 216 217 System.out.println("file has been sent"); 218 219 myDataSocket.close(); 220 } 221 222 } catch (IOException e) { 223 System.err.println(e.getMessage()); 224 } 225 226 227 228 } 229 230 //从请求中提取所需文件路径 231 public static String getFilePath(String apply){ 232 int flag1 = 0; 233 int flag2 = 0; 234 for(int i = 0;i<apply.length();i++){ 235 if(apply.charAt(i) == ' '){ 236 flag1 = i; 237 break; 238 } 239 } 240 for(int i = flag1 + 1;i<apply.length();i++){ 241 if(apply.charAt(i) == ' '){ 242 flag2 = i; 243 break; 244 } 245 246 } 247 String path = apply.substring(flag1+1, flag2); 248 return path; 249 } 250 251 //把请求中的"/"换成"\\" 252 public static String getPathSlashOK(String rawPath){ 253 String cookedPath = ""; 254 int i = 0; 255 //若传"/"进来,即无指定路径,则直接赋值"\\" 256 int flag = rawPath.length(); 257 while(true){ 258 if(flag == 1){ 259 rawPath = "\\"; 260 break; 261 } 262 if(i == rawPath.length()) 263 break; 264 if(rawPath.charAt(i) == '/'){ 265 rawPath = rawPath.substring(0, i) + "\\" + rawPath.substring(i+1, rawPath.length()); 266 i += 2; 267 } 268 else 269 i++; 270 } 271 272 cookedPath = rawPath; 273 274 return cookedPath; 275 } 276 277 278 }