手搓 HTTP服务器 手把手带你实现高并发HTTP服务器 C++ HTTP服务器 服务器项目实战 高性能服务器实战 服务器项目 服务器

手搓 HTTP服务器 高并发HTTP服务器 C++ HTTP服务器

1、什么是socket

Socket是一种用于网络通信的编程接口,允许不同计算机之间通过网络发送和接收数据。它在客户端和服务器之间创建连接,支持多种协议,如TCP和UDP。

2、实现socket 通讯

1、socket通讯步骤

  • 1、创建socket
  • 2、绑定socket
  • 3、监听socket
  • 4、accpet 接受客户端连接请求
  • 5、收发数据

2、具体实现

1、前骤

WSAStartup

socket编程要调用各种socket函数,但是需要库Ws2_32.lib和头文件Winsock2.h,这里的WSAStartup就是为了向操作系统说明,我们要用哪个库文件,让该库文件与当前的应用程序绑定,从而就可以调用该版本的socket的各种函数了。
#include <winsock.h>
#include <stdio.h>
#include <thread>

//socket 需要的库函数
#pragma comment(lib,"ws2_32.lib");

int main(){
	//初始化系统库指向,告知操作系统要使用socket库,版本号位 2
	WSADATA ws;
	WSAStartup(MAKEWORD(2, 2), &ws);
	
	//todo  后续操作	

}



2、创建socket
  • 参数解释
int socket(int af,int type,int protocol);
af:  地址族   一般使用AF_INET
type:  连接类型,我们是TCP  使用SOCK_STREAM
	TCP:  SOCK_STREAM
	UDP:   SOCK_DGRAM
protocol:  默认 0

返回值:   socket的句柄,如果该值大于0说明创建成功,小于等于0,创建失败!
  • 代码实现
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock <= 0){
	printf("create socket fail\r\n");
	return 0;
}
3、bind 绑定socket
  • 参数解释
 int bind(int socket,const struct sockaddr *address,socklent address_len);
 
 1、socket  套接字
 2、address  地址结构体
 	struct sockaddr_in {
	     short            sin_family;    // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
	     unsigned short   sin_port;      // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
	     struct in_addr   sin_addr;      // 4 字节 ,32位IP地址
	     char             sin_zero[8];   // 8 字节 ,不使用
	};
3、address_len   address 大小

4、返回值:  0  绑定成功   其他值,绑定失败

  • 代码实现

sockaddr_in saddr;
saddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  //此处需要的值填写方式分为两种 1、采用inet_addr("绑定的ip地址")  2、绑定本地端口任一ip地址,直接填写  INADDR_ANY
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);	//此处htons  是将本地字节序转为网络字节序,因此我们设置端口号需要使用htons(8888)来转一次

int b = bind(sock,(sockaddr*)&saddr,sizeof(saddr));
if(b != 0){
	printf("bind fail\r\n");
	return 0;
}

4、监听 Listen
  • 参数解释
int listen(int sockfd, int backlog)

1、sockfd:是调用socket()函数创建的socket描述符(套接字号)

2、backlog:指定内核为此套接字维护的最大连接个数,该队列不能太长也不能没有,因为当队列太长时,需要耗费一定的资源进行维护和管理,没有的话也会降低操作系统的效率

3、返回值: 成功时返回0,错误时返回-1
  • 代码实现
int l = listen(sock,100);
if(l != 0){
	printf("listen fail\r\n");
	return 0;
}
5、accept 接收客户端连接
  • 参数解释
int client = accept (SOCKET s,sockaddr *addr,int *addrlen);
1、 s    服务器创建的socket
2、 addr   客户端连接后,客户端的地址相关数据会存在addr 里面
3、 addrlen    addr 的长度,这里取的是地址
4、返回值     客户端套接字socket  当然,socket 小于或等于0  ,这个套接字无效

注意  
1、accept 默认是阻塞的,每次接收到客户端的连接就会向下运行一次
2、如果想取消阻塞 改为手动控制接受,可以采用 ioctlsocket函数取消阻塞

