《Beej's 网络编程指南.ppt》由会员分享,可在线阅读,更多相关《Beej's 网络编程指南.ppt(45页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、Beejs 网络编程指南,介绍,Hey! Socket 编程让你沮丧吗?从 man pages 中很难得到有用的信息吗?你想 跟上时代去做一做 Internet 程序,但是为你在调用 connect() 前的 bind() 的结构而愁眉不展?,读者,这个文档是写成一个指南,而不是参考书。如果你刚开始 socket 编程并想找一本 入门书,那么你是我的读者。这可不是一本完全的 socket 编程书。,平台和编译器,这篇文章中的大多数代码都在一台 Linux PC 上用 GNU 的 gcc 成功编译过。 而且他们在一台 HPUX 上用 gcc 也成功编译过。但是注意,并不是每个代码 片段都独立测试
2、过。,目录,什么是套接口? Internet 套接口的两种类型 网络理论 struct-要么了解他们,要么等异形入侵地球 Convert the Natives! IP 地址和如何处理他们,目录,socket()-得到文件描述符! bind()-我们在哪个端口? connect()-Hello! listen()-有人给我打电话吗? accept()-Thank you for calling port 3490.,目录,send() 和 recv()-Talk to me, baby! sendto() 和 recvfrom()-Talk to me, DGRAM-style close()
3、 和 shutdown()-滚开! getpeername()-你是谁? gethostname()-我是谁?,目录,DNS-你说“白宫”,我说 198.137.240.100 客户-服务器背景知识 简单的服务器 简单的客户端 数据报 Socket,目录,阻塞 select()-多路同步 I/O,酷! 参考资料 Disclaimer and Call for Help,什么是 socket?,你始终听到人们谈论着 socket,而你不知道他的确切含义。那么,现在我告诉你: 他是使用 Unix 文件描述符 (fiel descriptor) 和其他程序通讯的方式。 什么? Ok-你也许听到一些
4、Unix 高手 (hacker) 这样说:“呀,Unix 中所有的东西 就是文件!”那个家伙也许正在说到一个事实:Unix 程序在执行任何形式的 I/O 的时候, 程序是在读或者写一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数。 但是(注意后面的话),这个文件可能是一个网络连接,FIFO,管道,终端,磁盘上的文件 或者什么其他的东西。Unix 中所有的东西是文件!因此,你想和 Internet 上别 的程序通讯的时候,你将要通过文件描述符。最好相信刚才的话。 现在你脑海中或许冒出这样的念头:“那么我从哪里得到网络通讯的文件描述符呢,聪明 人?”无论如何,我要回答这个问题:你利用
5、系统调用 socket()。他返回套接口描 述符 (socket descriptor),然后你再通过他来调用 send() 和 recv()。 “但是.”,你可能现在叫起来,“如果他是个文件描述符,那么为什么不用一般的调用 read() 和 write() 来通过套接口通讯?”简单的答案是:“你可以使用 一般的函数!”。详细的答案是:“你可以,但是使用 send() 和 recv() 让你更好的控制数据传输。” 有这样一个事实:在我们的世界上,有很多种套接口。有 DARPA Internet 地址 (Internet 套接口),本地节点的路径名 (Unix 套接口),CCITT X.25 地
6、址 (你可以完全忽略 X.25 套接口)。 也许在你的 Unix 机器上还有其他的。我们在这里只讲第一种:Internet 套接口。,Internet 套接口的两种类型,什么意思?有两种 Internet 套接口?是的。不,我在撒谎。其实还有很多,但是我可不想 吓着你。我们这里只讲两种。 Except for this sentence, where Im going to tell you that Raw Sockets are also very powerful and you should look them up. 好了,好了。那两种类型是什么呢?一种是 Stream Socket
7、s,另外一种是 Datagram Sockets。我们以后谈到他们的时候也会用到 SOCK_STREAM 和 SOCK_DGRAM。 数据报套接口有时也叫“无连接套接口”(如果你确实要连接的时候用 connect()。) 流式套接口是可靠的双向通讯的数据流。如果你向套接口安顺序输出“1,2”,那么他们 将安顺序“1,2”到达另一边。他们也是无错误的传递的,有自己的错误控制。 那么数据报套接口呢?为什么他叫无连接呢?为什么他是不可靠的呢?恩,有这样的事实: 如果你发送一个数据报,他可能到达,他可能次序颠倒了。如果他到达,那么在这个包的内部 是无错误的。,网络理论,既然我刚才提到了协议层,那么现在
8、是讨论网络究竟如何工作和演示 SOCK_DGRAM 的工作。当然,你也可以跳过这一段,如果你认为 已经熟悉的话。 朋友们,现在是学习 数据封装 (Data Encapsulation) 的时候了! 这非常非常重要。Its so important that you might just learn about it if you take the networks course here at Chico State ;-). 主要的内容是:一个包,先是被第一个协议(在这里是 TFTP )包装(“封装”), 然后,整个数据(包括 TFTP 头)被另外一个协议(在这里是 UDP )封装,然后下
9、一个( IP ),一直重复下去,直到硬件(物理)层( Ethernet )。 当另外一台机器接收到包,硬件先剥去 Ethernet 头,内核剥去 IP 和 UDP 头,TFTP 程序再剥去 TFTP 头,最后得到数据。 现在我们终于讲到臭名远播的 网络分层模型 (Layered Network Model)。 这种网络模型在描述网络系统上相对其他模型有很多优点。例如,你可以写一个套接口 程序而不用关心数据的物理传输(串行口,以太网,连接单元接口 (AUI) 还是其他介质。 因为底层的程序为你处理他们。实际的网络硬件和拓扑对于程序员来说是透明的。,structs,注意这样的事实: 有两种字节排列
10、顺序:重要的字节在前面(有时叫 “octet”),或者不重要的字节在前面。 前一种叫“网络字节顺序 (Network Byte Order)”。有些机器在内部是按照这个顺序储 存数据,而另外一些则不然。当我说某数据必须按照 NBO 顺序,那么你要调用函数(例 如 htons() )来将他从本机字节顺序 (Host Byte Order) 转换过来。如果我 没有提到 NBO, 那么就让他是本机字节顺序吧 第一个结构(TM)-struct sockaddr. 这个数据结构 为许多类型的套接口储存套接口地址信息: struct sockaddr unsigned short sa_family; /
11、* address family, AF_xxx */ char sa_data14; /* 14 bytes of protocol address */ ;,sa_family 能够是各种各样的事情,但是在这篇文章中是 AF_INET。 sa_data 为套接口储存目标地址和端口信息。看上去很笨拙,不是吗。 为了对付 struct sockaddr,程序员创造了一个并列的结构: struct sockaddr_in (in 代表 Internet.) struct sockaddr_in short int sin_family; /* Address family */ unsigned
12、 short int sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ unsigned char sin_zero8; /* Same size as struct sockaddr */ ;,这个数据结构让可以轻松处理套接口地址的基本元素。注意 sin_zero (他 被加入到这个结构,并且长度和 struct sockaddr 一样) 应该使用函数 bzero() 或 memset() 来全部置零。 Also, and this is the important bit, a point
13、er to a struct sockaddr_in can be cast to a pointer to a struct sockaddr and vice-versa. 这样的话 即使 socket() 想要的是 struct sockaddr *, 你仍然可以使用 struct sockaddr_in,and cast it at the last minute! 同时,注意 sin_family 和 struct sockaddr 中的 sa_family 一致并能够设置为 AF_INET。最后, sin_port 和 sin_addr 必须是网络字节顺序 (Network Byt
14、e Order)!,Convert the Natives!,我们现在到达下个章节。我们曾经讲了很多网络到本机字节顺序,现在是采取行动的时刻了! 你能够转换两种类型: short (两个字节)和 long (四个字节)。这个 函数对于变量类型 unsigned 也适用。假设你想将 short 从本机字节顺序 转换为网络字节顺序。用 h 表示 本机 (host),接着是 to,然后用 n 表示 网络 (network),最后用 s 表示 short: h-to-n-s, 或者 htons() (Host to Network Short)。 太简单了. 如果不是太傻的话,你一定想到了组合 n,h
15、,s,和 l。但是这里没有 stolh() (Short to Long Host) 函数,但是这里有: htons()-Host to Network Short htonl()-Host to Network Long ntohs()-Network to Host Short ntohl()-Network to Host Long,IP 地址和如何处理他们,现在我们很幸运,因为我们有很多的函数来方便地操作 IP 地址。没有必要用手工计算 他们,也没有必要用 操作符来操作 long。 首先,假设你用 struct sockaddr_in ina,你想将 IP 地址 132.241.5.1
16、0 储存到其中。你要用的函数是 inet_addr(),转换 numbers-and-dots 格式的 IP 地址到 unsigned long。这个工作可以这样来做: ina.sin_addr.s_addr = inet_addr(132.241.5.10); 注意:inet_addr() 返回的地址已经是按照网络字节顺序的,你没有必要再去调用 htonl()。 好了,你现在可以转换字符串形式的 IP 地址为 long 了。那么你有一个数据结构 struct in_addr,该如何按照 numbers-and-dots 格式打印呢? 在这个 时候,也许你要用函数 inet_ntoa() (n
17、toa 意思是 network to ascii): printf(%s,inet_ntoa(ina.sin_addr);,他将打印 IP 地址。注意的是:函数 inet_ntoa() 的参数是 struct in_addr,而不是 long。同时要注意的是他返回的是一个指向字符的指针。 在 inet_ntoa 内部的指针静态地储存字符数组,因此每次你调用 inet_ntoa() 的时候他将覆盖以前的内容。例如: char *a1, *a2; . . a1 = inet_ntoa(ina1.sin_addr); /* this is 198.92.129.1 */ a2 = inet_ntoa
18、(ina2.sin_addr); /* this is 132.241.5.10 */ printf(address 1: %sn,a1); printf(address 2: %sn,a2); 运行结果是: address 1: 132.241.5.10 address 2: 132.241.5.10 如果你想保存地址,那么用 strcpy() 保存到自己的字符数组中。,socket()-得到文件描述符!,我猜我不会再扯远了-我必须讲 socket() 这个系统调用了。这里是详细的定义: #include #include int socket(int domain, int type, i
19、nt protocol); 但是他们的参数怎么用? 首先,domain 应该设置成 AF_INET,就象上面的 数据结构 struct sockaddr_in 中一样。然后,参数 type 告诉内核是 SOCK_STREAM 类型还是 SOCK_DGRAM 类型。最后,把 protocol 设置为 0。(注意:有很多种 domain、type, 我不可能一一列出了,请看 socket() 的 man page。当然,还有一个更好的方式 去得到 protocol。请看 getprotobyname() 的 man page。) socket() 只是返回你以后在系统调用种可能用到的 socket
20、 描述符,或者在错误 的时候返回-1。全局变量 errno 中储存错误值。(请参考 perror() 的 man page。),bind()-我在哪个端口?,一旦你得到套接口,你可能要将套接口和机器上的一定的端口关联起来。(如果你想用 listen() 来侦听一定端口的数据,这是必要一步-MUD 经常告诉你说用命令 telnet x.y.z 6969.)如果你只想用 connect(),那么这个步骤没有必要。但是无论如何,请继续读下去。 这里是系统调用 bind() 的大略: #include #include int bind(int sockfd, struct sockaddr *my_
21、addr, int addrlen); sockfd 是调用 socket 返回的文件描述符。my_addr 是指向 数据结构 struct sockaddr 的指针,他保存你的地址(即端口和 IP 地址) 信息。addrlen 设置为 sizeof(struct sockaddr)。,例子,#include #define MYPORT 3490 main() int sockfd; struct sockaddr_in my_addr; sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */ my_ad
22、dr.sin_family = AF_INET; /* host byte order */ my_addr.sin_port = htons(MYPORT); /* short, network byte order */ my_addr.sin_addr.s_addr = inet_addr(132.241.5.10); bzero(,这里也有要注意的几件事情。my_addr.sin_port 是网络字节顺序,my_addr.sin_addr.s_addr 也是的。另外要注意到的事情是因系统的不同, 包含的头文件也不尽相同,请查阅自己的 man page。 在 bind() 主题中最后要说
23、的话是,在处理自己的 IP 地址和/或端口的时候,有些工作 是可以自动处理的。 my_addr.sin_port = 0; /* choose an unused port at random */ my_addr.sin_addr.s_addr = INADDR_ANY; /* use my IP address */ 通过将0赋给 my_addr.sin_port,你告诉 bind() 自己选择合适的端口。同样, 将 my_addr.sin_addr.s_addr 设置为 INADDR_ANY,你告诉他自动填上 他所运行的机器的 IP 地址。,bind() 在错误的时候依然是返回-1,并且
24、设置全局变量 errno。 在你调用 bind() 的时候,你要小心的另一件事情是:不要采用小于1024的端口号。所有小于1024的端口号都 被系统保留!你可以选择从1024到65535(如果他们没有被别的程序使用的话)。 你要注意的另外一件小事是:有时候你根本不需要调用他。如果你使用 connect() 来 和远程机器通讯,你不要关心你的本地端口号(就象你在使用 telnet 的时候),你只要 简单的调用 connect() 就够可,他会检查套接口是否绑定,如果没有,他会自己绑定 一个没有使用的本地端口,connect()-Hello!,connect() 系统调用是这样的: #includ
25、e #include int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); sockfd 是系统调用 socket() 返回的套接口文件描述符。 serv_addr 是保存着目的地端口和 IP 地址的数据结构 struct sockaddr。 addrlen 设置为 sizeof(struct sockaddr)。,example,#include main() int sockfd; struct sockaddr_in dest_addr; /* will hold the destination addr */
26、 sockfd = socket(AF_INET, SOCK_STREAM, 0); /* dosomeerrorchecking! */ dest_addr.sin_family = AF_INET; /* host byte order */ dest_addr.sin_port = htons(23); /* short, network byte order */ dest_addr.sin_addr.s_addr = inet_addr(132.241.5.10); bzero(,listen(),int listen(int sockfd, int backlog); sockfd
27、 是调用 socket() 返回的套接口文件描述符。 backlog 是 在进入队列中允许的连接数目。是什么意思呢? 进入的连接是在队列中一直等待直到你接受 (accept() 请看下面的文章)的连接。他们的数目限制于队列的允许。大多数系统 的允许数目是20,你也可以设置为5到10。 和别的函数一样,在发生错误的时候返回-1,并设置全局变量 errno。,你可能想象到了,在你调用 listen() 前你或者要调用 bind() 或者让 内核随便选择一个端口。如果你想侦听进入的连接,那么系统调用的顺序可能是这样的: socket(); bind(); listen(); /* accept()
28、goes here */ 因为他相当的明了,我将在这里不给出例子了。 (在 accept() 那一章的代码将更加 完全。)真正麻烦的部分在 accept()。,accept(),准备好了,系统调用 accept() 会有点古怪的地方的!你可以想象发生这样的事情: 有人从很远的地方通过一个你在侦听 (listen() 的端口连接 (connect() 到你的机器。他的连接将加入到等待接受 (accept() 的队列中。你调用 accept() 告诉他你有空闲的连接。他将返回一个新的套接口文件描述符! 原来的一个还在侦听你的那个端口,新的最后在准备发送 (send() 和接收 ( recv() 数
29、据。这就是这个过程! 函数是这样定义的: #include int accept(int sockfd, void *addr, int *addrlen); sockfd 相当简单,是和 listen() 中一样的套接口描述符。addr 是个指向局部的数据结构 struct sockaddr_in 的指针。This is where the information about the incoming connection will go (and you can determine which host is calling you from which port). 在他的地址传递给 a
30、ccept 之前,addrlen 是个局部的整形变量,设置为 sizeof(struct sockaddr_in)。accept 将 不会将多余的字节给 addr。如果你放入的少些,那么在 addrlen 的值中反映 出来。,sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */ my_addr.sin_family = AF_INET; /* host byte order */ my_addr.sin_port = htons(MYPORT); /* short, network byte order
31、*/ my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */ bzero( .,send() and recv(),这两个函数用于流式套接口和数据报套接口的通讯。如果你喜欢使用无连接的数据报 套接口,你应该看一看下面关于 sendto() 和 recvfrom() 的章节。 send() 是这样的: int send(int sockfd, const void *msg, int len, int flags); sockfd 是你想发送数据的套接口描述符(或者是调用 socket() 或者是 accept() 返回的
32、。) msg 是指向你想发送的数据的指针。 len 是 数据的长度。 把 flags 设置为 0 就可以了。(详细的资料请看 send() 的 man page)。,可能的例子,char *msg = Beej was here!; int len, bytes_sent; . . len = strlen(msg); bytes_sent = send(sockfd, msg, len, 0); .,recv() 函数很相似: int recv(int sockfd, void *buf, int len, unsigned int flags); sockfd 是要读的套接口描述符。 bu
33、f 是要读的信息的缓冲。len 是 缓冲的最大长度。 flags 也可以设置为0。(请参考recv() 的 man page。) recv() 返回实际读入缓冲的数据的字节数。或者在错误的时候返回-1,同时设置 errno。,sendto() 和 recvfrom(),int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen); 你已经看到了,除了另外的两个信息外,其余的和函数 send() 是一样的。 to 是个指向数据结构 struct
34、sockaddr 的指针,他包含了目的地的 IP 地址和断口信息。tolen 可以简单地设置为 sizeof(struct sockaddr)。 相似的还有函数 recv() 和 recvfrom()。 recvfrom() 的定义是 这样的: int recvfrom(int sockfd, void *buf, int len, unsigned int flags struct sockaddr *from, int *fromlen);,close() 和 shutdown(),你已经整天都在发送 (send() 和接收 (recv() 数据了,现在你准备 关闭你的套接口描述符了。这很
35、简单,你可以使用一般的 Unix 文件描述符的 close() 函 数: close(sockfd); 他将防止套接口上更多的数据的读写。任何在另一端读写套接口的企图都将返回错误信息。 如果你想在如何关闭套接口上有多一点的控制,你可以使用函数 shutdown()。他能够让 你将一定方向的通讯或者双向的通讯(就象 close() 一样)关闭,你可以使用: int shutdown(int sockfd, int how);,sockfd 是你想要关闭的套接口文件描述复。how 的值是下面的其中之一: 0 - Further receives are disallowed 1 - Further
36、 sends are disallowed 2 - Further sends and receives are disallowed (和 close() 一样 shutdown() 成功时返回 0,失败时返回 -1(同时设置 errno。) 如果在无连接的数据报套接口中使用 shutdown(),那么只不过是让 send() 和 recv() 不能使用(记得你在数据报套接口中使用了 connect 后是可以 使用他们的吗?),getpeername()-Who are you?,函数 getpeername() 告诉你在连接的流式套接口上谁在另外一边。函数是这样的: #include in
37、t getpeername(int sockfd, struct sockaddr *addr, int *addrlen); sockfd 是连接的流式套接口的描述符。 addr 是一个指向结构 struct sockaddr (或者是 struct sockaddr_in) 的指针,他保存着 连接的另一边的信息。 addrlen 是一个 int 型的指针,他初始化为 sizeof(struct sockaddr)。 函数在错误的时候返回 -1,设置相应的 errno。,gethostname()-Who am I?,甚至比 getpeername() 还简单的函数是 gethostname
38、()。他返回你程序 所运行的机器的主机名字。然后你可以使用 gethostbyname() 以获得你的机器的 IP 地址。 下面是定义: #include nt gethostname(char *hostname, size_t size); 参数很简单: hostname 是一个字符数组指针,他将在函数返回时保存 主机名。size 是 hostname 数组的字节长度。 函数调用成功时返回 0,失败时返回 -1,并设置 errno。,DNS,You say whitehouse.gov, I say 198.137.240.100 如果你不知道 DNS 的意思,那么我告诉你,他代表域名服务
39、 (Domain Name Service)。 他主要的功能是:你给他一个容易记忆的某站点的地址,他给你 IP 地址(然后你就可以 使用 bind(), connect(), sendto() 或者其他函数。)当一个人 输入: $ telnet whitehouse.gov telnet 能知道他将连接 (connect() 到 198.137.240.100。,但是这是如何工作的呢? 你可以调用函数 gethostbyname(): #include struct hostent *gethostbyname(const char *name); 很明白的是,他返回一个指向 struct h
40、ostent 的指针。这个数据结构是 这样的: struct hostent char *h_name; char *h_aliases; int h_addrtype; int h_length; char *h_addr_list; ; #define h_addr h_addr_list0,example,struct hostent *h; if (argc != 2) /* error check the command line */ fprintf(stderr,usage: getip addressn); exit(1); if (h=gethostbyname(argv1)
41、 = NULL) /* get the host info */ herror(gethostbyname); exit(1); printf(Host name : %sn, h-h_name); printf(IP Address : %sn,inet_ntoa(*(struct in_addr *)h-h_addr);,Client-Server Background,阻塞,阻塞,你也许早就听说了。阻塞是 sleep 的科技行话。你可能注意到前面运行的 listener 程序,他在那里不停地运行,等待数据包的到来。实际在运行的是 他调用 recvfrom(),然后没有数据,因此 recv
42、from() 说阻塞 (block) 直到数据的到来。 很多函数都利用阻塞。accept() 阻塞,所有的 recv*() 函数阻塞。他们 之所以能这样做是因为他们被允许这样做。当你第一次调用 socket() 建立套接口 描述符的时候,内核就将他设置为阻塞。如果你不想套接口阻塞,你就要调用函数 fcntl(): #include #include . . sockfd = socket(AF_INET, SOCK_STREAM, 0); cntl(sockfd, F_SETFL, O_NONBLOCK); . . 通过设置套接口为非阻塞,你能够有效地询问套接口以获得信息。如果你尝试着 从一个
43、非阻塞的套接口读信息并且没有任何数据,他不会变成阻塞-他将返回 -1 并 将 errno 设置为 EWOULDBLOCK。,select()-多路同步 I/O,select() 让你可以同时监视多个套接口。如果你想知道的话,那么他就会告诉你哪个套接口准备读,哪个又 准备好了写,哪个套接口又发生了例外 (exception)。 闲话少说,下面是 select(): #include #include #include int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);,参考书籍,UNIX网络编程richard stevens UNIX环境高级编程,实验,编程,实现一个简单的C/S模型 Sock程序源代码分析报告,