[转]C语言SOCKET编程指南


1、介绍html

Socket编程让你沮丧吗?从man pages中很可贵到有用的信息吗?你想跟上时代去编Internet相关的程序,可是为你在调用 connect() 前的bind() 的结构而不知所措?等等…程序员

好在我已经将这些事完成了,我将和全部人共享个人知识了。若是你了解C语言并想穿过网络编程的沼泽,那么你来对地方了。web

2、读者对象编程

这个文档是一个指南,而不是参考书。若是你刚开始socket编程并想找一本入门书,那么你是个人读者。但这不是一本彻底的 socket 编程书。c#

3、平台和编译器数组

这篇文档中的大多数代码都在 Linux 平台PC 上用 GNU的 gcc 成功编译过。并且它们在HPUX平台 上用 gcc也成功编译过。可是注意,并非每一个代码片断都独立测试过。浏览器

 

 

 

目录服务器

1、介绍网络

2、读者对象数据结构

3、平台和编译器

4、什么是 socket

5、Internet套接字的两种类型

6、网络理论

7、结构体

8、本机转换

9、IP地址和如何处理它们

10、socket()函数

11、bind()函数

12、connect()程序

13、listen()函数

14、accept()函数

15、send() and recv()函数

16、sendto()和recvfrom()函数

17、close()和shutdown()函数

18、getpeername()函数

19、gethostname()函数

20、域名服务(DNS

21、客户-服务器背景知识

22、简单的服务器

23、简单的客户程序

24、数据包 Sockets

25、阻塞

26、select()--多路同步 I/O

27、参考书目:

28、修改历史

四、什么是socket

  你常常听到人们谈论着 “socket”,或许你还不知道它的确切含义。如今让我告诉你:它是使用标准Unix 文件描述符 (file descriptor)和其它程序通信的方式。什么?你也许听到一些Unix高手(hacker)这样说过:“呀,Unix中的一切就是文件!”那个家伙也许正在说到一个事实:Unix 程序在执行任何形式的 I/O的时候,程序是在读或者写一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数。可是(注意后面的话),这个文件多是一个网络链接,FIFO,管道,终端,磁盘上的文件或者什么其它的东西。Unix 中全部的东西就是文件!因此,你想和Internet上别的程序通信的时候,你将要使用到文件描述符。你必须理解刚才的话。如今你脑海中或许冒出这样的念头:“那么我从哪里获得网络通信的文件描述符呢?”,这个问题不管如何我都要回答:你利用系统调用socket(),它返回套接字描述符(socketdescriptor),而后你再经过它来进行send() 和 recv()调用。“可是...”,你可能有很大的疑惑,“若是它是个文件描述符,那么为什 么不用通常调用read()和write()来进行套接字通信?”简单的答案是:“你可使用!”。详细的答案是:“你能够,可是使用send()和recv()让你更好的控制数据传输。”存在这样一个状况:在咱们的世界上,有不少种套接字。有DARPA Internet 地址 (Internet 套接字),本地节点的路径名 (Unix套接字),CCITTX.25地址 (你能够将X.25套接字彻底忽略)。也许在你的Unix 机器上还有其它的。咱们在这里只讲第一种:Internet 套接字。

5Internet 套接字的两种类型

  什么意思?有两种类型的Internet套接字?是的。不,我在撒谎。其实还有不少,可是我可不想吓着你。咱们这里只讲两种。除了这些, 我打算另外介绍的 "Raw Sockets" 也是很是强大的,很值得查阅。

那么这两种类型是什么呢?一种是"Stream Sockets"(流格式),另一种是"DatagramSockets"(数据包格式)。咱们之后谈到它们的时候也会用到"SOCK_STREAM" 和 "SOCK_DGRAM"。数据报套接字有时也叫“无链接套接字”(若是你确实要链接的时候能够用connect()。) 流式套接字是可靠的双向通信的数据流。若是你向套接字按顺序输出“1,2”,那么它们将按顺序“1,2”到达另外一边。它们是无错误的传递的,有本身的错误控制,在此不讨论。

有什么在使用流式套接字?你可能据说过telnet,不是吗?它就使用流式套接字。你须要你所输入的字符按顺序到达,不是吗?一样,WWW浏览器使用的 HTTP 协议也使用它们来下载页面。实际上,当你经过端口80 telnet 到一个 WWW 站点,而后输入 “GETpagename” 的时候,你也能够获得 HTML 的内容。为何流式套接字能够达到高质量的数据传输?这是由于它使用了“传输控制协议(The Transmission ControlProtocol)”,也叫 “TCP” (请参考RFC-793得到详细资料。)TCP控制你的数据按顺序到达而且没有错

误。你也许听到 “TCP” 是由于听到过 “TCP/IP”。这里的 IP 是指“Internet 协议”(请参考 RFC-791。)IP 只是处理Internet路由而已。

那么数据报套接字呢?为何它叫无链接呢?为何它是不可靠的呢?有这样的一些事实:若是你发送一个数据报,它可能会到达,它可能次序颠倒了。若是它到达,那么在这个包的内部是无错误的。数据报也使用IP 做路由,可是它不使用TCP。它使用“用户数据报协议(User DatagramProtocol)”,也叫 “UDP” (请参考RFC-768。)

为何它们是无链接的呢?主要是由于它并不象流式套接字那样维持一个链接。你只要创建一个包,构造一个有目标信息的IP 头,而后发出去。无需链接。它们一般使用于传输包-包信息。简单的应用程序有:tftp, bootp等等。

你也许会想:“假如数据丢失了这些程序如何正常工做?”个人朋友,每一个程序在UDP上有本身的协议。例如,tftp协议每发出的一个被接受到包,收到者必须发回一个包来讲“我收到了!” (一个“命令正确应答”也叫“ACK” 包)。若是在必定时间内(例如5秒),发送方没有收到应答,它将从新发送,直到获得 ACK。这一ACK过程在实现SOCK_DGRAM 应用程序的时候很是重要。

6、网络理论

  既然我刚才提到了协议层,那么如今是讨论网络究竟如何工做和一些关于 SOCK_DGRAM包是如何创建的例子。固然,你也能够跳过这一段, 若是你认为已经熟悉的话。

如今是学习数据封装(Data Encapsulation)的时候了!它很是很是重要。它重要性重要到你在网络课程学(图1:数据封装)习中不管如何也得也得掌握它。主要 的内容是:一个包,先是被第一个协议(在这里是TFTP )在它的报头(也许 是报尾)包装(“封装”),而后,整个数据(包括 TFTP头)被另一个协议 (在这里是 UDP )封装,而后下一个(IP ),一直重复下去,直到硬件(物理)层( 这里是以太网 )。

当另一台机器接收到包,硬件先剥去以太网头,内核剥去IP和UDP 头,TFTP程序再剥去TFTP头,最后获得数据。如今咱们终于讲到声名狼藉的网络分层模型 (Layered NetworkModel)。这种网络模型在描述网络系统上相对其它模型有不少优势。例如,你能够写一个套接字程序而不用关心数据的物理传输(串行口,以太网,连 接单元接口 (AUI) 仍是其它介质),由于底层的程序会为你处理它们。实际 的网络硬件和拓扑对于程序员来讲是透明的。

不说其它废话了,我如今列出整个层次模型。若是你要参加网络考试,可必定要记住:

应用层 (Application)

表示层 (Presentation)

会话层 (Session)

传输层(Transport)

网络层(Network)

数据链路层(Data Link)

物理层(Physical)

物理层是硬件(串口,以太网等等)。应用层是和硬件层相隔最远的--它 是用户和网络交互的地方。

这个模型如此通用,若是你想,你能够把它做为修车指南。把它对应 到Unix,结果是:

应用层(Application Layer) (telnet, ftp,等等)

传输层(Host-to-Host Transport Layer) (TCP, UDP)

Internet层(Internet Layer) (IP和路由)

网络访问层 (Network Access Layer)(网络层,数据链路层和物理层)

如今,你可能看到这些层次如何协调来封装原始的数据了。

看看创建一个简单的数据包有多少工做?哎呀,你将不得不使用"cat"来创建数据包头!这仅仅是个玩笑。对于流式套接字你要做的是 send() 发 送数据。对于数据报式套接字,你按照你选择的方式封装数据而后使用sendto()。内核将为你创建传输层和 Internet 层,硬件完成网络访问层。这就是现代科技。

如今结束咱们的网络理论速成班。哦,忘记告诉你关于路由的事情了。可是我不许备谈它,若是你真的关心,那么参考 IPRFC。

7、结构体

  终于谈到编程了。在这章,我将谈到被套接字用到的各类数据类型。由于它们中的一些内容很重要了。

首先是简单的一个:socket描述符。它是下面的类型:

int 仅仅是一个常见的 int。

从如今起,事情变得难以想象了,而你所需作的就是继续看下去。注意这样的事实:有两种字节排列顺序:重要的字节 (有时叫"octet",即八 位位组) 在前面,或者不重要的字节在前面。前一种叫“网络字节顺序 (Network ByteOrder)”。有些机器在内部是按照这个顺序储存数据,而另外 一些则否则。当我说某数据必须按照 NBO 顺序,那么你要调用函数(例如htons() )来将它从本机字节顺序 (Host Byte Order) 转换过来。若是我没有 提到 NBO, 那么就让它保持本机字节顺序。 

个人第一个结构(在这个技术手册TM中)--struct sockaddr。这个结构 为许多类型的套接字储存套接字地址信息:

structsockaddr

unsigned short sa_family;

char sa_data[14];  

}; 