unsigned long ul = 1;
int res = ioctlsocket(sock, FIONBIO, &ul);
if(res != 0){
	printf("ioctlsocket set fail\r\n");
	return 0;
}
  • 代码实现

//这里我们使用了一个无限循环一直等待客户端连接

while(true)
{
	sockaddr_in caddr;
	int caddrLen = sizeof(caddr);
	int client = accept(sock,(sockaddr*)&caddr,&caddrLen);
	if(client <= 0){
		continue;
	}
	//todo 收发消息, 
	//1、这里开启了一个线程用于专门处理收发消息
	//2、th.detect  表示该线程独立
	//3、[=]  表示lambda表达式内部使用外部参数以值类型传递,
	//4、如果采用引用类型传递可以改为[&] 
	//5、如果是[] 表示不引用外部参数,
	//6、当然也可以引用具体参数[client] 
	std::thread th([=](){
		Run(client); //收发消息函数
	});
	th.detach();
}

6、收发消息
  • 参数解释
int send (SOCKET s,char * buf,int len,int flags);

1、s : 发送对象套接字socket
2、buff : 发送的数据
3、len : 发送的数据长度
4、flags:  默认0
5、返回值:   小于或等于0 发送失败,否则,发送成功
int recv (SOCKET s,char * buf,int len,int flags);
1、s : 发送对象套接字socket
2、buff : 接收的数据存储位置指针
3、len : 存储位置长度
4、flags:  默认0
5、返回值:   接收的数据长度,如果小于或等于0 接收失败
  • 代码实现

void Run(int client)
{
	char buff[1024] = { 0 };
		while (true)
		{
			//这里recv 默认是阻塞的,接收到消息会向下执行一次,因此采用while循环
			//解除阻塞任然采用ioctlsocket 
			int len = recv(client, buff, sizeof(buff), 0);
			if (len <= 0)
			{
				printf("接收失败,客户端已断开!");
				break;
			}
			//这里直接将收到的数据发送出去
			len = send(client, buff, len, 0);
			if (len <= 0) {
				printf("发送失败,客户端已断开!");
				break;
			}
		}
}

3、socket 服务器实现完整代码

#include <winsock.h>
#include <stdio.h>
#include <stdlib.h>
#include <thread>

#pragma comment(lib,"ws2_32.lib");


void Run(int client)
{
	char buff[1024] = { 0 };
	while (true)
	{
		//这里recv 默认是阻塞的,接收到消息会向下执行一次,因此采用while循环
		//解除阻塞任然采用ioctlsocket 
		int len = recv(client, buff, sizeof(buff), 0);
		if (len <= 0)
		{
			printf("接收失败,客户端已断开!\r\n");
			break;
		}
		//这里直接将收到的数据发送出去
		len = send(client, buff, len, 0);
		if (len <= 0) {
			printf("发送失败,客户端已断开!\r\n");
			break;
		}
	}
}

