官方服务微信:dat818 购买与出租对接

利用TCP和HTTP协议搭建网站教程,含C/C++基础及关键代码

3万

主题

2

回帖

11万

积分

管理员

积分
114813
发表于 昨天 10:55 | 显示全部楼层 |阅读模式
    [id_20[]]

    要掌握个人网站搭建的技能,首先必须具备C/C++编程的基础知识,同时理解服务器和客户端的基本概念。此外,如果你对TCP或HTTP协议有所了解,这将大大加速你学习搭建个人网站的过程。

    该服务器采用IOCP模式进行操作,我将对put代码中至关重要的几个环节进行详细阐述。

    请准备好您的HTML文档,这将是您即将发布的网页,它既可以是静态页面,也可以是动态页面。为了帮助大家顺利入门,我特别准备了一份非常简单的HTML代码示例。

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'>    <pre class="syl-page-code"><code><!DOCTYPE html>
<html lang="ch-ZH">
<head>
  <meta charset="UTF-8">
  <title>My Website</title>
  <link rel="icon" href="img/sad.png">
  <link rel="stylesheet" href="index.css">
</head>
<body>
  
    <div style="width: 100px;height:100px;">
      
    </div>
    <span>HelloWorld</span>
    <div style="background-color: blue; border: 1px solid orange">
      Hi.Im david
    </div>
