计算机网络实验:Linux下C语言实现Web服务器

总述

本项目实现了Linux系统下一个简易Web服务器要求:

  1. 当一个客户(浏览器)连接时,创建一个连接套接字;
  2. 从这个连接套接字接收 HTTP 请求;
  3. 解释该请求以确定所请求的特定文件;
  4. 从服务器的文件系统获得请求的文件;
  5. 创建一个由请求的文件组成的 HTTP 响应报文,报文前面有首部行;
  6. 经 TCP 连接向请求浏览器发送响应;
  7. 如果浏览器请求一个在该服务器中不存在的文件,服务器应当返回一个“404 Not Found”差错报文。

只支持http1.0和get方法


技术实现

网页访问流程

启动服务器,监听特定端口,接收客户端http请求,解析客户端http请求,根据url返回特定资源。

socket流程

首先自然是创建一个服务器套接字,使其能够接收客户端的请求

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

#define PORT 8880
#define BUFFER_SIZE 5120
#define MAX_CLIENTS 100
#define FILE_SIZE 4096

int main(int argc, char *argv[]){
    int server_sock, client_sock, *new_sock;
    struct sockaddr_in server, client;
    socklen_t client_len = sizeof(client);
    
    server_sock = socket(AF_INET, SOCK_STREAM, 0);
    
    if(server_sock == -1){
        perror("Could not create socket");
        return 1;
    }

    server.sin_family = AF_INET;
    server.sinaddr.s_addr = INADDR_ANY;
    server.sinport = htons(PORT);
    if(bind(server_sock, (struct sockaddr *)&server, sizeof(server)) < 0){
        perror("Bind failed");
        return 1;
    }
    
    listen(server_sock, MAX_CLIENTS);
    
    //...
}

AF.INET表示能够接收ipv4,INADDR_ANY表示接收任何地址的客户端连接,htons则将端口号转换成网络字节,然后用bind函数将服务器套接字和端口绑定起来,再调用listen,使系统开始监听服务器套接字的端口,等待客户端的连接。

接收客户端连接,解析http请求并返回资源

while (1) {
        
    client_sock = accept(server_sock, (struct sockaddr *)&client, &client_len);
    if (client_sock < 0) {
        perror("Accept failed");
        return 1;
    }

    pthread_t thread_id;
    new_sock = malloc(1);
    *new_sock = client_sock;


    if (pthread_create(&thread_id, NULL, connection_handler, (void*) new_sock) < 0)                         {
        perror("Could not create thread");
        return 1;
    }
}

服务器每接收到一个客户端,则分配到客户端套接字client_sock中,然后使用pthread创建一个线程处理来处理请求。将client_sock分配到一个新的套接字new_sock中,使得client_sock可以继续接收其他客户端的请求。

pthread_t 定义了一个新的线程id,pthread_create则利用这个线程id创建一个新的线程,而第三个参数则是该线程执行的函数,第四个参数就是执行函数的参数。pthread编程可以参考下面这篇文章:

Linux多线程编程:pthread线程创建、退出、回收、分离、取消

connection_handler函数

void handle_http_request(int sock, char* buffer);

void *connection_handler(void *socket_desc) {
    int sock = *(int*)socket_desc;
    char buffer[BUFFER_SIZE];
    memset(buffer, 0, sizeof(buffer));//初始化缓冲区
    ssize_t n = read(sock, buffer, sizeof(buffer) - 1);//读取套接字到缓冲区
    if (n <= 0) {
        perror("Read failed");
        return 0;
    }
    puts(buffer);//输出到控制台
    handle_http_request(sock, buffer);//处理缓冲区中的数据
    write(sock, buffer, strlen(buffer));//将缓存区数据返回给客户端
    close(sock);
    free(socket_desc);

    return 0;
}

void handle_http_request(int sock, char* buffer) {
    
    // 解析请求方法和请求路径
    char method[50], path[255];
    sscanf(buffer, "%[^ ] %[^ ]", method, path);//按空格匹配方法,url字符串

    // 处理GET请求
    if (strcmp(method, "GET") == 0) {
        if (strcmp(path, "/") == 0) {//如果只有"/"表示访问默认页面,我设置为index文件
        FILE *file = fopen("index", "r");//直接在当前文件夹打开index文件
            if (file == NULL) {
                sprintf(buffer, "HTTP/1.0 404 Not Found\r\n\r\n<h1>404 Not Found</h1>\n");
            } else {
                char file_buffer[FILE_SIZE];
                fread(file_buffer, sizeof(char), BUFFER_SIZE, file);
                sprintf(buffer, "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n%s", file_buffer);
                fclose(file);
            }
        }else{
            FILE *file = fopen(path + 1, "r");
            if (file == NULL) {
                sprintf(buffer, "HTTP/1.0 404 Not Found\r\n\r\n<h1>404 Not Found</h1>\n");
            } else {
                char file_buffer[FILE_SIZE];
                fread(file_buffer, sizeof(char), BUFFER_SIZE, file);
                sprintf(buffer, "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n%s", file_buffer);//读取文件内容,放入缓冲区
                fclose(file);
            }
        }   


    }
    // 不支持的请求方法
    else {
        const char *response = "HTTP/1.0 405 Method Not Allowed\r\n\r\n";
        write(sock, response, strlen(response));
    }
}

