【项目】在线编译系统

一、需求分析

1、客户端

(1)允许用户选择不同的语言,比如说:C,C++。
(2)提供用户编写代码的功能,将用户编写代码保存到本地
(3)将用户编写的代码传输到服务器
(4)能够接受服务器处理结果并显示

2、服务器

(1)接受客户端传输的数据,包括语言类型和代码
(2)能根据用户选择的语言类型对代码进行编译,编译完成后有两种结果

  • 编译成功:将编译的可执行文件执行,将执行结果发送给客户端
  • 编译失败:将出错信息反馈给客户端
    在这里插入图片描述

3、功能流程

在这里插入图片描述

二、具体功能实现

(一)服务器功能实现

1、主函数

int main()
{
	//一、创建一个套接字
	int sockfd = CreateSocket();
	assert(sockfd != -1);

	//二、创建一个内核事件表
	int epfd = epoll_create(5);
	assert(epfd != -1);

	//三、将sockfd添加到内核事件表中
	struct epoll_event event;
	event.data.fd = sockfd;
	event.events = EPOLLIN;//用户关注的事件类型
	epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);

	//四、监听内核事件表上的所有文件描述符
	while (1)
	{
		struct epoll_event events[MAXEVENTS];
		int n = epoll_wait(epfd, events, MAXEVENTS - 1, -1);//有epoll_wait返回说明有事件就绪
		if (n <= 0)
		{
			printf("epoll_wait error\n");
			continue;
		}

		//五、处理就绪事件
		DealFinishEvents(sockfd, epfd, events, n);
	}
}

2、创建套接字CreateSocket()

首先要实现的是服务器与客户端的连接,所以第一步我们创建出套接字,这里我们选用TCP协议使得客户端和服务器相连接(三次握手建立连接),也就是实现socket()、bind()和listen()等操作。
注意:

  • 如果实现的是一台主机上完成操作,那么就可以将IP地址设为回环地址“127.0.0.1”。但是要是在两台主机(一个模拟客户端一个模拟服务器)来实现通讯就要把这个IP地址设置为服务器主机的IP地址
  • listen的第二个参数是内核维护的完成三次握手的连接,一般设置为5
  • bind()失败有两种原因:①ip地址不是我们本机的IP地址。
    ②端口号不对(使用了没有权限的端口号或者使用了别的程序正在用的端口号)。
//一、创建一个监听套接字(实现对套接字的初始化)
int CreateSocket()
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd == -1)
	{
		return -1;
	}

	struct sockaddr_in ser;
	memset(&ser, 0, sizeof(ser));//对地址初始化
	ser.sin_family = AF_INET;//地址簇
	ser.sin_port = htons(6000);//主机字节序转网络字节序
	ser.sin_addr.s_addr = inet_addr("127.0.0.1");//点分十进制转转网络字节序标准IPV4
	
	int res = bind(sockfd, (struct sockaddr*)&ser, sizeof(ser));
	if (res == -1)
	{
		return -1;
	}
	
	res = listen(sockfd, 5);//5是为内核维护的完成三次握手的连接
	if (res == -1)
	{
		return -1;
	}
	return sockfd;
}

3、处理就绪事件前的准备工作

  • 创建一个内核事件表来存储客户端的信息;
    创建epoll_creat(),将客户端所连接上的所有事件添加到epoll在内核所创建的事件表中,实现了I/O复用技术
  • 将sockfd添加到内核事件表中;
  • 开始监听内核时间表上的所有文件描述符。
    (1)具体实现:首先要通过epoll_create()创建一个内核事件表epfd。然后将套接字sockfd使用cepoll_ctl的方式添加到内核事件表当中,关注的事件为读事件。最后使用epoll_wait监听内核事件表,返回就绪事件
    (2)因为有的事件为空,所以我们使用epoll_wait()循环获取就绪的文件描述符;有epoll_wait返回说明有事件就绪。

4、处理就绪事件

  • 代码实现
//五、处理就绪事件
void DealFinishEvents(int sockfd, int epfd, struct epoll_event *events, int num)
{
	int i = 0;
	for (; i < num; i++)
	{
		//获取就绪文件描述符的值
		int fd = events[i].data.fd;

		//1.有新的客户端链接造成sockfd就绪
		if (fd == sockfd)
		{
			//获取新客户端链接
			GetNewClient(sockfd, epfd);
		}
		//客户端的链接文件描述符上有事件就绪
		else
		{
			//2.断开链接的事件
			if (events[i].events & EPOLLRDHUP)
			{
				close(fd);
				epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
			}
			//3.处理客户端数据
			else
			{
				DealClientData(fd);
			}
		}
	}
}

