WebServer 03

我们回到WebServer,继续看Config类中的parse_arg函数(命令行参数解析器)。

Config::Config(){
    //端口号,默认9006
    PORT = 9006;
​
    //日志写入方式,默认同步
    LOGWrite = 0;
​
    //触发组合模式,默认listenfd LT + connfd LT
    TRIGMode = 0;
​
    //listenfd触发模式,默认LT
    LISTENTrigmode = 0;
​
    //connfd触发模式,默认LT
    CONNTrigmode = 0;
​
    //优雅关闭链接,默认不使用
    OPT_LINGER = 0;
​
    //数据库连接池数量,默认8
    sql_num = 8;
​
    //线程池内的线程数量,默认8
    thread_num = 8;
​
    //关闭日志,默认不关闭
    close_log = 0;
​
    //并发模型,默认是proactor
    actor_model = 0;
}


void Config::parse_arg(int argc, char*argv[]){
    int opt;
    const char *str = "p:l:m:o:s:t:c:a:";
    while ((opt = getopt(argc, argv, str)) != -1)
    {
        switch (opt)
        {
        case 'p':
        {
            PORT = atoi(optarg);
            break;
        }
        case 'l':
        {
            LOGWrite = atoi(optarg);
            break;
        }
        case 'm':
        {
            TRIGMode = atoi(optarg);
            break;
        }
        case 'o':
        {
            OPT_LINGER = atoi(optarg);
            break;
        }
        case 's':
        {
            sql_num = atoi(optarg);
            break;
        }
        case 't':
        {
            thread_num = atoi(optarg);
            break;
        }
        case 'c':
        {
            close_log = atoi(optarg);
            break;
        }
        case 'a':
        {
            actor_model = atoi(optarg);
            break;
        }
        default:
            break;
        }
    }
}

我们先理解一下这个getopt函数做了什么。getopt函数是Linux/Unix系统中用来解析命令行参数的标准库函数。

函数原型:

int getopt(int argc,char *const argv[],const char *optstring)

argc和argv就不再赘述了,在01中学习过。

opstring的格式规则:单个字母表示选项,后面跟冒号表示需要参数

"abc" //-a,-b,-c选项都不需要参数
"a:b:c:" //-a,-b,-c选项都需要参数
"ab:c" //-a和-c不需要参数

它使用几个全局变量:(即这些变量在<unistd.h>中已经声明了)

optarg:当前选项的参数值

optind:下一个要处理的参数索引。初始值为1,解析时自动递增。

opterr:是否输出错误信息(默认为1,输出错误)

optopt:当getopt遇到错误时,optopt会存储导致错误的选项字符。当getopt遇到错误时,会返回"?".

例子:

#include <iostream>
#include <unistd.h>
​
​
int main(int argc, char *argv[]) {
    int opt;
    const char *str="a:b:c:";
    while ((opt = getopt(argc, argv, str)) != -1) {
        switch (opt) {
            case 'a':
                std::cout << "Option a with value: " << optarg << std::endl;
                break;
            case 'b':
                std::cout << "Option b with value: " << optarg << std::endl;
                break;
            case 'c':
                std::cout << "Option c with value: " << optarg << std::endl;
                break;
            case '?':
                if (optopt == 'a' || optopt == 'b' || optopt == 'c') {
                    std::cerr << "Option -" << static_cast<char>(optopt) << " requires an argument." << std::endl;
                } else {
                    std::cerr << "Unknown option: -" << static_cast<char>(optopt) << std::endl;
                }
                return 1;
            default:
                return 0;
        }
    }
    return 0;
}

编译:

g++ -std=c++20 -O1 main.cpp -o main

运行

./main -a cpp -b java -c golang

结果:

Option a with value:cpp
Option b with value:java
Option c with value:golang

所有参数解析完毕后,getopt函数返回-1.

-p:配置项为PORT(服务器端口号),例如-p 9006

-l:配置项为LOGWrite(日志写入模式),例如-l 1

-m:配置项为TRIGMode(触发模式),例如-m 3

-o:配置项为OPT_LINGER(优雅关闭选项)

