逐步构建HTTP服务器(一)——构建一个能返回HTTP报文的服务器

HTTP | MDN (https://developer.mozilla.org/zh-CN/docs/Web/HTTP)

我们知道HTTP依赖于面向连接的TCP进行消息传递。所以我们实际上是要构建一个能够接受TCP连接并通过TCP发送HTTP报文给用户浏览器的服务器。

如何构建一个TCP服务器?

《UNIX网络编程》 第四章 基本TCP套接字编程

作为服务器,我们要做的就是:1.接受TCP连接请求,2.接收客户发的数据(即HTTP请求报文),3.并回传数据(即HTTP响应报文),4.随后断开该连接。

一个典型的TCP客户端和服务器交互过程以及用到的函数:

  1. 接受TCP连接请求

    // 使用socket()获得一个TCP套接字
    // AF_INET      IPv4
    // SOCK_STREAM  字节流套接字
    // IPPROTO_TCP  TCP传输协议
    int listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    
    
    // 准备一个网际网套接字地址,该地址主要包括协议族、主机地址和端口
    // 我们准备接受所有主机的连接,所以赋的是通配地址(INADDR_ANY)
    // 端口是8080,之后我们将通过这个端口访问我们的服务器
    sockaddr_in servaddr;
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(8080);
    
    // 将TCP套接字和网际网套接字地址绑定在一起
    bind(listenfd, (sockaddr *) &servaddr, sizeof(servaddr));
    
    // listen()将该套接字转换成一个监听套接字
    // LISTENQ为连接完成+正在连接的连接数的最大值
    listen(listenfd, LISTENQ);
    
    while (1) {
        // accept()用于接受连接,并返回一个文件描述符代表与所返回的客户的TCP连接,之后与客户的通信(read/write/close)都将通过这个描述符进行
        // accept()会一直阻塞在这里,直至有新的连接建立
        // cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址
        sockaddr_in cliaddr;
        socklen_t clilen;   // socklen_t aka unsigned int
        connfd = accept(listenfd, (sockaddr *) &cliaddr, &clilen);
    
  2. 接收客户发的数据(即HTTP请求报文)

        char buf[MAXLINE];
        // read()用来接收数据,并返回收到数据的字节数
        // 若用户没有发送数据,将一直阻塞在这里
        int n = read(connfd, buf, MAXLINE);
    

    在这里为了简单,我们不对HTTP请求报文做解析,也就是说对任何的HTTP请求都返回相同数据。

  3. 并回传数据(即HTTP响应报文)

        // write()用来向客户发送数据
        // write()也会阻塞在此直至数据发送完成
        write(connfd, package.c_str(), package.size());
    

    这里package是c++的string对象。
    我们需要先了解一下HTTP响应报文:

    我们根据格式写了一个响应报文:

    std::string header = "HTTP/1.1 200 OK\r\nServer: myhttpd\r\nContent-Type: text/html\r\n";
    std::string body =
        "<!DOCTYPE html>\r\n<html>\r\n  <head>\r\n    <title>Welcome to myhttpd!</title>\r\n    <style>\r\n      body {\r\n        width: 35em;\r\n        margin: 0 auto;\r\n        font-family: Tahoma, Verdana, Arial, sans-serif;\r\n      }\r\n      span:nth-child(1) {\r\n        color: red;\r\n      }\r\n      span:nth-child(2) {\r\n        color: orange;\r\n      }\r\n      span:nth-child(3) {\r\n        color: yellow;\r\n      }\r\n      span:nth-child(4) {\r\n        color: green;\r\n      }\r\n      span:nth-child(5) {\r\n        color: blue;\r\n      }\r\n      span:nth-child(6) {\r\n        color: blueviolet;\r\n      }\r\n      span:nth-child(7) {\r\n        color: purple;\r\n      }\r\n    </style>\r\n  </head>\r\n  <body>\r\n    <h1>\r\n      Welcome to <span>m</span><span>y</span><span>h</span><span>t</span\r\n      ><span>t</span><span>p</span><span>d</span>!\r\n    </h1>\r\n    <p>\r\n      If you see this page, the myhttpd web server is successfully completed.\r\n    </p>\r\n\r\n    <p>\r\n      For more information please refer to\r\n      <a href=\"http://github.com/ithepug/myhttpd\">myhttpd</a>.<br />\r\n    </p>\r\n\r\n    <p><em>Thank you for using myhttpd.</em></p>\r\n  </body>\r\n</html>\r\n";
    
    std::string package = header + "\r\n" + body;
    
  4. 随后断开该连接

        close(connfd);
    }
    

需要注意的地方

  1. 1.接受TCP连接请求,2.接收客户发的数据,3.并回传数据,4.随后断开该连接,四个步骤都在一个while(1)中。

    while(1)
    {
        accept();
        read();
        write();
        close();
    }
    

    这样我们无意中实现了一个服务器可以连接多个客户端,虽然只是每一时刻与一个客户端连接。

  2. 我们获得的套接字是阻塞的。

    若一个套接字是阻塞的,就意味着对它进行输入操作(read)、输出操作(write)、接受外来连接(accept)和发起外出连接(connect),都是阻塞的!

成果

运行程序,在浏览器地址栏输入:http://localhost:8080/ ,便可得到下面页面

遗憾

  1. 每次客户想连接到服务器必须等上一个客户完成连接建立、发送数据、接受数据和断开连接的过程。
posted @ 2021-08-02 11:43  ithepug  阅读(113)  评论(0编辑  收藏  举报