中国DOS联盟论坛

中国DOS联盟

-- 联合DOS 推动DOS 发展DOS --

联盟域名:www.cn-dos.net  论坛域名:www.cn-dos.net/forum
DOS,代表着自由开放与发展,我们努力起来,学习FreeDOS和Linux的自由开放与GNU精神,共同创造和发展美好的自由与GNU GPL世界吧!

游客:  注册 | 登录 | 命令行 | 会员 | 搜索 | 上传 | 帮助 »
中国DOS联盟论坛 » DOS开发编程 & 发展交流 (开发室) » [原创]在DOS下进行网络编程(下)
作者:
标题: [原创]在DOS下进行网络编程(下) 上一主题 | 下一主题
whowin
初级用户





积分 134
发帖 37
注册 2006-9-28
状态 离线
『楼 主』:  [原创]在DOS下进行网络编程(下)

文章是从我的网志中贴过来的,其中的图片可能过不来,看完整内容,请访问我的网志:
点击进入《DOS编程技术》

在上一篇中,我们为在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.hengch.com/Manual/BSDsocket.pdf

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

        http://blog.hengch.com/specification/rtfc765-ftp.pdf

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

侦听FTP端口21(listen)
接受来自FTP客户端的连接请求(accept)
接受FTP客户端的登录,但并不对登录信息做验证
接受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,其通讯过程大致如下(仅与例子有关的过程):

客户端软件首先向服务器21端口请求连接
服务器接受连接后向客户端发送以“220 ”为开始的字符串,本程序发出“220 FTP Server, service ready.”
客户端收到“220 ”的信息后进行登录,发送“user xxxxxx”的命令,其中xxxxxx为用户名
服务器检验该用户名合法后,请求客户输入密码,发送“331 ”为开始的字符串,本程序发送“331 Password required for xxxxxx”,其中xxxxxx为收到的用户名
客户端收到“331 ”的信息后发送密码到服务器,发送“pass xxxxxx”命令,其中xxxxxx为密码
服务器在检验密码正确后,向客户端发送“230 ”开头的字符串,表示登录成功,可以接收其他命令,本程序发送“230 Logged in okay.”
之后客户端与服务器间为传送文件、目录等要做大量的交互
结束服务时,客户端向服务器发送“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()函数),从而导致运行失败,请用下面方法测试:

在windows下点“开始”-->“运行”,输入:ftp 192.168.0.20(如果你的IP地址不一样,请更改)


按下“确定”后,出现下面窗口,我们看到第二行的“220 FTP...”就是我们的程序发过来的


我们输入“abcd”,当然输入其它的也可以,因为我们的程序并不检验,按回车后出现下面的窗口,其中第4行是我们的程序在收到用户名后返回的


在第5行任意输入几个字母数字,比如“1234”,按回车,由于是密码,屏幕并不显示你输入的内容,回车后看到如下窗口,其中,第6行的内容是我们程序在收到pass命令后返回的


最后,我们在ftp>的后面输入退出命令:quit,按回车后屏幕闪一下就关闭了,所以我们看不清返回的内容

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

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

   
更多关于DOS编程的文章看我的网志

点击进入《DOS编程技术》

[ Last edited by whowin on 2008-5-9 at 11:50 AM ]

2008-5-9 11:36
查看资料  发送邮件  发短消息 网志   编辑帖子  回复  引用回复
wzy19806206
初级用户





积分 20
发帖 9
注册 2007-6-1
状态 离线
『第 2 楼』:  

还不顶

2008-5-9 16:21
查看资料  发送邮件  发短消息 网志   编辑帖子  回复  引用回复
leejonny
新手上路





积分 2
发帖 1
注册 2008-9-22
状态 离线
『第 3 楼』:  

更多关于DOS编程的文章看我的网志

2008-9-22 15:58
查看资料  发送邮件  发短消息 网志   编辑帖子  回复  引用回复
acejoo
新手上路





积分 7
发帖 4
注册 2008-10-22
状态 离线
『第 4 楼』:  

好,不错,不知道有没有其他的,譬如音频传输等的。p2p的

2008-10-22 15:11
查看资料  发送邮件  发短消息 网志   编辑帖子  回复  引用回复
52book
新手上路





积分 4
发帖 2
注册 2006-9-30
状态 离线
『第 5 楼』:  

LZ高人,一直关注你的BOKE

2009-4-9 00:46
查看资料  发短消息 网志   编辑帖子  回复  引用回复
alphatruly
新手上路





积分 6
发帖 3
注册 2008-11-27
状态 离线
『第 6 楼』:  

哈哈!whowin来了,一直很关注你的文章啊!

简直就是神贴!

2009-4-11 15:51
查看资料  发送邮件  发短消息 网志   编辑帖子  回复  引用回复
djxxt
新手上路





积分 4
发帖 2
注册 2009-11-16
状态 离线
『第 7 楼』:  

多谢,学习

2009-11-29 19:48
查看资料  发送邮件  发短消息 网志   编辑帖子  回复  引用回复
messen
新手上路





积分 17
发帖 8
注册 2010-1-3
状态 离线
『第 8 楼』:  

追着你学

2010-5-5 19:04
查看资料  发送邮件  发短消息 网志   编辑帖子  回复  引用回复
ecurb2006
中级用户

www.ecgui.com



积分 272
发帖 123
注册 2006-9-23
状态 离线
『第 9 楼』:  

好帖!



eCGUI-微型嵌入式GUI/ DOS/Linux/uC/OS-II/等 图形界面开发
www.ecgui.com
2010-5-18 04:25
查看资料  发送邮件  发短消息 网志   编辑帖子  回复  引用回复

请注意:您目前尚未注册或登录,请您注册登录以使用论坛的各项功能,例如发表和回复帖子等。


可打印版本 | 推荐给朋友 | 订阅主题 | 收藏主题



论坛跳转: