Webserver学习笔记

发布时间 2023-04-26 18:43:03作者: Lyz09

前言

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/

第一个是请求类型,请求类型分为两种:GETPOST。可以简单的理解为 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,导致正文被当作请求头里面了。

暂时只写了这么多,欢迎吐槽。