概述
爲了避免大家說我是標題黨,先說明背景。前段時間,領導和我說了一個項目,需要開發一些無線設備。要求:
- 傳輸距離1500m左右
- 帶寬1000M
問題就來了,在公司已有的資源下,是沒有哪一款能夠達到以上的性能。我很肯定的和領導說:“我們的設備性能達不到。如果要做的話,短期內做不出來的”。但是領導可不管,就說:“小謝,你想想辦法,功能簡單點也沒關係。這個項目利潤不錯,你好好幹,年底少不了獎金的”。聽到獎金二字,我就有點激動,回覆道:好好,我試試。
時間短,重新畫板子開發肯定不行了。於是我想到了包裝方案。所謂的包裝方案,就是將別的公司的產品改爲自己的產品,進行出售。ODM就是這樣的生產方式。但是ODM的需要投入的資金也比較多,我們自己有軟件團隊,爲什麼將軟件開發費給別人呢?於是我想到了一個類似ODM的方式:
選擇能夠滿足性能要求的設備M,再找一個千兆帶寬的ARM板 L,利用L設備 telent到M設備上,進行參數設置:SSID,PASSWORD,Mode等基本參數。通過訪問L設備進行參數設置,當然,L設備上的web頁面都是本公司的頁面。
有了想法之後,就在網上找了一些設備,驗證了能夠telnet登陸並修改參數。並將這個方案和領導說了一下,領導聽了之後,感覺能夠滿足客戶需求,並且能夠節約成本,並且能夠在規定時間內完成,便愉快的答應了。通過一個多月的開發和調試,最終項目得以完美交付。
ps:可能有人會有疑惑,爲什麼客戶不找其它廠家,非要找你們,並且還貴。那是因爲我們公司銷售厲害啊-。-
其實在我們工作中,我相信遇到相似的情況並不少。希望能夠給遇到朋友一些思路和建議。說不定就會有柳暗花明又一村的醒悟。
開發過程中遇到的問題
以下內容主要分享一下,在開發過程中遇到的一些問題或思路。希望給有相同想法的小夥伴一些幫助。
telnet的輸入和輸出
在這個方案中,L設備和M設備的通信都是通過telent作爲橋樑,因此,它的輸入輸出極爲重要。
- 輸入:就是設置L設備參數的命令
- 輸出:就是L設備返回的狀態
方案一:剛開始我通過shell的輸入輸出重定向到文件中
通過調試一段時間後,發現不可取。總會遇到這樣或那樣的問題,可能是因爲我對shell的標準輸出和標準輸入理解不夠充分。
方案二:修改telnet.c源碼
我們知道telent的輸入輸出都是終端。因此我修改了源碼,將輸入和輸出分別通過read /tmp/input
和write /tmp/output
文件實現。在功能實現上,我們只需要將設置參數的命令寫入 tmp/input
,獲取設備參數從tmp/output
中獲取即可。這裏發一下初版修改內容:
int telnet_main(int argc UNUSED_PARAM, char **argv)
{
char *host;
int port;
int len;
struct pollfd ufds[2];
INIT_G();
#if ENABLE_FEATURE_TELNET_WIDTH
get_terminal_width_height(0, &G.win_width, &G.win_height);
#endif
#if ENABLE_FEATURE_TELNET_TTYPE
G.ttype = getenv("TERM");
#endif
if (tcgetattr(0, &G.termios_def) >= 0) {
G.do_termios = 1;
G.termios_raw = G.termios_def;
cfmakeraw(&G.termios_raw);
}
#if ENABLE_FEATURE_TELNET_AUTOLOGIN
if (1 == getopt32(argv, "al:", &G.autologin)) {
/* Only -a without -l USER picks $USER from envvar */
G.autologin = getenv("USER");
}
argv += optind;
#else
argv++;
#endif
if (!*argv)
bb_show_usage();
host = *argv++;
port = bb_lookup_port(*argv ? *argv++ : "telnet", "tcp", 23);
if (*argv) /* extra params?? */
bb_show_usage();
xmove_fd(create_and_connect_stream_or_die(host, port), netfd);
setsockopt_keepalive(netfd);
signal(SIGINT, record_signo);
#if 0
ufds[0].fd = STDIN_FILENO;
ufds[0].events = POLLIN;
ufds[1].fd = netfd;
ufds[1].events = POLLIN;
#else
ufds[1].fd = STDIN_FILENO;
ufds[1].events = POLLIN;
ufds[0].fd = netfd;
ufds[0].events = POLLIN;
int fdin = open("/tmp/input",O_RDWR|O_CREAT);
int fdout = open("/tmp/output",O_RDWR|O_CREAT);
#endif
while (1) {
if (poll(ufds, 1, 500) < 0) {
/* error, ignore and/or log something, bay go to loop */
if (bb_got_signal)
con_escape();
else
sleep(1);
continue;
}
// FIXME: reads can block. Need full bidirectional buffering.
#if 0
if (ufds[0].revents) {
len = safe_read(STDIN_FILENO, G.buf, DATABUFSIZE);
if (len <= 0)
doexit(EXIT_SUCCESS);
TRACE(0, ("Read con: %d\n", len));
handle_net_output(len);
}
#else
len = safe_read(fdin,G.buf,DATABUFSIZE);
if(len < 0)
doexit(EXIT_SUCCESS);
else if(len == 0)
{
//sleep(1);
}
else
{
handle_net_output(len);
}
#endif
if (ufds[0].revents) {
len = safe_read(netfd, G.buf, DATABUFSIZE);
if (len <= 0) {
full_write1_str("Connection closed by foreign host\r\n");
doexit(EXIT_FAILURE);
}
TRACE(0, ("Read netfd (%d): %d\n", netfd, len));
handle_net_input(fdout,len);
}
} /* while (1) */
}
linux 轉義序列ansi
對於轉義序列大家可能不太清楚,可參考下面兩圖 ,比較直觀
很明顯文本的內容對於我們而言就是亂碼,但是終端能夠實現對這些ansi轉義序列進行解釋,故能過優美的解釋。但是對於我們web端,它的屬性時text,是不能進行解釋的,顯示出來的就是亂碼。
方案一: 讓web也能夠識別轉義序列
這種解決方案是最好的,我嘗試使用比較火的xterm.js插件,的確可以識別部分的轉義序列,比如可以識別字體顏色。但是還是有部分不能識別,會造成部分亂碼。由於我對前端也不是很瞭解,所以我也沒有深究
方案二:過濾文本中的轉義序列
這個解決方案也是無奈之舉,經過好幾天的糾結還是選擇了這個方式。通過二進制查看文本內容,發現了一些規律,就寫了一個過濾demo。沒想到效果還不錯,就選擇的這一方案。
下面爲過濾demo:
#include<stdlib.h>
#include<stdio.h>
#include<fcntl.h>
#define MAXLEN 1024*8
int main()
{
char *input = "./output";
char buff[MAXLEN] = {0};
char out[MAXLEN] = {0};
int fd = open(input,O_RDWR);
if(fd < 0 )
printf("open output is error");
int len = read(fd,buff,MAXLEN);
char* p = buff;
int i = 0;
while (len--)
{
if(*p == 0x00 || *p == 0x1b)
{
p++;
}
else if(*p == '[' && *(p-1) == 0x1b)
{
while(1)
{
if(*p == 'm' || *p=='K')
{
p++;
break;
}
else
p++;
}
}
else if(*p == 0x0d && *(p+1) == '[')
{
p++;
while(1)
{
if(*p == 0x0d)
{
p++;
break;
}
else
p++;
}
}
else if(*p == 0x0d)
{
p++;
}
else
{
out[i++]=*p;
p++;
}
}
write(fd,out,i);
close(fd);
return 0;
以上便是項目中可能比較麻煩的地方,其它的工作基本沒有什麼難度。也正因爲難度不大,所以不失爲一個好方案啊,希望能夠給你帶來幫助。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
補充:2020-03-12
優化telent頁面
優化web頁面
在之前的web顯示中,我曾嘗試使用xterm.js來實現,但由於瞭解不深,遇到問題就沒有繼續。採用了textarea
,但事實證明偷懶的效果就是醜,並且效果使用也不方便。咱也不多說,直接上圖。
這樣看起是不是覺得很low?並且使用起來很麻煩,你需要在上面的input框中輸入命令,再點擊submit才能正常交互。要是一個前端工作者看到,肯定要把我凌遲。(太不人性化了)。
這兩天沒啥事(主要是因爲我的效率比較高,哈哈),再搗鼓搗鼓一下xterm.js,通過查看官網的文檔,進行修改,也就能正常使用了,並且使用效果還不錯。(遇到困難還是要堅持一下呢)
上圖:
看起來肯定比上面的要簡潔,並且使用起來更加人性化。主要修改的代碼就是htm文件,後臺沒有修改。僅供參考:
<%#
Copyright 2008 Steven Barth <[email protected]>
Copyright 2008-2015 Jo-Philipp Wich <[email protected]>
Licensed to the public under the Apache License 2.0.
-%>
<%+header%>
<div class="cbi-section">
<link rel="stylesheet" href="/luci-static/xterm/xterm.css"/>
<script src="/luci-static/xterm/xterm.js"></script>
<h2 name="content">Telnet</h2>
<textarea style="display:none;" id="showlog"><%=showlog:pcdata()%></textarea>
<form class="inline" id="inputcommand" method="post" action="<%=url('admin/telnet/submit')%>" enctype="multipart/form-data">
<textarea style="display:none;" class="command-textarea" style="width: 60%" name="command" id="cbid.luci.command" rows="1"></textarea>
<input type="hidden" name="token" value="<%=token%>" />
<input type="hidden" class="cbi-button cbi-button-action important" name="restore" value="<%:submit%>" />
</form>
<div class="cbi-section-descr">This is a web telnet.</div>
<div id="terminal"></div>
</div>
<script type="text/javascript">
var term = new window.Terminal.Terminal();
term.open(document.getElementById('terminal'));
function runFakeTerminal() {
if (term._initialized) {
return;
}
term._initialized = true;
var showlog= document.getElementById('showlog').value
var logs= showlog.split("\n")
for (i=0; i<logs.length-1; i++ ){
term.writeln(logs[i]); //分割後的字符輸出
}
term.write(logs[i]);
prelen = logs[i].length;
var command = ""
term.onKey(e => {
const printable = !e.domEvent.altKey && !e.domEvent.altGraphKey && !e.domEvent.ctrlKey && !e.domEvent.metaKey;
if (e.domEvent.keyCode === 13) {
if(command === "?") //這裏是後臺對?不識別問題,這裏做一個容錯
command = "??"
document.getElementById("cbid.luci.command").value=command;
document.getElementById("inputcommand").submit();
} else if (e.domEvent.keyCode === 8) {
// Do not delete the prompt
if (term._core.buffer.x > prelen) {
term.write('\b \b');
}
} else if(e.domEvent.keyCode === 37 || e.domEvent.keyCode === 38 || e.domEvent.keyCode === 39 || e.domEvent.keyCode === 40) {
}
else if (printable) {
term.write(e.key);
command = command + e.key
}
});
}
function prompt(term) {
term.write('\r\n$ ');
}
runFakeTerminal();
</script>
<%+footer%>