从正无穷开始用 socket 制作简单的多线程 http 服务器(Windows)

· · 科技·工程

序言

  1. 看这篇流水账之前建议先开个 diff,以查看各版本代码间的差异。我语言表达能力不是很好。。。
  2. 当你使用 Dev-C++ 时,勿忘链接 -lws2_32,不然,你会看到一大堆 undefined reference.
  3. 如果你是 Dev-C++er,恭喜你,不需要开一个项目。把所有文件放一起就行了。

声明:这篇文章里的所有代码可供学习参考,均未加水印,不可倒卖、盗代码(这里定义为不带出处地讲代码复制至任一地方)。

正文

一、前置知识

1. 线程

线程函数

Windows 中,线程函数的格式为:

DWORD WINAPI ThreadProc(LPVOID lpParam);

启动线程

启动线程的函数是 CreateThread

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  __drv_aliasesMem LPVOID lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

其中,参数 lpThreadAttributes 一般置为 NULLdwStackSize 一般为 0,由系统自己设置,dwCreationFlags0(立即运行),lpThreadId 设为 NULL(如果不需要的话)。

里面重点只有两个参数,lpStartAddresslpParamter.lpStartAddress 写你写的线程函数名如 ThreadProclpParamter 则填你要传给线程的参数(取地址,再把指针强制转换至 void*),对应线程函数的 lpParam.

2. socket

什么是 socket?

socket 就是应用层和传输层中间的抽象层,把各种繁琐的操作包装起来,使开发者使用更简便。(当然,你说不简便我也没话说)具体可参考C++ Socket 入门 - kpole - 博客园 (cnblogs.com),在这里不多赘述。

3. http

在这个简单的 http 服务器中,我们并不多去管消息中的标头。

浏览器浏览 ....../demo?a=111222333 时发送的 GET 指令一般是这样的:

GET /demo?a=111222333 HTTP/1.1
...一大堆 header...

注意,这里多一个换行并不是漏敲。HTTP 请求或回复中,用一个空行分隔标头和正文。其中,GET 表示请求的方法,后面跟的一坨就是网址后面的玩意,也就是地址。HTTP/1.1 则表示浏览器当前的 HTTP 版本号。对此,服务器这么回复它(在这个简单个服务器,我们会传的标头只会有 Location,在重定向时用到):

HTTP/1.1 200 OK
...一大堆 header...

<!DOCTYPE html><html lang="zh-cn"><head>demo</head><body><h1>哈哈哈,你是不是让 a=111222333 了?</h1></body></html>

如果是重定向,可能是这种情况:

HTTP/1.1 302 Found
Location: /main
...一大堆 header...

4. ini

对于 ini 读写,我们并不需要手搓,可以直接使用 Windows 自带的 Get(Write)PrivateProfile____ 系列,具体参见MSDN:

目前,我们只需要这些函数便可以进行配置文件读写。

5. 调试

因为我们的服务器需要在每个 IP 地址进行监听,所以我们需要用不同的颜色区分开。具体有关控制台颜色,参见:SetConsoleTextAttribute 函数 - Windows Console | Microsoft Learn和控制台屏幕缓冲区 - Windows Console | Microsoft Learn。为此,我们置了一张表:

int ipcolor[] =
{
    0x04, 0x06, 0x02, 0x03, 0x01, 0x05,
    0x40, 0x46, 0x42, 0x43, 0x41, 0x45,
    0x64, 0x60, 0x62, 0x63, 0x61, 0x65,
    0x24, 0x26, 0x20, 0x23, 0x21, 0x25,
    0x34, 0x36, 0x32, 0x30, 0x31, 0x35,
    0x14, 0x16, 0x12, 0x13, 0x10, 0x15,
    0x54, 0x56, 0x52, 0x53, 0x51, 0x50
};

其中,每一个值都显示不同的颜色(指颜色和底色有一个不同),可以区分。设置颜色:

void col(int n = -1)
{
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), n == -1 ? 0x07 : ipcolor[n]);
}

为了不让各线程的输出搅和在一起,我们可以添加一个等待队列:

queue<pair<int, int>> writq;

其中每一个元素的 first 为主编号,second 为次编号。

为了打印输出时间,再写一个时间函数:

string GetFormatTime()
{
    SYSTEMTIME st;
    GetSystemTime(&st);
    string ret = "";
    ret += to_string(st.wYear) + "/" + to_string(st.wMonth) + "/" + to_string(st.wDay) + " ";
    ret += to_string(st.wHour) + ":" + to_string(st.wMinute) + ":" + to_string(st.wSecond) + "." + to_string(st.wMilliseconds);
    return ret;
}

之后,我们仿照 printf 的格式,写一个 cprintf

void cprintf(int major, int minor, const char *format, ...) 
{ 
    writq.push(make_pair(major, minor)); 
    va_list va;
    char buf[BUFLEN]; 
    va_start(va, format); 
    vsprintf(buf, format, va); 
    va_end(va); 
    string tmp = buf;
    tmp = to_string(major) + "." + to_string(minor) + "@" + GetFormatTime() + " [LOG] " + tmp;
    while (writq.front().first != major || writq.front().second != minor) ;
    col(major);
    cout << tmp << flush;
    col();
    writq.pop();
}

这样,我们就可以用类似于使用 printf 的方法使用此函数。当然,如果你要写一个流,也不是不可以。不过,光是实现,就可以单成一个项目了。

二、开始

1. 尝试

既然知道了 socket,那么写出一个简单的服务器吧!

#include<winsock2.h>
#include<Windows.h>
#pragma comment(lib, "-lws2_32")
#include<bits/stdc++.h>

#define PORT 8080 // 绑定端口

using namespace std;
int main()
{
    // 在 Windows 下需要加上这一段。
    WSADATA wsd;
    WSAStartup(MAKEWORD(2, 2), &wsd);
    // 创建 TCP 套接字(http 是基于 TCP 的)
    SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    // 写好绑定地址
    sockaddr_in listenaddr;
    ZeroMemory(&listenaddr, sizeof(listenaddr)); // 置零
    listenaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 绑定 IP
    listenaddr.sin_family = AF_INET;
    listenaddr.sin_port = htons(PORT); // 注意,这个 htons 是必需的,把主机地址转化为网络地址
    // 绑定
    bind(listenfd, (sockaddr*)&listenaddr, sizeof(sockaddr));
    // 监听
    listen(listenfd, 20);
    // 循环接受连接请求
    SOCKET clientfd;
    sockaddr_in clientaddr;
    int tmplen;
    while((clientfd = accept(listenfd, (sockaddr*)&clientaddr, &tmplen)) != INVALID_SOCKET)
    {
        clog << "收到来自 " << inet_ntoa(clientaddr.sin_addr) << " 的连接请求" << endl;
        string message = "Hello there!";
        send(clientfd, message.data(), message.size(), 0);
        recv(clientfd, message.data(), message.capacity(), 0);
        cout << message << endl;
    }
    // 释放 socket 资源
    WSACleanup();
}

2. 对每个 IP 地址绑定监听

获取电脑 IP 的方法详见 这里。

我们本地调试一般会用 localhost(谁愿意输一大串 IP 地址调试?!),也就是 127.0.0.1,但那篇文章给出的代码结果并没有 127.0.0.1,所以我们需增加一个 IP 127.0.0.1.

具体方法如下:

string strIP("127.0.0.1");
vector<string> ret;
ret.clear();
ret.push_back(strIP + "@0"); // 先加上 127.0.0.1
char strName[1024];
gethostname(strName, 1024);
struct hostent *pHostEnt = gethostbyname(strName);
int n = 0;
while (pHostEnt->h_addr_list[n] != NULL)
{
    stringstream ss;
    ss << (pHostEnt->h_addr_list[n][0] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][1] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][2] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][3] & 0x00FF) << flush; // 以字符串流的形式获取完整 IP
    string strTemp(ss.str());
    cprintf(n, 0, strTemp.data());
    if (string::npos == strTemp.find("127.0.0."))
    {
        ret.push_back(strTemp + "@" + to_string(n + 1)); // 此处的 @ 是用于分隔 IP 与编号
    }
    n++;
}

然后,我们对每一个 IP 启动一个线程,叫做 tIPServer,把之前的代码放进去,改一下(主要就是参数处理、IP 换一下、原来的输出改为调试语句):

DWORD WINAPI tIPServer(LPVOID lpParam)
{
    // 参数处理:lpParam 处是一个 string.
    string ip = *(string *)lpParam;
    int major = stoi(ip.substr(ip.find('@') + 1)); // 在 @ 后面寻找主编号
    ip = ip.substr(0, ip.find('@'));
    // 原来的语句
    // 创建 TCP 套接字(http 是基于 TCP 的)
    SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    // 写好绑定地址
    sockaddr_in listenaddr;
    ZeroMemory(&listenaddr, sizeof(listenaddr)); // 置零
    // 注意:这里一个 127.0.0.1 不要忘记改,不然 3221226536(~~我就掉坑了~~)
    listenaddr.sin_addr.s_addr = inet_addr(ip.data()); // 绑定 IP, inet_addr 把字符串样式的地址转换为 addr
    listenaddr.sin_family = AF_INET; // 和套接字的 family(af) 要一样
    listenaddr.sin_port = htons(PORT); // 注意,这个 htons 是必需的,把主机地址转化为网络地址 (host to network short)
    cprintf(major, 0, "Start, IP = %s\n", ip.data());
    // 绑定
    bind(listenfd, (sockaddr*)&listenaddr, sizeof(sockaddr));
    cprintf(major, 0, "bind OK\n");
    // 监听
    listen(listenfd, 20);
    cprintf(major, 0, "listen OK\n");
    // 循环接受连接请求
    SOCKET clientfd;
    sockaddr_in clientaddr;
    int tmplen = sizeof(sockaddr);
    while((clientfd = accept(listenfd, (sockaddr*)&clientaddr, &tmplen)) != INVALID_SOCKET)
    {
        // 这里的 inet_ntoa 是把 addr 转换成 C 格式字符串;ntohs 把网络地址转成主机地址 (network to host short)
        cprintf(major, 0, "收到来自 %s:%d 的连接请求\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        string message;
        recv(clientfd, message.data(), message.capacity(), 0);
        cprintf(major, 0, "msg:\n%s\n", message.data());
        send(clientfd, message.data(), message.size(), 0); // Echo Server
    }
}

这样,我们把主程序换掉,程序就成了这样:

#include<winsock2.h>
#include<Windows.h>
#pragma comment(lib, "-lws2_32")
#include<bits/stdc++.h>

#define PORT 8080 // 绑定端口
#define BUFLEN 1048576

using namespace std;

queue<pair<int, int>> writq;
int ipcolor[] =
{
    0x04, 0x06, 0x02, 0x03, 0x01, 0x05,
    0x40, 0x46, 0x42, 0x43, 0x41, 0x45,
    0x64, 0x60, 0x62, 0x63, 0x61, 0x65,
    0x24, 0x26, 0x20, 0x23, 0x21, 0x25,
    0x34, 0x36, 0x32, 0x30, 0x31, 0x35,
    0x14, 0x16, 0x12, 0x13, 0x10, 0x15,
    0x54, 0x56, 0x52, 0x53, 0x51, 0x50
};
void col(int n = -1)
{
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), n == -1 ? 0x07 : ipcolor[n]);
}
string GetFormatTime()
{
    SYSTEMTIME st;
    GetSystemTime(&st);
    string ret = "";
    ret += to_string(st.wYear) + "/" + to_string(st.wMonth) + "/" + to_string(st.wDay) + " ";
    ret += to_string(st.wHour) + ":" + to_string(st.wMinute) + ":" + to_string(st.wSecond) + "." + to_string(st.wMilliseconds);
    return ret;
}
void cprintf(int major, int minor, const char *format, ...) 
{ 
    writq.push(make_pair(major, minor)); 
    va_list va;
    char buf[BUFLEN]; 
    va_start(va, format); 
    vsprintf(buf, format, va); 
    va_end(va); 
    string tmp = buf;
    tmp = to_string(major) + "." + to_string(minor) + "@" + GetFormatTime() + " [LOG] " + tmp;
    while (writq.front().first != major || writq.front().second != minor) ;
    col(major);
    cout << tmp << flush;
    col();
    writq.pop();
}

DWORD WINAPI tIPServer(LPVOID lpParam)
{
    // lpParam 处是一个 string.
    string ip = *(string *)lpParam;
    int major = stoi(ip.substr(ip.find('@') + 1)); // 在 @ 后面寻找主版本号
    ip = ip.substr(0, ip.find('@'));
    // 创建 TCP 套接字(http 是基于 TCP 的)
    SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    // 写好绑定地址
    sockaddr_in listenaddr;
    ZeroMemory(&listenaddr, sizeof(listenaddr)); // 置零
    listenaddr.sin_addr.s_addr = inet_addr(ip.data()); // 绑定 IP
    listenaddr.sin_family = AF_INET;
    listenaddr.sin_port = htons(PORT); // 注意,这个 htons 是必需的,把主机地址转化为网络地址
    cprintf(major, 0, "Start, IP = %s\n", ip.data());
    // 绑定
    bind(listenfd, (sockaddr*)&listenaddr, sizeof(sockaddr));
    cprintf(major, 0, "bind OK\n");
    // 监听
    listen(listenfd, 20);
    cprintf(major, 0, "listen OK\n");
    // 循环接受连接请求
    SOCKET clientfd;
    sockaddr_in clientaddr;
    int tmplen = sizeof(sockaddr);
    while((clientfd = accept(listenfd, (sockaddr*)&clientaddr, &tmplen)) != INVALID_SOCKET)
    {
        cprintf(major, 0, "收到来自 %s:%d 的连接请求\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        string message;
        recv(clientfd, message.data(), message.capacity(), 0);
        cprintf(major, 0, "msg:\n%s\n", message.data());
        send(clientfd, message.data(), message.size(), 0); // Echo Server
    }
}

int main()
{
    // 在 Windows 下需要加上这一段。
    WSADATA wsd;
    WSAStartup(MAKEWORD(2, 2), &wsd);
    // 获取所有 IP
    string strIP("127.0.0.1");
    vector<string> ret;
    ret.clear();
    ret.push_back(strIP + "@0");
    cprintf(0, 0, "127.0.0.1\n");
    char strName[1024];
    gethostname(strName, 1024);
    struct hostent *pHostEnt = gethostbyname(strName);
    int n = 0;
    while (pHostEnt->h_addr_list[n] != NULL)
    {
        stringstream ss;
        ss << (pHostEnt->h_addr_list[n][0] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][1] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][2] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][3] & 0x00FF) << flush;
        string strTemp(ss.str());
        cprintf(n + 1, 0, (strTemp + "\n").data());
        if (string::npos == strTemp.find("127.0.0."))
        {
            ret.push_back(strTemp + "@" + to_string(n + 1)); // 此处的 @ 是用于分隔 IP 与编号
        }
        n++;
    }
    // 对每个 IP 启动线程
    for(int i = 0; i < ret.size(); i++)
    {
        CreateThread(NULL, 0, tIPServer, (LPVOID)&ret[i], 0, NULL);
    }
    // 死循环,不要提前退了
    while(1);
    // 释放 socket 资源
    WSACleanup();
}

OK,如果你这边成功了,那么,我们进入下一环节吧!(注:网上随便复制一个客户端改一下应该都能满足需求)

3. 对每个客户端单开房间(线程)

与每个客户端对话,核心代码只有一点。我们把它帖进去,加个 minor 计数器(以免输出混乱)就行了:

#include<winsock2.h>
#include<Windows.h>
#pragma comment(lib, "-lws2_32")
#include<bits/stdc++.h>

#define PORT 8080 // 绑定端口
#define BUFLEN 1048576

using namespace std;

queue<pair<int, int>> writq;
int ipcolor[] =
{
    0x04, 0x06, 0x02, 0x03, 0x01, 0x05,
    0x40, 0x46, 0x42, 0x43, 0x41, 0x45,
    0x64, 0x60, 0x62, 0x63, 0x61, 0x65,
    0x24, 0x26, 0x20, 0x23, 0x21, 0x25,
    0x34, 0x36, 0x32, 0x30, 0x31, 0x35,
    0x14, 0x16, 0x12, 0x13, 0x10, 0x15,
    0x54, 0x56, 0x52, 0x53, 0x51, 0x50
};
void col(int n = -1)
{
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), n == -1 ? 0x07 : ipcolor[n]);
}
string GetFormatTime()
{
    SYSTEMTIME st;
    GetSystemTime(&st);
    string ret = "";
    ret += to_string(st.wYear) + "/" + to_string(st.wMonth) + "/" + to_string(st.wDay) + " ";
    ret += to_string(st.wHour) + ":" + to_string(st.wMinute) + ":" + to_string(st.wSecond) + "." + to_string(st.wMilliseconds);
    return ret;
}
void cprintf(int major, int minor, const char *format, ...) 
{ 
    writq.push(make_pair(major, minor)); 
    va_list va;
    char buf[BUFLEN]; 
    va_start(va, format); 
    vsprintf(buf, format, va); 
    va_end(va); 
    string tmp = buf;
    tmp = to_string(major) + "." + to_string(minor) + "@" + GetFormatTime() + " [LOG] " + tmp;
    while (writq.front().first != major || writq.front().second != minor) ;
    col(major);
    cout << tmp << flush;
    col();
    writq.pop();
}

DWORD WINAPI tTalk(LPVOID lpParam) // 新加函数
{
    pair<SOCKET, pair<int, int>> psii; // 前一个是客户端 socket,后面是编号
    psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
    // 获取 socket 与主次编号
    SOCKET clientfd = psii.first;
    int major = psii.second.first;
    int minor = psii.second.second;

    string message;
    recv(clientfd, message.data(), message.capacity(), 0);
    cprintf(major, minor, "msg:\n%s\n", message.data());
    send(clientfd, message.data(), message.size(), 0); // Echo Server
    // 处理完后关闭
    shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
    closesocket(clientfd);
    return EXIT_SUCCESS;
}
DWORD WINAPI tIPServer(LPVOID lpParam)
{
    // lpParam 处是一个 string.
    string ip = *(string *)lpParam;
    int major = stoi(ip.substr(ip.find('@') + 1)); // 在 @ 后面寻找主版本号
    ip = ip.substr(0, ip.find('@'));
    // 创建 TCP 套接字(http 是基于 TCP 的)
    SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    // 写好绑定地址
    sockaddr_in listenaddr;
    ZeroMemory(&listenaddr, sizeof(listenaddr)); // 置零
    listenaddr.sin_addr.s_addr = inet_addr(ip.data()); // 绑定 IP
    listenaddr.sin_family = AF_INET;
    listenaddr.sin_port = htons(PORT); // 注意,这个 htons 是必需的,把主机地址转化为网络地址
    cprintf(major, 0, "Start, IP = %s\n", ip.data());
    // 绑定
    bind(listenfd, (sockaddr*)&listenaddr, sizeof(sockaddr));
    cprintf(major, 0, "bind OK\n");
    // 监听
    listen(listenfd, 20);
    cprintf(major, 0, "listen OK\n");
    // 循环接受连接请求
    SOCKET clientfd;
    sockaddr_in clientaddr;
    int tmplen = sizeof(sockaddr);
    int counter = 0;
    while((clientfd = accept(listenfd, (sockaddr*)&clientaddr, &tmplen)) != INVALID_SOCKET)
    {
        cprintf(major, 0, "收到来自 %s:%d 的连接请求\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        pair<SOCKET, pair<int, int>> psii = make_pair(clientfd, make_pair(major, (++counter))); // 把东西放进去
        CreateThread(NULL, 0, tTalk, (LPVOID)&psii, 0, NULL); // 取指针,传给 tTalk
    }
}

int main()
{
    // 在 Windows 下需要加上这一段。
    WSADATA wsd;
    WSAStartup(MAKEWORD(2, 2), &wsd);
    // 获取所有 IP
    string strIP("127.0.0.1");
    vector<string> ret;
    ret.clear();
    ret.push_back(strIP + "@0");
    cprintf(0, 0, "127.0.0.1\n");
    char strName[1024];
    gethostname(strName, 1024);
    struct hostent *pHostEnt = gethostbyname(strName);
    int n = 0;
    while (pHostEnt->h_addr_list[n] != NULL)
    {
        stringstream ss;
        ss << (pHostEnt->h_addr_list[n][0] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][1] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][2] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][3] & 0x00FF) << flush;
        string strTemp(ss.str());
        cprintf(n + 1, 0, (strTemp + "\n").data());
        if (string::npos == strTemp.find("127.0.0."))
        {
            ret.push_back(strTemp + "@" + to_string(n + 1)); // 此处的 @ 是用于分隔 IP 与编号
        }
        n++;
    }
    // 对每个 IP 启动线程
    for(int i = 0; i < ret.size(); i++)
    {
        CreateThread(NULL, 0, tIPServer, (LPVOID)&ret[i], 0, NULL);
    }
    // 死循环,不要提前退了
    while(1);
    // 释放 socket 资源
    WSACleanup();
}

三、HTTP

1. 对 /html 文件夹内文件的读取

在这里,我们需要注意,读的内容可能是二进制文件,所以为了不缺失(读到 \0),我们要在模式里按位或上一个 ios::binary,其他就是流了。

bool IsFileExist(string path)
{
    FILE *f = fopen(path.data(), "r");
    bool ret = f == NULL;
    if (!ret)
        fclose(f);
    return !ret;
}
string GetFile(string path)
{
    if (!IsFileExist(path))
    {
        cprintf(33, 0, "request file %s not exist\n", path.data());
        return "";
    }
    cprintf(33, 0, "request file %s succ\n", path.data());
    ifstream ifs(path, ios::in | ios::binary);
    // 获取文件所有内容
    stringstream ss;
    ss << ifs.rdbuf();
    ifs.close();
    return ss.str();
}

2. 分析客户端的请求并将对应文件传给它

分析请求

我们现在要实现的是 GET,它的格式:

GET ...URL... HTTP/1.1
...标头...

而我们要返回(我们目前不处理标头):

HTTP/1.1 200 OK

...内容...

所以,我们修改一下 tTalk 的内容:

DWORD WINAPI tTalk(LPVOID lpParam)
{
    pair<SOCKET, pair<int, int>> psii; // 前一个是客户端 socket,后面是编号
    psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
    // 获取 socket 与主次编号
    SOCKET clientfd = psii.first;
    int major = psii.second.first;
    int minor = psii.second.second;

    char *buf = new char[BUFLEN];
    int len = recv(clientfd, buf, BUFLEN, 0);
    string message(buf, len);
    delete []buf;
    cprintf(major, minor, "msg:\n%s\n", message.data());

    // 处理 http 请求
    stringstream ss(message); // 字符串流
    string reqtype;
    ss >> reqtype;
    if(reqtype == "GET") // 确保是 GET 方法
    {
        string path;
        ss >> path;
        path = ROOT + path; // 定义:#define ROOT "./html"
        string version;
        ss >> version;
        message = VERSION" "; // #define VERSION "HTTP/1.1"
        message += IsFileExist(path) ? " 404 Not Found" : " 200 OK";
        message += "\n\n" + GetFile(path);
    }

    send(clientfd, message.data(), message.size(), 0); // http Server
    // 处理完后关闭
    shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
    closesocket(clientfd);
    return EXIT_SUCCESS;
}

在这里,我们增加了处理请求的逻辑(17\~30)。现在,我们把代码组合起来:

#include<winsock2.h>
#include<Windows.h>
#pragma comment(lib, "-lws2_32")
#include<bits/stdc++.h>

#define PORT 8080 // 绑定端口
#define BUFLEN 1048576
#define ROOT "./html"
#define VERSION "HTTP/1.1"

using namespace std;

