C/C++ WebServer服务器一


基于TCP的套接字通信

在这里插入图片描述       这是一个单线程流程,服务器创建用于监听的套接字,绑定本地的ip和端口,listen函数去监听绑定的端口。
      如果有客户端进行连接,服务器端就可以和发起连接的客户端建立连接,连接建立成功会生成一个用于通信的套接字。用于监听的套接字和用于通信的套接字是不一样的。监听的套接字用于建立连接,通信的套接字用于数据交互。用于数据交互的read和write都是阻塞函数,在单线程下面,一个服务器想和多客户端进行通信,肯定是做不到的,因为accept,read,write都是阻塞的。
      为了使服务器可以正常的与多个客户端建立连接,并进行数据交互,需要用到多线程,多线程中的主线程负责建立连接(调用accept),子线程负责数据通信。
      多线程切换有一定的开销,因此引入非阻塞 I/O。非阻塞 I/O 不会将进程挂起,调用时会立即返回成功或错误,因此可以在一个线程里轮询多个文件描述符是否就绪。但是这种做法缺点是,每次发起系统调用,只能检查一个文件描述符是否就绪。当文件描述符很多时,系统调用的成本很高
      IO多路复用,也就是select,poll,epoll,可以通过一次系统调用,检查多个文件描述符的状态,相比于非阻塞 I/O,在文件描述符较多的场景下,避免了频繁的用户态和内核态的切换,减少了系统调用的开销。在IO多路复用中,阻塞是由内核实现的,自己编写的代码可以少许多不必要的阻塞。
      在单线程下,只用IO多路复用,没办法同时处理两件事情,为了提高效率,一般采用多线程+IO多路复用的方法。

单线程服务器流程

      自己编写的代码充当服务器,浏览器作为客户端的角色进行固定地址的访问。

1.	在终端输入 启动程序 端口 和 程序主目录。
2.	启动监听套接字initListenFd(unsigned short port):
	a)	创建监听fd,采用IPv4,TCP协议
	b)	设置端口复用:如果程序服务器是主动断开连接的一方,会有一个2msl的等待时长,为了确认客户端已经收到我断开确认ack
	c)	绑定IP和端口
	d)	设置监听
	e)	返回监听套接字lfd
3.	启动服务器程序epollRun(int lfd):
	a)	创建epoll 树的根节点
	b)	lfd上树:上树用的是epoll_ctl函数
	c)	while true不停地检测是否有事件到来,根据epoll_wait返回的数组中的fd文件描述符,判断事件是连接请求,还是数据通信请求:
		i.	如果是连接请求,调用acceptClient(lfd, epfd),建立新的连接。
		ii.	如果是数据通信请求,调用recvHttpRequest(cfd, epfd),以http协议的方式传递消息。

acceptClient(lfd, epfd)1.	建立连接,调用accept函数。
2.	设置非阻塞模式,非阻塞说的是文件描述符,默认得到的cfd是阻塞的,用fcntl修改文件描述符的属性。
3.	cfd添加到epoll中,设置边沿触发方式。


recvHttpRequest(cfd, epfd)1.	读取客户端发送过来的http请求头:
2.	判断数据是否被接收完毕:
	a)	if (len == -1 && errno == EAGAIN):证明有数据
	解析请求行:parseRequestLine(const char* line, int cfd):
		i.	sscanf拆分字符串,得到请求方法和请求路径,仅处理get请求:
		ii.	对请求路径中的中文进行处理,否则会乱码
		iii. 判断文件路径是指向目录,还是文件,或者文件不存在:
			1.	若文件路径不存在,发送404.html
			2.	若文件路径是目录,则发送html的头部,然后发送格式化的目录列表,也是符合html格式。
			3.	若文件路径指向具体文件,则分析文件类型,然后发送具体文件。
			由于通信的文件描述符是非阻塞的,用sendfile发送文件的时候要处理返回值,
			不断根据偏移量去发送文件,直到文件发送完毕,否则大文件的传输会出现问题。
			cfd去读取发送数据内存的时候,是非阻塞的,读数据块速度很快,读到文件末尾偏移量之后,再进行while循环读取的时候
			ret返回值为-1,errno == EAGAIN代表没有数据,可以再次进行尝试。
			
			off_t offset = 0;
			int size = lseek(fd, 0, SEEK_END);
			lseek(fd, 0, SEEK_SET);
			while (offset < size) // 如果偏移量小于size,则表示文件没有发送完,继续发送
			{
				// 通信的文件描述符是非阻塞的
				int ret = sendfile(cfd, fd, &offset, size - offset);
				printf("ret value: %d\n", ret);
				if (ret == -1 && errno == EAGAIN) // EAGAIN的意思是没有数据,可以再次进行尝试
				{
					printf("没数据...\n"); 
				}
			}
			close(fd);
	b)	否则说明客户端断开了连接,要及时的将cfd下树,并且关闭对应文件描述符