int main()
{
	WSADATA ws;
	WSAStartup(MAKEWORD(2, 2), &ws);

	int sock = socket(AF_INET, SOCK_STREAM, 0);
	if (sock <= 0) {
		printf("create socket fail\r\n");
		return 0;
	}
	sockaddr_in saddr;
	saddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  //此处需要的值填写方式分为两种 1、采用inet_addr("绑定的ip地址")  2、绑定本地端口任一ip地址,直接填写  INADDR_ANY
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(8888);	//此处htons  是将本地字节序转为网络字节序,因此我们设置端口号需要使用htons(8888)来转一次

	int b = bind(sock, (sockaddr*)&saddr, sizeof(saddr));
	if (b != 0) {
		printf("bind fail\r\n");
		return 0;
	}
	int l = listen(sock, 100);
	if (l != 0) {
		printf("listen fail\r\n");
		return 0;
	}
	while (true)
	{
		sockaddr_in caddr;
		int caddrLen = sizeof(caddr);
		int client = accept(sock, (sockaddr*)&caddr, &caddrLen);
		if (client <= 0) {
			continue;
		}
		//todo 收发消息, 
		//1、这里开启了一个线程用于专门处理收发消息
		//2、th.detect  表示该线程独立
		//3、[=]  表示lambda表达式内部使用外部参数以值类型传递,
		//4、如果采用引用类型传递可以改为[&] 
		//5、如果是[] 表示不引用外部参数,
		//6、当然也可以引用具体参数[client] 
		std::thread th([=]() {
			//此处使用 inet_ntoa 将 sin_addr 转为ip
			char* ip = inet_ntoa(caddr.sin_addr);
			//此处使用 ntohs 将sin_port 转为端口号
			int port = ntohs(caddr.sin_port);
			printf("客户端%s - %d已连接!\r\n", ip, port);
			Run(client); //收发消息函数
			});
		th.detach();
	}
	//关闭套接字
	closesocket(sock);
	//清理
	WSACleanup();
	return 0;
}

3、正则表达式

  • 正则表达式在线测试地址
https://www.jyshare.com/front-end/854/

1、语法参考

在这里插入图片描述

2、实现一个简单的匹配

  • 解释
匹配字符串   GET /test HTTP/1.1

正则表达式   (^[A-Za-z]+) ([/A-Za-z]+) HTTP/1.1

1^ 表示必须以什么开始,我这里是以字母开始,
2[A-Za-z] 表示 从大写字符A-Z和小写字符a-z中任意一个匹配上就算成功, +号表示至少匹配到一个
3() 括号表示分组

int main()
{
	std::string input = "GET /test HTTP/1.1";

	std::regex reg("(^[A-Za-z]+) ([/A-Za-z]+) HTTP/1.1");
	std::smatch sma;
	auto res = std::regex_search(input, sma, reg);
	if (!res)
	{
		printf("match fail\r\n");
		return 0;
	}
	//括号分组后  默认的第一组是原字符串
	//第二组就是第一个括号
	//第三组就是第二个括号,依次类推
	auto sub_match1 = sma[0];	//1、GET /test HTTP/1.1
	auto sub_match2 = sma[1];	//2、GET
	auto sub_match3 = sma[2];	//3、/test

	return 0;
}

4、字符串操作

字符串操作方法可能会出现不安全问题的报错,请在main 方法的c/cpp文件第一行定义,如果定义过就不需要重复定义了

#define  _CRT_SECURE_NO_WARNINGS

在这里插入图片描述

1、strstr

  • 从左向右查找指定字符串并截取到字符串末尾
std::string re = strstr("11&22&33&44", "&");

//re  =  &22&33&44

2、std::String 中的 substr

  • 通过给定的索引截取字符串
std::string inputStr = "123456789";
std::string re = inputStr.substr(0, 5);

//re =  12345

3、strchr

  • 从左向右查找指定字符并截取到字符串末尾
std::string re = strstr("11&22&33&44", '&');

//re  =  &22&33&44

3、strrchr

  • 从右向左查找指定字符并截取到字符串末尾
std::string re = strstr("11&22&33&44", '&');

//re  =  &44

5、strcmp

  • 使用ascii的方式比较字符串是否相等
  • 相等返回0
	int re = strcmp("aaa", "aaa");

6、std::String 中的 replace

  • 指定替换位置,指定替换长度,替换指定的字符串
- string replace (size_t pos, size_t len, const string& str);
- pos  替换开始位置
- len   被替换字符串长度
- str    新的字符串
std::string inputStr = "abcd1234";
std::string re = inputStr.replace(0,4,"");

// re = 1234

5、获取字符串长度

  • std::string 直接通过size() 属性获取
  • char * 通过 strlen 返回值就是字符串长度

5、文件操作

文件操作方法可能会出现不安全问题的报错,请在main 方法的c/cpp文件第一行定义,如果定义过就不需要重复定义了