queue<pair<int, int>> writq;
int ipcolor[] =
{
    0x04, 0x06, 0x02, 0x03, 0x01, 0x05,
    0x40, 0x46, 0x42, 0x43, 0x41, 0x45,
    0x64, 0x60, 0x62, 0x63, 0x61, 0x65,
    0x24, 0x26, 0x20, 0x23, 0x21, 0x25,
    0x34, 0x36, 0x32, 0x30, 0x31, 0x35,
    0x14, 0x16, 0x12, 0x13, 0x10, 0x15,
    0x54, 0x56, 0x52, 0x53, 0x51, 0x50
};
void col(int n = -1)
{
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), n == -1 ? 0x07 : ipcolor[n]);
}
string GetFormatTime()
{
    SYSTEMTIME st;
    GetSystemTime(&st);
    string ret = "";
    ret += to_string(st.wYear) + "/" + to_string(st.wMonth) + "/" + to_string(st.wDay) + " ";
    ret += to_string(st.wHour) + ":" + to_string(st.wMinute) + ":" + to_string(st.wSecond) + "." + to_string(st.wMilliseconds);
    return ret;
}
void cprintf(int major, int minor, const char *format, ...) 
{ 
    writq.push(make_pair(major, minor)); 
    va_list va;
    char buf[BUFLEN]; 
    va_start(va, format); 
    vsprintf(buf, format, va); 
    va_end(va); 
    string tmp = buf;
    tmp = to_string(major) + "." + to_string(minor) + "@" + GetFormatTime() + " [LOG] " + tmp;
    while (writq.front().first != major || writq.front().second != minor) ;
    col(major);
    cout << tmp << flush;
    col();
    writq.pop();
}

bool IsFileExist(string path)
{
    FILE *f = fopen(path.data(), "r");
    bool ret = f == NULL;
    if (!ret)
        fclose(f);
    return !ret;
}
string GetFile(string path)
{
    if (!IsFileExist(path))
    {
        cprintf(33, 0, "request file %s not exist\n", path.data());
        return "";
    }
    cprintf(33, 0, "request file %s succ\n", path.data());
    ifstream ifs(path, ios::in | ios::binary);
    // 获取文件所有内容
    stringstream ss;
    ss << ifs.rdbuf();
    ifs.close();
    return ss.str();
}

DWORD WINAPI tTalk(LPVOID lpParam)
{
    pair<SOCKET, pair<int, int>> psii; // 前一个是客户端 socket,后面是编号
    psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
    // 获取 socket 与主次编号
    SOCKET clientfd = psii.first;
    int major = psii.second.first;
    int minor = psii.second.second;

    char *buf = new char[BUFLEN];
    int len = recv(clientfd, buf, BUFLEN, 0);
    string message(buf, len);
    delete []buf;
    cprintf(major, minor, "msg:\n%s\n", message.data());

    // 处理 http 请求
    stringstream ss(message); // 字符串流
    string reqtype;
    ss >> reqtype;
    if(reqtype == "GET") // 确保是 GET 方法
    {
        string path;
        ss >> path;
        path = ROOT + path;
        string version;
        ss >> version;
        message = VERSION" ";
        message += IsFileExist(path) ? " 404 Not Found" : " 200 OK";
        message += "\n\n" + GetFile(path);
    }

    send(clientfd, message.data(), message.size(), 0); // http Server
    // 处理完后关闭
    shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
    closesocket(clientfd);
    return EXIT_SUCCESS;
}
DWORD WINAPI tIPServer(LPVOID lpParam)
{
    // lpParam 处是一个 string.
    string ip = *(string *)lpParam;
    int major = stoi(ip.substr(ip.find('@') + 1)); // 在 @ 后面寻找主版本号
    ip = ip.substr(0, ip.find('@'));
    // 创建 TCP 套接字(http 是基于 TCP 的)
    SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    // 写好绑定地址
    sockaddr_in listenaddr;
    ZeroMemory(&listenaddr, sizeof(listenaddr)); // 置零
    listenaddr.sin_addr.s_addr = inet_addr(ip.data()); // 绑定 IP
    listenaddr.sin_family = AF_INET;
    listenaddr.sin_port = htons(PORT); // 注意,这个 htons 是必需的,把主机地址转化为网络地址
    cprintf(major, 0, "Start, IP = %s\n", ip.data());
    // 绑定
    bind(listenfd, (sockaddr*)&listenaddr, sizeof(sockaddr));
    cprintf(major, 0, "bind OK\n");
    // 监听
    listen(listenfd, 20);
    cprintf(major, 0, "listen OK\n");
    // 循环接受连接请求
    SOCKET clientfd;
    sockaddr_in clientaddr;
    int tmplen = sizeof(sockaddr);
    int counter = 0;
    while((clientfd = accept(listenfd, (sockaddr*)&clientaddr, &tmplen)) != INVALID_SOCKET)
    {
        cprintf(major, 0, "收到来自 %s:%d 的连接请求\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        pair<SOCKET, pair<int, int>> psii = make_pair(clientfd, make_pair(major, (counter++))); // 把东西放进去
        CreateThread(NULL, 0, tTalk, (LPVOID)&psii, 0, NULL); // 取指针,传给 tTalk
    }
}

int main()
{
    // 在 Windows 下需要加上这一段。
    WSADATA wsd;
    WSAStartup(MAKEWORD(2, 2), &wsd);
    // 获取所有 IP
    string strIP("127.0.0.1");
    vector<string> ret;
    ret.clear();
    ret.push_back(strIP + "@0");
    cprintf(0, 0, "127.0.0.1\n");
    char strName[1024];
    gethostname(strName, 1024);
    struct hostent *pHostEnt = gethostbyname(strName);
    int n = 0;
    while (pHostEnt->h_addr_list[n] != NULL)
    {
        stringstream ss;
        ss << (pHostEnt->h_addr_list[n][0] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][1] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][2] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][3] & 0x00FF) << flush;
        string strTemp(ss.str());
        cprintf(n + 1, 0, (strTemp + "\n").data());
        if (string::npos == strTemp.find("127.0.0."))
        {
            ret.push_back(strTemp + "@" + to_string(n + 1)); // 此处的 @ 是用于分隔 IP 与编号
        }
        n++;
    }
    // 对每个 IP 启动线程
    for(int i = 0; i < ret.size(); i++)
    {
        CreateThread(NULL, 0, tIPServer, (LPVOID)&ret[i], 0, NULL);
    }
    // 死循环,不要提前退了
    while(1);
    // 释放 socket 资源
    WSACleanup();
}

当然,你也可以对那些无关紧要的逻辑或是已经证明正确且以后不用修改的代码压行(如果你耐心,可以全压)。

3. 文件不存在时发送 404 错误页面

我们要发送 404 页面的话,状态码就得是 200(为什么?浏览器一看到 404,就迫不及待地拎出它那专属错误页了)。

DWORD WINAPI tTalk(LPVOID lpParam)
{
    pair<SOCKET, pair<int, int>> psii; // 前一个是客户端 socket,后面是编号
    psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
    // 获取 socket 与主次编号
    SOCKET clientfd = psii.first;
    int major = psii.second.first;
    int minor = psii.second.second;

    char *buf = new char[BUFLEN];
    int len = recv(clientfd, buf, BUFLEN, 0);
    string message(buf, len);
    delete []buf;
    cprintf(major, minor, "msg:\n%s\n", message.data());

    // 处理 http 请求
    stringstream ss(message); // 字符串流
    string reqtype;
    ss >> reqtype;
    if(reqtype == "GET") // 确保是 GET 方法
    {
        string path;
        ss >> path;
        path = ROOT + path;
        string version;
        ss >> version;
        message = VERSION" ";
        message += "200 OK\n\n";
        if(IsFileExist(path)) message += GetFile(path);
        else message += GetFile(ROOT"/404.html");
    }

    send(clientfd, message.data(), message.size(), 0); // http Server
    // 处理完后关闭
    shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
    closesocket(clientfd);
    return EXIT_SUCCESS;
}

4. 读写配置文件

为了不用每次更改时都修改程序,聪明的人类发明了配置文件。如何读取?见前置知识 4 【ini】。

我们先写一个 class,实现对配置文件的读写:

class INI
{
protected:
    char file[BUFLEN];
public:
    void open(const char *File)
    {
        strcpy(file, File);
    }
    string Read(string Sec, string Key)
    {
        string ret = "";
        char *buf = new char[BUFLEN];
        GetPrivateProfileStringA(Sec.data(), Key.data(), "", buf, BUFLEN, file);
        ret = buf;
        delete[]buf;
        return ret;
    }
    int ReadInt(string Sec, string Key)
    {
        return GetPrivateProfileIntA(Sec.data(), Key.data(), 0, file);
    }
    void Write(string Sec, string Key, string Value)
    {
        WritePrivateProfileStringA(Sec.data(), Key.data(), Value.data(), file);
    }
    void WriteInt(string Sec, string Key, int Value)
    {
        WritePrivateProfileStringA(Sec.data(), Key.data(), to_string(Value).data(), file);
    }
};

我们将错误页信息写进配置文件 config.ini 中,并专门写一个函数读/写错误页信息:

string GetErrorPage(int ErrorCode)
{
    INI ini;
    ini.open(CONFIG); // #define CONFIG "./config.ini"
    string ret = ini.Read("errc", to_string(ErrorCode));
    return ret == "" ? "/err/" + to_string(ErrorCode) + ".htm" : ret; // 默认为 /err/*.htm
}
void SetErrorPage(int ErrCode, string path)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("errc", to_string(ErrCode), path);
}

这样,代码就成了这样:

DWORD WINAPI tTalk(LPVOID lpParam)
{
    pair<SOCKET, pair<int, int>> psii; // 前一个是客户端 socket,后面是编号
    psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
    // 获取 socket 与主次编号
    SOCKET clientfd = psii.first;
    int major = psii.second.first;
    int minor = psii.second.second;

    char *buf = new char[BUFLEN];
    int len = recv(clientfd, buf, BUFLEN, 0);
    string message(buf, len);
    delete []buf;
    cprintf(major, minor, "msg:\n%s\n", message.data());

    // 处理 http 请求
    stringstream ss(message); // 字符串流
    string reqtype;
    ss >> reqtype;
    if(reqtype == "GET") // 确保是 GET 方法
    {
        string path;
        ss >> path;
        path = ROOT + path;
        string version;
        ss >> version;
        message = VERSION" ";
        message += "200 OK\n\n";
        if(IsFileExist(path)) message += GetFile(path);
        else message += GetFile(ROOT + GetErrorPage(404));
    }

    send(clientfd, message.data(), message.size(), 0); // http Server
    // 处理完后关闭
    shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
    closesocket(clientfd);
    return EXIT_SUCCESS;
}

5. 客户端临时重定向

我们先把重定向的配置搞好。

string GetRedirect(string file)
{
    INI ini;
    ini.open(CONFIG);
    string ret = ini.Read("redir", file);
    return ret;
}
void SetRedirect(string file, string redir)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("redir", file, redir);
}

接下来,在 tTalk 中加一个判断:

DWORD WINAPI tTalk(LPVOID lpParam)
{
    pair<SOCKET, pair<int, int>> psii; // 前一个是客户端 socket,后面是编号
    psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
    // 获取 socket 与主次编号
    SOCKET clientfd = psii.first;
    int major = psii.second.first;
    int minor = psii.second.second;

    char *buf = new char[BUFLEN];
    int len = recv(clientfd, buf, BUFLEN, 0);
    string message(buf, len);
    delete []buf;
    cprintf(major, minor, "msg:\n%s\n", message.data());

    // 处理 http 请求
    stringstream ss(message); // 字符串流
    string reqtype;
    ss >> reqtype;
    if(reqtype == "GET") // 确保是 GET 方法
    {
        string path;
        ss >> path;
        path = ROOT + path;
        string version;
        ss >> version;
        message = VERSION" ";
        // 新加判断
        if(GetRedirect(path) != "") // 重定向优先,不然文件一大堆
        {
            message += "302 Found\nLocation: " + GetRedirect(path).substr(strlen(ROOT)) + "\n";
        }
        else if(!IsFileExist(path)) // 404
        {
            message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(404));
        }
        else
        {
            message += "200 OK\n\n" + GetFile(path);
        }
    }

    send(clientfd, message.data(), message.size(), 0); // http Server
    // 处理完后关闭
    shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
    closesocket(clientfd);
    return EXIT_SUCCESS;
}

这便是重定向。

6. 对某些文件的访问禁止 (403)

一样,也是先搞好配置。这里,以文件名为键值,是否访问禁止为数值(1 禁止,0 允许)

bool IsForbidden(string file)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("attr", file);
}
void SetFileAttrib(string file, bool forbidden = true)
{
    INI ini;
    ini.open(CONFIG);
    ini.WriteInt("attr", file, forbidden);
}

其次,加一个特判:

DWORD WINAPI tTalk(LPVOID lpParam)
{
    pair<SOCKET, pair<int, int>> psii; // 前一个是客户端 socket,后面是编号
    psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
    // 获取 socket 与主次编号
    SOCKET clientfd = psii.first;
    int major = psii.second.first;
    int minor = psii.second.second;

    char *buf = new char[BUFLEN];
    int len = recv(clientfd, buf, BUFLEN, 0);
    string message(buf, len);
    delete []buf;
    cprintf(major, minor, "msg:\n%s\n", message.data());

    // 处理 http 请求
    stringstream ss(message); // 字符串流
    string reqtype;
    ss >> reqtype;
    if(reqtype == "GET") // 确保是 GET 方法
    {
        string path;
        ss >> path;
        path = ROOT + path;
        string version;
        ss >> version;
        message = VERSION" ";
        if(GetRedirect(path) != "") // 重定向优先,不然文件一大堆
        {
            message += "302 Found\nLocation: " + GetRedirect(path).substr(strlen(ROOT)) + "\n";
        }
        else if(IsForbidden(path)) // 403
        {
            message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(403));
        }
        else if(!IsFileExist(path)) // 404
        {
            message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(404));
        }
        else
        {
            message += "200 OK\n\n" + GetFile(path);
        }
    }

    send(clientfd, message.data(), message.size(), 0); // http Server
    // 处理完后关闭
    shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
    closesocket(clientfd);
    return EXIT_SUCCESS;
}

7. 对某些 IP 的屏蔽(黑名单)

同样为了访问方便,我们以 IP 为键值,是否屏蔽为数值(屏蔽为 1)存储。

bool IsConfused(string IP)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("blacklist", IP);
}
void Confuse(string IP, bool confuse = true)
{
    INI ini;
    ini.open(CONFIG);
    ini.WriteInt("blacklist", IP, confuse);
}

这里,我们要修改函数 tIPServer,简单地说,被我们屏蔽了,连个线程都不开,直接关了 socket 走人。

DWORD WINAPI tIPServer(LPVOID lpParam)
{
    // lpParam 处是一个 string.
    string ip = *(string *)lpParam;
    int major = stoi(ip.substr(ip.find('@') + 1)); // 在 @ 后面寻找主版本号
    ip = ip.substr(0, ip.find('@'));
    // 创建 TCP 套接字(http 是基于 TCP 的)
    SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    // 写好绑定地址
    sockaddr_in listenaddr;
    ZeroMemory(&listenaddr, sizeof(listenaddr)); // 置零
    listenaddr.sin_addr.s_addr = inet_addr(ip.data()); // 绑定 IP
    listenaddr.sin_family = AF_INET;
    listenaddr.sin_port = htons(PORT); // 注意,这个 htons 是必需的,把主机地址转化为网络地址
    cprintf(major, 0, "Start, IP = %s\n", ip.data());
    // 绑定
    bind(listenfd, (sockaddr*)&listenaddr, sizeof(sockaddr));
    cprintf(major, 0, "bind OK\n");
    // 监听
    listen(listenfd, 20);
    cprintf(major, 0, "listen OK\n");
    // 循环接受连接请求
    SOCKET clientfd;
    sockaddr_in clientaddr;
    int tmplen = sizeof(sockaddr);
    int counter = 0;
    while((clientfd = accept(listenfd, (sockaddr*)&clientaddr, &tmplen)) != INVALID_SOCKET)
    {
        cprintf(major, 0, "收到来自 %s:%d 的连接请求\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        if(IsConfused(inet_ntoa(clientaddr.sin_addr))) // 新增判断
        {
            closesocket(clientfd);
            continue;
        }
        pair<SOCKET, pair<int, int>> psii = make_pair(clientfd, make_pair(major, (++counter))); // 把东西放进去
        CreateThread(NULL, 0, tTalk, (LPVOID)&psii, 0, NULL); // 取指针,传给 tTalk
    }
}

现在,整个代码看上去是这样的:

#include<winsock2.h>
#include<Windows.h>
#pragma comment(lib, "-lws2_32")
#include<bits/stdc++.h>

#define PORT 8080 // 绑定端口
#define BUFLEN 1048576
#define ROOT "./html"
#define VERSION "HTTP/1.1"
#define CONFIG "./config.ini"

using namespace std;

// 调试
queue<pair<int, int>> writq;
int ipcolor[] =
{
    0x04, 0x06, 0x02, 0x03, 0x01, 0x05,
    0x40, 0x46, 0x42, 0x43, 0x41, 0x45,
    0x64, 0x60, 0x62, 0x63, 0x61, 0x65,
    0x24, 0x26, 0x20, 0x23, 0x21, 0x25,
    0x34, 0x36, 0x32, 0x30, 0x31, 0x35,
    0x14, 0x16, 0x12, 0x13, 0x10, 0x15,
    0x54, 0x56, 0x52, 0x53, 0x51, 0x50
};
void col(int n = -1)
{
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), n == -1 ? 0x07 : ipcolor[n]);
}
string GetFormatTime()
{
    SYSTEMTIME st;
    GetSystemTime(&st);
    string ret = "";
    ret += to_string(st.wYear) + "/" + to_string(st.wMonth) + "/" + to_string(st.wDay) + " ";
    ret += to_string(st.wHour) + ":" + to_string(st.wMinute) + ":" + to_string(st.wSecond) + "." + to_string(st.wMilliseconds);
    return ret;
}
void cprintf(int major, int minor, const char *format, ...) 
{ 
    writq.push(make_pair(major, minor)); 
    va_list va;
    char buf[BUFLEN]; 
    va_start(va, format); 
    vsprintf(buf, format, va); 
    va_end(va); 
    string tmp = buf;
    tmp = to_string(major) + "." + to_string(minor) + "@" + GetFormatTime() + " [LOG] " + tmp;
    while (writq.front().first != major || writq.front().second != minor) ;
    col(major);
    cout << tmp << flush;
    col();
    writq.pop();
}

// 文件操作
bool IsFileExist(string path)
{
    FILE *f = fopen(path.data(), "r");
    bool ret = f == NULL;
    if (!ret)
        fclose(f);
    return !ret;
}
string GetFile(string path)
{
    if (!IsFileExist(path))
    {
        cprintf(33, 0, "request file %s not exist\n", path.data());
        return "";
    }
    cprintf(33, 0, "request file %s succ\n", path.data());
    ifstream ifs(path, ios::in | ios::binary);
    // 获取文件所有内容
    stringstream ss;
    ss << ifs.rdbuf();
    ifs.close();
    return ss.str();
}

// 配置文件读写有关函数
class INI
{
protected:
    char file[BUFLEN];
public:
    void open(const char *File)
    {
        strcpy(file, File);
    }
    string Read(string Sec, string Key)
    {
        string ret = "";
        char *buf = new char[BUFLEN];
        GetPrivateProfileStringA(Sec.data(), Key.data(), "", buf, BUFLEN, file);
        ret = buf;
        delete[]buf;
        return ret;
    }
    int ReadInt(string Sec, string Key)
    {
        return GetPrivateProfileIntA(Sec.data(), Key.data(), 0, file);
    }
    void Write(string Sec, string Key, string Value)
    {
        WritePrivateProfileStringA(Sec.data(), Key.data(), Value.data(), file);
    }
    void WriteInt(string Sec, string Key, int Value)
    {
        WritePrivateProfileStringA(Sec.data(), Key.data(), to_string(Value).data(), file);
    }
};
string GetErrorPage(int ErrorCode)
{
    INI ini;
    ini.open(CONFIG);
    string ret = ini.Read("errc", to_string(ErrorCode));
    return ret == "" ? "/err/" + to_string(ErrorCode) + ".htm" : ret; // 默认为 /err/*.htm
}
void SetErrorPage(int ErrCode, string path)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("errc", to_string(ErrCode), path);
}
string GetRedirect(string file)
{
    INI ini;
    ini.open(CONFIG);
    string ret = ini.Read("redir", file);
    return ret;
}
void SetRedirect(string file, string redir)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("redir", file, redir);
}
bool IsForbidden(string file)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("attr", file);
}
void SetFileAttrib(string file, bool forbidden = true)
{
    INI ini;
    ini.open(CONFIG);
    ini.WriteInt("attr", file, forbidden);
}
bool IsConfused(string IP)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("blacklist", IP);
}
void Confuse(string IP, bool confuse = true)
{
    INI ini;
    ini.open(CONFIG);
    ini.WriteInt("blacklist", IP, confuse);
}

// 线程
DWORD WINAPI tTalk(LPVOID lpParam)
{
    pair<SOCKET, pair<int, int>> psii; // 前一个是客户端 socket,后面是编号
    psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
    // 获取 socket 与主次编号
    SOCKET clientfd = psii.first;
    int major = psii.second.first;
    int minor = psii.second.second;

    char *buf = new char[BUFLEN];
    int len = recv(clientfd, buf, BUFLEN, 0);
    string message(buf, len);
    delete []buf;
    cprintf(major, minor, "msg:\n%s\n", message.data());

    // 处理 http 请求
    stringstream ss(message); // 字符串流
    string reqtype;
    ss >> reqtype;
    if(reqtype == "GET") // 确保是 GET 方法
    {
        string path;
        ss >> path;
        path = ROOT + path;
        string version;
        ss >> version;
        message = VERSION" ";
        if(GetRedirect(path) != "") // 重定向优先,不然文件一大堆
        {
            message += "302 Found\nLocation: " + GetRedirect(path).substr(strlen(ROOT)) + "\n";
        }
        else if(IsForbidden(path)) // 403
        {
            message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(403));
        }
        else if(!IsFileExist(path)) // 404
        {
            message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(404));
        }
        else
        {
            message += "200 OK\n\n" + GetFile(path);
        }
    }

    send(clientfd, message.data(), message.size(), 0); // http Server
    // 处理完后关闭
    shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
    closesocket(clientfd);
    return EXIT_SUCCESS;
}
DWORD WINAPI tIPServer(LPVOID lpParam)
{
    // lpParam 处是一个 string.
    string ip = *(string *)lpParam;
    int major = stoi(ip.substr(ip.find('@') + 1)); // 在 @ 后面寻找主版本号
    ip = ip.substr(0, ip.find('@'));
    // 创建 TCP 套接字(http 是基于 TCP 的)
    SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    // 写好绑定地址
    sockaddr_in listenaddr;
    ZeroMemory(&listenaddr, sizeof(listenaddr)); // 置零
    listenaddr.sin_addr.s_addr = inet_addr(ip.data()); // 绑定 IP
    listenaddr.sin_family = AF_INET;
    listenaddr.sin_port = htons(PORT); // 注意,这个 htons 是必需的,把主机地址转化为网络地址
    cprintf(major, 0, "Start, IP = %s\n", ip.data());
    // 绑定
    bind(listenfd, (sockaddr*)&listenaddr, sizeof(sockaddr));
    cprintf(major, 0, "bind OK\n");
    // 监听
    listen(listenfd, 20);
    cprintf(major, 0, "listen OK\n");
    // 循环接受连接请求
    SOCKET clientfd;
    sockaddr_in clientaddr;
    int tmplen = sizeof(sockaddr);
    int counter = 0;
    while((clientfd = accept(listenfd, (sockaddr*)&clientaddr, &tmplen)) != INVALID_SOCKET)
    {
        cprintf(major, 0, "收到来自 %s:%d 的连接请求\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        if(IsConfused(inet_ntoa(clientaddr.sin_addr)))
        {
            closesocket(clientfd);
            continue;
        }
        pair<SOCKET, pair<int, int>> psii = make_pair(clientfd, make_pair(major, (++counter))); // 把东西放进去
        CreateThread(NULL, 0, tTalk, (LPVOID)&psii, 0, NULL); // 取指针,传给 tTalk
    }
}

int main()
{
    // 在 Windows 下需要加上这一段。
    WSADATA wsd;
    WSAStartup(MAKEWORD(2, 2), &wsd);
    // 获取所有 IP
    string strIP("127.0.0.1");
    vector<string> ret;
    ret.clear();
    ret.push_back(strIP + "@0");
    cprintf(0, 0, "127.0.0.1\n");
    char strName[1024];
    gethostname(strName, 1024);
    struct hostent *pHostEnt = gethostbyname(strName);
    int n = 0;
    while (pHostEnt->h_addr_list[n] != NULL)
    {
        stringstream ss;
        ss << (pHostEnt->h_addr_list[n][0] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][1] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][2] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][3] & 0x00FF) << flush;
        string strTemp(ss.str());
        cprintf(n + 1, 0, (strTemp + "\n").data());
        if (string::npos == strTemp.find("127.0.0."))
        {
            ret.push_back(strTemp + "@" + to_string(n + 1)); // 此处的 @ 是用于分隔 IP 与编号
        }
        n++;
    }
    // 对每个 IP 启动线程
    for(int i = 0; i < ret.size(); i++)
    {
        CreateThread(NULL, 0, tIPServer, (LPVOID)&ret[i], 0, NULL);
    }
    // 死循环,不要提前退了
    while(1);
    // 释放 socket 资源
    WSACleanup();
}

四、指令

为了方便更改配置,我们要加一个功能:指令。

1. 指令窗口

我们为什么选择新建窗口而非将原来的控制台窗口分屏?原因只有一点:方便。想想,分屏需要搞一大堆,最后好处只有共享所有变量。而分窗口呢?每个窗口是独立了,但是仍然可以通过共享文件、Pipe 甚至再开一个 socket 进行通信。(我之前的实现是 Pipe 把指令传回主窗口,但现在想想,好像在从窗口执行就行了,我就把原来的 4.2 “主从窗口之间的 Pipe 通信”删了)

那好,如何启动新窗口?ShellExecute.函数原型如下:

HINSTANCE ShellExecuteA(
  HWND   hwnd,
  LPCSTR lpOperation,
  LPCSTR lpFile,
  LPCSTR lpParameters,
  LPCSTR lpDirectory,
  INT    nShowCmd
);

参数 hwnd 为父窗口句柄,一般设为 NULLlpOperation 为操作类型,有 open,print,explore,edit,find,runas 等。NULL 为使用默认方式。nShowCmd 为显示类型,为 CW_ 开头的宏,与 ShowWindow 函数共用。

另外,我们知道,程序的参数第一位(argv[0])总是文件名,所以,我们可以把 main 函数改为 httpmain,在 main 函数中建立条件分支,启动从窗口。

int main(int argc, char *argv[])
{
    if(argc == 1) // 主窗口
    {
        ShellExecuteA(NULL, "open", argv[0], "cmd", NULL, SW_SHOW);
        httpmain();
    }
    else // 从窗口
    {
        commandmain();
    }
}

这里,commandmain 函数为指令输入 \& 处理函数。我们先实现输入的逻辑。

void commandmain()
{
    while(true)
    {
        col(3);
        cout << "Input command > " << flush;
        string cmd;
        col(2);
        getline(cin, cmd);
        col(0);
        cout << "command response:" << endl;
        col(5);
        cout << process(cmd) << endl;
        col();
    }
}

这下好了,又多了一个 process 函数。没事,我们留到下一节。

2. 指令处理

我们先定义好可用指令。看看上面有哪些关于配置文件的?错误页,重定向,文件权限,IP 屏蔽。我们对应出五种指令:

errorpage 错误码 [文件]
redirect 网址 [重定向目标]
attrib 文件 [--public | --private]
blacklist IP [--confuse | --accept]
help

是不是很震惊?还有 help!没事,接着来。

string process(string cmd)
{

}

由于 Windows 不区分大小写,所以我们可以转成全小写。

string process(string cmd)
{
    for(auto u : cmd)
    {
        u = isupper(u) ? tolower(u) : u;
    }
}

字符串流读写,先读出指令:

string process(string cmd)
{
    for(auto u : cmd)
    {
        u = isupper(u) ? tolower(u) : u;
    }
    stringstream ss;
    ss << cmd;
    string type;
    ss >> type;
    if(type == "errorpage")
    {

    }
    else if(type == "redirect")
    {

    }
    else if(type == "attrib")
    {

    }
    else if(type == "blacklist")
    {

    }
    else // help
    {
        return "errorpage 错误码 [文件]\nredirect 网址 [重定向目标]\nattrib 文件 [--public | --private]\nblacklist IP [--confuse | --accept]";
    }
}

接下来,实现每个的逻辑(由于这个和命令行相似,我们可以使用不要钱的 CommandLineToArgvA):

LPSTR* CommandLineToArgvA(LPCSTR lpCmdLine, INT *pNumArgs) // https://blog.csdn.net/qq_22000459/article/details/83539644
{
    int retval;
    retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1, NULL, 0);
    if (!SUCCEEDED(retval))
        return NULL;

    LPWSTR lpWideCharStr = (LPWSTR)malloc(retval * sizeof(WCHAR));
    if (lpWideCharStr == NULL)
        return NULL;

    retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1, lpWideCharStr, retval);
    if (!SUCCEEDED(retval))
    {
        free(lpWideCharStr);
        return NULL;
    }

    int numArgs;
    LPWSTR* args;
    args = CommandLineToArgvW(lpWideCharStr, &numArgs);
    free(lpWideCharStr);
    if (args == NULL)
        return NULL;

    int storage = numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++ i)
    {
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, NULL, 0, NULL, &lpUsedDefaultChar);
        if (!SUCCEEDED(retval))
        {
            LocalFree(args);
            return NULL;
        }

        storage += retval;
    }

    LPSTR* result = (LPSTR*)LocalAlloc(LMEM_FIXED, storage);
    if (result == NULL)
    {
        LocalFree(args);
        return NULL;
    }

    int bufLen = storage - numArgs * sizeof(LPSTR);
    LPSTR buffer = ((LPSTR)result) + numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++ i)
    {
        assert(bufLen > 0);
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, buffer, bufLen, NULL, &lpUsedDefaultChar);
        if (!SUCCEEDED(retval))
        {
            LocalFree(result);
            LocalFree(args);
            return NULL;
        }

        result[i] = buffer;
        buffer += retval;
        bufLen -= retval;
    }

    LocalFree(args);

    *pNumArgs = numArgs;
    return result;
}
// 真正的处理
string process(string cmd)
{
    for(auto u : cmd)
    {
        u = isupper(u) ? tolower(u) : u;
    }
    stringstream ss;
    ss << cmd;
    vector<string> argv;
    int argc;
    LPSTR *argv_ = CommandLineToArgvA(cmd.data(), &argc);
    for(int i = 0; i < argc; i++)
    {
        argv.push_back(argv_[i]);
    }
    string type = argv[0];
    if(type == "errorpage")
    {
        if(argc != 3) return "errorpage needs 2 arguments.";
        int errc;
        try
        {
            errc = stoi(argv[1]);
        }
        catch(...)
        {
            return "Invalid errorcode " + argv[1];
        }
        if(errc < 100 || errc > 999)
        {
            return "Invalid errorcode " + to_string(errc);
        }
        string file = argv[2];
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        SetErrorPage(errc, file);
        return "success: Errc " + to_string(errc) + ", File " + file;
    }
    else if(type == "redirect")
    {
        if(argc != 3) return "redirect needs 2 arguments.";
        string file(argv[2]);
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        SetRedirect(argv[1], argv[2]);
        return "success: " + argv[1] + " -> " + argv[2];
    }
    else if(type == "attrib")
    {
        if(argc != 3) return "attrib needs 2 arguments.";
        string file(argv[1]);
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        bool priv;
        if(argv[2] == "--public") priv = 0; else
        if(argv[2] == "--private") priv = 1; else
            return "Invalid argument " + argv[2];
        SetFileAttrib(file, priv);
        return "success: " + argv[1] + " forbidden: " + to_string(priv);
    }
    else if(type == "blacklist")
    {
        if(argc != 3) return "blacklist needs 2 arguments.";
        string IP = argv[1];
        if(inet_addr(IP.data()) == INADDR_NONE)
        {
            return "invalid IP address " + IP;
        }
        bool c;
        if(argv[2] == "--accept") c = 0; else
        if(argv[2] == "--confuse") c = 1; else
            return "Invalid argument " + argv[2];
        Confuse(IP, c);
        return "success: " + argv[1] + " confuse: " + to_string(c);
    }
    else // help
    {
        return "errorpage 错误码 [文件]\nredirect 网址 [重定向目标]\nattrib 文件 [--public | --private]\nblacklist IP [--confuse | --accept]";
    }
}

我们再加个查询:

LPSTR* CommandLineToArgvA(LPCSTR lpCmdLine, INT *pNumArgs) // https://blog.csdn.net/qq_22000459/article/details/83539644
{
    int retval;
    retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1, NULL, 0);
    if (!SUCCEEDED(retval))
        return NULL;

    LPWSTR lpWideCharStr = (LPWSTR)malloc(retval * sizeof(WCHAR));
    if (lpWideCharStr == NULL)
        return NULL;

    retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1, lpWideCharStr, retval);
    if (!SUCCEEDED(retval))
    {
        free(lpWideCharStr);
        return NULL;
    }

    int numArgs;
    LPWSTR* args;
    args = CommandLineToArgvW(lpWideCharStr, &numArgs);
    free(lpWideCharStr);
    if (args == NULL)
        return NULL;

    int storage = numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++ i)
    {
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, NULL, 0, NULL, &lpUsedDefaultChar);
        if (!SUCCEEDED(retval))
        {
            LocalFree(args);
            return NULL;
        }

        storage += retval;
    }

    LPSTR* result = (LPSTR*)LocalAlloc(LMEM_FIXED, storage);
    if (result == NULL)
    {
        LocalFree(args);
        return NULL;
    }

    int bufLen = storage - numArgs * sizeof(LPSTR);
    LPSTR buffer = ((LPSTR)result) + numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++ i)
    {
        assert(bufLen > 0);
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, buffer, bufLen, NULL, &lpUsedDefaultChar);
        if (!SUCCEEDED(retval))
        {
            LocalFree(result);
            LocalFree(args);
            return NULL;
        }

        result[i] = buffer;
        buffer += retval;
        bufLen -= retval;
    }

    LocalFree(args);

    *pNumArgs = numArgs;
    return result;
}
// 真正的处理
string process(string cmd)
{
    for(auto u : cmd)
    {
        u = isupper(u) ? tolower(u) : u;
    }
    stringstream ss;
    ss << cmd;
    vector<string> argv;
    int argc;
    LPSTR *argv_ = CommandLineToArgvA(cmd.data(), &argc);
    for(int i = 0; i < argc; i++)
    {
        argv.push_back(argv_[i]);
    }
    string type = argv[0];
    if(type == "errorpage")
    {
        if(argc != 2 && argc != 3) return "errorpage needs 1 or 2 arguments.";
        int errc;
        try
        {
            errc = stoi(argv[1]);
        }
        catch(...)
        {
            return "Invalid errorcode " + argv[1];
        }
        if(errc < 100 || errc > 999)
        {
            return "Invalid errorcode " + to_string(errc);
        }
        if(argc == 2)
        {
            return "errorcode " + to_string(errc) + ": " + GetErrorPage(errc);
        }
        argv[2] = ROOT + argv[2];
        string file = argv[2];
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        SetErrorPage(errc, file);
        return "success: Errc " + to_string(errc) + ", File " + file;
    }
    else if(type == "redirect")
    {
        if(argc != 2 && argc != 3) return "redirect needs 1 or 2 arguments.";
        argv[1] = ROOT + argv[1];
        if(argc == 2)
        {
            return argv[1] + " -> " + GetRedirect(argv[1]);
        }
        argv[2] = ROOT + argv[2];
        string file(argv[2]);
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        SetRedirect(argv[1], argv[2]);
        return "success: " + argv[1] + " -> " + argv[2];
    }
    else if(type == "attrib")
    {
        if(argc != 2 && argc != 3) return "attrib needs 1 or 2 arguments.";
        argv[1] = ROOT + argv[1];
        string file(argv[1]);
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        if(argc == 2)
        {
            return file + ": " + (IsForbidden(file) ? "private" : "public");
        }
        bool priv;
        if(argv[2] == "--public") priv = 0; else
        if(argv[2] == "--private") priv = 1; else
            return "Invalid argument " + argv[2];
        SetFileAttrib(file, priv);
        return "success: " + argv[1] + " forbidden: " + to_string(priv);
    }
    else if(type == "blacklist")
    {
        if(argc != 2 && argc != 3) return "blacklist needs 1 or 2 arguments.";
        string IP = argv[1];
        if(inet_addr(IP.data()) == INADDR_NONE)
        {
            return "invalid IP address " + IP;
        }
        if(argc == 2)
        {
            return IP + ": " + (IsConfused(IP) ? "confuse" : "accept");
        }
        bool c;
        if(argv[2] == "--accept") c = 0; else
        if(argv[2] == "--confuse") c = 1; else
            return "Invalid argument " + argv[2];
        Confuse(IP, c);
        return "success: " + argv[1] + " confuse: " + to_string(c);
    }
    else // help
    {
        return "errorpage 错误码 [文件]\nredirect 网址 [重定向目标]\nattrib 文件 [--public | --private]\nblacklist IP [--confuse | --accept]";
    }
}

现在,我们就已经做出了一个成型的服务器了:

#include<winsock2.h>
#include<Windows.h>
#pragma comment(lib, "-lws2_32")
#include<bits/stdc++.h>

#define PORT 8080 // 绑定端口
#define BUFLEN 1048576
#define ROOT "./html"
#define VERSION "HTTP/1.1"
#define CONFIG "./config.ini"

using namespace std;

// 调试
queue<pair<int, int>> writq;
int ipcolor[] =
{
    0x04, 0x06, 0x02, 0x03, 0x01, 0x05,
    0x40, 0x46, 0x42, 0x43, 0x41, 0x45,
    0x64, 0x60, 0x62, 0x63, 0x61, 0x65,
    0x24, 0x26, 0x20, 0x23, 0x21, 0x25,
    0x34, 0x36, 0x32, 0x30, 0x31, 0x35,
    0x14, 0x16, 0x12, 0x13, 0x10, 0x15,
    0x54, 0x56, 0x52, 0x53, 0x51, 0x50
};
void col(int n = -1)
{
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), n == -1 ? 0x07 : ipcolor[n]);
}
string GetFormatTime()
{
    SYSTEMTIME st;
    GetSystemTime(&st);
    string ret = "";
    ret += to_string(st.wYear) + "/" + to_string(st.wMonth) + "/" + to_string(st.wDay) + " ";
    ret += to_string(st.wHour) + ":" + to_string(st.wMinute) + ":" + to_string(st.wSecond) + "." + to_string(st.wMilliseconds);
    return ret;
}
void cprintf(int major, int minor, const char *format, ...) 
{ 
    writq.push(make_pair(major, minor)); 
    va_list va;
    char buf[BUFLEN]; 
    va_start(va, format); 
    vsprintf(buf, format, va); 
    va_end(va); 
    string tmp = buf;
    tmp = to_string(major) + "." + to_string(minor) + "@" + GetFormatTime() + " [LOG] " + tmp;
    while (writq.front().first != major || writq.front().second != minor) ;
    col(major);
    cout << tmp << flush;
    col();
    writq.pop();
}

// 文件操作
bool IsFileExist(string path)
{
    FILE *f = fopen(path.data(), "r");
    bool ret = f == NULL;
    if (!ret)
        fclose(f);
    return !ret;
}
string GetFile(string path)
{
    if (!IsFileExist(path))
    {
        cprintf(33, 0, "request file %s not exist\n", path.data());
        return "";
    }
    cprintf(33, 0, "request file %s succ\n", path.data());
    ifstream ifs(path, ios::in | ios::binary);
    // 获取文件所有内容
    stringstream ss;
    ss << ifs.rdbuf();
    ifs.close();
    return ss.str();
}

// 配置文件读写有关函数
class INI
{
protected:
    char file[BUFLEN];
public:
    void open(const char *File)
    {
        strcpy(file, File);
    }
    string Read(string Sec, string Key)
    {
        string ret = "";
        char *buf = new char[BUFLEN];
        GetPrivateProfileStringA(Sec.data(), Key.data(), "", buf, BUFLEN, file);
        ret = buf;
        delete[]buf;
        return ret;
    }
    int ReadInt(string Sec, string Key)
    {
        return GetPrivateProfileIntA(Sec.data(), Key.data(), 0, file);
    }
    void Write(string Sec, string Key, string Value)
    {
        WritePrivateProfileStringA(Sec.data(), Key.data(), Value.data(), file);
    }
    void WriteInt(string Sec, string Key, int Value)
    {
        WritePrivateProfileStringA(Sec.data(), Key.data(), to_string(Value).data(), file);
    }
};
string GetErrorPage(int ErrorCode)
{
    INI ini;
    ini.open(CONFIG);
    string ret = ini.Read("errc", to_string(ErrorCode));
    return ret == "" ? "/err/" + to_string(ErrorCode) + ".htm" : ret; // 默认为 /err/*.htm
}
void SetErrorPage(int ErrCode, string path)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("errc", to_string(ErrCode), path);
}
string GetRedirect(string file)
{
    INI ini;
    ini.open(CONFIG);
    string ret = ini.Read("redir", file);
    return ret;
}
void SetRedirect(string file, string redir)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("redir", file, redir);
}
bool IsForbidden(string file)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("attr", file);
}
void SetFileAttrib(string file, bool forbidden = true)
{
    INI ini;
    ini.open(CONFIG);
    ini.WriteInt("attr", file, forbidden);
}
bool IsConfused(string IP)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("blacklist", IP);
}
void Confuse(string IP, bool confuse = true)
{
    INI ini;
    ini.open(CONFIG);
    ini.WriteInt("blacklist", IP, confuse);
}

// 线程
DWORD WINAPI tTalk(LPVOID lpParam)
{
    pair<SOCKET, pair<int, int>> psii; // 前一个是客户端 socket,后面是编号
    psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
    // 获取 socket 与主次编号
    SOCKET clientfd = psii.first;
    int major = psii.second.first;
    int minor = psii.second.second;

    char *buf = new char[BUFLEN];
    int len = recv(clientfd, buf, BUFLEN, 0);
    string message(buf, len);
    delete []buf;
    cprintf(major, minor, "msg:\n%s\n", message.data());

    // 处理 http 请求
    stringstream ss(message); // 字符串流
    string reqtype;
    ss >> reqtype;
    if(reqtype == "GET") // 确保是 GET 方法
    {
        string path;
        ss >> path;
        path = ROOT + path;
        string version;
        ss >> version;
        message = VERSION" ";
        if(GetRedirect(path) != "") // 重定向优先,不然文件一大堆
        {
            message += "302 Found\nLocation: " + GetRedirect(path).substr(strlen(ROOT)) + "\n";
        }
        else if(IsForbidden(path)) // 403
        {
            message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(403));
        }
        else if(IsFileExist(path))
        {
            message += "200 OK\n\n" + GetFile(path);
        }
        else // 404
        {
            cprintf(major, minor, "%s doen't exist\n", path.data());
            message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(404));
        }
    }
    cprintf(major, minor, "Send msg to peer:\n%s\n", message.data());
    send(clientfd, message.data(), message.size(), 0); // http Server
    // 处理完后关闭
    shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
    closesocket(clientfd);
    return EXIT_SUCCESS;
}
DWORD WINAPI tIPServer(LPVOID lpParam)
{
    // lpParam 处是一个 string.
    string ip = *(string *)lpParam;
    int major = stoi(ip.substr(ip.find('@') + 1)); // 在 @ 后面寻找主版本号
    ip = ip.substr(0, ip.find('@'));
    // 创建 TCP 套接字(http 是基于 TCP 的)
    SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    // 写好绑定地址
    sockaddr_in listenaddr;
    ZeroMemory(&listenaddr, sizeof(listenaddr)); // 置零
    listenaddr.sin_addr.s_addr = inet_addr(ip.data()); // 绑定 IP
    listenaddr.sin_family = AF_INET;
    listenaddr.sin_port = htons(PORT); // 注意,这个 htons 是必需的,把主机地址转化为网络地址
    cprintf(major, 0, "Start, IP = %s\n", ip.data());
    // 绑定
    bind(listenfd, (sockaddr*)&listenaddr, sizeof(sockaddr));
    cprintf(major, 0, "bind OK\n");
    // 监听
    listen(listenfd, 20);
    cprintf(major, 0, "listen OK\n");
    // 循环接受连接请求
    SOCKET clientfd;
    sockaddr_in clientaddr;
    int tmplen = sizeof(sockaddr);
    int counter = 0;
    while((clientfd = accept(listenfd, (sockaddr*)&clientaddr, &tmplen)) != INVALID_SOCKET)
    {
        cprintf(major, 0, "收到来自 %s:%d 的连接请求\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        if(IsConfused(inet_ntoa(clientaddr.sin_addr)))
        {
            closesocket(clientfd);
            continue;
        }
        pair<SOCKET, pair<int, int>> psii = make_pair(clientfd, make_pair(major, (++counter))); // 把东西放进去
        CreateThread(NULL, 0, tTalk, (LPVOID)&psii, 0, NULL); // 取指针,传给 tTalk
    }
}

// 指令处理函数
LPSTR* CommandLineToArgvA(LPCSTR lpCmdLine, INT *pNumArgs) // https://blog.csdn.net/qq_22000459/article/details/83539644
{
    int retval;
    retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1, NULL, 0);
    if (!SUCCEEDED(retval))
        return NULL;

    LPWSTR lpWideCharStr = (LPWSTR)malloc(retval * sizeof(WCHAR));
    if (lpWideCharStr == NULL)
        return NULL;

    retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1, lpWideCharStr, retval);
    if (!SUCCEEDED(retval))
    {
        free(lpWideCharStr);
        return NULL;
    }

    int numArgs;
    LPWSTR* args;
    args = CommandLineToArgvW(lpWideCharStr, &numArgs);
    free(lpWideCharStr);
    if (args == NULL)
        return NULL;

    int storage = numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++ i)
    {
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, NULL, 0, NULL, &lpUsedDefaultChar);
        if (!SUCCEEDED(retval))
        {
            LocalFree(args);
            return NULL;
        }

        storage += retval;
    }

    LPSTR* result = (LPSTR*)LocalAlloc(LMEM_FIXED, storage);
    if (result == NULL)
    {
        LocalFree(args);
        return NULL;
    }

    int bufLen = storage - numArgs * sizeof(LPSTR);
    LPSTR buffer = ((LPSTR)result) + numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++ i)
    {
        assert(bufLen > 0);
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, buffer, bufLen, NULL, &lpUsedDefaultChar);
        if (!SUCCEEDED(retval))
        {
            LocalFree(result);
            LocalFree(args);
            return NULL;
        }

        result[i] = buffer;
        buffer += retval;
        bufLen -= retval;
    }

    LocalFree(args);

    *pNumArgs = numArgs;
    return result;
}
// 真正的处理
string process(string cmd)
{
    for(auto u : cmd)
    {
        u = isupper(u) ? tolower(u) : u;
    }
    stringstream ss;
    ss << cmd;
    vector<string> argv;
    int argc;
    LPSTR *argv_ = CommandLineToArgvA(cmd.data(), &argc);
    for(int i = 0; i < argc; i++)
    {
        argv.push_back(argv_[i]);
    }
    string type = argv[0];
    if(type == "errorpage")
    {
        if(argc != 2 && argc != 3) return "errorpage needs 1 or 2 arguments.";
        int errc;
        try
        {
            errc = stoi(argv[1]);
        }
        catch(...)
        {
            return "Invalid errorcode " + argv[1];
        }
        if(errc < 100 || errc > 999)
        {
            return "Invalid errorcode " + to_string(errc);
        }
        if(argc == 2)
        {
            return "errorcode " + to_string(errc) + ": " + GetErrorPage(errc);
        }
        argv[2] = ROOT + argv[2];
        string file = argv[2];
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        SetErrorPage(errc, file);
        return "success: Errc " + to_string(errc) + ", File " + file;
    }
    else if(type == "redirect")
    {
        if(argc != 2 && argc != 3) return "redirect needs 1 or 2 arguments.";
        argv[1] = ROOT + argv[1];
        if(argc == 2)
        {
            return argv[1] + " -> " + GetRedirect(argv[1]);
        }
        argv[2] = ROOT + argv[2];
        string file(argv[2]);
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        SetRedirect(argv[1], argv[2]);
        return "success: " + argv[1] + " -> " + argv[2];
    }
    else if(type == "attrib")
    {
        if(argc != 2 && argc != 3) return "attrib needs 1 or 2 arguments.";
        argv[1] = ROOT + argv[1];
        string file(argv[1]);
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        if(argc == 2)
        {
            return file + ": " + (IsForbidden(file) ? "private" : "public");
        }
        bool priv;
        if(argv[2] == "--public") priv = 0; else
        if(argv[2] == "--private") priv = 1; else
            return "Invalid argument " + argv[2];
        SetFileAttrib(file, priv);
        return "success: " + argv[1] + " forbidden: " + to_string(priv);
    }
    else if(type == "blacklist")
    {
        if(argc != 2 && argc != 3) return "blacklist needs 1 or 2 arguments.";
        string IP = argv[1];
        if(inet_addr(IP.data()) == INADDR_NONE)
        {
            return "invalid IP address " + IP;
        }
        if(argc == 2)
        {
            return IP + ": " + (IsConfused(IP) ? "confuse" : "accept");
        }
        bool c;
        if(argv[2] == "--accept") c = 0; else
        if(argv[2] == "--confuse") c = 1; else
            return "Invalid argument " + argv[2];
        Confuse(IP, c);
        return "success: " + argv[1] + " confuse: " + to_string(c);
    }
    else // help
    {
        return "errorpage 错误码 [文件]\nredirect 网址 [重定向目标]\nattrib 文件 [--public | --private]\nblacklist IP [--confuse | --accept]";
    }
}

// 各种主函数
int httpmain()
{
    // 在 Windows 下需要加上这一段。
    WSADATA wsd;
    WSAStartup(MAKEWORD(2, 2), &wsd);
    // 获取所有 IP
    string strIP("127.0.0.1");
    vector<string> ret;
    ret.clear();
    ret.push_back(strIP + "@0");
    cprintf(0, 0, "127.0.0.1\n");
    char strName[1024];
    gethostname(strName, 1024);
    struct hostent *pHostEnt = gethostbyname(strName);
    int n = 0;
    while (pHostEnt->h_addr_list[n] != NULL)
    {
        stringstream ss;
        ss << (pHostEnt->h_addr_list[n][0] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][1] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][2] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][3] & 0x00FF) << flush;
        string strTemp(ss.str());
        cprintf(n + 1, 0, (strTemp + "\n").data());
        if (string::npos == strTemp.find("127.0.0."))
        {
            ret.push_back(strTemp + "@" + to_string(n + 1)); // 此处的 @ 是用于分隔 IP 与编号
        }
        n++;
    }
    // 对每个 IP 启动线程
    for(int i = 0; i < ret.size(); i++)
    {
        CreateThread(NULL, 0, tIPServer, (LPVOID)&ret[i], 0, NULL);
    }
    // 死循环,不要提前退了
    while(1);
    // 释放 socket 资源
    WSACleanup();
    return 0;
}
void commandmain()
{
    while(true)
    {
        col(3);
        cout << "Input command > " << flush;
        string cmd;
        col(2);
        getline(cin, cmd);
        col(0);
        cout << "command response:" << endl;
        col(5);
        cout << process(cmd) << endl;
        col();
    }
}
int main(int argc, char *argv[])
{
    if(argc == 1) // 主窗口
    {
        ShellExecuteA(NULL, "open", argv[0], "cmd", NULL, SW_SHOW);
        httpmain();
    }
    else // 从窗口
    {
        commandmain();
    }
}

五、登录

1. 处理 POST 请求(application/x-www-form-urlencoded

假设我们的 html 长这样:

<!DOCTYPE html>
<html>
<head><title>demo</title></head>
<body>
    <form enctype="application/x-www-form-urlencoded" method="POST" action="/post">
        <input type="text" name="name" value="value"><input type="submit" value="submit">
    </form>
</body>
</html>

那么,当客户点击 submit 按钮时,我们会受到这么一条 POST 消息(本地测试):

POST /post HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 10
Cache-Control: max-age=0
sec-ch-ua: "Not_A Brand";v="99", "Microsoft Edge";v="109", "Chromium";v="109"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.140
Origin: http://localhost:8080
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8080/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

name=value

其中,第二至二十行都是标头,对我们数据处理没多大用,只除了第 12 行 Content-Type,告诉我们,这个数据是什么类型的。到了 multipart/form-data,我们还能看到 boundary。看到第 22 行,这个 name=value,这个便是用户的输入。若有多组,则用 & 号隔开。我们可以在 tTalk 中增加一个条件分支(前面语句经过更新添加了一个用于分析的 HTTP 类):

#include<winsock2.h>
#include<Windows.h>
#pragma comment(lib, "-lws2_32")
#include<bits/stdc++.h>

#define PORT 8080 // 绑定端口
#define BUFLEN 1048576
#define ROOT "./html"
#define VERSION "HTTP/1.1"
#define CONFIG "./config.ini"

using namespace std;

// 调试
queue<pair<int, int>> writq;
int ipcolor[] =
{
    0x04, 0x06, 0x02, 0x03, 0x01, 0x05,
    0x40, 0x46, 0x42, 0x43, 0x41, 0x45,
    0x64, 0x60, 0x62, 0x63, 0x61, 0x65,
    0x24, 0x26, 0x20, 0x23, 0x21, 0x25,
    0x34, 0x36, 0x32, 0x30, 0x31, 0x35,
    0x14, 0x16, 0x12, 0x13, 0x10, 0x15,
    0x54, 0x56, 0x52, 0x53, 0x51, 0x50
};
void col(int n = -1)
{
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), n == -1 ? 0x07 : ipcolor[n]);
}
string GetFormatTime()
{
    SYSTEMTIME st;
    GetSystemTime(&st);
    string ret = "";
    ret += to_string(st.wYear) + "/" + to_string(st.wMonth) + "/" + to_string(st.wDay) + " ";
    ret += to_string(st.wHour) + ":" + to_string(st.wMinute) + ":" + to_string(st.wSecond) + "." + to_string(st.wMilliseconds);
    return ret;
}
void cprintf(int major, int minor, const char *format, ...) 
{ 
    writq.push(make_pair(major, minor)); 
    va_list va;
    char buf[BUFLEN]; 
    va_start(va, format); 
    vsprintf(buf, format, va); 
    va_end(va); 
    string tmp = buf;
    tmp = to_string(major) + "." + to_string(minor) + "@" + GetFormatTime() + " [LOG] " + tmp;
    while (writq.front().first != major || writq.front().second != minor) ;
    col(major);
    cout << tmp << flush;
    col();
    writq.pop();
}

// 文件操作
bool IsFileExist(string path)
{
    FILE *f = fopen(path.data(), "r");
    bool ret = f == NULL;
    if (!ret)
        fclose(f);
    return !ret;
}
string GetFile(string path)
{
    if (!IsFileExist(path))
    {
        cprintf(33, 0, "request file %s not exist\n", path.data());
        return "";
    }
    cprintf(33, 0, "request file %s succ\n", path.data());
    ifstream ifs(path, ios::in | ios::binary);
    // 获取文件所有内容
    stringstream ss;
    ss << ifs.rdbuf();
    ifs.close();
    return ss.str();
}

// 配置文件读写有关函数
class INI
{
protected:
    char file[BUFLEN];
public:
    void open(const char *File)
    {
        strcpy(file, File);
    }
    string Read(string Sec, string Key)
    {
        string ret = "";
        char *buf = new char[BUFLEN];
        GetPrivateProfileStringA(Sec.data(), Key.data(), "", buf, BUFLEN, file);
        ret = buf;
        delete[]buf;
        return ret;
    }
    int ReadInt(string Sec, string Key)
    {
        return GetPrivateProfileIntA(Sec.data(), Key.data(), 0, file);
    }
    void Write(string Sec, string Key, string Value)
    {
        WritePrivateProfileStringA(Sec.data(), Key.data(), Value.data(), file);
    }
    void WriteInt(string Sec, string Key, int Value)
    {
        WritePrivateProfileStringA(Sec.data(), Key.data(), to_string(Value).data(), file);
    }
};
string GetErrorPage(int ErrorCode)
{
    INI ini;
    ini.open(CONFIG);
    string ret = ini.Read("errc", to_string(ErrorCode));
    return ret == "" ? "/err/" + to_string(ErrorCode) + ".htm" : ret; // 默认为 /err/*.htm
}
void SetErrorPage(int ErrCode, string path)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("errc", to_string(ErrCode), path);
}
string GetRedirect(string file)
{
    INI ini;
    ini.open(CONFIG);
    string ret = ini.Read("redir", file);
    return ret;
}
void SetRedirect(string file, string redir)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("redir", file, redir);
}
bool IsForbidden(string file)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("attr", file);
}
void SetFileAttrib(string file, bool forbidden = true)
{
    INI ini;
    ini.open(CONFIG);
    ini.WriteInt("attr", file, forbidden);
}
bool IsConfused(string IP)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("blacklist", IP);
}
void Confuse(string IP, bool confuse = true)
{
    INI ini;
    ini.open(CONFIG);
    ini.WriteInt("blacklist", IP, confuse);
}

// http 标头处理
typedef vector<pair<string, string>> Header;
typedef map<string, string> Headerlist;
class HTTP // 这个类到了 multipart 就会很方便
{
public:
    Header header;
    Headerlist header_list;
    string request_type;
    string request_path;
    string http_version;
    string content;
    void ParseHttp(string http)
    {
        header_list.clear();
        header.clear();
        stringstream ss;
        ss << http;
        string fl;
        getline(ss, fl);
        stringstream ss2; // 不知道为啥不这样会崩
        ss2 << fl;
        string rp;
        request_path.clear();
        ss2 >> request_type >> rp >> http_version;
        for(int i = 0; i < rp.size(); i++)
        {
            if(rp[i] != '%')
            {
                request_path.push_back(rp[i]);
            }
            else // %20 等特殊字符处理
            {
                auto h2d = [&](char c) -> int { return isupper(c) ? c - 'A' + 10 : islower(c) ? c - 'a' + 10 : c - '0'; };
                char c = (h2d(rp[++i]) << 4) | h2d(rp[++i]);
                request_path.push_back(c);
            }
        }
        while(getline(ss, fl) && fl != "")
        {
            int pos = fl.find_first_of(':');
            string lhs = fl.substr(0, pos), rhs = fl.substr(pos + 2);
            for(auto u : lhs)
            {
                u = isupper(u) ? tolower(u) : u;
            }
            header.push_back(make_pair(lhs, rhs));
            header_list[lhs] = rhs;
        }
        content = http.substr(http.find("\n\n") + 2);
    }
    void operator()(string http)
    {
        ParseHttp(http);
    }
};

// 线程
DWORD WINAPI tTalk(LPVOID lpParam)
{
    pair<SOCKET, pair<int, int>> psii; // 前一个是客户端 socket,后面是编号
    psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
    // 获取 socket 与主次编号
    SOCKET clientfd = psii.first;
    int major = psii.second.first;
    int minor = psii.second.second;

    char *buf = new char[BUFLEN];
    int len = recv(clientfd, buf, BUFLEN, 0);
    string message(buf, len);
    delete []buf;
    cprintf(major, minor, "msg:\n%s\n", message.data());

    // 处理 http 请求
//  stringstream ss(message); // 字符串流
    HTTP http;
    http.ParseHttp(message);
    string reqtype = http.request_type;
//  ss >> reqtype;
    if(reqtype == "GET") // 确保是 GET 方法
    {
        string path = http.request_path;
//      ss >> path;
        path = ROOT + path;
        string version = http.http_version;
//      ss >> version;
        message = VERSION" ";
        if(GetRedirect(path) != "") // 重定向优先,不然文件一大堆
        {
            message += "302 Found\nLocation: " + GetRedirect(path).substr(strlen(ROOT)) + "\n";
        }
        else if(IsForbidden(path)) // 403
        {
            message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(403));
        }
        else if(IsFileExist(path))
        {
            message += "200 OK\n\n" + GetFile(path);
        }
        else // 404
        {
            cprintf(major, minor, "%s doen't exist\n", path.data());
            message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(404));
        }
    }
    else if(reqtype == "POST")
    {
        if(http.header_list["content-type"].find("application/x-www-form-urlencoded"))
        {
            map<string, string> key_value;
            string s;
            stringstream ss(http.content);
            while(getline(ss, s, '&'))
            {
                int pos = s.find('=');
                key_value[s.substr(0, pos)] = s.substr(pos + 1);
            }
            // login
        }
        else if(http.header_list["content-type"].find("multipart/form-data"))
        {
            string boundary = http.header_list["content-type"].substr(
                http.header_list["content-type"].find("; ") + 2
            );
            // multipart 处理语句
        }
    }
    cprintf(major, minor, "Send msg to peer:\n%s\n", message.data());
    send(clientfd, message.data(), message.size(), 0); // http Server
    // 处理完后关闭
    shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
    closesocket(clientfd);
    return EXIT_SUCCESS;
}
DWORD WINAPI tIPServer(LPVOID lpParam)
{
    // lpParam 处是一个 string.
    string ip = *(string *)lpParam;
    int major = stoi(ip.substr(ip.find('@') + 1)); // 在 @ 后面寻找主版本号
    ip = ip.substr(0, ip.find('@'));
    // 创建 TCP 套接字(http 是基于 TCP 的)
    SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    // 写好绑定地址
    sockaddr_in listenaddr;
    ZeroMemory(&listenaddr, sizeof(listenaddr)); // 置零
    listenaddr.sin_addr.s_addr = inet_addr(ip.data()); // 绑定 IP
    listenaddr.sin_family = AF_INET;
    listenaddr.sin_port = htons(PORT); // 注意,这个 htons 是必需的,把主机地址转化为网络地址
    cprintf(major, 0, "Start, IP = %s\n", ip.data());
    // 绑定
    bind(listenfd, (sockaddr*)&listenaddr, sizeof(sockaddr));
    cprintf(major, 0, "bind OK\n");
    // 监听
    listen(listenfd, 20);
    cprintf(major, 0, "listen OK\n");
    // 循环接受连接请求
    SOCKET clientfd;
    sockaddr_in clientaddr;
    int tmplen = sizeof(sockaddr);
    int counter = 0;
    while((clientfd = accept(listenfd, (sockaddr*)&clientaddr, &tmplen)) != INVALID_SOCKET)
    {
        cprintf(major, 0, "收到来自 %s:%d 的连接请求\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        if(IsConfused(inet_ntoa(clientaddr.sin_addr)))
        {
            closesocket(clientfd);
            continue;
        }
        pair<SOCKET, pair<int, int>> psii = make_pair(clientfd, make_pair(major, (++counter))); // 把东西放进去
        CreateThread(NULL, 0, tTalk, (LPVOID)&psii, 0, NULL); // 取指针,传给 tTalk
    }
}

// 指令处理函数
LPSTR* CommandLineToArgvA(LPCSTR lpCmdLine, INT *pNumArgs) // https://blog.csdn.net/qq_22000459/article/details/83539644
{
    int retval;
    retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1, NULL, 0);
    if (!SUCCEEDED(retval))
        return NULL;

    LPWSTR lpWideCharStr = (LPWSTR)malloc(retval * sizeof(WCHAR));
    if (lpWideCharStr == NULL)
        return NULL;

    retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1, lpWideCharStr, retval);
    if (!SUCCEEDED(retval))
    {
        free(lpWideCharStr);
        return NULL;
    }

    int numArgs;
    LPWSTR* args;
    args = CommandLineToArgvW(lpWideCharStr, &numArgs);
    free(lpWideCharStr);
    if (args == NULL)
        return NULL;

    int storage = numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++ i)
    {
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, NULL, 0, NULL, &lpUsedDefaultChar);
        if (!SUCCEEDED(retval))
        {
            LocalFree(args);
            return NULL;
        }

        storage += retval;
    }

    LPSTR* result = (LPSTR*)LocalAlloc(LMEM_FIXED, storage);
    if (result == NULL)
    {
        LocalFree(args);
        return NULL;
    }

    int bufLen = storage - numArgs * sizeof(LPSTR);
    LPSTR buffer = ((LPSTR)result) + numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++ i)
    {
        assert(bufLen > 0);
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, buffer, bufLen, NULL, &lpUsedDefaultChar);
        if (!SUCCEEDED(retval))
        {
            LocalFree(result);
            LocalFree(args);
            return NULL;
        }

        result[i] = buffer;
        buffer += retval;
        bufLen -= retval;
    }

    LocalFree(args);

    *pNumArgs = numArgs;
    return result;
}
// 真正的处理
string process(string cmd)
{
    for(auto u : cmd)
    {
        u = isupper(u) ? tolower(u) : u;
    }
    stringstream ss;
    ss << cmd;
    vector<string> argv;
    int argc;
    LPSTR *argv_ = CommandLineToArgvA(cmd.data(), &argc);
    for(int i = 0; i < argc; i++)
    {
        argv.push_back(argv_[i]);
    }
    string type = argv[0];
    if(type == "errorpage")
    {
        if(argc != 2 && argc != 3) return "errorpage needs 1 or 2 arguments.";
        int errc;
        try
        {
            errc = stoi(argv[1]);
        }
        catch(...)
        {
            return "Invalid errorcode " + argv[1];
        }
        if(errc < 100 || errc > 999)
        {
            return "Invalid errorcode " + to_string(errc);
        }
        if(argc == 2)
        {
            return "errorcode " + to_string(errc) + ": " + GetErrorPage(errc);
        }
        argv[2] = ROOT + argv[2];
        string file = argv[2];
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        SetErrorPage(errc, file);
        return "success: Errc " + to_string(errc) + ", File " + file;
    }
    else if(type == "redirect")
    {
        if(argc != 2 && argc != 3) return "redirect needs 1 or 2 arguments.";
        argv[1] = ROOT + argv[1];
        if(argc == 2)
        {
            return argv[1] + " -> " + GetRedirect(argv[1]);
        }
        argv[2] = ROOT + argv[2];
        string file(argv[2]);
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        SetRedirect(argv[1], argv[2]);
        return "success: " + argv[1] + " -> " + argv[2];
    }
    else if(type == "attrib")
    {
        if(argc != 2 && argc != 3) return "attrib needs 1 or 2 arguments.";
        argv[1] = ROOT + argv[1];
        string file(argv[1]);
        if(!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        if(argc == 2)
        {
            return file + ": " + (IsForbidden(file) ? "private" : "public");
        }
        bool priv;
        if(argv[2] == "--public") priv = 0; else
            if(argv[2] == "--private") priv = 1; else
            return "Invalid argument " + argv[2];
        SetFileAttrib(file, priv);
        return "success: " + argv[1] + " forbidden: " + to_string(priv);
    }
    else if(type == "blacklist")
    {
        if(argc != 2 && argc != 3) return "blacklist needs 1 or 2 arguments.";
        string IP = argv[1];
        if(inet_addr(IP.data()) == INADDR_NONE)
        {
            return "invalid IP address " + IP;
        }
        if(argc == 2)
        {
            return IP + ": " + (IsConfused(IP) ? "confuse" : "accept");
        }
        bool c;
        if(argv[2] == "--accept") c = 0; else
            if(argv[2] == "--confuse") c = 1; else
            return "Invalid argument " + argv[2];
        Confuse(IP, c);
        return "success: " + argv[1] + " confuse: " + to_string(c);
    }
    else // help
    {
        return "errorpage 错误码 [文件]\nredirect 网址 [重定向目标]\nattrib 文件 [--public | --private]\nblacklist IP [--confuse | --accept]";
    }
}

// 各种主函数
int httpmain()
{
    // 在 Windows 下需要加上这一段。
    WSADATA wsd;
    WSAStartup(MAKEWORD(2, 2), &wsd);
    // 获取所有 IP
    string strIP("127.0.0.1");
    vector<string> ret;
    ret.clear();
    ret.push_back(strIP + "@0");
    cprintf(0, 0, "127.0.0.1\n");
    char strName[1024];
    gethostname(strName, 1024);
    struct hostent *pHostEnt = gethostbyname(strName);
    int n = 0;
    while (pHostEnt->h_addr_list[n] != NULL)
    {
        stringstream ss;
        ss << (pHostEnt->h_addr_list[n][0] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][1] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][2] & 0x00FF) << "."
        << (pHostEnt->h_addr_list[n][3] & 0x00FF) << flush;
        string strTemp(ss.str());
        cprintf(n + 1, 0, (strTemp + "\n").data());
        if (string::npos == strTemp.find("127.0.0."))
        {
            ret.push_back(strTemp + "@" + to_string(n + 1)); // 此处的 @ 是用于分隔 IP 与编号
        }
        n++;
    }
    // 对每个 IP 启动线程
    for(int i = 0; i < ret.size(); i++)
    {
        CreateThread(NULL, 0, tIPServer, (LPVOID)&ret[i], 0, NULL);
    }
    // 死循环,不要提前退了
    while(1);
    // 释放 socket 资源
    WSACleanup();
    return 0;
}
void commandmain()
{
    while(true)
    {
        col(3);
        cout << "Input command > " << flush;
        string cmd;
        col(2);
        getline(cin, cmd);
        col(0);
        cout << "command response:" << endl;
        col(5);
        cout << process(cmd) << endl;
        col();
    }
}
int main(int argc, char *argv[])
{
    if(argc == 1) // 主窗口
    {
        ShellExecuteA(NULL, "open", argv[0], "cmd", NULL, SW_SHOW);
        httpmain();
    }
    else // 从窗口
    {
        commandmain();
    }
}

2. 登录、状态保持(记入 ini)

登录,这个看似简单的动作,我们要修改这些:

当然,加登录页时,不能忘记加一个注册页。

配置文件中,我选用六个项存储用户数据:

然后,用 page 存储登录页、注册页。

这绝对是个大改动。我决定把这分几节讲。

从后端开始:配置文件

这是配置文件中关于用户、页的一个例子:

[id2user]
total=1919810
...
346134=hdkghc
...
[user2id]
...
hdkghc=346134
...
[password]
...
346134=xxxxxxxxxxxxxxxx(经过 MD5 处理后的密码)
...
[lastactive]
...
346134=xxxxxxxxxx(使用 time 函数获得的 UNIX 时间戳)
...
[lastip]
...
346134=127.0.0.1
...
[ip2id]
...
127.0.0.1=346134
...
[page]
login=./html/login
register=./html/register

添加函数

我们从一个用户的功能上列举:

那么,我们需要实现这些功能函数:

  1. 通过 ID 获取用户名(没有则为空串)
  2. 通过用户名获取 ID(没有则为 0)
  3. 修改对应 ID 的用户名(有返回值)
  4. 获取 ID 的加密密码
  5. 更改 ID 的密码(有返回值)
  6. 获取 IP 登录的 ID
  7. 刷新,当一个用户不活跃时间达到 n 天以上时自动退登
  8. 登录/退登
  9. 注册

然后,我们的 MD5 长这样:

