96SEO 2026-02-19 09:43 34
第二个模块是一个编译运行的模块#xff0c;这个模块的主要功能就是将用户的代码收集上来之后要…项目目标

这个项目共分成三个模块第一个模块为公共的模块用于解决字符串处理文件操作网络连接等等的问题。
第二个模块是一个编译运行的模块这个模块的主要功能就是将用户的代码收集上来之后要在服务器上形成临时文件进行编译最后运行最后的还有一个Judge模块采用的是MSVC的模式来调用后端的编译模块以及访问文件和数据库把题目列表和编辑界面展示给用户让用户能进行正常的操作。
最后运行会形成两个服务器两个服务器之间使用网络套接字进行通信这样就能将遍历服务部署在服务器后端的多态机器上oj_server只有一台这样就能让oj_server自动的负载均衡的选择后端的编译服务能够以集群处理的方式对外输出oj服务。
这个项目的可扩展性很强。
然后建立三个模块目录common[公共方法都放在这个模块中]oj_server(判题是否正确的服务),compile_server(编译代码运行的模块)。
以上整体是一个oj服务其中有两个重要模块一个是oj_server提供题目给与客户端编写代码的功能可以不止存在一个然后是编译和运行代码的模块然后还有文件/数据库里面保存的就是题目。
然后如果客户端只是为了获取题目列表或者请求特定的题目的话此时的oj_server不需要调用编译和运行的模块而是去到文件中获取题目列表或者特定的题目然后返回给客户端。
如果客户端时提交代码就需要负载均衡的选择服务器执行代码然后将结果和题目的正确结果做判断判断客户端题目是否编写成功如果失败就返回失败的原因。
成功则返回成功结果
下面先编写compile_server模块因为这个模块的功能较为独立只需要编译运行代码以及接收请求就够了编写完成之后再去编写oj_server模块。
最后再去设计前端的内容。
首先需要编译的服务所以需要一个编译服务的hpp文件。
然后再写一个runner.hpp提供运行代码的服务.有了两个服务之后需要对外提供使用接口也就是整合上面两个功能的hpp文件需要一个complie_run文件。
虽然现在这个服务的基本功能已经具有了但是不要忘了这个服务是需要作为一个网络服务而进行的所以这里还需要一个源文件compile_server.cc用于接收网络请求并且调用上面的三个源代码来完成编译和运行代码的功能。
这三个hpp功能首先完成编译的功能这里我们有两个网络服务第一个网络服务也就是现在正在编写的服务用于接收网络请求然后根据网络请求形成代码文件编译运行然后将运行结果以响应的方式返回去另外一个网络服务(oj_server)的作用就是接收客户端发送的请求根据客户端的请求决策是否需要调用编译服务调用编译服务就接收编译结果然后返回给用户不需要调用编译服务就根据用户的请求返回题目列表或者特定的题目。
需要注意这个文件只负责代码的编译。
为了更好的完成这个功能所以会起一个命名空间然后根据面向对象的原则写一个编译的类。
下图中的命名空间名字不太好后面我改成了ns_compilefunc
然后就是编译功能了在写这个功能之前要知道一个点那就是这个服务被调用的时候oj_server一定会将用户的代码以网络请求的方式发送给这个编译服务。
经过主函数的处理之后到这里我们可以认为这个服务已经得到了用户的代码。
然后我们知道要编译代码就需要有临时文件这里我不打算将创建临时文件的功能放在这里所以这里假设已经存在一个临时文件了。
临时文件和代码都已经有了下面就是进行编译了。
而编译的结果就两个要么编译成功要么编译失败告警也是归类于失败的。
当编译失败的时候回想stderr中打印失败的原因。
对于这个编译模块来说只关心编译是否成功出错的原因是什么这个功能是不关心的。
所以为了最后能够知道错误的原因需要形成一个临时的文件用于保存编译出错的结果的。
既然要编译那么是提供编译服务的这个进程取执行编译的功能吗很显然不是如果是这个编译服务的进程去执行编译的功能从系统编程的角度来说就是要进行程序替换那么这个编译服务就无法接收远端提交的代码了所以这里需要fork子进程去完成编译工作
子进程如果编译错误就会往stderr中打印错误的原因此时又想要将结果保存到临时文件中所以需要进行文件fd的重定向。
只是一个编译的功能自然是不需要成员函数的然后就是要为外部提供编译的方法了这里有两种做法第一种以对象的方式呈现给外部当然也可以只提供功能。
子进程要进行编译自然是要调用g去进行编译了而调用g的方法自然是使用程序替换了但是程序替换的函数有下面这些要使用哪一个呢?这里我只想通过g就直接去调用g编译器而不需要带上什么冗余的其它的路径。
而带path的程序替换是需要带上路径的所以这里选择的是最后带p的程序替换函数那么是execlp还是execvp呢这里选择使用execlp。
现在根据g的使用格式我们需要知道源代码文件所在的路径已经生成的可执行程序放在那里的文件这就意味着这里会产生临时文件。
同时因为远端传过来的是一个文件默认文件都是不带后缀也需要一个地方储存所以这里需要在创建一个文件夹用于储存这些临时文件。
现在编译成功要形成的文件名字已经有了错误时要写入错误信息的文件也有了源代码文件1234经过变化之后需要变成1234.cc。
重新排序之后也就是这样首先1234这个文件是在./temp中的所以子进程首先要给这个文件加上.cc后缀然后根据file_name形成剩下的.stderr文件和可执行程序的文件。
这些文件都需要增加路径和后缀。
在这个过程中就出现了第一个需要写到comm模块中的方法因为外部在传递file_name的时候可能不会增加后缀和路径需要我添加后缀和路径而这个功能不止编译服务需要运行模块可能也需要这个服务编译和运行服务的整合服务可能也需要这个功能所以这里需要将这个功能写到comm中去。
现在再回到g函数处就可以继续了。
首先就是在这个compile.hpp中引入路径拼接这个功能增加头文件和是释放命名空间using
然后来完成路径拼接这个工具类的函数。
因为对于每一个文件而言要填到的路径都说是一样的所以这里将路径作为了一个const
string的全局变量然后因为三个函数具有相同的逻辑所以这里再写一个辅助函数。
然后三个工具函数就可以通过调用这个辅助函数完成减少重复代码的编写
现在对于编译功能来说已经能够完成编译了但是编译的结果怎么样是否出错也是需要告诉父进程的所以父进程需要等待子进程。
在等待完成之后需要判断编译是否成功这里不使用子进程的退出码来判断如何判断呢如果可执行程序成功创建了那么自然编译成功了没有创建出来编译自然错误了。
所以这里通过判断某个文件是否存在来判断编译是否成功。
通过这个原理就能够知道了子进程程序替换的退出码是不重要的而子进程程序替换是可能失败的所以最后写一个exit让子进程在替换成功/失败后都不再往后走了。
因为这个判断某个文件是否存在的功能其它的模块可能也需要所以我依旧是将这个功能写到comm中。
对于某个文件是否存在可以使用打开这个文件的方式来判断如果打开失败那么这个文件自然不存在但是这种方法过于暴力了。
这里使用一个接口
这个接口的第一个参数是某个路径上的某个文件第二个参数是一个输出型参数。
struct
这个接口的作用就是获取某个文件的某个属性获取到的属性就放在这个输出型参数中。
文件的acm时间文件的inode文件的大小等等都在这个结构体中有。
这个函数的返回值是获取文件属性成功了返回0失败-1被返回错误码被设置。
文件存在才能获取成功否则就是文件不存在。
到这里编译的主体逻辑就已经完成了。
但是如果编译出错了g会打印出错的信息。
现在需要这个信息而g打印的信息是往stderr中打印的所以子进程在进程g程序替换之前需要进行将stderr进行重定向。
同时打印错误文件将错误信息写到这个错误文件中。
因为日志肯定不止一个模块需要使用所以这里我将日志写到comm中。
下面来编写日志的功能首先就是让日志信息增加等级错误文件的名字和错误的行号
但是对于日志来说还需要一个时间获取时间这个功能我打算将其作为一个公共的组件所以这里再去util中写一个获取时间的工具。
最后使用std::cout的原因在注释中写明了。
这样就能开放式的拼接错误信息这就是一个开放式的日志函数。
但是在使用日志函数的每次都要Log()然后传递一大堆的参数去调用,能否直接LOG(日志等级)去使用呢当然可行。
FILE它会在预处理时被替换为包含当前源文件名的字符串字面量。
在编译时它表示正在编译的源文件的名称通常是绝对路径或相对路径。
LINE它会在预处理时被替换为当前源文件的行号。
在编译时它表示代码中
这两个宏通常用于调试和日志记录以便在输出中包含有关源文件和代码位置的信息有助于快速定位和修复问题。
第一个参数是一个输出型参数会输出当前的时间而第二个输出型参数输出的是一个时区这里不关心结构体中的成员
这意味着需要我传入这个结构。
其中这两个成员中的第一个就是秒数代表从1970年到现在累计的秒数为多少第二个参数为微秒这个参数不使用暂不做处理。
对于gettimeofday这个函数的返回值成功返回0失败返回-1但是这里一般不关心。
首先如果子进程创建失败自然当前用户的请求无法在继续了但是不会影响整体服务的运行
然后就是没有成功打开储存错误信息的文件创建错误信息的文件失败了。
此时就意味着编译可能成功用户的请求能够完成也可能失败。
所以这里的等级为warning。
这里的umask是为了让0644创建的错误文件的权限是正确的。
让创建的错误信息文件不会受到平台的约束。
然后就是如果g编译器的路径或者g本身没有安装导致程序替换出现错误也需要打印错误信息。
因为程序替换成功是不会有日志打印的。
最后如果形成了可执行程序那么就带上一个Info信息。
因为是开放式的日志还可以带上源文件的名字。
因为当前已经存在了编译的功能只要给这个编译的功能调教文件名字即可。
到这里编译功能就测试完成了。
也就说明基本的编译功能就没有问题了。
而整个编译服务不仅需要完成编译还需要运行功能。
到这里为止完成的主要功能就是一个在特定目录下提供特定后缀的c文件能够完成编译以及一个开放式的日志功能也有了。
大概的思路为你要运行就要告诉我你要运行的可执行程序的名称对于runner而言也不需要可执行程序的后缀你只需要给我文件名我也可以通过公共模块中的拼接功能自动的拼接可运行程序的名称然后内部运行这个程序即可运行程序也需要内部创建子进程然后让子进程程序替换去执行目标程序。
而运行的结果也有三种运行完结果对运行完结果不对以及异常这三种结果如何处理呢
这些问题待会处理首先要明确一点对于运行功能来说也只需要告诉runner你要运行的文件名就可以了不需要说明后缀后缀可以通过comm中的字符串拼接功能区完成。
下面就是在ns_runner这个命名空间中runner这个类提供的服务函数了。
然后对于这个运行服务来说需要知道可执行程序文件的名字不需要后缀。
除此之外当一个可执行程序运行的时候会默认打开三个文件流:标准输入标准输出标准错误。
如果想要给一个正在运行的程序输入参数那么就需要使用cin/scanf输入参数但是在oj当中大部分情况下标准输入是不进行考虑的因为当程序提交完成之后提交给上层上层去运行这个程序输入程序一般不做但是在力扣的某些题目中是能够让用户去输入一些参数然后将运行结果返回给用户的。
也就是用户的自测试功能此时就需要处理标准输入我的这个在线oj项目没有这个功能所以对于标准输入时不处理。
也就是用户无法指定测试用例全部由上层的服务来指定测试用例。
在这个项目中我打算设计三个和当前运行程序名相同的文件将三个流的信息都放到这些同名文件中之后如果想要查看这个三个流中的数据直接扫描这三个文件即可。
到这里就能知道对于Run函数来说不关心代码跑完的结果是否正确重点关注代码能否跑完。
然后将三个文件流的信息都写到对应的临时文件中。
那么在编译功能处产生的错误文件就不能时StdErr了而应该是CompilerError。
然后在comm中增加这个函数同时编译服务产生的错误文件的后缀也不应该是err了
而之前也说过了在Run中需要使用到三个临时文件用于储存标准输入标准输出标准错误流中的数据。
对于临时文件名字的拼接也是需要的所以需要在工具类中再增加三个函数用于拼接这三个文件名标准错误已经有了
对于Run函数来说只考虑在运行的过程中是否出现异常即可。
而对于其它的信息直接写到三个临时文件中即可为了写文件所以需要先打开这三个文件。
再打开文件的时候也是在创建这三个文件。
然后对于父进程来说并不需要关心这个三个文件所以父进程一开始就直接关闭这三个文件fd
同时如果创建子进程失败了但是此时已经打开了三个文件也需要将这三个文件关闭:
然后是子进程当程序替换之后需要将程序执行的结果写到这三个文件中或者从某个文件读取信息。
所以需要进行重定向
到这里不要忘了这个运行服务的虽然不需要关心运行结果是否正确但是需要关心运行是否出现异常那么如何才能知道子进程的运行没有出现异常呢首先如果出现异常那么子进程一定是被信号杀掉的而waitpid这个参数的第二个接口的作用为:
直接让这个status0x7f只保留最后的7位这样就能判断这个程序的运行是否出现异常了。
如果这个子进程运行的程序出现异常了这里的返回值就是杀死子进程的信号编号一定大于0【信号为1到31】。
到这里runner服务也就能够正常的运行了。
下面就是添加日志。
首先就是如果打开文件失败了就需要输出日志但是等级呢这种内部错误一般是不暴露给用户的只有用户的错误才应该让用户知道即便用户使用你的服务没有反应也不应该将内部的错误信息返回给用户。
而这里就内部打印一个err等级的日志。
因为打开文件失败了那么也没必要继续往后走了。
然后在最后代表子进程至少运行成功了所以可以打印一个Info信息。
至于子进程是否异常可以再将最后的结果也一起在日志中打印一下即可。
这里提一下现在每一次测试都会让temp中充满了临时文件之后是可以使用代码去清理这些临时文件的。
这里如果我在测试程序中写一个cerr让这个程序手动往标准错误中写信息测试一下:
如果one代码是一个错误的代码,失败的信息就会写到新的编译错误的文件中了。
那么接下来就可以组合编译和运行的程序提供给外部了吗当然不行有没有可能用户传递上来一个错误代码呢死循环等等并且如果用户的代码崩溃了那么运行服务不仅要将信号给上层上层也要知道崩溃的原因是什么。
所以还需要给运行服务添加更多的功能比如时间和空间的约束以及通过返回的信号告知上层错误原因是什么)。
上面已经说明过了运行模块还不够完善首先如果用户提交的代码是一个死循环的代码那么运行模块是会崩溃的。
并且在力扣上的题目都是有时间和空间复杂度的要求的。
在这个项目中没有那么强力的限制这里做约束主要是为了防止恶意用户。
如何做呢这里需要介绍一个新的Linux接口
这个接口能够限制进程使用的虚拟地址空间单位为字节以及设置这个进程所占用的cpu时间还有其它的资源也能限制。
这里只需要认识两个选项即可
然后第二个参数是一个结构体这个结构体是os为我们提供的一个系统接口。
因为我的进程都是非特权的进程所以硬限制直接设置为无限制即可通过软限制来约束对资源的使用即可。
当我没有给这个进程做相关的限制的时候这个进程就会一直占用cpu资源。
因为我打算让硬限制是无限制的所以需要使用下面的这个选项来填充硬限制
这就是为什么再写力扣上的某些题目的时候即使能够通过测试用例力扣也知道超时了因为内部是设置了这样的cpu时间的每一道题目都设置了特定的时间长度。
这里我使用一个死循环让其不断申请空间但是这个测试用例在申请到一定的空间之后就会被os识别到让之后的new失败因为其它进程的行为不能影响os影响os的行为会被强制停止。
这里还需要注意一点在使用上面的接口去约束空间的时候并不会像约束cpu时间一样那样的严格因为对于os来说要查看一个进程的cpu运行时间很好查看而查看进程占用的空间是不太好查看的。
所以会导致限制内存的使用接口的严格程度没有限制cpu时间那样的严格。
但是能够看到有约束的
在没有添加空间约束的时候最大的count能够达到2036。
在添加之后count会是20吗
为什么呢?这里限制的是虚拟地址空间不要忘了这个程序本身在加载的时候也是需要使用虚拟地址空间的。
并且stl容器也是需要虚拟地址空间的。
由此就导致了size没有达到20。
如果我还想让我的题目在oj的时候想要限制栈大小。
都是可以的这些精细化设置都是可以的但是这样就很麻烦了因为不同题目的时间和空间复杂度都是和测试用例强相关的。
那么cpu是如何终止这个进程的呢当一个进程越界之后os会发送信号给越界的进程。
那么收到的是几号信号呢
没有关系我直接捕捉重写所有信号的方法即可当然部分特殊信号例如9号是不可捕捉的。
知道这个进程是收到哪一个信号终止的了。
通过这个就能知道了当进程申请的资源超过约束和进程出现异常的处理方式是类似的。
回到runner功能中就可以在子进程程序替换代码之前进行资源的约束来让子进程在一个资源可控的上下文中运行。
如果资源申请异常了出现信号上层就可以通过6号和24信号来决定这个进程出现错误的原因了。
在增加约束之前需要制动runner模块只是用来运行程序的具体约束的时间和空间值runner是
不知道的。
具体约束的时间和空间值是由出题的人需要指名的所以为让让runner能够更加灵活的供给上层去使用的话可以在Run函数(runner服务中实现功能的函数)增加两个函数参数作为cpu_limit(cpu时间限制)和mem_limit内存限制。
所以上层在调用Run的时候需要传入两个约束。
然后在单独写一个函数用于让子进程去设置时间和空间的限制。
这个函数可以放到comm中但是因为时间和空间的限制只有在runner中才会考虑所以这里我就没有放到comm中
而还有一个问题就是runner的返回值的处理了通过返回值去知道某一个程序运行错误的原因。
为了解决这个问题就需要去编写complie_run.hpp和complie_run.cc了。
这个模块的主要功能就是适配用户的请求还有一个功能和临时文件有关因为临时文件中的代码都是从客户端来的。
而这里还有一些工作需要在这个服务中去完成。
最后还有一个重要的功能未来使用这个服务的用户很多那么编译服务也是可能被多个用户请求的但是temp区域只有一个所以为了区分不同的用户:
然后需要明确一下这里的软件层次complie_run.cc只需要包含complie_run.hpp即可。
complie_run.cc也只需要使用complie_run.hpp中的方法。
complie_run.hpp就需要完成上面说的这些问题。
通过这个图就能够知道了complie_run.cc是需要通过http接收外部的网络请求的既然要接收外部的网络请求就需要进行序列化和反序列化这里就需要使用json了json是一种成熟的序列化和反序列化的方法。
然后在接口的实现上外部会传进来一个json式的数据complie_run.cc在处理之后又会返回一个json式的数据。
首先json是一个第三方库需要安装安装方法网上是有的这里就介绍一种centeros的一种安装方法使用下面的指令
安装完成之后在complie_run.hpp中包含下面的头文件:
在上面将kv值一填下面通过writer对象中的方法就会形成对应的json串。
直接使用g编译时会报错的因为你需要告诉g使用的json库在哪里。
这里能够直接使用-ljsoncpp因为当你安装json的时候在os的默认搜索路径上就已经安装了这个库了所以能够直接使用-ljsoncpp这样去连接这个库。
json就能够将结构化的数据转化为一个字符串方便进行网络发送。
如果不想使用上面的那样形成的字符串还可以使用:
当用户发送数据的时候会存在很多数据例如要运行的程序的数据要发送给这个程序的标准输入的数据以及一些其它的数据等等。
这些数据就是在一个结构体中的结构化数据。
将这些结构化的数据转化为一个字符串这个过程就是序列化而反序列化自然就是将字符串转化为结构化的数据了。
由此就需要进行规定了这里就规定用户给complie_run这个模块的数据格式必须为:
同时这个服务完成之后也需要给上层进行输出而输出的结构也是有要求的:
然后在complie_run.hpp中的第一个向外提供的函数中需要两个参数一个就是输入的json字符串一个就是输出的json字符串。
现在这个start函数能够收到一个json串首先就要将这个json串中的数据解析出来放到另外一个json中。
要完成这个工作就需要Json中的另外一个类对象去辅助。
这样就能将in_json字符串中的内容反序列化到in_value中了。
这里的parse其实有三个参数最后一个参数是有缺省的一般缺省为true.
但是这个parse这里还有许多细节需要处理这些细节会造成差错问题最后再来处理。
如果获取的code为空代表没有代码那么这个服务自然就不能为用户提供了这里就需要做一些差错处理了。
和上面的一样先记录最后再来完成。
既然code中存在代码了而下层的编译和运行服务需要这个代码形成一个文件所以这里需要将这个code写到文件中但是正如之前所说可能会有多个用户一起访问这个服务所以要为每一个用户的代码文件起一个唯一的代码文件名字。
产生唯一文件名的函数虽然只有这一个服务需要但是因为在工具命名中已经声明了存在文件的工具类。
所以我就这个函数的实现放到了命名空间中。
UniqueFileName只负责形成一个唯一性的文件名。
至于这个文件的后缀和下而不是放在temp目录下。
所以就需要使用之前写的路径工具类中的方法了。
这样才是正确的带路径和后缀的文件名字。
有了唯一的临时文件之后就是调用编译和运行服务了。
然后就可以将这两个数据交给Run数据了。
这就是将Run的参数暴露给上层让上层告诉底层这里需要填写的内容。
到这里就完成了吗当然不是在上面的步骤中如果某一个步骤出现了错误差错要如何处理。
完成运行之后还要告诉上层这个程序的运行结果因为服务是基于http的所以又需要将结果反序列化为一个json字符串。
这些步骤都还没有写。
到这里只是完成了一个大体的框架而已。
根据输出的json串来进行差错处理因为即使出现了错误也是要个给上层返回错误的原因的。
首先如果用户传递的code根本没有交给我那么就不要说编译和运行了。
然后下面的逻辑是形成一个唯一的文件名这里形成唯一的文件名使用的思路为毫秒级别的时间戳原子性递增的唯一值来形成唯一的文件名字。
由此就不需要对形成文件名字的函数做判断了。
而写文件也是的返回值为bool也就是写文件也是存在成功和失败两个情况的。
这里就体现了虽然是服务器内部的错误但是不能告诉用户因为怕群众中存在坏人。
形成了临时文件之后就是处理编译了编译依旧只有成功和失败两个返回。
依旧是要在编译失败的时候返回json串但是编译失败了需要告诉用户编译错误的原因是什么这个原因在哪里呢就在xxx.compiler_error这个文件中要让用户知道错误的原因就需要读取文件的内容。
而读取文件的内容
第二种情况程序运行时出现了异常程序接收到的信号编号的描述就是异常的原因这里我就在这个类中专门写了一个函数用于将信号的编号翻译为描述
最后就是_code0代表编译运行成功了。
后面我认为_code这个名字不好改为了run_code
但是现在的代码显得很臃肿因为每一次的差错处理都要给out_value增加描述然后序列化然后返回。
能否将out_value的增加描述放到最后去实现呢当然是可以的。
需要通过goto来实现。
因为需要使用到goto所以将goto跳转代码之间变量的声明放到前面:
然后就是在END这里统一通过status_code处理不同的情况了这也是为什么我在前面的错误的时候将返回值设置为了负数而信号的返回值为正数运行成功为0。
这样就能够通过status_code的值来确定不同的原因了。
其实这里使用if和else判断就可以了因为如果程序运行成功了最后的run_coe也是0不需要进行0的的判断。
但是我这里为了逻辑的清晰就这样写了
既然可以通过status_code的值来确定不同的原因修改一下上面的SinalToDes将其变成通过status_code中的值返回不同的描述负数一定不是信号正数一定是信号。
如果status_code为0说明运行成功了需要读取这个程序运行的结果以及如果出现了非信号的错误也需要储存起来。
当然这里的信号转换是没有写完的之后如果用户提交的代码引发了其它的信号直接在这里继续往后加即可。
但是当run_code为-3的时候我这里更想要知道的是编译的g报错是什么。
也就是那个compile_err文件中的信息。
所以这里需要使用读取文件的接口同时需要将文件名字加入到函数参数中。
根据之前所说这里的实现要通过毫秒时间戳一个原子性递增的唯一值来实现。
所以需要在时间工具类中写一个函数用于获取毫秒时间戳。
因为之前的时间类中只能获得秒时间戳。
然后在UniqueFileName中使用这个函数就能得到毫秒级别的时间戳了现在还需要一个原子性的唯一值可以使用一个static变量然后加锁来实现也可以通过信号量实现。
在C11标准中提供了
标准库中提供的一种原子数据类型用于在多线程环境中对无符号整数进行原子操作。
它提供了一种线程安全的方式来操作无符号整数避免了数据竞争和不一致性的问题。
现在已经形成了唯一的文件名下面就是写入了写入函数只告诉我写入的字符串内容和文件名字即可。
下面就来完成这个函数
然后就是读文件了这里我打算修改一下读文件的参数接口因为读文件的返回值我也想修改为bool类型然后使用输出型参数的方式将读取的文件内容发送给外部。
通过getline的返回值进行判断不会出现错误吗while循环需要的不应该是一个bool类型吗这里就要知道在getline内部重载了强制类型转换。
所以即使getline的返回值不是bool类型的经过强制类型转换也能转化为bool类型的。
但是在某些情况下需要保存这个行分隔符号某些情况下又不需要保存。
例如在某些读取的文件中我使用这个分隔符号可以做了一些格式这个时候就需要保存这个分隔符号了。
所以是否保存这个分隔符号交给上层决定我增加一个bool参数让上层决定是否保存。
这样就完成了现在就需要修改一下使用ReadFile接口的地方了。
而在使用读取文件的接口那里对于返回值我就不做判断了因为即使读取失败了也就是文件为空而已后面也会处理如果在做判断就会让代码显得臃肿。
需要传入的是两个json串这个json串是client通过http传递过来的但是我这里只是进行测试就不使用http让client来传了.
这里使用一个c11中的字符串字面量R在中的内容即使是一个很长的字符串包含特殊字符也不会导致编译器报错因为R中的内容会被编译器直接替换成字符串字面量。
不会认为这些字符串中的特殊字符存在任何的意义。
临时文件是通过时间戳加上原子静态变量形成的这里因为我每一次都在重启服务所以都是1。
但是唯一的文件名确实是产生了。
可以看到这里报了一个错误这个错误是因为我设定的可以使用虚拟内存实在是太少了导致连一些必须使用的库都无法包含完成所以出现了这个错误。
这里我增大一下可以使用的空间。
再去运行:
这些我设定的错误都能正常的处理到这里编译和运行的整合服务也就调试完毕了。
这个服务就能完成你传入一个json我就给你返回一个json经过处理下面要做的就是要将这个编译运行服务进行打包形成一个网络服务。
只有成为了一个网络服务别人才能以网络的形式来访问我的这个服务。
这里需要引入网络库c还没有官方的网络库。
当然我也可以自己写无非就是通过TCP套接字获取客户端的链接监听获取等等代码要写还是很容易的但是很麻烦这里就可以使用c的一个开源的第三方网络库cpp-httplib使用这个将这个服务直接打包为网络服务。
以及在完成一个编译运行服务之后再temp中的临时文件应该将其删除了。
首先解决临时文件清理的问题。
因为可能出现编译或者其它什么原因导致的错误我是不知道具体形成了什么文件的所以在清理文件的时候需要先判断这个文件是否存在如果存在就清理但是每一个需要清理的文件名我是知道的所以可以完成删除文件函数。
并且这个函数因为只有编译和运行模块需要使用所以我就放在编译并且允许模块了。
主要使用unlink函数删除这个文件。
这个函数需要带路径的文件名字。
gitee链接:https://gitee.com/yuanfeng1897/cpp-httplib
这里我使用的版本是0.7.15版本在仓库的标签那里选择就可以了比较稳定的版本。
这个项目只需要将httplib.h包含到你要使用的代码中就可以使用了head-only。
使用7.x/8.x/9.x的版本都是可行的但是不推荐使用4.x版本的。
这样可能导致程序在编译或者运行时出现错误。
经过上面的步骤只是让这个会话暂时升级了如果你想让你的账号一直使用升级后的gcc可以
如果修改了这个文件那么使用scl命令就会出现问题。
导致无法登录
并且要修改的是自己的家目录下的.bash_profile文件。
因为comm是一个公共组件oj服务可以使用编译运行服务也可以使用。
这是因为再httplib这个库中使用thread。
所以需要修改一下Makefile。
这里编译成功运行程序如果你的gcc版本是4.x的就会出现下面的错误:
替换成7.x/8.x/9.x版本就不会出现问题如果你是再vscode的终端下运行的需要重新执行
bash这个指令因为vscode使用的是自己的账户登录云服务器不会执行你在另外一个账号执行的脚本指令。
但是我的这个服务什么都没有所以即便已经启动了被人请求什么都得不到。
所以在启动服务器之前需要提前注册/绑定服务。
这样当请求过来的时候才会有服务能够提供。
如何注册呢?
这样就相当于在我的服务器中我注册了一个服务当别人使用/hello作为url的一部分访问的时候就会运行这个服务。
响应这个文本。
运行测试:
出现了文本但是打印的方式不对因为在服务器返回的响应中应该带上正文所用的编码方式:
这个正在编译的程序被os直接终止了和代码没有问题原因是我的云服务器配置比较低这里我使用vscode连上了这个服务器并且还在调用高版本g比较耗费资源在编译的时候因为资源耗费过多就被os杀死了每一个进程在os中都是有资源使用上限的。
上面都是在vscode上运行的所以直接关闭vscode重新进行连接在编译一般就能解决在vscode上进行编译出现这个错误的问题了。
如果还是存在这个问题那还是在xshell上连接服务器在进行编译吧。
现在服务已经能够请求回来了/hello只是一个我测试的服务后面当用户的请求重新来的时候就能够得到用户请求的Request
json串然后调用刚刚写好的编译运行服务得到一个json串在将这个json串返回给用户。
这样就完成了一个远程的编译运行的服务。
但是还有一个问题上面的代码没有设置web根下的一个默认的网页这样当用户直接访问我的ip地址的时候就会跳转到默认的首页了。
但是这里不需要这么麻烦因为这里我只需要提供一个编译运行服务就够了不需要默认的网页/默认的服务这是一个简单的服务就够了其它的不设置。
没有问题但是现在很难测试这个服务因为我没有客户端但是可以通过某些工具构建一个符合要求的post请求及那个其转为json串之后就是ojserver模块发送给我的服务器来进行测试。
这里我通过postman来完成模拟构建工作。
下面是postman的官方网站:
https://www.postman.com/downloads/
之后在上面填写上我的ip地址和编译运行服务器所在的url即可。
上面已经完成了一个编译和运行的服务下面就来完成oj_server服务
oj_server的本质就是建立一个小型的网站。
这里我的这个oj_server打算提供三个主要功能。
第一个就是功能就是获取首页其实也就是获取一个默认的.html文件这个首页我也可以做的很漂亮但是因为我的重点并不在前端所以就使用题目列表当作首页了。
点进一个题目就能得到题目的详细信息。
然后就是提交代码判断正误。
最后的c就是control控制器这个模块的作用为你什么时候拿数据你要拿多少的数据你要拿的数据要和哪一个网页结合成为一个提供给用户的网页返回给浏览器都由这个control来决定。
control也就是核心业务逻辑。
之前一直听说的核心业务逻辑说的也就是这个了。
要写核心业务逻辑也就是写这个模块了。
当然其它的模块也有写的时候。
这个文件就是要形成的网络服务上面说的所有的功能都需要这个提供文件提供给用户。
因为很显然oj_server的网络功能比起之前写的编译运行的网络服务只需要有一个网络服务compileandrun就够了要多很多。
这个网站服务需要提供的东西需要更多。
因为这个网站会收到很多客户的请求例如想要查看题目列表做题等等不同的请求。
所以这个.cc服务首先要做的就是一个用户请求的路由服务。
如何做呢其实也就是当用户输入不同的网址然后将其路由到不同的服务函数上
但是到这里还没有给用户提供一个默认的首页的网页文件。
这个网页的文件我打算写到wwwroot文件夹下。
完成路由功能之后最先要写的模块本来应该是model模块但是没有题库只有存在了题库model模块才能去获取题库中的信息。
所以下面就是设计题库了设计题库自然就需要设计题目了
这里在oj_server中新建一个questions的文件夹用于储存所有题目的信息。
然后通过力扣网页上来看当你在力扣的题目列表中选中了一道题目选中点击之后才会给你显示某一道题目的详细说明代码编写区域等等。
这里默认了预设值的代码是在header.cpp中写的同时用户写的代码也是写到这个文件中的。
然后为了判断用户的代码是否能够通过所以还需要一个测试用例代码,这个测试用例在tail.cpp这个文件中。
其中的1就是这个题目在题目列表中的编号然后难度为简单最后的两个数其中第一个为时间限制第二个为空间限制。
中间的?在后面说明填什么(不过最后我没有填)
然后我在和questions.list同级的位置上设置一个id为1的文件夹。
这就意味着.list文件是一个顶级的中和1编号有关的信息就在这个1文件夹中。
然后在这个1号文件夹中加入这个题目的描述文件然后就是预设代码文件。
然后就是当用户请求这个题目的时候将这个题目预设的代码进行编写了也就是题目头部的编写提交给用户的代码
这样在之后完成项目之后上面的代码就会被预设到用户浏览器的在线编辑器中。
但是现在还有一个问题就是oj_server这个模块怎么知道用户提交上来的代码是成功通过测试了
然后在test1和test2中直接调用header.cpp中写的类的方法虽然现在这两个文件是分开的但是在项目真正运行的时候这个两个文件(header.cpp和tail.cpp中的代码文件是会放在一起的)。
为了我写测试用例的方便这里我加入了条件编译。
当没有定义某一个宏的时候就includeheader.cpp之后可以通过g编译器的-D选项让g编译器创建一个我指定的宏就能让inclueheader.cpp这个包含直接消失了.
虽然在力扣那样的oj网站中是不会把测试用例暴露出来的因为怕别人去猜它的值但是我这里为了便于调试就公开了。
当然还可以继续添加测试用例从这里也能看出来设计测试用例也是需要非常了解这道算法题目的。
如果用户的代码跑过了上面的三个测试用例在我的这个项目中我就认为你通过了。
2.oj并不是直接就将用户写的代码(header.cpp)文件直接提交给后端的compile_run服务的。
而是将header.cpp文件中的内容拼接上该题号对应的测试用例(tail.cpp)就能够形成一个可以交给compile_run运行的代码文件了。
经过compile_run模块运行之后的结果文件会在stdout文件中以json的方式返回给oj_server。
然后oj_server把通过测试用例的个数返回给客户端就完成了相当于判题的功能。
通过编写这个功能就能知道了对于一道算法题目最难的点在于测试用例的书写(如何写全测试用例)。
然后回到题目列表中的?的填写为了更加方便的找到每一个题目这里的?可以选择将这个题目的描述所在的文件夹的路径加上。
但是这样我认为有点没必要所以我就没有加。
直接规定在获取题目列表之后想要找某一个题目就直接去找这个题目编号同名的文件夹就可以了。
到这里文件版本的题库就设计完毕了。
下面就是编写model模块了这个模块的第一个工作就是根据题目list文件加载所有的题目信息到内存中。
不需要担心成本因为算法题目本来就只有几千道并且因为是纯文本所以不需要担心内存存不下。
对于model模块依旧是要具有命名空间和类的。
类名就叫做Modelmodel的功能为和数据进行交互而我这里的数据自然就是每一道题目的数据了。
因为一道题目的数据有很多所以这里使用一个结构体去描述题目,然后在保存的时候使用题目的编号和描述某个题目的对象进行映射(使用map进行保存即可)
然后就是Mode类的填写了。
对于这个类来说第一个工作构造函数要做的工作应该是将题目加载到内存中。
这个函数的主要目的就是通过question_list所在的路径加载list文件。
然后将这个函数放在构造函数中去。
到这里题目列表就有了。
然后根据列表信息就能够拿到所有题目的其它信息了。
第一个就是获取所有的题目通过vectorquestion的输出型参数来给外部。
第二个就是获取一个题目的信息其实就是通过文件的编号获得一个题目。
其实除此之外还应该有增加题目删除题目修改题目的功能但是这个是文件版本的所以我就不写了。
首先来写获取所有题目列表的函数,虽然后面我能保证获取一个题目和获取所有的题目不会失败但是为了代码的健壮性还是选择了使用bool作为返回值而不是void。
到这里获取所有题目和获取单个题目的函数就完成了下面就是如何读取配置文件了。
也就是load函数了这个函数需要读取文件中每一个题目的信息然后将其填写到结构体中最后插入到容器中。
这个结构体需要的信息都在文件中所以这里的load函数就需要读取文件中的内容了并且是按行去读取内容。
打开文件。
现在已经读到一行数据了这一行数据中就有一道题目的基本信息并且是以空格为分隔符号的而字符串切分的函数不止这一个模块能够使用所以将这一个模块写到comm中.
到这里还有三个数据需要填充给question一个是题目的描述一个是预设的代码信息一个是测试信息。
这是三个信息都在各自的文件中需要使用之前写的文件工具类中读取文件函数。
这里就需要某一个文件具体的文件信息所在的文件路径了:
定制一个这样的全局变量这个变量题目编号就是特定的某一个题目的文件夹了。
首先是在获取所有题目这里如果用户获取题目失败了也就是构造函数没有成功:
然后如果在load函数中没有将题目加载到map中那么我的这个服务影响到所有正在使用的人所以这个错误是致命的。
最后就是完成字符串切割的函数当然可以使用string中的函数来完成切分但是我打算使用boost库中的切分函数下面就是如何安装boost库
在centeros上使用上面的指令安装。
回到切分的函数上如果不使用boost库自己做切分无非就是扫描字符串找到分隔符然后将这个分隔符号之前的内容拿出来就可以了。
但是有了boost库就不需要这样了。
首先使用一个代码测试一下这个boost库这个库是c的准标准库。
运行测试成功。
此时因为我添加了压缩所以即使你的原始字符串中间使用:::::这样的分隔符号最后也只会算作一个如果你是off的话:
因为在求最大值和难度之间存在很多的分隔符号这里没有压缩就将这些都保存下来了。
使用boost库切分就这么简单就完成了到这里文件版本的model模块就完成了。
然后将这个切分增加到mode中:
现在model已经编写完成了虽然还没有测试但是这里先不着急测试下面先去编写一写control模块一样这个模块也要有自己的命名空间和类。
那么在这个控制器中应该有什么呢control肯定要访问到题目的数据所以我将model模块直接包含了进来然后将Model类作为成员对象。
这个control未来是能够被路由功能直接去使用的。
所以Control主要负责的就是核心的逻辑。
如何设计函数呢这里从路由功能开始设计当oj_server收到一个用户的请求是获取所有的题目那么我的这个服务最后应该返回一张包含所有题目的html网页。
这个工作就交给control来做。
因为control模块做的本来就是核心的逻辑。
这个函数因为我还没有写到control中所以存在红线但是之后会写进去的。
这里就使用输出型参数的方式将获取到的网页信息放到html中最后返回给客户端的浏览器。
只不过在获取单个题目这里我是知道这个题目的编号的所以需要将题目编号也传递过去。
这样也只是完成了将题目的信息获取出来了但是如何将获取的题目信息构造成一个网页还需要一个模块去完成这个模块就是view模块。
至于view如何写之后再说通过这里oj_server主要是获取用户的请求然后通过control控制器来完成对路由功能的解耦也就是路由功能的具体实现交给了control模块去实现。
之后的control中还有一个成员就是view模块的成员对象。
这个模块需要使用一个c/c用于网络渲染的库这是谷歌开源的渲染程序。
然后就是如何使用这个库了,以上步骤就是将这个库安装到了我系统搜索的默认路径下。
然后就能使用这个库了如何使用呢可以去看这个项目的手册这里简单说明一下:
然后在html文件中需要使用下面的方式来表示这个变量会被填充:
这就意味着这里需要的就是,保存数据的数据字典以及一个待被渲染的网页内容:
所以所谓的渲染就是将数据字典中的数据替换到html中这就是所谓的渲染的功能。
注意这个代码只是用来测试的,到这里基本的使用就完成了。
现在在写一个简单的html文件。
使用ctemplate库了因为这个库使用了线程所以还需要加上pthread。
可以看到这个新形成的html信息确实将key替换成为了测试信息。
此时就完成了将数据和网页合并形成了一个新的网页也就完成了渲染工作。
这里的网页渲染的工作是由view模块完成的现在要做的就是编写view了。
在编写之前需要思考一个这个view我要如何使用。
因为control模块中需要我通过一个题库渲染出一整个题目的网页所以我打算将一整个题库渲染网页的工作写成view的一个函数。
以及当用户想要看一道题目的详细信息时也需要通过一个question结构体的信息构建出一个题目的详细信息这个功能依旧是view的其中一个函数。
这样两个函数接口就设计完成了。
一个接口用于接收一个题库然后将这个题库中的部分题目信息渲染成一个网页。
需要注意的是这里的所有题目的显示我最后想要显示的是一个表格网页。
到这里这个view就已经设计完成并且在control中使用了。
因为所有题目的显示我最后想要显示的是一个表格网页所以我想先认识一下网页的结构。
所以需要知道表格在html中是如何写的
此时一个简单的表格就出现了。
虽然目前来看比较难看。
但是后面是会进行优化的。
只不过网页我最后再去优化。
但是这样很明显是不可行的如果我的题目很多我还使用这样的代码去显示对于用户来说使用体验是很差的。
之后如果有时间我再来优化这个首页。
这里回到view中view需要渲染两个网页所以这里新增两个html网页用于给view渲染一个是题目列表一个是单个题目的网页。
这是题库网页的编写但是这样渲染出来的题目只有一道很明显是不符合题库的名称的。
所以这里就需要循环了。
使用这个就意味着在question_list两者中间的部分是可能形成多份的。
这就是现阶段我需要的html需要渲染的内容就是question_list中间的部分。
现在回到view部分首先需要告诉view我这个需要它渲染的网页在哪里依旧采用的是一个全局变量的方式:
然后就是引入ctemplate库来完成使用数据和网页渲染成新的网页了。
如果只是单独的一道题目的话下面就可以使用set往这个数据字典中写信息了但是这里我想形成的问题网页需要多个题目所以这里需要使用循环的方式将多个信息添加到网页中。
这里就可以将上面的root当作根网页然后形成子网页往子网页中不断添加数据最后就会形成一个含有多道题目信息的网页。
此时当在oj_server中请求所有的题目时就会调用control模块中的加载和渲染题库的功能control会调用model模块完成对题库的加载然后调用view模块中的渲染网页的函数(也就是上面的函数)。
完成渲染之后ctrol模块会将这个渲染的网页信息返回给oj_serveroj_server在发送给客户端。
那么能否完成呢这里可以测试运行一下在测试之前为了方便跳转我在首页index.html中添加一点东西
可以看到这个题目的页面就正式显示出来了。
虽然没有按照正常的排序但是这个问题不大只要之后对储存题目的容器进行按照编号排一个序就完成了。
到这里已经完成了对题库的渲染下面的问题就是如何完成对单个题目的渲染也就是让用户能够看到一个题目的详细的信息。
依旧是要准备好一个网页用于渲染单个题目。
要渲染给单个题目的信息有这个题目标题这个题目的难度这个题目的编号这个题目的信息以及给用户提供写代码的位置以及测试用例。
其中的number就是题目的编号而tittle就是题目的标题。
网页的标题已经有了现在需要的就是给用户一个代码编辑区以及当前题目的标题和难度和题目的描述
其中的h4就是题目的标题其中desc就是就是这个题目的描述。
到这里需要使用到的网页元素就完成了。
root.SetValue(pre_code,q.question_header);//预设的代码
之后点击一下就可以到下面的框了但是这里的这个代码框有点小我这里手动扩大了。
但是这个问题不大修改一下html即可完成
到时候点击提交就可以提交用户自己写的代码。
虽然这个网页很简单但是大体的框架就是这样的。
到这里网页请求的基本框架就完成了。
下面就是提交功能了。
但是要完成提交功能首先我的oj_server需要先完成判题功能。
而判题就涉及到选择后端服务的过程了。
此时就有两个路径了。
负载均衡逻辑之后再去进行网页的优化。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback