手把手教你撸一个Web服务器(一)

声明:本文大概需要30分钟,如果只依据本文不看源码就能写出Web服务器就算学会了~如有错误欢迎指正~

首先我们要知道web服务器是什么?
  一般指网站服务器,是指驻留于因特网上某种类型计算机的程序
服务器有什么作用:
  1.放置网站文件,让别人浏览
  2.可以放置数据文件,供别人下载
服务器分类:
  1.Apache(例如TomCat)
  2.Nginx
  3.IIS
Web服务器的工作原理,分四步:
  1.连接过程
  2.请求过程
  3.应答过程
  4.关闭连接

手撸web服务器就是根据web服务器的工作原理去手写代码以实现例如Tomcat的部分核心功能

根据这两天的学习可以分为基础和进阶版本:
  基础就是简单实现,灵活度不高
  进阶就是把部分固定功能的代码封装,再做一些动态的方法以供调用

下面我总结一下手撸Web服务器的业务逻辑

 

逻辑

 1 /**
 2  * 极其简易的版本
 3  * @author shaking
 4  *
 5  */
 6 public class WebServer {
 7     
 8     public static void main(String[] args) {
 9         WebServer webServer = new WebServer();
10         webServer.start();
11     }
12 
13     private ServerSocket server;
14     
15     public WebServer() {
16         try {
17             server = new ServerSocket(8080);
18         } catch (IOException e) {
19             e.printStackTrace();
20         }
21     }
22     
23     public void start() {
24         try {
25             while(true) {
26                 Socket socket = server.accept();
27                 OutputStream outputStream = socket.getOutputStream();
28                 outputStream.write("abcaaa".getBytes());
29                 outputStream.flush();
30                 socket.close();
31             }
32         } catch (IOException e) {
33             e.printStackTrace();
34         }
35         
36     }
37     
38 }

基础实现:
一、创建服务器类即WebServer类(连接过程)
  ·1声明ServerSocket类,代表服务器;ServerSocket作用是监听特定端口,例如:8080,8086等;端口号总共用65535个
    绑定端口方法:ServerSocket server = new ServerSocket(8080);
    利用构造方法初始化ServerSocket;
    端口可以传入0,表示操作来为服务器分配一个任意可用的端口,也称为匿名端口,但不推荐使用;
    如果端口被占用会抛出BindException异常
    解决办法:win + r进入运行,输入CMD进入命令行模式
    输入 netstat -ano查看所有被占用端口 找到想要关闭的端口对应的Listening后的值
    输入 taskkill -f -pid (Listening后的值)


  ·2创建开始方法start()(请求和应答过程)
    (请求)调用ServerSocket的accept()方法监听并接受套接字(socket)的连接,返回值是Socket对象
    因为服务器是被动程序,需要等有请求的时候才会响应,所以accept()方法应该是持续运行的;所以要用到while(true)
    (应答)接收到请求之后根据请求的不同应该回应不同的信息,此处基础实现回应相同的信息
    ·调用socket的getOutputStream()返回这个套接字的输出流(OutputStream)
    ·然后调用OutputStream的方法write(byte[] b)输入想写内容;因为我们输入的是字符串,而write方法要求传入字节数组,
    所以调用字符串的getBytes()方法返回字符数组
    ·然后调用OutputStream的方法flush()刷新流并强制写出所有缓冲的输出字节
  ·3关闭连接
    调用Socket的close()方法关闭连接

  ·4测试该基础服务器能否成功运行
    利用HttpWatch监听该连接过程,查看请求和响应;如果响应的是你在write中写的内容,该服务器即创建成功!
    但是其中页面一直处于加载过程,原因是你的响应不符合Http协议,浏览器一直在等待想要的内容(即Http的标准响应格式)

  ·5修改程序不当的响应方式
    ·这个时候就要修改write以符合Http的标准响应格式
    ·调用PrintStream对象,该对象继承了FilterOutputStream,而FilterOutputStream继承了OutputStream;
    ·该对象与其他输出流不同的是永远不会抛出IOException,并且会自动调用flush()方法(一般在执行print,println,write时自动执行),在需要写入字符的时候推荐使用;因为IO流是基于装饰者模式,所以使用该对象必须两种类型(File或OutputStream)参数传入一种,此处传入的是OutputStream;而OutputStream可以通过Socket的getOutputStream()方法得到
    ·调用PrintStream的println()方法拼接出标准的Http协议响应格式(状态行,响应头,空行,响应内容)如果在调试过程中没有响应,可以尝试以下方法,查看方法调用顺序是否有问题、重新启动浏览器、换一个浏览器、重启Eclipse

 1 /**
 2  * 修改符合HTTP协议的版本
 3  * @author shaking
 4  *
 5  */
 6 public class WebServer {
 7     
 8     public static void main(String[] args) {
 9         WebServer webServer = new WebServer();
10         webServer.start();
11     }
12 
13     private ServerSocket server;
14     
15     public WebServer() {
16         try {
17             server = new ServerSocket(8080);
18         } catch (IOException e) {
19             e.printStackTrace();
20         }
21     }
22     
23     public void start() {
24         try {
25             while(true) {
26                 Socket socket = server.accept();
27                 PrintStream ps = new PrintStream(socket.getOutputStream());
28                 ps.println("HTTP/1.1 200 OK");
29                 ps.println("Content-Type:text/html");
30                 String s = "server is running->->->->";
31                 ps.println("Content-Length:" + s.length());
32                 
33                 ps.println("");
34                 
35                 ps.write(s.getBytes());
36                 socket.close();
37             }
38         } catch (IOException e) {
39             e.printStackTrace();
40         }
41         
42     }
43     
44 }