namespace MD5
{
    #define A 0x67452301
    #define B 0xefcdab89
    #define C 0x98badcfe
    #define D 0x10325476
    const char str16[] = "0123456789abcdef";
    const unsigned int T[] =
    {
        0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
        0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
        0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
        0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
        0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
        0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
        0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
        0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
        0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
        0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
        0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
        0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
        0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
        0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
        0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
        0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
    };
    const unsigned int s[] =
    {
        7, 12, 17, 22, 7 , 12, 17, 22, 7 , 12, 17, 22, 7 , 12, 17, 22,
        5, 9 , 14, 20, 5 , 9 , 14, 20, 5 , 9 , 14, 20, 5 , 9 , 14, 20,
        4, 11, 16, 23, 4 , 11, 16, 23, 4 , 11, 16, 23, 4 , 11, 16, 23,
        6, 10, 15, 21, 6 , 10, 15, 21, 6 , 10, 15, 21, 6 , 10, 15, 21
    };
    class MD5
    {
    private:
        unsigned int tempA, tempB, tempC, tempD, strlength;
    public:
        unsigned int F(unsigned int b, unsigned int c, unsigned int d)
        {
            return (b & c) | ((~b) & d);
        }
        unsigned int G(unsigned int b, unsigned int c, unsigned int d)
        {
            return (b & d) | (c & (~d));
        }
        unsigned int H(unsigned int b, unsigned int c, unsigned int d)
        {
            return b ^ c ^ d;
        }
        unsigned int I(unsigned int b, unsigned int c, unsigned int d)
        {
            return c ^ (b | (~d));
        }
        unsigned int shift(unsigned int a, unsigned int n)
        {
            return (a << n) | (a >> (32 - n));
        }
        string encode(string src)
        {
            tempA = A;
            tempB = B;
            tempC = C;
            tempD = D;
            strlength = 0;
            vector<unsigned int> rec = padding(src);
            for (unsigned int i = 0; i < strlength / 16; i++)
            {
                unsigned int num[16];
                for (int j = 0; j < 16; j++)
                {
                    num[j] = rec[i * 16 + j];
                }
                iterateFunc(num, 16);
            }
            return format(tempA) + format(tempB) + format(tempC) + format(tempD);
        }
        void iterateFunc(unsigned int *X, int size = 16)
        {
            unsigned int a = tempA,
            b = tempB,
            c = tempC,
            d = tempD,
            rec = 0,
            g, k;
            for (int i = 0; i < 64; i++)
            {
                if (i < 16)
                {
                    // F迭代
                    g = F(b, c, d);
                    k = i;
                }
                else if (i < 32)
                {
                    // G迭代
                    g = G(b, c, d);
                    k = (1 + 5 * i) % 16;
                }
                else if (i < 48)
                {
                    // H迭代
                    g = H(b, c, d);
                    k = (5 + 3 * i) % 16;
                }
                else
                {
                    // I迭代
                    g = I(b, c, d);
                    k = (7 * i) % 16;
                }
                rec = d;
                d = c;
                c = b;
                b = b + shift(a + g + X[k] + T[i], s[i]);
                a = rec;
            }
            tempA += a;
            tempB += b;
            tempC += c;
            tempD += d;
        }
        // 填充字符串
        vector<unsigned int> padding(string src)
        {
            // 以512位,64个字节为一组
            unsigned int num = ((src.length() + 8) / 64) + 1;
            vector<unsigned int> rec(num * 16);
            strlength = num * 16;
            for (unsigned int i = 0; i < src.length(); i++)
            {
                // 一个unsigned int对应4个字节,保存4个字符信息
                rec[i >> 2] |= (int)(src[i]) << ((i % 4) * 8);
            }
            // 补充1000...000
            rec[src.length() >> 2] |= (0x80 << ((src.length() % 4) * 8));
            // 填充原文长度
            rec[rec.size() - 2] = (src.length() << 3);
            return rec;
        }
        // 整理输出
        string format(unsigned int num)
        {
            string res = "";
            unsigned int base = 1 << 8;
            for (int i = 0; i < 4; i++)
            {
                string tmp = "";
                unsigned int b = (num >> (i * 8)) % base & 0xff;
                for (int j = 0; j < 2; j++)
                {
                    tmp = str16[b % 16] + tmp;
                    b /= 16;
                }
                res += tmp;
            }
            return res;
        }
    };
}

那么,开始吧。

int TotalID() // 获取总数
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("id2user", "total");
}
string GetNameByID(int ID)
{
    if(ID < 1 || ID > TotalID()) return "";
    INI ini;
    ini.open(CONFIG);
    return ini.Read("id2user", to_string(ID));
}
int GetIDByName(string name)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("user2id", name);
}
string ChangeUsername(int ID, string newname)
{
    if(ID < 1 || ID > TotalID()) return "Invalid ID";
    if(GetIDByName(newname) != 0) return "Used Username"; // 为避免出现 ID 成为辨识用户的唯一方式
    if(newname == "") return "Invalid Username";
    INI ini;
    ini.open(CONFIG);
    ini.Write("user2id", GetNameByID(ID), "0"); // 置 0,方便以后用户注册
    ini.Write("id2user", to_string(ID), newname);
    ini.Write("user2id", newname, to_string(ID)); // 注册
    return "Command executes successfully.";
}
string GetEncryptedPassword(int ID)
{
    if(ID < 1 || ID > TotalID()) return "";
    INI ini;
    ini.open(CONFIG);
    return ini.Read("password", to_string(ID));
}
string ChangePassword(int ID, string EncryptedPassword)
{
    if(ID < 1 || ID > TotalID()) return "Invalid ID";
    INI ini;
    ini.open(CONFIG);
    ini.Write("password", to_string(ID), EncryptedPassword);
    return "Command executes successfully.";
}
int GetIDByIP(string IP)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("ip2id", IP);
}
string Login(int ID, string EncryptedPassword, string IP)
{
    if(ID < 1 || ID > TotalID()) return "Invalid ID";
    INI ini;
    ini.open(CONFIG);
    if(EncryptedPassword != GetEncryptedPassword(ID)) return "Incorrect password";
    // 密码对了
    ini.Write("lastactive", to_string(ID), to_string(time(NULL)));
    ini.Write("lastip", to_string(ID), IP);
    ini.Write("ip2id", IP, to_string(ID));
    return "Command executes successfully.";
}
string Logout(string IP)
{
    INI ini;
    ini.open(CONFIG);
    int ID = ini.ReadInt("ip2id", IP);
    ini.Write("ip2id", IP, "0");
    ini.Write("lastip", to_string(ID), "");
    return "Command executes successfully.";
}
string Register(string Username, string EncryptedPassword)
{
    int ID = TotalID() + 1;
    INI ini;
    ini.open(CONFIG);
    if(GetIDByName(Username) != 0) return "Used Username"; // 为避免出现 ID 成为辨识用户的唯一方式
    if(Username == "") return "Invalid Username";
    ini.Write("ip2user", "total", to_string(ID)); // 到现在我才发现我忽略了我的 WriteInt
    ini.Write("user2id", Username, to_string(ID));
    ini.Write("ip2user", to_string(ID), Username);
    ini.Write("password", to_string(ID), EncryptedPassword);
    // 这边 lastactive,lastip,ip2id 就不用处理了
    return "Command executes successfilly.";
}
void FlushAll(time_t second = 60 * 60 * 24 * 10)
{
    int Tot = TotalID();
    INI ini;
    ini.open(CONFIG);
    for(int i = 1; i <= Tot; i++)
    {
        if(time(NULL) - ini.ReadInt("lastactive", to_string(i)) > second)
        {
            Logout(ini.Read("lastip", to_string(i)));
        }
    }
}
string GetLoginPage()
{
    INI ini;
    ini.open(CONFIG);
    return ini.Read("page", "login");
}
void SetLoginPage(string page_no_root)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("page", "login", ROOT + page_no_root);
}
string GetRegPage()
{
    INI ini;
    ini.open(CONFIG);
    return ini.Read("page", "register");
}
void SetRegPage(string page_no_root)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("page", "register", ROOT + page_no_root);
}

这里的 INI 使用系统提供的函数,在读取以及写入时都会自动加锁,故暂时无需考虑冲突等情况。

添加指令

我开了一个指令 user,里面几个分指令,具体定义如下:

user register [name] [pass]
user get name [ID]
user get ID --name [name]
user get pass [ID]
user get ID --IP [IP]
user change name [ID] [newname]
user change pass [ID] [newpass]
user login [IP] [ID] [pass]
user logout IP [IP]
user logout ID [ID]

很好,又有活干了。

    else if(type == "user")
    {
        if(argc < 4) return "user needs 3+ arguments.\nUsage:\nuser register [name] [pass]\nuser get name [ID]\nuser get ID --name [name]\nuser get pass [ID]\nuser get ID --IP [IP]\nuser change name [ID] [newname]\nuser change pass [ID] [newpass]\nuser login [IP] [ID] [pass]\nuser logout IP [IP]\nuser logout ID [ID]";
        if(argv[1] == "register")
        {
            MD5::MD5 md5;
            system("cls");
            return Register(argv[2], md5.encode(argv[3]));
        }
        else if(argv[1] == "get")
        {
            if(argv[2] == "name")
            {
                int id;
                try
                {
                    id = stoi(argv[3]);
                }
                catch(...)
                {
                    return "invalid id " + argv[3];
                }
                if(id < 1 || id > TotalID())
                {
                    return "invalid id " + argv[3];
                }
                return GetNameByID(id);
            }
            else if(argv[2] == "id")
            {
                if(argc < 5) return "user get id needs 2 arguments.";
                if(argv[3] == "--name")
                {
                    return to_string(GetIDByName(argv[4]));
                }
                else if(argv[3] == "--ip")
                {
                    return to_string(GetIDByIP(argv[4]));
                }
                else
                {
                    return "user get ID --name [name]\nuser get ID --IP [IP]";
                }
            }
            else if(argv[2] == "pass")
            {
                int id;
                try
                {
                    id = stoi(argv[3]);
                }
                catch(...)
                {
                    return "invalid id " + argv[3];
                }
                if(id < 1 || id > TotalID())
                {
                    return "invalid id " + argv[3];
                }
                return "Encrypted: " + GetEncryptedPassword(id);
            }
            else return "user get name [ID]\nuser get ID --name [name]\nuser get pass [ID]\nuser get ID --IP [IP]";
        }
        else if(argv[1] == "change")
        {
            if(argc < 5) return "user change needs 3 arguments.";
            if(argv[2] == "name")
            {
                int id;
                try
                {
                    id = stoi(argv[3]);
                }
                catch(...)
                {
                    return "invalid id " + argv[3];
                }
                if(id < 1 || id > TotalID())
                {
                    return "invalid id " + argv[3];
                }
                return ChangeUsername(id, argv[4]);
            }
            else if(argv[2] == "pass")
            {
                int id;
                try
                {
                    id = stoi(argv[3]);
                }
                catch(...)
                {
                    return "invalid id " + argv[3];
                }
                if(id < 1 || id > TotalID())
                {
                    return "invalid id " + argv[3];
                }
                system("cls");
                MD5::MD5 md5;
                return ChangePassword(id, md5.encode(argv[4]));
            }
            else
            {
                return "user change name [ID] [newname]\nuser change pass [ID] [newpass]";
            }
        }
        else if(argv[1] == "login")
        {
            if(argc < 5) return "user login needs 3 arguments.";
            int id;
            try
            {
                id = stoi(argv[3]);
            }
            catch(...)
            {
                return "invalid id " + argv[3];
            }
            if(id < 1 || id > TotalID())
            {
                return "invalid id " + argv[3];
            }
            system("cls");
            MD5::MD5 md5;
            return Login(id, md5.encode(argv[4]), argv[2]);
        }
        else if(argv[1] == "logout")
        {
            if(argv[2] == "ip")
            {
                return Logout(argv[3]);
            }
            else if(argv[2] == "id")
            {
                int id;
                try
                {
                    id = stoi(argv[3]);
                }
                catch(...)
                {
                    return "invalid id " + argv[3];
                }
                if(id < 1 || id > TotalID())
                {
                    return "invalid id " + argv[3];
                }
                INI ini;
                ini.open(CONFIG);
                return Logout(ini.Read("lastip", to_string(id)));
            }
        }
        else
        {
            return "user register [name] [pass]\nuser get name [ID]\nuser get ID --name [name]\nuser get pass [ID]\nuser get ID --IP [IP]\nuser change name [ID] [newname]\nuser change pass [ID] [newpass]\nuser login [IP] [ID] [pass]\nuser logout IP [IP]\nuser logout ID [ID]";
        }
    }

还没完!还要加配置登录页、注册页的指令。这个不是指 html 的登录页、注册页,而是 POST 表单要提交处理的地方。

    else if(type == "page")
    {
        if(argc < 2) return "page needs 1 or 2 arguments.";
        if(argv[1] == "login")
        {
            if(argc == 2) return GetLoginPage();
            SetLoginPage(argv[2]);
        }
        else if(argv[1] == "register")
        {
            if(argc == 2) return GetRegPage();
            SetRegPage(argv[2]);
        }
        else
        {
            return "invalid arg " + argv[1];
        }
    }

HTML

好了,我们实现一下登录和注册的 HTML

我们规定:

登录页:

<!doctype html>
<html lang="zh-cn">
    <head>
        <title>Login</title>
    </head>
    <body>
        <h1>
            欢迎!请登录。
        </h1>
        <br>
        <form action="/login" method="POST" enctype="application/x-www-form-urlencoded">
            <fieldset>
                <legend>用户信息</legend>
                用户名:<input type="text" name="username">
                密码:<input type="password" name="password">
                <input type="submit" value="登录">
            </fieldset>
        </form>
        <a href="/register.html">没有账号?注册一个!</a>
    </body>
</html>

注册页:

<!doctype html>
<html lang="zh-cn">
    <head>
        <title>Register</title>
    </head>
    <body>
        <h1>
            注册页
        </h1>
        <br>
        <form action="/register" method="POST" enctype="application/x-www-form-urlencoded" oninput="x.value=a.value==b.value?'':'两次密码不一样';y.type=a.value==b.value?'submit':'hidden'">
            <fieldset>
                <legend>用户信息</legend>
                用户名:<input type="text" name="username"><br>
                密码:<input type="password" name="password" id="a"><br>
                重复密码:<input type="password" id="b"><output style="color:red" id="x" for="a b"></output><br>
                <input type="submit" id="y" value="注册">
            </fieldset>
        </form>
    </body>
</html>

登录、注册等 POST 请求的处理

在这里,我们要先判断是哪个页,然后再处理用户。同时,这里还封装了新的函数,修了一些 bug。

#include <winsock2.h>
#include <Windows.h>
#pragma comment(lib, "-lws2_32")
#include <bits/stdc++.h>
#define PORT 8080 // 绑定端口
#define BUFLEN 1048576
#define ROOT "./html"
#define VERSION "HTTP/1.1"
#define CONFIG "./config.ini"
using namespace std;

namespace MD5
{
#define A 0x67452301
#define B 0xefcdab89
#define C 0x98badcfe
#define D 0x10325476
    const char str16[] = "0123456789abcdef";
    const unsigned int T[] =
    {
        0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
        0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
        0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
        0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
        0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
        0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
        0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
        0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
        0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
        0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
        0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
        0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
        0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
        0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
        0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
        0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
    };
    const unsigned int s[] =
    {
        7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
        5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
        4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
        6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
    };
    class MD5
    {
        private:
            unsigned int tempA, tempB, tempC, tempD, strlength;
        public:
            unsigned int F(unsigned int b, unsigned int c, unsigned int d)
            {
                return (b & c) | ((~b) & d);
            }
            unsigned int G(unsigned int b, unsigned int c, unsigned int d)
            {
                return (b & d) | (c & (~d));
            }
            unsigned int H(unsigned int b, unsigned int c, unsigned int d)
            {
                return b ^ c ^ d;
            }
            unsigned int I(unsigned int b, unsigned int c, unsigned int d)
            {
                return c ^ (b | (~d));
            }
            unsigned int shift(unsigned int a, unsigned int n)
            {
                return (a << n) | (a >> (32 - n));
            }
            string encode(string src)
            {
                tempA = A;
                tempB = B;
                tempC = C;
                tempD = D;
                strlength = 0;
                vector<unsigned int> rec = padding(src);
                for (unsigned int i = 0; i < strlength / 16; i++)
                {
                    unsigned int num[16];
                    for (int j = 0; j < 16; j++)
                    {
                        num[j] = rec[i * 16 + j];
                    }
                    iterateFunc(num, 16);
                }
                return format(tempA) + format(tempB) + format(tempC) + format(tempD);
            }
            void iterateFunc(unsigned int *X, int size = 16)
            {
                unsigned int a = tempA,
                             b = tempB,
                             c = tempC,
                             d = tempD,
                             rec = 0,
                             g, k;
                for (int i = 0; i < 64; i++)
                {
                    if (i < 16)
                    {
                        // F迭代
                        g = F(b, c, d);
                        k = i;
                    }
                    else if (i < 32)
                    {
                        // G迭代
                        g = G(b, c, d);
                        k = (1 + 5 * i) % 16;
                    }
                    else if (i < 48)
                    {
                        // H迭代
                        g = H(b, c, d);
                        k = (5 + 3 * i) % 16;
                    }
                    else
                    {
                        // I迭代
                        g = I(b, c, d);
                        k = (7 * i) % 16;
                    }
                    rec = d;
                    d = c;
                    c = b;
                    b = b + shift(a + g + X[k] + T[i], s[i]);
                    a = rec;
                }
                tempA += a;
                tempB += b;
                tempC += c;
                tempD += d;
            }
            // 填充字符串
            vector<unsigned int> padding(string src)
            {
                // 以512位,64个字节为一组
                unsigned int num = ((src.length() + 8) / 64) + 1;
                vector<unsigned int> rec(num * 16);
                strlength = num * 16;
                for (unsigned int i = 0; i < src.length(); i++)
                {
                    // 一个unsigned int对应4个字节,保存4个字符信息
                    rec[i >> 2] |= (int)(src[i]) << ((i % 4) * 8);
                }
                // 补充1000...000
                rec[src.length() >> 2] |= (0x80 << ((src.length() % 4) * 8));
                // 填充原文长度
                rec[rec.size() - 2] = (src.length() << 3);
                return rec;
            }
            // 整理输出
            string format(unsigned int num)
            {
                string res = "";
                unsigned int base = 1 << 8;
                for (int i = 0; i < 4; i++)
                {
                    string tmp = "";
                    unsigned int b = (num >> (i * 8)) % base & 0xff;
                    for (int j = 0; j < 2; j++)
                    {
                        tmp = str16[b % 16] + tmp;
                        b /= 16;
                    }
                    res += tmp;
                }
                return res;
            }
    };
}
// 调试
queue<pair<int, int>> writq;

int ipcolor[] =
{
    0x04, 0x06, 0x02, 0x03, 0x01, 0x05,
    0x40, 0x46, 0x42, 0x43, 0x41, 0x45,
    0x64, 0x60, 0x62, 0x63, 0x61, 0x65,
    0x24, 0x26, 0x20, 0x23, 0x21, 0x25,
    0x34, 0x36, 0x32, 0x30, 0x31, 0x35,
    0x14, 0x16, 0x12, 0x13, 0x10, 0x15,
    0x54, 0x56, 0x52, 0x53, 0x51, 0x50
};

void col(int n = -1)
{
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
                            n == -1 ? 0x07 : ipcolor[n]);
}

string GetFormatTime()
{
    SYSTEMTIME st;
    GetSystemTime(&st);
    string ret = "";
    ret += to_string(st.wYear) + "/" + to_string(st.wMonth) + "/" + to_string(
               st.wDay) + " ";
    ret += to_string(st.wHour) + ":" + to_string(st.wMinute) + ":" + to_string(
               st.wSecond) + "." + to_string(st.wMilliseconds);
    return ret;
}

void cprintf(int major, int minor, const char *format, ...)
{
    writq.push(make_pair(major, minor));
    va_list va;
    char *buf = new char[BUFLEN];
    va_start(va, format);
    vsprintf(buf, format, va);
    va_end(va);
    string tmp = buf;
    delete []buf;
    tmp = to_string(major) + "." + to_string(minor) + "@" + GetFormatTime() +
          " [LOG] " + tmp;
    while (writq.front().first != major || writq.front().second != minor) ;
    col(major);
    cerr << tmp << flush;
    col();
    writq.pop();
}

// 文件操作
bool IsFileExist(string path)
{
    FILE *f = fopen(path.data(), "r");
    bool ret = f == NULL;
    if (!ret)
    {
        fclose(f);
    }
    return !ret;
}

string GetFile(string path)
{
    if (!IsFileExist(path))
    {
        cprintf(33, 0, "request file %s not exist\n", path.data());
        return "";
    }
    cprintf(33, 0, "request file %s succ\n", path.data());
    ifstream ifs(path, ios::in | ios::binary);
    // 获取文件所有内容
    stringstream ss;
    ss << ifs.rdbuf();
    ifs.close();
    return ss.str();
}

// 配置文件读写有关函数
class INI
{
    protected:
        char *file;
    public:
        INI()
        {
            file = new char[BUFLEN];
        }
        ~INI()
        {
            delete []file;
        }
        void open(const char *File)
        {
            strcpy(file, File);
        }
        string Read(string Sec, string Key)
        {
            string ret = "";
            char *buf = new char[BUFLEN];
            GetPrivateProfileStringA(Sec.data(), Key.data(), "", buf, BUFLEN, file);
            ret = buf;
            delete[]buf;
            return ret;
        }
        int ReadInt(string Sec, string Key)
        {
            return GetPrivateProfileIntA(Sec.data(), Key.data(), 0, file);
        }
        void Write(string Sec, string Key, string Value)
        {
            WritePrivateProfileStringA(Sec.data(), Key.data(), Value.data(), file);
        }
        void WriteInt(string Sec, string Key, int Value)
        {
            WritePrivateProfileStringA(Sec.data(), Key.data(), to_string(Value).data(), file);
        }
};