sa_family可以是各类各样的类型,可是在这篇文章中都是"AF_INET"。 sa_data包含套接字中的目标地址和端口信息。这好像有点不明智。

为了处理struct sockaddr,程序员创造了一个并列的结构: structsockaddr_in ("in"表明"Internet"。)

struct sockaddr_in

short int sin_family; 

unsigned short int sin_port; 

struct in_addr sin_addr; 

unsigned char sin_zero[8]; 

}; 

用这个数据结构能够轻松处理套接字地址的基本元素。注意sin_zero(它被加入到这个结构,而且长度和 structsockaddr 同样)应该使用函数 bzero()或memset() 来所有置零。同时,这一重要的字节,一个指向 sockaddr_in结构体的指针也能够被指向结构体sockaddr而且代替它。这 样的话即便 socket() 想要的是 struct sockaddr *,你仍然可使用 struct sockaddr_in,而且在最后转换。同时,注意sin_family 和 struct sockaddr 中的 sa_family 一致并可以设置为 "AF_INET"。最后,sin_port和 sin_addr 必须是网络字节顺序 (Network Byte Order)!

你也许会反对道:"可是,怎么让整个数据结构 struct in_addr sin_addr 按照网络字节顺序呢?" 要知道这个问题的答案,咱们就要仔细的看一看这 个数据结构:struct in_addr, 有这样一个联合(unions):

 

struct in_addr

unsigned long s_addr; 

}; 

 

它曾经是个最坏的联合,可是如今那些日子过去了。若是你声明"ina" 是数据结构 struct sockaddr_in 的实例,那么"ina.sin_addr.s_addr" 就储存4字节的 IP 地址(使用网络字节顺序)。若是你不幸的系统使用的仍是恐 怖的联合 struct in_addr ,你仍是能够放心4字节的 IP地址而且和上面 我说的同样(这是由于使用了“#define”。

 

8、本机转换

  咱们如今到了新的章节。咱们曾经讲了不少网络到本机字节顺序的转换,如今能够实践了!

你可以转换两种类型: short (两个字节)和 long(四个字节)。这个函数对于变量类型 unsigned也适用。假设你想将 short 从本机字节顺序转换为网络字节顺序。用 "h" 表示"本机 (host)",接着是 "to",而后用 "n" 表 示 "网络 (network)",最后用 "s" 表示 "short": h-to-n-s, 或者 htons() ("Host to Network Short")。

太简单了... 

若是不是太傻的话,你必定想到了由"n","h","s",和"l"造成的正确组合,例如这里确定没有stolh() ("Short toLong Host") 函数,不只在这里 没有,全部场合都没有。可是这里有:

htons()--"Host to Network Short"

htonl()--"Host to Network Long"

ntohs()--"Network to Host Short"

ntohl()--"Network to Host Long"

如今,你可能想你已经知道它们了。你也可能想:“若是我想改变char 的顺序要怎么办呢?” 可是你也许立刻就想到,“用不着考虑的”。你也许 会想到:个人68000机器已经使用了网络字节顺序,我没有必要去调用htonl() 转换 IP 地址。你多是对的,可是当你移植你的程序到别的机器 上的时候,你的程序将失败。可移植性!这里是Unix 世界!记住:在你将数据放到网络上的时候,确信它们是网络字节顺序的。

最后一点:为何在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 须要转换为网络字节顺序,而sin_family 需不须要呢? 答案是: sin_addr 和sin_port 分别封装在包的 IP 和 UDP层。所以,它们必需要 是网络字节顺序。可是 sin_family 域只是被内核 (kernel) 使用来决定在数据结构中包含什么类型的地址,因此它必须是本机字节顺序。同时,sin_family没有发送到网络上,它们能够是本机字节顺序。

9IP 地址和如何处理它们

如今咱们很幸运,由于咱们有不少的函数来方便地操做IP 地址。没有必要用手工计算它们,也没有必要用"<< span>操做来储存成长整字型。首先,假设你已经有了一个sockaddr_in结构体ina,你有一个IP地 址"132.241.5.10"要储存在其中,你就要用到函数inet_addr(),将IP地址从 点数格式转换成无符号长整型。使用方法以下:

ina.sin_addr.s_addr = inet_addr("132.241.5.10");

注意,inet_addr()返回的地址已是网络字节格式,因此你无需再调用 函数htonl()。

咱们如今发现上面的代码片段不是十分完整的,由于它没有错误检查。显而易见,当inet_addr()发生错误时返回-1。记住这些二进制数字?(无符 号数)-1仅仅和IP地址255.255.255.255相符合!这但是广播地址!大错特错!记住要先进行错误检查。

好了,如今你能够将IP地址转换成长整型了。有没有其相反的方法呢? 它能够将一个in_addr结构体输出成点数格式?这样的话,你就要用到函数inet_ntoa()("ntoa"的含义是"network to ascii"),就像这样:

printf("%s",inet_ntoa(ina.sin_addr));

它将输出IP地址。须要注意的是inet_ntoa()将结构体in-addr做为一 个参数,不是长整形。一样须要注意的是它返回的是一个指向一个字符的指针。它是一个由inet_ntoa()控制的静态的固定的指针,因此每次调用 inet_ntoa(),它就将覆盖上次调用时所得的IP地址。例如:

char *a1, *a2;

a1 = inet_ntoa(ina1.sin_addr);

a2 = inet_ntoa(ina2.sin_addr);

printf("address 1: %s/n",a1);

printf("address 2: %s/n",a2);

 

输出以下:

 

address 1:132.241.5.10

address 2:132.241.5.10

假如你须要保存这个IP地址,使用strcopy()函数来指向你本身的字符 指针。

上面就是关于这个主题的介绍。稍后,你将学习将一个相似"wintehouse.gov"的字符串转换成它所对应的IP地址(查阅域名服务,稍后)。

 

10socket()函数

我想我不能再不提这个了-下面我将讨论一下socket()系统调用。

下面是详细介绍:

#include 

#include 

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

可是它们的参数是什么? 首先,domain 应该设置成"AF_INET",就 象上面的数据结构struct sockaddr_in 中同样。而后,参数 type 告诉内核 是 SOCK_STREAM 类型仍是 SOCK_DGRAM 类型。最后,把 protocol 设置为 "0"。(注意:有不少种 domain、type,我不可能一一列出了,请看 socket() 的 man帮助。固然,还有一个"更好"的方式去获得 protocol。同 时请查阅 getprotobyname() 的 man 帮助。

socket()只是返回你之后在系统调用种可能用到的socket 描述符,或者在错误的时候返回-1。全局变量errno中将储存返回的错误值。(请参考perror() 的 man 帮助。

 

11bind()函数

  一旦你有一个套接字,你可能要将套接字和机器上的必定的端口关联起来。(若是你想用listen()来侦听必定端口的数据,这是必要一步--MUD 告 诉你说用命令 "telnet x.y.z 6969"。)若是你只想用 connect(),那么这个步 骤没有必要。可是不管如何,请继续读下去。

这里是系统调用 bind() 的大概:

#include

#include

int bind(intsockfd, struct sockaddr *my_addr, intaddrlen); 

sockfd是调用socket 返回的文件描述符。my_addr 是指向数据结构struct sockaddr 的指针,它保存你的地址(即端口和 IP 地址)信息。 addrlen 设置为sizeof(structsockaddr)。

简单得很不是吗? 再看看例子:

#include

#include

#include

#define MYPORT 3490 

Void  main()

{

int sockfd;

struct sockaddr_in my_addr;

sockfd = socket(AF_INET, SOCK_STREAM,0);

my_addr.sin_family = AF_INET; 

my_addr.sin_port = htons(MYPORT); 

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

bzero(&(my_addr.sin_zero),; 

 

bind(sockfd, (struct sockaddr*)&my_addr, sizeof(struct sockaddr));

}

这里也有要注意的几件事情。my_addr.sin_port 是网络字节顺序,my_addr.sin_addr.s_addr也是的。另外要注意到的事情是因系统的不一样, 包含的头文件也不尽相同,请查阅本地的 man 帮助文件。

在 bind() 主题中最后要说的话是,在处理本身的 IP 地址和/或端口的 时候,有些工做是能够自动处理的。

 

my_addr.sin_port = 0; 

my_addr.sin_addr.s_addr = INADDR_ANY; 

 

经过将0赋给 my_addr.sin_port,你告诉 bind() 本身选择合适的端 口。一样,将my_addr.sin_addr.s_addr 设置为 INADDR_ANY,你告诉 它自动填上它所运行的机器的 IP 地址。

若是你一贯当心谨慎,那么你可能注意到我没有将INADDR_ANY 转换为网络字节顺序!这是由于我知道内部的东西:INADDR_ANY 实际上就 是 0!即便你改变字节的顺序,0依然是0。可是完美主义者说应该到处一 致,INADDR_ANY或许是12呢?你的代码就不能工做了,那么就看下面 的代码:

my_addr.sin_port = htons(0); 

my_addr.sin_addr.s_addr =htonl(INADDR_ANY); 

你或许不相信,上面的代码将能够随便移植。我只是想指出,既然你所遇到的程序不会都运行使用htonl的INADDR_ANY。

bind()在错误的时候依然是返回-1,而且设置全局错误变量errno。

在你调用 bind() 的时候,你要当心的另外一件事情是:不要采用小于 1024的端口号。全部小于1024的端口号都被系统保留!你能够选择从1024 到65535的端口(若是它们没有被别的程序使用的话)。

你要注意的另一件小事是:有时候你根本不须要调用它。若是你使 用connect()来和远程机器进行通信,你不须要关心你的本地端口号(就象 你在使用 telnet 的时候),你只要简单的调用 connect() 就能够了,它会检查套接字是否绑定端口,若是没有,它会本身绑定一个没有使用的本地端口。

 

12connect()程序

  如今咱们假设你是个 telnet 程序。你的用户命令你获得套接字的文件描述符。你遵从命令调用了socket()。下一步,你的用户告诉你经过端口 23(标准 telnet 端口)链接到"132.241.5.10"。你该怎么作呢? 幸运的是,你正在阅读 connect()--如何链接到远程主机这一章。你可 不想让你的用户失望。

connect()系统调用是这样的:

#include 

#include

int connect(int sockfd, struct sockaddr*serv_addr, int addrlen); 

 

sockfd是系统调用socket() 返回的套接字文件描述符。serv_addr 是 保存着目的地端口和 IP 地址的数据结构 struct sockaddr。addrlen 设置 为 sizeof(struct sockaddr)。

想知道得更多吗?让咱们来看个例子:

#include 

#include 

#include 

#define DEST_IP"132.241.5.10" 

#define DEST_PORT 23 

main() 

int sockfd; 

struct sockaddr_in dest_addr; 

sockfd = socket(AF_INET, SOCK_STREAM, 0); 

dest_addr.sin_family = AF_INET; 

dest_addr.sin_port = htons(DEST_PORT); 

dest_addr.sin_addr.s_addr =inet_addr(DEST_IP); 

bzero(&(dest_addr.sin_zero),; 

 

connect(sockfd, (struct sockaddr*)&dest_addr, sizeof(struct sockaddr));

  再一次,你应该检查 connect() 的返回值--它在错误的时候返回-1,并 设置全局错误变量 errno。

同时,你可能看到,我没有调用 bind()。由于我不在意本地的端口号。我只关心我要去那。内核将为我选择一个合适的端口号,而咱们所链接的 地方也自动地得到这些信息。一切都不用担忧。

 

13listen()函数

  是换换内容得时候了。假如你不但愿与远程的一个地址相连,或者说,仅仅是将它踢开,那你就须要等待接入请求而且用各类方法处理它们。处 理过程分两步:首先,你听--listen(),而后,你接受--accept() (请看下面的 内容)。

除了要一点解释外,系统调用 listen 也至关简单。

int listen(intsockfd, int backlog); 

sockfd是调用socket() 返回的套接字文件描述符。backlog 是在进入 队列中容许的链接数目。什么意思呢? 进入的链接是在队列中一直等待直 到你接受 (accept() 请看下面的文章)链接。它们的数目限制于队列的容许。 大多数系统的容许数目是20,你也能够设置为5到10。

和别的函数同样,在发生错误的时候返回-1,并设置全局错误变量 errno。

你可能想象到了,在你调用 listen() 前你或者要调用 bind() 或者让内核随便选择一个端口。若是你想侦听进入的链接,那么系统调用的顺序可 能是这样的:

socket(); 

bind(); 

listen(); 

 

由于它至关的明了,我将在这里不给出例子了。(在 accept() 那一章的 代码将更加彻底。)真正麻烦的部分在 accept()。

14accept()函数

  准备好了,系统调用 accept() 会有点古怪的地方的!你能够想象发生这样的事情:有人从很远的地方经过一个你在侦听 (listen()) 的端口链接 (connect()) 到你的机器。它的链接将加入到等待接受 (accept()) 的队列 中。你调用 accept() 告诉它你有空闲的链接。它将返回一个新的套接字文件描述符!这样你就有两个套接字了,原来的一个还在侦听你的那个端口, 新的在准备发送 (send()) 和接收 ( recv()) 数据。这就是这个过程!

函数是这样定义的:

#include

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

sockfd至关简单,是和listen() 中同样的套接字描述符。addr 是个指 向局部的数据结构 sockaddr_in 的指针。这是要求接入的信息所要去的地方(你能够测定那个地址在那个端口呼叫你)。在它的地址传递给 accept 之 前,addrlen 是个局部的整形变量,设置为 sizeof(struct sockaddr_in)。 accept 将不会将多余的字节给 addr。若是你放入的少些,那么它会经过改

变 addrlen 的值反映出来。

一样,在错误时返回-1,并设置全局错误变量 errno。

如今是你应该熟悉的代码片断。

#include

#include

#include

#define MYPORT 3490  

#define BACKLOG 10  

main() 

int sockfd, new_fd;  

struct sockaddr_in my_addr; 

struct sockaddr_in their_addr; 

int sin_size; 

sockfd = socket(AF_INET, SOCK_STREAM, 0); 

my_addr.sin_family = AF_INET; 

my_addr.sin_port = htons(MYPORT); 

my_addr.sin_addr.s_addr = INADDR_ANY; 

bzero(&(my_addr.sin_zero),; 

 

bind(sockfd, (struct sockaddr*)&my_addr, sizeof(structsockaddr)); 

listen(sockfd,BACKLOG); 

sin_size = sizeof(structsockaddr_in); 

new_fd = accept(sockfd,&their_addr,&sin_size);

}

注意,在系统调用 send() 和 recv() 中你应该使用新的套接字描述符 new_fd。若是你只想让一个链接进来,那么你可使用 close() 去关闭原 来的文件描述符 sockfd 来避免同一个端口更多的链接。

15send() and recv()函数

  这两个函数用于流式套接字或者数据报套接字的通信。若是你喜欢使用无链接的数据报套接字,你应该看一看下面关于sendto() 和recvfrom() 的章节。

send()是这样的:

int send(intsockfd, const void *msg, int len, intflags); 

sockfd是你想发送数据的套接字描述符(或者是调用 socket() 或者是accept() 返回的。)msg 是指向你想发送的数据的指针。len 是数据的长度。 把 flags 设置为 0 就能够了。(详细的资料请看send() 的 manpage)。

这里是一些可能的例子:

char *msg = "Beej washere!"; 

int len, bytes_sent;

len = strlen(msg);

bytes_sent = send(sockfd, msg, len,0);

send()返回实际发送的数据的字节数--它可能小于你要求发送的数 目!注意,有时候你告诉它要发送一堆数据但是它不能处理成功。它只是 发送它可能发送的数据,而后但愿你可以发送其它的数据。记住,若是send() 返回的数据和len不匹配,你就应该发送其它的数据。可是这里也 有个好消息:若是你要发送的包很小(小于大约 1K),它可能处理让数据一 次发送完。最后要说得就是,它在错误的时候返回-1,并设置 errno。

recv()函数很类似:

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

sockfd是要读的套接字描述符。buf 是要读的信息的缓冲。len 是缓 冲的最大长度。flags 能够设置为0。(请参考recv() 的 manpage。) recv()返回实际读入缓冲的数据的字节数。或者在错误的时候返回-1, 同时设置 errno。

很简单,不是吗? 你如今能够在流式套接字上发送数据和接收数据了。 你如今是 Unix 网络程序员了!

16sendto() 和 recvfrom()函数

  “这很不错啊”,你说,“可是你尚未讲无链接数据报套接字呢?”没问题,如今咱们开始这个内容。

既然数据报套接字不是链接到远程主机的,那么在咱们发送一个包以前须要什么信息呢?不错,是目标地址!看看下面的:

int sendto(intsockfd, const void *msg, int len, unsigned intflags, 

const struct sockaddr *to, inttolen); 

你已经看到了,除了另外的两个信息外,其他的和函数send() 是同样 的。to 是个指向数据结构 struct sockaddr 的指针,它包含了目的地的IP 地址和端口信息。tolen 能够简单地设置为 sizeof(struct sockaddr)。 和函数 send() 相似,sendto() 返回实际发送的字节数(它也可能小于 你想要发送的字节数!),或者在错误的时候返回 -1。

类似的还有函数 recv() 和 recvfrom()。recvfrom() 的定义是这样的:

int recvfrom(int sockfd, void *buf, int len,unsigned int flags,struct sockaddr *from, int *fromlen);

又一次,除了两个增长的参数外,这个函数和 recv() 也是同样的。from 是一个指向局部数据结构 struct sockaddr 的指针,它的内容是源机器的 IP 地址和端口信息。fromlen 是个 int 型的局部指针,它的初始值为 sizeof(struct sockaddr)。函数调用返回后,fromlen 保存着实际储存在 from 中的地址的长度。

recvfrom()返回收到的字节长度,或者在发生错误后返回 -1。

记住,若是你用 connect() 链接一个数据报套接字,你能够简单的调 用 send() 和 recv() 来知足你的要求。这个时候依然是数据报套接字,依 然使用UDP,系统套接字接口会为你自动加上了目标和源的信息。

17close()和shutdown()函数

  你已经成天都在发送 (send()) 和接收 (recv()) 数据了,如今你准备关 闭你的套接字描述符了。这很简单,你可使用通常的Unix 文件描述符 的 close() 函数:

close(sockfd);

它将防止套接字上更多的数据的读写。任何在另外一端读写套接字的企图都将返回错误信息。

若是你想在如何关闭套接字上有多一点的控制,你可使用函数shutdown()。它容许你将必定方向上的通信或者双向的通信(就象close()一 样)关闭,你可使用:

int shutdown(intsockfd, int how); 

sockfd是你想要关闭的套接字文件描述复。how 的值是下面的其中之 一:

0 - 不容许接受

1 - 不容许发送

2 - 不容许发送和接受(和close()同样)

shutdown()成功时返回 0,失败时返回 -1(同时设置 errno。)若是在无链接的数据报套接字中使用shutdown(),那么只不过是让 send() 和 recv() 不能使用(记住你在数据报套接字中使用了 connect 后 是可使用它们的)。

18getpeername()函数

  这个函数太简单了。

它太简单了,以致我都不想单列一章。可是我仍是这样作了。 函数getpeername()告诉你在链接的流式套接字上谁在另一边。函 数是这样的:

#include

int getpeername(int sockfd, struct sockaddr *addr,int *addrlen);

sockfd是链接的流式套接字的描述符。addr 是一个指向结构 struct sockaddr (或者是 struct sockaddr_in) 的指针,它保存着链接的另外一边的 信息。addrlen 是一个 int 型的指针,它初始化为sizeof(struct sockaddr)。 函数在错误的时候返回 -1,设置相应的 errno。

一旦你得到它们的地址,你可使用 inet_ntoa() 或者 gethostbyaddr()来打印或者得到更多的信息。可是你不能获得它的账号。(若是它运行着愚 蠢的守护进程,这是可能的,可是它的讨论已经超出了本文的范围,请参考RFC-1413以得到更多的信息。

19gethostname()函数

  甚至比 getpeername() 还简单的函数是 gethostname()。它返回你程 序所运行的机器的主机名字。而后你可使用gethostbyname() 以得到你的机器的 IP 地址。

  下面是定义:

#include

intgethostname(char *hostname, size_t size);

参数很简单:hostname 是一个字符数组指针,它将在函数返回时保存

主机名。size是hostname 数组的字节长度。

函数调用成功时返回 0,失败时返回 -1,并设置errno。

20、域名服务(DNS)

  若是你不知道 DNS 的意思,那么我告诉你,它表明域名服务(Domain NameService)。它主要的功能是:你给它一个容易记忆的某站点的地址, 它给你 IP 地址(而后你就可使用 bind(), connect(), sendto() 或者其它 函数) 。当一我的输入:

$ telnet whitehouse.gov 

telnet能知道它将链接(connect()) 到 "198.137.240.100"。

可是这是如何工做的呢? 你能够调用函数 gethostbyname():

#include

struct hostent *gethostbyname(const char*name); 

很明白的是,它返回一个指向 struct hostent 的指针。这个数据结构 是这样的:

struct hostent

{

char *h_name;

char **h_aliases;

int h_addrtype;

int h_length;

char **h_addr_list;

};

#define h_addrh_addr_list[0] 

这里是这个数据结构的详细资料:

structhostent: 

h_name - 地址的正式名称。

h_aliases - 空字节-地址的预备名称的指针。

h_addrtype -地址类型; 一般是AF_INET。

h_length - 地址的比特长度。

h_addr_list - 零字节-主机网络地址指针。网络字节顺序。

h_addr - h_addr_list中的第一地址。

gethostbyname()成功时返回一个指向结构体 hostent 的指针,或者 是个空 (NULL) 指针。(可是和之前不一样,不设置errno,h_errno 设置错 误信息。请看下面的 herror()。

可是如何使用呢? 有时候(咱们能够从电脑手册中发现),向读者灌输信息是不够的。这个函数可不象它看上去那么难用。

这里是个例子:

#include

#include

#include

#include

#include

#include

int main(int argc, char *argv[])

{

struct hostent *h;

if (argc != 2)

{

fprintf(stderr,"usage: getip address/n");

exit(1);

}

if ((h=gethostbyname(argv[1])) == NULL)

{

herror("gethostbyname");

exit(1);

}

printf("Host name : %s/n",h->h_name);

printf("IP Address : %s/n",inet_ntoa(*((structin_addr *)h->h_addr)));

return 0;

}

在使用 gethostbyname() 的时候,你不能用 perror() 打印错误信息 (由于 errno 没有使用),你应该调用 herror()。

至关简单,你只是传递一个保存机器名的字符串(例如 "whitehouse.gov") 给 gethostbyname(),而后从返回的数据结构 struct hostent 中获取信息。

惟一也许让人不解的是输出 IP 地址信息。h->h_addr 是一个 char *, 可是 inet_ntoa() 须要的是 struct in_addr。所以,我转换 h->h_addr 成 struct in_addr *,而后获得数据。

 

21、客户-服务器背景知识

  这里是个客户--服务器的世界。在网络上的全部东西都是在处理客户进程和服务器进程的交谈。举个telnet的例子。当你用 telnet(客户)经过23 号端口登录到主机,主机上运行的一个程序(通常叫 telnetd,服务器)激活。 它处理这个链接,显示登录界面,等等。

 

2:客户机和服务器的关系

图 2 说明了客户和服务器之间的信息交换。

注意,客户--服务器之间可使用SOCK_STREAM、SOCK_DGRAM 或者其它(只要它们采用相同的)。一些很好的客户--服务器的例子有telnet/telnetd、 ftp/ftpd 和bootp/bootpd。每次你使用 ftp 的时候,在远 端都有一个 ftpd 为你服务。

通常,在服务端只有一个服务器,它采用 fork() 来处理多个客户的链接。基本的程序是:服务器等待一个链接,接受(accept()) 链接,而后 fork() 一个子进程处理它。这是下一章咱们的例子中会讲到的。

 

22、简单的服务器

  这个服务器所作的所有工做是在流式链接上发送字符串"Hello,World!/n"。你要测试这个程序的话,能够在一台机器上运行该程序,而后 在另一机器上登录:

$ telnet remotehostname3490 

remotehostname是该程序运行的机器的名字。

服务器代码:

#include

#include

#include

#include

#include

#include

#include

#include

#define MYPORT 3490  

#define BACKLOG 10  

main() 

int sockfd, new_fd; 

struct sockaddr_in my_addr; 

struct sockaddr_in their_addr; 

int sin_size;

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) ==-1)

perror("socket"); 

exit(1); 

}

my_addr.sin_family = AF_INET; 

my_addr.sin_port = htons(MYPORT); 

my_addr.sin_addr.s_addr =INADDR_ANY;  

bzero(&(my_addr.sin_zero),;

if (bind(sockfd, (struct sockaddr*)&my_addr,sizeof(struct sockaddr))== -1)

perror("bind"); 

exit(1); 

if (listen(sockfd, BACKLOG) == -1)

perror("listen"); 

exit(1); 

}

while(1) {  

sin_size = sizeof(structsockaddr_in); 

if ((new_fd = accept(sockfd,(struct sockaddr *)&their_addr,/ 

&sin_size)) ==-1)

perror("accept"); 

continue; 

printf("server: got connectionfrom %s/n", / 

inet_ntoa(their_addr.sin_addr)); 

if (!fork())

{  

if (send(new_fd, "Hello, world!/n", 14, 0) ==-1) 

perror("send"); 

close(new_fd); 

exit(0); 

close(new_fd); 

while(waitpid(-1,NULL,WNOHANG) >0);  

若是你很挑剔的话,必定不满意我全部的代码都在一个很大的main() 函数中。若是你不喜欢,能够划分得更细点。

你也能够用咱们下一章中的程序获得服务器端发送的字符串。

 

23、简单的客户程序

  这个程序比服务器还简单。这个程序的全部工做是经过3490端口链接到命令行中指定的主机,而后获得服务器发送的字符串。

客户代码

#include

#include

#include

#include

#include

#include

#include

#include

#define PORT 3490  

#define MAXDATASIZE 100 

int main(int argc, char*argv[]) 

int sockfd,numbytes; 

charbuf[MAXDATASIZE]; 

struct hostent*he; 

struct sockaddr_in their_addr; 

if (argc != 2)

Fprintf(stderr,"usage: clienthostname/n"); 

exit(1); 

if ((he=gethostbyname(argv[1])) ==NULL)

{  

herror("gethostbyname"); 

exit(1); 

}

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) ==-1)

perror("socket"); 

exit(1); 

}

their_addr.sin_family = AF_INET; 

their_addr.sin_port = htons(PORT); 

their_addr.sin_addr = *((struct in_addr*)he->h_addr); 

bzero(&(their_addr.sin_zero),; 

if (connect(sockfd, (struct sockaddr*)&their_addr,sizeof(struct 

sockaddr)) == -1)

perror("connect"); 

exit(1); 

if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0))== -1)

perror("recv"); 

exit(1); 

buf[numbytes] = '/0'; 

printf("Received:%s",buf); 

close(sockfd);  

return 0; 

注意,若是你在运行服务器以前运行客户程序,connect() 将返回 "Connection refused" 信息,这很是有用。

 

24、数据包Sockets

  我不想讲更多了,因此我给出代码 talker.c 和 listener.c。

listener在机器上等待在端口4590 来的数据包。talker 发送数据包到必定的机器,它包含用户在命令行输入的内容。

这里就是 listener.c:

#include

#include

#include

#include

#include

#include

#include

#include

#define MYPORT 4950  

#define MAXBUFLEN 100 

Void main() 

intsockfd; 

struct sockaddr_in my_addr; 

struct sockaddr_in their_addr; 

int addr_len,numbytes; 

charbuf[MAXBUFLEN]; 

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) ==-1)

perror("socket"); 

exit(1); 

my_addr.sin_family = AF_INET; 

my_addr.sin_port =htons(MYPORT);  

my_addr.sin_addr.s_addr =INADDR_ANY;  

bzero(&(my_addr.sin_zero),; 

if (bind(sockfd, (struct sockaddr*)&my_addr, sizeof(struct sockaddr))==-1){ 

perror("bind"); 

exit(1); 

}

addr_len = sizeof(structsockaddr); 

if ((numbytes=recvfrom(sockfd,buf, MAXBUFLEN, 0, / 

(struct sockaddr*)&their_addr, &addr_len)) ==-1)

perror("recvfrom"); 

exit(1); 

printf("got packet from%s/n",inet_ntoa(their_addr.sin_addr)); 

printf("packet is %d byteslong/n",numbytes); 

buf[numbytes] ='/0'; 

printf("packet contains/"%s/"/n",buf); 

close(sockfd); 

注意在咱们的调用 socket(),咱们最后使用了 SOCK_DGRAM。同时, 没有必要去使用 listen() 或者 accept()。咱们在使用无链接的数据报套接 字!

下面是 talker.c:

#include

#include

#include

#include

#include

#include

#include

#include

#define MYPORT 4950  

int main(int argc, char*argv[]) 

intsockfd; 

struct sockaddr_in their_addr; 

struct hostent*he; 

int numbytes;

if (argc != 3)

fprintf(stderr,"usage: talker hostnamemessage/n"); 

exit(1); 

}

if ((he=gethostbyname(argv[1])) ==NULL)

{  

herror("gethostbyname"); 

exit(1); 

}

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) ==-1)

perror("socket"); 

exit(1); 

}

their_addr.sin_family = AF_INET; 

their_addr.sin_port =htons(MYPORT);  

their_addr.sin_addr = *((structin_addr*)he->h_addr); 

bzero(&(their_addr.sin_zero),; 

if ((numbytes=sendto(sockfd, argv[2],strlen(argv[2]), 0, / 

(struct sockaddr*)&their_addr, sizeof(struct sockaddr))) ==-1)

perror("sendto"); 

exit(1); 

printf("sent %d bytes to %s/n",numbytes,inet_ntoa(their_addr.sin_addr)); 

close(sockfd); 

return 0; 

}  

这就是全部的了。在一台机器上运行 listener,而后在另一台机器上 运行 talker。观察它们的通信!

除了一些我在上面提到的数据套接字链接的小细节外,对于数据套接字,我还得说一些,当一个讲话者呼叫connect()函数时并指定接受者的地 址时,从这点能够看出,讲话者只能向connect()函数指定的地址发送和接受信息。所以,你不须要使用sendto()和recvfrom(),你彻底能够用send() 和recv()代替。

 

25、阻塞

  阻塞,你也许早就据说了。"阻塞"是"sleep"的科技行话。你可能注意 到前面运行的 listener 程序,它在那里不停地运行,等待数据包的到来。 实际在运行的是它调用recvfrom(),而后没有数据,所以recvfrom() 说" 阻塞(block)",直到数据的到来。

不少函数都利用阻塞。accept() 阻塞,全部的 recv*() 函数阻塞。它 们之因此能这样作是由于它们被容许这样作。当你第一次调用socket() 创建套接字描述符的时候,内核就将它设置为阻塞。若是你不想套接字阻塞, 你就要调用函数 fcntl():

#include

#include

sockfd = socket(AF_INET, SOCK_STREAM,0); 

fcntl(sockfd, F_SETFL,O_NONBLOCK); 

  经过设置套接字为非阻塞,你可以有效地"询问"套接字以得到信息。如 果你尝试着从一个非阻塞的套接字读信息而且没有任何数据,它不容许阻塞--它将返回 -1 并将errno 设置为 EWOULDBLOCK。

可是通常说来,这种询问不是个好主意。若是你让你的程序在忙等状态查询套接字的数据,你将浪费大量的 CPU时间。更好的解决之道是用 下一章讲的 select() 去查询是否有数据要读进来。

 

26select()--多路同步 I/O

  虽然这个函数有点奇怪,可是它颇有用。假设这样的状况:你是个服务器,你一边在不停地从链接上读数据,一边在侦听链接上的信息。 没问题,你可能会说,不就是一个 accept() 和两个 recv() 吗?这么 容易吗,朋友? 若是你在调用accept()的时候阻塞呢? 你怎么可以同时接 受recv() 数据? “用非阻塞的套接字啊!” 不行!你不想耗尽全部的CPU 吧? 那么,该如何是好?

