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 ]
|
|