欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【项目】在线编译系统

程序员文章站 2022-05-02 20:22:42
...

一、需求分析

1、客户端

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

2、服务器

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

  • 编译成功:将编译的可执行文件执行,将执行结果发送给客户端
  • 编译失败:将出错信息反馈给客户端
    【项目】在线编译系统

3、功能流程

【项目】在线编译系统

二、具体功能实现

1、服务器功能实现

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

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

	struct sockaddr_in ser_addr;
	memset(&ser_addr, 0, sizeof(ser_addr));//地址初始化
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_port = htons(port);
	ser_addr.sin_addr.s_addr = inet_addr(ip);

	int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
	if (res == -1) return -1;

	res = listen(listenfd, LISTENSIZE);
	if (res == -1) return -1;

	return listenfd;
}

(2)创建epoll_creat(),将客户端所连接上的所有事件添加到epoll在内核所创建的事件表中,实现了I/O复用技术
实现:首先要通过epoll_create()创建一个内核事件表epfd。然后将套接字sockfd使用cepoll_ctl的方式添加到内核事件表当中,关注的事件为读事件。最后使用epoll_wait监听内核事件表,返回就绪事件

(3)因为有的事件为空,所以我们使用epoll_wait()循环获取就绪的文件描述符

  • 获取新的客户端连接
    当有新的客户端连接造成sockfd就绪的时候就需要获取新的客户端连接 。通过socket编程里面的accept函数获取。再将该连接关注的事件类型设置为可读和断开异常事件并采取ET模式通过epoll_ctl添加到内核事件表当中。最后将监听的文件描述符通过fcntl函数设置为非阻塞
//客户端有事件要处理
static void InsertFdToEpoll(int epfd, int fd, short event)
{
	struct epoll_event ev;
	ev.data.fd = fd;
	ev.events = event;
	int res = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);//将事件放入内核事件事件表中
	assert(res != -1);
}

//获取新的客户端
static void GetNewLink(int epfd, int listenfd)
{
	struct sockaddr_in cli_addr;//定义客户端结构
	socklen_t len = sizeof(cli_addr);

	int c = accept(listenfd, (struct sockaddr*)&cli_addr, &len);//连接客户端
	if (c == -1) return;

	InsertFdToEpoll(epfd, c, EPOLLIN | EPOLLRDHUP | EPOLLET);
}
  • 连接成功,处理事件
//删除对应内核时间表中的事件
static void DeleteFdFromEpoll(int epfd, int fd)
{
	int res = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
	assert(res != -1);
}
//处理就绪事件和处理就绪非连接事件
static void DealReadyFd(int epfd, struct epoll_event *evbuff, int n, int listenfd)
{
	int i = 0;
	for (; i < n; ++i)
	{
		//循环遍历就绪描述符号
		int fd = evbuff[i].data.fd;
		if (fd == listenfd)  //  有新的客户端连接 sockfd就绪
		{
			//获取新的客户端进行连接
			GetNewLink(epfd, fd);//要获取新的客户端必须拿到就绪sockfd和内核事件表的文件描述符
		}
		else  //  fd这个客户端有数据到达
		{
			//有事件就绪——也可能为客户端断开了连接
			if (evbuff[i].events & EPOLLRDHUP)
			{
				//客户端断开连接
				//1.删除对应内核时间表中的事件
				//2.关闭文件描述符
				DeleteFdFromEpoll(epfd, fd);
				close(fd);
			}
			else
			{
				// 两种选择: 1) 一次将数据读完  2)分多次将数据读完
				int language = RecvFile(fd);
				int err_size = BuildFile(language);
				if (err_size > 0)
				{
					//  将错误信息发送给客户端
					SendFile(fd, ERR, err_size);
					continue;
				}

				// 执行 a.out     java  main
			}
		}
	}
}
  • 接收客户端的代码
static int RecvFile(int fd)
{
	//接收协议头。根据语言类型创建对应文件
	//接受到代码信息后保存到本地
	Head head;  //  language   filesize
	//创建本地文件-接收协议头
	int n = recv(fd, &head, sizeof(head), 0);
	if (n <= 0)
	{
		return -1;
	}

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

	int sum = 0;

	//接收代码
	while (1)
	{
		char buff[128] = { 0 };
		int size = head.filesize - sum > 127 ? 127 : head.filesize - sum;
		int n = recv(fd, buff, size, 0);
		sum += n;

		write(filefd, buff, n);

		if (sum == head.filesize)
		{
			break;
		}
	}

	close(filefd);

	//返回语言类型
	return head.language;
}

(4)根据所获取的具体时间,安排不同的任务;

2、客户端功能实现

(1)在线编译系统实现,首先与客户端相连接
建立连接即就是socket编程。首先通过socket创建一个套接字,然后客户端通过connect的方式与服务器建立连接。
(2)其次选择要使用的语言 然后编写代码
(3)发送自己写的代码
这里我们采用协议的方式,我们与服务器相互合作的协议是:语言类型+代码量
其次发送代码

int BuildFile(int language)
{
	//  C  --   gcc    CPP  --  g++     Java  ---  javac    go ---  go build
	int num = 0;
	int fds[2];
	pipe(fds);
	//应该创建子进程去替换系统的可执行文件vim
	pid_t pid = fork();
	assert(pid != -1);
	if (pid == 0)
	{
		close(fds[0]);
		close(1);
		close(2);
		dup(fds[1]);
		dup(fds[1]);
		switch (language)
		{
		case 0:
			execl("/usr/bin/gcc", "/usr/bin/gcc", file[language]);
			perror("gcc error: ");
			break;
		case 1:
			execl("/usr/bin/g++", "/usr/bin/g++", file[language]);
			perror("g++ error: ");
			break;
		case 2:
			execl("/usr/bin/javac", "/usr/bin/javac", file[language]);
			perror("javac error: ");
			break;
		default:
			break;
		}

		close(fds[1]);
		close(1);
		close(2);
	}
	else
	{
		close(fds[1]);
		wait(NULL);
		int fd = open("error.txt", O_WRONLY | O_CREAT | O_TRUNC, 0664);
		assert(fd != -1);

		
		while (1)
		{
			char buff[128] = { 0 };
			int n = read(fds[0], buff, 127);
			if (n <= 0)
			{
				break;
			}

			num += n;

			write(fd, buff, n);
		}

		close(fd);
	}

	return num;  // 0    > 0
}

(4)得到反馈结果,做出下一步的选择