select()让你能够同时监视多个套接字。若是你想知道的话,那么它就会告诉你哪一个套接字准备读,哪一个又准备写,哪一个套接字又发生了例外 (exception)。

闲话少说,下面是 select():

#include

#include

#include

int select(int numfds, fd_set *readfds, fd_set*writefds,fd_set 

*exceptfds, struct timeval *timeout);

 

这个函数监视一系列文件描述符,特别是 readfds、writefds 和exceptfds。若是你想知道你是否可以从标准输入和套接字描述符 sockfd 读入数据,你只要将文件描述符 0 和 sockfd 加入到集合readfds 中。参 数 numfds 应该等于最高的文件描述符的值加1。在这个例子中,你应该 设置该值为 sockfd+1。由于它必定大于标准输入的文件描述符 (0)。 当函数 select() 返回的时候,readfds 的值修改成反映你选择的哪一个 文件描述符能够读。你能够用下面讲到的宏FD_ISSET() 来测试。在咱们继续下去以前,让我来说讲如何对这些集合进行操做。每一个集 合类型都是fd_set。下面有一些宏来对这个类型进行操做:

FD_ZERO(fd_set*set) -清除一个文件描述符集合

FD_SET(int fd,fd_set *set) -添加fd到集合

FD_CLR(int fd,fd_set *set) -从集合中移去fd 

