C/S架构的Telnet工具实现的主要思想是:界面组件捕获键盘事件,经过过滤、转换将消息发送到服务端,服务通过Telnet第三方开发包,将消息发送给Telnet服务端(多是支持Telnet的设备),然后将设备返回的信息返回到客户端,客户端进行处理后显示在界面。
同样在B/S中要实现Telnet/SSH功能,也需要实现类似C/S界面组件功能,这个组件不仅要过滤输入字符,而且要根据Telnet协议要求实现了键盘输入字符和发送字符命令之间的转换以及设备返回内容的处理(格式处理,字符替换)。
通过相关资料的搜索,发现网上有一个叫termlib的javascript组件(http://www.masswerk.at/termlib),这个js文件就是用javascript实现了一个命令行终端组件,虽然代码仅仅几千行,但是几乎实现了命令行终端的所有功能,提供了相关的控制API。因此可以使用该组件来实现Web版的Telnet工具。
Termlib可以使用在命令终端、小游戏界面等相关的应用场景,其中著名的PC机模拟项目——Javascript实现(http://www.bellard.org/jslinux/)模拟PC机,界面的实现过程也参考了termlib。termlib的最简单demo可以看termlib网站的demo和帮助。
Termlib使用分几种模式:默认模式、字符模式、Raw模式、文件模式。默认的是命令模式,是以回车键标志命令事件,捕获事件后回调使用者handler。其构造函数可以定制term相关的参数。
分析网站中的demo发现,使用默认模式来实现Web版本的Telnet是最好的办法。但是还需要解决如下几个关键问题:
1、 从C/S的Telnent工具实现来看,我们必须捕获每个键盘事件,并且发命令给设备,即发送命令不是以回车键为标识。
2、 即使捕获键盘事件,但是每个键盘(尤其是控制键)点击时间要发送什么样的命令需要我们特殊实现,termlib不提供这种处理,因为termlib仅仅做界面处理,并不是针对Telnet来实现的。
3、 对于设备返回的信息需要进行处理后再让termlib显示。
4、 由于Telnet连接过程中有可能超时和异常,因此Web服务端需要向浏览器push这些信息。
5、 Telnet连接需要进行额外的管理。
1、 如何捕获原始的键盘敲击事件?
通过覆盖prototype方法keyHandler,将我们的事件处理替换原有的keyHandler事件处理,从而捕获原始的键盘点击事件:
function customTerm(term){
Terminal.prototype.globals.keyHandler=function(e) {
。。。。。。
};
}
2、 如何处理键盘事件?
通过分析Telnet的协议处理,将键盘事件归类如下
键盘 |
给设备的命令字节 |
回显处理 |
Entry |
键盘Code(13) |
需要处理“删除”字符 |
ESC |
键盘Code |
直接关闭窗口 |
LEFT |
27,91,68 |
根据telnet协议特殊处理 |
RIGHT |
27,91,67 |
根据telnet协议特殊处理 |
UP |
27,91,65 |
根据telnet协议特殊处理 |
DOWN |
27,91,66 |
根据telnet协议特殊处理 |
BS |
8 |
根据telnet协议特殊处理 |
DEL |
127 |
根据telnet协议特殊处理 |
TAB |
键盘Code |
根据telnet协议特殊处理 |
空格 |
键盘Code |
需要处理“删除”字符 |
可打印字符 |
字符Code |
需要处理“删除”字符 |
Ctrl+Z |
字符Code(26) |
根据telnet协议特殊处理 |
Ctrl+V |
字符Code(22) |
根据telnet协议特殊处理 |
其他特殊字符 |
不发送返回 |
不支持 |
3、 对于设备返回的信息如何处理?
将命令分类,分析规律,根据需要处理回显字符,然后再根据需要现实,回显处理可以参考上表,具体还需要在实现的时候进一步分析
4、 服务端的信息的push方法? jquery + cometd
5、 如何进行telnet链接管理?
根据http session和Telnet服务端(设备)关键字,在业务层提供统一的Telnent连接管理,实现过程要考虑并发性问题。
这个demo中实现了web版telnet工具的基本功能,很多特殊处理还需要在实际实现中完善,界面组织、用户接入等等问题也都需要在实际问题中解决。
1、演示效果如下:
2、demo中的web层代码如下:
html层
termlib Socket Sample
js 层
var term;
var help = [];
function termOpen()
{
if ((!term) || (term.closed))
{
term = new Terminal(
{
cols:100,
rows:36,
x: 220,
y: 70,
blinkDelay:1000,
crsrBlinkMode:true,
ps:'',
termDiv: 'termDiv',
bgColor: '#232e45',
greeting: help.join('\n'),
handler: termHandler,
exitHandler: myExitHandler
}
);
customTerm(term);
term.open();
term.currentEvent=KeyEventDetail.prototype.getNomalDetail(10000);
term.handler();
}
}
function customTerm(term){
Terminal.prototype.globals.keyHandler=function(e) {
var tg=Terminal.prototype.globals;
var term=tg.activeTerm;
if (tg.keylock || term.lock || term.isMac && e && e.metaKey) return true;
if (window.event) {
if (window.event.preventDefault) window.event.preventDefault();
if (window.event.stopPropagation) window.event.stopPropagation();
}
else if (e) {
if (e.preventDefault) e.preventDefault();
if (e.stopPropagation) e.stopPropagation();
}
var ch;
var ctrl=false;
var shft=false;
var remapped=false;
var termKey=term.termKey;
var keyRepeat=0;
if (e) {
ch=e.which;
ctrl=((e.ctrlKey && !e.altKey) || e.modifiers==2);
shft=(e.shiftKey || e.modifiers==4);
if (e._remapped) {
remapped=true;
if (window.event) {
//ctrl=(ctrl || window.event.ctrlKey);
ctrl=(ctrl || (window.event.ctrlKey && !window.event.altKey));
shft=(shft || window.event.shiftKey);
}
}
if (e._repeated) {
keyRepeat=2;
}
else if (e._repeat) {
keyRepeat=1;
}
}
else if (window.event) {
ch=window.event.keyCode;
//ctrl=(window.event.ctrlKey);
ctrl=(window.event.ctrlKey && !window.event.altKey); // allow alt gr == ctrl alt
shft=(window.event.shiftKey);
if (window.event._repeated) {
keyRepeat=2;
}
else if (window.event._repeat) {
keyRepeat=1;
}
}
else {
return true;
}
if (ch=='' && remapped==false) {
// map specials
if (e==null) e=window.event;
if (e.charCode==0 && e.keyCode) {
if (e.DOM_VK_UP) {
var dkr=tg.termDomKeyRef;
for (var i in dkr) {
if (e[i] && e.keyCode == e[i]) {
ch=dkr[i];
break;
}
}
}
else {
// NS4
if (e.keyCode==28) { ch=termKey.LEFT; }
else if (e.keyCode==29) { ch=termKey.RIGHT; }
else if (e.keyCode==30) { ch=termKey.UP; }
else if (e.keyCode==31) { ch=termKey.DOWN; }
// Mozilla alike but no DOM support
else if (e.keyCode==37) { ch=termKey.LEFT; }
else if (e.keyCode==39) { ch=termKey.RIGHT; }
else if (e.keyCode==38) { ch=termKey.UP; }
else if (e.keyCode==40) { ch=termKey.DOWN; }
// just to have the TAB mapping here too
else if (e.keyCode==9) { ch=termKey.TAB; }
}
}
}
// leave on unicode private use area (might be function key etc)
if ((ch>=0xE000) && (ch<= 0xF8FF)) return;
if (keyRepeat) {
tg.clearRepeatTimer();
tg.keyRepeatTimer = window.setTimeout(
'Terminal.prototype.globals.doKeyRepeat('+ch+')',
(keyRepeat==1)? tg.keyRepeatDelay1:tg.keyRepeatDelay2
);
}
if (!ctrl) {
// special keys
if (ch==termKey.CR) {//entry*********************************************************************************************************************
term.lock=true;
term.cursorOff();
term.insert=false;
term.lineBuffer=term._getLine(true);
if (
term.lineBuffer!='' &&
(!term.historyUnique || term.history.length==0 ||
term.lineBuffer!=term.history[term.history.length-1])
) {
term.history[term.history.length]=term.lineBuffer;
}
term.histPtr=term.history.length;
term.lastLine='';
term.inputChar=0;
term.currentEvent=KeyEventDetail.prototype.getSpeciclDetail(ch);
term.handler();
term.lock=false;
term.cursorOn();
term.insert=true;
if (window.event) window.event.cancelBubble=true;
return false;
}
else if (ch==termKey.ESC && term.conf.closeOnESC) {//ESC********************************************************************************
term.close();
if (window.event) window.event.cancelBubble=true;
return false;
}
{
if (ch==termKey.LEFT) {//LEFT*****************************************************************************************************************
//term.cursorLeft();
term.currentEvent=KeyEventDetail.prototype.getSpeciclDetail(ch);
term.handler();
//no need if add follow ,curcor move becaome slow
//term.lock=false;
//term.cursorOn();
//term.insert=true;
if (window.event) window.event.cancelBubble=true;
return false;
}
else if (ch==termKey.RIGHT) {
//term.cursorRight();
term.currentEvent=KeyEventDetail.prototype.getSpeciclDetail(ch);
term.handler();
if (window.event) window.event.cancelBubble=true;
return false;
}
else if (ch==termKey.UP) {
term.cursorOff();
/**
if (term.histPtr==term.history.length) term.lastLine=term._getLine();
term._clearLine();
if (term.history.length && term.histPtr>=0) {
if (term.histPtr>0) term.histPtr--;
term.type(term.history[term.histPtr]);
}
else if (term.lastLine) {
term.type(term.lastLine);
}
*/
term.currentEvent=KeyEventDetail.prototype.getSpeciclDetail(ch);
term.handler();
term.cursorOn();
if (window.event) window.event.cancelBubble=true;
return false;
}
else if (ch==termKey.DOWN) {
term.cursorOff();
/**
if (term.histPtr==term.history.length) term.lastLine=term._getLine();
term._clearLine();
if (term.history.length && term.histPtr<=term.history.length) {
if (term.histPtr=65 && ch<=96) || ch==63) {
// remap canonical
if (ch==63) {
ch=31;
}
else {
ch-=64;
}
}
term.inputChar=ch;
//term.ctrlHandler();
term.currentEvent=KeyEventDetail.prototype.getSpeciclDetail(0);
term.handler();
if (window.event) window.event.cancelBubble=true;
return false;
}
else if (ctrl || !term.isPrintable(ch,true)) {//Ctrl printable key ************************************************************************************************
if(ctrl && ch==122||ctrl && ch==90){//Ctrl z
term.currentEvent=KeyEventDetail.prototype.getSpeciclDetail(26);
}else if(ctrl && ch==118||ctrl && ch==86){//Ctrl v
term.currentEvent=KeyEventDetail.prototype.getSpeciclDetail(22);
}else{
term.currentEvent=KeyEventDetail.prototype.getSpeciclDetail(0);
}
term.handler();
if (window.event) window.event.cancelBubble=true;
return false;
}
else if (term.isPrintable(ch,true)) {//printable key*****************************************************************************************************************
if (term.blinkTimer) clearTimeout(term.blinkTimer);
/**
if (term.insert) {
term.cursorOff();
term._scrollRight(term.r,term.c);
}
term._charOut(ch);
term.cursorOn();
*/
if (ch==32 && window.event) {
window.event.cancelBubble=true;
}
else if (window.opera && window.event) {
window.event.cancelBubble=true;
}
term.currentEvent=KeyEventDetail.prototype.getNomalDetail(ch);
term.handler();
term.lock=false;
term.cursorOn();
term.insert=true;
return false;
}
}
return true;
};
}
function termHandler() {
var keyEvent=this.currentEvent;
var termpCharCode=0;
var sendStr="0";
if(keyEvent){
termpCharCode=keyEvent.inputchar;
sendStr=String(termpCharCode);
}else{
return;
}
if(keyEvent.isSpecial){
if(keyEvent.isSupport){
sendStr=keyEvent.getSendCmd();
}else{
return ;
}
}
var myDataObject = {
charCode: sendStr,
ipaddress: "10.46.60.69"
};
this.send(
{
url: "demoApp/easyCRT.action",
method: 'get',
callback: myServerCallback,
data: myDataObject
}
);
}
function myServerCallback()
{
var response=this.socket;
if (response.success)
{
var func=null;
try{
func=eval(response.responseText);
}catch (e){
}
if (typeof func=='function'){
try{
func.apply(this);
}catch(e){
this.write('An error occured within the imported function: '+e);
}
}else{
var respText=response.responseText;
var temptg=Terminal.prototype.globals;
var tempterm=temptg.activeTerm;
var keyEvent=tempterm.currentEvent;
if(keyEvent.isSpecial&&keyEvent.isSupport){
var flag=keyEvent.inputchar;
switch (flag){
case Terminal.prototype.globals.termKey.CR:
break;
case Terminal.prototype.globals.termKey.LEFT:
leftkey(tempterm,respText);
return;
break;
case Terminal.prototype.globals.termKey.RIGHT:
rightkey(tempterm,respText);
return;
break;
case Terminal.prototype.globals.termKey.UP:
break;
case Terminal.prototype.globals.termKey.DOWN:
break;
case Terminal.prototype.globals.termKey.BS:
break;
case KeyEventDetail.prototype.specialKeysSupported.CTRLZ:
break;
case KeyEventDetail.prototype.specialKeysSupported.CTRLV:
break;
default:
;
}
}
respText=keyEvent.preResponText(respText,tempterm);
var text=HelperUtil.prototype.removeCharbyDelIndex(respText);
this.write(respText);
}
}
else
{
var s='Request failed: ' + response.status + ' ' + response.statusText;
if (response.errno) s += '\n' + response.errstring;
this.write(s);
}
}
function leftkey(tempterm,respText){
if(respText){
tempterm.cursorLeft();
}
}
function rightkey(tempterm,respText){
if(respText){
tempterm.cursorRight();
}
}
function myExitHandler()
{
var mainPane = (document.getElementById)?
document.getElementById('mainPane') : document.all.mainPane;
if (mainPane) mainPane.className = 'lh15';
}
function KeyEventDetail(){
this.inputchar='';
this.isSpecial=false;
this.isSupport=true;
this.isControl=false;
this.isShift=false;
this.getSendCmd=function(){
var sendStrTemp=String(this.inputchar);
if(this.isSpecial){
var flag=this.inputchar;
switch (flag){
case Terminal.prototype.globals.termKey.CR:
sendStrTemp=String(this.inputchar);
break;
case Terminal.prototype.globals.termKey.LEFT:
sendStrTemp="27,91,68";
break;
case Terminal.prototype.globals.termKey.RIGHT:
sendStrTemp="27,91,67";
break;
case Terminal.prototype.globals.termKey.UP:
sendStrTemp="27,91,65";
break;
case Terminal.prototype.globals.termKey.DOWN:
sendStrTemp="27,91,66";
break;
case Terminal.prototype.globals.termKey.BS:
sendStrTemp="8";
break;
case Terminal.prototype.globals.termKey.DEL:
sendStrTemp="127";
break;
case KeyEventDetail.prototype.specialKeysSupported.CTRLZ:
sendStrTemp="26";
break;
case KeyEventDetail.prototype.specialKeysSupported.CTRLV:
sendStrTemp="22";
break;
default:
;
}
}
return sendStrTemp;
};
this.preResponText=function(respText,term){
if(this.isSpecial&&this.isSupport){
var flag=this.inputchar;
switch (flag){
case Terminal.prototype.globals.termKey.CR:
break;
case Terminal.prototype.globals.termKey.LEFT:
break;
case Terminal.prototype.globals.termKey.RIGHT:
break;
case Terminal.prototype.globals.termKey.UP:
break;
case Terminal.prototype.globals.termKey.DOWN:
break;
case Terminal.prototype.globals.termKey.BS:
break;
case Terminal.prototype.globals.termKey.DEL:
break;
case KeyEventDetail.prototype.specialKeysSupported.CTRLZ:
break;
case KeyEventDetail.prototype.specialKeysSupported.CTRLV:
break;
default:
;
}
}
return respText;
};
}
KeyEventDetail.prototype={
specialKeysSupported:{
'CR': Terminal.prototype.globals.termKey.CR,
'LEFT': Terminal.prototype.globals.termKey.LEFT,
'RIGHT': Terminal.prototype.globals.termKey.RIGHT,
'UP': Terminal.prototype.globals.termKey.UP,
'DOWN': Terminal.prototype.globals.termKey.DOWN,
'BS': Terminal.prototype.globals.termKey.BS,
'DEL': Terminal.prototype.globals.termKey.DEL,
'CTRLZ': 26,
'CTRLV': 22
},
getNomalDetail:function(charValue){
var currentEvent=new KeyEventDetail();
currentEvent.inputchar=charValue;
return currentEvent;
},
getSpeciclDetail:function(charValue){
var detail= KeyEventDetail.prototype.getDefaultSpecialDetail(charValue);
switch (charValue){
case Terminal.prototype.globals.termKey.CR:
detail=KeyEventDetail.prototype.getEnterDetail();
break;
case Terminal.prototype.globals.termKey.LEFT:
detail=KeyEventDetail.prototype.getLeftDetail();
break;
case Terminal.prototype.globals.termKey.RIGHT:
detail=KeyEventDetail.prototype.getRightDetail();
break;
case Terminal.prototype.globals.termKey.UP:
detail=KeyEventDetail.prototype.getUpDetail();
break;
case Terminal.prototype.globals.termKey.DOWN:
detail=KeyEventDetail.prototype.getDownDetail();
break;
case Terminal.prototype.globals.termKey.BS:
detail=KeyEventDetail.prototype.getBsDetail();
break;
case Terminal.prototype.globals.termKey.DEL:
detail=KeyEventDetail.prototype.getDelDetail();
break;
case KeyEventDetail.prototype.specialKeysSupported.CTRLZ:
detail=KeyEventDetail.prototype.getCtrlzDetail();
break;
case KeyEventDetail.prototype.specialKeysSupported.CTRLV:
detail=KeyEventDetail.prototype.getCtrlvDetail();
break;
default:
;
}
return detail;
},
getDefaultSpecialDetail:function(chatValue){
var currentEvent=new KeyEventDetail();
currentEvent.inputchar=chatValue;
currentEvent.isSpecial=true;
currentEvent.isSupport=false;
return currentEvent;
},
getEnterDetail:function(){
var currentEvent=new KeyEventDetail();
currentEvent.inputchar=Terminal.prototype.globals.termKey.CR;
currentEvent.isSpecial=true;
return currentEvent;
},
getLeftDetail:function(){
var currentEvent=new KeyEventDetail();
currentEvent.inputchar=Terminal.prototype.globals.termKey.LEFT;
currentEvent.isSpecial=true;
return currentEvent;
},
getRightDetail:function(){
var currentEvent=new KeyEventDetail();
currentEvent.inputchar=Terminal.prototype.globals.termKey.RIGHT;
currentEvent.isSpecial=true;
return currentEvent;
},
getUpDetail:function(){
var currentEvent=new KeyEventDetail();
currentEvent.inputchar=Terminal.prototype.globals.termKey.UP;
currentEvent.isSpecial=true;
return currentEvent;
},
getDownDetail:function(){
var currentEvent=new KeyEventDetail();
currentEvent.inputchar=Terminal.prototype.globals.termKey.DOWN;
currentEvent.isSpecial=true;
return currentEvent;
},
getBsDetail:function(){
var currentEvent=new KeyEventDetail();
currentEvent.inputchar=Terminal.prototype.globals.termKey.BS;
currentEvent.isSpecial=true;
return currentEvent;
},
getDelDetail:function(){
var currentEvent=new KeyEventDetail();
currentEvent.inputchar=Terminal.prototype.globals.termKey.DEL;
currentEvent.isSpecial=true;
return currentEvent;
},
getCtrlzDetail:function(){
var currentEvent=new KeyEventDetail();
currentEvent.inputchar=KeyEventDetail.prototype.specialKeysSupported.CTRLZ;
currentEvent.isSpecial=true;
return currentEvent;
},
getCtrlvDetail:function(){
var currentEvent=new KeyEventDetail();
currentEvent.inputchar=KeyEventDetail.prototype.specialKeysSupported.CTRLZ;
currentEvent.isSpecial=true;
return currentEvent;
}
}
function HelperUtil(){
}
HelperUtil.prototype={
removeCharbyDelIndex:function(text){
return text;
}
}
3、demo中的服务端代码:略