多线程服务流程

单线程的服务器模型中,主程序会不断阻塞的进行连接和数据通信两项工作,两项工作和主线程不独立,当连接请求比较多的时候,效率相对较低。
多线程的处理方法:在建立连接 acceptClient(lfd, epfd) 和 数据通信模块 recvHttpRequest(cfd, epfd)两部分,都开辟新的线程去做,让子线程去处理动作。
注意要在项目的输入,库依赖项中输入pthread,否则linux链接的时候找不到。

main.c代码:

#include <stdio.h>
#include"Server.h"
#include<unistd.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
    if (argc < 3) {
        printf("./a.out port path\n");
        return -1;
    }
    
    unsigned short port = atoi(argv[1]);
    // 切换服务器的工作目录
    chdir(argv[2]);

    // 初始化监听的套接字
    int lfd = initListenFd(port);

    // 启动服务器程序
    epollRun(lfd);

    return 0;
}

Server.h代码:

#pragma once

// 初始化监听的文件描述符
int initListenFd(unsigned short port);

// 启动epoll
int epollRun(int lfd);

// 和客户端建立连接
// int acceptClient(int lfd, int epfd);
void* acceptClient(void* arg);

// 接收http请求
// int recvHttpRequest(int cfd, int epfd);
void recvHttpRequest(void* arg);

// 解析请求行
int parseRequestLine(const char* line, int cfd);

// 发送文件
int sendFile(const char* fileName, int cfd);

// 发送响应头(状态行和响应头)
/**
* cfd 通信文件描述符
* status 状态码
* descr 状态描述
* type 描述数据格式
* length 数据库长度 若为-1,则告诉浏览器去计算长度
*/
int sendHeadMsg(int cfd, int status, const char* descr, const char* type, int length);

// 获取文件类型,已经有,不用再写
const char* getFileType(const char* name);

// 发送目录
int sendDir(const char* dirName, int cfd);

// 将数字从十六进制转换成十进制
int hexToDec(char c);

// 解码,解决中文乱码问题,from传入参数,to传出参数
void decodeMsg(char* to, char* from);

Server.c代码:

#include "Server.h"
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/sendfile.h>
#include <dirent.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <ctype.h>

struct FdInfo
{
	int fd;
	int epfd;
	pthread_t tid;
};


int initListenFd(unsigned short port )
{
	/**
	* 1. 创建监听的fd
	* AF_INET:基于IPv4协议
	* SOCK_STREAM:采用流式协议,即tcp协议
	*/
	int lfd = socket(AF_INET, SOCK_STREAM,0);
	if (lfd == -1) {
		perror("socket");
		return -1;
	}

	// 2. 设置端口复用:如果程序服务器是主动断开连接的一方,会有一个2msl的等待时长,为了确认客户端已经收到我断开确认ack,
	// 2msl之后才能释放端口
	int opt = 1;
	int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt));
	if (ret == -1) {
		perror("setsockopt");
		return -1;
	}

	// 3. 绑定
	struct sockaddr_in addr;
	addr.sin_family = AF_INET; // 地址协议ipv4
	addr.sin_port = htons(port); // 指定端口,指定的网络字节序为大端,需要进行转换
	addr.sin_addr.s_addr = INADDR_ANY; 
	ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
	if (ret == -1) {
		perror("bind");
		return -1;
	}

	// 4. 设置监听 
	// 128代表在监听过程中一次性能监听多少个连接请求
	ret = listen(lfd,128);
	if (ret == -1) {
		perror("listen");
		return -1;
	}

	// 5. 返回fd
	return lfd;
}