FD_ISSET(int fd,fd_set *set) -测试fd是否在集合中

最后,是有点古怪的数据结构 struct timeval。有时你可不想永远等待别人发送数据过来。也许什么事情都没有发生的时候你也想每隔96秒在终 端上打印字符串 "Still Going..."。这个数据结构容许你设定一个时间,若是 时间到了,而select()尚未找到一个准备好的文件描述符,它将返回让 你继续处理。

数据结构 struct timeval 是这样的:

struct timeval

int tv_sec;  

int tv_usec;  

}; 

只要将 tv_sec 设置为你要等待的秒数,将 tv_usec 设置为你要等待 的微秒数就能够了。是的,是微秒而不是毫秒。1,000微秒等于1毫秒,1,000 毫秒等于1秒。也就是说,1秒等于1,000,000微秒。为何用符号 "usec" 呢?字母 "u" 很象希腊字母Mu,而 Mu 表示 "微"的意思。固然,函数 返回的时候 timeout多是剩余的时间,之因此是可能,是由于它依赖于 你的 Unix 操做系统。

哈!咱们如今有一个微秒级的定时器!别计算了,标准的Unix 系统 的时间片是100毫秒,因此不管你如何设置你的数据结构 struct timeval,你都要等待那么长的时间。

还有一些有趣的事情:若是你设置数据结构 struct timeval 中的数据为 0,select() 将当即超时,这样就能够有效地轮询集合中的全部的文件描述 符。若是你将参数timeout 赋值为 NULL,那么将永远不会发生超时,即一直等到第一个文件描述符就绪。最后,若是你不是很关心等待多长时间, 那么就把它赋为 NULL 吧。

下面的代码演示了在标准输入上等待 2.5 秒:

#include

#include

#include

#define STDIN 0  

void main() 

struct timeval tv; 

fd_set readfds; 

tv.tv_sec = 2; 

tv.tv_usec = 500000; 

FD_ZERO(&readfds); 

FD_SET(STDIN,&readfds); 

 

select(STDIN+1, &readfds, NULL,NULL, &tv); 

i f (FD_ISSET(STDIN,&readfds)) 

printf("A key waspressed!/n"); 

else 

printf("Timedout./n"); 

若是你是在一个 line buffered 终端上,那么你敲的键应该是回车 (RETURN),不然不管如何它都会超时。

如今,你可能回认为这就是在数据报套接字上等待数据的方式--你是对 的:它多是。有些 Unix 系统能够按这种方式,而另一些则不能。你 在尝试之前可能要先看看本系统的man page 了。

最后一件关于 select() 的事情:若是你有一个正在侦听 (listen()) 的套 接字,你能够经过将该套接字的文件描述符加入到readfds 集合中来看是 否有新的链接。

这就是我关于函数select() 要讲的全部的东西。

27、参考书目:

Internetworking with TCP/IP, volumes I-III byDouglas E. Comer and 

David L. Stevens. Published by Prentice Hall.Second edition ISBNs: 

0-13-468505-9, 0-13-472242-6, 0-13-474222-2. Thereis a third edition of 

this set which covers IPv6 and IP overATM. 

Using C on the UNIX System by David A. Curry.Published by 

O'Reilly & Associates, Inc. ISBN0-937175-23-4. 

TCP/IP Network Administration by Craig Hunt.Published by O'Reilly 

& Associates, Inc. ISBN0-937175-82-X. 

TCP/IP Illustrated, volumes 1-3 by W. RichardStevens and Gary R. 

Wright. Published by Addison Wesley. ISBNs:0-201-63346-9, 

0-201-63354-X,0-201-63495-3. 

Unix Network Programming by W. Richard Stevens.Published by 

Prentice Hall. ISBN0-13-949876-1. 

On the web: 

BSD Sockets: A Quick And DirtyPrimer 

(http://www.cs.umn.edu/~bentlema/unix/--has othergreat Unix 

system programming info,too!) 

Client-ServerComputing 

(http://pandonia.canberra.edu.au/ClientServer/socket.html) 

Intro to TCP/IP (gopher)

(gopher://gopher-chem.ucdavis.edu/11/Index/Internet_aw/Intro_the_Inter

 

net/intro.to.ip/) 

Internet Protocol Frequently Asked Questions(France) 

(http://web.cnam.fr/Network/TCP-IP/) 

The Unix Socket FAQ 

(http://www.ibrado.com/sock-faq/) 

RFCs--the real dirt: 

RFC-768 -- The User Datagram Protocol

28、修改历史

 

版本

修改描述

修改时间

V1.0

初始版本

2012-08-14

相关文章
相关标签/搜索