(1)获取新的客户端连接
当有新的客户端连接时,会造成sockfd就绪,通过accept函数获取到新的连接事件。再将该连接关注的事件类型设置为EPOLLIN | EPOLLRDHUP对端断开连接触发,并将epoll事件设置为边缘触发的状态,再采取ET模式通过epoll_ctl添加到内核事件表当中。最后将监听的文件描述符状态设置为非阻塞通过fcntl函数给文件描述符。

//1.获取一个新的链接
void GetNewClient(int sockfd, int epfd)
{
	struct sockaddr_in cli;
	socklen_t len = sizeof(cli);//保存连接的客户端信息

	int fd = accept(sockfd, (struct sockaddr*)&cli, &len);
	if (fd < 0)
	{
		return;
	}
	printf("客户端%d已连接\n", fd);

	//设置新的链接关注的事件并将其添加到内核事件表中
	struct epoll_event event;
	event.data.fd = fd;
	event.events = EPOLLIN | EPOLLRDHUP | EPOLLET;
	epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);

	int flag = fcntl(fd, F_GETFL);//获得文件描述符状态
	flag = flag | O_NONBLOCK;//将状态设置为非阻塞
	fcntl(fd, F_SETFL, flag);//将非阻塞状态设置给文件描述符
}

(2)断开连接
如果客户端的链接文件描述符上有事件就绪,而新的连接为对端断开触发事件,那么就将连接断开,防止将错误信息传到上层,导致异常。
(3)处理客户端数据

  • 在这一阶段前,用户会在客户端进行代码编写完成前的工作。
  • 然后服务器接收客户端的数据,并将它保存在本地文件中;
  • 服务器对代码进行编译,编译的结果存放在一个编译文件中;
    此时有两个结果,如果编译成功,则继续执行;如果编译失败,则将编译失败的结果反馈给客户端。
  • 编译成功后执行代码;
    执行成功或失败,都将最终的结果发送给客户端。
//3.处理客户端的数据
void DealClientData(int fd)
{
	//3.1接收客户端的数据,将代码存储到本地文件中
	int language = RecvCoding(fd);

	//3.2编译代码,将编译结果存储到编译错误文件中
	int flag = BuildCoding(language);
	if (flag == 0)
	{
		//3.3执行代码,结果存储到文件中
		Carry(language);
		//3.4发送执行结果
		SendResult(fd, flag);
	}
	else
	{
		//发送编译失败执行结果
		SendResult(fd, flag);
	}
}

(3.1)接收客户端数据

  • 因为我们使用的是TCP协议传递数据,所有我们就要考虑粘包的问题,所以我们要将整个信息分为两部分进行接收,即协议头和代码;
  • 接受协议头根据语言来创建对应的本地文件,所以也需要函数指针,定义一个文件头,每次数据交互的时候先发送,解决粘包问题
  • 接收代码:用recv函数接收客户端发来的代码
    • 当代码长度 n == 0 时,说明没有需要接收的代码,直接break掉
    • 当 n == -1 时,说明缓冲区中没有数据可读,唤醒sockfd进行下一次读操作,如果不设置,文件描述符将一直等着数据到来
      因为是ET模式,errno == EAGAIN || errno == EWOULDBLOCK表示此次数据已经处理完成,退出循环
    • 当 n > 0 时,将收到的数据通过write的方式存储到可执行文件中
  • 返回值:返回语言类型,在编译的过程中要根据语言类型选择合适的编译方式。
//3.1接收客户端数据,返回用户传递的语言类型
int RecvCoding(int fd)
{
	//接收协议头。根据语言类型创建对应文件
	struct Head head;
	recv(fd, &head, sizeof(head), 0);

	int filefd = open(file[head.language - 1], O_WRONLY | O_TRUNC | O_CREAT, 0664);

	//接收代码
	int size = 0;
	while (1)
	{
		//如果代码长度>127,就将127个字节读取进去;如果小于,就只将剩余的字节读取进去
		int num = head.file_size - size > 127 ? 127 : head.file_size - size;
		char buff[127] = { 0 };
		int n = recv(fd, buff, num, 0);
		if (n == 0)
		{
			break;
		}
		if (n == -1)
		{
			//表示缓冲区中没有数据可读,唤醒sockfd进行下一次读操作,如果不设置文件描述符一直等着数据到来
			if (errno == EAGAIN || errno == EWOULDBLOCK)
			{
				printf("ser read over\n");
				break;
			}
		}
		size += n;
		write(filefd, buff, n);//将接收到的数据存储到可执行文件中
		if (size >= head.file_size)
		{
			break;
		}
	}
	close(filefd);
	return head.language;//返回语言类型,根据语言类型进行语言编译
}

