1)實驗平臺:正點原子開拓者FPGA 開發板
2)摘自《開拓者 Nios II開發指南》關注官方微信號公眾號,獲取更多資料:正點原子
3)全套實驗源碼+手冊+視頻下載地址:http://www.openedv.com/docs/index.html
第二十五章基于NicheStack的WebServer實驗
本章我們通過修改Intel官方的一個Web Server的例程來展示如何在開拓者FPGA開發板上
實現Web Server服務器。我們可以通過在瀏覽器中輸入開發板的IP地址來訪問開發板,這時開
發板會返回一個網頁,我們可以通過網頁控制開發板的LED燈的顯示。本章分為以下幾個部分:
25.1 簡介
25.2 實驗任務
25.3 硬件設計
25.4 軟件設計
25.5 下載驗證
簡介
一、 Web Server簡介
Web Server中文名稱叫網頁服務器或web服務器。WEB服務器也稱為WWW(WORLD WIDE WEB)
服務器,主要功能是存儲、處理和向客戶端提供Web頁面。。
Web服務器的客戶端和服務器之間的通信使用超文本傳輸協議(HTTP)進行。提供的頁面
最常見的是HTML文檔,除了文本內容之外,還可能包括圖像、樣式表和腳本。
用戶代理(通常是Web瀏覽器或Web爬蟲程序)通過使用HTTP發出對特定資源的請求來啟動
通信,并且服務器響應該資源的內容或者如果不能這樣做則響應錯誤消息。資源通常是服務器
輔助存儲上的真實文件,但不一定是此種情況,取決于Web服務器的實現方式。
雖然Web Server的主要功能是提供內容,但HTTP的完整實現還包括從客戶端接收內容的方
式。此功能用于提交Web表單,包括上載文件。許多通用Web服務器還支持使用Active Server
Pages(ASP),PHP(超文本預處理器)或其他腳本語言的服務器端腳本。這意味著Web服務器
的行為可以在單獨的文件中編寫腳本,而實際的服務器軟件保持不變。通常,此函數用于動態
生成HTML文檔,而不是返回靜態文檔。前者主要用于從數據庫中檢索或修改信息。后者通常更
快,更容易緩存,但無法提供動態內容。Web服務器不僅用于服務萬維網,還可以嵌入在諸如
打印機,路由器,網絡攝像頭之類的設備中,并且僅用于本地網絡。用作監視或管理以上設備
系統的一部分。這通常意味著不需要在客戶端計算機上安裝其他軟件,因為只需要Web瀏覽器
(現在大多數操作系統都包含瀏覽器)。
二、 Read-Only Zip File簡介
英特爾FPGA提供了一個只讀zip文件系統,用于硬件抽象層(HAL)。只讀zip文件系統提
供對存儲在Flash中的簡單文件系統的訪問。該文件系統適用于嵌入式軟件。驅動程序利用文
件子系統的HAL通用設備驅動程序框架。因此,可以使用ANSI C標準庫I/O函數(例如fopen()
和fread())訪問zip文件子系統。英特爾FPGA只讀zip文件系統作為軟件包提供。HAL驅動程
序的所有源文件和頭文件都位于<Nios II EDS安裝路徑>/components/altera_ro_zipfs/HAL
目錄中。
zip文件必須是未壓縮的。Altera只讀zip文件系統僅使用zip格式將文件捆綁在一起;它不
提供已知zip工具的文件解壓縮功能。使用WinZip GUI可以直接創建沒有壓縮的zip文件。或者
使用-e0選項在命令行中使用winzip或pkzip時禁用壓縮。
實驗任務
本章的實驗任務是使用基于Nios II SBT for Eclipse自帶的Web Server工程模板建立一
個適用于開拓者開發板的Web Server。
硬件設計
本章的Web Server實驗硬件部分可以基于《基于NicheStack的簡單telnet服務器實驗》,
在該實驗的硬件部分(Qsys)對三個IP核的設置進行小的修改即可。
第一個修改的IP核是添加到Qsys的EPCS IP核, 將其名稱修改為epcs_flash_controller,將
其地址設置為0并鎖定,如下圖所示:
圖 25.3.1 設置EPCS的名稱和基址
修改名稱是為了和軟件部分相對應,后面會講到,修改地址是為了方便后面使用,也是因
為此工程的要求。
第二個修改的IP核是SDRAM,因為我們將EPCS的地址設為0,所以我們需要將SDRAM的地址
設為0x0200_0000或其它的合適值,如下圖所示:
圖 25.3.2 設置SDRAM的基址
第三個修改的IP核是Nios II,由于修改了EPCS的名稱,所以需要將Reset Vector重新設
置為更改之后的EPCS,如下圖所示:
圖 25.3.3 重新設置Nios II的復位向量
修改完之后如果沒有錯誤的話就重新Generate,如果有地址錯誤,就先點擊自動分配地址,
然后再修改SDRAM的地址為0x0200_0000或其它的合適值即可。重新Generate之后,由于硬件的
Verilog HDL頂層代碼和之前一樣,無需改動,所以我們直接編譯生成sof文件。
至此,硬件部分設計完成,下面開始基于 Nios II SBT for Eclipse 的軟件部分的設計。
軟件設計
在打開Nios II SBT for Eclipse之前,我們需要做一些準備工作以便能正確運行Web
Server。
因為Nios II SBT for Eclipse自帶的Web Server工程模板例程是需要Read-Only Zip文件
系統才可以正常運行,很不幸的是官方的Read-Only Zip文件系統的驅動程序是基于并行接口
的CFI Flash,而我們開拓者開發板上只有基于串行SPI接口的EPCS,而沒有并口的CFI Flash,
所 以 不 進 行 相 應 修 改 是 使 用 不 了 的 。 這 里 我 們 需 要 將 位 于 <Quartus 安 裝 文 件
夾>\nios2eds\components\altera_ro_zipfs\HAL\src目錄下的altera_ro_zipfs.c文件替換
成我們提供在本工程的doc文件夾下的支持串行接口Flash的altera_ro_zipfs.c文件。下面我
們以read_word函數來簡單的看一下這兩個文件的區別。
原始的altera_ro_zipfs.c文件的read_word函數:
圖 25.4.1 原read_word函數
更改后的altera_ro_zipfs.c文件的read_word函數:
圖 25.4.2 修改后的read_word函數
可以看到更改后的文件增加了if-else語句來判斷是串口的Flash還是并口的Flash。需要
注意的是其中的EPCS_FLASH_CONTROLLER_BASE和epcs_flash_controller,只有當我們在Qsys
中將EPCS的名稱設置為epcs_flash_controller時才會出現的。
現在我們打開Nios II SBT for Eclipse,點擊File/New/Nios Application and BSP from
Template選項后進入圖 25.4.3所示界面,輸入完相應的信息后,我們在左下方的“Template”
欄選擇“Web Server”,這是一個在MicroC/OS-II上使用NichStack的HTTP服務器工程模板,
服務器可以處理從Altera只讀zip文件系統提供的HTML、JPEG和GIF文件的基本請求,需要注意
的是它并不是功能齊全的HTTP服務器的完整實現。
圖 25.4.3 創建Web Server工程
選擇完成后,點擊“Finish”,工程創建完成后,有如下圖所示的工程目錄結構:
圖 25.4.4 WebServer工程結構
其中的C源文件說明如下:
alt_error_handler.c:alt_uCOSIIErrorHandler函數的定義,用于MicroC/OS-II錯誤的
簡單錯誤處理程序,并打印相應信息。
alt_error_handler.h:上述函數的聲明。
http.c:HTTP服務器的實現,包括所有必要的套接字調用,以處理多個連接并解析基本HTTP
命令以處理GET和POST請求。通過HTTP GET對文件的請求指示服務器從Flash文件系統獲取文件
(如果可用)并將其發送到請求它的客戶端。
http.h:定義HTTP服務器實現和常見HTTP服務器字符串和常量的頭信息。
network_utilities.c:包含用于管理尋址的MAC地址、IP地址和DHCP。這些在初始化期間
由NicheStack使用,但是是特定于實現的,所以我們需要對該文件稍作修改——將MAC地址設
置為任何合適的值,后面我們會講解如何修改。
srec_flash.c:包含遠程配置所需的SREC解析和flash編程例程。
web_server.c:main函數所在的源文件,包含大量代碼,包括,網絡初始化例程,Web服
務器任務(WSTask)以及所有板控制實用程序/任務。
web_server.h:整個示例應用程序的定義。
另外system文件夾下的ro_zipfs.zip文件是ro_zip文件系統所使用的文件,我們將其替換
成我們自己的ro_zipfs.zip文件(放在工程所在的doc文件夾下)。
因為此例程默認使用DHCP客戶端,由于我們不需要使用,所以將其關閉。右鍵點擊
qsys_webserver_bsp,選擇“Nios II/BSP Editor”,進入下圖所示界面
圖 25.4.5 取消勾選DHCP客戶端
在 “ Software Packages” 菜 單 欄 下 選中“ altera_iniche ” 后 取 消 勾 選
“enable_dhcp_client” 。 除 此 之 外 , 我 們 還 需 要 設 置 “ altera_ro_zipfs ” 。 選 中
“altera_ro_zipfs”后,將“ro_zipfs_offset”設置為0x100,如下圖所示:
圖 25.4.6 設置ro_zip文件系統的偏移地址
其中“ro_zipfs_base”是Flash器件在Qsys中的基址,由于我們在Qsys中將EPCS的基址設為0x0,所以此處無需修改,此處需要注意的是該工程模板的要求是存放Flash的基址必須是0x0,
“ro_zipfs_name”為文件系統的掛在點,保持默認即可,“ro_zipfs_offset”指明文件系統
在Flash中的偏移量,默認值為0x100000,遠大于EPCS器件的大小(0x800),所以此處我們將
其設置為0x100。設置完成后,點擊右下角的“Generate”,再點擊“Exit”退出,彈出下圖
所示信息時點擊“Yes,Save”。
圖 25.4.7 保存修改
為了編譯能正確通過,我們再次右鍵點擊qsys_webserver_bsp,選擇“Nios II/ Generate
BSP”。
因為不使用DHCP,所以我們需要設置靜態IP地址。打開qsys_webserver目錄下的
web_server.h文件,修改第64行~72行的宏定義,如下圖所示:
圖 25.4.8 設置靜態IP地址
修改完靜態IP地址后,我們還需要給WebServer服務器一個MAC地址。我們只需將
qsys_webserver目錄下的network_utilities.c文件第282行的get_board_mac_addr函數修改
成如下形式:
圖 25.4.9 設置MAC地址
MAC地址任意設置。設置完成后,如果我們直接編譯工程,會出現如下錯誤:
圖 25.4.10 未定義錯誤
因為我們沒有“EXT_FLASH_NAME”只有“EPCS_FLASH_CONTROLLER_NAME”,所以我們需要
將錯誤所在行的“EXT_FLASH_NAME”替換為“EPCS_FLASH_CONTROLLER_NAME”。替換完成后,
編譯通過。
經過以上修改后,軟件設計部分就可以正常使用了。
下載驗證
講完了軟件工程,接下來我們就將該實驗下載至我們的開拓者開發板進行驗證。
首先我們用一根網線將開發板和電腦進行連接,然后連接JTAG和電源,開發板上電后我們
在Quartus II軟件中將qsys_eth.sof文件下載至我們的開拓者開發板,qsys_eth.sof下載完成
后,我們就將ro_zip文件系統下載至我們的EPCS,下載方法如下:
在Nios II SBT for Eclipse軟件中點擊“Nios II”菜單欄,單擊“Flash Programmer”,
如下圖所示:
圖 25.5.1 打開Flash Programmer
或者按快捷鍵Ctrl+7,彈出下圖所示界面:
圖 25.5.2 新建下載項
我們點擊“File”菜單,選擇“New...”,彈出下圖所示界面:
圖 25.5.3 加載settings.bsp文件
我們先點擊箭頭1所指的“…”添加qsys_webserver_bsp工程下的settings.bsp文件,然
后點擊箭頭2所指的“OK”按鈕,進入下圖所示界面:
圖 25.5.4 出現時間戳不匹配錯誤
可以看到界面下方出現了系統時間戳不匹配的錯誤,我們單擊右上角箭頭1所指的
“Connection...”按鈕,在下圖所示界面中,勾選“System ID checks”下的兩個選項:
圖 25.5.5 勾選忽視時間戳匹配
此時系統時間戳不匹配的錯誤變成警告,不影響下載,單擊“Close”按鈕。點擊圖 25.5.4
箭頭2所指的“Add...”按鈕,在彈出的界面中將標識1的“Files of type”設置為“All Files”,
標識2的“Look in”為qsys_webserver的system文件夾,選中標識3的“ro_zipfs”,單擊標
識4的“Select” ,如下圖所示:
圖 25.5.6 添加ro_zip文件
回到下圖所示界面后,我們將箭頭1所指“Flash Offset”設置為0x100,這是我們在BSP中
設置的ro_zip文件系統在Flash中的偏移地址,然后點擊箭頭2所指的“Start”,開始將ro_zip
文件系統下載到EPCS中。
圖 25.5.7 設置偏移地址
下載完成后,在win10系統下可能會彈出下圖所示的警告,這是由于版本不新引起的,不過不影響功能,在win7系統下則沒有此警告。點擊“Exit”,退出下載Flash界面。
圖 25.5.8 win10下的警告
將ro_zip文件系統下載到開拓者開發板的EPCS Flash中后,我們將最后一個文件
qsys_webserver.elf下載至我們的開拓者開發板,qsys_webserver.elf下載完成以后,我們的
C程序就會執行在我們的開拓者開發板上,此時在Nios II Console上打印如下信息:
圖 25.5.9 Nios II控制臺打印信息
從中我們可以看到此處MAC地址(十六進制)與我們設置的十進制相符,IP地址也是我們
設置的靜態IP地址192.168.1.234。et1的IP地址為192.168.1.234,此IP地址為我們可以通過
電腦瀏覽器訪問的地址。
現在我們打開電腦的瀏覽器,看一下會出現什么結果吧。此處我們選擇Google Chrome瀏
覽器(任一瀏覽器都可以),在地址欄輸入“192.168.1.234”,如下圖所示:
圖 25.5.10 瀏覽器中輸入IP地址
回車后,出現下圖所示界面:
圖 25.5.11 連接成功后顯示的網頁
與此同時,控制臺會打印以下信息:
圖 25.5.12 控制臺打印相應信息
表明成功抓取到需要顯示的文件。
我們點擊“LEDs”下的“Start”按鈕后,開拓者開發板上的4個led燈顯示快速流水效果,
如下:
圖 25.5.13 顯示結果
ASP.NET Core SignalR 是一個庫,可用于簡化向應用添加實時 Web 功能。 它會盡可能地使用 WebSocket。
對于大多數應用程序,我們建議使用 SignalR,而不是原始 WebSocket。 SignalR 可為 WebSocket 不可用的環境提供傳輸回退。 它還可提供基本的遠程過程調用應用模型。 并且在大多數情況下,與使用原始 WebSocket 相比,SignalR 沒有顯著的性能缺點。
對于某些應用,.NET 上的 gRPC 提供了 WebSocket 的替代方法。
在 Startup 類的 Configure 方法中添加 WebSocket 中間件:
app.UseWebSockets();
備注
如果您想要接受控制器中的 WebSocket 請求,必須在 app.UseEndpoints 之前調用 app.UseWebSockets。
可配置以下設置:
var webSocketOptions=new WebSocketOptions()
{
KeepAliveInterval=TimeSpan.FromSeconds(120),
};
app.UseWebSockets(webSocketOptions);
在請求生命周期后期(例如在 Configure 方法或操作方法的后期),檢查它是否是 WebSocket 請求并接受 WebSocket 請求。
以下示例來自 Configure 方法的后期:
app.Use(async (context, next)=>
{
if (context.Request.Path=="/ws")
{
if (context.WebSockets.IsWebSocketRequest)
{
using (WebSocket webSocket=await context.WebSockets.AcceptWebSocketAsync())
{
await Echo(context, webSocket);
}
}
else
{
context.Response.StatusCode=(int) HttpStatusCode.BadRequest;
}
}
else
{
await next();
}
});
WebSocket 請求可以來自任何 URL,但此示例代碼只接受 /ws 的請求。
可在控制器方法中采用類似的方法:
public class WebSocketController : ControllerBase
{
[HttpGet("/ws")]
public async Task Get()
{
if (HttpContext.WebSockets.IsWebSocketRequest)
{
using WebSocket webSocket=await
HttpContext.WebSockets.AcceptWebSocketAsync();
await Echo(HttpContext, webSocket);
}
else
{
HttpContext.Response.StatusCode=(int)HttpStatusCode.BadRequest;
}
}
使用 WebSocket 時,“必須”在連接期間保持中間件管道運行。 如果在中間件管道結束后嘗試發送或接收 WebSocket 消息,可能會遇到以下異常情況:
復制
System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.
如果使用后臺服務將數據寫入 WebSocket,請確保保持中間件管道運行。 通過使用 TaskCompletionSource<TResult> 執行此操作。 傳遞 TaskCompletionSource 到背景服務,并在通過 WebSocket 完成時讓其調用 TrySetResult。 在請求期間對 Task 執行 await,如下面的示例所示:
app.Use(async (context, next)=>
{
using (WebSocket webSocket=await context.WebSockets.AcceptWebSocketAsync())
{
var socketFinishedTcs=new TaskCompletionSource<object>();
BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);
await socketFinishedTcs.Task;
}
});
如果從操作方法返回過快,則還可能發生 WebSocket 關閉異常。 接受操作方法中的套接字時,請等待使用該套接字的代碼完成運行,然后再從操作方法返回。
堅決不要使用 Task.Wait、Task.Result 或類似阻塞調用來等待套接字完成,因為這可能導致嚴重的線程處理問題。 請始終使用 await。
AcceptWebSocketAsync 方法將 TCP 連接升級到 WebSocket 連接,并提供 WebSocket 對象。 使用 WebSocket 對象發送和接收消息。
之前顯示的接受 WebSocket 請求的代碼將 WebSocket 對象傳遞給 Echo 方法。 代碼接收消息并立即發回相同的消息。 循環發送和接收消息,直到客戶端關閉連接:
private async Task Echo(HttpContext context, WebSocket webSocket)
{
var buffer=new byte[1024 * 4];
WebSocketReceiveResult result=await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
result=await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
如果在開始循環之前接受 WebSocket 連接,中間件管道會結束。 關閉套接字后,管道展開。 即接受 WebSocket 時,請求停止在管道中推進。 循環結束且套接字關閉時,請求繼續回到管道。
當客戶端由于失去連接而斷開連接時不會自動向服務器發送通知。 服務器只有在客戶端發送通知時才會收到斷開連接消息,而此操作無法在失去 Internet 連接的情況下進行。 如果想要在發生此情況時采取某個操作,在特定時間范圍內未收到來自客戶端的任何消息后設置超時。
如果客戶端并非總是發送消息且不希望僅由于連接進入空閑狀態就設置超時,則讓客戶端使用一個計時器并每隔 X 秒發送一條 ping 消息。 在服務器上,如果某條消息在上一條消息發出后的 2*X 秒內尚未到達,則終止連接并報告客戶端已斷開連接。 等待兩次預測的時間間隔,以便為可能延遲 ping 消息的網絡延遲提供額外的時間。
CORS 提供的保護不適用于 WebSocket。 瀏覽器不會:
但是,瀏覽器在發出 WebSocket 請求時會發送 Origin 標頭。 應將應用程序配置為驗證這些標頭,以確保只允許來自預期來源的 WebSocket。
如果在“https://server.com ”上托管服務器并在“https://client.com”上托管客戶端,請將“https://client.com”添加到 AllowedOrigins 列表以驗證 WebSocket。
var webSocketOptions=new WebSocketOptions()
{
KeepAliveInterval=TimeSpan.FromSeconds(120),
};
webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");
app.UseWebSockets(webSocketOptions);
備注
與 Referer 標頭一樣,Origin 標頭由客戶端控制,并可以偽造。 請勿將這些標頭用作身份驗證機制。
安裝了 IIS/IIS Express 8 或更高版本的 Windows Server 2012 或更高版本以及 Windows 8 或更高版本支持 WebSocket 協議。
備注
使用 IIS Express 時始終啟用 WebSocket。
在 Windows Server 2012 或更高版本上啟用對 WebSocket 協議的支持:
備注
使用 IIS Express 時無需執行這些步驟
在 Windows 8 或更高版本上啟用對 WebSocket 協議的支持:
備注
使用 IIS Express 時無需執行這些步驟
如果在 Node.js 的 socket.io 中使用 WebSocket 支持,請使用 web.config 或 applicationHost.config 中的 webSocket 元素禁用默認的 IIS WebSocket 模塊 。如果不執行此步驟,IIS WebSocket 模塊將嘗試處理 WebSocket 通信而不是 Node.js 和應用。
<system.webServer>
<webSocket enabled="false" />
</system.webServer>
本文附帶的示例應用是一個 echo 應用。 它有一個可建立 WebSocket 連接的網頁,且服務器將其收到的消息都重新發回到客戶端。 示例應用未配置為使用 IIS Express 從 Visual Studio 運行,因此請在命令行界面中使用 dotnet run 運行應用,并在瀏覽器中導航到 http://localhost:5000。 該網頁顯示連接狀態:
選擇“連接”,向顯示的 URL 發送 WebSocket 請求。 輸入測試消息并選擇“發送”。 完成后,請選擇“關閉套接字”。 “通信日志”部分會報告每一個發生的“打開”、“發送”和“關閉”操作。