您的位置:首页 > 理论基础 > 计算机网络

HTTP学习与Web服务器编程

2017-11-20 00:00 232 查看
这次的主题是查找HTTP协议的相关资料,基于此编写一个简单的Web服务器。

需要完成的几大主要的要求有:

1)编写一个简单的Web服务器;

2)实现的服务器应能与标准的浏览器进行简单的交互;

3)记录浏览器与服务的交互过程;

4)利用HTML语言编写网页浏览器可通过编写的Web服务器正常访问该网页;

5)支持多用户并发访问;

6)扩展编写的简单Web服务器,使浏览器能够浏览Web上存储的图像

一.了解http协议(参考百度百科)

HTTP是一个客户端和服务器端请求和应答的标准(TCP)。客户端是终端用户,服务器端是网站。通过使用Web浏览器、网络爬虫或者其它的工具,客户端发起一个到服务器上指定端口(默认端口为80)的HTTP请求。(我们称这个客户端)叫用户代理,应答的服务器上存储着(一些)资源,比如HTML文件和图像;(我们称)这个应答服务器为源服务器。“客户”与“服务器”是一个相对的概念,只存在于一个特定的连接期间,即在某个连接中的客户在另一个连接中可能作为服务器。基于HTTP协议的客户/服务器模式的信息交换过程,它分四个过程:建立连接、发送请求信息、发送响应信息、关闭连接。

HTTP协议是基于请求/响应范式的。一个客户机与服务器建立连接后,发送一个请求给服务器,请求方式的格式为,统一资源标识符、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和可能的内容。服务器接到请求后,给予相应的响应信息,其格式为一个状态行包括信息的协议版本号、一个成功或错误的代码,后边是MIME信息包括服务器信息、实体信息和可能的内容。

其实简单说就是任何服务器除了包括HTML文件以外,还有一个HTTP驻留程序,用于响应用户请求。你的浏览器是HTTP客户,向服务器发送请求,当浏览器中输入了一个开始文件或点击了一个超级链接时,浏览器就向服务器发送了HTTP请求,此请求被送往由IP地址指定的URL。驻留程序接收到请求,在进行必要的操作后回送所要求的文件。在这一过程中,在网络上发送和接收的数据已经被分成一个或多个数据包,每个数据包包括:要传送的数据;控制信息,即告诉网络怎样处理数据包。TCP/IP决定了每个数据包的格式。如果事先不告诉你,你可能不会知道信息被分成用于传输和再重新组合起来的许多小块。

HTTP报文由从客户机到服务器的请求和从服务器到客户机的响应构成。

请求报文格式为:请求行 - 通用信息头 - 请求头 - 实体头 - 报文主体。请求行以方法字段开始,后面分别是 URL 字段和 HTTP 协议版本字段,并以 CRLF 结尾。SP 是分隔符。除了在最后的 CRLF 序列中 CF 和 LF 是必需的之外,其他都可以不要。有关通用信息头,请求头和实体头方面的具体内容可以参照相关文件。

应答报文格式为:状态行 - 通用信息头 - 响应头 - 实体头 - 报文主体。状态码元由3位数字组成,表示请求是否被理解或被满足。原因分析是对原文的状态码作简短的描述,状态码用来支持自动操作,而原因分析用来供用户使用。客户机无需用来检查或显示语法。有关通用信息头,响应头和实体头方面的具体内容可以参照相关文件。

简而言之,使用http就像我们打电话订货一样,我们可以打电话给商家,告诉他我们需要什么规格的商品,然后商家再告诉我们什么商品有货,什么商品缺货。这些,我们是通过电话线用电话联系(HTTP是通过TCP/IP),当然我们也可以通过传真,只要商家那边也有传真。

二.建立简单的web服务器

我们将使用java语言,基于java.net.Socket和java.net.ServerSocket实现一个简单的web服务器。

首先我们来看一下整个程序的大致结构(使用的IDE为eclipse):



有三个Java类,分别是服务器类MyWebServer.java和需要在服务器类里调用的Request类和Responses类,分别用来处理接收到的http协议报文的解析工作和响应工作。在本工程目录下建一个文件夹resource用来储存所有的html文件和图片。

现在主要说说整个程序的思路:

1.创建一个ServerSocket对象;

2.调用ServerSocket对象的accept方法,等待连接,连接成功会返回一个Socket对象,否则一直阻塞等待;

3.从Socket对象中获取InputStream和OutputStream字节流,这两个流分别对应request请求和response响应;

4.处理请求:读取InputStream字节流信息,转成字符串形式,并解析