-s:配置项为sql_num(数据库连接数)

-t:配置项为thread_num(线程池连接数)

-c:配置项为close_log(是否关闭日志)

-a:配置项为actor_model(并发模型)

atoi(optarg):将字符串参数转换为整数

这么一来,我们就可以使用多种方式启动服务器,不用重新编译就能改变服务器行为:

# 自定义端口和线程数
./server -p 8080 -t 16

解析过程:

第一次循环:opt='p',optarg="8080" -> PORT=8080

第二次循环:opt='t',optarg="16" -> thread_num=12

第三次循环:getopt返回-1,循环结束

总结:parse_arg的作用是解析启动程序时传入的命令行参数,用来配置WebServer的各种运行参数,提高了运行灵活性。

WebServer类

webserver.h:

#ifndef WEBSERVER_H
#define WEBSERVER_H
​
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <cassert>
#include <sys/epoll.h>
​
#include "./threadpool/threadpool.h"
#include "./http/http_conn.h"
​
const int MAX_FD = 65536;           //最大文件描述符
const int MAX_EVENT_NUMBER = 10000; //最大事件数
const int TIMESLOT = 5;             //最小超时单位
​
class WebServer
{
public:
    WebServer();
    ~WebServer();
​
    void init(int port , string user, string passWord, string databaseName,
              int log_write , int opt_linger, int trigmode, int sql_num,
              int thread_num, int close_log, int actor_model);
​
    void thread_pool();
    void sql_pool();
    void log_write();
    void trig_mode();
    void eventListen();
    void eventLoop();
    void timer(int connfd, struct sockaddr_in client_address);
    void adjust_timer(util_timer *timer);
    void deal_timer(util_timer *timer, int sockfd);
    bool dealclientdata();
    bool dealwithsignal(bool& timeout, bool& stop_server);
    void dealwithread(int sockfd);
    void dealwithwrite(int sockfd);
​
public:
    //基础
    int m_port;
    char *m_root;
    int m_log_write;
    int m_close_log;
    int m_actormodel;
​
    int m_pipefd[2];
    int m_epollfd;
    http_conn *users;
​
    //数据库相关
    connection_pool *m_connPool;
    string m_user;         //登陆数据库用户名
    string m_passWord;     //登陆数据库密码
    string m_databaseName; //使用数据库名
    int m_sql_num;
​
    //线程池相关
    threadpool<http_conn> *m_pool;
    int m_thread_num;
​
    //epoll_event相关
    epoll_event events[MAX_EVENT_NUMBER];
​
    int m_listenfd;
    int m_OPT_LINGER;
    int m_TRIGMode;
    int m_LISTENTrigmode;
    int m_CONNTrigmode;
​
    //定时器相关
    client_data *users_timer;
    Utils utils;
};
#endif

我们给类成员变量做个分类:

1.网络通信核心

int m_listenfd;//监听socket
int m_epollfd;//epoll实例
int m_pipefd[2];//用于信号处理的管道
http_conn *users;//http连接
epoll_event events[MAX_EVENT_NUMBER];//epoll事件数组

2.组件管理

threadpool<http_conn> *m_pool;//线程池
connection_pool *m_connPool;//数据库连接池

3.配置参数

int m_listenfd;//监听Socket的文件描述符
int m_OPT_LINGER;//socket关闭时的行为选项,0为默认关闭,1为优雅关闭(等待数据发送完毕)
int m_TRIGMode;//触发模式
int m_LISTENTrigmode;//监听socket的触发模式,决定epoll如何通知有新连接到达
int m_CONNTrigmode;//连接socket的触发模式,决定epoll如何通知连接上有数据可读/写
int m_thread_num;//线程数量
int m_sql_num;//连接池连接数量
int m_port;//端口号
char *m_root;//网站根目录的路径
int m_log_write;//日志写入方式(同步/异步)
int m_close_log;//是否关闭日志功能
int m_actormodel;//并发模型(0:Proactor模式,异步I/O;1:Reactor模式,同步I/O+非阻塞)
//Reactor:主线程监听事件,工作线程处理业务逻辑
//Proactor:主线程完成I/O操作,工作线程直接处理数据

4.定时器相关

client_data *users_timer; // 用户定时器数据
Utils utils;// 工具类

再给类成员函数做个分类:

1.初始化方法

void init(int port , string user, string passWord, string databaseName,
              int log_write , int opt_linger, int trigmode, int sql_num,
              int thread_num, int close_log, int actor_model);

这个方法接收所有配置参数,为服务器运行做准备。

2.组件初始化

void thread_pool();//初始化线程池
void sql_pool();//初始化数据库连接池
void log_write();//初始化日志系统
void trig_mode();//设置触发模式

3.事件循环核心

void eventListen();//创建监听socket,设置epoll
void eventLoop();//主事件循环

4.事件处理

bool dealclientdata();//处理新连接
bool dealwithsignal(bool& timeout, bool& stop_server);//处理信号
void dealwithread(int sockfd);//处理读事件
void dealwithwrite(int sockfd);//处理写事件

5.定时器管理

void timer(int connfd, struct sockaddr_in client_address);//为新连接创建定时器
void adjust_timer(util_timer *timer);//调整定时器
void deal_timer(util_timer *timer, int sockfd);//处理超时连接

现在我们对服务器的启动流程有了一个大致的了解:

1.解析参数:init()->各组件初始化。

2.创建监听:eventListen()->创建Socket,绑定端口,开始监听。

3.进入循环:eventLoop()->主事件循环。

这个过程和我之前写的聊天室还是很像的。

构造函数实现
WebServer::WebServer()
{
    //http_conn类对象
    users = new http_conn[MAX_FD];
​
    //root文件夹路径
    char server_path[200];
    getcwd(server_path, 200);
    char root[6] = "/root";
    m_root = (char *)malloc(strlen(server_path) + strlen(root) + 1);
    strcpy(m_root, server_path);
    strcat(m_root, root);
​
    //定时器
    users_timer = new client_data[MAX_FD];
}

一、http连接对象数组初始化

users = new http_conn[MAX_FD];

预分配了MAX_FD(65536)个http_conn对象,每个文件描述符对应一个http_conn实例,这样文件描述符直接作为数组索引,实现了O(1)时间复杂度的查找。

为什么选择MAX_FD为65536?因为Linux默认文件描述符上限通常是65536(2的16次方)。而且每个http连接大约是1KB,总共也就大约为64MB,这是可以接受的内存开销。

什么是文件描述符?它是操作系统为了管理打开的文件/资源而分配的一个非负整数,是进程级别的标识符。更通俗地说,文件对象本身是唯一的,但在不同进程下,它们可能有不同的编号,每个进程都有自己的文件描述符表。

举个例子:

// 假设有个文件 /home/user/data.txt
// 内核中只有一个对应的文件对象
​
// 进程A
int fd1 = open("/home/user/data.txt", O_RDONLY);  // 返回3
int fd2 = open("/home/user/data.txt", O_RDWR);    // 返回4(再次打开)
​
// 进程B
int fd1 = open("/home/user/data.txt", O_RDONLY);  // 返回3(不同进程!)
​
// 实际情况:
// 内核:只有一个 /home/user/data.txt 的文件对象
// 进程A:文件描述符3和4都指向这个文件对象
// 进程B:文件描述符3也指向这个文件对象

需要注意的是,文件描述符的分配发生在socket创建时,而不是http_conn实例化时。

//创建监听socket(eventListen方法中)
m_listenfd=socket(AF_INET,SOCK_STREAM,0);//操作系统分配文件描述符,假设返回3

此时,文件描述符3已经分配给了监听socket,users[3]还是一个未初始化的http_conn对象

//接受新连接
int connfd=accept(m_listenfd,...);//操作系统分配新的文件描述符,假设返回4
​
//初始化对应的http_conn对象
users[connfd].init(connfd,client_addr);

现在users[4]才被真正激活。文件描述符4是在accept调用时由操作系统分配的,然后我们才用这个文件描述符濑初始化对应的http_conn对象。最终,通过close(connfd),将文件描述符4回收,并通过users[connfd].close_conn()将对应的http_conn对象重置为空闲状态。