#define  _CRT_SECURE_NO_WARNINGS

在这里插入图片描述

1、c 语言操作文件 FILE

1、fopen 打开文件
FILE* fopen(char const* _FileName,char const* _Mode)

参数	1、文件路径   2、打开方式
打开方式: 
	"r"	打开一个用于读取的文件。该文件必须存在。
	"w"	创建一个用于写入的空文件。如果文件名称与已存在的文件相同,则会删除已有文件的内容,文件被视为一个新的空文件。
	"a"	追加到一个文件。写操作向文件末尾追加数据。如果文件不存在,则创建文件。
	"r+"	打开一个用于更新的文件,可读取也可写入。该文件必须存在。
	"w+"	创建一个用于读写的空文件。
	"a+"	打开一个用于读取和追加的文件。
2、fseek() 移动文件指针位置
int  fseek(FILE * _Stream,long  _Offset,int   _Origin);

参数	
	1、文件指针   
	2、偏移距离,这个偏移是在参数3移动位置的枚举类型确定了的基础上再偏移固定距离,如: 3选择为文件开始,那么偏移距离就是这个固定距离,如果选择为文件末尾,那么就是文件大小+固定距离   
	3、移动位置枚举
		SEEK_CUR    //当前光标
		SEEK_END    //文末
		SEEK_SET    //文首

3、ftell() 获取文件大小
int ftell(FILE * _Stream);

参数:
	1、文件指针
返回值:
	文件大小

4、fread() 读取文件
	size_t fread(void* _Buffer,size_t _ElementSize,size_t _ElementCount,FILE * _Stream);
	
	参数:
		1、读取的流存放位置指针
		2、存放位置的参数元素大小
		3、存放位置的参数长度
		4、文件指针
		5、返回值  读取数据的长度

	//定义一个char 类型的数组存放
	char buff[1024] = { 0 };
	//读取文件内容存放到char 数组中,当然一次可能读取不完,需要多次读取和转移
	int len = fread(buff, sizeof(char), sizeof(buff), fp);
5、fwrite() 写入文件
	size_t fwrite(void* _Buffer,size_t _ElementSize,size_t _ElementCount,FILE * _Stream);
	
	参数:
		1、写入的流存放位置指针
		2、存放位置的参数元素大小
		3、存放位置的参数长度
		4、文件指针
		5、返回值  写入数据的长度

	//定义一个char 类型的数组存放
	char buff[1024] = { 0 };
	//读取文件内容存放到char 数组中,当然一次可能读取不完,需要多次读取和转移
	int len = fwrite(buff, sizeof(char), sizeof(buff), fp);
  • 6、fclose() 关闭文件
fclose(fp);
7、案例
//安全报警定义
#define  _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

int main()
{

	FILE* fp = fopen(R"(C:\Users\70970\Desktop\111.txt)", "rb");
	if (fp == nullptr)
	{
		printf("打开读取文件失败!");
		return 0;
	}
	//将光标移动到文末
	fseek(fp, 0, SEEK_END);
	//获取文件大小
	int fileSize = ftell(fp);
	printf("文件大小%d!",fileSize);
	//将光标移动到文件头,便于读取操作,如果是从文末写入,就直接在文末就可以了,输出默认移动到文末
	fseek(fp, 0, SEEK_SET);
	char buff[1024] = { 0 };
	//定义输出
	FILE* fw = fopen(R"(C:\Users\70970\Desktop\222.txt)", "wb");
	if(fw == nullptr)
	{
		printf("打开写入文件失败!");
		return 0;
	}
	//循环读取和写出
	while (true)
	{
		int len = fread(buff, sizeof(char), sizeof(buff), fp);
		if (len <= 0) {
			break;
		}
		len = fwrite(buff, sizeof(char), len, fw);
		if(len <= 0)
		{
			printf("写入失败!");
			break;
		}
	}
	//关闭
	fclose(fw);
	fclose(fp);
	return 0;
}

2、c++ fstream类操作文件