5.处理响应:根据解析出来的uri信息,从WEB_ROOT目录中寻找请求的资源资源文件, 读取资源文件,并将其写入到OutputStream字节流中;

6.关闭Socket对象;

7.转到步骤2,继续等待连接请求;

(1)服务器类

//MyWebServer.java
package homework3;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class MyWebServer {
public static int PORT=8888;
public static final String WEB_ROOT="resource";
public static void main(String[] args) {
System.out.println("开启服务器");
try {
ServerSocket WebServer=null;
WebServer = new ServerSocket(PORT,1,InetAddress.getByName("127.0.0.1"));
while(true)
//不断循环监听是否有新的请求,有的话启动一个线程响应
{
Socket Client=WebServer.accept();
new HttpConnectThread(Client).start();
}
} catch (IOException e) {

4000
e.printStackTrace();
}
}
}

class HttpConnectThread extends Thread
{
private Socket Client;
public HttpConnectThread(Socket s)
{
Client=s;
}

public void run()
{
try {
InputStream input=null;
OutputStream output=null;
input=Client.getInputStream();
output=Client.getOutputStream();
//从Socket对象中获取InputStream和OutputStream字节流,
//这两个流分别对应request请求和response响应;
Request request=new Request(input);
if(request.parse(Client)==1){
//处理请求:读取InputStream字节流信息,转成字符串形式,并解析
Response response=new Response(output);
response.setRequest(request);
response.sendStaticResource(Client);
}
//处理响应:根据解析出来的信息,从WEB_ROOT目录中寻找请求的资源资源文件,
//读取资源文件,并将其写入到OutputStream字节流中;
Client.close();
} catch (IOException e) {
e.printStackTrace();
}

}
}


简单的说一下这一块:

先是定义了服务器的端口port(8888)和基目录webroot(resource),然后利用ServerSocket建立了一个本机上8888端口的服务器。为了完成要求5)支持多用户并发访问,我们在这边做了两件事,首先用一个while循环不断地监听是否有对应本服务器的请求,第二件事对于每一个请求访问的ip地址,新建一个线程与其进行交互。所以while里边就是利用新建一个socket并且让他WebServer.accept()去监听别人的请求,没听到就阻塞在这里一直监听,听到了new一个我们写好的线程HttpConnectThread并且start它。

再来看看这个线程的run函数。首先利用socket的getInputStream()和getOutputStream()从Socket对象中获取InputStream和OutputStream字节流,这两个流分别对应request请求和response响应。request处理请求主要工作为读取InputStream字节流信息,转成字符串形式,并解析。Response处理响应会根据解析出来的信息,从WEB_ROOT目录中寻找请求的资源文件,读取资源文件,并将其写入到OutputStream字节流中。需要说明的是这里我使用if(request.parse(Client)==1)这句话是因为这里出现的一个小bug,后边我在说明request类再详细说明。

(2)请求类

//request.java
package homework3;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.Socket;