关于socket

先说一下socket。可以将它视为“接待室”。

a.创建接待室

int reception = socket(AF_INET, SOCK_STREAM, 0);

socket函数原型:

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数:

domain(协议族):AF_INET(IPv4)、AF_INTE6(IPv6)、AF_UNIX(本地通信)

type(通信类型):SOCK_STREAM(面向连接的流式socket,TCP)、SOCK_DGRAM(无连接的数据报socket,UDP)

protocol:具体协议,通常填0.

b.布置接待室

struct sockaddr_in address = {...};
bind(reception, (sockaddr*)&address, sizeof(address));

c.开始营业(listen监听)

listen(reception, 5);  // 允许5个客户在等候区等待

d.接待客户(accept阻塞等待)

int private_room = accept(reception, &client_addr, &addrlen);
//返回一个"私密会议室"的文件描述符,专门与这个客户交流

accept函数原型:

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

sockfd:监听socket的文件描述符

addr:输出参数,存放客户端地址信息

addrlen:输入输出参数,地址结构体长度

成功了就返回新的文件描述符,用于与客户端通信。失败了就返回-1.

例子:

// 1. 创建socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
​
// 2. 设置socket选项(可选)
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
​
// 3. 绑定地址和端口
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;  // 监听所有网卡
server_addr.sin_port = htons(8080);        // 端口8080
int result = bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (result == 0) {
    printf("绑定成功!服务器将在 0.0.0.0:8080 监听\n");
} else {
    perror("绑定失败");
    // 常见错误:EADDRINUSE(端口被占用)、EACCES(权限不足)
}
​
// 4. 开始监听
listen(listen_fd, 10);  
​
// 5. 接受连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
​
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd >= 0) {
    printf("New connection from %s:%d\n", 
           inet_ntoa(client_addr.sin_addr), 
           ntohs(client_addr.sin_port));
    
    // 6. 使用conn_fd与客户端通信
    // ...
    
    // 7. 关闭连接
    close(conn_fd);
}
​
// 8. 关闭监听socket
close(listen_fd);

关于第三点:

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;  // 监听所有网卡
server_addr.sin_port = htons(8080);        // 端口8080

第一行:定义了一个IPv4地址结构体

struct sockaddr_in server_addr;

sockaddr_in结构:

struct sockaddr_in {
    sa_family_t    sin_family;   // 地址族:AF_INET
    in_port_t      sin_port;     // 端口号(16位)
    struct in_addr sin_addr;     // IPv4地址(32位)
    char           sin_zero[8];  // 填充字段
};

第二行:清空结构体

memset(&server_addr, 0, sizeof(server_addr));

将结构体中所有字节设为0,避免未初始化数据,防止结构体中的填充字段包含随机值。

第三行:设置地址族

server_addr.sin_family = AF_INET;

表示使用IPv4协议族。

第四行:设置IP地址

server_addr.sin_addr.s_addr = INADDR_ANY;

INADDR_ANY是一个特殊常量,值为0.0.0.0,表示监听所有可用的网络接口。

第五行:设置端口号

server_addr.sin_port = htons(8080);

htons全称:Host To Network Short,它将16位整数从主机字节序转换为网络字节序。之所以要进行转换,是因为不同的CPU架构使用不同的字节序。网络字节序是大端序,x86主机是小端序。转换函数有以下四个:

htons(8080)   // Host to Network Short (16位)
htonl(12345)  // Host to Network Long (32位)  
ntohs()       // Network to Host Short
ntohl()       // Network to Host Long

端口号规则:

// 特权端口(0-1023):需要root权限
server_addr.sin_port = htons(80);    // HTTP(需要sudo)
​
// 注册端口(1024-49151):常用服务
server_addr.sin_port = htons(8080);  // Web开发常用
server_addr.sin_port = htons(3306);  // MySQL
​
// 动态端口(49152-65535):临时使用
server_addr.sin_port = htons(0);     // 系统自动分配

第六行至结尾:将socket与特定的IP地址和端口号关联

bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

bind()函数原型:

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

1.int sockfd

