Webserver学习笔记
前言
Webserver 这个东西真的恶心的一批,很难自学,但是网上又没有现成的教程(谁没事写一个 Webserver 啊)。
这篇文章主要提供 Webserver 的基本框架的思路,毕竟网站基本框架相同无疑于抄袭,SSD 可以先走了。
正文
准备
本篇博客的 Webserver 基于 SOCKET 实现,这样只是为了追求底层,相对于其他方法较为麻烦。(当然你也可以使用其他封装好的库)
这段内容已经了解过 SOCKET 的人可以不看,不了解的不必深究。
SOCKET 是什么?
提到这个就不得不说到网络通信的原理了,众所周知,网络通信的协议有以下几种(从底层到高层排序):
物理层、数据链路层、网络层、传输层、会话层、表现层和应用层。
IP协议对应于网络层,TCP协议对应于传输层,而HTTP协议对应于应用层。(怎么这么像 CSP 初赛啊)
而我们所要讲的主角 SOCKET 则是使用 TCP/IP 协议。SOCKET 可以简单的理解为把 TCP/IP 封装了一下,方便程序使用。
这里有一张 SOCKET 的原理图,其中标红的为我们主要使用的路径:
而我们主要需要管理的就是我们的程序和 SOCKET 之间的协议,剩下的交给 SOCKET 做就行了。
PS:C++ SOCKET 是内核级别的。
如何使用 SOCKET?
重点:
如果你使用的是 Dev-C++ 这一类编辑器的话,请必须注意这里:
在工具-编译选项中,在编译选项中加上:
-std=c++11
在连接选项中加上:
-static-libgcc -lws2_32
然后保存。只有这样,你的程序才能够正常编译,不然一切都没有用,这无论是对于自己的 Webserver,也是对每一个使用 WinSocket(SOCKET) 所需要的事情。
至于这两段代码的意思,可以自己去查。
SOCKET 常用函数使用方法
函数百科(不是):
WSAStartup
函数
函数原型:int WSAStartup(WORD wVersionRequest,LPWSADATA lpWSAData)
WSAStartup
用于初始化WinSock,即检查系统中是否有Windows Sockets的实现库。
必须第一个调用此函数才能使用其他 Win Socket 函数。
WSACleanup
函数
函数原型:int WSACleanup()
WSACleanup
终止使用WinSock,释放为应用程序分配的相关资源。
此函数在 SOCKET 结束时调用。
listen
函数
函数原型:int listen(int sockfd, int backlog)
listen
把进程变为一个服务器,并指定相应的套接字变为被动连接。
调用顺序:listen
函数在一般在调用 bind
之后,调用 accept
之前调用。
accept
函数
函数原型:int accept(int s,struct sockaddr* addr,int* addrlen)
accept
用来接受参数 s 的 socket 连接。
socket
函数
函数原型:SOCKET socket(int af, int type, int protocol)
socket
为应用程序创建套接字。
bind
函数
函数原型:int bind(SOCKET s, const struct sockaddr *name, int namelen)
bind
实现套接字与主机本地 IP 地址和端口号的绑定。
注意此函数可能会与 C++11 中的 bind()
函数冲突(函数名冲突)。
send
函数
函数原型:int send(SOCKET s,const char* buf,int len,int flags)
send
在已建立连接的套接字上发送数据。
recv
函数
函数原型:int recv(SOCKET s, char* buf,int len,int flags)
recv
在已建立连接的套接字上接收数据。
closesocket
函数
函数原型:int closesocket(SOCKET s)
closesocket
关闭套接字,释放与套接字关联的所有资源。
shutdown
函数
函数原型:int shutdown(SOCKET s,int how)
shutdown
关闭套接字读写通道,即停止套接字接受传递的功能。
connect
函数
函数原型:int connect(SOCKET s,const struct sockaddr FAR *name,int namelen)
shutdown
提出与服务器建立连接的请求,如果服务器进程接受请求,则服务器进程与客户机进城之间便建立了一条通信连接。
在本篇文章中 Webserver 无需调用此函数。
实践
以上函数除个别外,返回值为 -1
即 SOCKET_ERROR 为错误。
既然已经知道这些函数是干什么的了,那么现在来实践一下(有部分代码不规范)。
#include<winsock2.h>
#include<sys/stat.h>
#include<iostream>
#include<windows.h>
#include<fstream>
#include<thread>
#define SERVER_PORT 81 //自定义的服务端口
#define HOSTLEN 256 //主机名长度
#define BACKLOG 50000 //同时等待的连接个数
using namespace std;
/***********************************
用于发送数据
***********************************/
int sendall(int s,char *buf,int *len)
{
int total=0;
int bytesleft=*len;
int n;
while(total<*len)
{
n=send(s,buf+total,bytesleft,0);
if(n==-1)
break;
total+=n;
bytesleft-=n;
}
*len=total;
return n==-1?-1:0;
}
/***********************************
解析并处理用户请求
***********************************/
void handle_req(char* request,int client_sock)
{
//你需要做的事情
}
/*************************************
该方法构造服务器端的SOCKET
返回构造好的socket描述符
*************************************/
int make_server_socket()
{
struct sockaddr_in server_addr;
int tempSockId;
tempSockId=socket(PF_INET,SOCK_STREAM,0);
if(tempSockId==-1)
return -1;
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(SERVER_PORT);
server_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
memset(&(server_addr.sin_zero),'\0',8);
if(bind(tempSockId,(struct sockaddr*)&server_addr,sizeof(server_addr))==-1)
{
printf("bind error!\n");
return -1;
}
if(listen(tempSockId,BACKLOG)==-1)
{
printf("listen error!\n");
return -1;
}
return tempSockId;
}
int main(int argc,char* argv[])
{
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
{
fprintf(stderr,"WSAStartup failed.\n");
exit(1);
}
printf("My web server started...\n");
int server_socket;
int acc_socket;
int sock_size=sizeof(struct sockaddr_in);
struct sockaddr_in user_socket;
server_socket=make_server_socket();
if(server_socket==-1)
{
printf("Server exception!\n");
exit(2);
}
//前面的都是初始化 Webserver。
while(true)
{
acc_socket=accept(server_socket,(struct sockaddr*)&user_socket,&sock_size);//在此处接受请求,可以从这里之后就写多线程。
if(acc_socket==-1)//判断无效链接(可以不写)
{
Sleep(1);
continue;
}
int numbytes;
char *buf=new char[100000000];//为以后预留空间。
memset(buf,0,sizeof(buf));
if((numbytes=recv(acc_socket,buf,99999999,0))==-1)//接受第一段请求(通常只有一段)
{
perror("recv");
continue;
}
thread t(handle_req,buf,acc_socket);
t.detach();
Sleep(5);//防止占用过高
}
shutdown(server_socket,SD_SEND);
}
至此,我们已经完成了 SOCKET 的建立,可以通过浏览器链接并发送请求到我们的 Webserver 上,但是看不到任何东西,这是因为我们并没有做出响应请求的部分。
在做响应请求之前,我们必须先了解一下浏览器的请求,下面有使用 Google Chrome 访问 Webserver 根目录的请求(你也可以自己输出请求):
GET /index.html HTTP/1.1
Host: myweb.com
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://myweb.com
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: lastlang=1;
虽然浏览器给了我们这么多信息,但暂时对我们有用的只有最前面的两串:
GET
和 /
第一个是请求类型,请求类型分为两种:GET
和 POST
。可以简单的理解为 GET
为直接访问,POST
为特殊访问。(以后会提到)
第二个是请求地址,/
所代表的是根目录,也就是说浏览器想获取直接访问网站所能获取的信息。此外,像 /index.html
,/blog/284013
这种都是请求地址。
此外还有一些带 ?
&
=
的请求并非只是请求这个文件,这个后面会讲。
也就是说我们进行 简单 处理数据的时候,暂时对我们有用的就只有请求地址。
以上都是浏览器发给我们的请求,是按照格式规范发送过来的,那我们也要按照格式规范发送过来。
那具体格式是怎么样呢,我们看一下一个合格的相应是怎么样的:
响应头:
HTTP/1.0 200 OK\r\n
Content-type: text/html\r\n
\r\n
正文自己随便返回。
响应头解释:
HTTP/1.0
是我们的 Webserver 与浏览器共同的协议,虽然还有 Https 但是那东西需要付费和安全证书(再说了我也不会)。
200 OK
这个东西你也可以换成 200
浏览器都能看懂,200 所代表的是网页正常,是一个状态码,下面会有状态码百科。
Content-type: text/html
顾名思义,就是文件的类型,后面会给出如何返回其他文件的响应头,这段响应所说的文件类型是 text/html
。
在给浏览器的响应头中,上文后面的换行不用发送,但是需要发送 \r\n
,\r\n
本身是不可见的,但是为了展示就把它写了出来。
响应头百科
因为响应头非常重要,所以这里提供一个函数,专门用来发送响应头,但是这个函数并没有发送最后一个 \r\n
(方便后期),所以需要手动发送。
建议不要更改字符串中的内容。
/*************************************
发送Http协议头部信息
其中包括响应类型和Content Type
*************************************/
void send_header(int send_to, char* content_type)
{
char head[]="HTTP/1.0 200 OK\r\n";
int len=strlen(head);
if(sendall(send_to,head,&len)==-1)
{
printf("Sending error");
return;
}
if(content_type)
{
char temp_1[30]="Content-type: ";
strcat(temp_1,content_type);
strcat(temp_1,"\r\n");
len=strlen(temp_1);
if(sendall(send_to,temp_1,&len)==-1)
{
printf("Sending error!");
return;
}
}
}
正文部分就不多说了,你可以自己存在程序内部,也可以读取磁盘上的文件,如果你是读取磁盘文件的话,建议在请求地址前面加上 ./
,并使用下面这份模板,不会多读或者少读。
获取文件大小(有四种方法,可以通过更改 ff
的数值换方法)
/**************************************
获取文件大小
**************************************/
long long getsize(char *filepath)
{
int ff=3;
long long size=0;
if(1==ff)
{
HANDLE handle=CreateFile(filepath,FILE_READ_EA,FILE_SHARE_READ,0,OPEN_EXISTING,0,0);
if(handle!=INVALID_HANDLE_VALUE)
{
size=GetFileSize(handle,NULL);
CloseHandle(handle);
}
}
if(2==ff)
{
WIN32_FIND_DATA fileInfo;
HANDLE hFind;
DWORD fileSize;
hFind=FindFirstFile(filepath,&fileInfo);
if(hFind!=INVALID_HANDLE_VALUE)
fileSize=fileInfo.nFileSizeLow;
size=fileSize;
FindClose(hFind);
}
if(3==ff)
{
FILE *file=fopen(filepath,"r");
if(file)
{
size=filelength(fileno(file));
fclose(file);
}
}
if(4==ff)
{
struct _stat info;
_stat(filepath,&info);
size=info.st_size;
}
return size;
}
读取并发送文件(这里面的 sock 即为接收到的 SOCKET)
char read_buf[1000];
long long filelen=getsize((char*)"文件路径"),sendlen=0;
memset(read_buf,0,sizeof(read_buf));
ifstream inn("文件路径",ios::binary);//这里的ios::binary是使用2进制读取文件,不要更改
while(sendlen<filelen)
{
long long nowbufsize=min((long long)1000,filelen-sendlen);
char* buf=new char[nowbufsize];
memset(buf,0,sizeof(buf));
inn.read(buf,nowbufsize);
long long nowsendlen=send(sock,buf,nowbufsize,0);
delete[] buf;
sendlen+=nowsendlen;
if(nowsendlen==SOCKET_ERROR)
break;
}
inn.close();
最好在处理请求结束以后加上 closesocket
。
等你做好处理请求之后,可以尝试通过浏览器访问你的网站,如果不行,可以参考下面的注意事项;
注意事项
1.访问格式,如果是 Webserver 在本机则访问 127.0.0.1:Webserver的端口
,如果在同一个局域网下,先通过 ipconfig
查看 Webserver 所在的电脑上的 IP,然后输入 IP:Webserver的端口
,冒号和逗号不要使用中文符号。
2.如果出现 xxx.xxx.xxx.xxx
已拒绝了我们的连接请求,请检查:
-
1.是否打开了局域网。
-
2.检查 Webserver 的端口有没有被占用,初始化有没有成功,是否炸掉。
3.能连接上 Webserver,但是只返回了状态码或者什么都没有返回或者连接已重置,可能是:
-
1.
closesocket
太快了,最好等待几毫秒。 -
2.处理请求时 Webserver 出了点小问题,导致停止工作。
-
3.因为请求头少加了个
\r\n
,导致正文被当作请求头里面了。
暂时只写了这么多,欢迎吐槽。