这个时候浏览器上应该有write()里的内容:“server is running->->->->”;如果出错了检查是不是没有加空行

 

进阶实现:
具体的Web服务器结构
·cn.itlou----core 核心包: WebServer ,ClientHandler
       |
       ---http 封装Http协议相关内容:HttpRequest ,HttpResponse
       |
       ---common 参数配置信息:ServletContext ,HttpContext

config配置文件:web.xml

一、基础实现有许多许多的不足,单线程不能同时接收过多的请求,所以我们加入多线程技术
  具体的服务器架构不用变,增加线程池对象的引用并初始化线程池;
  ExecutorService threadPool = Executors.newFixedThreadPool(int a);线程池的创建方法,记得数字不要给的过高,可能电脑不行导致程序出错;
  在start()方法中加入线程的应用,调用execute(Runnable command)方法;传入实现Runnable的对象ClientHandler
  而该对象应该包含所有我们希望通过利用多线程提高性能的方法(应答,关闭连接)
  我们把该对象命名为ClientHandler它实现了Runnable接口,重写run方法并写入应答,关闭连接的代码;
  ·1声明一个代表客户端的对象Socket,并将该对象传入ClientHandler构造方法;
  ·2提取响应代码写入run方法中
  ·3利用线程池执行写好的ClientHandler类
  注意:重写时写入网页数据应该用PrintStream的write方法,而不是println()方法,使用println方法会输出地址

 1 /**
 2  * 进阶利用多线程的版本
 3  * @author shaking
 4  *
 5  */
 6 public class WebServer {
 7     
 8     public static void main(String[] args) {
 9         WebServer webServer = new WebServer();
10         webServer.start();
11     }
12 
13     private ServerSocket server;
14     private ExecutorService threadPool;
15     
16     public WebServer() {
17         try {
18             server = new ServerSocket(8080);
19             threadPool = Executors.newFixedThreadPool(100);
20         } catch (IOException e) {
21             e.printStackTrace();
22         }
23     }
24     
25     public void start() {
26         try {
27             while(true) {
28                 Socket socket = server.accept();
29                 threadPool.execute(new ClientHandler(socket));
30             }
31         } catch (IOException e) {
32             e.printStackTrace();
33         }
34         
35     }
36     
37 }
 1 /**
 2  * 多线程部分代码
 3  * @author shaking
 4  *
 5  */
 6 public class ClientHandler implements Runnable{
 7     
 8     private Socket socket;
 9     
10     public ClientHandler(Socket socket) {
11         this.socket = socket;
12     }
13 
14     public void run() {
15         try {
16             PrintStream ps = new PrintStream(socket.getOutputStream());
17             ps.println("HTTP/1.1 200 OK");
18             ps.println("Content-Type:text/html");
19             String s = "server is running------->>>";
20             ps.println("Content-Length:" + s.length());
21             
22             ps.println("");
23             
24             ps.write(s.getBytes());
25             socket.close();
26         } catch (IOException e) {
27             e.printStackTrace();
28         }
29         
30     }
31     
32 }

这个时候浏览器上应该有write()里的内容:“server is running------->>>”;如果出错了检查是不是线程池加入过多线程数;