它是要绑定的socket文件描述符,其来源是socket()的返回值,必须要是未绑定的socket.

2.const struct sockaddr *addr

它是指向地址结构体的指针。要注意的是,需要将具体的地址结构体转换为通用的sockaddr指针,因为IPv4专用结构体和IPv6专用结构体都是通用地址结构体的派生类:

// 通用地址结构体(基类)
struct sockaddr {
    sa_family_t sa_family;    // 地址族
    char        sa_data[14];  // 地址数据
};
​
// IPv4专用结构体
struct sockaddr_in {
    sa_family_t    sin_family; // 地址族:AF_INET
    in_port_t      sin_port;   // 端口号
    struct in_addr sin_addr;   // IP地址
    // ... 填充字段
};
​
// IPv6专用结构体
struct sockaddr_in6 {
    sa_family_t     sin6_family;   // AF_INET6
    in_port_t       sin6_port;     // 端口号
    struct in6_addr sin6_addr;     // IPv6地址
    // ...
};

bind()被设计为通用接口,可以处理多种地址族,但我们在使用时传递的是具体的结构体,所以需要强制类型转换。

3.socklen_t addrlen

它是地址结构体的长度,通常使用sizeof()获取。

绑定失败的常见原因:

1.端口被占用(EADDRINUSE)

// 错误:Address already in use
// 解决:设置SO_REUSEADDR选项
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bind(listen_fd, ...);

2.权限不足(EACCES):绑定1024以下的特权端口需要root权限

// 错误:Permission denied
server_addr.sin_port = htons(80);  // HTTP端口需要sudo

3.地址不可用

需要注意的是,bind()不是建立连接,而是给socket分配一个本地地址,是告诉别人自己的位置。

比喻:socket()是建好酒店大楼,bind()是给大楼挂上地址门牌,listen()是开始营业,accept()是接待客人。

服务端:

//告诉别人我在哪里
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
​
// 设置服务器地址
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;  // 本机所有IP
server_addr.sin_port = htons(8080);        // 端口8080
​
// bind:声明:将在8080端口提供服务
bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
​
// listen:开始监听连接请求
listen(server_fd, 10);
​
// accept:等待客户端连接
int client_fd = accept(server_fd, ...);

客户端:

// 主动连接服务器
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
​
// 设置目标服务器地址
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr);// 绑定到特定IP
​
// connect:主动连接到服务器
connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 这里才真正建立网络连接(三次握手)

另外,一个监听socket可以服务多个客户端:

int listen_fd = socket(...);
bind(...);
listen(...);
​
while (true) {
    // 每次accept返回一个新的通信socket
    int client1_fd = accept(listen_fd, ...);  // 客户端1
    int client2_fd = accept(listen_fd, ...);  // 客户端2  
    int client3_fd = accept(listen_fd, ...);  // 客户端3
    
    // listen_fd 继续监听新连接
    // client1_fd, client2_fd, client3_fd 分别与不同客户端通信
}

二、 根目录路径设置

char server_path[200];
getcwd(server_path, 200); // 获取当前工作目录
char root[6] = "/root";
m_root = (char *)malloc(strlen(server_path) + strlen(root) + 1);
strcpy(m_root, server_path);
strcat(m_root, root);

getcwd的全称:Get Current Working Directory

函数原型:

#include <unistd.h>
char *getcwd(char *buf, size_t size);

buf:存储路径的缓冲区,size:缓冲区大小。成功就返回指向缓冲区的指针,失败则返回NULL.

执行过程:getcwd()获取程序运行的当前目录,拼接/root子目录作为网站根目录,动态分配内存以存储完整路径。

