为在telnet自己TCP服务器程序的界面上实现shell一样的自动补齐和历史记录的功能。
为在telnet自己TCP服务器程序的界面上实现shell一样的自动补齐和历史记录的功能。
2010年07月01日
希望在telnet自己TCP服务器程序的界面上实现shell一样的自动补齐和历史记录的功能。 程序的远程登陆的telnet界面通常是通过一个TCP服务器来实现的,但是如果想在这个TCP服务器端实现客户端登陆界面的自动补齐和历史记录的功能会有如下的问题:
(1)常见的telnet客户端是以行模式发送数据的,即输入一个字符串后再按一个回车,整个数据才会被发送到服务器端。
(2)常见的telnet客户端是自动回显的,即你在键盘上输入一个字符后,客户端自己将这个字符显示在客户端界面上,而不是显示的从服务器端发送过来的数据。
默认这样做的原因,是为了简化了客户端和服务器端的实现。但是如果希望能够让客户端能够具有shell一样的自动补齐和历史记录功能则无法实现,原因如下: (1)自动补齐功能一般是按tab键后按现有输入的部分进行匹配的,不能等按回车,因为按回车一般表示客户端输入完成,等待服务器处理的结果。应该是按下每个按键都发送一次。
(2)历史记录功能一般有按"↑"和"↓"键来实现对记录上翻和下翻的功能,而如上翻功能在按下"↑"键后应用上条历史记录来替代当前行输入的值,这就需要抹除当前行已有的字符,这点在自动回显的功能中很难做到。换句话说,最好由服务器端完全控制客户端的显示,不要让客户端自动显示任何东西。
按照telnet协议的规定,详见附录1,将回显功能关闭,将输入模式从逐行模式改为逐个字符模式。具体参考命令参考大全。
echo
在输入字符的本地屏幕显示与禁止本地屏幕显示间切换。本地屏幕显示用于正常处理,而禁止屏幕显示便于输入不宜显示在屏幕上的文本,如密码。该变量仅可用于逐行方式
输入方式
当带参数发出 telnet 命令时,它执行包含这些参数的open子命令,然后进入输入方式。输入方式的类型为逐个字母或逐行,取决于远程系统所支持的是什么。在逐个字母方式下,大部分输入的文本会立即送到远程主机处理。在逐行方式下,所有文本在本地屏幕显示,然后将完整的行发送到远程主机。
这样做增加了客户端和服务端实现的工作量,尤其是服务器端,需要对接收的字符进行拼装处理,同时要控制客户端的显示。主要实现功能如下:
(1)实现对客户端显示的控制功能,因为客户端不再自动显示,一些原有的功能现在需要由服务器来完成,比如删除一个字符,现在服务器需要三步完成:1.发送指令让客户端的光标向前移动一个字符à2.向客户端发送一个空格à3.再发送指令让客户端的光标向前移动一个字符。
(2)在服务器端对接收到的数据进行缓存和管理,根据收到的数据对前面的数据做对应的处理。如收到一个删除字符就要将缓存的字符串最后一个字符删除并将长度减一并进行(1)提到的操作。
(3)实现自动补齐算法,主要包括两种情况:A.如果和现有输入匹配的命令只有一个,直接用其在当前行替代已有输入。B.如果和现有输入匹配的命令有多个,一般是在当前行下面将这多个命令显示。
(4)对历史记录的管理。
整体实现参考了zebra源代码部分的telnet部分的实现,但是zebra源码实现的较为复杂,功能虽强,但超出我当前的需求代价太大,故参考简化实现。只在linux下的telnet测试通过,windows下可能有点小问题。具体的怎样将命令执行参考asterisk源码中cli的实现简化实现,感觉asterisk的cli的设计思路更加清晰些,但是只支持本地显示,不能telnet。
大概代码如下,省略部分
#define TEL_MAXHIST 20
#define TEL_COM_LEN 100
typedef struct tel_vty
{
/* socket fd */
int fd;
/* 命令接受缓冲区 */
char buf[TEL_COM_LEN];
/* 命令当前指针,主要用于左右移动等操作需要记录当前在操作字符串的那个位置 */
int cp;
/* 命令长度 */
int length;
/* 命令历史记录数组,准备循环使用,当未到达最大长度时每个新的节点都插入在hp之后的位置,数组最后一个位置的下一个位置是第一个位置 */
char *hist[TEL_MAXHIST];
/* 当前历史记录指针,主要用于在历史记录中上下翻时记录当前指向的历史记录 */
int hp;
/* 历史插入的结尾指针 */
int hindex;
}tel_vty;
/*------------------------------------------------ -------------------------*/
/* TELNET 协议处理 */
/*------------------------------------------------ -------------------------*/
char telnet_backward_char = 0x08;//用与控制客户端光标向前移动一个字符
char telnet_space_char = ' ';
//向客户端发送值
void vty_out(tel_vty *p_tel_vty, char cmd[], int len)
//清空当前buf
void vty_clear(tel_vty *p_tel_vty)
{
memset(p_tel_vty->buf, 0x00, p_tel_vty->length);
p_tel_vty->length=0;
}
/*使客户端不要自动回显*/
void vty_will_echo(tel_vty *p_tel_vty)
{
char cmd[] = { IAC, WILL, TELOPT_ECHO, '\0' };
vty_out(p_tel_vty, cmd, sizeof(cmd));
}
/* 使用suppress Go-Ahead telnet选项. */
static void vty_will_suppress_go_ahead(tel_vty *p_tel_vty)
{
char cmd[] = { IAC, WILL, TELOPT_SGA, '\0' };
vty_out(p_tel_vty, cmd, sizeof(cmd));
}
/* 使用逐个字符显示的输入方式. */
static void vty_dont_linemode(tel_vty *p_tel_vty)
{
char cmd[] = { IAC, DONT, TELOPT_LINEMODE, '\0' }; vty_out(p_tel_vty, cmd, sizeof(cmd)); } /* Use window size. */ static void vty_do_window_size(tel_vty *p_tel_vty) { char cmd[] = { IAC, DO, TELOPT_NAWS, '\0' }; vty_out(p_tel_vty, cmd, sizeof(cmd)); } //使客户端光标向前移动一个字符
static void vty_backward_char(tel_vty *p_tel_vty)
{
if(p_tel_vty->length > 0)
{
p_tel_vty->buf[p_tel_vty->length--]='\0';
vty_out(p_tel_vty, &telnet_backward_char, 1);
}
}
//发送空格字符
static void vty_space_char(tel_vty *p_tel_vty)
//回到当前行的开始
static void vty_beginning_of_line(tel_vty *p_tel_vty)
{
while(p_tel_vty->length)
vty_backward_char(p_tel_vty);
}
//使客户端删除一个字符
static void vty_ec(tel_vty *p_tel_vty)
{
// printf("ec len is %d\n", p_tel_vty->length);
vty_backward_char(p_tel_vty);
vty_space_char(p_tel_vty);
vty_backward_char(p_tel_vty);
}
//使客户端删除当前位置向后的一行
static void vty_kill_line(tel_vty *p_tel_vty)
{
int i=0;
for ( i = 0; ilength = 0;
}
//添加历史记录
static void vty_hist_add(tel_vty *p_tel_vty)
{
int index;
if (p_tel_vty->length == 0)
return;
index = p_tel_vty->hindex?p_tel_vty->hindex - 1: TEL_MAXHIST - 1;
/* 如果和上一次命令相同则忽略 */
if (p_tel_vty->hist[index])
if(strcmp(p_tel_vty->buf, p_tel_vty->hist[index]) == 0)
{
p_tel_vty->hp = p_tel_vty->hindex;
return;
}
if (p_tel_vty->hist[p_tel_vty->hindex]) { free(p_tel_vty->hist[p_tel_vty->hindex]); } p_tel_vty->hist[p_tel_vty->hindex] = strdup(p_tel_vty->buf); p_tel_vty->hindex++; if(p_tel_vty->hindex == TEL_MAXHIST) p_tel_vty->hindex = 0; p_tel_vty->hp = p_tel_vty->hindex; } //显示一行
static void vty_redraw_line(tel_vty *p_tel_vty)
{
vty_out(p_tel_vty, p_tel_vty->buf, p_tel_vty->length);
p_tel_vty->cp = p_tel_vty->length;
}
//打印历史记录
static void vty_histroy_print(tel_vty *p_tel_vty)
{
int length;
vty_kill_line_from_beginning(p_tel_vty);
/* 从历史记录缓存中取得字符窜 */
length = strlen(p_tel_vty->hist[p_tel_vty->hp]);
memcpy(p_tel_vty->buf, p_tel_vty->hist[p_tel_vty->hp], length);
p_tel_vty->cp = p_tel_vty->length = length;
/* 显示此行 */
vty_redraw_line(p_tel_vty);
}
//显示当前记录的下一个历史记录
static void vty_next_line(tel_vty *p_tel_vty)
{
int try_index;
if(p_tel_vty->hp == p_tel_vty->hindex)
return;
/* 寻找下个位置,如果已经在最后,循环到第一位 */
try_index = p_tel_vty->hp;
if (try_index == (TEL_MAXHIST - 1))
try_index = 0;
else
try_index++;
/* 查看此位置有没有记录 */
if (p_tel_vty->hist[try_index] == NULL)
return;
else
p_tel_vty->hp = try_index;
vty_histroy_print(p_tel_vty);
}
//显示当前记录的上一个历史记录
static void vty_previous_line(tel_vty *p_tel_vty)
{
int try_index;
try_index = p_tel_vty->hp;
if (try_index == 0)
{
try_index = TEL_MAXHIST - 1;
}
else
{
try_index--;
}
if (p_tel_vty->hist[try_index] == NULL)
return;
else
p_tel_vty->hp = try_index;
vty_histroy_print(p_tel_vty); } //自动补齐函数
static int vty_complete(tel_vty * p_tel_vty)
{
int fd = p_tel_vty->fd;
char matchstr[80] = "";
struct ngn_cli_entry *e;
struct ngn_cli_entry *match_e;
int len = 0;
int found = 0;
char tmp_out_str[80];
int tmp_showed = 0;
if (p_tel_vty->buf)
{
sprintf(matchstr,"%s",p_tel_vty->buf);
len = strlen(matchstr);
}
NGN_LIST_LOCK(&helpers);
e = NGN_LIST_FIRST(&helpers);
if (e)
{
do{
/* Hide commands that start with '_' */
if (p_tel_vty->buf && strncasecmp(p_tel_vty->buf, e->cmda[0], len))
continue;
if (found == 0)
{
sprintf(tmp_out_str, "%25.20s %s\n\r", e->cmda[0], S_OR(e->summary,""));
}
match_e = e;
found++;
if (found > 1)//多于一个匹配时的处理
{
if (tmp_showed == 0)
{
ngn_cli(fd, "\r\n");
ngn_cli(fd, tmp_out_str);
tmp_showed = 1;
}
ngn_cli(fd, "%25.20s %s\n\r", e->cmda[0], S_OR(e->summary,""));
}
}while(e = NGN_LIST_NEXT(e, list));
}
/* 找到唯一匹配 */
if ((found == 1))
{
e = match_e;
vty_kill_line_from_beginning(p_tel_vty);
memset(p_tel_vty->buf, 0x00, p_tel_vty->length);
memcpy(p_tel_vty->buf,e->cmda[0], strlen(e->cmda[0]));
p_tel_vty->length = strlen(e->cmda[0]);
vty_redraw_line(p_tel_vty);
}
else if (found > 1)//如果多于一个,其实已经在上面处理过了
{
vty_clear(p_tel_vty);
ngn_cli(fd, "\r\n->");
}
NGN_LIST_UNLOCK(&helpers); if (!found && matchstr[0]) { return RESULT_FAILURE; } return RESULT_SUCCESS; } //执行一个命令
static void vty_excute(tel_vty *p_tel_vty)
{
char recv_buf[MAX_CLI_LEN];
memset(recv_buf, 0x00, MAX_CLI_LEN);
ast_cli(p_tel_vty->fd, "\r\n");
vty_hist_add(p_tel_vty);
memcpy(recv_buf, p_tel_vty->buf, p_tel_vty->length);
ast_cli_command(p_tel_vty->fd,(const char *)recv_buf);//具体执行一个命令,这部分另外参考astersik源码实现。
memset(p_tel_vty->buf, 0x00 , p_tel_vty->length);
p_tel_vty->length = 0;
}
//初始化结构
static void init_tel_vty(tel_vty *p_tel_vty)
{
memset(p_tel_vty->buf, 0x00, TEL_COM_LEN);
p_tel_vty->cp = 0;
p_tel_vty->length = 0;
for ( int i = 0; ihist[i] = NULL;
}
p_tel_vty->hp = 0;
p_tel_vty->hindex = 0;
}
/* 主函数
*/
#define CONTROL_TAB 0X09
#define CONTROL_DEL 0X7F
void *processDebugThread(void *arg)
{
…
Int connfd = *((int *)arg);
/*设置终端 */
tel_vty tel_vty;
tel_vty.fd = connfd;
init_tel_vty(&tel_vty);
vty_will_echo(&tel_vty);
vty_will_suppress_go_ahead(&tel_vty);
/* 上面的处理严格的话要检查客户端的返回值,是OK才表明客户端支持这样设置,但对我的应用环境暂不需要 */
vty_dont_linemode(&tel_vty);
vty_do_window_size(&tel_vty);
const char *wel =
"--------Hello, you are entering the ren911's debug cmd terminal, welcome-------\n";
write(connfd, wel, strlen(wel));
write(connfd, "\r->", 3);
while ((n = read(connfd, buf, MAX_CLI_LEN)) > 0) {
{
/* 是特殊键,以^]]开头的键值( 如向上键是^]]A ) */
if (3 == n)
{
switch(buf[2]) {
case CONTROL_UP:
{
vty_previous_line(&tel_vty);
}
break; case CONTROL_DOWN: { vty_next_line(&tel_vty); } break; } } /* 主要处理的是回车换行"\r\n" */
else if ( 2 == n )
{
switch(buf[0]) {
case '\r':
{
if (tel_vty.length > 0)
{
vty_excute(&tel_vty);
}
vty_clear(&tel_vty);
ngn_cli(connfd, "\r\n->");
}
}
}
/* 单个字符的处理和一些特殊键的处理,例如删除键和TAB键 */
else if ( 1 == n )
{
switch (buf[0]) {
case CONTROL_DEL:
{
vty_ec(&tel_vty);
}
break;
case CONTROL_TAB:
{
vty_complete(&tel_vty);
}
break;
default:
{
ngn_cli(connfd, "%c", buf[0]);
tel_vty.buf[tel_vty.length++] = buf[0];
}
}
}
}
}
…
}
5 相关参考
1. telnet协议介绍http://support.microsoft.com/kb/231866/en-us/
2. zebra Zebra 是一个开源的 TCP/IP 路由软件,同 Cisco Internet 网络操作系统(IOS)类似。详见http://www.zebra.org/
3. asterisk 是一个开源的软PBX软件。详见http://www.asterisk.org/