用 Java 实现一个简单的多线程 web 服务器

1. 整体思路

  • 主线程
  1. 建立一个ServerSocket
  2. 调用ServerSocket的accept方法。该方法一直阻塞,等待连接。如果连接建立,就会返回一个Socket对象。
  3. 生成一个子线程处理Socket。
  4. 执行步骤2。
  • 子线程
  1. 从Socket获得输入流,读入请求报文,找出请求资源的路径。
  2. 从Socket获得输出流,响应请求的资源(资源存在) 或 返回错误页面(资源不存在)。

2. 源码

  • Tomtiger类(主线程)
package tomtiger;

/**
 * @author liuleilei liuleilei2015@gmail.com
 * @date 2017年11月20日 下午4:52:55
 * @Description:the new Web Server Tomtiger, haha! 
 */

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.LinkedList;

public class Tomtiger {
    //定义html页面等的存放位置
    public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "WEB_ROOT";
    //定义web服务器占用的端口号
    public static final int port = 8080;
    //定义一个列表,存放 为每个连接的socket建立的线程。
    private List<Thread> connectlist = null;

    //main方法,服务器开始启动
    public static void main(String[] args) {
        //webserver 开始启动
        Tomtiger server = new Tomtiger();
        server.start();
    }

    public void start() {
        ServerSocket serversocket = null;
        try {
            //server 监听127.0.0.1:8080
            serversocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));

            //server 已经启动
            System.out.println("Tomtiger is running!!");

        }catch(IOException e) {
            e.printStackTrace();
        }

        //用来记录求情的数量
        int count = 0;

        //各线程存放的列表
        connectlist = new LinkedList<Thread>();
        while(true) {
            Socket socket = null;
            try {
                //为该请求建立连接
                socket = serversocket.accept();

                System.out.println("连接"+ count +"以建立!!");

                //为该socket建立多线程,启动,并加入列表
                ConnectionThread connectionthread = new ConnectionThread(socket);
                Thread thread = new Thread(connectionthread);
                thread.start();
                connectlist.add(thread);

                System.out.println("连接"+ count++ +"的线程已启动并加入队列!!");

            }catch(Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }
}
  • ConnectionThread类(子线程)
package tomtiger;
/**
 * @author liuleilei liuleilei2015@gmail.com
 * @date 2017年11月20日 下午2:46:53
 * @Description: implement the thread for every connection
 */

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.io.IOException;

public class ConnectionThread implements Runnable{

    Socket socket = null;
    InputStream inputstream = null;
    OutputStream outputstream = null;

    // 根据socket初始化socket的多线程类
    public ConnectionThread(Socket socket) {
        this.socket = socket;
    }

    /* (non-Javadoc)
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            // 获得该连接的输入输出流
            inputstream = socket.getInputStream();
            outputstream = socket.getOutputStream();
            //为该连接建立request,request从inputstream中读入http请求报文,转化为字符串,并截取到请求的资源。
            Request request = new Request(inputstream);
            //从request中获得请求资源的uri
            String uri = request.getUri();

            // 为请求建立response,response根据传入的outputstream,和uri。建立请求资源文件到outputstream之间的通路,并相应相应的资源。若资源不存在,响应默认页面
            Response response = new Response(outputstream);
            // response响应请求的资源
            response.responseResource(uri);

        }catch(IOException e) {
            e.printStackTrace();
        }finally {

        }
    }

}
  • Request类(处理Socket的输入流)
package tomtiger;
/**
 * @author liuleilei liuleilei2015@gmail.com
 * @date 2017年11月20日 下午4:00:14
 * @Description: implement the request for connection to get the uri
 */

import java.io.InputStream;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Request {

    private InputStream inputstream = null;
    private String uri = null;

    // 根据inputstream初始化request
    public Request(InputStream inputstream) {
        this.inputstream = inputstream;
        parseUri();
    }

    // 读取inputstream并将其转化为字符串
    @SuppressWarnings("finally")
    private String requestToString() {
        String requestString = null;
        // 字节流转字符流
        BufferedReader bfreader = new BufferedReader(new InputStreamReader(inputstream));
        StringBuffer buffer = new StringBuffer();
        // 这里我开始用的常用的while进行读取,但是一直卡在这里(不理解)!!!!
        char[] temp = new char[2048];
        int length = 0;
        try {
            length = bfreader.read(temp);
            buffer.append(temp,0,length);   
            requestString = buffer.toString();

        }catch(IOException e) {
            e.printStackTrace();
        }finally {
            // 输出request
            System.out.println("request为:");
            System.out.println(requestString);

            return requestString;
        }
    }

    // 根据请求报文的特点,请求的文件在第一个和第二个空格之间。所以有了以下方法
    public void parseUri() {
        String request = requestToString();
        if(request != null) {
            int space1 = -1;
            int space2 = -1;
            space1 = request.indexOf(' ');
            if(space1 != -1) {
                space2 = request.indexOf(' ',space1 + 1);
            }
            if(space2 > space1) {
                // 截取第一个和第二个空格之间的字符串,即请求资源的uri
                uri = request.substring(space1 + 1, space2);
            }
        }
    }

    // 返回请求资源的uri
    public String getUri() {
        return uri;
    }
}
  • Response类(处理Socket的输出流)
package tomtiger;
/**
 * @author liuleilei liuleilei2015@gmail.com
 * @date 2017年11月20日 下午4:37:49
 * @Description: implement the response of connection
 */
import java.io.OutputStream;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;

public class Response {
    // 定义读取文件时byte[] 数组的大小
    private static final int BUFFER_SIZE = 1024;
    // 用来响应的outputstream,该输出流从 为该请求建立的socket中获得,并传入
    OutputStream outputstream = null;

    // 用outputstream初始化response
    public Response(OutputStream outputstream) {
        this.outputstream = outputstream;
    }

    // 该请求资源的文件路径
    public void responseResource(String uri) {
        // 读取文件时的byte[]
        byte[] resourcetemp = new byte[BUFFER_SIZE];
        // 输入流
        FileInputStream fileinputstream = null;
        if(uri == null)
            return;
        try {
            // 建立文件
            File resource = new File(Tomtiger.WEB_ROOT,uri);
            // 判断文件是否存在
            if(resource.exists()) {

                System.out.println("请求的资源是:" + resource.getName());

                fileinputstream = new FileInputStream(resource);
                int length = 0;

                String responsehead = "HTTP/1.1 200 OK\r\n" +  "\r\n";
                outputstream.write(responsehead.getBytes());

                while((length = fileinputstream.read(resourcetemp)) > 0) {
                    outputstream.write(resourcetemp, 0, length);
                }
            }
            else {
                String errorPage = "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>";
                outputstream.write(errorPage.getBytes());
            }

            outputstream.close();
        }catch(IOException e){
            e.printStackTrace();
        }finally {
            outputstream = null;
            fileinputstream = null;
        }
    }
}

3. 执行

  • 控制台
  • java多线程启动tcpserver_java

  • 浏览器
  • java多线程启动tcpserver_套接字编程_02