例子:如果程序在`/home/user/TinyWebServer中运行,那么m_root=/home/user/TinyWebServer/root

三、定时器数据初始化

users_timer = new client_data[MAX_FD];

作用:为每个可能的文件描述符预分配定时器数据,与users数组一一对应,用来管理连接超时。

构造函数中的预分配的设计体现了用空间换时间的思想。

优点:

1.高性能:文件描述符直接作为索引,查找操作时间复杂度为O(1)

2.避免内存碎片:一次性分配大块内存

3.线程安全:每个连接独立,减少锁竞争

缺点:

1.内存浪费:可能有大部分槽位空闲

2.扩展性限制:最大连接数固定

❓ 如果服务器需要支持更多连接,如何改进这个设计?

我们可以使用连接池:

class ConnectionPool {
private:
    std::queue<http_conn*> free_connections;
    std::unordered_map<int, http_conn*> active_connections;
    std::mutex pool_mutex;
    
public:
    ConnectionPool(size_t initial_size = 10000) {
        // 预分配一批对象
        for (size_t i = 0; i < initial_size; ++i) {
            free_connections.push(new http_conn());
        }
    }
    
    http_conn* acquire(int fd) {
        std::lock_guard<std::mutex> lock(pool_mutex);
        
        http_conn* conn = nullptr;
        if (!free_connections.empty()) {
            conn = free_connections.front();
            free_connections.pop();
        } else {
            conn = new http_conn();
        }
        
        active_connections[fd] = conn;
        return conn;
    }
    
    void release(int fd) {
        std::lock_guard<std::mutex> lock(pool_mutex);
        
        auto it = active_connections.find(fd);
        if (it != active_connections.end()) {
            free_connections.push(it->second);  //对象复用
            active_connections.erase(it);
        }
    }
};

### 关于 WebServer03 的技术信息与配置 WebServer 是一种用于提供网页服务的技术架构,其核心功能在于接收 HTTP 请求并返回相应的 HTML 页面或其他资源[^1]。对于特定版本的 WebServer(如 WebServer03),虽然未有直接提及该版本的具体细节,但从一般性的 WebServer 特性和常见配置出发,可以推测出以下几个方面的内容: #### 1. **WebServer03 的基本特性** 基于已知的 WebServer 功能描述[^2],WebServer03 很可能是某个轻量级项目的具体实现版本。这类服务器通常具备以下特点: - 支持静态文件的服务。 - 提供基础的 HTTP 协议解析能力。 - 可能内置简单的路由机制来处理不同类型的请求。 #### 2. **WebServer03 的典型配置项** 下面是一些常见的 WebServer 配置选项及其作用说明,这些配置适用于大多数轻量级 WebServer 实现,也包括假设中的 WebServer03: - **监听端口 (Port)** 默认情况下,WebServer 可能运行在标准的 HTTP 端口 `80` 上,但如果需要与其他应用共存,则可以通过修改配置指定其他端口号。例如,在某些场景下可能会使用 `8080` 或更高编号的端口[^4]。 - **根目录设置 (Document Root)** 这一参数定义了 WebServer 所托管的静态文件所在的路径。通过调整这一配置,可以让 WebServer 加载来自不同位置的内容。 - **日志记录 (Logging)** 日志记录是调试和监控的重要工具。典型的 WebServer 都允许启用访问日志 (`access.log`) 和错误日志 (`error.log`) 来跟踪用户的请求行为以及潜在的问题。 - **超时时间 (Timeout Settings)** 设置连接的最大保持时间和读写操作的时间限制有助于优化性能并减少资源浪费。 #### 3. **示例配置代码** 以下是基于 Python 的简单 WebServer 配置样例,可作为 WebServer03 的参考实现之一: ```python from http.server import HTTPServer, BaseHTTPRequestHandler class SimpleRequestHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.end_headers() message = b"Welcome to the simple web server!" self.wfile.write(message) if __name__ == "__main__": PORT = 8080 # 自定义端口 server_address = ("", PORT) httpd = HTTPServer(server_address, SimpleRequestHandler) print(f"Serving on port {PORT}...") httpd.serve_forever() ``` 这段代码展示了如何创建一个简易的 HTTP 服务器,并将其绑定至本地地址上的自定义端口[^2]。 #### 4. **高级功能扩展** 如果 WebServer03 被设计为支持更复杂的功能,则可能涉及以下方面: - **负载均衡**:通过代理层分发流量以提高可用性[^4]。 - **SSL/TLS 支持**:增强安全性,保护传输过程中的敏感数据[^5]。 - **动态页面渲染**:集成模板引擎或脚本解释器以便实时生成响应内容。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值