string GetErrorPage(int ErrorCode)
{
    INI ini;
    ini.open(CONFIG);
    string ret = ini.Read("errc", to_string(ErrorCode));
    return ret == "" ? "/err/" + to_string(ErrorCode) + ".htm" :
           ret; // 默认为 /err/*.htm
}
void SetErrorPage(int ErrCode, string path)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("errc", to_string(ErrCode), path);
}
string GetRedirect(string file)
{
    INI ini;
    ini.open(CONFIG);
    string ret = ini.Read("redir", file);
    return ret;
}
void SetRedirect(string file, string redir)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("redir", file, redir);
}
bool IsForbidden(string file)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("attr", file);
}
void SetFileAttrib(string file, bool forbidden = true)
{
    INI ini;
    ini.open(CONFIG);
    ini.WriteInt("attr", file, forbidden);
}
bool IsConfused(string IP)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("blacklist", IP);
}
void Confuse(string IP, bool confuse = true)
{
    INI ini;
    ini.open(CONFIG);
    ini.WriteInt("blacklist", IP, confuse);
}
int TotalID()   // 获取总数
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("id2user", "total");
}
string GetNameByID(int ID)
{
    if (ID < 1 || ID > TotalID())
    {
        return "";
    }
    INI ini;
    ini.open(CONFIG);
    return ini.Read("id2user", to_string(ID));
}
int GetIDByName(string name)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("user2id", name);
}
string ChangeUsername(int ID, string newname)
{
    if (ID < 1 || ID > TotalID())
    {
        return "Invalid ID";
    }
    if (GetIDByName(newname) != 0)
    {
        return "Used Username";    // 为避免出现 ID 成为辨识用户的唯一方式
    }
    if (newname == "")
    {
        return "Invalid Username";
    }
    INI ini;
    ini.open(CONFIG);
    ini.Write("user2id", GetNameByID(ID), "0"); // 置 0,方便以后用户注册
    ini.Write("id2user", to_string(ID), newname);
    ini.Write("user2id", newname, to_string(ID)); // 注册
    return "Command executes successfully.";
}
string GetEncryptedPassword(int ID)
{
    if (ID < 1 || ID > TotalID())
    {
        return "";
    }
    INI ini;
    ini.open(CONFIG);
    return ini.Read("password", to_string(ID));
}
string ChangePassword(int ID, string EncryptedPassword)
{
    if (ID < 1 || ID > TotalID())
    {
        return "Invalid ID";
    }
    INI ini;
    ini.open(CONFIG);
    ini.Write("password", to_string(ID), EncryptedPassword);
    return "Command executes successfully.";
}
int GetIDByIP(string IP)
{
    INI ini;
    ini.open(CONFIG);
    return ini.ReadInt("ip2id", IP);
}
string Logout(string IP)
{
    INI ini;
    ini.open(CONFIG);
    int ID = ini.ReadInt("ip2id", IP);
    ini.Write("ip2id", IP, "0");
    ini.Write("lastip", to_string(ID), "");
    return "Command executes successfully.";
}
string Login(int ID, string EncryptedPassword, string IP)
{
    if (ID < 1 || ID > TotalID())
    {
        return "Invalid ID";
    }
    INI ini;
    ini.open(CONFIG);
    if (EncryptedPassword != GetEncryptedPassword(ID))
    {
        return "Incorrect password";
    }
    // 密码对了
    Logout(ini.Read("lastip", to_string(ID))); // 把登录着的挤出去
    ini.Write("lastactive", to_string(ID), to_string(time(NULL)));
    ini.Write("lastip", to_string(ID), IP);
    ini.Write("ip2id", IP, to_string(ID));
    return "Command executes successfully.";
}
string Register(string Username, string EncryptedPassword)
{
    int ID = TotalID() + 1;
    INI ini;
    ini.open(CONFIG);
    if (GetIDByName(Username) != 0)
    {
        return "Used Username";    // 为避免出现 ID 成为辨识用户的唯一方式
    }
    if (Username == "")
    {
        return "Invalid Username";
    }
    ini.Write("id2user", "total",
              to_string(ID)); // 到现在我才发现我忽略了我的 WriteInt
    ini.Write("user2id", Username, to_string(ID));
    ini.Write("id2user", to_string(ID), Username);
    ini.Write("password", to_string(ID), EncryptedPassword);
    // 这边 lastactive,lastip,ip2id 就不用处理了
    return "Command executes successfilly.";
}
void FlushAll(time_t second = 60 * 60 * 24 * 10)
{
    int Tot = TotalID();
    INI ini;
    ini.open(CONFIG);
    for (int i = 1; i <= Tot; i++)
    {
        if (time(NULL) - ini.ReadInt("lastactive", to_string(i)) > second)
        {
            Logout(ini.Read("lastip", to_string(i)));
        }
    }
}
string GetLoginPage()
{
    INI ini;
    ini.open(CONFIG);
    return ini.Read("page", "login");
}
string SetLoginPage(string page_no_root)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("page", "login", ROOT + page_no_root);
    return "Command executes successfully.";
}
string GetRegPage()
{
    INI ini;
    ini.open(CONFIG);
    return ini.Read("page", "register");
}
string SetRegPage(string page_no_root)
{
    INI ini;
    ini.open(CONFIG);
    ini.Write("page", "register", ROOT + page_no_root);
    return "Command executes successfully.";
}
// http 标头处理
typedef vector<pair<string, string>> Header;
typedef map<string, string> Headerlist;
class HTTP   // 这个类到了 multipart 就会很方便
{
    public:
        Header header;
        Headerlist header_list;
        string request_type;
        string request_path;
        string http_version;
        string content;
        string Utf8ToGbk(const char *src_str) const
        {
            int len = MultiByteToWideChar(CP_UTF8, 0, src_str, -1, NULL, 0);
            wchar_t* wszGBK = new wchar_t[len + 1];
            memset(wszGBK, 0, len * 2 + 2);
            MultiByteToWideChar(CP_UTF8, 0, src_str, -1, wszGBK, len);
            len = WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, NULL, 0, NULL, NULL);
            char* szGBK = new char[len + 1];
            memset(szGBK, 0, len + 1);
            WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, szGBK, len, NULL, NULL);
            string strTemp(szGBK);
            if (wszGBK) delete[] wszGBK;
            if (szGBK) delete[] szGBK;
            return strTemp;
        }
        string DecodeURL(string encoded) const
        {
            string ret;
            ret.clear();
            for (int i = 0; i < encoded.size(); i++)
            {
                if (encoded[i] != '%' || i == encoded.size() - 1 || !isxdigit(encoded[i + 1]))
                {
                    ret.push_back(encoded[i]);
                }
                else   // %20 等特殊字符处理
                {
                    auto h2d = [&](char c) -> int { return isupper(c) ? c - 'A' + 10 : islower(c) ? c - 'a' + 10 : c - '0'; };
                    char c = (h2d(encoded[++i]) << 4) | h2d(encoded[++i]);
                    ret.push_back(c);
                }
            }
            return Utf8ToGbk(ret.data()); // 网页访问用的 UTF-8,我们要转 GBK(按需要,你本身 UTF-8 就注释掉)
        }
        void ParseHttp(string http)
        {
            try
            {
                cprintf(35, 0, "Parsing http\n");
                header_list.clear();
                header.clear();
                stringstream ss;
                ss << http;
                string fl;
                getline(ss, fl);
                stringstream ss2; // 不知道为啥不这样会崩
                ss2 << fl;
                string rp;
                request_path.clear();
                ss2 >> request_type >> rp >> http_version;
                request_path = DecodeURL(rp);
                cprintf(35, 0, "\nrequest type = %s\nrequest path = %s\n\tprocessed = %s\nversion = %s\n", request_type.data(),
                        rp.data(),
                        request_path.data(),
                        http_version.data());
                int num = 0;
                while (getline(ss, fl) && fl != "" && !iscntrl(fl[0]) && !isspace(fl[0]))
                {
                    int pos = fl.find_first_of(':');
                    string lhs = fl.substr(0, pos), rhs = fl.substr(pos + 1);
                    if (rhs[0] == ' ')
                    {
                        rhs = rhs.substr(1);
                    }
                    for (auto &u : lhs)
                    {
                        u = isupper(u) ? tolower(u) : u;
                    }
                    cprintf(35, 0, "header #%d, lhs = %s, rhs = %s\n", ++num, lhs.data(), rhs.data());
                    header.push_back(make_pair(lhs, rhs));
                    header_list[lhs] = rhs;
                }
                content = http.substr(http.find("\r\n\r\n") + 4);
                cprintf(35, 0, "content:\n%s\n------------------------------------\n", content.data());
            }
            catch (...)
            {
                return;
            }
        }
        void operator()(string http)
        {
            ParseHttp(http);
        }
};
// 线程
DWORD WINAPI tTalk(LPVOID lpParam)
{
    pair<SOCKET, pair<int, int>>
                              psii; // 前一个是客户端 socket,后面是编号
    psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
    // 获取 socket 与主次编号
    SOCKET clientfd = psii.first;
    int major = psii.second.first;
    int minor = psii.second.second;
    char *buf = new char[BUFLEN];
    int len = recv(clientfd, buf, BUFLEN, 0);
    if(len < 0) return EXIT_SUCCESS;
    string message(buf, len);
    delete []buf;
    cprintf(major, minor, "msg:\n%s\n", message.data());
    // 处理 http 请求
    //  stringstream ss(message); // 字符串流
    HTTP http;
    http.ParseHttp(message);
    string reqtype = http.request_type;
    //  ss >> reqtype;
    if (reqtype == "GET")  // 确保是 GET 方法
    {
        string path = http.request_path;
        //      ss >> path;
        path = ROOT + path;
        string version = http.http_version;
        //      ss >> version;
        message = VERSION" ";
        if (GetRedirect(path) != "")  // 重定向优先,不然文件一大堆
        {
            message += "302 Found\nLocation: " + GetRedirect(path).substr(strlen(
                           ROOT)) + "\n";
        }
        else if (IsForbidden(path))  // 403
        {
            message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(403));
        }
        else if (IsFileExist(path))
        {
            message += "200 OK\n\n" + GetFile(path);
        }
        else   // 404
        {
            cprintf(major, minor, "%s doen't exist\n", path.data());
            message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(404));
        }
    }
    else if (reqtype == "POST")
    {
        cprintf(major, minor, "content-type: %s\n",
                http.header_list["content-type"].data());
        if (http.header_list["content-type"].find("application/x-www-form-urlencoded") != string::npos)
        {
            string path = ROOT + http.request_path;
            map<string, string> key_value;
            string s;
            stringstream ss(http.content);
            int nummm = 0;
            while (getline(ss, s, '&'))
            {
                int pos = s.find('=');
                string lhs = s.substr(0, pos), rhs = s.substr(pos + 1);
//              lhs = http.DecodeURL(lhs);
//              rhs = http.DecodeURL(rhs);
                key_value[lhs] = rhs; // 这里由于是用户名密码,我们还是采用 URL 编码,不然 ini 会记乱掉
                cprintf(major, minor, "parse x-www, #%d, s = %s, lhs = %s, rhs = %s\n", ++nummm, s.data(), lhs.data(), rhs.data());
            }
            if (path == GetLoginPage())
            {
                MD5::MD5 md5;
                sockaddr_in addr;
                int len = sizeof(sockaddr);
                getpeername(clientfd, (sockaddr *)&addr, &len);
                message = VERSION" 200 OK\n\n" + Login(GetIDByName(key_value["username"]),
                                                       md5.encode(key_value["password"]), inet_ntoa(addr.sin_addr));
            }
            else if (path == GetRegPage())
            {
                MD5::MD5 md5;
                message = VERSION" 200 OK\n\n" + Register(key_value["username"],
                          md5.encode(key_value["password"]));
            }
        }
        else if (http.header_list["content-type"].find("multipart/form-data") != string::npos)
        {
            string boundary = http.header_list["content-type"].substr(
                                  http.header_list["content-type"].find("; ") + 2
                              );
            // multipart 处理语句
        }
    }
    cprintf(major, minor, "Send msg to peer:\n%s\n", message.data());
    send(clientfd, message.data(), message.size(), 0); // http Server
    // 处理完后关闭
    shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
    closesocket(clientfd);
    return EXIT_SUCCESS;
}
DWORD WINAPI tIPServer(LPVOID lpParam)
{
    // lpParam 处是一个 string.
    string ip = *(string *)lpParam;
    int major = stoi(ip.substr(ip.find('@') + 1)); // 在 @ 后面寻找主版本号
    ip = ip.substr(0, ip.find('@'));
    // 创建 TCP 套接字(http 是基于 TCP 的)
    SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    // 写好绑定地址
    sockaddr_in listenaddr;
    ZeroMemory(&listenaddr, sizeof(listenaddr)); // 置零
    listenaddr.sin_addr.s_addr = inet_addr(ip.data()); // 绑定 IP
    listenaddr.sin_family = AF_INET;
    listenaddr.sin_port = htons(
                              PORT); // 注意,这个 htons 是必需的,把主机地址转化为网络地址
    cprintf(major, 0, "Start, IP = %s\n", ip.data());
    // 绑定
    bind(listenfd, (sockaddr *)&listenaddr, sizeof(sockaddr));
    cprintf(major, 0, "bind OK\n");
    // 监听
    listen(listenfd, 20);
    cprintf(major, 0, "listen OK\n");
    // 循环接受连接请求
    SOCKET clientfd;
    sockaddr_in clientaddr;
    int tmplen = sizeof(sockaddr);
    int counter = 0;
    while ((clientfd = accept(listenfd, (sockaddr *)&clientaddr,
                              &tmplen)) != INVALID_SOCKET)
    {
        cprintf(major, counter + 1, "收到来自 %s:%d 的连接请求\n",
                inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        if (IsConfused(inet_ntoa(clientaddr.sin_addr)))
        {
            closesocket(clientfd);
            continue;
        }
        pair<SOCKET, pair<int, int>> psii = make_pair(clientfd, make_pair(major,
                                            (++counter))); // 把东西放进去
        CreateThread(NULL, 0, tTalk, (LPVOID)&psii, 0,
                     NULL); // 取指针,传给 tTalk
    }
}
// 指令处理函数
LPSTR *CommandLineToArgvA(LPCSTR lpCmdLine,
                          INT *pNumArgs)   // https://blog.csdn.net/qq_22000459/article/details/83539644
{
    int retval;
    retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1, NULL,
                                 0);
    if (!SUCCEEDED(retval))
    {
        return NULL;
    }
    LPWSTR lpWideCharStr = (LPWSTR)malloc(retval * sizeof(WCHAR));
    if (lpWideCharStr == NULL)
    {
        return NULL;
    }
    retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1,
                                 lpWideCharStr, retval);
    if (!SUCCEEDED(retval))
    {
        free(lpWideCharStr);
        return NULL;
    }
    int numArgs;
    LPWSTR *args;
    args = CommandLineToArgvW(lpWideCharStr, &numArgs);
    free(lpWideCharStr);
    if (args == NULL)
    {
        return NULL;
    }
    int storage = numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++ i)
    {
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, NULL, 0, NULL,
                                     &lpUsedDefaultChar);
        if (!SUCCEEDED(retval))
        {
            LocalFree(args);
            return NULL;
        }
        storage += retval;
    }
    LPSTR *result = (LPSTR *)LocalAlloc(LMEM_FIXED, storage);
    if (result == NULL)
    {
        LocalFree(args);
        return NULL;
    }
    int bufLen = storage - numArgs * sizeof(LPSTR);
    LPSTR buffer = ((LPSTR)result) + numArgs * sizeof(LPSTR);
    for (int i = 0; i < numArgs; ++ i)
    {
        assert(bufLen > 0);
        BOOL lpUsedDefaultChar = FALSE;
        retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, buffer, bufLen, NULL,
                                     &lpUsedDefaultChar);
        if (!SUCCEEDED(retval))
        {
            LocalFree(result);
            LocalFree(args);
            return NULL;
        }
        result[i] = buffer;
        buffer += retval;
        bufLen -= retval;
    }
    LocalFree(args);
    *pNumArgs = numArgs;
    return result;
}
// 真正的处理
string process(string cmd)
{
    for (auto &u : cmd)
    {
        u = isupper(u) ? tolower(u) : u;
    }
    stringstream ss;
    ss << cmd;
    vector<string> argv;
    int argc;
    LPSTR *argv_ = CommandLineToArgvA(cmd.data(), &argc);
    for (int i = 0; i < argc; i++)
    {
        argv.push_back(argv_[i]);
    }
    string type = argv[0];
    if (type == "errorpage")
    {
        if (argc != 2 && argc != 3)
        {
            return "errorpage needs 1 or 2 arguments.";
        }
        int errc;
        try
        {
            errc = stoi(argv[1]);
        }
        catch (...)
        {
            return "Invalid errorcode " + argv[1];
        }
        if (errc < 100 || errc > 999)
        {
            return "Invalid errorcode " + to_string(errc);
        }
        if (argc == 2)
        {
            return "errorcode " + to_string(errc) + ": " + GetErrorPage(errc);
        }
        argv[2] = ROOT + argv[2];
        string file = argv[2];
        if (!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        SetErrorPage(errc, file);
        return "success: Errc " + to_string(errc) + ", File " + file;
    }
    else if (type == "redirect")
    {
        if (argc != 2 && argc != 3)
        {
            return "redirect needs 1 or 2 arguments.";
        }
        argv[1] = ROOT + argv[1];
        if (argc == 2)
        {
            return argv[1] + " -> " + GetRedirect(argv[1]);
        }
        argv[2] = ROOT + argv[2];
        string file(argv[2]);
        if (!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        SetRedirect(argv[1], argv[2]);
        return "success: " + argv[1] + " -> " + argv[2];
    }
    else if (type == "attrib")
    {
        if (argc != 2 && argc != 3)
        {
            return "attrib needs 1 or 2 arguments.";
        }
        argv[1] = ROOT + argv[1];
        string file(argv[1]);
        if (!IsFileExist(file))
        {
            return "File " + file + " doesn't exist.";
        }
        if (argc == 2)
        {
            return file + ": " + (IsForbidden(file) ? "private" : "public");
        }
        bool priv;
        if (argv[2] == "--public")
        {
            priv = 0;
        }
        else if (argv[2] == "--private")
        {
            priv = 1;
        }
        else
        {
            return "Invalid argument " + argv[2];
        }
        SetFileAttrib(file, priv);
        return "success: " + argv[1] + " forbidden: " + to_string(priv);
    }
    else if (type == "blacklist")
    {
        if (argc != 2 && argc != 3)
        {
            return "blacklist needs 1 or 2 arguments.";
        }
        string IP = argv[1];
        if (inet_addr(IP.data()) == INADDR_NONE)
        {
            return "invalid IP address " + IP;
        }
        if (argc == 2)
        {
            return IP + ": " + (IsConfused(IP) ? "confuse" : "accept");
        }
        bool c;
        if (argv[2] == "--accept")
        {
            c = 0;
        }
        else if (argv[2] == "--confuse")
        {
            c = 1;
        }
        else
        {
            return "Invalid argument " + argv[2];
        }
        Confuse(IP, c);
        return "success: " + argv[1] + " confuse: " + to_string(c);
    }
    else if (type == "user")
    {
        if (argc < 4)
        {
            return "user needs 3+ arguments.\nUsage:\nuser register [name] [pass]\nuser get name [ID]\nuser get ID --name [name]\nuser get pass [ID]\nuser get ID --IP [IP]\nuser change name [ID] [newname]\nuser change pass [ID] [newpass]\nuser login [IP] [ID] [pass]\nuser logout IP [IP]\nuser logout ID [ID]";
        }
        if (argv[1] == "register")
        {
            MD5::MD5 md5;
            system("cls");
            return Register(argv[2], md5.encode(argv[3]));
        }
        else if (argv[1] == "get")
        {
            if (argv[2] == "name")
            {
                int id;
                try
                {
                    id = stoi(argv[3]);
                }
                catch (...)
                {
                    return "invalid id " + argv[3];
                }
                if (id < 1 || id > TotalID())
                {
                    return "invalid id " + argv[3];
                }
                return GetNameByID(id);
            }
            else if (argv[2] == "id")
            {
                if (argc < 5)
                {
                    return "user get id needs 2 arguments.";
                }
                if (argv[3] == "--name")
                {
                    return to_string(GetIDByName(argv[4]));
                }
                else if (argv[3] == "--ip")
                {
                    return to_string(GetIDByIP(argv[4]));
                }
                else
                {
                    return "user get ID --name [name]\nuser get ID --IP [IP]";
                }
            }
            else if (argv[2] == "pass")
            {
                int id;
                try
                {
                    id = stoi(argv[3]);
                }
                catch (...)
                {
                    return "invalid id " + argv[3];
                }
                if (id < 1 || id > TotalID())
                {
                    return "invalid id " + argv[3];
                }
                return "Encrypted: " + GetEncryptedPassword(id);
            }
            else
            {
                return "user get name [ID]\nuser get ID --name [name]\nuser get pass [ID]\nuser get ID --IP [IP]";
            }
        }
        else if (argv[1] == "change")
        {
            if (argc < 5)
            {
                return "user change needs 3 arguments.";
            }
            if (argv[2] == "name")
            {
                int id;
                try
                {
                    id = stoi(argv[3]);
                }
                catch (...)
                {
                    return "invalid id " + argv[3];
                }
                if (id < 1 || id > TotalID())
                {
                    return "invalid id " + argv[3];
                }
                return ChangeUsername(id, argv[4]);
            }
            else if (argv[2] == "pass")
            {
                int id;
                try
                {
                    id = stoi(argv[3]);
                }
                catch (...)
                {
                    return "invalid id " + argv[3];
                }
                if (id < 1 || id > TotalID())
                {
                    return "invalid id " + argv[3];
                }
                system("cls");
                MD5::MD5 md5;
                return ChangePassword(id, md5.encode(argv[4]));
            }
            else
            {
                return "user change name [ID] [newname]\nuser change pass [ID] [newpass]";
            }
        }
        else if (argv[1] == "login")
        {
            if (argc < 5)
            {
                return "user login needs 3 arguments.";
            }
            int id;
            try
            {
                id = stoi(argv[3]);
            }
            catch (...)
            {
                return "invalid id " + argv[3];
            }
            if (id < 1 || id > TotalID())
            {
                return "invalid id " + argv[3];
            }
            system("cls");
            MD5::MD5 md5;
            return Login(id, md5.encode(argv[4]), argv[2]);
        }
        else if (argv[1] == "logout")
        {
            if (argv[2] == "ip")
            {
                return Logout(argv[3]);
            }
            else if (argv[2] == "id")
            {
                int id;
                try
                {
                    id = stoi(argv[3]);
                }
                catch (...)
                {
                    return "invalid id " + argv[3];
                }
                if (id < 1 || id > TotalID())
                {
                    return "invalid id " + argv[3];
                }
                INI ini;
                ini.open(CONFIG);
                return Logout(ini.Read("lastip", to_string(id)));
            }
        }
        else
        {
            return "user register [name] [pass]\nuser get name [ID]\nuser get ID --name [name]\nuser get pass [ID]\nuser get ID --IP [IP]\nuser change name [ID] [newname]\nuser change pass [ID] [newpass]\nuser login [IP] [ID] [pass]\nuser logout IP [IP]\nuser logout ID [ID]";
        }
    }
    else if (type == "page")
    {
        if (argc < 2)
        {
            return "page needs 1 or 2 arguments.";
        }
        if (argv[1] == "login")
        {
            if (argc == 2)
            {
                return GetLoginPage();
            }
            return SetLoginPage(argv[2]);
        }
        else if (argv[1] == "register")
        {
            if (argc == 2)
            {
                return GetRegPage();
            }
            return SetRegPage(argv[2]);
        }
        else
        {
            return "invalid arg " + argv[1];
        }
    }
    else   // help
    {
        return "errorpage 错误码 [文件]\nredirect 网址 [重定向目标]\nattrib 文件 [--public | --private]\nblacklist IP [--confuse | --accept]\nuser register [name] [pass]\nuser get name [ID]\nuser get ID --name [name]\nuser get pass [ID]\nuser get ID --IP [IP]\nuser change name [ID] [newname]\nuser change pass [ID] [newpass]\nuser login [IP] [ID] [pass]\nuser logout IP [IP]\nuser logout ID [ID]\npage [login | register] [page]";
    }
}
// 各种主函数
int httpmain()
{
    // 在 Windows 下需要加上这一段。
    WSADATA wsd;
    WSAStartup(MAKEWORD(2, 2), &wsd);
    // 获取所有 IP
    string strIP("127.0.0.1");
    vector<string> ret;
    ret.clear();
    ret.push_back(strIP + "@0");
    cprintf(0, 0, "127.0.0.1\n");
    char *strName = new char[1024];
    gethostname(strName, 1024);
    struct hostent *pHostEnt = gethostbyname(strName);
    delete []strName;
    int n = 0;
    while (pHostEnt->h_addr_list[n] != NULL)
    {
        stringstream ss;
        ss << (pHostEnt->h_addr_list[n][0] & 0x00FF) << "."
           << (pHostEnt->h_addr_list[n][1] & 0x00FF) << "."
           << (pHostEnt->h_addr_list[n][2] & 0x00FF) << "."
           << (pHostEnt->h_addr_list[n][3] & 0x00FF) << flush;
        string strTemp(ss.str());
        cprintf(n + 1, 0, (strTemp + "\n").data());
        if (string::npos == strTemp.find("127.0.0."))
        {
            ret.push_back(strTemp + "@" + to_string(n +
                                                    1)); // 此处的 @ 是用于分隔 IP 与编号
        }
        n++;
    }
    // 对每个 IP 启动线程
    for (int i = 0; i < ret.size(); i++)
    {
        CreateThread(NULL, 0, tIPServer, (LPVOID)&ret[i], 0, NULL);
    }
    // 死循环,不要提前退了
    while (1);
    // 释放 socket 资源
    WSACleanup();
    return 0;
}
void commandmain()
{
    while (true)
    {
        col(3);
        cout << "Input command > " << flush;
        string cmd;
        col(2);
        getline(cin, cmd);
        col(0);
        cout << "command response:" << endl;
        col(5);
        cout << process(cmd) << endl;
        col();
    }
}
int main(int argc, char *argv[])
{
    try
    {
        if (argc == 1)  // 主窗口
        {
            ShellExecuteA(NULL, "open", argv[0], "cmd", NULL, SW_SHOW);
            httpmain();
        }
        else   // 从窗口
        {
            commandmain();
        }
    }
    catch(...)
    {
        ShellExecuteA(NULL, "open", argv[0], NULL, NULL, SW_SHOW);
        return 114514;
    }
    return 0;
}

3. JS 开放 API

我们为图省事,让 javascript 在前端装配,耗前端的 CPU,就给它开放一个 API,把每个函数都做一个接口传上去。我们在 tTalk 函数里修改,在 POST 里加上一个 json。这样,我们虽然限制了用 API 一定要用 POST 方法和 json 传递信息,但是省了不少事。

    else if (reqtype == "POST")
    {
        cprintf(major, minor, "content-type: %s\n",
                http.header_list["content-type"].data());
        if (http.header_list["content-type"].find("application/x-www-form-urlencoded") != string::npos)
        {
            string path = ROOT + http.request_path;
            map<string, string> key_value;
            string s;
            stringstream ss(http.content);
            int nummm = 0;
            while (getline(ss, s, '&'))
            {
                int pos = s.find('=');
                string lhs = s.substr(0, pos), rhs = s.substr(pos + 1);
//              lhs = http.DecodeURL(lhs);
//              rhs = http.DecodeURL(rhs);
                key_value[lhs] = rhs; // 这里由于是用户名密码,我们还是采用 URL 编码,不然 ini 会记乱掉
                cprintf(major, minor, "parse x-www, #%d, s = %s, lhs = %s, rhs = %s\n", ++nummm, s.data(), lhs.data(), rhs.data());
            }
            if (path == GetLoginPage())
            {
                MD5::MD5 md5;
                sockaddr_in addr;
                int len = sizeof(sockaddr);
                getpeername(clientfd, (sockaddr *)&addr, &len);
                message = VERSION" 200 OK\n\n" + Login(GetIDByName(key_value["username"]),
                                                       md5.encode(key_value["password"]), inet_ntoa(addr.sin_addr));
            }
            else if (path == GetRegPage())
            {
                MD5::MD5 md5;
                message = VERSION" 200 OK\n\n" + Register(key_value["username"],
                          md5.encode(key_value["password"]));
            }
        }
        else if (http.header_list["content-type"].find("multipart/form-data") != string::npos)
        {
            string boundary = http.header_list["content-type"].substr(
                                  http.header_list["content-type"].find("; ") + 2
                              );
            // multipart 处理语句
        }
        else if(http.header_list["content-type"].find("application/json") != string::npos)
        {
            if(http.request_path.size() > 5 && http.request_path.substr(0, 5) == "/api/")
            {
                string what = http.request_path.substr(5); // 具体访问什么
                // 在这里加上 API 处理
            }
        }
    }

接下来,我们先把源文件分成几个命名空间:

#include <winsock2.h>
#include <Windows.h>
#pragma comment(lib, "-lws2_32")
#include <bits/stdc++.h>
#define PORT 8080 // 绑定端口
#define BUFLEN 1048576
#define ROOT "./html"
#define VERSION "HTTP/1.1"
#define CONFIG "./config.ini"
using namespace std;

namespace MD5
{
#define A 0x67452301
#define B 0xefcdab89
#define C 0x98badcfe
#define D 0x10325476
    const char str16[] = "0123456789abcdef";
    const unsigned int T[] =
    {
        0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
        0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
        0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
        0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
        0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
        0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
        0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
        0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
        0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
        0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
        0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
        0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
        0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
        0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
        0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
        0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
    };
    const unsigned int s[] =
    {
        7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
        5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
        4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
        6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
    };
    class MD5
    {
        private:
            unsigned int tempA, tempB, tempC, tempD, strlength;
        public:
            unsigned int F(unsigned int b, unsigned int c, unsigned int d)
            {
                return (b & c) | ((~b) & d);
            }
            unsigned int G(unsigned int b, unsigned int c, unsigned int d)
            {
                return (b & d) | (c & (~d));
            }
            unsigned int H(unsigned int b, unsigned int c, unsigned int d)
            {
                return b ^ c ^ d;
            }
            unsigned int I(unsigned int b, unsigned int c, unsigned int d)
            {
                return c ^ (b | (~d));
            }
            unsigned int shift(unsigned int a, unsigned int n)
            {
                return (a << n) | (a >> (32 - n));
            }
            string encode(string src)
            {
                tempA = A;
                tempB = B;
                tempC = C;
                tempD = D;
                strlength = 0;
                vector<unsigned int> rec = padding(src);
                for (unsigned int i = 0; i < strlength / 16; i++)
                {
                    unsigned int num[16];
                    for (int j = 0; j < 16; j++)
                    {
                        num[j] = rec[i * 16 + j];
                    }
                    iterateFunc(num, 16);
                }
                return format(tempA) + format(tempB) + format(tempC) + format(tempD);
            }
            void iterateFunc(unsigned int *X, int size = 16)
            {
                unsigned int a = tempA,
                             b = tempB,
                             c = tempC,
                             d = tempD,
                             rec = 0,
                             g, k;
                for (int i = 0; i < 64; i++)
                {
                    if (i < 16)
                    {
                        // F迭代
                        g = F(b, c, d);
                        k = i;
                    }
                    else if (i < 32)
                    {
                        // G迭代
                        g = G(b, c, d);
                        k = (1 + 5 * i) % 16;
                    }
                    else if (i < 48)
                    {
                        // H迭代
                        g = H(b, c, d);
                        k = (5 + 3 * i) % 16;
                    }
                    else
                    {
                        // I迭代
                        g = I(b, c, d);
                        k = (7 * i) % 16;
                    }
                    rec = d;
                    d = c;
                    c = b;
                    b = b + shift(a + g + X[k] + T[i], s[i]);
                    a = rec;
                }
                tempA += a;
                tempB += b;
                tempC += c;
                tempD += d;
            }
            // 填充字符串
            vector<unsigned int> padding(string src)
            {
                // 以512位,64个字节为一组
                unsigned int num = ((src.length() + 8) / 64) + 1;
                vector<unsigned int> rec(num * 16);
                strlength = num * 16;
                for (unsigned int i = 0; i < src.length(); i++)
                {
                    // 一个unsigned int对应4个字节,保存4个字符信息
                    rec[i >> 2] |= (int)(src[i]) << ((i % 4) * 8);
                }
                // 补充1000...000
                rec[src.length() >> 2] |= (0x80 << ((src.length() % 4) * 8));
                // 填充原文长度
                rec[rec.size() - 2] = (src.length() << 3);
                return rec;
            }
            // 整理输出
            string format(unsigned int num)
            {
                string res = "";
                unsigned int base = 1 << 8;
                for (int i = 0; i < 4; i++)
                {
                    string tmp = "";
                    unsigned int b = (num >> (i * 8)) % base & 0xff;
                    for (int j = 0; j < 2; j++)
                    {
                        tmp = str16[b % 16] + tmp;
                        b /= 16;
                    }
                    res += tmp;
                }
                return res;
            }
    };
}

// 调试
namespace n_debug
{
    queue<pair<int, int>> writq;

    int ipcolor[] =
    {
        0x04, 0x06, 0x02, 0x03, 0x01, 0x05,
        0x40, 0x46, 0x42, 0x43, 0x41, 0x45,
        0x64, 0x60, 0x62, 0x63, 0x61, 0x65,
        0x24, 0x26, 0x20, 0x23, 0x21, 0x25,
        0x34, 0x36, 0x32, 0x30, 0x31, 0x35,
        0x14, 0x16, 0x12, 0x13, 0x10, 0x15,
        0x54, 0x56, 0x52, 0x53, 0x51, 0x50
    };

    void col(int n = -1)
    {
        SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
                                n == -1 ? 0x07 : ipcolor[n]);
    }

    string GetFormatTime()
    {
        SYSTEMTIME st;
        GetSystemTime(&st);
        string ret = "";
        ret += to_string(st.wYear) + "/" + to_string(st.wMonth) + "/" + to_string(
                   st.wDay) + " ";
        ret += to_string(st.wHour) + ":" + to_string(st.wMinute) + ":" + to_string(
                   st.wSecond) + "." + to_string(st.wMilliseconds);
        return ret;
    }

    void cprintf(int major, int minor, const char *format, ...)
    {
        writq.push(make_pair(major, minor));
        va_list va;
        char *buf = new char[BUFLEN];
        va_start(va, format);
        vsprintf(buf, format, va);
        va_end(va);
        string tmp = buf;
        delete []buf;
        tmp = to_string(major) + "." + to_string(minor) + "@" + GetFormatTime() +
              " [LOG] " + tmp;
        while (writq.front().first != major || writq.front().second != minor) ;
        col(major);
        cerr << tmp << flush;
        col();
        writq.pop();
    }
}
using namespace n_debug;

// 文件操作
namespace n_file
{
    bool IsFileExist(string path)
    {
        FILE *f = fopen(path.data(), "r");
        bool ret = f == NULL;
        if (!ret)
        {
            fclose(f);
        }
        return !ret;
    }

    string GetFile(string path)
    {
        if (!IsFileExist(path))
        {
            cprintf(33, 0, "request file %s not exist\n", path.data());
            return "";
        }
        cprintf(33, 0, "request file %s succ\n", path.data());
        ifstream ifs(path, ios::in | ios::binary);
        // 获取文件所有内容
        stringstream ss;
        ss << ifs.rdbuf();
        ifs.close();
        return ss.str();
    }
}
using namespace n_file;

// 配置文件读写有关函数
namespace n_ini
{
    class INI
    {
        protected:
            char *file;
        public:
            INI()
            {
                file = new char[BUFLEN];
            }
            ~INI()
            {
                delete []file;
            }
            void open(const char *File)
            {
                strcpy(file, File);
            }
            string Read(string Sec, string Key)
            {
                string ret = "";
                char *buf = new char[BUFLEN];
                GetPrivateProfileStringA(Sec.data(), Key.data(), "", buf, BUFLEN, file);
                ret = buf;
                delete[]buf;
                return ret;
            }
            int ReadInt(string Sec, string Key)
            {
                return GetPrivateProfileIntA(Sec.data(), Key.data(), 0, file);
            }
            void Write(string Sec, string Key, string Value)
            {
                WritePrivateProfileStringA(Sec.data(), Key.data(), Value.data(), file);
            }
            void WriteInt(string Sec, string Key, int Value)
            {
                WritePrivateProfileStringA(Sec.data(), Key.data(), to_string(Value).data(), file);
            }
    };

    string GetErrorPage(int ErrorCode)
    {
        INI ini;
        ini.open(CONFIG);
        string ret = ini.Read("errc", to_string(ErrorCode));
        return ret == "" ? "/err/" + to_string(ErrorCode) + ".htm" :
               ret; // 默认为 /err/*.htm
    }
    void SetErrorPage(int ErrCode, string path)
    {
        INI ini;
        ini.open(CONFIG);
        ini.Write("errc", to_string(ErrCode), path);
    }
    string GetRedirect(string file)
    {
        INI ini;
        ini.open(CONFIG);
        string ret = ini.Read("redir", file);
        return ret;
    }
    void SetRedirect(string file, string redir)
    {
        INI ini;
        ini.open(CONFIG);
        ini.Write("redir", file, redir);
    }
    bool IsForbidden(string file)
    {
        INI ini;
        ini.open(CONFIG);
        return ini.ReadInt("attr", file);
    }
    void SetFileAttrib(string file, bool forbidden = true)
    {
        INI ini;
        ini.open(CONFIG);
        ini.WriteInt("attr", file, forbidden);
    }
    bool IsConfused(string IP)
    {
        INI ini;
        ini.open(CONFIG);
        return ini.ReadInt("blacklist", IP);
    }
    void Confuse(string IP, bool confuse = true)
    {
        INI ini;
        ini.open(CONFIG);
        ini.WriteInt("blacklist", IP, confuse);
    }
    int TotalID()   // 获取总数
    {
        INI ini;
        ini.open(CONFIG);
        return ini.ReadInt("id2user", "total");
    }
    string GetNameByID(int ID)
    {
        if (ID < 1 || ID > TotalID())
        {
            return "";
        }
        INI ini;
        ini.open(CONFIG);
        return ini.Read("id2user", to_string(ID));
    }
    int GetIDByName(string name)
    {
        INI ini;
        ini.open(CONFIG);
        return ini.ReadInt("user2id", name);
    }
    string ChangeUsername(int ID, string newname)
    {
        if (ID < 1 || ID > TotalID())
        {
            return "Invalid ID";
        }
        if (GetIDByName(newname) != 0)
        {
            return "Used Username";    // 为避免出现 ID 成为辨识用户的唯一方式
        }
        if (newname == "")
        {
            return "Invalid Username";
        }
        INI ini;
        ini.open(CONFIG);
        ini.Write("user2id", GetNameByID(ID), "0"); // 置 0,方便以后用户注册
        ini.Write("id2user", to_string(ID), newname);
        ini.Write("user2id", newname, to_string(ID)); // 注册
        return "Command executes successfully.";
    }
    string GetEncryptedPassword(int ID)
    {
        if (ID < 1 || ID > TotalID())
        {
            return "";
        }
        INI ini;
        ini.open(CONFIG);
        return ini.Read("password", to_string(ID));
    }
    string ChangePassword(int ID, string EncryptedPassword)
    {
        if (ID < 1 || ID > TotalID())
        {
            return "Invalid ID";
        }
        INI ini;
        ini.open(CONFIG);
        ini.Write("password", to_string(ID), EncryptedPassword);
        return "Command executes successfully.";
    }
    int GetIDByIP(string IP)
    {
        INI ini;
        ini.open(CONFIG);
        return ini.ReadInt("ip2id", IP);
    }
    string Logout(string IP)
    {
        INI ini;
        ini.open(CONFIG);
        int ID = ini.ReadInt("ip2id", IP);
        ini.Write("ip2id", IP, "0");
        ini.Write("lastip", to_string(ID), "");
        return "Command executes successfully.";
    }
    string Login(int ID, string EncryptedPassword, string IP)
    {
        if (ID < 1 || ID > TotalID())
        {
            return "Invalid ID";
        }
        INI ini;
        ini.open(CONFIG);
        if (EncryptedPassword != GetEncryptedPassword(ID))
        {
            return "Incorrect password";
        }
        // 密码对了
        Logout(ini.Read("lastip", to_string(ID))); // 把登录着的挤出去
        ini.Write("lastactive", to_string(ID), to_string(time(NULL)));
        ini.Write("lastip", to_string(ID), IP);
        ini.Write("ip2id", IP, to_string(ID));
        return "Command executes successfully.";
    }
    string Register(string Username, string EncryptedPassword)
    {
        int ID = TotalID() + 1;
        INI ini;
        ini.open(CONFIG);
        if (GetIDByName(Username) != 0)
        {
            return "Used Username";    // 为避免出现 ID 成为辨识用户的唯一方式
        }
        if (Username == "")
        {
            return "Invalid Username";
        }
        ini.Write("id2user", "total",
                  to_string(ID)); // 到现在我才发现我忽略了我的 WriteInt
        ini.Write("user2id", Username, to_string(ID));
        ini.Write("id2user", to_string(ID), Username);
        ini.Write("password", to_string(ID), EncryptedPassword);
        // 这边 lastactive,lastip,ip2id 就不用处理了
        return "Command executes successfilly.";
    }
    void FlushAll(time_t second = 60 * 60 * 24 * 10)
    {
        int Tot = TotalID();
        INI ini;
        ini.open(CONFIG);
        for (int i = 1; i <= Tot; i++)
        {
            if (time(NULL) - ini.ReadInt("lastactive", to_string(i)) > second)
            {
                Logout(ini.Read("lastip", to_string(i)));
            }
        }
    }
    string GetLoginPage()
    {
        INI ini;
        ini.open(CONFIG);
        return ini.Read("page", "login");
    }
    string SetLoginPage(string page_no_root)
    {
        INI ini;
        ini.open(CONFIG);
        ini.Write("page", "login", ROOT + page_no_root);
        return "Command executes successfully.";
    }
    string GetRegPage()
    {
        INI ini;
        ini.open(CONFIG);
        return ini.Read("page", "register");
    }
    string SetRegPage(string page_no_root)
    {
        INI ini;
        ini.open(CONFIG);
        ini.Write("page", "register", ROOT + page_no_root);
        return "Command executes successfully.";
    }
}
using namespace n_ini;

// http 处理
namespace n_http
{
    typedef vector<pair<string, string>> Header;
    typedef map<string, string> Headerlist;
    class HTTP   // 这个类到了 multipart 就会很方便
    {
        public:
            Header header;
            Headerlist header_list;
            string request_type;
            string request_path;
            string http_version;
            string content;
            string Utf8ToGbk(const char *src_str) const
            {
                int len = MultiByteToWideChar(CP_UTF8, 0, src_str, -1, NULL, 0);
                wchar_t *wszGBK = new wchar_t[len + 1];
                memset(wszGBK, 0, len * 2 + 2);
                MultiByteToWideChar(CP_UTF8, 0, src_str, -1, wszGBK, len);
                len = WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, NULL, 0, NULL, NULL);
                char *szGBK = new char[len + 1];
                memset(szGBK, 0, len + 1);
                WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, szGBK, len, NULL, NULL);
                string strTemp(szGBK);
                if (wszGBK)
                    delete[] wszGBK;
                if (szGBK)
                    delete[] szGBK;
                return strTemp;
            }
            string DecodeURL(string encoded) const
            {
                string ret;
                ret.clear();
                for (int i = 0; i < encoded.size(); i++)
                {
                    if (encoded[i] != '%' || i == encoded.size() - 1 || !isxdigit(encoded[i + 1]))
                    {
                        ret.push_back(encoded[i]);
                    }
                    else   // %20 等特殊字符处理
                    {
                        auto h2d = [&](char c) -> int { return isupper(c) ? c - 'A' + 10 : islower(c) ? c - 'a' + 10 : c - '0'; };
                        char c = (h2d(encoded[++i]) << 4) | h2d(encoded[++i]);
                        ret.push_back(c);
                    }
                }
                return Utf8ToGbk(ret.data()); // 网页访问用的 UTF-8,我们要转 GBK(按需要,你本身 UTF-8 就注释掉)
            }
            void ParseHttp(string http)
            {
                try
                {
                    cprintf(35, 0, "Parsing http\n");
                    header_list.clear();
                    header.clear();
                    stringstream ss;
                    ss << http;
                    string fl;
                    getline(ss, fl);
                    stringstream ss2; // 不知道为啥不这样会崩
                    ss2 << fl;
                    string rp;
                    request_path.clear();
                    ss2 >> request_type >> rp >> http_version;
                    request_path = DecodeURL(rp);
                    cprintf(35, 0, "\nrequest type = %s\nrequest path = %s\n\tprocessed = %s\nversion = %s\n", request_type.data(),
                            rp.data(),
                            request_path.data(),
                            http_version.data());
                    int num = 0;
                    while (getline(ss, fl) && fl != "" && !iscntrl(fl[0]) && !isspace(fl[0]))
                    {
                        int pos = fl.find_first_of(':');
                        string lhs = fl.substr(0, pos), rhs = fl.substr(pos + 1);
                        if (rhs[0] == ' ')
                        {
                            rhs = rhs.substr(1);
                        }
                        for (auto &u : lhs)
                        {
                            u = isupper(u) ? tolower(u) : u;
                        }
                        cprintf(35, 0, "header #%d, lhs = %s, rhs = %s\n", ++num, lhs.data(), rhs.data());
                        header.push_back(make_pair(lhs, rhs));
                        header_list[lhs] = rhs;
                    }
                    content = http.substr(http.find("\r\n\r\n") + 4);
                    cprintf(35, 0, "content:\n%s\n------------------------------------\n", content.data());
                }
                catch (...)
                {
                    return;
                }
            }
            void operator()(string http)
            {
                ParseHttp(http);
            }
    };

    class X_WWW
    {
        public:
            map<string, string> key_value;
            void Parse(string str)
            {
                string s;
                stringstream ss(str);
                int num = 0;
                while (getline(ss, s, '&'))
                {
                    int pos = s.find('=');
                    string lhs = s.substr(0, pos), rhs = s.substr(pos + 1);
                    HTTP http;
                    lhs = http.DecodeURL(lhs);
                    rhs = http.DecodeURL(rhs);
                    key_value[lhs] = rhs;
                    cprintf(34, 0, "parse x-www, #%d, s = %s, lhs = %s, rhs = %s\n", ++num, s.data(), lhs.data(), rhs.data());
                }
            }
    };

    namespace JSON
    {

    }

    class MULTIPART
    {
        public:

    };
}
using namespace n_http;