1、open 打开文件
void open(const char* _Filename, ios_base::openmode _Mode)

参数:
	1、文件名
	2、打开模式
		std::ios::in	输入
		std::ios::out	输出(覆盖源文件)
		std::ios::app	所有数据追加在末尾(在源文件末尾追加内容)
		std::ios::ate	打开一个输出文件并移动到文件末尾。数据可以写入文件的任何位置
		std::ios::trunc	如果文件已存在,则丢弃文件内容(ios::out的缺省方式)
		std::ios::binary	以二进制格式输入输出(用于图片,视频,可执行文件等)
2、seekg 移动文件读取时的位置

basic_istream&  seekg(off_type _Off, ios_base::seekdir _Way) 

参数:
	1、偏移距离
	2、移动方式枚举
		ios::beg 从文件头开始计算偏移量
		ios::end 从文件末尾开始计算偏移量
		ios::cur 从当前位置开始计算偏移量


file.seekp(32L, ios::beg); 将写入位置设置为从文件开头开始的第 33 个字节(字节 32)
file.seekp(-10L, ios::end); 将写入位置设置为从文件末尾开始的第 11 个字节(字节 10)
file.seekp(120L, ios::cur); 将写入位置设置为从当前位置开始的第 121 个字节(字节 120)

3、seekp 移动文件写入时的位置

basic_istream&  seekp(off_type _Off, ios_base::seekdir _Way) 

参数:
	1、偏移距离
	2、移动方式枚举
			ios::beg 从文件头开始计算偏移量
			ios::end 从文件末尾开始计算偏移量
			ios::cur 从当前位置开始计算偏移量


file.seekg(2L, ios::beg); 将读取位置设置为从文件开头开始的第 3 个字节(字节 2)
file.seekg(-100L, ios::end); 将读取位置设置为从文件末尾开始的第 101 个字节(字节 100)
file.seekg(40L, ios::cur); 将读取位置设置为从当前位置开始的第 41 个字节(字节 40)
file.seekg(0L, ios:rend); 将读取位置设置为文件末尾


4、tellg 获取文件读取时偏移位置
int fileSize = fs.tellg();

//读取时偏移位置

5、tellp 获取文件写入时偏移位置
int fileSize = fs.tellp();

//写入时偏移位置

6、eof 文末标识

光标是否在文末

bool iseof = fr.eof();   
7、gcount 实际读取长度

先执行读取后,再调用gcount 获取读取长度


fr.read();
int readLen = fr.gcount();

7、std::getline 逐行读取文件
  • 1、std::getline 是标准库的方法

按照默认的换行为一行进行读取


basic_istream<_Elem, _Traits>& getline(basic_istream<_Elem, _Traits>& _Istr, basic_string<_Elem, _Traits, _Alloc>& _Str);

参数:
	1、文件指针
	2、输出读取内容


#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::fstream file("example.txt", std::ios::in);
    if (!file.is_open()) {
        std::cerr << "Error opening file." << std::endl;
        return 1;
    }

    // 逐行读取文件
    std::string line;
    while (std::getline(file, line)) {
        std::cout << line << std::endl;
    }

    // 关闭文件
    file.close();
    return 0;
}

8、read 读取文件
int main()
{
	std::fstream fr;
	fr.open(R"(C:\Users\Desktop\111.txt)", std::ios::in);
	fr.seekg(0, std::ios::end);
	int fileSize = fr.tellg();
	fr.seekg(0, std::ios::beg);

	char* buff = new char[fileSize];

	fr.read(buff, fileSize);

	printf("%s", buff);

	fr.close();
	delete []buff;

	return 0;
}


10、write 写入文件
 basic_istream&  write(_Elem* _Str, streamsize _Count);

参数:
	1、写入数据存放位置
	2、存放位置长度
返回值:  实际写入内容长度


char buff[1024] = { 0 };
int len = fs.write(buff, sizeof(buff));
11、close 关闭
fs.close();
12、案例
#define  _CRT_SECURE_NO_WARNINGS