(3.2)编译代码
编译代码可以直接用系统自带的编译器进行编译。

  • 定义存储文件信息的结构体,最后用来一返回值的形式看是否编译成功。
  • 创建子进程,用来替换对应语言的编译程序。
  • 默认编译器会直接将编译的错误信息打在显示器上,标准输入1,标准错误输入2,但是我们是想要实现让系统将错误信息全部存放在./build_error.txt这个文件中,那么就先将标准输入全部关闭,再将文件描述符重定向到文件中,这样就能把错误信息全部存放在文件中。打开的文件不用手动关闭,因为子进程结束后就会释放这些资源。
    • 为什么要用文件来保存错误信息的大小?
      因为用子进程替换为自带编译器,将编译结果写入错误文件,就可以根据文件大小可以判定是否编译成功,文件大小为0,编译成功,大于0,表示有错误信息写入,编译失败。
  • 父进程必须先等待子进程结束,再将编译错误信息的文件大小存放在st结构体中
  • 最后返回错误文件大小,即st,0表示编译成功,>0表示编译失败
//3.2编译代码,替换为系统自带的编译器进行编译
int BuildCoding(int language)
{
	struct stat st;//定义存储文件信息的结构体
	pid_t pid = fork();
	assert(pid != -1);

	//子进程编写编译错误的信息
	if (pid == 0)
	{
		//以重定向来解决:子进程编译失败,将错误信息反馈给客户端
		int fd = open("./build_error.txt", O_CREAT | O_WRONLY | O_TRUNC, 0664);
		close(1);//关闭标准输入
		close(2);//关闭标准错误输入
		dup(fd);//将文件描述符副重定位
		dup(fd);
		//进程替换编译文件
		execl(build[language - 1], build[language - 1], file[language - 1], (char*)0);
		write(fd, "build error", 11);//替换失败的处理
		exit(0);
	}
	else//父进程
	{
		wait(NULL);//阻塞等待子进程的结束
		stat("./build_error.txt", &st);//将build_error文件大小放到st结构中
	}
	return st.st_size;//返回错误文件大小,0表示编译成功,>0表示编译失败
}

(3.3)执行代码

  • 编译会生成.out文件,直接执行.out文件即可,最终将执行结果保存在"./result.txt"文件中。
//3.3执行代码,替换程a.out程序,将执行结果保存在文件中
void Carry(int language)
{
	pid_t pid = fork();
	assert(pid != -1);
	if (pid == 0)
	{
		int fd = open("./result.txt", O_WRONLY | O_TRUNC | O_CREAT, 0664);
		close(1);
		close(2);
		dup(fd);
		dup(fd);
		execl(carry[language - 1], carry[language - 1], (char*)0);
		write(fd, "carry error", 11);
		exit(0);
	}
	else
	{
		wait(NULL);
	}
}

(3.4)给客户端发送执行结果

  • 发送结果和接收客户端文件一样,为了防止粘包,所以要以两部分进行发送,状态信息和文件大小
//3.4发送结果
void SendResult(int fd, int flag)
{
	//1、先发送状态信息
	char* file = "./result.txt";
	if (flag)
	{
		file = "./build_error.txt";
	}
	struct stat st;
	stat(file, &st);

	//2、再发送文件大小
	send(fd, (int*)&st.st_size, 4, 0);

	//发送服务器反馈内容
	int filefd = open(file, O_RDONLY);
	while (1)
	{
		char buff[128] = { 0 };
		int n = read(filefd, buff, 127);
		if (n <= 0)
		{
			break;
		}
		send(fd, buff, n, 0);
	}
	close(filefd);
}

(二)客户端代码实现

1、主函数