// 线程
namespace n_threads
{
    DWORD WINAPI tTalk(LPVOID lpParam)
    {
        pair<SOCKET, pair<int, int>>
                                  psii; // 前一个是客户端 socket,后面是编号
        psii = *(pair<SOCKET, pair<int, int>> *)lpParam;
        // 获取 socket 与主次编号
        SOCKET clientfd = psii.first;
        int major = psii.second.first;
        int minor = psii.second.second;
        char *buf = new char[BUFLEN];
        int len = recv(clientfd, buf, BUFLEN, 0);
        if (len < 0)
            return EXIT_SUCCESS;
        string message(buf, len);
        delete []buf;
        cprintf(major, minor, "msg:\n%s\n", message.data());
        // 处理 http 请求
        //  stringstream ss(message); // 字符串流
        n_http::HTTP http;
        http.ParseHttp(message);
        string reqtype = http.request_type;
        //  ss >> reqtype;
        if (reqtype == "GET")  // 确保是 GET 方法
        {
            string path = http.request_path;
            //      ss >> path;
            path = ROOT + path;
            string version = http.http_version;
            //      ss >> version;
            message = VERSION" ";
            if (GetRedirect(path) != "")  // 重定向优先,不然文件一大堆
            {
                message += "302 Found\nLocation: " + GetRedirect(path).substr(strlen(
                               ROOT)) + "\n";
            }
            else if (IsForbidden(path))  // 403
            {
                message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(403));
            }
            else if (IsFileExist(path))
            {
                message += "200 OK\n\n" + GetFile(path);
            }
            else   // 404
            {
                cprintf(major, minor, "%s doen't exist\n", path.data());
                message += "200 OK\n\n" + GetFile(ROOT + GetErrorPage(404));
            }
        }
        else if (reqtype == "POST")
        {
            cprintf(major, minor, "content-type: %s\n",
                    http.header_list["content-type"].data());
            if (http.header_list["content-type"].find("application/x-www-form-urlencoded") != string::npos)
                // 采用 x-www 编码
            {
                string path = ROOT + http.request_path;
                n_http::X_WWW parser;
                parser.Parse(http.content);
                if (path == GetLoginPage())
                {
                    MD5::MD5 md5;
                    sockaddr_in addr;
                    int len = sizeof(sockaddr);
                    getpeername(clientfd, (sockaddr *)&addr, &len);
                    message = VERSION" 200 OK\n\n" + Login(GetIDByName(parser.key_value["username"]),
                                                           md5.encode(parser.key_value["password"]), inet_ntoa(addr.sin_addr));
                }
                else if (path == GetRegPage())
                {
                    MD5::MD5 md5;
                    message = VERSION" 200 OK\n\n" + Register(parser.key_value["username"],
                              md5.encode(parser.key_value["password"]));
                }
            }
            else if (http.header_list["content-type"].find("multipart/form-data") != string::npos)
            {
                string boundary = http.header_list["content-type"].substr(
                                      http.header_list["content-type"].find("; ") + 2
                                  );
                // multipart 处理语句
                n_http::MULTIPART multi;

            }
            else if (http.header_list["content-type"].find("application/json") != string::npos)
            {
                if (http.request_path.size() > 5 && http.request_path.substr(0, 5) == "/api/")
                {
                    string what = http.request_path.substr(5); // 具体访问什么
                    n_http::JSON::JSON json;

                }
            }
        }
        cprintf(major, minor, "Send msg to peer:\n%s\n", message.data());
        send(clientfd, message.data(), message.size(), 0); // http Server
        // 处理完后关闭
        shutdown(clientfd, SD_BOTH); // 这句话可要可不要,最好留下
        closesocket(clientfd);
        return EXIT_SUCCESS;
    }
    DWORD WINAPI tIPServer(LPVOID lpParam)
    {
        // lpParam 处是一个 string.
        string ip = *(string *)lpParam;
        int major = stoi(ip.substr(ip.find('@') + 1)); // 在 @ 后面寻找主版本号
        ip = ip.substr(0, ip.find('@'));
        // 创建 TCP 套接字(http 是基于 TCP 的)
        SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        // 写好绑定地址
        sockaddr_in listenaddr;
        ZeroMemory(&listenaddr, sizeof(listenaddr)); // 置零
        listenaddr.sin_addr.s_addr = inet_addr(ip.data()); // 绑定 IP
        listenaddr.sin_family = AF_INET;
        listenaddr.sin_port = htons(
                                  PORT); // 注意,这个 htons 是必需的,把主机地址转化为网络地址
        cprintf(major, 0, "Start, IP = %s\n", ip.data());
        // 绑定
        bind(listenfd, (sockaddr *)&listenaddr, sizeof(sockaddr));
        cprintf(major, 0, "bind OK\n");
        // 监听
        listen(listenfd, 20);
        cprintf(major, 0, "listen OK\n");
        // 循环接受连接请求
        SOCKET clientfd;
        sockaddr_in clientaddr;
        int tmplen = sizeof(sockaddr);
        int counter = 0;
        while ((clientfd = accept(listenfd, (sockaddr *)&clientaddr,
                                  &tmplen)) != INVALID_SOCKET)
        {
            cprintf(major, counter + 1, "收到来自 %s:%d 的连接请求\n",
                    inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
            if (IsConfused(inet_ntoa(clientaddr.sin_addr)))
            {
                closesocket(clientfd);
                continue;
            }
            pair<SOCKET, pair<int, int>> psii = make_pair(clientfd, make_pair(major,
                                                (++counter))); // 把东西放进去
            CreateThread(NULL, 0, tTalk, (LPVOID)&psii, 0,
                         NULL); // 取指针,传给 tTalk
        }
    }
}
using namespace n_threads;