#include <fstream>
#include <stdio.h>
#include <string>

int main()
{

	std::fstream fr,fw;
	fr.open(R"(C:\Users\Desktop\111.txt)", std::ios::in);
	fr.seekg(0, std::ios::end);
	int fileSize = fr.tellg();
	printf("文件大小%d\r\n", fileSize);
	fr.seekg(0, std::ios::beg);

	fw.open(R"(C:\Users\Desktop\222.txt)", std::ios::out);
	char buff[1024] = { 0 };

	while (!fr.eof())
	{
		fr.read(buff, sizeof(buff));
		//实际读取长度
		int readLen = fr.gcount();
		//如果不用文末标识,可判断readLen 是否小于等于0,如果是,直接退出循环
		if(readLen <= 0) break;
		fw.write(buff, readLen);
	}
	return 0;
}

6、实现HTTP通讯

HTTP 通讯就是根据浏览器请求类型返回对应的数据,通讯完毕关闭连接。

1、解析浏览器请求的URL

后台接收到浏览器请求数据样式,我们用正则表达式截取第一行URL即可

GET /Index.html HTTP/1.1
Host: 127.0.0.1:8888
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 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.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Cookie: AdminCode=vA9U
1、分析浏览器请求和后台接收数据(截取第一行)

浏览器请求地址

  • 127.0.0.1:8888
  • 127.0.0.1:8888/Index.html
  • 127.0.0.1:8888/loginSpecial/css/default.css
  • 127.0.0.1:8888/loginSpecial/js/controlLogin.js
  • 127.0.0.1:8888/loginSpecial/layui/css/modules/layer/default/icon.png
  • 127.0.0.1:8888/loginSpecial/images/favicon.ico
  • 127.0.0.1:8888/loginSpecial/js/controlLogin.js?Index=1

服务器收到

  • GET / HTTP/1.1
  • GET /Index.html HTTP/1.1
  • GET /loginSpecial/css/default.css HTTP/1.1
  • GET /loginSpecial/js/controlLogin.js HTTP/1.1
  • GET /loginSpecial/layui/css/modules/layer/default/icon.png HTTP/1.1
  • GET /loginSpecial/images/favicon.ico HTTP/1.1
  • GET //loginSpecial/js/controlLogin.js?Index=1 HTTP/1.1
2、通过正则表达式解析截取第一行

这里接收到的数据直接进行正则表达式处理

	//源数据,这里只拿了第一行测试
	char buff[10240] = "GET /Index.html HTTP/1.1";
	printf("%s\r\n", buff);
	if(len <= 0) return;
	//todo 解析接收字符串   GET / HTTP/1.1
	std::regex reg(R"((^[A-Za-z]+) ([/A-Za-z0-9-.=_?]+) HTTP/1.)");
	std::smatch sma;
	std::string src  = buff;
	bool result = std::regex_search(src, sma, reg);
	if (!result) {
		printf("no match\r\n%s", buff);
		return;
	}
	
	std::string method = sma[1];  //GET /POST
	std::string url = sma[2];  ///Index.html 


3、处理url
  • 1、将问号后面的数据扔掉,问号后面的数据一般为参数(如查询参数),我们这里暂时扔掉
size_t index = url.find("?");
if(index != -1)
{
	std::string subStr = strstr(url.c_str(), "?");
	url.replace(url.size() - subStr.size(), subStr.size(), "");
}
  • 2、判断url是否是/,如果是/我们默认下发Index.html文件
if(strcmp(url.c_str(),"/") == 0)
{
	url = "/Index.html";
}
  • 3、判断文件类型,组装返回文件的Content-Type
std::string type = strrchr(url.c_str(), '.');
if(strcmp(type.c_str(),".js") == 0)
{
  //reMsg += "application/javascript";
	reMsg += "application/javascript;charset=utf-8";
}else if (strcmp(type.c_str(), ".html") == 0)
{
	reMsg += "text/html";
}else if (strcmp(type.c_str(), ".png") == 0)
{
	reMsg += "image/png";
}else if (strcmp(type.c_str(), ".ico") == 0)
{
	reMsg += "image/ico";
}
else if (strcmp(type.c_str(), ".css") == 0)
{
	reMsg += "text/css";
}
else
{
	reMsg += "text/html";
}
  • 4、根据文件类型获取文件大小

  • 5、组装返回头

返回头格式:

1、HTTP/1.1 200 OK\r\n				//开头  以\r\n分割
2、Content-Type: image/png\r\n		//设置返回类型 以\r\n分割
3、Content-Length: 10\r\n			//设置内容长度 以\r\n分割

文本头结束最后是两个\r\n

示例:


HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: 10\r\n\r\n

开始组装头并发送


	std::string reMsg = "HTTP/1.1 200 OK\r\n";
	reMsg += "Content-Type: ";
	//todo type;
	std::string type = strrchr(url.c_str(), '.');
	if(strcmp(type.c_str(),".js") == 0)
	{
	  //reMsg += "application/javascript";
		reMsg += "application/javascript;charset=utf-8";
	}else if (strcmp(type.c_str(), ".html") == 0)
	{
		reMsg += "text/html";
	}else if (strcmp(type.c_str(), ".png") == 0)
	{
		reMsg += "image/png";
	}else if (strcmp(type.c_str(), ".ico") == 0)
	{
		reMsg += "image/ico";
	}
	else if (strcmp(type.c_str(), ".css") == 0)
	{
		reMsg += "text/css";
	}
	else
	{
		reMsg += "text/html";
	}
	reMsg += "\r\n";
	reMsg += "Content-Length: ";
	//todo size
	while (true)
	{
		index = url.find("/");
		if(index == -1)
		{
			break;
		}
		url.replace(index, 1, "\\");
	}

	//_pgmptr  是获取当前执行程序的路径
	std::string basePath = _pgmptr;
	//获取当前执行程序的路径
	if(basePath.find("\\") == -1) return; 
	std::string subStr = strrchr(basePath.c_str(), '\\');
	basePath.replace(basePath.size() - subStr.size(), subStr.size(), "");
	//此处我将资源文件放入wwww文件夹下,所以有个www的路径
	std::string fileName = basePath + "\\" + "www" + url;
	FILE* fp = nullptr;
	fopen_s(&fp, fileName.c_str(), "rb");
	if (fp == nullptr) {
		return;
	}
	fseek(fp, 0, SEEK_END);
	int fileSize = ftell(fp);
	fseek(fp, 0, SEEK_SET);

	reMsg += fileSize;
	reMsg += "\r\n\r\n";
	//发送头
	send(client, reMsg.c_str(), reMsg.size(), 0);



发送内容

char sendBuff[1024] = { 0 };
while (true)
{
	int rLen = fread(sendBuff, sizeof(char), sizeof(sendBuff), fp);
	if(rLen <= 0) break;
	rLen = send(client, sendBuff, rLen, 0);
	if(rLen <= 0) break;
}
fclose(fp);

7、完整案例源码

#define _CRT_SECURE_NO_WARNINGS
#include <regex>
#include <winsock.h>
#include <stdlib.h>
#include <stdio.h>
#include <thread>

