HTTP学习与Web服务器编程

这次的主题是查找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) {
                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!

  • 9
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值