Osheep

时光不回头,当下最重要。

跟我一起来用C++写Web服务器吧

获取本服务器完整的源码
首先,要实现一个Web服务器,必须要有一定的理论基础,才知道要做什么事情(如何写代码)

理论知识

首先,我们要知道什么是客户端和服务器

客户端

指与服务器相对应,为客户提供本地服务的程序。除了一些只在本地运行的应用程序之外,一般安装在普通的客户机上,需要与服务端互相配合运行               --摘自百度百科

举个例子,我们常常用的浏览器就是一个客户端

服务器

与客户端相对的,提供服务的。一般服务器上面都放有客户端需要的资源。当客户端请求服务器上的某些资源的时候,如果服务器允许的话,那么服务器可以返回这些资源给客户端。例如,浏览器去访问www.baidu.com的时候,百度的服务器就会返回一个html文件给浏览器,然后经过浏览器的渲染,呈现给用户一个美丽的页面

客户端与服务器进行交流的本质

我们知道,无论是客户端的程序还是服务器的程序,本质都是程序。那么当程序跑起来之后,就是进程了。而进程之间进行交流就需要使用进程通信的一些方法了。并且,这两个进程是处于不同的位置对吧(可能在世界的两端),不是简单的由父进程fork一下,然后使用类似于管道、信号之类的进程通信手段就可以完成通信的。既然它们是在网络中的,就需要遵循网络协议

网络协议

HTTP是一个客户端和服务器端请求和应答的标准协议。通常,由HTTP客户端发起一个请求,建立一个到服务器指定端口(默认是80端口)的TCP连接

因此我们要做的工作就是利用Linux系统提供的TCP通信接口来实现HTTP协议。此时我们用到的就是socket(套接字)

每一对网络连接称为一个socket对,包括两个端点的socket地址

socket处理请求与响应示意图

《跟我一起来用C++写Web服务器吧》

我们接下来要写的代码就是围绕这幅图进行的

IP和端口号

IP是用来在互联网中寻找主机用的,端口号则是用来区分应用的。比如说,我要访问www.baidu.com这个网页,我是需要知道它的IP地址才能访问的,除此之外,我还必须知道端口号。为什么呢?我们可以把IP地址想象成,而我不可能和家通信吧,我还必须知道我要通信的这个人在哪里对吧,此时端口号就起了作用,用来标识哪个人

实现接受GET请求的功能

代码实现

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/unistd.h>
#include <netinet/in.h>
#include <fstream>

using namespace std;

const int BUFFSIZE = 1024;
const int MAXLINK = 10; // 未经过处理的连接请求队列可以容纳的最大数目
const int DEFAULT_PORT = 8080;

char* get_file_name (char* buff) {
    char* file_name = buff + 5;
    char *space = strchr(file_name, ' ');
    *space = '\0';
    return file_name;
}
 
void deal_get_http(int connect_fd, char* buff) {
    char* file_name = get_file_name(buff);
    const char http_correct_header[] = "HTTP/1.1 200 OK\r\nContent-type: text/html\r\n\r\n";
    int res = write(connect_fd, http_correct_header, strlen(http_correct_header));
    if (res > 0) {
        cout<<"send success"<<endl;
    }
}

bool is_get_http(char* buff) {
    if (!strncmp(buff, "GET", 3)) { // 如果是GET请求
        return true;
    }
    else {
        return false;
    }
}

int main(int argc, char const *argv[])
{


    int socket_fd, connect_fd;
    struct sockaddr_in servaddr;
    char buff[BUFFSIZE];

    socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd == -1) {
            cout<<"create socket error"<<endl;
            return -1;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP必须是网络字节序,INADDR_ANY是绑定本机上所有IP
    servaddr.sin_port = htons(DEFAULT_PORT); // 端口号必须是网络字节序
    
    if (bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
        cout<<"bind error"<<endl;
        return -1;
    }

    if (listen(socket_fd, MAXLINK) == -1) {
        cout<<"listen error"<<endl;
    }
    
    connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL);
    if (connect_fd == -1) {
        cout<<"accept error"<<endl;
    }
    else {
        cout<<"连接成功"<<endl;
    }

    memset(buff, '\0', sizeof(buff));

    recv(connect_fd, buff, BUFFSIZE - 1, 0); // 把请求头(或发送的消息)写入buff中
    send(connect_fd, buff, BUFFSIZE - 1, 0); // 向客户端发送消息(发送buff中的内容)

    cout<<"recive message from client: "<<buff<<endl;

    if (is_get_http(buff)) {
        cout<<"it is get http"<<endl;
        deal_get_http(connect_fd, buff);
    }

    close(connect_fd);  
    close(socket_fd);

    return 0;
}

好的,到这里,我们就已经写完了一个可以接收GET请求的服务器了,我们来测试一下:

《跟我一起来用C++写Web服务器吧》

在上面,我们只是模拟了使用telnet发送一个GET请求,这个请求去请求index.html文件,然后我们返回一部分响应头给客户端,此时并没有返回index.html的内容给客户端。接下来,我们就来完成这部分功能,一步一步的让这个服务器健全起来。

返回请求文件的内容

代码实现

void deal_get_http(int connect_fd, char* request_header) {
    char* file_name = get_file_name(request_header);
    int file_size = get_file_size(file_name);
    char file_type[BUFFSIZE];
    get_filetype(file_name, file_type);

    int fd = open(file_name, O_RDONLY);
    void* file_in_mem_addr = mmap(0, file_size, PROT_READ, MAP_PRIVATE, fd, 0); // 存储映射
    close(fd);

    char buff[BUFFSIZE];
    strcat(buff, "HTTP/1.0 200 OK\r\n");
    strcat(buff, "Server: HHTWS Web Server\r\n");
    strcat(buff, "Connection: close\r\n");
    strcat(buff, "Content-length: \r\n");
    strcat(buff, "Content-type: \r\n\r\n");
    
    send(connect_fd, buff, strlen(buff), 0);
    send(connect_fd, file_in_mem_addr, file_size, 0);

    munmap(file_in_mem_addr, file_size);
}

我们来看看效果:

《跟我一起来用C++写Web服务器吧》

存储映射机制

除了标准文件I/O,内核提供了另一种高级的I/O方式,允许应用程序将文件映射到内存中,即内存和文件中数据是一一对应的。程序员可以直接通过内存来访问文件,就像操作内存的数据块一样,甚至可以写入内存数据区,然后通过透明的映射机制将文件写入磁盘。

当映射一个文件描述符的时候,描述符引用计数增加。如果映射文件后关闭文件,你的进程依然可以访问该文件。当你取消映射或者进程终止时,对应的文件引用计数会减1。

遇到的问题

读取整个文件

开始的时候是想到了常规的方法,也就是使用fgets()函数等等,但是,因为它会给每一个字符串加上’\0’。所以当我使用它的时候,出现了一些乱码。之后我突然想起了我最近在《Linux系统编程》中看到的一个存储映射机制,于是就用上了它。非常方便的把文件的内容获取到了。

Segmentation fault问题

我在运行这个服务器的时候,出现了这个错误。最后经过排查,发现是由于存在野指针。开始的时候,我是这样定义一个文件类型的:

char* file_type;

然后我就直接把这个野指针给传进get_filetype函数里面去了。导致问题发生。

好了,到这里,我们就写完了一个可以说是完整的支持GET请求的Web服务器了。那小伙伴们是不是想在浏览器里面试一试效果!!看看能不能看到这个HTML文件被浏览器渲染出来!!

我们来试试!!

《跟我一起来用C++写Web服务器吧》

咦( ′◔ ‸◔`)?为什么不能在浏览器上面看到呢?小伙伴们,我将在下面一篇文章中继续为大家讲解。happy ending

(未完–持续更新加入新功能中)

点赞