最后,由于最大连接数不超过100,需要设置临界资源的互斥使用

在main函数中初始化锁lock,然后在while循环中加锁,判断临界资源client_count是否达到100,没有达到100再分配线程,将client_count++,每次connection_handle处理完一个连接就将client_count--,还要释放资源sock。具体代码见源码。


运行效果

项目结构

运行示例

访问不存在的页面

至于访问的文件内容,大家可以自己写html代码试试。


源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

#define PORT 8880
#define BUFFER_SIZE 5120
#define MAX_CLIENTS 100
#define FILE_SIZE 4096

int client_count = 0;
pthread_mutex_t lock;
void handle_http_request(int sock, char* buffer);

void *connection_handler(void *socket_desc) {
    int sock = *(int*)socket_desc;
    char buffer[BUFFER_SIZE];
    memset(buffer, 0, sizeof(buffer));
    ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
    if (n <= 0) {
        perror("Read failed");
        return 0;
    }
    puts(buffer);
    handle_http_request(sock, buffer);
    write(sock, buffer, strlen(buffer));
    close(sock);
    free(socket_desc);

    pthread_mutex_lock(&lock);
    client_count--;
    pthread_mutex_unlock(&lock);

    return 0;
}

void handle_http_request(int sock, char* buffer) {
    
    // 解析请求方法和请求路径
    char method[50], path[255];
    sscanf(buffer, "%[^ ] %[^ ]", method, path);

    // 处理GET请求
    if (strcmp(method, "GET") == 0) {
        if (strcmp(path, "/") == 0) {
        FILE *file = fopen("index", "r");
            if (file == NULL) {
                sprintf(buffer, "HTTP/1.0 404 Not Found\r\n\r\n<h1>404 Not Found</h1>\n");
            } else {
                char file_buffer[FILE_SIZE];
                fread(file_buffer, sizeof(char), BUFFER_SIZE, file);
                sprintf(buffer, "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n%s", file_buffer);
                fclose(file);
            }
        }else{
            FILE *file = fopen(path + 1, "r");
            if (file == NULL) {
                sprintf(buffer, "HTTP/1.0 404 Not Found\r\n\r\n<h1>404 Not Found</h1>\n");
            } else {
                char file_buffer[FILE_SIZE];
                fread(file_buffer, sizeof(char), BUFFER_SIZE, file);
                sprintf(buffer, "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n%s", file_buffer);
                fclose(file);
            }
        }   


    }
    // 不支持的请求方法
    else {
        const char *response = "HTTP/1.0 405 Method Not Allowed\r\n\r\n";
        write(sock, response, strlen(response));
    }
}

int main(int argc, char *argv[]) {
    int server_sock, client_sock, *new_sock;
    struct sockaddr_in server, client;
    socklen_t client_len = sizeof(client);

    pthread_mutex_init(&lock, NULL);

    server_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock == -1) {
        perror("Could not create socket");
        return 1;
    }

    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons(PORT);

    if (bind(server_sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
        perror("Bind failed");
        return 1;
    }

    listen(server_sock, MAX_CLIENTS);

    while (1) {
        pthread_mutex_lock(&lock);
        if (client_count >= MAX_CLIENTS) {
            pthread_mutex_unlock(&lock);
            continue;
        }
        pthread_mutex_unlock(&lock);

        client_sock = accept(server_sock, (struct sockaddr *)&client, &client_len);
        if (client_sock < 0) {
            perror("Accept failed");
            return 1;
        }

        pthread_t thread_id;
        new_sock = malloc(1);
        *new_sock = client_sock;

        pthread_mutex_lock(&lock);
        client_count++;
        pthread_mutex_unlock(&lock);

        if (pthread_create(&thread_id, NULL, connection_handler, (void*) new_sock) < 0) {
            perror("Could not create thread");
            return 1;
        }
    }

    pthread_mutex_destroy(&lock);

    return 0;
}
  • 23
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值