前言:
這篇文章將講述如何利用Docker雲計算框架和PHP技術在Linux環境下實現一個多人在線編程環境,同時保證服務器安全。我把它叫做雲IDE。可能沒有桌面級IDE的全部功能,只有簡單的多語言編程,運行,下載代碼功能,雖然現在這種平臺在網絡上還是不少的,不乏包括百度、華爲這些大廠。不過當前有敘述這一實現思路的文獻並不多(國內),中國知網上泛眼過去幾乎看不到影子,我看到的一篇是《CodeRun_瀏覽器裏的雲端編程開發IDE》,結果下載下來發現就是一段騰訊科技的新聞。或許這篇文章更適合作爲一篇學術論文發表,但是嘛,我最近剛考完研,有一些時間,想做一些學術研究,又不是很想依照論文的格式死死地來,主要是沒有啥參考文獻吧,純個人創造,還請大家多多包含。
主要用到的技術有:Dockerfile文件編寫、PHP編程、JQuery Ajax異步交互、Linux Shell編程
一、背景
隨着雲計算和大數據時代的到來,越來越多的服務被集成至雲端,這其中不乏編程領域。雲計算的宗旨是PaaS,Platform as a Service,即平臺即服務,用一個瀏覽器或手持設備就能完成所有的事情。在傳統編程領域,我們都是在本地安裝編譯程序,而在雲時代,這些都可以遷往雲端,利用容器虛擬化技術加上現在流行的HTML5前端設計語言可以打造類似本地編譯環境的效果。目前,一些平臺已經做到了在線使用工程並且debug的功能。鑑於網上缺乏這類平臺的實現思路,於是我就利用所學知識,打造了這個簡易的雲端IDE平臺,希望能給各位的深入研究提供一個思路。
二、實現思路
整體的實現思路還是非常直接的。我們可以爲每一個用戶建立一個私有的文件夾,文件夾中存放用戶代碼,和用戶指定的輸入數據,以及程序的運行結果數據。然後使用PHP語言調用docker容器,可以在執行參數中對容器的運行環境進行限制,通過管道流進而在容器中運行用戶代碼。這一切的數據傳輸都將由Ajax異步刷新技術實現,用戶不必刷新頁面。在服務器上,有一個專門的守護進程,來保證用戶寫了死循環程序,又關閉瀏覽器導致服務器資源一直被消耗的情況。思路的大致框架如下:
三、實現過程
1、Docker鏡像的獲取
有兩種方式,一種我們可以去DockerHub上去找,但是因爲天朝,於是就有了另外的一種方式,就是從國內的鏡像倉庫,比如上圖中的DaoCloud。有一個鏡像是一定要的,就是Linux系統鏡像,我用的是Ubuntu,你也可以拖Cent OS。剩下的就根據你想讓系統實現什麼樣的語言編程。我這裏只做了C/C++和Java,於是我需Java的鏡像就好了,GCC的我是基於Ubuntu構建的,當然也可以使用官方。如果以後想實現現在比較火的Python編程的話,只需後面再拖Python的鏡像就行了。注意認準offical標識哦,其他的可能是有做過某些方面的修改,我們希望用的比較純淨的官方鏡像。下圖以Ubuntu鏡像爲例。
2、鏡像的編輯之一
有的鏡像不是一拖下來就能用的,比如GCC,這時我們就要使對原始鏡像進行改造。Docker提供了兩種方式構建新的鏡像,一種是使用commit命令,另外一種是DockerFile。下面對兩種方式都做一個簡單介紹。
2.1 先介紹一下DocketFile,以及我們要用到的一些指令,如果想更深入瞭解,建議參閱官方文檔 。
關於DockerFile,官方的說明是:
Docker can build images automatically by reading the instructions from a Dockerfile. A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. Using docker build users can create an automated
build that executes several command-line instructions in succession.
譯文:Docker能夠通過從Dockerfile中讀取指令來自動地構建鏡像。一個Dockerfile是一個包括所有能夠組成一個鏡像的,用戶能夠調用的命令行文本文件。使用docker build命令,用戶能夠創建一個自動化的鏡像,這個鏡像通過成功執行一些命令行指令來構建。
2.1.2 DockerFile語法,這裏就提我們要用的
2.1.2.1 escape指令:用於表示換行,如果不具體話的話,默認是【\】,在Linux下影響到不是很大,如果是Windows,要設置爲【`】。
2.1.2.2 From指令:用於標識構建新鏡像的基礎鏡像。
2.1.2.3 WORKDIR指令:對RUN、CMD、ENTRYPOINT,COPY和ADD指令設置工作目錄,如果我們設置的工作目錄不存在的話,即使在後面的指令中沒有用到,它也會被創建。
2.1.2.4 CMD指令:官方文檔顯示這個指令有3種使用方法,我們使用
CMD command param1 param2 (shell form)
這種方式。
CMD指令的主要目的是在容器運行的時候給予其一個默認的操作。在一個Dockerfile中,CMD指令只能有一個,如果有多個,只有最後一個指令會被執行。
2.2 接下來是commit指令,這個命令用於從一個容器來構建新鏡像。具體示例可參閱官方文檔
2.2.1 語法
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
2.2.2 簡單例子(來自官方,我們用到的就是這種,官方還有其他的)這個例子是從ID爲c3f279d17e0a的鏡像構建新的
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3f279d17e0a ubuntu:12.04 /bin/bash 7 days ago Up 25 hours desperate_dubinsky
197387f1b436 ubuntu:12.04 /bin/bash 7 days ago Up 25 hours focused_hamilton
$ docker commit c3f279d17e0a svendowideit/testimage:version3
f5283438590d
3、鏡像的編輯之二
a、我們先進入拖下來的Ubuntu鏡像,記得進入控制檯交互模式,就是在docker run的時候加參數-t -i。t即terminal,i即interaction,我是這麼理解的。
b、運行apt-get update更新源,然後運行apt-get install gcc,安裝gcc編譯器。
c、安裝完gcc後退出容器,使用docker ps -a指令,找到剛纔剛退出的那個容器(從Status欄中的時間可推知)
d、使用命令docker commit 06a8d7269e27 XXX指令,構建新鏡像,06a8d7269e27爲容器ID,XXX爲新鏡像名,注意要全部小寫。
經過以上4步,接下來就是Dockerfile登場了,我的Dockerfile是這樣的。
#escape\
FROM 265d2046fe63
WORKDIR /tmp
CMD ./myappp
做個說明:escape \ 指明分隔符
FROM 265d2046fe63 就是我們前面基於容器構建的新鏡像的ID
WORKDIR /tmp 設置容器啓動時的工作目錄爲/tmp
CMD ./myappp 容器啓動時,執行./myapp指令,其實就是運行用戶編寫的程序,程序實體將通過映射的方式傳入容器中
編寫完Dockerfile後,使用docker build -t gccrun .命令構建tag爲gccrun的鏡像。不要忘記了後面的那個小【.】,這個句點表示的是上下文,如果爲句點的話,就表示以當前目錄爲構建鏡像的上下文,上下文中要包含構建新鏡像所用到的所有文件,所以建議在一個新的空目錄中執行指令,不要是用【/】,在Linux中,/代表根目錄,如果這樣的話,將會導致整個硬盤的內容作爲上下文傳輸給docker守護進程。
這裏創建的是運行時要用的鏡像,編譯的話使用將Dockerfile的CMD改爲編譯的命令就行,然後構建一個叫gcccom的鏡像。
4、網站服務器用戶權限的設置
要先查詢網頁服務器所對應用戶的權限,適當提升,因爲我們需要進行一些系統級操作,比如讀寫文件。我用的是Apache,進入apache2.conf中找到${APACHE_RUN_USER}以及${APACHE_RUN_GROUP},這顯然是兩個引入變量名,注意上面的藍字,發現這些變量名定義在/etc/apache2/envvars中
跟蹤,發現Apache執行的用戶名爲www-data,所在用戶組爲www-data,然後要將這個用戶具有執行sudo的權限,具體可見這篇文章http://blog.csdn.net/mgsky1/article/details/79059400
5、PHP編程部分之一
第4步的作用是能夠讓PHP執行一些Shell命令,要調用system函數,還有操作文件的fopen函數,如果不設置的話,將會導致權限不足提示Permission Denied。system之類的函數屬於比較“危險”的函數,默認PHP並不開放,記得去php.ini中進行修改。創建compiler.php文件
5.1 根據語言做好初始化操作,以Java爲例,代碼中的$random爲隨機數,下同,每一個用戶都有自己的文件夾
if($lan == "java")
{
$myfile = fopen("$random/Main.java", "w") or die("Unable to open file!");//創建Main.java文件
fwrite($myfile, $code);//將用戶代碼寫入,$code爲提交表單後POST來的代碼
fclose($myfile);//關閉文件
$type = "class";//設置類型,用於運行完畢後進行清理
$comCMD ="sudo docker run --rm -v '/var/www/html/cloud/$random':/usr/src/myapp -w /usr/src/myapp daocloud.io/java:7 javac Main.java 2>$random/com.txt";//編譯指令,運用管道流進行輸出重定向
$runCMD = "sudo docker run --rm --name $random -i -v '/var/www/html/cloud/$random':/usr/src/myapp -w /usr/src/myapp daocloud.io/java:7 java Main <$random/in.txt >$random/out.txt 2>&1";//運行指令,運用管道流進行輸入輸出重定向
}
5.2 在容器進行編譯,若編譯失敗清理文件
system($comCMD,$status);//$status保存命令的返回值,在這裏,就是執行是否成功
$myfile = fopen("$random/com.txt", "r") or die("Unable to open file!");//讀取com.txt文件
$error = @fread($myfile,filesize("$random/com.txt"));//將文件內容讀入至$error
if($error != "" )
{
if($lan == "gcc" && strstr($error,"error") == true)//判斷是否編譯錯誤
{
echo $error;//顯示編譯錯誤信息
fclose($myfile);
system("rm -r /var/www/html/cloud/$random");//刪除文件夾
return;
}
else if($lan != "gcc")
{
echo $error;
fclose($myfile);
system("rm -r /var/www/html/cloud/$random");//刪除文件夾
return;
}
}
5.3 如果編譯成功則在容器中運行,運行完成後執行清理
$myfile = fopen("$random/in.txt", "w") or die("Unable to open file!");
fwrite($myfile, $input);
fclose($myfile);
system($runCMD,$status);
$myfile = fopen("$random/out.txt", "r") or die("Unable to open file!");
$info = @fread($myfile,filesize("$random/out.txt"));
if($info != "")
{
echo $info;//顯示運行結果
fclose($myfile);
system("find . -name '*.".$type."' | xargs rm -rf");//根據$type變量,尋找雜項文件(比如java的.class文件)並清除
system("rm -r /var/www/html/cloud/$random");//刪除文件夾
return;
}
system("rm -r /var/www/html/cloud/$random");//刪除文件夾
6、PHP編程部分之二
第5步是正常的程序執行步驟,如果用戶想中途停止呢?(比如是一個死循環程序),我們就需要一個用於停止容器的代碼。killContainer.php。這部分的內容就很簡單了,在第5步中,我的容器名稱就是那個隨機數$random,所以,在這裏直接把這個容器殺死就好
$cmd = "sudo docker kill ".$_GET['num'];//num爲提交表單後獲取的用戶隨機數
system($cmd);
7、前端JQuery編程部分
雲IDE在瀏覽器上主要通過JQuery腳本來進行控制,使用Ajax異步交互與後臺進行數據傳輸。比較核心的兩個函數就是tj(提交)和zz(終止)
7.1 tj(提交函數),分兩個部分,第一個是計時,第二個是Ajax
7.1.1 計時部分
這一部分的功能就是在預定的時間(我設定是5秒)內將運行按鈕變灰,即不可點擊,5秒後恢復可點擊狀態,這樣做的緣由是給予程序一定的運行時間,避免創建重複容器導致運行失敗。核心就是setInterval 和 clearInterval 這兩個函數。
var intvalID = setInterval(function()
{
count++;
$('#startRun').text("等待("+count+")");//告知用戶等待秒數
if(count == 5)//count用於計時,初始值爲0
{
$('#startRun').removeAttr("disabled");//使運行按鈕可點擊
$('#startRun').text("運行");//修改運行按鈕的內容爲"運行"
count = 0;
clearInterval(intvalID);
}
}
,1000);//每過1s執行function()
7.1.2 Ajax異步交互部分
這裏就是與後臺服務器交互的核心了,使用Ajax避免用戶刷新頁面,提升用戶體驗。Jquery Ajax語法點這裏 。這裏藉助了setTimeout函數來延時執行zz(終止)函數,zz(終止)函數的具體內容見下節。
$flag = 0;//用於標記是否運行成功,如果5秒後依然是0,則有可能是死循環,執行強制終止
$.ajax({
cache: true,
type: 'POST',
url:'compiler.php',
data:$('#myForm').serialize(),// 你的formid
async: true,
error: function(request) { //如遇網絡問題導致連接失敗,彈出提醒
alert("Connection error");
},
success: function(data) { //如果執行成功,將回傳數據顯示在屏幕上
$flag = 1;
$("#output").html(data);
$("#running").html('');
$('#stopRun').attr("disabled","true");//將停止按鈕設置爲不可點擊
}
});
setTimeout(function () {//若5秒後依然運行未完成,強制終止
if($flag == 0)
{
zz();
$('#stopRun').attr("disabled","true");
}
}, 5000);
7.2 zz(終止)函數,這裏就直接一個Ajax異步
function zz() {
$("#running").html('<img src="img/loading.gif" style="width:20px"/>終止中');
var id = $('#random').val();
$.ajax({
cache: true,
type: 'GET',
url:'killContainer.php?num='+id,//所以上面提及的PHP腳本中使用GET方法,與提交用的POST不同,POST可以傳輸比較大的數據,GET限制1024字節,POST理論上沒有限制
async: true,
error: function(request) {
alert("Connection error");
},
success: function(data) {//成功終止後,清除屏幕多餘數據
$("#output").html('');
$("#running").html('');
$("#stop").html('已終止程序');
}
});
}
8、Linux Shell編程部分之一
第7步是保障服務器安全的第一步,在前端保障,但是有可能出現用戶提交了一段死循環程序,然後關閉了瀏覽器,這時Jquery就不起作用了,而這個死循環程序依然在消耗服務器資源,所以,在服務器上也必須有措施做好“最後一道防線”。這裏,我將把一個定時清理容器的.sh腳本註冊爲Linux系統服務,在後臺靜默運行。
先上代碼吧:daemon.sh
#!/bin/bash
while echo "Begin New Round"
do
sleep 0.5m;
sudo docker ps | grep grun | awk '{print $1}' | xargs docker rm -f;
sudo docker ps | grep java | awk '{print $1}' | xargs docker rm -f;
echo "End This Round";
done
看上去似乎非常簡單,思路和是很清晰的,一個死循環,每隔半分鐘清理名爲grun和java的容器(注:grun是C/C++運行的容器,編譯期間應該是不會造成死循環的)。但是那兩個sudo執行的命令就展現了Linux Shell命令的精簡和強大文字處理能力。
第一個是【|】,這個是管道符號,也就是說,將符號左邊命令的輸出作爲符號右邊命令的輸入
第二個是【grep】,這個是Linux的文本查詢命令,全稱是(global search regular expression(RE) and print out the line,全面搜索正則表達式並把行打印出來),支持正則,但是我們這邊用到的是精確查找,因爲我們是根據鏡像名定位,鏡像名又是我們定死的。例子:
$ sudo
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
956165ee5c24 ubuntu "/bin/bash" 9 hours ago Up 9 hours 0.0.0.0:8087->22/tcp, 0.0.0.0:8086->80/tcp adoring_jang
d56f8e4b698f ubuntu "/bin/bash" 4 months ago Up 10 hours 0.0.0.0:8085->80/tcp silly_yalow
629978e3bdb9 registry "/entrypoint.sh /e..." 4 months ago Up 10 hours 0.0.0.0:5000->5000/tcp suspicious_jang
$ sudo docker ps | grep ubuntu 956165ee5c24
ubuntu
"/bin/bash" 10 hours ago Up 10 hours 0.0.0.0:8087->22/tcp, 0.0.0.0:8086->80/tcp adoring_jang d56f8e4b698f
ubuntu
"/bin/bash" 4 months ago Up 11 hours 0.0.0.0:8085->80/tcp silly_yalow
可以發現返回的是匹配行,想詳細瞭解這個命令,點這裏。
第三個是【awk】,awk是一種編程語言,用於在linux/unix下對文本和數據進行處理。數據可以來自標準輸入(stdin)、一個或多個文件,或其它命令的輸出。腳本通常是攘括在單引號或者雙引號中。用{ }包裹。它先讀入有'\n'換行符分割的一條記錄,然後將記錄按指定的域分隔符劃分域,填充域,$0則表示所有域,$1表示第一個域,$n表示第n個域。默認域分隔符是"空白鍵" 或 "[tab]鍵"。下面舉一個簡單的例子,就可以看到$0,$1有什麼區別。
$ sudo
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
956165ee5c24 ubuntu "/bin/bash" 9 hours ago Up 9 hours 0.0.0.0:8087->22/tcp, 0.0.0.0:8086->80/tcp adoring_jang
d56f8e4b698f ubuntu "/bin/bash" 4 months ago Up 10 hours 0.0.0.0:8085->80/tcp silly_yalow
629978e3bdb9 registry "/entrypoint.sh /e..." 4 months ago Up 10 hours 0.0.0.0:5000->5000/tcp suspicious_jang
然後我調用命令sudo docker ps | grep ubuntu | awk '{print $0}'
$ sudo docker ps | grep ubuntu | awk '{print $0}'
956165ee5c24 ubuntu "/bin/bash" 9 hours ago Up 9 hours 0.0.0.0:8087->22/tcp, 0.0.0.0:8086->80/tcp adoring_jang
d56f8e4b698f ubuntu "/bin/bash" 4 months ago Up 10 hours 0.0.0.0:8085->80/tcp silly_yalow
現在我改成sudo docker ps | grep ubuntu | awk '{print $1}'
$ sudo docker ps | grep ubuntu | awk '{print $1}'
956165ee5c24
d56f8e4b698f
所以,按照其工作原理,按照空格或者TAB鍵分割,$0顯示的是所有,$1顯示的是第一列,在這裏,有點類似表格。
想詳細瞭解,點這裏。
第四個是【xargs】,它擅長將標準輸入數據或管道傳來的數據轉換成命令行參數。下面再舉個例子,就用最簡單的echo字符串,echo命令正常的順序是echo "string",使用了xargs後,變爲some command | xargs echo
$ echo "abc" | xargs echo
abc
想詳細瞭解,點這裏。
以sudo docker ps | grep grun | awk '{print $1}' | xargs docker rm -f;爲例,意思就是,將docker ps 的輸出作爲grep的輸入,尋找與grun鏡像名相匹配的行,然後打印第一列,也就是容器ID,最後通過xargs命令,將這些ID轉換爲docker rm -f XXX中的XXX(參數),對容器進行銷燬。
9、Linux Shell編程部分之二
有了8的鋪墊,我們還需要將這個sh腳本註冊爲系統服務。先上代碼,這段代碼是網絡上搜索來進行修改的,在代碼中使用我使用註釋簡單解釋一下意思,本人水平有限,服務這方面還是第一次涉獵,查了一些資料,還請多多包涵。
#!/bin/bash
#description: hello.sh
#chkconfig: 2345 20 81 #優先級
#From Internet
EXEC_PATH=/var/www/html/cloud/Shell/ #shell文件存放路徑
EXEC=daemon.sh #shell文件名
DAEMON=/var/www/html/cloud/Shell/daemon.sh
PID_FILE=/var/run/daemon.sh.pid
. /etc/rc.d/init.d/functions
#判斷Shell文件是否存在
if [ ! -x $EXEC_PATH/$EXEC ] ; then
echo "ERROR: $DAEMON not found"
exit 1
fi
#關閉服務
stop()
{
echo "Stoping $EXEC ..."
ps aux | grep "$DAEMON" | kill -9 `awk '{print $2}'` >/dev/null 2>&1 #與上文8步類似,找到服務pid,殺死,並把輸出重定向至無底洞/dev/null
rm -f $PID_FILE #刪除pid文件
usleep 100
echo "Shutting down $EXEC: [ OK ]"
}
#啓動服務
start()
{
echo "Starting $EXEC ..."
$DAEMON > /dev/null & #將shell運行時的輸出重定向至無底洞/dev/null,結果就是在控制檯開不到輸出
pidof $EXEC > $PID_FILE #寫入pid文件,防止進程啓動多個副本
usleep 100
echo "Starting $EXEC: [ OK ]"
}
#重啓服務
restart()
{
stop
start
}
#case語句判斷傳入參數,根據此執行相應操作
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status -p $PID_FILE $DAEMON
;;
*)
echo "Usage: service $EXEC {start|stop|restart|status}"
exit 1
esac
exit $?
要說明的一個地方是chkconfig命令,這個用來控制服務啓動的優先級,我這裏是2345 20 81,2345表示系統運行級別爲2、3、4、5時啓動服務,20表示啓動優先級,81表示關閉優先級。關於前4個數,這裏有張表格,來自百度百科。
等級0 | 關機 |
等級1 | 單用戶模式 |
等級2 | 無網絡連接的多用戶命令行模式 |
等級3 | 有網絡連接的多用戶命令行模式 |
等級4 | 不可用 |
等級5 | 帶圖形界面的多用戶模式 |
等級6 | 重新啓動 |
10、代碼下載的實現
這一部分主要用到的是PHP的header函數,設置http報頭。$code爲用戶代碼,$filename爲文件名。設置好報頭之後,直接把用戶代碼打印出來就能實現下載功能了。
header("Content-type:application/octet-stream");//.*( 二進制流,不知道下載文件類型)
header("Accept-Ranges:bytes");
header("Accept-Length:".strlen($code));//代碼長度
header("Content-Disposition:attachment; filename=".$filename);//以附件形式下載
最後來一張效果圖吧
寫在最後:爲了理出這篇文章,查閱了不少po主的博客,包括CSDN、博客園還有百度百科,在此一一表示感謝!現在在等研究生出成績的這段時間內整文章,也是我現在想做也可以做的。眼看馬上就要畢業,真的很想能夠如願以償上第一志願,不論最後研究生能不能上,還是會繼續在這行倒騰,不論代碼是自己寫的還是部分引用網絡的,能夠大致讀懂,弄清架構還是重要的,更重要的是創意,東西就是我們創造出來的嘛,這是一個很有意思的過程,加油!