int epollRun(int lfd)
{
	// 1. 创建epoll 树的根节点
	int epfd = epoll_create(1);
	if (epfd == -1) {
		perror("epoll_create");
		return -1;
	}

	// 2. lfd上树:上树用的是epoll_ctl函数,epoll_ctl函数功能强大,第二个参数是表示当前对epoll树做什么操作
	// 上树的时候,第二个参数是add
	struct epoll_event ev;
	ev.data.fd = lfd; 
	ev.events = EPOLLIN; //检测读事件

	int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
	if (ret == -1) {
		perror("epoll_ctl");
		return -1;
	}

	// 3. 检测
	struct epoll_event evs[1024];

	while (1) {
		// 若有事件连接,会存到evs中,并返回有多少个值为num。
		int size = sizeof(evs) / sizeof(struct epoll_event);
		int num = epoll_wait(epfd, evs, size,  -1);  //最后一个参数为-1,没有连接就一直阻塞
		for (int i = 0; i < num; i++) {
			struct FdInfo* info = (struct FdInfo*)malloc(sizeof(struct FdInfo));
			int fd = evs[i].data.fd;
			info->epfd = epfd;
			info->fd = fd;

			if (fd == lfd) {
				// 建立新连接,将新连接添加到epoll树上,添加之后,epoll_wait再检测的节点就变多了。
				//acceptClient(lfd, epfd);
				pthread_create(&info->tid, NULL, acceptClient, info);
			}
			else {
				// 数据通信,主要接收对端的数据,格式为http协议格式
				//recvHttpRequest(fd, epfd);
				pthread_create(&info->tid, NULL, recvHttpRequest, info);
			}
		}
	}
	return 0;
}
// int acceptClient(int lfd, int epfd);
void* acceptClient(void* arg)
{
	struct FdInfo* info = (struct FdInfo*)arg;
	/**
	* 1. 建立连接,调用accept函数
	* accept 三个参数:
	* 第一个参数:需要监听的文件描述符
	* 第二个参数:传出参数,用来保存客户端的ip和端口信息,这里不需要保存
	* 第三个参数:计算第二个参数的大小
	*/
	int cfd = accept(info->fd, NULL, NULL);
	if (cfd == -1) {
		perror("accept");
		return NULL;
	}

	// 2. 设置边沿非阻塞模式,非阻塞说的是文件描述符,默认得到的cfd是阻塞的,用fcntl修改文件描述符的属性
	int flag = fcntl(cfd, F_GETFL);
	flag |= O_NONBLOCK;
	fcntl(cfd, F_SETFL, flag);

	// 3. cfd添加到epoll中:
	struct epoll_event ev;
	ev.data.fd = cfd;
	ev.events = EPOLLIN | EPOLLET; //检测读事件,EPOLLET设置为边沿触发

	int ret = epoll_ctl(info->epfd, EPOLL_CTL_ADD, cfd, &ev);
	if (ret == -1) {
		perror("epoll_ctl");
		return NULL;
	}
	return NULL;
}

// int recvHttpRequest(int cfd, int epfd);
void recvHttpRequest(void* arg)
{
	struct FdInfo* info = (struct FdInfo*)arg;
	int len = 0, total = 0;
	char tmp[1024] = { 0 }; 
	char buf[4096] = { 0 }; //用来存储客户端发过来的整个数据,不够长也没事,只要读出来请求头其实就可以了。
	while ((len = recv(info->fd, tmp ,sizeof tmp, 0)) > 0) {
		if (total + len < sizeof buf) {
			memcpy(buf+total, tmp, len);
		}
		total += len;
	}

	// 判断数据是否被接收完毕
	if (len == -1 && errno == EAGAIN) {
		//解析请求行,另写一个函数,这里只解析请求头
		char* pt = strstr(buf, "\r\n");
		int reqLen = pt - buf;
		buf[reqLen] = '\0';
		parseRequestLine(buf, info->fd);
	}
	else if (len == 0) {
		// 客户端断开了连接
		epoll_ctl(info->epfd, EPOLL_CTL_DEL, info->fd, NULL);
		close(info->fd);
	}
	else {
		perror("recv");
	}
	return NULL;
}

int parseRequestLine(const char* line, int cfd)
{
	// 解析请求行
	// sscanf拆分字符串
	char method[12]; // get or post
	char path[1024]; 
	sscanf(line, "%[^ ] %[^ ]", method, path);
	// 只处理get请求
	if (strcasecmp(method, "get") != 0) {
		return -1;
	}
	decodeMsg(path, path); //处理中
	// 处理静态资源(目录或文件)
	char* file = NULL;
	if (strcmp(path, "/") == 0)
	{
		file = "./";
	}
	else
	{
		file = path + 1;
	}
	// 获取文件属性,判断是目录还是文件
	struct stat st;
	int ret = stat(file, &st);
	if (ret == -1) {
		// 文件不存在, 404
		sendHeadMsg(cfd, 404, "Not Found", getFileType(".html"), -1);
		sendFile("404.html", cfd); //在当前目录下
		return 0;
	}
	else if(S_ISDIR(st.st_mode)) {
		// 是目录
		sendHeadMsg(cfd, 200, "OK", getFileType(".html"), -1);
		sendDir(file, cfd);
	}
	else {
		// 请求的是文件,发送文件
		sendHeadMsg(cfd, 200, "OK", getFileType(file), st.st_size);
		sendFile(file, cfd);
	}
	return 0;
}