</body>
</html>
该决定明确指出,所有相关活动均不得擅自更改既定规则,严格遵循现有流程,确保各项操作符合既定标准,不得有任何形式的违规行为。</code></pre></p>
    您可以将此文档直接复制,然后将其保存为HTML格式,文件名称应命名为 index.html。

    若您对自学编写代码以构建个人网站的过程不感兴趣,请点击该链接。

    正式的编程活动即将展开。鉴于您已具备TCP/IP协议的相关知识,我将省略对这一领域的详细阐述。

    首先,我们需要搭建开发环境。若你采用的是VS,那么,你需遵循以下步骤以完成网络通信环境的构建。

    创建项目后,请进入项目属性界面。在链接器设置中,切换到输入选项部分,然后在附加依赖项中新增“.lib”文件;这个库是专门用于网络通信的。

    引入相关头文件,即 # 和 #,需特别留意:务必将其放置在指定位置,否则将引发重复定义的问题。

    b) 创建套接字过程代码

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'>    <pre class="syl-page-code"><code>#include <WinSock2.h>
#include <Windows.h>
int main(){
在Windows操作系统中,若要创建套接字,必须使用WSADATA类型的对象。
        HANDLE hComPort;                //完成端口CP对象
        SYSTEM_INFO sysInfo;        //获取系统信息
        WSAEVENT wEvent;                //重叠事件
客户端所涉及的信息,尤其是与I/O操作相关的部分,必须确保是动态分配的。若非如此,一旦这些信息被传递至其他线程,将导致引用的是原始地址,进而无法实现对新客户端与I/O数据的重新分配。
定义LPCLNTINFO类型的变量pClntInfo,用于存储客户端的相关信息。
LP_IO_DATA指针pIoData,负责存储与I/O操作相关的信息。
定义服务器端套接字为servSock,客户端套接字为clntSock;
定义了两个结构体变量,分别用于存储服务器和客户端的地址信息。
        int clntAddrSz;                                        //客户端地址长度
DWORD变量recvBytes和flag分别用于存储接收到的数据量以及状态标识。
        int port = 0;                                        //端口号
创建文件输出流对象connectLog,用于将客户端的连接数据写入"connectLog.txt"文件中。
       //这是一个处理输入的函数,我们将会在之后讲到。
执行getPortNumber函数对传入的port进行处理,从中提取端口号。
若启动Winsock库成功,即(WSAStartup函数调用返回值非空),且版本号正确,即(MAKEWORD(2, 2)构造的版本号),并且wsaData结构体被成功初始化,即(&wsaData作为参数传递给函数后非空)。
显示错误信息:启动Winsock服务失败。
        ///创建完后端口号。
hComPort等于创建了一个I/O完成端口,该端口以INVALID_HANDLE_VALUE作为句柄值,以NULL作为回调函数的上下文,并且设置了0个线程和0个完成端口数量。
执行GetSystemInfo函数,传入sysInfo参数以获取系统详细信息。
        ///创建多个线程来分离IO处理
        for (int i = 0; i < sysInfo.dwNumberOfProcessors; ++i)
                _beginthreadex(NULL, 0, ClntHandle, (LPVOID)hComPort, 0, NULL);                //开始线程处理。
       
        servSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);        //创建重叠套接字
        ///错误处理
        if (servSock == INVALID_SOCKET)
                ErrorMsg("WSASocket() Error");
        ///初始化IP地址和端口号。
        memset(&servAddr, 0, sizeof(servAddr));
        servAddr.sin_family = AF_INET;                                //IPV4协议
        servAddr.sin_port = htons(port);                        //小端序转位网路大端序
        servAddr.sin_addr.s_addr = htonl(INADDR_ANY);                //自动获取本地IP地址
        ///绑定服务器地址信息
        if (bind(servSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
                ErrorMsg("bind() error");
        ///监听来自客户端的连接请求。 500表示最大接代500个客户端同时请求。
        if (listen(servSock, 500) == SOCKET_ERROR)
                ErrorMsg("listen() error");
clntAddrSz = sizeof(clntAddr);                //获取客户端地址结构的大小
        std::cout << std::left;                                //设置左对齐
        处理来自客户端的连接请求
        while (1)
        {
               
                clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &clntAddrSz);
                wEvent = WSACreateEvent();                //存放事件句柄
                if (clntSock == INVALID_SOCKET)        //处理连接请求出错
                {
                        ErrorMsg("accept() error");
                }               
                ///动态分配内存处理客户端的连接
                pClntInfo = (LPCLNTINFO)malloc(sizeof(CLNTINFO));
                pClntInfo->hClntSock = clntSock;
将客户端套接字的地址信息复制至pClntInfo结构体的hClntAddr成员变量中,具体操作是将clntAddr指针指向的数据块,按照clntAddrSz的大小,复制到pClntInfo的hClntAddr所指向的内存区域。
                /// 建立套接字到完后端口的连接
创建IO完成端口时,将客户端套接字的句柄、通信端口的句柄、客户端信息指针以及一个零值传递给函数,其中包含了指向clntInfo整个结构体的地址,之后该地址可以被提取使用。
                /// 动态分配保存着数据传输信息的IO对象
pIoData是指向IO_DATA结构体的指针,通过调用malloc函数,分配了足够存储一个IO_DATA结构体所需的空间。
初始化结构体(pIoData->overlapped)的所有成员为0,操作大小为OVERLAPPED结构体所占的字节数。
pIoData的wsaBuf成员的buf指针被设置为pIoData的buf指针;pIoData->wsaBuf.buf等于pIoData->buf。
pIoData->wsaBuf.length赋值为BUF_SIZE;同时,pIoData指针指向的wsaBuf结构体的长度成员被设置为缓冲区的大小。
                ///接收客户端信息
若客户端尚未向服务器传输任何数据,则可推断,客户端正需向服务器提出数据请求。
若在接收客户端套接字数据时,调用WSARecv函数未能成功,即返回值为SOCKET_ERROR,那么在执行过程中,传递给该函数的参数包括指向ioData结构体的整个地址、接收缓冲区wsaBuf、接收缓冲区大小1、接收到的字节数recvBytes的指针、标志flag的指针以及overlapped结构体的地址,而最后一个参数为NULL。
                {
若调用WSAGetLastError()函数返回的错误码为WSA_IO_PENDING,则表明数据接收过程尚未完成。
        //                {
                        //        std::cout << "数据仍在接收中..." << std::endl;
        //                }
                       
                }       
               
        }


        closesocket(servSock);
        WSACleanup();
        return 0;
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899</code></pre></p>
    其中相关数据结构如下:

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'>    <pre class="syl-page-code"><code>通过使用结构指针的地址,即结构体首成员的地址,来传递关于IO端口的详细信息。
typedef struct
{
        SOCKET hClntSock;                        //客户端套接字
定义变量hClntAddr为SOCKADDR_IN类型,用于存储客户端的IP地址信息。
客户端信息结构体,以及指向该结构体的指针,它们存储了客户端套接字的属性数据。
typedef struct
{
重叠特性结构,重叠属性结构;
wsaBuf变量用于存储缓冲数据,其中主要包含了待传输数据的尺寸信息以及缓冲区的地址。
定义了一个数组buf,其大小为BUF_SIZE,用于存储数据。
IO_DATA和LP_IO_DATA指针,它们承载着与输入输出操作相关的数据信息。
12345678910111213</code></pre></p>
    之前所提及的函数,其C++版本的实现涉及输入处理。若您熟悉C语言,亦可采用C代码进行替换。需特别指出的是,我所编写的代码是C与C++两种语言的混合实现。此函数的核心功能在于确保用户输入的数据流是准确无误的。

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'>    <pre class="syl-page-code"><code>定义函数以获取端口号,参数为引用类型整数。
{
        ///输入端口号进行监听
        std::cout << "输入有效的监听端口号:";
        ///确保输入了有效的换行符
        while (!(std::cin >> port))
        {
清除cin的错误标志,同时重置输入流。
在输入出现错误的情况下,程序将忽略输入流中的部分缓冲数据。这一过程最多会忽略224个字符,直至遇到换行符。
                std::cout << "输入的端口号有误,重新输入:";
        }
}
12345678910111213</code></pre></p>
    接下来需要分别讲解在main函数之中调用的各个函数:

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'>    <pre class="syl-page-code"><code>/// 建立套接字到完后端口的连接
创建I/O完成端口的操作被执行,其中客户端套接字句柄被转换为HANDLE类型后传递给函数,通信端口句柄作为参数之一,客户端信息指针以DWORD类型传递,最后参数0被指定。
12</code></pre></p>
    该函数扮演着至关重要的角色,其主要任务在于将接入的客户端与我们的输入输出端口实现有效连接。

    之后该套接字存在事件时,将会转到我们的线程处理函数之中。

    开始接收来自客户端的请求数据。使用的是重叠IO无阻塞模式。

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'>    <pre class="syl-page-code"><code>调用WSARecv函数,传入客户端套接字clntSock,以及指向pIoData结构体中wsaBuf成员的指针,参数1表示接收一个数据包,接收字节数recvBytes和标志flag的指针分别赋值给LPDWORD类型的变量,同时传入指向pIoData结构体中overlapped成员的指针用于异步操作,最后一个参数为NULL。
1</code></pre></p>
    之后每次当客户端发出请求数据,都会在以下函数中处理

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'>    <pre class="syl-page-code"><code>参数cp代表客户端的指针。
{
接收字节数量设为recvBytes,标志位设为flag。
LP_IO_DATA pIoData,用于存储IO对象的相关信息;该结构体与通过WSARecv函数接收的overlapped结构相关联。
LPCLNTINFO pClntInfo; // 用于存储与客户端相关的详细资料,该变量通过传递给CreateIoCompletionPort函数的第三个参数来获取。
SOCKET类型的变量sock;这主要是指代客户端的套接字。
HANDLE变量hComPort被赋值为CP对象的类型转换结果。
        while (1)
        {
                ///确定IO完成状态
若未能成功获取队列完成状态,无法从hComPort获取接收字节数,无法获取客户端信息指针,无法获取I/O重叠结构指针,且超时设置无限。
                {
在客户端进行数据请求时,若请求被中断,该函数将触发错误,因此我们必须从错误状态中恢复并解决问题。
                        std::cout << GetLastError() << std::endl;
                        closesocket(pClntInfo->hClntSock);                //关闭该套接字。
执行FreeData函数,以释放由pIoData和pClntInfo指向的动态分配内存。
                        continue;
                }
                ///连接的客户端 IP地址信息
在将客户端地址转换为点分十进制表示后,connectCnt数组的相应元素值增加一。
                sock = pClntInfo->hClntSock;
piodata结构体中的wsaBuf成员的buf数组,在接收到的字节recvBytes的位置上,被赋值为0。
若接收的字节数量为零,则表明此刻尚未获取到任何数据。
                {
                        closesocket(sock);
释放数据指针,指向输入输出数据结构,以及客户端信息结构。
                        continue;
                }
        //        std::cout << "开始接收客户端信息:" << pIoData->wsaBuf.buf << std::endl;
                std::string fileName;        //文件名
                std::string compleHead(pIoData->wsaBuf中的buf字段,用于存储部分头部信息,其容量限制为100字符。
                char cntType[SMALL_SIZE];
int mFind = compleHead.indexOf('/'); //定位到请求类型的位置
compleHead中搜索"HTTP/"的结果被赋值给变量fFind。
若非GET类型请求,则不予理睬;对于非HTTP/协议产生的请求,同样不予关注;遇到格式有误的请求,亦不予理会。
若(mFind 等于 std::string::npos 或 fFind 等于 std::string::npos),
如果从compleHead中截取的字符串,长度从0到mFind减1的部分,与"GET"不匹配,则表示请求方式有误。
                {
                        closesocket(sock);
对客户端信息指针指向的地址进行异常请求处理,并将涉及的数据缓冲区内容写入系统日志文件中。
        释放数据指针pIoData,同时释放客户端信息指针pClntInfo。
                        continue;
                }
在多种可能性被推断之后,此情形下获取文件名应当是相对安全的。
fileName赋值为从compleHead字符串的mFind位置之后开始,到fFind位置之前结束的子字符串。
若文件名处于空缺状态,则意味着用户所请求的是网站的主页。
                {
                        fileName = "index.html";
将cntType变量赋值为"text/html"字符串。
                }
                else
                {
cs是指向获取文件类型结果的指针,该指针通过调用GetContentType函数并传入fileName参数得到。
当返回的指针为空,意味着文件不存在,且其后缀未被指定。
                        {
                                ErrorFile(sock);
释放数据指针pIoData,并传递客户端信息指针pClntInfo至FreeData函数。
                                continue;
                        }
cntType = cs; // 将cs的内容复制到cntType变量中
                }
                ///给客户端发送数据
向套接字发送数据文件,文件名为fileName,类型为cntType,数据由pIoData指针指向。
                ///释放内存
                closesocket(sock);
                FreeData(&pIoData, &pClntInfo);
        //        std::cout << "网页数据发送完成..." << std::endl;
        }
        return 0;
}


禁止对特定内容进行修改,确保专有名词不受影响,同时维护原文的语言风格。将原本的长句拆分为若干简短的小句,以逗号分隔,避免遗漏任何重要信息。65666768697071727374757677787980818283848586</code></pre></p>
    该函数运用了一个至关重要的函数,该函数的职责是向客户端传输所需文件。

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'>    <pre class="syl-page-code"><code>
执行发送数据文件操作,需传入套接字参数sock,文件名参数fileName,内容类型参数contType,以及引用类型参数pIoData。
{
以读取模式,使用二进制模式打开文件名为fileName的输入文件流。
        if (!inFile)                        //请求的文件不存在时
        {
                ErrorFile(sock);
                return -1;
        }
        ///传输回应头信息
回车符号(Carriage return)代表字符的换行,而换行符号(New line)则用于标识文本的段落分隔。
char protocol[] = "HTTP版本1.1,状态码200,表示成功\r\n";
char servName[] = "此服务器名为TD网络服务器,";
cntEncode数组存储的内容为:“内容编码:gzip”,表示压缩方式。
transEncode字符数组表示的字符串为:"传输编码方式:分块编码\r\n"。
char vary[] = "Vary: 允许的编码类型\r\n";
定义了一个字符数组cntType,其大小为SMALL_SIZE,用于接收特定类型的数据。
        char buf[BUF_SIZE];                //数据
        char end[] = "\r\n";                                //结束符
定义一个名为cntLen的数组,其大小为SMALL_SIZE,用于存储内容长度。
        WSABUF wsaBuf;                                                //存放数据缓冲
        DWORD sendBytes = 0;
创建了一个名为wsaEvent的事件对象,该对象是通过调用WSACreateEvent函数实现的。
       
cntType变量被赋予了一个字符串,该字符串格式为"Content-type:"后跟contType变量的值,并在其后添加了回车换行符和换行符。
        ///向客户端发送回应头信息
向套接字发送协议信息,包含协议长度,并指定无附加信息。
向套接字发送信息,包括服务端名称、名称长度以及一个标志位,即执行传递消息头中的服务端名称操作。
向套接字发送(content-type)类型信息,包括类型标识、长度和空终止符。
在首次发送文件时,需计算该文件的具体尺寸。此后,每次发送文件时,只需提取该文件已知的尺寸数据。
若在文件大小中未能找到文件名,则
        {
fileName对应的fileSize值通过调用MyGetFileSize函数获得。
        }
定义了一个名为size的长整型变量,其值等于fileName对应的fileSize数组中的元素。
        ///向客户端发送文件大小长度
cntLen变量被赋予了一个格式化字符串,其中包含了“Content-length:”前缀和size变量的64位整数部分,随后跟上了回车换行符。
向套接字发送数据,其中数据长度为cntLen,发送的字节数为strlen(cntLen),且不携带任何额外标志。
向套接字发送结束标记,并指定其长度,同时不设置任何附加标志。
        wsaBuf.buf = buf;
        wsaBuf.len = BUF_SIZE;
        ///读取请求文件的数据并传递给客户端
        do
        {
文件读取函数被调用来从输入文件中读取数据,将读取的数据存储到wsaBuf缓冲区中,读取的字节数不超过缓冲区大小减去一个字节。
                int cnt = inFile.gcount();
                wsaBuf.buf[cnt] = 0;
               
                send(sock, buf, cnt, 0);
        } while (!inFile.eof());
        inFile.close();                //关闭文件
        return 0;
}
禁止对特定内容进行篡改,确保信息的准确性,维护版权的合法权益。232425262728293031323334353637383940414243444546474849505152535455565758596061626364</code></pre></p>
    此函数扮演着关键角色,务必留意其内部的响应头部信息,这些信息需严格依照HTTP协议规定的标准格式进行传输。

    其中又牵扯到了其他几个函数。分别是获取文件的大小信息

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'>    <pre class="syl-page-code"><code>定义函数MyGetFileSize,该函数接受一个字符指针类型的文件名参数,用于获取文件大小。
{
在创建文件句柄的同时,我们借此机会获取文件的具体大小数据。然而,对于操作系统而言,每一次创建句柄和执行系统调用都会带来额外的成本,这种成本无疑会对整体性能产生负面影响。
创建文件句柄时,我们调用了CreateFileA函数,指定了文件名为fileName,允许进行读操作,同时允许其他进程读取,没有设置安全属性,以只读模式打开文件,并保持默认文件属性,最后传入了NULL作为参数。
        LARGE_INTEGER size;
通过调用GetFileSizeEx函数,获取文件句柄hFile所指向的文件的实际大小,并将结果存储在size变量中。
        return size;
}
12345678910</code></pre></p>
    获取请求文件的格式

<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'>    <pre class="syl-page-code"><code>获取文件类型函数,名为GetContentType,接受一个字符串类型的文件名作为参数。
{
        ///通过string的反向查找函数首先查找.符号
获取文件名中最后一个点字符的位置索引,变量名为index。
若索引值等于`std::string::npos`或等于文件名长度,则表示请求的文件缺乏后缀名,此时将返回一个空指针。
                return nullptr;
使用fileName和index + 1构造字符串s,若文件名带有后缀,则需对后缀进行检查。
                                                                                        ///通过后缀名来进行判断请求数据的类型
        if (type.empty())
若在相关容器内未发现匹配的类型,则返回“text/plain”。
        ///返回转换为char*的类型
        static        char str[20];
        strcpy_s(str, type.c_str());
        return str;
}
12345678910111213141516171819202122</code></pre></p>
    当然,这还涉及到其他一些结构问题。具体细节我并未一一列举,如有需要,欢迎您通过电子邮件与我联系。

    一旦你构建好程序的基础框架,便可以对其进行测试。服务器需在80端口上启动监听。随后,请打开浏览器,输入相应指令,然后按回车键。

    此刻,你的控制台将展示出已连接客户的详细信息。当然,这得基于你成功开发出这一功能。然而,至少你已在浏览器中成功浏览到了你的网页。

    若你期望他人也能浏览你的网页,不妨试试以下途径:

    购置服务器,并将应用程序以及HTML文档部署于其上执行,其他用户可借助你的服务器公开IP地址来实现访问。

    运用特定工具实现内网与公网的连接,将内网转化为公网状态,并设定相应的端口号。其他用户需借助你提供的公网IP地址和端口来建立连接。你可以在百度搜索“免费frp”以寻找合适的软件,例如frp。

    若您不打算亲自编写服务器端的代码,仅仅希望将自己的网站开放给他人浏览,那么您可以考虑以下软件:

    提供的 IIS云盾… 你可以百度搜索网站托管

    若您希望他人能够通过网址访问您的网站,那么就需要注册一个网址,例如网址就是一个典型的网址。您需要前往购买该网址,具体购买流程您可以通过百度搜索了解。

    完成域名认证和备案流程后,需将域名与您的服务器公网IP地址进行关联。为了提升网站访问速度,您还可以配置CDN缓存服务,这一操作的相关信息您可以在百度上找到。

    一旦你的域名备案手续顺利完成,便需着手进行域名解析。以下是我为你准备的域名解析策略。更详细的操作步骤,你可以通过百度搜索获取。

    完成这些步骤后,恭喜你,你的网站应当能够顺利被访问。接下来,不妨进一步丰富你的网站内容,让更多的人得以一睹风采。
您需要登录后才可以回帖 登录 | 立即注册

Archiver|手机版|小黑屋|关于我们

Copyright © 2001-2025, Tencent Cloud.    Powered by Discuz! X3.5    京ICP备20013102号-30

违法和不良信息举报电话:86-13718795856 举报邮箱:hwtx2020@163.com

GMT+8, 2025-6-1 20:44 , Processed in 0.110046 second(s), 17 queries .