public class Request {
private InputStream input;
private String uri;

public Request(InputStream input){
this.input=input;
}

public int parse(Socket socket){
//Read a set of characters from the socket
StringBuffer request=new StringBuffer(2048);
int i;
byte[] buffer=new byte[2048];
try {
i=input.read(buffer);
} catch (Exception e) {
e.printStackTrace();
i=-1;
}
for(int j=0;j<i;j++){
request.append((char)buffer[j]);
}
System.out.print(request.toString());
uri=parseUri(request.toString());

if(request.toString().split("\n")[0].contains("html")||request.toString().split("\n")[0].contains("jpg")){
return 1;
}
else{
// 下面是由服务器直接生成的主页内容
// 1、首先向浏览器输出响应头信息
PrintStream out;
try {
out = new PrintStream(socket.getOutputStream(), false, "GB2312");

out.println("HTTP/1.1 200\r");
out.println("Content_Type:text/html\r");
out.println("");//报文头和信息之间要空一行
// 2、输出主页信息
out.println(
"<HTML><BODY>"
+ "<center>"
+ "<H1>HTTP协议测试服务器"
+ "</H1>"
+ "<form method='get' action='http://127.0.0.1:8888/'>username:<input type='text' name='username'/>password:<input type='text' name='password'/><input type='submit' value='GET测试'/></form><br/>"
+ "<form method='post' enctype='text/plain' action='http://127.0.0.1:8888/'>username:<input type='text' name='username'/>password:<input type='text' name='password'/><input type='submit' value='POST测试'/></form><br/>"
+ "</center>您提交的数据如下:<pre>" + request.toString()
+ "</pre></BODY></HTML>");
out.flush();
out.close();
System.out.println("msg.toString()   "+request.toString());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

return 1;

}

public String parseUri(String requestString){
int index1,index2;
index1=requestString.indexOf(" ");
if(index1!=-1){
index2=requestString.indexOf(" ",index1+1);
if(index2>index1){
return requestString.substring(index1+1,index2);
}
}
return null;
}

public String getUri(){
return this.uri;
}
}


这个程序我调了很久的bug,因为本来并没有写html文件,而是像上边程序中一样直接通过printStream输出流往socket的getOutputStream输出html语句就可以出现对应的网页,但是在显示图片的时候出现了坑爹的情况。首先使用绝对路径的时候浏览器会报错“Not allowed to load local resource”,究其原因是浏览器基于安全考虑不允许直接访问。换成相对路径吧,服务器当前工作路径下建一个resource放图片,写地址用/resource/xxx.jpg。好么,图片死活不出来,在浏览器按F12审查了半天也没审查个所以然来,感觉就是传过去的文件type是text/html类型的而不是jpg类型的(但是最后改完可以显示了我一看还是text/html类型),总之经历了很久debug的绝望以后我换成了现在这种写法,即要显示图片的话还是老老实实写一个html页面,如果是动态显示记录的浏览器与服务的交互过程(是的本程序你不知可以在console里看,在浏览器也可以直接看),没涉及图片输出,用我原来的想法。这就解释了我为什么要把require返回给服务器的return分为1和0了,解析报文如果有请求图片和html文件,那么返回1,调用response回应对应文件,否则返回0不调用response的内容,而是直接通过printStream输出html语句动态显示get和post过程中的报文协议。

(3)回应请求类

//response.Java
package homework3;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;

/**
* HTTP Response = Status-Line
*      *(( general-header | response-header | entity-header ) CRLF)
*      CRLF
*      [message-body]
*      Status-Line=Http-Version SP Status-Code SP Reason-Phrase CRLF
*
*/
public class Response {
private static final int BUFFER_SIZE=1024*1024;
Request request;
OutputStream output;

public Response(OutputStream output){
this.output=output;
}

public void setRequest(Request request){
this.request=request;
}

public void sendStaticResource(Socket socket)throws IOException{
byte[] bytes=new byte[BUFFER_SIZE];
FileInputStream fis=null;
try {
File file=new File(MyWebServer.WEB_ROOT,request.getUri());
if(file.exists()){

fis=new FileInputStream(file);
int ch=fis.read(bytes,0,BUFFER_SIZE);
String header = "HTTP/1.1 200\r\n" + "Content-Type: text/html\r\n"+
"Content-Length: " + file.length() + "\r\n" + "\r\n";
output.write(header.getBytes());
while(ch!=-1){

output.write(bytes, 0, BUFFER_SIZE);
ch=fis.read(bytes, 0, BUFFER_SIZE);
}

}else{
//file not found
String errorMessage="HTTP/1.1 404 File Not Found\r\n"+
"Content-Type:text/html\r\n"+
"Content-Length:23\r\n"+
"\r\n"+
"<h1>File Not Found</h1>";
output.write(errorMessage.getBytes());
}
} catch (Exception e) {
System.out.println(e.toString());
}finally{
if(fis!=null){
output.close();
fis.close();
}
}
}
}


(2)(3)两个类就不详细一句句说明了,就着注释应该很容易明白。

三.实现效果

点开MyWebServer文件,点击运行,console会跳出:



随便打开一个浏览器(我用的是chrome),输入127.0.0.1:8888



在get和post随便输入数据点击按钮,可以观察下边的数据变化情况

OK~如果想要访问图片和服务器里的html文件,输入127.0.0.1:8888/index.html



根据页面内容点击链接可以访问不同的文件。当然回头看一下console里面记录着所有的protocol信息。





最后附上index.html和index2.html(图片大家自己找啦)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Web服务器</title>
</head>
<body>
<h1>This is yaozonghai's webserver</h1>
<img src="pic\a3.jpg" /><br>
<h5>This is a simple index.Your request will be sent to my WebServer</h5>
<p>you can click <a href="index2.html"> more image</a> to scan picture on web<p>
<img src="pic\a5.jpg" /><br>
<p>you can click <a href="xxx"> observe the http protocol</a> to observe the http protocol<p>
<h3>Thank you for using<h3>
</body>
</html>


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>LBJ</title>
</head>
<body>
<h1>so handsome the man is</h1>
<img src="pic\a1.jpg" />
<img src="pic\a2.jpg" />
<img src="pic\a3.jpg" />
<img src="pic\a4.jpg" />
<a href="index.html"> 返回</a>

</body>
</html>


That’s all,thank you!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息