void HandleUrl(int client,std::string url)
{
	if(url.empty()) return;
	size_t index = url.find("?");
	if(index != -1)
	{
		std::string subStr = strstr(url.c_str(), "?");
		url.replace(url.size() - subStr.size(), subStr.size(), "");
	}
	printf("%s\r\n", url.c_str());
	if(strcmp(url.c_str(),"/") == 0)
	{
		url = "/Index.html";
	}

	std::string reMsg = "HTTP/1.1 200 OK\r\n";
	reMsg += "Content-Type: ";
	//todo type;
	std::string type = strrchr(url.c_str(), '.');
	if(strcmp(type.c_str(),".js") == 0)
	{
	  //reMsg += "application/javascript";
		reMsg += "application/javascript;charset=utf-8";
	}else if (strcmp(type.c_str(), ".html") == 0)
	{
		reMsg += "text/html";
	}else if (strcmp(type.c_str(), ".png") == 0)
	{
		reMsg += "image/png";
	}else if (strcmp(type.c_str(), ".ico") == 0)
	{
		reMsg += "image/ico";
	}
	else if (strcmp(type.c_str(), ".css") == 0)
	{
		reMsg += "text/css";
	}
	else
	{
		reMsg += "text/html";
	}
	reMsg += "\r\n";
	reMsg += "Content-Length: ";
	//todo size
	while (true)
	{
		index = url.find("/");
		if(index == -1)
		{
			break;
		}
		url.replace(index, 1, "\\");
	}

	std::string basePath = _pgmptr;
	if(basePath.find("\\") == -1) return; 
	std::string subStr = strrchr(basePath.c_str(), '\\');
	basePath.replace(basePath.size() - subStr.size(), subStr.size(), "");

	std::string fileName = basePath + "\\" + "www" + url;
	FILE* fp = nullptr;
	fopen_s(&fp, fileName.c_str(), "rb");
	if (fp == nullptr) {
		return;
	}
	fseek(fp, 0, SEEK_END);
	int fileSize = ftell(fp);
	fseek(fp, 0, SEEK_SET);

	reMsg += fileSize;
	reMsg += "\r\n\r\n";

	send(client, reMsg.c_str(), reMsg.size(), 0);

	char sendBuff[1024] = { 0 };
	while (true)
	{
		int rLen = fread(sendBuff, sizeof(char), sizeof(sendBuff), fp);
		if(rLen <= 0) break;
		rLen = send(client, sendBuff, rLen, 0);
		if(rLen <= 0) break;
	}
	fclose(fp);
}

void Run(sockaddr_in caddr,int client)
{
	char buff[10240] = { 0 };
	int len = recv(client, buff, sizeof(buff), 0);
	printf("%s\r\n", buff);
	if(len <= 0) return;
	//todo 解析接收字符串   GET / HTTP/1.1
	std::regex reg(R"((^[A-Za-z]+) ([/A-Za-z0-9-.=_?]+) HTTP/1.)");
	std::smatch sma;
	std::string src  = buff;
	bool result = std::regex_search(src, sma, reg);
	if (!result) {
		printf("no match\r\n%s", buff);
		return;
	}
	HandleUrl(client, sma[2]);
}

int main()
{
	WSADATA ws;
	WSAStartup(MAKEWORD(2, 2), &ws);

	int sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock <= 0)
	{
		printf("create socket fail\r\n");
		return 0;
	}
	sockaddr_in saddr;
	//saddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	saddr.sin_addr.S_un.S_addr = INADDR_ANY;
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(8888);
	int b = bind(sock, (sockaddr*)&saddr, sizeof(saddr));
	if(b != 0)
	{
		printf("bind fail\r\n");
		return 0;
	}
	int l = listen(sock, 1000);
	if(l != 0)
	{
		printf("listen fail\r\n");
		return 0;
	}
	while (true)
	{
		sockaddr_in caddr;
		int caddrLen = sizeof(caddr);
		int client = accept(sock, (sockaddr*)&caddr, &caddrLen);
		if(client <= 0) break;
		std::thread th([=]()
			{
				Run(caddr, client);
				closesocket(client);
			});
		th.detach();
	}
	closesocket(sock);
	WSACleanup();
	return 0;
}



8、高并发测试

采用AB进行高并发测试

.\ab.exe -n 1000 -c 100 "http://127.0.0.1:8888/"

9、资料路径

链接: https://pan.baidu.com/s/1vR_wB-Jrwn1lCFRmIRbCHw?pwd=56ts 提取码: 56ts

提示: www 文件放到执行程序下面

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笑非不退

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值