// 指令处理函数
namespace n_command
{
    LPSTR *CommandLineToArgvA(LPCSTR lpCmdLine,
                              INT *pNumArgs)   // https://blog.csdn.net/qq_22000459/article/details/83539644
    {
        int retval;
        retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1, NULL,
                                     0);
        if (!SUCCEEDED(retval))
        {
            return NULL;
        }
        LPWSTR lpWideCharStr = (LPWSTR)malloc(retval * sizeof(WCHAR));
        if (lpWideCharStr == NULL)
        {
            return NULL;
        }
        retval = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, lpCmdLine, -1,
                                     lpWideCharStr, retval);
        if (!SUCCEEDED(retval))
        {
            free(lpWideCharStr);
            return NULL;
        }
        int numArgs;
        LPWSTR *args;
        args = CommandLineToArgvW(lpWideCharStr, &numArgs);
        free(lpWideCharStr);
        if (args == NULL)
        {
            return NULL;
        }
        int storage = numArgs * sizeof(LPSTR);
        for (int i = 0; i < numArgs; ++ i)
        {
            BOOL lpUsedDefaultChar = FALSE;
            retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, NULL, 0, NULL,
                                         &lpUsedDefaultChar);
            if (!SUCCEEDED(retval))
            {
                LocalFree(args);
                return NULL;
            }
            storage += retval;
        }
        LPSTR *result = (LPSTR *)LocalAlloc(LMEM_FIXED, storage);
        if (result == NULL)
        {
            LocalFree(args);
            return NULL;
        }
        int bufLen = storage - numArgs * sizeof(LPSTR);
        LPSTR buffer = ((LPSTR)result) + numArgs * sizeof(LPSTR);
        for (int i = 0; i < numArgs; ++ i)
        {
            assert(bufLen > 0);
            BOOL lpUsedDefaultChar = FALSE;
            retval = WideCharToMultiByte(CP_ACP, 0, args[i], -1, buffer, bufLen, NULL,
                                         &lpUsedDefaultChar);
            if (!SUCCEEDED(retval))
            {
                LocalFree(result);
                LocalFree(args);
                return NULL;
            }
            result[i] = buffer;
            buffer += retval;
            bufLen -= retval;
        }
        LocalFree(args);
        *pNumArgs = numArgs;
        return result;
    }
// 真正的处理
    string process(string cmd)
    {
        for (auto &u : cmd)
        {
            u = isupper(u) ? tolower(u) : u;
        }
        stringstream ss;
        ss << cmd;
        vector<string> argv;
        int argc;
        LPSTR *argv_ = CommandLineToArgvA(cmd.data(), &argc);
        for (int i = 0; i < argc; i++)
        {
            argv.push_back(argv_[i]);
        }
        string type = argv[0];
        if (type == "errorpage")
        {
            if (argc != 2 && argc != 3)
            {
                return "errorpage needs 1 or 2 arguments.";
            }
            int errc;
            try
            {
                errc = stoi(argv[1]);
            }
            catch (...)
            {
                return "Invalid errorcode " + argv[1];
            }
            if (errc < 100 || errc > 999)
            {
                return "Invalid errorcode " + to_string(errc);
            }
            if (argc == 2)
            {
                return "errorcode " + to_string(errc) + ": " + GetErrorPage(errc);
            }
            argv[2] = ROOT + argv[2];
            string file = argv[2];
            if (!IsFileExist(file))
            {
                return "File " + file + " doesn't exist.";
            }
            SetErrorPage(errc, file);
            return "success: Errc " + to_string(errc) + ", File " + file;
        }
        else if (type == "redirect")
        {
            if (argc != 2 && argc != 3)
            {
                return "redirect needs 1 or 2 arguments.";
            }
            argv[1] = ROOT + argv[1];
            if (argc == 2)
            {
                return argv[1] + " -> " + GetRedirect(argv[1]);
            }
            argv[2] = ROOT + argv[2];
            string file(argv[2]);
            if (!IsFileExist(file))
            {
                return "File " + file + " doesn't exist.";
            }
            SetRedirect(argv[1], argv[2]);
            return "success: " + argv[1] + " -> " + argv[2];
        }
        else if (type == "attrib")
        {
            if (argc != 2 && argc != 3)
            {
                return "attrib needs 1 or 2 arguments.";
            }
            argv[1] = ROOT + argv[1];
            string file(argv[1]);
            if (!IsFileExist(file))
            {
                return "File " + file + " doesn't exist.";
            }
            if (argc == 2)
            {
                return file + ": " + (IsForbidden(file) ? "private" : "public");
            }
            bool priv;
            if (argv[2] == "--public")
            {
                priv = 0;
            }
            else if (argv[2] == "--private")
            {
                priv = 1;
            }
            else
            {
                return "Invalid argument " + argv[2];
            }
            SetFileAttrib(file, priv);
            return "success: " + argv[1] + " forbidden: " + to_string(priv);
        }
        else if (type == "blacklist")
        {
            if (argc != 2 && argc != 3)
            {
                return "blacklist needs 1 or 2 arguments.";
            }
            string IP = argv[1];
            if (inet_addr(IP.data()) == INADDR_NONE)
            {
                return "invalid IP address " + IP;
            }
            if (argc == 2)
            {
                return IP + ": " + (IsConfused(IP) ? "confuse" : "accept");
            }
            bool c;
            if (argv[2] == "--accept")
            {
                c = 0;
            }
            else if (argv[2] == "--confuse")
            {
                c = 1;
            }
            else
            {
                return "Invalid argument " + argv[2];
            }
            Confuse(IP, c);
            return "success: " + argv[1] + " confuse: " + to_string(c);
        }
        else if (type == "user")
        {
            if (argc < 4)
            {
                return "user needs 3+ arguments.\nUsage:\nuser register [name] [pass]\nuser get name [ID]\nuser get ID --name [name]\nuser get pass [ID]\nuser get ID --IP [IP]\nuser change name [ID] [newname]\nuser change pass [ID] [newpass]\nuser login [IP] [ID] [pass]\nuser logout IP [IP]\nuser logout ID [ID]";
            }
            if (argv[1] == "register")
            {
                MD5::MD5 md5;
                system("cls");
                return Register(argv[2], md5.encode(argv[3]));
            }
            else if (argv[1] == "get")
            {
                if (argv[2] == "name")
                {
                    int id;
                    try
                    {
                        id = stoi(argv[3]);
                    }
                    catch (...)
                    {
                        return "invalid id " + argv[3];
                    }
                    if (id < 1 || id > TotalID())
                    {
                        return "invalid id " + argv[3];
                    }
                    return GetNameByID(id);
                }
                else if (argv[2] == "id")
                {
                    if (argc < 5)
                    {
                        return "user get id needs 2 arguments.";
                    }
                    if (argv[3] == "--name")
                    {
                        return to_string(GetIDByName(argv[4]));
                    }
                    else if (argv[3] == "--ip")
                    {
                        return to_string(GetIDByIP(argv[4]));
                    }
                    else
                    {
                        return "user get ID --name [name]\nuser get ID --IP [IP]";
                    }
                }
                else if (argv[2] == "pass")
                {
                    int id;
                    try
                    {
                        id = stoi(argv[3]);
                    }
                    catch (...)
                    {
                        return "invalid id " + argv[3];
                    }
                    if (id < 1 || id > TotalID())
                    {
                        return "invalid id " + argv[3];
                    }
                    return "Encrypted: " + GetEncryptedPassword(id);
                }
                else
                {
                    return "user get name [ID]\nuser get ID --name [name]\nuser get pass [ID]\nuser get ID --IP [IP]";
                }
            }
            else if (argv[1] == "change")
            {
                if (argc < 5)
                {
                    return "user change needs 3 arguments.";
                }
                if (argv[2] == "name")
                {
                    int id;
                    try
                    {
                        id = stoi(argv[3]);
                    }
                    catch (...)
                    {
                        return "invalid id " + argv[3];
                    }
                    if (id < 1 || id > TotalID())
                    {
                        return "invalid id " + argv[3];
                    }
                    return ChangeUsername(id, argv[4]);
                }
                else if (argv[2] == "pass")
                {
                    int id;
                    try
                    {
                        id = stoi(argv[3]);
                    }
                    catch (...)
                    {
                        return "invalid id " + argv[3];
                    }
                    if (id < 1 || id > TotalID())
                    {
                        return "invalid id " + argv[3];
                    }
                    system("cls");
                    MD5::MD5 md5;
                    return ChangePassword(id, md5.encode(argv[4]));
                }
                else
                {
                    return "user change name [ID] [newname]\nuser change pass [ID] [newpass]";
                }
            }
            else if (argv[1] == "login")
            {
                if (argc < 5)
                {
                    return "user login needs 3 arguments.";
                }
                int id;
                try
                {
                    id = stoi(argv[3]);
                }
                catch (...)
                {
                    return "invalid id " + argv[3];
                }
                if (id < 1 || id > TotalID())
                {
                    return "invalid id " + argv[3];
                }
                system("cls");
                MD5::MD5 md5;
                return Login(id, md5.encode(argv[4]), argv[2]);
            }
            else if (argv[1] == "logout")
            {
                if (argv[2] == "ip")
                {
                    return Logout(argv[3]);
                }
                else if (argv[2] == "id")
                {
                    int id;
                    try
                    {
                        id = stoi(argv[3]);
                    }
                    catch (...)
                    {
                        return "invalid id " + argv[3];
                    }
                    if (id < 1 || id > TotalID())
                    {
                        return "invalid id " + argv[3];
                    }
                    INI ini;
                    ini.open(CONFIG);
                    return Logout(ini.Read("lastip", to_string(id)));
                }
            }
            else
            {
                return "user register [name] [pass]\nuser get name [ID]\nuser get ID --name [name]\nuser get pass [ID]\nuser get ID --IP [IP]\nuser change name [ID] [newname]\nuser change pass [ID] [newpass]\nuser login [IP] [ID] [pass]\nuser logout IP [IP]\nuser logout ID [ID]";
            }
        }
        else if (type == "page")
        {
            if (argc < 2)
            {
                return "page needs 1 or 2 arguments.";
            }
            if (argv[1] == "login")
            {
                if (argc == 2)
                {
                    return GetLoginPage();
                }
                return SetLoginPage(argv[2]);
            }
            else if (argv[1] == "register")
            {
                if (argc == 2)
                {
                    return GetRegPage();
                }
                return SetRegPage(argv[2]);
            }
            else
            {
                return "invalid arg " + argv[1];
            }
        }
        else   // help
        {
            return "errorpage 错误码 [文件]\nredirect 网址 [重定向目标]\nattrib 文件 [--public | --private]\nblacklist IP [--confuse | --accept]\nuser register [name] [pass]\nuser get name [ID]\nuser get ID --name [name]\nuser get pass [ID]\nuser get ID --IP [IP]\nuser change name [ID] [newname]\nuser change pass [ID] [newpass]\nuser login [IP] [ID] [pass]\nuser logout IP [IP]\nuser logout ID [ID]\npage [login | register] [page]";
        }
    }
}
using namespace n_command;

// 各种主函数
namespace n_main
{
    int httpmain()
    {
        // 在 Windows 下需要加上这一段。
        WSADATA wsd;
        WSAStartup(MAKEWORD(2, 2), &wsd);
        // 获取所有 IP
        string strIP("127.0.0.1");
        vector<string> ret;
        ret.clear();
        ret.push_back(strIP + "@0");
        cprintf(0, 0, "127.0.0.1\n");
        char *strName = new char[1024];
        gethostname(strName, 1024);
        struct hostent *pHostEnt = gethostbyname(strName);
        delete []strName;
        int n = 0;
        while (pHostEnt->h_addr_list[n] != NULL)
        {
            stringstream ss;
            ss << (pHostEnt->h_addr_list[n][0] & 0x00FF) << "."
               << (pHostEnt->h_addr_list[n][1] & 0x00FF) << "."
               << (pHostEnt->h_addr_list[n][2] & 0x00FF) << "."
               << (pHostEnt->h_addr_list[n][3] & 0x00FF) << flush;
            string strTemp(ss.str());
            cprintf(n + 1, 0, (strTemp + "\n").data());
            if (string::npos == strTemp.find("127.0.0."))
            {
                ret.push_back(strTemp + "@" + to_string(n +
                                                        1)); // 此处的 @ 是用于分隔 IP 与编号
            }
            n++;
        }
        // 对每个 IP 启动线程
        for (int i = 0; i < ret.size(); i++)
        {
            CreateThread(NULL, 0, tIPServer, (LPVOID)&ret[i], 0, NULL);
        }
        // 死循环,不要提前退了
        while (1);
        // 释放 socket 资源
        WSACleanup();
        return 0;
    }
    void commandmain()
    {
        while (true)
        {
            col(3);
            cout << "Input command > " << flush;
            string cmd;
            col(2);
            getline(cin, cmd);
            col(0);
            cout << "command response:" << endl;
            col(5);
            cout << process(cmd) << endl;
            col();
        }
    }
}
using namespace n_main;

int main(int argc, char *argv[])
{
    try
    {
        if (argc == 1)  // 主窗口
        {
            ShellExecuteA(NULL, "open", argv[0], "cmd", NULL, SW_SHOW);
            httpmain();
        }
        else   // 从窗口
        {
            commandmain();
        }
    }
    catch (...)
    {
        ShellExecuteA(NULL, "open", argv[0], NULL, NULL, SW_SHOW);
        return 114514;
    }
    return 0;
}

然后再实现一个 json 的解析器,以解析客户端传来的访问请求。

这是解析器,定义在 namespace n_http 中:

    namespace JSON
    {
        enum class JSONValueType
        {
            OBJECT,
            ARRAY,
            STRING,
            NUMBER,
            BOOLEAN,
            NULL_VALUE,
            INVALID
        };

        struct JSONValue
        {
            JSONValueType type;
            string stringValue;
            double numberValue;
            bool boolValue;
            /*unordered_*/map<string, JSONValue> objectValue;
            vector<JSONValue> arrayValue;
        };

        class JSON
        {
            public:
                JSON(const string &jsonStr) : jsonStr(jsonStr), pos(0) {}

                JSONValue parse()
                {
                    return parseValue();
                }

            private:
                JSONValue parseValue()
                {
                    skipWhitespace();

                    switch (jsonStr[pos])
                    {
                        case '{':
                            return parseObject();
                        case '[':
                            return parseArray();
                        case '"':
                            return parseString();
                        case 't':
                        case 'f':
                            return parseBoolean();
                        case 'n':
                            return parseNull();
                        default:
                            if (isdigit(jsonStr[pos]) || jsonStr[pos] == '-')
                            {
                                return parseNumber();
                            }
                            return JSONValue{JSONValueType::INVALID};
                    }
                }

                JSONValue parseObject()
                {
                    JSONValue result;
                    result.type = JSONValueType::OBJECT;
                    pos++;

                    while (pos < jsonStr.length() && jsonStr[pos] != '}')
                    {
                        skipWhitespace();
                        string key = parseString().stringValue;
                        skipWhitespace();
                        if (jsonStr[pos] == ':')
                        {
                            pos++;
                            JSONValue value = parseValue();
                            result.objectValue[key] = value;
                        }
                        skipWhitespace();
                        if (jsonStr[pos] == ',')
                        {
                            pos++;
                        }
                    }

                    pos++; // Skip the '}'
                    return result;
                }

                JSONValue parseArray()
                {
                    JSONValue result;
                    result.type = JSONValueType::ARRAY;
                    pos++;

                    while (pos < jsonStr.length() && jsonStr[pos] != ']')
                    {
                        skipWhitespace();
                        JSONValue value = parseValue();
                        result.arrayValue.push_back(value);
                        skipWhitespace();
                        if (jsonStr[pos] == ',')
                        {
                            pos++;
                        }
                    }

                    pos++; // Skip the ']'
                    return result;
                }

                JSONValue parseString()
                {
                    JSONValue result;
                    result.type = JSONValueType::STRING;
                    pos++; // Skip the opening '"'

                    while (pos < jsonStr.length() && jsonStr[pos] != '"')
                    {
                        result.stringValue += jsonStr[pos];
                        pos++;
                    }

                    pos++; // Skip the closing '"'
                    return result;
                }

                JSONValue parseNumber()
                {
                    JSONValue result;
                    result.type = JSONValueType::NUMBER;
                    string numStr;

                    while (pos < jsonStr.length() &&
                            (isdigit(jsonStr[pos]) || jsonStr[pos] == '-' || jsonStr[pos] == '.'))
                    {
                        numStr += jsonStr[pos];
                        pos++;
                    }

                    result.numberValue = stod(numStr);
                    return result;
                }

                JSONValue parseBoolean()
                {
                    JSONValue result;
                    result.type = JSONValueType::BOOLEAN;

                    if (jsonStr.compare(pos, 4, "true") == 0)
                    {
                        result.boolValue = true;
                        pos += 4;
                    }
                    else if (jsonStr.compare(pos, 5, "false") == 0)
                    {
                        result.boolValue = false;
                        pos += 5;
                    }
                    else
                    {
                        result.type = JSONValueType::INVALID;
                    }

                    return result;
                }

                JSONValue parseNull()
                {
                    JSONValue result;
                    result.type = JSONValueType::NULL_VALUE;

                    if (jsonStr.compare(pos, 4, "null") == 0)
                    {
                        pos += 4;
                    }
                    else
                    {
                        result.type = JSONValueType::INVALID;
                    }

                    return result;
                }

                void skipWhitespace()
                {
                    while (pos < jsonStr.length() && isspace(jsonStr[pos]))
                    {
                        pos++;
                    }
                }

                string jsonStr;
                size_t pos;
        };
    }

这个解释器,传进去一个字符串,会返回一个层层嵌套的类似递归定义的 JSON 语法树。然后,我们只要分析一下里面几个键值就可以确定对方想要什么,我们要给他什么。我们还是新建一个函数,形参为解析过的 JSON 语法树和大概访问内容(URL 中 .../api/ 后面的部分),返回值则为我们要传递给对方的信息。呕对,我们也是要传给对方 JSON 的,所以我们还要实现一个 json 的组装器。

我们组装最好组装得规范一点,免得出什么幺蛾子,所以我在这里借用一下菜鸟的语法规则图,或者干脆去页面看:

这是组装器的源码,定义在 n_http::JSON 里。这个组装器相当于是解析逆操作,传进去一个 JSONValue 的语法树,它给你组装好。这里我并不打算再写一个类。一个递归调用的函数足以胜任这个工作:

        string Builder(JSONValue json)
        {
            string ret = " "; // 每个 value 外面都有空格
            switch(json.type)
            {
            case JSONValueType::OBJECT:
                {
                    ret += "{ ";
                    bool is_first = 1;
                    for(auto u : json.objectValue)
                    {
                        if(!is_first) ret += ", ";
                        is_first = 0;
                        ret += '"';
                        ret += u.first;
                        ret += '"';
                        ret += " :";
                        ret += Builder(u.second); // value
                    }
                    ret += "}";
                    break;
                }
            case JSONValueType::ARRAY:
                {
                    ret += "[";
                    if(json.arrayValue.size() == 0) ret += " "; // 我这是严格按照它写的hh
                    else for(int i = 0; i < json.arrayValue.size(); i++)
                    {
                        if(i != 0) ret += ",";
                        ret += Builder(json.arrayValue[i]);
                    }
                    ret += "]";
                    break;
                }
            case JSONValueType::BOOLEAN:
                {
                    ret += json.boolValue ? "true" : "false";
                    break;
                }
            case JSONValueType::NULL_VALUE:
                {
                    ret += "null";
                    break;
                }
            case JSONValueType::STRING:
                {
                    ret += '"'; // 懒得打反斜杠(doge
                    ret += json.stringValue;
                    ret += '"';
                    break;
                }
            case JSONValueType::NUMBER:
                {
                    ret += to_string(json.numberValue);
                    break;
                }
            case JSONValueType::INVALID:
            default:
                {
                    ret += "\"500 Server Error (ノ`Д)ノ!\""; // 这种情况貌似不会出现,乱搞一下。正规点应该要打500
                    break;
                }
            }
            ret += " ";
            return ret;
        }

接下来,还不能写函数。我们要规定各种函数对应什么 API,以及对应的 json 请求应该长什么样。列表!

访问 URL 客户端 POST 过来的 json 我们告诉客户端的 json
/api/user/changepassword { "id" : 1 , "old" : "xxx" , "new" : "xxx" } { "status" : "完成情况" }
/api/user/changeusername { "id" : 1 , "new" : "xxx" } 同上
/api/user/getid/ip { "ip" : "127.0.0.1" } { "id" : 1 }
/api/user/getid/name { "name" : "xxx" } { "id" : 1 }
/api/user/getname/id { "id" : 1 } { "name" : "xxx"}
/api/auth/login { "id" : 1 , "pass" : "xxx" , "ip" : "127.0.0.1" } 同 1
/api/auth/logout { "ip" : "127.0.0.1" } 同上
/api/auth/register { "name" : "xxx" , "pass" : "xxx" } 同上

总算可以写函数了。这个函数,我定义在 n_thread 里面。虽然这并不是一个线程,但是为了方便起见(其实是我傻*想不到该归到哪一类),就……了。多说无益,放贷码:

    string UseAPI(string what, n_http::JSON::JSONValue json)
    {
        n_http::JSON::JSONValue ret;
        ret.type = n_http::JSON::JSONValueType::OBJECT;
        if(what == "user/changepassword")
        {
            ret.objectValue["status"].type = n_http::JSON::JSONValueType::STRING;
            MD5::MD5 md5;
            ret.objectValue["status"].stringValue = (n_ini::GetEncryptedPassword(json.objectValue["id"].numberValue) == md5.encode(json.objectValue["old"].stringValue)) ? n_ini::ChangePassword(json.objectValue["id"].numberValue, md5.encode(json.objectValue["new"].stringValue)) : "Wrong Password";
        }
        if(what == "user/changeusername")
        {
            ret.objectValue["status"].type = n_http::JSON::JSONValueType::STRING;
            ret.objectValue["status"].stringValue = n_ini::ChangeUsername(json.objectValue["id"].numberValue, json.objectValue["new"].stringValue);
        }
        if(what == "user/getid/ip")
        {
            ret.objectValue["id"].type = n_http::JSON::JSONValueType::NUMBER;
            ret.objectValue["id"].numberValue = n_ini::GetIDByIP(json.objectValue["ip"].stringValue);
        }
        if(what == "user/getid/name")
        {
            ret.objectValue["id"].type = n_http::JSON::JSONValueType::NUMBER;
            ret.objectValue["id"].numberValue = n_ini::GetIDByName(json.objectValue["name"].stringValue);
        }
        if(what == "user/getname/id")
        {
            ret.objectValue["name"].type = n_http::JSON::JSONValueType::STRING;
            ret.objectValue["name"].stringValue = n_ini::GetNameByID(json.objectValue["id"].numberValue);
        }
        if(what == "auth/login")
        {
            ret.objectValue["status"].type = n_http::JSON::JSONValueType::STRING;
            MD5::MD5 md5;
            ret.objectValue["status"].stringValue = n_ini::Login(json.objectValue["id"].numberValue, md5.encode(json.objectValue["pass"].stringValue), json.objectValue["ip"].stringValue);
        }
        if(what == "auth/logout")
        {
            ret.objectValue["status"].type = n_http::JSON::JSONValueType::STRING;
            ret.objectValue["status"].stringValue = n_ini::Logout(json.objectValue["ip"].stringValue);
        }
        if(what == "auth/register")
        {
            ret.objectValue["status"].type = n_http::JSON::JSONValueType::STRING;
            MD5::MD5 md5;
            ret.objectValue["status"].stringValue = n_ini::Register(json.objectValue["name"].stringValue, md5.encode(json.objectValue["pass"].stringValue));
        }
        return n_http::JSON::Builder(ret);
    }

4. 退出登录

实现退出登录,只需在个人简介页加一个按钮即可。既然我们已经给 js 开放了 API,那我们就用 js,不要浪费。

这是个人简介页的 html

<!doctype html>
<html lang="zh-cn">
    <head>
        <title id="title"></title>
    </head>
    <body>
        <h1 id="title"></h1>
        用户 ID:<p id="id"></p>
        <br>
        <form action="/changename" method="POST" enctype="application/x-www-form-urlencoded">
            用户名:<input type="text" name="username" id="username"><input type="submit" value="更改用户名">
        </form>
        <br>
        更改密码:<br>
        <form action="/changepass" method="POST" enctype="application/x-www-form-urlencoded" oninput="x.value=a.value==b.value?'':'两次密码不一样';y.type=a.value==b.value?'submit':'hidden'">
            原密码:<input type="password" name="oldpassword"><br>
            密码:<input type="password" name="password" id="a"><br>
            重复密码:<input type="password" id="b"><output style="color:red" id="x" for="a b"></output><br>
            <input type="submit" id="y" value="注册">
        </form>
    </body>
</html>

后半部分敬请期待