int sendFile(const char* fileName, int cfd)
{
	// 1.打开文件
	int fd = open(fileName, O_RDONLY);
	assert(fd > 0);
	//while (1) {
	//	char buf[1024];
	//	int len = read(fd, buf, sizeof buf);
	//	if (len > 0) {
	//		send(cfd, buf, len, 0);
	//		usleep(10); //不要发送太快,给对端一个喘口气的时间
	//	}
	//	else if (len == 0) {
	//		break;
	//	}
	//	else {
	//		prror("read");
	//	}
	//}

	off_t offset = 0;
	int size = lseek(fd, 0, SEEK_END);
	lseek(fd, 0, SEEK_SET);
	while (offset < size) // 如果偏移量小于size,则表示文件没有发送完,继续发送
	{
		// 通信的文件描述符是非阻塞的
		int ret = sendfile(cfd, fd, &offset, size - offset);
		printf("ret value: %d\n", ret);
		if (ret == -1 && errno == EAGAIN) // EAGAIN的意思是没有数据,可以再次进行尝试
		{
			printf("没数据...\n"); 
		}
	}
	close(fd);
	return 0;
}

int sendHeadMsg(int cfd, int status, const char* descr, const char* type, int length)
{
	// 状态行
	char buf[4096] = { 0 };
	sprintf(buf, "http/1.1 %d %s\r\n", status, descr); //版本 状态码 描述语言
	// 响应头
	sprintf(buf + strlen(buf), "content-type: %s\r\n", type); 
	sprintf(buf + strlen(buf), "content-length: %d\r\n\r\n", length);

	send(cfd, buf, strlen(buf), 0);
	return 0;
}

const char* getFileType(const char* name)
{
	// a.jpg a.mp4 a.html
	// 自右向左查找‘.’字符, 如不存在返回NULL
	const char* dot = strrchr(name, '.');
	if (dot == NULL)
		return "text/plain; charset=utf-8";	// 纯文本
	if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
		return "text/html; charset=utf-8";
	if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
		return "image/jpeg";
	if (strcmp(dot, ".gif") == 0)
		return "image/gif";
	if (strcmp(dot, ".png") == 0)
		return "image/png";
	if (strcmp(dot, ".css") == 0)
		return "text/css";
	if (strcmp(dot, ".au") == 0)
		return "audio/basic";
	if (strcmp(dot, ".wav") == 0)
		return "audio/wav";
	if (strcmp(dot, ".avi") == 0)
		return "video/x-msvideo";
	if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
		return "video/quicktime";
	if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
		return "video/mpeg";
	if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
		return "model/vrml";
	if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
		return "audio/midi";
	if (strcmp(dot, ".mp3") == 0)
		return "audio/mpeg";
	if (strcmp(dot, ".ogg") == 0)
		return "application/ogg";
	if (strcmp(dot, ".pac") == 0)
		return "application/x-ns-proxy-autoconfig";

	return "text/plain; charset=utf-8";
}

int sendDir(const char* dirName, int cfd)
{
	char buf[4096] = { 0 };
	sprintf(buf, "<html><head><title>%s</title></head><body><table>", dirName);
	struct dirent** namelist;
	int num = scandir(dirName, &namelist, NULL, alphasort); //修改vs设置c语言的标准为GUN11
	for (int i = 0; i < num; ++i)
	{
		// 取出文件名 namelist 指向的是一个指针数组 struct dirent* tmp[]
		char* name = namelist[i]->d_name;
		struct stat st;
		char subPath[1024] = { 0 };
		sprintf(subPath, "%s/%s", dirName, name); //拼接字符串
		stat(subPath, &st);
		if (S_ISDIR(st.st_mode))
		{
			// a标签 <a href="">name</a>
			sprintf(buf + strlen(buf),
				"<tr><td><a href=\"%s/\">%s</a></td><td>%ld</td></tr>",
				name, name, st.st_size);
		}
		else
		{
			sprintf(buf + strlen(buf),
				"<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>",
				name, name, st.st_size);
		}
		send(cfd, buf, strlen(buf), 0);
		memset(buf, 0, sizeof(buf));
		free(namelist[i]); //释放内存
	}
	sprintf(buf, "</table></body></html>");
	send(cfd, buf, strlen(buf), 0);
	free(namelist);
	return 0;
}

// 将数字从十六进制转换成十进制
int hexToDec(char c)
{
	if (c >= '0' && c <= '9')
		return c - '0';
	if (c >= 'a' && c <= 'f')
		return c - 'a' + 10;
	if (c >= 'A' && c <= 'F')
		return c - 'A' + 10;

	return 0;
}

void decodeMsg(char* to, char* from)
{
	for (; *from != '\0'; ++to, ++from)
	{
		// isxdigit -> 判断字符是不是16进制格式, 取值在 0-f
		if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2]))
		{
			// 将16进制的数 -> 十进制 将这个数值赋值给了字符 int -> char
			// B2 == 178
			// 将3个字符, 变成了一个字符, 这个字符就是原始数据
			*to = hexToDec(from[1]) * 16 + hexToDec(from[2]);

			// 跳过 from[1] 和 from[2] 因此在当前循环中已经处理过了
			from += 2;
		}
		else
		{
			// 字符拷贝, 赋值
			*to = *from;
		}

	}
	*to = '\0';
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值