登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

DOS编程技术

讨论在纯DOS下的编程技术

 
 
 

日志

 
 
关于我

1984年大学毕业,1985年底有机会开始接触PC机,1986年开始在PC机上做开发工作,曾接触过MS-DOS、CP/M、UNIX、VMS、LINUX、iRMX等众多的操作系统并在上面从事技术开发,擅长做底层与硬件相关的软件开发,目前主要在DOS和LINUX平台下工作,主要从事软件,在硬件开发上也有一定造诣,亦有在8051系列、6502系列(凌阳)、z80系列、ARM、X86等各类平台下开发软硬件的经历。更详细情况可以参考http://resume.whowin.net

在DOS下进行网络编程(下)  

2008-04-11 10:10:33|  分类: 网络编程 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

    在上一篇中,我们为在DOS下进行网络编程做了大量的准备工作,我们在DJGPP下安装了WATT-32库,同时,配置好了网络环境,下面我们用一个实例来说明在DOS下进行网络编程的方法。

    上一篇中,我们编译了WATT-32库中的一个范例程序ftpsrv.c,这是一个FTP服务器的范例程序,下面我们也编一个FTP服务器的程序,但我们有两点不同,第一,我们主要使用BSD网络编程的标准函数,这是一个UNIX下进行网络编程的规范,WATT-32库中实现了绝大多数的BSD编程函数,在《在DOS下进行网络编程(上)》中介绍了一篇文章《Beej's Guide to Network Programming Using internet Sockets》,这篇文章中介绍的编程方法也是基于这个规范,有关在这个规范下的函数介绍可以从下面这个网址下载,也可以参考UNIX下网络编程的书籍。

        http://blog.whowin.net/Manual/BSDsocket.pdf(2017年3月17日注:该链接已修复)

     下面继续我们的FTP服务器,要编写一个FTP服务器程序,首先要了解一下FTP协议,有关FTP协议的完整规范,可以在下面网址下载:

        http://blog.whowin.net/specification/rtfc765-ftp.pdf(2017年3月17日注:该链接已修复)

    实际操作上并没有协议中那么复杂,况且我们的实例也并不想完成所有的协议,我们的实例计划完成下面的功能:

  1. 侦听FTP端口21(listen)
  2. 接受来自FTP客户端的连接请求(accept)
  3. 接受FTP客户端的登录,但并不对登录信息做验证
  4. 接受FTP客户端发来的退出(quit)命令,关闭连接(close)

    整个程序只接受一个FTP客户端的请求,当已经为一个FTP客户端提供服务时,如果有新的连接请求将不予理睬。

    好我们现在可以开始了,以下是我们这个例子的源程序,为了说明方便,我们在前面加了行号。

    01  #include <stdio.h>
    02  #include <string.h>
    03  #include <sys/socket.h>

    04  int FtpServer(int s);

    05  int main (void) {
    06    struct sockaddr_in my_addr;    // my address information
    07    struct sockaddr_in their_addr; // connector's address information
    08    int    sockfd, new_fd;         // listen on sockfd, new connection on new_fd
    09    int    sin_size;
    10    int    Loop;
    11    char   tempStr[100];

    12    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    13      printf("Socket Error!\n");
    14      return 1;
    15    }

    16    my_addr.sin_family = AF_INET;         // host byte order
    17    my_addr.sin_port   = htons(21);       // short, network byte order
    18    my_addr.sin_addr.s_addr = INADDR_ANY; // automatically fill with my IP
    19    memset(&my_addr.sin_zero, 0, 8);      // zero the rest of the struct

    20    if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {
    21      printf("Bind Error!\n");
    22      return 1;
    23    }

    24    if (listen(sockfd, 5) == -1) {
    25      printf("Listen Error!\n");
    26      return 1;
    27    }

    28    new_fd   = -1;
    29    sin_size = sizeof(struct sockaddr_in);
    30    new_fd   = accept(sockfd, (struct sockaddr*)&their_addr, &sin_size);
    31    if (new_fd == -1) {
    32      printf("Accept Error!\n");
    33      return 1;
    34    }
    35    printf ("Got connection from %s\n", inet_ntoa(their_addr.sin_addr));

    36    strcpy(tempStr, "220 FTP Server, service ready.\r\n");
    37    send(new_fd, tempStr, strlen(tempStr), 0);

    38    Loop = 1;
    39    while (Loop) {
    40      Loop = FtpServer(new_fd);
    41    }
    42    sleep(5);
    43    close(new_fd);
    44    close(sockfd);
    45    return 0;
    46  }

    47  int FtpServer(int s) {
    48    char   szBuf[100];
    49    char   tempStr[100];
    50    int    iBytes;
    51    char  *p, *p2;

    52    iBytes = recv(s, szBuf, 30, 0);
    53    if (iBytes >= 2) {
    54      iBytes -= 2;
    55      szBuf[iBytes] = NULL;
    56    } else {
    57      return 0;
    58    }

    59    p = szBuf;
    60    while (*p != ' ' && *p != NULL) {
    61      p++;
    62    }
    63    if (p) {
    64      *p = NULL;
    65      p2 = p + 1;    // p2 point to the second parameter
    66    }
    67    if (stricmp("user", szBuf) == 0) {    // start to process FTP commands
    68      sprintf(tempStr, "331 Password required for %s.\r\n", p2);
    69      send(s, tempStr, strlen(tempStr), 0);
    70      printf("Received 'user' command. User is %s\n", p2);
    71    } else if(stricmp("pass", szBuf) == 0) {
    72      strcpy(tempStr, "230 Logged in okay.\r\n");
    73      send(s, tempStr, strlen(tempStr), 0);
    74      printf("Received 'pass' command. Password is %s\n", p2);
    75    } else if (stricmp("quit", szBuf) == 0) {
    76      strcpy(tempStr, "221 Bye!\r\n");
    77      send(s, tempStr, strlen(tempStr), 0);
    78      printf("Received 'quit' command!\n");
    79      return 0;
    80    } else {
    81      strcpy(tempStr, "500 Command not understood.\r\n");
    82      send(s, tempStr, strlen(tempStr), 0);
    83      printf("Received a unknown command: %s\n", szBuf);
    84    }
    85    return 1;
    86  }
    整个程序很短,只有86行,为了突出主线,程序中去掉了大部分的错误处理,所以整个程序只是一个大致的框架,但能够说明问题。

    如果你手头有FTP客户端软件(比如CUTEFTP、LEAFFTP等),不妨试着连接一下任意一个FTP服务器,可以简单观察一下FTP的通讯过程,FTP的端口号是21,其通讯过程大致如下(仅与例子有关的过程):

  1. 客户端软件首先向服务器21端口请求连接
  2. 服务器接受连接后向客户端发送以“220 ”为开始的字符串,本程序发出“220 FTP Server, service ready.”
  3. 客户端收到“220 ”的信息后进行登录,发送“user xxxxxx”的命令,其中xxxxxx为用户名
  4. 服务器检验该用户名合法后,请求客户输入密码,发送“331 ”为开始的字符串,本程序发送“331 Password required for xxxxxx”,其中xxxxxx为收到的用户名
  5. 客户端收到“331 ”的信息后发送密码到服务器,发送“pass xxxxxx”命令,其中xxxxxx为密码
  6. 服务器在检验密码正确后,向客户端发送“230 ”开头的字符串,表示登录成功,可以接收其他命令,本程序发送“230 Logged in okay.”
  7. 之后客户端与服务器间为传送文件、目录等要做大量的交互
  8. 结束服务时,客户端向服务器发送“quit”命令,双方断开连接

    首先我们来了解两个数据结构,struct sockaddr和struct sockaddr_in。

    struct sockaddr {
      unsigned short  sa_family;      // address family
      char            sa_data[14];    // 14 bytes of protocol address

    }

    这个结构用来管理socket的地址信息,其中sa_family是地址的类别,我们填入“AF_INET”就可以了,该常数已经在WATT-32的头文件里定义好了,sa_data是14字节的地址信息,其中应该包含地址和端口信息。为了方便使用,建立了一个与sockaddr等同的结构,struct sockaddr_in

    struct sockaddr_in {
      short int          sin_family;    // address family
      unsigned short int sin_port;      // port number
      struct in_addr     sin_addr;      // internet address
      unsigned char      sin_zero[8];   // Same size as struct sockaddr
    }

    该结构的sin_family与sockaddr中的sa_family是相同的,填“AF_INET”就可以了,sin_port是端口号,FTP的端口号是21;struct in_addr的结构如下:

    struct in_addr {
      unsigned long s_addr;
    }

    是一个32位的IP地址,要把一个常规的IP地址转换成一个32位的IP地址,需要用到下面的方法:

    xx.sin_addr.s_addr = inet_addr("192.168.0.20");

    关于字节顺序问题,在《Beej's Guide to Network Programming Using internet Sockets》中也提到这个问题,其中一种字节顺序叫做“Host Byte Order”,另一种叫做“Network Byte Order”,因为该文中,对这个问题说得并不是很清楚,所以在这里多说几句,一个数字,比如Short int类型,占两个字节,假定这个数是0x6789,存放在内存地址为0x1000的位置,则有两种表示方法,一种是0x1000处放0x67,0x1001处放0x89;另一种表示方法是0x1000处放0x89,0x1001处放0x67,第一种存放方式叫big-endian,第二种存放方式叫little-endian,在CPU为x86的机器中,使用的是little-endian的顺序,而网络传输协议TCP/IP采用的是big-endian,在我们这个特定的环境中,Host Byte Order指的就是我们PC机的字符顺序,也就是little-endian顺序,而Network Byte Order则指的是网络传输顺序,即big-endian,由于采用的字节顺序不同,所以要经常进行1转换,为此专门有一组转换函数,函数中的“h”指Host Byte Order,“n”指Network Byte Order,“s”指short int,“l”指long int,所以,这组函数的意义如下:

    htons()----"Host to Network Short"
    htonl()----"Host to Network Long"
    ntohs()----"Network to Host Short"
    ntohl()----"Network to Host Long"

    所以,在网络编程中,一旦遇到整数等数值操作时,一定要想一想是否需要进行转换。我希望我的解释不仅能让你明白其中的道理,同时记住这几个转换函数。

    在我们这个例子中,需要两个这样的数据结构,一个用来管理我们本地的网络地址,一个用来管理与我们连接的远端节点的网络地址,这两个结构,我们分别命名为:my_addr和their_addr,见源程序第06和07行。

    在我们这个例子中,我们还需要两个socket,一个用来表示是我们本地正在侦听的网络,一个用来表示与远端FTP客户端的网络连接,我们不必追问什么是socket,仅仅把它理解成一个类似文件handle的东西就可以了,实际上socket就是一个整数而已。

    socket有很多种,但常用的只有两种,一种是“Stream Sockets”,另一种是“Datagram Sockets”,前一种用于TCP连接,后一种用于UDP连接,了解这些暂时就足够了。

    我们程序的一开始,首先初始化一个socket,socket函数的原型如下:

    int socket(int domain, int type, int protocol);

    domain一般情况下均填“AF_INET”,type指的就是socket的类型,如果是Stream Sockets,请填SOCK_STREAM,如果是Datagram Sockets则填SOCK_DGRAM,本程序中应该为SOCK_DGRAM,protocol置为0即可。socket()的返回值为一个可用的socket值,程序的第12行,我们得到了一个socket:sockfd。

    第16-19行,我们描述了本地的网络地址结构my_addr,要说明的是,第17行中的21是FTP的专用端口,由于my_addr.sin_port是一个short int类型,所以要使用htons()进行一下转换,第18行把my_addr.sin_addr.s_addr填入常数INADDR_ANY其含义是使用本机在WATTCP.CFG中设置的IP地址,要注意的是s_addr的类型是long int,但这里却没有使用htonl()函数进行转换,这是因为我们知道INADDR_ANY的值是0,严格意义上说,这里的确需要使用htonl()函数进行转换,这点要特别注意,如果要自己填写IP地址,注意要使用inet_addr()函数来转换一个普通的IP地址,如下:

        my_addr.sin_addr.s_addr = inet_addr("192.168.0.20");

    把一个32 bits的IP地址转换成我们常见的形式要使用函数inet_ntoa(),如下:

        printf("IP address is %s", my_addr.sin_addr.s_addr);

    打印出来的是xxx.xxx.xxx.xxx的我们常见的IP地址形式。

    第19行仅仅是把结构的其余部分填上了0,没有任何含义。

    第20行我们把刚得到的sockfd和刚填好的结构my_addr使用bind()绑定在一起,bind()函数的原型如下:

    int bind(int sockfd, struct sockaddr *my_addr, int addr_len);

    好像没有什么好解释的。

    第24行设定在sockfd这个socket上侦听,最大允许5个连接,实际我们只接受一个连接。listen()的原型如下:

    int listen(int sockfd, int backlog);

    参数backlog可以指定该侦听允许多少个连接请求;没有更多需要解释的。

    第30行在等待一个连接请求,注意,accept()这个函数是一个阻塞函数,程序将停在这个函数里,一直等到有连接请求时才能返回,在某些场合是不能这样用的,accept()函数的原型如下:

    int accept(int sockfd, void *addr, int *addrlen);

    正常情况下,accept函数返回一个新的socket描述符,本程序中的new_sock,这个新的socket表示和一个远端节点的连接,以后当要操作这个连接时都会使用这个socket,同时,accept函数会把远端节点的地址信息填写到addr中,在本程序中是their_addr。

    第37行,我们向远端计算机发出了第一条信息,使用send()函数向new_sock上发送,send()函数的原型如下:

    int send(int sockfd, const void *msg, int len, int flags);

    函数的最后一个参数,一般情况下置为0即可。

    在向远端计算机发出一条信息后,程序进入一个循环,循环中不断地调用函数FtpServer(),直到该函数返回0才退出循环,FtpServer中,程序试图从new_sock上接收信息,然后分析处理信息,直到收到“quit”命令后返回0,使主程序可以退出循环。

    第52行使用recv()函数接收来自new_sock的信息,这个函数也是一个阻塞函数,也就是说,如果没有收到信息,这个函数是不会返回的,这在构造一个实时系时时不能允许的,另外一个问题就是当程序进入recv()函数后网络由于某种原因中断,程序是不会从recv()函数中返回的,程序将吊死在recv()函数内,所以,实际应用中是不能这样使用这个函数的;recv()函数的原型如下:

    int recv(int sockfd, void *buf, int len, unsigned int flags);

    和send()函数一样,flags填0就好了,len是接收信息的最大长度,这要参考buf的长度来确定,否则会出现越界的错误,实际接收时并不是要接收到len个字符才返回,这个函数将返回实际接收到的字符数。

    第53行我们限定收到的字符数至少要2个,这是因为所有FTP传送的命令后面都带有回车换行,也就是ascii码0x0d和0x0a,如果两这两个字符都没有,那收到的内容是没有意义的。

    第59-66行我们对收到的内容作了一个简单的分析,因为ftp的命令格式是:cmd para1 para2....,这段程序我们把命令部分的cmd专门分了出来,这段程序执行完毕后,szBuf指向cmd,而p2指向后面的参数,当然我们这个范例程序并不需要分析参数,所以实际上p2对我们并没有什么用。

    第67--80行我们处理了三个命令,并且按照协议给出了合法的返回或者动作,对于“user”、“pass”和“quit”以外的命令,我们都按照未知命令处理,并按照协议,返回了“500 ......”这样的信息。

    程序到此就解释完了,这个程序由于缺少错误处理等必要的部分,实际没有什么实用性,但其架构是完整的,经过加工,完全可以变成一个完整的Ftp服务器端程序。

    最后还要说一下怎么测试,首先设置好网络数据,这在前面有说明,然后用HUB将两台机器连接起来,我们不能用一般的FTP软件(比如CUTEFTP或者LeafFTP),因为我们处理的命令是在太少了,这些软件会自动地发送许多指令,由于我们的程序均回应“500 ...”,将导致一个正常的FTP软件出现“协议错误”之类的错误信息并终止运行,我们也不能使用telnet这样的软件来进行测试,因为这种软件是仿终端的软件,每输入一个字符将立即发送出去,而键盘输入的速度极慢,将导致我们的程序一次无法收到一个完整的命令(recv()函数),从而导致运行失败,请用下面方法测试:

  1. 在windows下点“开始”-->“运行”,输入:ftp 192.168.0.20(如果你的IP地址不一样,请更改)
    在DOS下进行网络编程(下) - whowin - DOS编程技术

    按下“确定”后,出现下面窗口,我们看到第二行的“220 FTP...”就是我们的程序发过来的
    在DOS下进行网络编程(下) - whowin - DOS编程技术
  2. 我们输入“abcd”,当然输入其它的也可以,因为我们的程序并不检验,按回车后出现下面的窗口,其中第4行是我们的程序在收到用户名后返回的
    在DOS下进行网络编程(下) - whowin - DOS编程技术
  3. 在第5行任意输入几个字母数字,比如“1234”,按回车,由于是密码,屏幕并不显示你输入的内容,回车后看到如下窗口,其中,第6行的内容是我们程序在收到pass命令后返回的
    在DOS下进行网络编程(下) - whowin - DOS编程技术
  4. 最后,我们在ftp>的后面输入退出命令:quit,按回车后屏幕闪一下就关闭了,所以我们看不清返回的内容
    在DOS下进行网络编程(下) - whowin - DOS编程技术

    整个过程在运行我们程序的FTP服务器端也表现得很清楚。

    好了,这个具体的例子我们说完了,大概在DOS下进行网络编程的方法你应该了解了,要注意,由于我们是在DJGPP下生成的程序,是32位保护模式的,所以要在有DPMI服务的机器上才能运行,当然这种变成方式也适用于实模式,而且,尽管WATT-32库是32位的,但实际也支持16位的实模式,所以使用turbo C等也是可以的,我们以后有机会会更进一步地谈在DOS下进行网络编程的方法,或者介绍Packet Driver的编写规范和方法,或者介绍一下DPMI等等。

   

  评论这张
 
阅读(4200)| 评论(12)

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018