二、修改程序使其能输出具体的网页
这时候只需要修改响应部分的部分代码;即修改ClientHandler类中run方法的部分内容
  ·1自制一个简单网页或者已有的网页,例如index.html,把其放入WebContent文件夹下
  思考一下在网页中怎么输出的字符串,网页同理(传入文件,写出文件内容)
  ·2    -1)传入文件,用File类,new File(); 在其中传入一个String类型的pathName,然后使用BufferedInputStream字节缓冲流,使用该类需要传入一个FileInputStream类型的对象,这里我们使用FileInputStream传入我们的File对象即想要输出的网页,将字节数组byte[] bs = new byte[(int)file.length()],传入BufferedInputStream的read(byte[])方法将文件读入
         -2)写出文件内容,调用PrintStream的write方法,write要求传入字符数组,调用PrintStream的write(byte[])方法传入已经创建好的字符数组,关闭BufferedInputStream流
  ·3关闭socket连接

 1 /**
 2  * 输出具体页面的代码
 3  * @author shaking
 4  *
 5  */
 6 public class ClientHandler implements Runnable{
 7     
 8     private Socket socket;
 9     
10     public ClientHandler(Socket socket) {
11         this.socket = socket;
12     }
13 
14     public void run() {
15         try {
16             PrintStream ps = new PrintStream(socket.getOutputStream());
17             ps.println("HTTP/1.1 200 OK");
18             ps.println("Content-Type:text/html");
19             String pathName = "WebContent/index.html";
20             File file = new File(pathName);
21             ps.println("Content-Length:" + file.length());
22             
23             ps.println("");
24             
25             BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
26             
27             byte[] bs = new byte[(int) file.length()];
28             
29             bis.read(bs);
30             ps.write(bs);
31             bis.close();
32             socket.close();
33         } catch (IOException e) {
34             e.printStackTrace();
35         }
36         
37     }
38     
39 }

三、我们这个服务器还是很低端,只能显示一个固定的网页,我们希望服务器能够动态的响应输入的所有网页,有的就显示,没有就404
  根据地址栏输入的网址不同,请求行也会相应的改变
  ·1获取请求行的部分内容,例如: GET /abc.html HTTP/1.1
    这里我们使用字符输入流BufferedReader,传入新的对象InputStreamReader并在新的对象中传入socket的输入流;
    调用BufferedReader的方法readline()读取这一行数据,如果需要完整的HTTP请求可以写死循环
    我们利用字符串的split()方法切割请求行,以" "为目标切割成3份再用String类型数组接收,其中索引为1的数组就是我们想要的/abc.html
  ·2修改pathName的值使其可以动态的变化
  ·3设置index默认页面和404错误页面
    判断split切割后的地址,决定如何显示页面;如果为空显示index,如果没有对应的网页显示404
    index需要判断切割后的地址是否为空
    404需要判断切割后的地址对应的文件是否存在
  注意:如果控制台报错:FileNotFoundException:WebContent\favicon.ico在WebCont下放入一个后缀名为ico的图片文件即可

 1 /**
 2  * 输出具体页面的代码
 3  * 包括默认首页与404页面
 4  * @author shaking
 5  *
 6  */
 7 public class ClientHandler implements Runnable{
 8     
 9     private Socket socket;
10     
11     public ClientHandler(Socket socket) {
12         this.socket = socket;
13     }
14 
15     public void run() {
16         try {
17             BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
18             String line = reader.readLine();
19             String[] s = line.split(" ");
20             String uri = s[1];
21             
22             if(uri.equals("/")) {
23                 uri = "/index.html";
24             }
25             
26             PrintStream ps = new PrintStream(socket.getOutputStream());
27             ps.println("HTTP/1.1 200 OK");
28             ps.println("Content-Type:text/html");
29             String pathName = "WebContent" + uri;
30             File file = new File(pathName);
31             
32             if(!file.exists()) {
33                 uri = "/404.html";
34                 file = new File("WebContent" + uri);
35             }
36             
37             ps.println("Content-Length:" + file.length());
38             
39             ps.println("");
40             
41             BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
42             
43             byte[] bs = new byte[(int) file.length()];
44             
45             bis.read(bs);
46             ps.write(bs);
47             bis.close();
48             socket.close();
49         } catch (IOException e) {
50             e.printStackTrace();
51         }
52         
53     }
54     
55 }

                                                                    转载请注明出处:http://www.cnblogs.com/shak1ng/

转载于:https://www.cnblogs.com/shak1ng/p/8963177.html

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值