int main()
{
	//1、与服务器建立链接
	int sockfd = StartLink();
	assert(sockfd != -1);

	//2、用户选择语言
	int language = ChoiceLanguage();

	//第一次使用flag和编写下一个代码逻辑相同,所以初始化falg = 2
	int flag = 2;

	while (1)
	{
		//3、用户输入代码
		WriteCoding(flag, language);
		//4、将选择的语言和代码发送给服务器
		int empty = 0;
		empty = SendData(sockfd, language);
		if (!empty)//表示文件不为空
		{
			//5、获取服务器反馈的结果
			RecvData(sockfd);
		}
		//6、用户选择下一次操作
		flag = PrintTag();
		if (flag == 3)
		{
			break;
		}
	}
	close(sockfd);
}

2、用户选择语言

在本项目中,我只实现了C和C++两种语言。

//2.用户选择语言
int ChoiceLanguage()
{
	printf("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
	printf("~~~~~~~~~ 1   c 语言   ~~~~~~~~~~~~\n");
	printf("~~~~~~~~~ 2   c++     ~~~~~~~~~~~~\n");
	printf("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
	printf("请输入您编写语言对应的数字: ");
	int language = 0;
	scanf("%d", &language);
	return language;
}

3、用户输入代码

  • flag标志用户选择创建新文件还是打开上一次的文件继续编辑
    • flag=1,打开上一次的文件继续编辑;直接vim打开上次的文件即可,和flag=2的第二个步骤一样。
    • flag==2:创建新文件需要先删除原有的文件,再vim打开新的,打开上一次的直接vim打开即可。
  • 因为我们编写代码直接用的是系统的vim,所以我们要用execl函数将vim文件与可执行文件进行进程替换
  • 用子进程进行编写代码和进程替换,vim退出,子进程结束。
  • 父进程阻塞,等待子进程结束。
//3.用户输入代码,通过进程替换的方式打开系统vim,用户输入代码
void WriteCoding(int flag, int language)
{
	//flag标志用户选择创建新文件还是打开上一次的文件继续编辑
	if (flag == 2)
	{
		unlink(file[language - 1]);//删除文件
	}

	pid_t pid = fork();
	assert(pid != -1);
	if (pid == 0)//子进程
	{
		//子进程调用vim,创建一个文件,编写代码
		execl("/usr/bin/vim", "/usr/bin/vim", file[language - 1], (char*)0);
		printf("exec vim error\n");
		exit(0);
	}
	else//父进程
	{
		wait(NULL);//等待子进程结束
	}
}

4、发送数据

  • 先获取文件的大小,当用户打开编辑器,并没有写入内容,则不用发送。
  • 为了防止出现粘包,语言+文件大小,先发送协议内容:定义结构体保存,包含两个整型;再发送代码文件内容。
//4.发送数据
int  SendData(int sockfd, int language)
{
	//获取文件属性就能得到文件的大小
	struct stat st;
	stat(file[language - 1], &st);

	//用户打开文件没有输入数据的特殊情况
	if (st.st_size == 0)
	{
		int empty = 1;
		return empty;
	}

	//(1)发送文件头(先发送协议内容,语言+文件的大小)
	struct Head head;
	head.language = language;
	head.file_size = st.st_size;

	send(sockfd, &head, sizeof(head), 0);//用send字节流服务

	//(2)打开文件并发送文件内容
	int fd = open(file[language - 1], O_RDONLY);
	while (1)
	{
		char buff[128] = { 0 };
		int n = read(fd, buff, 127);
		if (n <= 0)
		{
			break;
		}
		send(sockfd, buff, n, 0);//发送给服务器
	}
	close(fd);
}

5、读取服务器反馈信息

//5.读取服务器反馈信息
void RecvData(int sockfd)
{
	//为了防止粘包问题,先接收文件大小
	int size;
	recv(sockfd, &size, 4, 0);

	//接收文件内容
	int num = 0;
	printf("*************  编译运行结果为 ***************\n");
	while (1)
	{
		int x = size - num > 127 ? 127 : size - num;
		char buff[128] = { 0 };
		int n = recv(sockfd, buff, x, 0);
		if (n <= 0)
		{
			close(sockfd);
			exit(0);
		}
		printf("%s\n", buff);
		num += n;
		if (num >= size)
		{
			break;
		}
	}
	printf("**********************************************\n");
}

6、用户选择下一次操作

//6.用户选择下一次操作
int PrintTag()
{
	printf("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
	printf("~~~~~~~ 1  修改代码     ~~~~~~~\n");
	printf("~~~~~~~ 2  编写下一个程序~~~~~~~\n");
	printf("~~~~~~~ 3  退出程序     ~~~~~~~\n");
	printf("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
	printf("please input number: ");
	int flag = 0;
	scanf("%d", &flag);
	return flag;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值