手写webserver服务器

手写webserver服务器

前言

webserver 服务器是网络通信必不可少的工具,手写web服务器有助于我们理解网络通信和tomcat这类服务器的执行流程。

手写Web服务器所需要的知识较多,大体包括:容器、io、多线程、反射、HTTP协议、xml解析等等,通过手写服务器能让我们更加熟练的掌握这些知识,同时这也是一个挑战。


一、web server执行流程

我们这个简易版的web服务器大致分为14个流程,具体如下图
在这里插入图片描述

是不是感jio特别熟悉,没错这个图我就是仿照Spring MVC执行流程来画的。为了便于理解,其中的一些命名也是和Spring MVC类似,但是请记住这是webserver的执行流程

  • 浏览器发送http请求给服务器,服务器通 Server 建立连接,将请求交给Dispatcher 处理
  • Dispatcher 启动多线程与客户端进行交互,将请求信息交给 Request 进行解析,得到请求方法、请求URL、请求参数
  • Dispatcher 根据得到的URL,调用WebAppgetServletFromUrl 方法反射创建相应的Servlet
  • 执行对应的Servlet ,Servlet 添加响应信息,Response 将信息交给 ResponseReslove 处理得到规范格式的响应的信息
  • Dispatcher调用 ResponsepushToClient方法将响应结果响应给用户

组件说明

  • Server
    管理与客户端的连接,是整个webserver的入口

  • Dispatcher 控制器
    整个流程的控制中心,控制其他组件处理请求

  • Request
    解析HTTP请求,获得请求方法、请求URL、请求参数

  • WebApp 处理映射器
    通过调用WebHandler处理解析web.xml文件,WebContext获得类路径反射创建Servlet对象

  • WebHandler 配置文件处理器
    处理解析web.xml文件

  • WebContext
    将实体类封装为map

  • Entity
    web.xml文件中servlet节点实体

  • Mapping
    web.xml文件中servlet-mapping节点

  • Servlet
    Servlet为抽象类,包含service方法

  • Response
    封装响应信息、包括响应头、响应正文、响应行

  • ResponseHandler
    解析html文件,将其转化为字符串

项目地址

我已经将源代码上传至 github地址,各位大佬可以给我一个小星星吗

二、代码实现

Server 建立与客户端连接端口号为 8888 ip地址为localhost,start()启动服务,receive()接收连接处理

public class Server {
	//是否启动
    private boolean isRunning=true;
    ServerSocket serverSocket=null;
    public static void main(String[] args)  {
        Server server = new Server();
        server.start();
    }
    //启动服务
    public void start() {
        //创建连接
        try {
            serverSocket = new ServerSocket(8888);
            receive();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("服务器启动失败...");
        }
    }
    //接收连接处理
   public void receive(){
        while(isRunning){
       try {
           Socket client = serverSocket.accept();
           System.out.println("一个客户端建立了连接");
          new Thread(new Dispatcher(client)).start();
       } catch (IOException e) {
           e.printStackTrace();
           System.out.println("客户端连接失败...");
       }
   }
    }
    //停止服务
    public void stop(){
        isRunning=false;
        CloseUtil.closeIo(serverSocket);
    }
}

Dispartcher控制其他组件完成请求响应,完成首页跳转、404页面、500页面和成功页面的跳转

public class Dispatcher implements Runnable{
    private Socket client;
    private  Request request;
    private Response response;

    Dispatcher(Socket client)  {
        this.client=client;
        //获取请求协议
        try {
            request = new Request(client);
            response=new Response(client);
        } catch (IOException e) {
            CloseUtil.closeIo(client);
        }

    }
    @Override
    public void run() {
        //返回响应信息
        try {
            //首页
            if(request.getUrl().equals("")){
                response.printPath("index.html");
                CloseUtil.closeIo(client);
                return ;
            }
            Servlet servlet = WebApp.getServletFromUrl(request.getUrl());
            if(servlet!=null){
                servlet.service(request,response);
                response.pushToClient(200);
            }else {
                //错误 404 not found
                response.printPath("404.html");
                //为响应添加状态码
                response.pushToClient(404);
            }
        }catch (Exception e){
            e.printStackTrace();
            try {
                //500 error
                response.printPath("500.html");
                response.pushToClient(500);
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
        CloseUtil.closeIo(client);

    }
}

Request:解析http请求,对于post 和 get 不同的方法解析的位置不一样

get请求格式,我们只需要关注第一行:也就是请求行
在这里插入图片描述

post请求格式 我们只需要关注post中的请求行和最后一行请求参数
在这里插入图片描述
Request代码

public class Request {
    //请求方式
    private String method;
    //请求资源
    private String url;
    //请求参数
    private Map<String, List<String>> paramMap=new HashMap<>();
//    private ;
    private final String CRLF="\r\n";
    private  String requestInfo;
    private InputStream is;

    public Request(Socket client) throws IOException {
        this(client.getInputStream());
    }
    public Request(InputStream is){
        this.is=is;
        //获取请求协议
        byte[] datas=new byte[1024*1024];
        int len= 0;
        try {
            len = is.read(datas);
            requestInfo=new String(datas,0,len);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        //分解字符串
        parseRequestInfo();

    }

    /**
     * 分析请求信息
     */
    private void parseRequestInfo() {
        String paramString =""; //接收请求参数
       if(requestInfo==null||(requestInfo=requestInfo.trim()).equals("")){
           return ;
       }
       //获取请求方式
        String firstLine=requestInfo.substring(0,requestInfo.indexOf(CRLF));
        // /的位置
        int idx=firstLine.indexOf("/");
       method=firstLine.substring(0,idx).trim();
       //获取请求url和参数
        String urlStr=firstLine.substring(idx+1,firstLine.indexOf("HTTP/")).trim();
        System.out.println("urlStr=="+urlStr);
        //判断方法是get 还是 post
        if(method.equalsIgnoreCase("post")){
            url=urlStr;
            //请求参数
            paramString=requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
        }else if(method.equalsIgnoreCase("get")){
            //是否含有参数
            if(urlStr.contains("?")){
                url=urlStr.substring(0,urlStr.indexOf("?"));
                paramString=urlStr.substring(urlStr.indexOf("?")+1).trim();
            }else {
                url=urlStr;
            }
        }
        //不存在请求参数
        if(paramString.equals("")){
            return ;
        }
        //将请求参数封装到Map
        parseParams(paramString);
    }

    /**
     * 将请求参数封装到Map
     * @param paramString
     */
    private void parseParams(String paramString) {
        //分割 将字符产转成数组
        StringTokenizer token = new StringTokenizer(paramString, "&");
        while(token.hasMoreTokens()){
            String keyValue=token.nextToken();
            String[] split = keyValue.split("=");
            split = Arrays.copyOf(split, 2);
            //获取key value
            String key=split[0];
            String value=split[1]==null?null:decode(split[1],"utf-8");
            //存储到map中
            if(!paramMap.containsKey(key)){
                paramMap.put(key,new ArrayList<String>());
            }
            paramMap.get(key).add(value);

        }
    }

    /**
     * 通过name 获取多个值value
     * @param key
     * @return
     */
    public String[] getParameterValues(String key){
        if(!paramMap.containsKey(key)){
            return null;
        }
        List<String> values=paramMap.get(key);
        return  values.toArray(new String[0]);
    }
    private String decode(String value,String code)  {
        try {
          return  java.net.URLDecoder.decode(value,code);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 通过name 获取一个值value
     * @param key
     * @return
     */
    public String getParameterValue(String key){
        String[] values = getParameterValues(key);
        return values==null?null:values[0];
    }

    /**
     * 关闭资源
     */
    public void close(){
        CloseUtil.closeIo(is);
    }
    public String getMethod() {
        return method;
    }

    public String getUrl() {
        return url;
    }

    public Map<String, List<String>> getParamMap() {
        return paramMap;
    }
}

WebApp解析web.xml文件,通过获得当前请求的URL匹配到web.xml文件中相应的servlet-mapping 通过servlet-mapping 中的name 获得 servlet中的servlet-class ,得到servlet-class就可以反射创建servelt对象。其中通过webhandler解析配置文件,webContext将解析到的配置文件转换为Map

public class WebApp {
   private static WebContext webContext;
    static {
        try {
            //1、获取解析工厂
            SAXParserFactory factory =SAXParserFactory.newInstance();
            //2、获取解析器
            SAXParser sax =factory.newSAXParser();
            //3、指定xml+处理器
            WebHandler handler = new WebHandler();
            sax.parse(Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream("web.xml"),handler);
            webContext = new WebContext(handler.getEntityList(),handler.getMappingList());
        }catch (Exception e){
            e.printStackTrace();
        }

    }

    /**
     * 通过url获得servlet
     * @param url
     * @return
     */
    public static Servlet getServletFromUrl(String url) throws Exception {
    //反射创建servlet对象
        String clzName = webContext.getClz("/"+url);
        if(clzName!=null){
        Class clz = null;
            clz = Class.forName(clzName);
            Servlet servlet= (Servlet) clz.getConstructor().newInstance();
            return servlet;
        }
        return null;
    }

}

WebContext 将实体类封装为Map<String,String>

public class WebContext {

    private List<Entity> entityList = null;
    private List<Mapping> mappingList = null;

    //key->servlet-name v->servlet-class
    private Map<String, String> entityMap = new HashMap<>();
    //key->url-pattern  v->servlet-name
    private Map<String, String> mappingMap = new HashMap<>();

    public WebContext(List<Entity> entityList, List<Mapping> mappingList) {
        this.entityList = entityList;
        this.mappingList = mappingList;
        //将entity的list转换成了对应的map
        for (Entity entity : entityList) {
            entityMap.put(entity.getName(), entity.getClz());
        }
        //将mapping的list转换成了对应的map
        for (Mapping mapping : mappingList) {
            for (String pattern : mapping.getPattern()) {
                mappingMap.put(pattern, mapping.getName());
            }

        }
    }
        public String getClz (String pattern){
            String s = mappingMap.get(pattern);
            String clz = entityMap.get(s);
            return clz;
        }

}

WebHandler 使用SAX解析web.xml,web.xml文件格式有严格要求 web.xml格式见下文

public class WebHandler extends DefaultHandler {
    private List<Entity> entityList;
    private List<Mapping> mappingList;
    private tjoker.server.core.Entity entity;
    private tjoker.server.core.Mapping mapping;
    private boolean isMapping;
    private String tag;//存储操作表示符

    @Override
    public void startDocument() throws SAXException {
        //文档解析开始初始化
        entityList=new ArrayList<>();
        mappingList=new ArrayList<>();
    }

    @Override
    public void endDocument() throws SAXException {
        //文档结束

    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        if(qName!=null){
            tag=qName;
            if(qName.equals("servlet")){
                isMapping=false;
                entity = new Entity();
            }else if(qName.equals("servlet-mapping")){
                isMapping=true;
                mapping = new Mapping();
            }
        }
    }
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {

        if(tag!=null){
                        String str =new String(ch,start,length).trim();
                        if(isMapping==false){
                            if(tag.equals("servlet-name")){
                                entity.setName(str);
                            }else if(tag.equals("servlet-class")){
                                entity.setClz(str);
                }
            }else {
                if(tag.equals("servlet-name")){
                    mapping.setName(str);
                }else if(tag.equals("url-pattern")){
                    mapping.addPattern(str);
                }
            }
        }
    }
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if(qName!=null){
            if(qName.equals("servlet")){
                entityList.add(entity);
            }else if(qName.equals("servlet-mapping")){
                mappingList.add(mapping);
            }

        }
        tag=null;
        }



    public List<Entity> getEntityList() {
        return entityList;
    }

    public List<Mapping> getMappingList() {
        return mappingList;
    }

    public Entity getEntity() {
        return entity;
    }

    public Mapping getMapping() {
        return mapping;
    }

}

Entity 对web.xml节点的servlet 其中 servlet-name–>name servlet-class–>clz

public class Entity {
    private String name;
    private String clz;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getClz() {
        return clz;
    }

    public void setClz(String clz) {
        this.clz = clz;
    }
}

Mapping 对应 web.xml文件中的servlet-mapping 节点

public class Mapping {
    private String name;
    private Set<String> pattern;

    public Mapping() {
        pattern=new HashSet<>();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set<String> getPattern() {
        return pattern;
    }

    public void setPattern(Set<String> pattern) {
        this.pattern = pattern;
    }
    public void addPattern(String pattern){
        this.pattern.add(pattern);
    }
}

Servlet 抽象方法

public abstract class Servlet {
    public void service(Request req,Response rep) throws Exception{
        this.doGet(req,rep);
        this.doPost(req,rep);
    }

    protected abstract void doGet(Request req,Response rep) throws Exception;
    protected abstract void doPost(Request req,Response rep) throws Exception;
}

Response 封装响应头、响应行、响应正文,其中每一个都有严格的格式要求

在这里插入图片描述

public class Response {
    //常量
    private final String CRLF="\r\n";
    private final String BLANK=" ";
    //响应头
    private StringBuilder headInfo;
    //响应内容
    private StringBuilder context;
    //流
   private BufferedWriter bw;
    //长度
    int len;
    private Response(){
        context=new StringBuilder();
        headInfo=new StringBuilder();
        len=0;
    }
    public Response(Socket client){
        this();
        try {
            bw=new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
        } catch (IOException e) {
            e.printStackTrace();
            headInfo=null;
        }
    }
    public Response(OutputStream os){
      this();
      bw=new BufferedWriter(new OutputStreamWriter(os));
    }

    /**
     * 响应正文,传递文件名
     * @param fileName
     * @return
     */
    public Response printPath(String fileName)  {
        //通过response处理响应信息
        ResponseResolver responseHandler = new ResponseResolver(fileName);
        String s = responseHandler.getString();
            context.append(s).append(CRLF);
            len+=(s+CRLF).getBytes().length;
            return this;
    }
    /**
     * 响应正文+换行
     * @param info
     * @return
     */
    public Response println(String info){
        context.append(info).append(CRLF);
        len+=(info+CRLF).getBytes().length;
        return this;
    }

    /**
     * 创建头部信息
     * @param code
     */
    private void createHeader(int code){
        //1)  HTTP协议版本、状态代码、描述
        headInfo.append("HTTP/1.1").append(BLANK).append(code).append(BLANK);
        switch(code){
            case 200:
                headInfo.append("OK");
                break;
            case 404:
                headInfo.append("NOT FOUND");
                break;
            case 500:
                headInfo.append("SEVER ERROR");
                break;
        }
        headInfo.append(CRLF);
        //2)  响应头(Response Head)
        headInfo.append("Server:tjoker Server/0.0.1").append(CRLF);
        headInfo.append("Date:").append(new Date()).append(CRLF);
        headInfo.append("Content-type:text/html;charset=utf-8").append(CRLF);
        //正文长度 :字节长度
        headInfo.append("Content-Length:").append(len).append(CRLF);
        headInfo.append(CRLF); //分隔符
    }

    /**
     * 推送到客户端
     */
    void pushToClient(int code) throws IOException {
        if(headInfo==null){
            code=500;
        }
        createHeader(code);
        //头信息+分隔符
        bw.append(headInfo.toString());
        //正文信息
        bw.append(context.toString());
        bw.flush();

    }
    /**
     * 关闭资源
     */
    public void close(){
        CloseUtil.closeIo(bw);
    }
}

ResponseResolver

public class ResponseResolver {
    private StringBuilder sb;
  public ResponseResolver(String fileName) {
      //解析路径
      File file=new File("src/main/java/tjoker/server/servlet/"+fileName);
        InputStream is = null;
        try {
            is = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        if(is==null) {
            System.out.println("页面不存在");
            return;
        }
        System.out.println("页面存在");
        byte[] bytes= new byte[1024*1024];
        int lens=0;
         sb=new StringBuilder();
        while(true) {
            try {
                if (!(-1 != (lens = is.read(bytes)))) break;
            } catch (IOException e) {
                e.printStackTrace();
            }
            //输出  字节数组转成字符串
            String info = new String(bytes, 0, lens);
            sb.append(info);
        }

    }
    public String getString(){
      return sb.toString();
    }

}

CloseUtil 关闭io资源流

public class CloseUtil {
    /**
     * 关闭io流
     * @param io
     */
    public static void closeIo(Closeable... io){
        for(Closeable temp:io){
            try {
                if (null != temp) {
                    temp.close();
                }
            } catch (Exception e) {
                // TODO: handle exception
            }
        }
    }

}

三、 效果展示

我们需要新建两个html文件,一个login.html用于测试,一个success.html用于成功后跳转,同样我们需要一个LoginServlet用于逻辑处理
测试项目结构如下
在这里插入图片描述

新建一个login.html 文件,用于测试

<html>
	<head>
		<meta charset="UTF-8">
		<title>头部</title>
	</head>
	<body >
			<!--action为项请求的路径,在web.xml中进行配置-->
		<form method="post" action="http://localhost:8888/g">
			用户名:<input type="text" name="uname" id="uname"/>
			密码:<input type="password" name="pwd" id="pwd"/>
			<input type="submit" value="提交"/>
		</form>
	</body>
</html>

同样新建一个success.html文件,用于请求成功后跳转的页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body >
<div >
    <h2 style="width:360px;margin:36px auto;">欢迎来到</h2>
    <h3 style="width:580px;margin:0 auto;">TJOKER----手写简易版WEBSERVER</h3>
</div>

</body>
</html>

新建LoginServelt类进行逻辑处理,继承抽象父类Servlet 重写Service方法

public class LoginServlet extends Servlet {
    @Override
    public void service(Request request, Response response) {
        StringBuffer responseContext = new StringBuffer("登录成功了");
        response.printPath("index.html");
    }
    @Override
    protected void doGet(Request req, Response rep) throws Exception {
    }
    @Override
    protected void doPost(Request req, Response rep) throws Exception {
    }
}

现在万事具备,只欠 web.xml 文件,配置文件格式严有格规定,必须按照特定格式书写,否则会出现项目无法启动的情况。则写过web的同学应该知道。web.xml文件的位置也有特定要求,需放在webapp下或者资源目录下resourse
在这里插入图片描述

web.xml文件

<?xml version="1.0" encoding="UTF-8"?>

<web-app>
    <servlet>
        <!--登录-->
        <servlet-name>login</servlet-name>
        <servlet-class>tjoker.server.servlet.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>login</servlet-name>
        <url-pattern>/g</url-pattern>
        <url-pattern>/y</url-pattern>
    </servlet-mapping>
    <servlet>
        <!--注册-->
        <servlet-name>reg</servlet-name>
        <servlet-class>tjoker.server.servlet.RegServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>reg</servlet-name>
        <url-pattern>/r</url-pattern>
        <url-pattern>/x</url-pattern>
    </servlet-mapping>
</web-app>

到此你所有准备工作已经完成了,上线(上线前默念:佛祖保佑,无bug,无bug)
在这里插入图片描述
用颤抖的双手点击run运行Server类,用浏览器开打login.html文件
在这里插入图片描述
输入用户名,密码,点击提交按钮(佛祖保佑,永无bug)

页面成功跳转,,感谢bug ,感谢佛祖保佑。
在这里插入图片描述
页面简单,各位看官别嫌弃。到此你的webserver已经成功完成了,恭喜你又向大佬迈进了一步。


四、总结

项目地址:我已经将源代码上传到 githubhttps://github.com/Tjoker-cell/webserver.git,各位看官不要忘了给我标个小星星

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
编写一个C语言手写HTTP网站服务器需要以下步骤: 1. 创建一个TCP套接字,监听来自客户端的连接请求。 2. 接受客户端连接请求,并从客户端读取HTTP请求。 3. 解析HTTP请求,包括请求方法、请求路径、请求头和请求体等。 4. 根据请求的路径,读取网站文件并将其作为HTTP响应的主体返回给客户端。 5. 设置HTTP响应的状态码、响应头和响应体。 6. 将HTTP响应发送回客户端,并关闭连接。 以下是一个简单的C语言示例: ``` #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #define PORT 8080 #define MAX_SIZE 1024 int main() { int server_fd, new_socket, valread; struct sockaddr_in address; char buffer[MAX_SIZE] = {0}; char *response = "HTTP/1.1 200 OK\nContent-Type: text/html\n\n<html><body><h1>Hello, World!</h1></body></html>"; if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } if (listen(server_fd, 3) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } printf("Server listening on port %d...\n", PORT); while (1) { if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) { perror("accept failed"); exit(EXIT_FAILURE); } valread = read(new_socket, buffer, MAX_SIZE); printf("%s\n", buffer); write(new_socket, response, strlen(response)); close(new_socket); } return 0; } ``` 这个示例程序使用socket创建了一个TCP服务器,监听8080端口。当客户端连接时,它会接受连接请求并从客户端读取HTTP请求。然后,它会将“Hello, World!”作为HTTP响应的主体返回给客户端。这只是一个最基本的示例,实际的HTTP服务器需要更复杂的功能和错误处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值