用Socket编程来实现Telnet协议.pdf
用 Socket 编程来实现 Telnet 协议这因为有个任务涉及到使用 telnet 来连接远端的路由器,获取信息,之后进行处理.所以需要写一个自动 telnet 登录到远端,之后获取信息进行处理的程序.自己 C+一塌糊涂,所以几乎最开始就没打算用 C+或者 C 写论自己的实力,还是走 C#路线稍微稳妥一点吧,因为 telnet 是使用 tcp/ip 协议折腾的事情很容易的想到使用 socket 来实现 telnet(当然你可以在进程里启用 telnet 命令,只不过总觉得那样不够技术,而且操作不自由-受限于 telnet 这个指令)ok,翻协议,弄清原理,结果比预想的难度要大一些定义=Telnet 协议是 TCPIP 协议族中应用最广泛的协议。它允许用户(Telnet 客户端)通过一个协商过程来与一个远程设备进行通信。Telnet 协议是基于网络虚拟终端 NVT(Network Virtual Termina1)的实现,NVT 是虚拟设备,连接双方(客户机和服务器)都必须把它们的物理终端和 NVT 进行相互转换=大概意思就是跟远端通信的一套协议,之后这个协议无视你机器是啥型号,啥样子只要是用 telnet 的,统统都可以看成是 NVT(类似面向对象中的继承关系:NVT 是父类,各种实用 telnet 的都继承与 NVT)好处非常明显,可以无视型号而直接使用标准命令,任何服从 NVT 的设备都能通信当然不可避免的,标准也同时代表着性能的损失:由于 NVT 得顾及到所有的各种型号的机器,所以他定义的 操作十分有限(因为考虑到包括要支持类似9城小*那些性能很差,系统简单的机器),为了解决 NVT 这个为了照顾小*,而导致高端设备的功能不能用的这个弊病,Telnet 琢磨出了一个比较好的解决方案用于扩展基本 NVT 功能的协议,提供了选项协商的机制来解决问题类似那个经典的英国绵羊笑话使用英文描述两只绵羊在路上碰到后发生的故事=绵羊 A:Hi,Sheep!绵羊 B:Hi,Can you speak Chinese?绵羊 A:yes,jin tian chi le ma?(今天吃了吗?)绵羊 B:chi la,hen shuang!(吃啦,很爽!).省略500字改卷的英国人累牛满面,因为他不会中文,但又不能说这篇文章有问题.=这里英文就可以理解为 NVT 的标准功能,为通用语,而后来的中文拼音,就是扩展.ok,原理就是那么回事,讲讲细节吧telnet 来连接的时候,需要发送一系列的指令来协商(绵羊协商)通信,流程图类似这个ok,那么,具体的命令是怎样的呢?很无趣的,就是 telnet 的命令格式IAC命令码选项码一个个的解释.IAC:命令解释符,说白了就是每条指令的前缀都得是它,固定值255(11111111B)命令码:一系列定义:(最常用的250 254 咱加粗表示)名称代码(十进制)描述EOF236文件结束符SUSP237挂起当前进程(作业控制)ABORT238异常中止进程EOR239记录结束符 iSE240自选项结束NOP241无操作DM242数据标记BRK243中断IP244中断进程AO245异常中止输出AYT246对方是否还在运行?EC247转义字符EL248删除行GA249继续进行SB250子选项开始WILL251同意启动(enable)选项WONT252拒绝启动选项DO253认可选项请求DONT254拒绝选项请求选项协商:4种请求1)WILL:发送方本身将激活选项2)DO:发送方想叫接受端激活选项3)WONT:发送方本身想禁止选项4)DONT:发送方想让接受端去禁止选项紧接着就是选项码选项标识名称1回显3抑制继续进行5状态6定时标记24终端类型31窗口大小32终端速度33远程流量控制34行方式36环境变量ok,为了搞掂这个 telnet 链接,我特地装了个 linux 作为 telnet 的链接对象进行 telnet 远程登录之后写了一个恶心的代码来帮助我进行调试额,在写这个程序之前,我搜了将近1天时间的网路发现大多数代码注释的不是太和谐,读起来很难理解所以自己根据网上的一个 win 程序改出了一个 console 的程序同时,我特地花了两天时间,几乎把每一句能写注释的都写了,基本上可以说是我目前注释写的最多的一次代码了,代码特别庞大,就做个窗口放上去了using System;using System.Collections.Generic;using System.Text;using System.Net;using System.Net.Sockets;using System.Collections;namespace ConsoleApplication1public class Program#region 一些 telnet 的数据定义,先没看懂没关系/标志符,代表是一个 TELNET 指令/readonly Char IAC=Convert.ToChar(255);/表示一方要求另一方使用,或者确认你希望另一方使用指定的选项。/readonly Char DO=Convert.ToChar(253);/表示一方要求另一方停止使用,或者确认你不再希望另一方使用指定的选项。/readonly Char DONT=Convert.ToChar(254);/表示希望开始使用或者确认所使用的是指定的选项。/readonly Char WILL=Convert.ToChar(251);/表示拒绝使用或者继续使用指定的选项。/readonly Char WONT=Convert.ToChar(252);/表示后面所跟的是对需要的选项的子谈判/readonly Char SB=Convert.ToChar(250);/子谈判参数的结束/readonly Char SE=Convert.ToChar(240);const Char IS=0;const Char SEND=1;const Char INFO=2;const CharVAR=0;const Char VALUE=1;const Char ESC=2;const Char USERVAR=3;/流/byte m_byBuff=new byte100000;/收到的控制信息/privateArrayList m_ListOptions=newArrayList();/存储准备发送的信息/string m_strResp;/一个 Socket 套接字/private Socket s;#endregion/主函数/static void Main(string args)/实例化这个对象Program p=new Program();/启动 socket 进行 telnet 链接p.doSocket();/启动 socket 进行 telnet 操作/private void doSocket()/获得链接的地址,可以是网址也可以是 IPConsole.WriteLine(ServerAddress:);/解析输入,如果是一个网址,则解析成 ipIPAddress import=GetIP(Console.ReadLine();/获得端口号Console.WriteLine(Server Port:);int port=int.Parse(Console.ReadLine();/建立一个 socket 对象,使用 IPV4,使用流进行连接,使用 tcp/ip 协议s=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);/获得一个链接地址对象(由 IP 地址和端口号构成)IPEndPoint address=new IPEndPoint(import,port);(本文下载自防锈油防锈油文档综合站文档综合站 。转载请说明出处)/*说明此 socket 不是处于阻止模式*msdn 对阻止模式的解释:*=*如果当前处于阻止模式,并且进行了一个并不立即完成的方法调用,*则应用程序将阻止执行,直到请求的操作完成后才解除阻止。*如果希望在请求的操作尚未完成的情况下也可以继续执行,*请将 Blocking 属性更改为 false。Blocking 属性对异步方法无效。*如果当前正在异步发送和接收数据,并希望阻止执行,*请使用 ManualResetEvent 类。*=*/s.Blocking=false;/*开始一个对远程主机连接的异步请求,*因为 Telnet 使用的是 TCP 链接,是面向连接的,*所以此处 BeginConnect 会启动一个异步请求,(本文下载自防锈油防锈油文档综合站文档综合站 。转载请说明出处)*请求获得与 给的 address 的连接*此方法的第二个函数是一个类型为AsyncCallback 的委托*这个 AsyncCallback msdn 给出的定义如下*=*使用 AsyncCallback 委托在一个单独的线程中处理异步操作的结果。A*syncCallback 委托表示在异步操作完成时调用的回调方法。*回调方法采用 IAsyncResult 参数,该参数随后可用来获取异步操作的结果。*=*这个方法里的委托实际上就是 当异步请求有回应了之后,执行委托的方法.*委托里的参数,实际上就是 BeginConnect 的第三个参数,*此处为 socket 本身*(本文下载自防锈油防锈油文档综合站文档综合站 。转载请说明出处)*我比较懒,写了一个匿名委托,实际上跟 AsyncCallback 效果一个样.*/s.BeginConnect(address,delegate(IAsyncResult ar)/*此处为一个匿名委托,*实际上等于*建立一个 AsyncCallback 对象,指定后在此引用一个道理*ok 这里的意义是,*当远程主机连接的异步请求有响应的时候,执行以下语句*/try/获得传入的对象(此处对象是 BeginConnect 的第三个参数)Socket sock1=(Socket)ar.AsyncState;(本文下载自防锈油防锈油文档综合站文档综合站 。转载请说明出处)/*如果 Socket 在最近操作时连接到远程资源,则为 true;否则为 false。*以下是 MSDN 对 Connected 属性的备注信息*=*Connected 属性获取截止到最后的 I/O 操作时 Socket 的连接状态。*当它返回 false 时,表明 Socket 要么从未连接,要么已断开连接。*Connected 属性的值反映最近操作时的连接状态。如果您需要确定连接的当前状态,*请进行非阻止、零字节的 Send 调用。(本文下载自防锈油防锈油文档综合站文档综合站 。转载请说明出处)*如果该调用成功返回或引发 WAEWOULDBLOCK 错误代码(10035),*则该套接字仍然处于连接状态;否则,该套接字不再处于连接状态。*=*/if(sock1.Connected)AsyncCallback recieveData=new AsyncCallback(OnRecievedData);/*此处没再用匿名委托的原因是,*一个匿名委托嵌套一个匿名委托,我自己思路跟不上来了.*ok,这里是当 Connected为 true 时,*使用 BeginReceive 方法*开始接收信息到 m_byBuff(我们在类中定义的私有属性)*以下是 MSDN 对 BeginReceive 的一些说明*=*异步 BeginReceive 操作必须通过调用 EndReceive 方法来完成。*通常,该方法由 callback 委托调用。此方法在操作完成前不会进入阻止状态。*若要一直阻塞到操作完成时为止,请使用 Receive 方法重载中的一个。*若要取消挂起的 BeginReceive,请调用 Close 方法。*=*当接收完成之后,他们就会调用 OnRecievedData 方法*我在 recieveData 所委托的方法 OnRecievedData 中调用了 sock.EndReceive(ar);*/sock1.BeginReceive(m_byBuff,0,m_byBuff.Length,SocketFlags.None,recieveData,sock1);catch(Exception ex)Console.WriteLine(初始化接收信息出错:+ex.Message);,s);/此处是为了发送指令而不停的循环while(true)/发送读出的数据DispatchMessage(Console.ReadLine();/因为每发送一行都没有发送回车,故在此处补上DispatchMessage(rn);/当接收完成后,执行的方法(供委托使用)/private void OnRecievedData(IAsyncResult ar)/从参数中获得给的 socket 对象Socket sock=(Socket)ar.AsyncState;/*EndReceive 方法为结束挂起的异步读取*(貌似是在之前的 beginReceive 收到数据之后,*socket 只是挂起,并未结束)*之后返回总共接收到的字流量*以下是 MSDN 给出的 EndReceive 的注意事项*=*EndReceive 方法完成在 BeginReceive 方法中启动的异步读取操作。*在调用 BeginReceive 之前,需创建一个实现 AsyncCallback 委托的回调方法。*该回调方法在单独的线程中执行并在 BeginReceive 返回后由系统调用。*回调方法必须接受 BeginReceive 方法所返回的 IAsyncResult 作为参数。*在回调方法中,调用 IAsyncResult 的 AsyncState 方法以获取传递给 BeginReceive 方法的状态对象。*从该状态对象提取接收 Socket。在获取 Socket 之后,可以调用 EndReceive 方法以成功完成读取操作,*并返回已读取的字节数。*EndReceive 方法将一直阻止到有数据可用为止。*如果您使用的是无连接协议,则 EndReceive 将读取传入网络缓冲区中第一个排队的可用数据报。*如果您使用的是面向连接的协议,则 EndReceive 方法将读取所有可用的数据,*直到达到 BeginReceive 方法的 size 参数所指定的字节数为止。*如果远程主机使用 Shutdown 方法关闭了 Socket 连接,并且所有可用数据均已收到,*则 EndReceive 方法将立即完成并返回零字节。*若要获取接收到的数据,请调用 IAsyncResult 的 AsyncState 方法,*然后提取所产生的状态对象中包含的缓冲区。*若要取消挂起的 BeginReceive,请调用 Close 方法。*=*/int nBytesRec=sock.EndReceive(ar);/如果有接收到数据的话if(nBytesRec 0)/将接收到的数据转个码,顺便转成 string 型string sRecieved=Encoding.GetEncoding(utf-8).GetString(m_byBuff,0,nBytesRec);/声明一个字符串,用来存储解析过的字符串string m_strLine=;/遍历 Socket 接收到的字符/*此循环用来调整 linux 和 windows 在换行上标记的区别*最后将调整好的字符赋予给 m_strLine*/for(int i=0;i nBytesRec;i+)Char ch=Convert.ToChar(m_byBuffi);switch(ch)case r:m_strLine+=Convert.ToString(rn);break;case n:break;default:m_strLine+=Convert.ToString(ch);break;try/获得转义后的字符串的长度int strLinelen=m_strLine.Length;/如果长度为零if(strLinelen=0)/则返回rn 即回车换行m_strLine=Convert.ToString(rn);/建立一个流,把接收的信息(转换后的)存进 mToProcess 中Byte mToProcess=new BytestrLinelen;for(int i=0;i strLinelen;i+)mToProcessi=Convert.ToByte(m_strLinei);/Process the incoming data/对接收的信息进行处理,包括对传输过来的信息的参数的存取和string mOutText=ProcessOptions(mToProcess);/解析命令后返回 显示信息(即除掉了控制信息)if(mOutText!=)Console.Write(mOutText);/Respond to any incoming commands/接收完数据,处理完字符串数据等一系列事物之后,开始回发数据RespondToOptions();catch(Exception ex)throw new Exception(接收数据的时候出错了!+ex.Message);else/如果没有接收到任何数据的话/输出关闭连接Console.WriteLine(Disconnected,sock.RemoteEndPoint);/关闭 socketsock.Shutdown(SocketShutdown.Both);sock.Close();Console.Write(Game Over);Console.ReadLine();/发送数据的函数/private void RespondToOptions()try/声明一个字符串,来存储 接收到的参数string strOption;/*此处的控制信息参数,是之前接受到信息之后保存的*例如 25525323等等*具体参数的含义需要去查telnet 协议*/for(int i=0;i m_ListOptions.Count;i+)/获得一个控制信息参数strOption=(string)m_ListOptionsi;/根据这个参数,进行处理ArrangeReply(strOption);DispatchMessage(m_strResp);m_strResp=;m_ListOptions.Clear();catch(Exception ers)Console.WriteLine(错错了,在回发数据的时候 +ers.Message);/解析接收的数据,生成最终用户看到的有效文字,同时将附带的参数存储起来/收到的处理后的数据/private string ProcessOptions(byte m_strLineToProcess)string m_DISPLAYTEXT=;string m_strTemp=;string m_strOption=;string m_strNormalText=;bool bScanDone=false;int ndx=0;int ldx=0;char ch;try/把数据从 byte 转化成 stringfor(int i=0;i lensmk)ndx=m_strTemp.Length;/此处为,如果搜寻到 IAC 标记的 telnet 指令,则执行以下步骤if(ndx!=-1)#region 如果存在 IAC 标志位/将 标志位 IAC 的字符 赋值给最终显示文字m_DISPLAYTEXT+=m_strTemp.Substring(0,ndx);/此处获得命令码ch=m_strTempndx+1;/如果命令码是253(DO)254(DONT)521(WILL)252(WONT)的情况下if(ch=DO|ch=DONT|ch=WILL|ch=WONT)/将以 IAC 开头3个字符组成的整个命令存储起来m_strOption=m_strTemp.Substring(ndx,3);m_ListOptions.Add(m_strOption);/将 标志位 IAC 的字符 赋值给最终显示文字m_DISPLAYTEXT+=m_strTemp.Substring(0,ndx);/将处理过的字符串删去string txt=m_strTemp.Substring(ndx+3);m_strTemp=txt;/如果 IAC 后面又跟了个 IAC(255)else if(ch=IAC)/则显示从输入的字符串头开始,到之前的 IAC 结束m_DISPLAYTEXT=m_strTemp.Substring(0,ndx);/之后将处理过的字符串排除出去m_strTemp=m_strTemp.Substring(ndx+1);/如果 IAC 后面跟的是 SB(250)else if(ch=SB)m_DISPLAYTEXT=m_strTemp.Substring(0,ndx);ldx=m_strTemp.IndexOf(Convert.ToString(SE);m_strOption=m_strTemp.Substring(ndx,ldx);m_ListOptions.Add(m_strOption);m_strTemp=m_strTemp.Substring(ldx);#endregion/若字符串里已经没有 IAC 标志位了else/显示信息累加上 m_strTemp 存储的字段m_DISPLAYTEXT=m_DISPLAYTEXT+m_strTemp;bScanDone=true;/输出人看到的信息m_strNormalText=m_DISPLAYTEXT;catch(Exception eP)throw new Exception(解析传入的字符串错误:+eP.Message);return m_strNormalText;/获得 IP 地址/private static IPAddress GetIP(string import)IPHostEntry IPHost=Dns.GetHostEntry(import);return IPHost.AddressList0;#region magic Function/解析传过来的参数,生成回发的数据到 m_strRespprivate void ArrangeReply(string strOption)tryChar Verb;Char Option;Char Modifier;Char ch;bool bDefined=false;/排错选项,无啥意义if(strOption.Length 3)return;/获得命令码Verb=strOption1;/获得选项码Option=strOption2;/如果选项码为 回显(1)或者是抑制继续进行(3)if(Option=1|Option=3)bDefined=true;/设置回发消息,首先为标志位255m_strResp+=IAC;/如果选项码为 回显(1)或者是抑制继续进行(3)=trueif(bDefined=true)#region 继续判断/如果命令码为253(DO)if(Verb=DO)/我设置我应答的命令码为 251(WILL)即为支持 回显或抑制继续进行ch=WILL;m_strResp+=ch;m_strResp+=Option;/如果命令码为 254(DONT)if(Verb=DONT)/我设置我应答的命令码为 252(WONT)即为我也会拒绝启动 回显或抑制继续进行ch=WONT;m_strResp+=ch;m_strResp+=Option;/如果命令码为251(WILL)if(Verb=WILL)/我设置我应答的命令码为 253(DO)即为我认可你使用回显或抑制继续进行ch=DO;m_strResp+=ch;m_strResp+=Option;/break;/如果接受到的命令码为251(WONT)if(Verb=WONT)/应答我也拒绝选项请求回显或抑制继续进行ch=DONT;m_strResp+=ch;m_strResp+=Option;/break;/如果接受到250(sb,标志子选项开始)if(Verb=SB)/*因为启动了子标志位,命令长度扩展到了4字节,*取最后一个标志字节为选项码*如果这个选项码字节为1(send)*则回发为 250(SB子选项开始)+获取的第二个字节+0(is)+255(标志位IAC)+240(SE子选项结束)*/Modifier=strOption3;if(Modifier=SEND)ch=SB;m_strResp+=ch;m_strResp+=Option;m_strResp+=IS;m_strResp+=IAC;m_strResp+=SE;#endregionelse/如果选项码不是1 或者3#region 底下一系列代表,无论你发那种请求,我都不干if(Verb=DO)ch=WONT;m_strResp+=ch;m_strResp+=Option;if(Verb=DONT)ch=WONT;m_strResp+=ch;m_strResp+=Option;if(Verb=WILL)ch=DONT;m_strResp+=ch;m_strResp+=Option;if(Verb=WONT)ch=DONT;m_strResp+=ch;m_strResp+=Option;#endregioncatch(Exception eeeee)throw new Exception(解析参数时出错:+eeeee.Message);/将信息转化成 charp 流的形式,使用 socket 进行发出/发出结束之后,使用一个匿名委托,进行接收,/之后这个委托里,又有个委托,意思是接受完了之后执行OnRecieveData 方法/void DispatchMessage(string strText)try/申请一个与字符串相当长度的char 流Byte smk=new BytestrText.Length;for(int i=0;i 出自于上面的匿名委托),*当接收完信息之后,执行 OnrecieveData 方法(由委托传进去),*注意,是异步调用*/sock1.BeginReceive(m_byBuff,0,m_byBuff.Length,SocketFlags.None,recieveData,sock1);,s);/*结束 异步发送*EndSend 完成在 BeginSend 中启动的异步发送操作。*在调用 BeginSend 之前,需创建一个实现 AsyncCallback 委托的回调方法。*该回调方法在单独的线程中执行并在 BeginSend 返回后由系统调用。*回调方法必须接受 BeginSend 方法所返回的 IAsyncResult 作为参数。*在回调方法中,调用 IAsyncResult 参数的 AsyncState 方法可以获取发送 Socket。*在获取 Socket 之后,则可以调用 EndSend 方法以成功完成发送操作,并返回发送的字节数。*/s.EndSend(ar2);catch(Exception ers)Console.WriteLine(出错了,在回发数据的时候:+ers.Message);#endregion效果如下:首先,收到远程服务端的信息(第一次接)255 253 24255 253 32255 253 35255 253 39远程服务器说/*=我想要求客户端激活终端类型我想要求客户端激活终端速度我想要求客户端激活39功能(手册没写是啥)=*/之后,我们客户端返回以下信息(第一次发)255 252 24255 252 32255 252 35255 252 39客户端说/*=客户端想禁止 你说的所有的功能(24,32,35,39)=*/服务器收到了我们发出的信息之后,又发出以下信息(第二次接)255 251 3255 253 1255 253 31255 251 5255 253 33服务器又说/*=我自己将激活回抑制继续进行我希望客户端激活回显功能我希望客户端激活窗口大小我自己将激活状态我希望客户端激活远程流量控制=*/之后我们客户端返回以下信息(第二次发)255 253 3255 251 1255 252 31255 254 5255 252 33客户端说/*=我希望服务器端激活抑制继续进行我自己将激活回显功能我自己想禁止窗口大小功能我希望服务端禁止状态功能我自己想禁止远程流量控制=*/服务器收到我们消息之后,又给我们消息(第三次接)255 254 1255 251 1意思为/*=我想让客户端禁用回显我想自己使用 回显=*/我们客户端接着发(第三次发)255 252 1255 253 1意思为/*=我也不想自己开启回显同事我也觉得你开启回显很合适=*/服务器端终于结束验证了,开始发正文显示.(第四次接)解析过来就是Ubuntu 9.04atpking-desktop login:之后为了告诉服务器咱们收到消息了(第四次发送消息)255 252 1255 253 1255 252 1255 253 1客户端=我的,禁止回显的设置服务器的,请你接受 回显我自己想禁用回显我希望服务器接受回显=登录画面登录成功=后记确实通信那块很嚼人,而且现在也还是半懂不懂的状态(不知道为什么第四次回发消息的时候,服务器就不再发消息了等)只不过最起码的,从 cocket 本身模拟来说,还算能写篇.花了不少时间研究 socket 和写这篇日子,希望能对看这篇文章的人产生一点帮助吧.