Berkeley socket 套接字编程详解

来源:互联网 发布:入门级相机 知乎 编辑:程序博客网 时间:2024/05/17 23:59
                                              Berkeley 套接字
6.1 引言
网络程序设计全靠套接字接受和发送信息,尽管套接字这个词好象显得有些神秘,但
其实这个概念极易理解。
这章主要讲述Sockets API(Application Program Interface),以及一些其他的细节(比
如Socket 的历史、数据中的常用结构等),通过这些介绍,使读者慢慢掌握Linux 下的Socket
编程。
6.2 概述
在开始介绍有关编程的知识之前,首先让我们来了解一些与socket 有关的背景知识。
6.2.1 Socket 的历史
在80 年代早期,远景研究规划局(Advanced Research Projects Agency, ARPA)资助了
佳利福尼亚大学伯克利分校的一个研究组,让他们将TCP/IP 软件移植到UNIX 操作系统
中,并将结果提供给其他网点。作为项目的一部分,设计者们创建了一个接口,应用进程
使用这个接口可以方便的进行通信。他们决定,只要有可能就使用以有的系统调用,对那
些不能方便的容入已有的函数集的情况,就再增加新的系统调用以支持TCP/IP 功能。
这样做的结果就出现了插口接口(Berkeley Socket),这个系统被称为Berkeley UNIX
或BSD UNIX。(TCP/IP 首次出现在BSD 4.1 版本release 4.1 of Berkeley Software
Distribution)。
由许多计算机厂商,都采用了Berkeley UNIX,于是许多机器上都可以使用Socket 了。
这样,Socket 接口就被广泛使用,到现在已经成为事实上的标准。(图6-1)
6.2.2 Socket 的功能
Socket 的英文原意就是“孔”或“插座”,现在,作为BSD UNIX 的进程通讯机制,
取其后一种意义。日常生活中常见的插座,有的是信号插座,有的是电源插座,有的可以
接受信号(或能量),有的可以发送信号(或能量)。假如电话线与电话机之间安放一个插
座(相当于二者之间的接口,这一部分装置物理上是存在的)则Socket 非常相似于电话插
座。
将电话系统与面向连接的Socket 机制相比,有着惊人相似的地方。以一个国家级的电
话网为例。电话的通话双方相当于相互通信的两个进程;通话双方所在的地区(享有一个
全局唯一的区号)相当于一个网络,区号是它的网络地址;区内的一个单位的交换机相当
于一台主机,主机分配给每个用户的局内号码相当于Socket 号(下面将谈到)。
第6 章berkeley 套接字- 137 -
图6-1 socket 接口示意图
任何用户在通话之前,首先要占有一部电话机,相当于申请一个Socket 号;同时要知
道对方的电话号码,相当于对方有一个Socket。然后向对方拨号呼叫,相当于发出连接请
求(假如对方不在同一区内,还要拨对方区号,相当于给出网络地址)。对方假如在场并
空闲(相当于通信的另一主机开机且可以接受连接请求),拿起电话话筒,双方就可以正
式通话,相当于连接成功。双方通话的过程,是向电话机发出信号和从电话机接受信号的
过程,相当于向Socket 发送数据和从Socket 接受数据。通话结束后,一方挂起电话机,
相当于关闭Socket,撤消连接。
在电话系统中,一般用户只能感受到本地电话机和对方电话号码的存在,建立通话的
过程、话音传输的过程以及整个电话系统的技术细节对它都是透明的,这也与Socket 机制
非常相似。Socket 利用网间网通信设施实现进程通信,但它对通信设施的细节毫不关心,
只要通信设施能提供足够的通信能力,它就满足了。
至此,我们对Socket 进行了直观的描述。抽象出来,Socket 实质上提供了进程通信的
端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互
通信的。正如打电话之前,双方必须各自拥有一台电话机一样。
每一个Socket 都用一个半相关描述:
{协议,本地地址,本地端口}
一个完整的Socket 则用一个相关描述
{协议,本地地址,本地端口,远程地址,远程端口}
每一个Socket 有一个本地的唯一Socket 号,由操作系统分配。
最重要的是,Socket 是面向客户-服务器模型而设计的,针对客户和服务器程序提供
不同的Socket 系统调用。客户随机申请一个Socket 号(相当于一个想打电话的人可以在
- 138 - Linux网络编程
任何一台入网的电话上拨叫呼叫);服务器拥有全局公认的Socket,任何客户都可以向它
发出连接请求和信息请求(相当于一个被呼叫的电话拥有一个呼叫方知道的电话号码)。
Socket 利用客户— 服务器模式巧妙的解决了进程之间建立通信连接的问题。服务器
Socket 为全局所公认非常重要。两个完全随机的用户进程之间,因为没有任何一方的Socket
是固定的,就像打电话却不知道别人的电话号码,要通话是不可能的。
6.2.3 套接字的三种类型
套接字有三种类型:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)
及原始套接字。
1.流式套接字(SOCK_STREAM)
流式的套接字可以提供可靠的、面向连接的通讯流。如果你通过流式套接字发送了顺
序的数据:“1”、“2”。那么数据到达远程时候的顺序也是“1”、“2”。
流式套接字可以做什么呢?你听说过Telnet 应用程序吗?听过?哦,最常用的BBS 服
务,以及系统的远程登陆都是通过Telnet 协议连接的。Telnet 就是一个流式连接。你是否
希望你在Telnet 应用程序上输入的字符(或汉字)在到达远程应用程序的时候是以你输入
的顺序到达的?答案应该是肯定的吧。还有WWW 浏览器,它使用的HTTP 协议也是通过
流式套接字来获取网页的。事实上,如果你Telnet 到一个Web Site 的80 端口上,然后输
入“GET 网页路径名”然后按两下回车(或者是两下Ctrl+回车)然后你就得到了“网页
路径名”所代表的网页!
流式套接字是怎样保证这种应用层次上的数据传输质量呢?它使用了TCP( The
Transmission Control Protocol)协议(可以参考RFC-793 来得到TCP 的细节)。TCP 保证
了你的数据传输是正确的,并且是顺序的。TCP 是经常出现的TCP/IP 中的前半部分。IP
代表Internet Protocol(因特网协议,参考RFC-791)IP 只处理网络路由。
第6 章berkeley 套接字- 139 -
图6-2 面向连接的socket 的工作流程
2.数据报套接字(SOCK_DGRAM)
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序
的,并且不保证可靠,无差错。原始套接字允许对低层协议如IP 或ICMP 直接访问,主要
用于新的网络协议实现的测试等。
数据报套接字(Datagram Sockets)怎样呢?为什么它叫做“无连接”?应该怎样处理
它们呢?为什么它们是不可靠的?好的,这里有一些事实:
l 如果你发送了一个数据报,它可能不会到达。
l 它可能会以不同的顺序到达。
l 如果它到达了,它包含的数据中可能存在错误。
数据报套接字也使用IP,但是它不使用TCP,它使用使用者数据报协议UDP(User
Datagram Protocol 可以参考RFC 768)
为什么说它们是“无连接”的呢?因为它(UDP)不像流式套接字那样维护一个打开
- 140 - Linux网络编程
的连接,你只需要把数据打成一个包,把远程的IP 贴上去,然后把这个包发送出去。这个
过程是不需要建立连接的。UDP 的应用例子有: tftp, bootp 等。
那么,数据包既然会丢失,怎样能保证程序能够正常工作呢?事实上,每个使用UDP
的程序都要有自己的对数据进行确认的协议。比如, TFTP 协议定义了对于每一个发送出
去的数据包,远程在接受到之后都要回送一个数据包告诉本地程序:“我已经拿到了!”(一
个“ACK” 包)。如果数据包发的送者在5 秒内没有的得到回应,它就会重新发送这个
数据包直到数据包接受者回送了“ACK” 信号。这些知识对编写一个使用UDP 协议的
程序员来说是非常必要的。
无连接服务器一般都是面向事务处理的,一个请求一个应答就完成了客户程序与服务
程序之间的相互作用。若使用无连接的套接字编程,程序的流程可以用图6-3 表示。
图6-3 无连接的socket 工作流程
面向连接服务器处理的请求往往比较复杂,不是一来一去的请求应答所能解决的,而
且往往是并发服务器。使用面向连接的套接字编程,可以通过图6-2 来表示。
套接字工作过程如下:服务器首先启动,通过调用socket()建立一个套接字,然后调用
bind()将该套接字和本地网络地址联系在一起,再调用listen()使套接字做好侦听的准备,
并规定它的请求队列的长度,之后就调用accept()来接收连接。客户在建立套接字后就可调
用connect()和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用read()
和write()来发送和接收数据。最后,待数据传送结束后,双方调用close()关闭套接字。
3.原始套接字
原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是
没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。
第6 章berkeley 套接字- 141 -
6.3 Linux 支配的网络协议
网络协议是系统进行系统与系统间通讯的的接口。在Linux 系统上, TCP/IP
(Transmission Control / Internet Protocol)是最常见的。TCP/IP 是一个网络协议协议族,
我们将在下面进行详细介绍。
6.3.1 什么是TCP/IP?
用简单的话来讲, TCP/IP 是一个网络协议族的名字,协议是所有软件产品必须遵守
的、能够保证各种软件产品能够正确通讯的规则。协议还定义了每一部分数据块怎样管理
所传输的数据。
精确一点说,一个协议定义了两个应用程序或是计算机之间能够进行互相通讯,对于
其中的每一个(应用程序或计算机)都保证使用同样的标准。TCP/IP 代表传输控制协议/
网络协议(注意:它们是两个不同的协议!),它是做为软件的网络组成部件而设计的。每
个TCP/IP 的协议都有他专门的工作,比如万维网(WWW),发送电子邮件(E-mail),传
输文件(Ftp),提供远程登陆服务等。
TCP/IP 协议可以根据提供的不同的服务分为几组:
1.控制数据的协议
TCP(传输控制协议Transmission Control Protocol)以连接为基础,也就是说两台电脑
必须先建立一个连接,然后才能传输数据。事实上,发送和接受的电脑必须一直互相通讯
和联系。
UDP(使用者数据报协议User Datagram Protocol)它是一个无连接服务,数据可以直
接发送而不必在两台电脑之间建立一个网络连接。它和有连接的TCP 相比,占用带宽少,
但是你不知道你的数据是否真正到达了你的客户端,而客户端收到的数据也不知道是否还
是原来的发送顺序。
2.数据路由协议
路由协议分析数据包的地址并且决定传输数据到目的电脑最佳路线。他们也可以把大
的数据分成几部分,并且在目的地再把他们组合起来。
IP(因特网协议Internet Protocol)处理实际上传输数据。
ICMP(因特网控制信息协议Internet Control Message Protocol)处理IP 的状态信息,
比如能影响路由决策的数据错误或改变。
RIP(路由信息协议Routing Information Protocol)它是几个决定信息传输的最佳路由
路线协议中的一个。
OSPF(Open Shortest Path First)一个用来决定路由的协议。网络地址协议决定了命名
电脑地址的方法:使用一个唯一的数字和一个字母名字。
ARP(地址决定协议Address Resolution Protocol)确定网络上一台电脑的数字地址。
DNS(域名系统Domain Name System)从机器的名字确定一个机器的数字地址。
RARP(反向地址决定协议Reverse Address Resolution Protocol)确定网络上一台计算
机的地址,和ARP(地址决定协议Address Resolution Protocol)正好相反。
3.用户服务
BOOTP(启动协议Boot Protocol) 由网络服务器上取得启动信息,然后将本地的网
- 142 - Linux网络编程
络计算机启动。
FTP(文件传输协议File Transfer Protocol)通过国际互连网从一台计算机上传输一个
或多个文件到另外一台计算机。
TELNET(远程登陆)允许一个远程登陆,使用者可以从网络上的一台机器通过TELNET
连线到另一台机器,就像使用者直接在本地操作一样。
EGP(外部网关协议Exterior Gateway Protocol)为外部网络传输路由信息。
GGP(网关到网关协议Gateway-to-Gateway Protocol)在网关和网关之间传输路由协
议。
IGP(内部网关协议Interior Gateway Protocol)在内部网络传输路由信息。
3.其他协议(也为网络提供了重要的服务)
NFS(网络文件系统Network File System)允许将一台机器的目录被另一台机器上的
用户安装(Mount)到自己的机器上,就像是对本地文件系统进行操作一样进行各式各样
的操作。
NIS(网络信息服务Network Information Service)对整个网络用户的用户名、密码进
行统一管理,简化在NIS 服务下整个网络登陆的用户名/密码检查。
RPC(远程过程调用Remote Procedure Call)通过它可以允许远程的应用程序通过简
单的、有效的手段联系本地的应用程序,反之也是。
SMTP(简单邮件传输协议Simple Mail Transfer Protocol)一个专门为电子邮件在多台
机器中传输的协议,平时发邮件的SMTP 服务器提供的必然服务。
SNMP(简单网络管理协议Simple Network Management Protocol)这是一项为超级用
户准备的服务,超级用户可以通过它来进行简单的网络管理。
6.4 套接字地址
好了,关于socket 的背景知识我们已经讲得够多了,下面,就让我们正式开始揭开socket
的神秘面纱吧!
6.4.1 什么是Socket?
大家经常谈论“Socket”(套接字),那么一个套接字究竟是什么呢?
一个套接字可以这样来解释:它是通过标准的UNIX 文件描述符和其他的程序通讯的
一个方法。
6.4.2 Socket 描述符
使用UNIX 的黑客高手有这么一句话:“恩,在UNIX 系统中,任何东西都是一个文
件。”这句话描述了这样一个事实:在UNIX 系统中,任何对I/O 的操作,都是通过读或写
一个文件描述符来实现的。
一个文件描述符只是一个简单的整形数值,代表一个被打开的文件(这里的文件是广
义的文件,并不只代表不同的磁盘文件,它可以代表一个网络上的连接,一个先进先出队
列,一个终端显示屏幕,以及其他的一切)。在UNIX 系统中任何东西都是一个文件!!所
以如果你想通过Internet 和另外一个程序通讯的话,你将会是通过一个文件来描述符实现
的。你最好相信这一点。
第6 章berkeley 套接字- 143 -
好的,你已经相信Socket 是一个文件描述符了,那么我们应该怎样才能得到这个代表
网络连接的文件描述符呢?你现在一定非常在意这个问题。是这样的:你首先调用系统函
数socket(),它返回一个套接字(Socket)描述符,然后你就可以通过对这个套接字描述符
进行一些操作:系统函数send() 和recv()(你可以使用“man”命令来查找系统帮助:man
send, man recv)。
你会想:“套接字描述符是一个文件描述符,为什么不能用对文件操作的write() 和
read() 来进行套接字通讯呢?”事实上, write() 和read() 是可以对套接字描述符进行操
作的,但是,通过使用send() 和recv() 函数,你可以对网络数据的传输进行更好的控制!
6.4.3 一个套接字是怎样在网络上传输数据的?
我们已经谈过了网络协议层,那么我们还应该继续多了解一些东西:物理网络上的数
据是怎样传送的。
我们可以认为是这样的:
数据被分成一个一个的包(Packet),包的数据头(或数据尾)被第一层协议(比如TFTP
协议) 加上第一层协议数据;然后整个包(包括内部加入的TFTP 信息头)被下层协议再
次包装(比如UDP),再这之后数据包会再次被下层协议包装(比如IP 协议),最后是被
最底层的硬件层(物理层)包装上最后一层信息(Ethernet 信息头)。
当接受端的计算机接收到这个包后,硬件首先剥去数据包中的Ethernet 信息头,然后
内核在剥去IP 和UDP 信息头,最后把数据包提交给TFTP 应用程序,由TFTP 剥去TFTP
信息头,最后得到了原始数据。
下面我们再大致回顾一下著名的网络层次模型。
通过这个网络模型,你可以写套接字的应用程序而不必在乎事实上数据在物理层中的
传输方法(无论是以太网,还是并口、AUI 或是其他的什么方法)。
因为已经有程序在底层为你处理了这些问题了。下面是OSI 模型,你可以记住它来应
付一些测验。
l 应用层
l 表示层
l 会话层
l 传输层
l 网络层
l 数据链路层
l 物理层
物理层就是硬件层(比如并口,以太网)。应用程序层离物理层很远很远,以至于它
可以不受物理层的影响。
上面这个模型是最一般的模型,但是在Linux 中,真正用到的模型是下面这样子的:
l 应用层(Telnet,Ftp,等等)
l 主机间对话层(TCP 和UDP)
l 网络层(IP 和路由)
l 网络底层(相当于OSI 模型中网络、数据链路和物理层)
现在,你大概已经明白各个协议层是怎样对原始数据进行包装和解包的了吧。看见对
- 144 - Linux网络编程
于每一个数据包有多少项工作需要做了吗?对!你对每一个数据包都需要自己用“cat”命
令来查看协议信息头!
开个玩笑。对流式套接字你所需要做的只是调用send() 函数来发送数据。而对于数据
报套接字,你需要自己加个信息头,然后调用sendto() 函数把数据发送出去。Linux 系统
内核中已经建立了Transport Layer 和Internet Layer。硬件负责NetworkAccess Layer。简单
而有效,不是吗?
6.5 套接字的一些基本知识
好的,从现在开始,我们应该谈些和程序有关的事情了。
6.5.1 基本结构
首先,我想介绍一些使用套接字编程中常见的网络数据结构对大家会很有帮助。
1.struct sockaddr
这个结构用来存储套接字地址。
数据定义:
struct sockaddr {
unsigned short sa_family; /* address族, AF_xxx */
char sa_data[14]; /* 14 bytes的协议地址*/
};
sa_family 一般来说,都是“AFINET”。
sa_data 包含了一些远程电脑的地址、端口和套接字的数目,它里面的数据是杂溶在一
切的。
为了处理struct sockaddr, 程序员建立了另外一个相似的结构struct sockaddr_in:
struct sockaddr_in (“in” 代表“Internet”)
struct sockaddr_in {
short int sin_family; /* Internet地址族*/
unsigned short int sin_port; /* 端口号*/
struct in_addr sin_addr; /* Internet地址*/
unsigned char sin_zero[8]; /* 添0(和struct sockaddr一样大小)*/
};
这个结构提供了方便的手段来访问socket address(struct sockaddr)结构中的每一个元
素。注意sin_zero[8] 是为了是两个结构在内存中具有相同的尺寸,使用sockaddr_in 的时
候要把sin_zero 全部设成零值(使用bzero()或memset()函数)。而且,有一点很重要,就
是一个指向struct sockaddr_in 的指针可以声明指向一个sturct sockaddr 的结构。所以虽然
socket() 函数需要一个structaddr * ,你也可以给他一个sockaddr_in * 。注意在struct
sockaddr_in 中,sin_family 相当于在struct sockaddr 中的sa_family,需要设成“AF_INET”。
最后一定要保证sin_port 和sin_addr 必须是网络字节顺序(见下节)!
2.struct in_addr
其定义如下:
/* 因特网地址(a structure for historical reasons) */
第6 章berkeley 套接字- 145 -
struct in_addr {
unsigned long s_addr;
};
如果你声明了一个“ ina ” 作为一个struct sockaddr_in 的结构, 那么
“ina.sin_addr.s_addr”就是4 个字节的IP 地址(按网络字节顺序排放)。需要注意的是,
即使你的系统仍然使用联合而不是结构来表示struct in_addr,你仍然可以用上面的方法得
到4 个字节的IP 地址(一些#defines 帮了你的忙)。
6.5.2 基本转换函数
在前面提到了网络字节顺序。那么什么是网络字节顺序,它有什么特殊性,又如何将
我们通常使用的数据转换成这种格式呢?
1.网络字节顺序
因为每一个机器内部对变量的字节存储顺序不同(有的系统是高位在前,底位在后,
而有的系统是底位在前,高位在后),而网络传输的数据大家是一定要统一顺序的。所以
对与内部字节表示顺序和网络字节顺序不同的机器,就一定要对数据进行转换(比如IP 地
址的表示,端口号的表示)。但是内部字节顺序和网络字节顺序相同的机器该怎么办呢?
是这样的:它们也要调用转换函数,但是真正转换还是不转换是由系统函数自己来决定的。
2.有关的转化函数
我们通常使用的有两种数据类型:短型(两个字节)和长型(四个字节)。下面介绍
的这些转换函数对于这两类的无符号整型变量都可以进行正确的转换。
如果你想将一个短型数据从主机字节顺序转换到网络字节顺序的话,有这样一个函
数:它是以“h”开头的(代表“主机”);紧跟着它的是“to”,代表“转换到”;然后是“n”
代表“网络”;最后是“s”,代表“短型数据”。H-to-n-s,就是htons() 函数(可以使用Host
to Network Short 来助记)
很简单吧??我没有理解的时候觉得这个函数不好记呢??
你可以使用“n”,“h”,“to”,“s”,“l”的任意组合??当然,你要在可能的情况下
进行组合。比如,系统是没有stolh() 函数的(Short to Long Host?)。
下面给出套接字字节转换程序的列表:
l htons()——“Host to Network Short” 主机字节顺序转换为网络字节顺序(对无符号
短型进行操作4 bytes)
l htonl()——“Host to Network Long” 主机字节顺序转换为网络字节顺序(对无符
号长型进行操作8 bytes)
l ntohs()——“Network to Host Short “ 网络字节顺序转换为主机字节顺序(对无符
号短型进行操作4 bytes)
l ntohl()——“Network to Host Long “ 网络字节顺序转换为主机字节顺序(对无符
号长型进行操作8 bytes)
注意:现在你可能认为自己已经精通于这几个函数的用处了??你可能会想:“恩??在我的68000
机器内部,字节的表示顺序已经是网络字节顺序了,那么我的程序里就不必调用htonl() 来转换我的IP 地
址了”。是的,你可能是对的。但是假如你把你的程序移植到一个内部字节顺序和网络字节顺序相反的机
器上,你的程序就会运行不正常!所以,一定要记住:在你把数据发送到Internet 之前,一定要把它的字
- 146 - Linux网络编程
节顺序从主机字节顺序转换到网络字节顺序!
在struct sockaddr_in 中的sin_addr 和sin_port 他们的字节顺序都是网络字节顺序,而
sin_family 却不是网络字节顺序的。为什么呢?
这个是因为sin_addr 和sin_port 是从IP 和UDP 协议层取出来的数据,而在IP 和UDP
协议层,是直接和网络相关的,所以,它们必须使用网络字节顺序。然而, sin_family 域
只是内核用来判断struct sockaddr_in 是存储的什么类型的数据,并且, sin_family 永远也
不会被发送到网络上,所以可以使用主机字节顺序来存储。
3.IP 地址转换
很幸运, Linux 系统提供和很多用于转换IP 地址的函数,使你不必自己再写出一段
费力不讨好的子程序来吃力的变换IP。
首先,让我假设你有一个struct sockaddr_in ina,并且你的IP 是166.111.69.52 ,你想
把你的IP 存储到ina 中。你可以使用的函数: inet_addr() ,它能够把一个用数字和点表
示IP 地址的字符串转换成一个无符号长整型。你可以像下面这样使用它:
ina.sin_addr.s_addr = inet_addr(“166.111.69.52”);
注意:
l inet_addr() 返回的地址已经是网络字节顺序了,你没有必要再去调用htonl() 函数,是不是很
方便呢?
l 上面的用法并不是一个很好的习惯,因为上面的代码没有进行错误检查。如果inet_addr() 函数
执行错误,它将会返回–1??等等!二进制的无符号整数值–1 相当于什么?相当于255.255.255.255 !! 一
个广播用的IP 地址!没有办法,你只能在你自己的程序里进行对症下药的错误检查了。
好,现在我们已经可以把字符串的IP 地址转换成长整型了。那么还有没有其他的方法
呢?如果你有一个struct in_addr 并且你想把它代表的IP 地址打印出来(按照数字.数字.数
字.数字的格式)??
这里,你可以使用函数inet_ntoa()(“ntoa”代表“Network to ASCII”):
printf(“%s”, inet_ntoa(ina.sin_addr));
这段代码将会把struct in_addr 里面存储的网络地址以数字.数字.数字.数字的格式显
示出来。
注意:
l inet_ntoa() 使用struct in_addr 作为一个参数,不是一个长整型值。
l inet_ntoa() 返回一个字符指针,它指向一个定义在函数inet_ntoa() 中的static 类型字符串。所
以每次你调用inet_ntoa(),都会改变最后一次调用inet_ntoa() 函数时得到的结果。
比如:
char *a1, a2;
a1 = inet_ntoa(ina1.sin_addr); /* this is 166.111.69.52 */
a2 = inet_ntoa(ina2.sin_addr); /* this is 166.111.69.53 */
printf(“address 1: %s/n”,a1);
printf(“address 2: %s/n”,a2);
将会显示出:
address 1: 166.111.69.53
address 2: 166.111.69.53
第6 章berkeley 套接字- 147 -
如果你想把结果保存下来,那么你可以在每次调用inet_ntoa() 后调用strcpy() 将结果存到另外
一个你自己的字符串中。
在后面,将会介绍怎样把域名转换为IP。
6.6 基本套接字调用
Linux 支持伯克利(BSD)风格的套接字编程.它同时支持面向连接和不连接类型的
套接字。
在面向连接的通讯中服务器和客户机在交换数据之前先要建立一个连接.再不连接通
讯中数据被作为信息的一部分被交换.无论那一种方式,服务器总是最先启动,把自己绑
定(Banding)在一个套接字上,然后侦听信息.服务器究竟怎样试图去侦听就得依靠你编
程所设定的连接的类型了。
你需要了解的一些系统调用:
l socket()
l bind()
l connect()
l listen()
l accept()
l send()
l recv()
l sendto()
l recvfrom()
l close()
l shutdown()
l setsockopt()
l getsockopt()
l getpeername()
l getsockname()
l gethostbyname()
l gethostbyaddr()
l getprotobyname()
l fcntl()
我们将在以下详细介绍这些系统调用。
6.6.1 socket() 函数
取得套接字描述符!(记得我们以前说过的吗?它其实就是一个文件描述符)
socket 函数的定义是下面这样子的:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain , int type , int protocol);
- 148 - Linux网络编程
你是否对int domain 和int type、int protocol 有些疑惑呢?调用socket()的参数是什么
呢?
首先,domain 需要被设置为“AF_INET”,就像上面的struct sockaddr_in。然后,type
参数告诉内核这个socket 是什么类型,“SOCK_STREAM”或是“SOCK_DGRAM”。最后,
只需要把protocol 设置为0 。
注意:事实上, domain 参数可以取除了“ AF_INET ”外的很多值,types 参数也可以取除了
“SOCK_STREAM”或“SOCK_DGRAM”的另外类型。具体可以参考socket 的man pages(帮助页)。
套接字创建时没有指定名字.客户机用套接字的名字读写它。这就是下面的绑定函数
所要做之事.
socket()函数只是简单的返回一个你以后可以使用的套接字描述符。如果发生错误,
socket()函数返回–1 。全局变量errno 将被设置为错误代码。(可以参考perror() 的man
pages)
6.6.2 bind() 函数
bind()函数可以帮助你指定一个套接字使用的端口。
当你使用socket() 函数得到一个套接字描述符,你也许需要将socket 绑定上一个你的
机器上的端口。
l 当你需要进行端口监听listen()操作,等待接受一个连入请求的时候,一般都需要
经过这一步。比如网络泥巴(MUD),Telnet a.b.c.d 4000。
l 如果你只是想进行连接一台服务器,也就是进行connect() 操作的时候,这一步
并不是必须的。
bind()的系统调用声明如下:
#include <sys/types.h>
#include <sys/socket.h>
int bind (int sockfd , struct sockaddr *my_addr , int addrlen) ;
参数说明:
l sockfd 是由socket()函数返回的套接字描述符。
l my_addr 是一个指向struct sockaddr 的指针,包含有关你的地址的信息:名称、
端口和IP 地址。
l addrlen 可以设置为sizeof(struct sockaddr)。
好,下面我们看一段程序:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define MYPORT 4000
main()
{ int sockfd ;
第6 章berkeley 套接字- 149 -
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(“166.111.69.52”) ;
bzero(&(my_addr.sin_zero), 8); /* 将整个结构剩余*/
/* 部分数据设为0 */
/* 不要忘记在你自己的程序中加入判断bind 错误的代码!! */
bind (sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
??
??
这里有一些值得注意的代码段:
l my_addr.sin_port 是网络字节顺序。
l my_addr.sin_addr.s_addr 也是网络字节顺序。
l 代码段包含的头文件,在不同的系统中可能有一点小小的区别。(不过在Linux
中是如此)如果并非如此,你可以查一查man pages 来获取帮助。
最后,bind()可以在程序中自动获取你自己的IP 地址和端口。
代码如下:
my_addr.sin_port = 0 ; /* 随机选择一个端口*/
my_addr.sin_addr.s_addr = INADDR_ANY ; /* 使用自己的地址*/
如上,通过设置my_addr.sin_port 为0,bind()可以知道你要它帮你选择合适的端口;
通过设置my_addr.sin_addr.s_addr 为INADDR_ANY,bind()知道你要它将s_addr 填充为运
行这个进程的机器的IP。这一切都可以要求bind()来自动的帮助你完成。
如果你注意到了一些细节的话,你可能会发现我并没有将INADDR_ANY 转换为网络
字节顺序!是这样的,INADDR_ANY的值为0,0 就是0,无论用什么顺序排列位的顺序,
它都是不变的。
有读者会想了,因为我用的INADDR_ANY 是一个#define,那么如果将我的程序移植
到另外一个系统,假如那里的INADDR_ANY是这样定义的:#define INADDR_ANY 100,
那么我的程序不是就会不运行了吗?那么下面这段代码就OK 了
my_addr.sin_port = htons(0); /* 随机选择一个未用的端口*/
my_addr.sin_addr.s_addr = htonl(INADDR_ANY) ; /* 使用自己的IP地址*/
现在我们已经是这么的严谨,对于任何数值的INADDR_ANY调用bind 的时候就都不
会有麻烦了。
当bind()函数调用错误的时候,它也是返回–1 作为错误发生的标志。errn 的值为错误
代码。
另外一件必须指出的事情是:当你调用bind()的时候,不要把端口数设置的过小!小
于1024 的所有端口都是保留下来作为系统使用端口的,没有root 权利无法使用。你可以
使用1024 以上的任何端口,一直到65535 :你所可能使用的最大的端口号(当然,你还
- 150 - Linux网络编程
要保证你所希望使用的端口没有被其他程序所使用)。
最后注意有关bind()的是:有时候你并不一定要调用bind()来建立网络连接。比如你只
是想连接到一个远程主机上面进行通讯,你并不在乎你究竟是用的自己机器上的哪个端口
进行通讯(比如Telnet),那么你可以简单的直接调用connect()函数,connect()将自动寻找
出本地机器上的一个未使用的端口,然后调用bind()来将其socket 绑定到那个端口上。
6.6.3 connect()函数
让我们花一点时间来假设你是一个Telnet 应用程序。你的使用者命令你建立一个套接
字描述符。你遵从命令,调用了socket()。然后,使用者告诉你连接到“166.111.69.52”
的23 端口(标准的Telnet 端口)??你应该怎么做呢?
你很幸运:Telnet 应用程序,你现在正在阅读的就是套接字的进行网络连接部分:
connect()。
connect() 函数的定义是这样的:
#include <sys/types.h>
#include <sys/socket.h>
int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);
connect()的三个参数意义如下:
l sockfd :套接字文件描述符,由socket()函数返回的。
l serv_addr 是一个存储远程计算机的IP 地址和端口信息的结构。
l addrlen 应该是sizeof(struct sockaddr)。
下面让我们来看看下面的程序片段:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#define DEST_IP “166.111.69.52”
#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);
第6 章berkeley 套接字- 151 -
dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
/* 将剩下的结构中的空间置0 */
bzero(&(dest_addr.sin_zero), 8);
/* 不要忘记在你的代码中对connect()进行错误检查!! */
connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
??
??
再次强调,一定要检测connect()的返回值:如果发生了错误(比如无法连接到远程主
机,或是远程主机的指定端口无法进行连接等)它将会返回错误值–1 。全局变量errno
将会存储错误代码。
另外,注意我们没有调用bind()函数。基本上,我们并不在乎我们本地用什么端口来
通讯,是不是?我们在乎的是我们连到哪台主机上的哪个端口上。Linux 内核自动为我们
选择了一个没有被使用的本地端口。
在面向连接的协议的程序中,服务器执行以下函数:
l 调用socket()函数创建一个套接字。
l 调用bind()函数把自己绑定在一个地址上。
l 调用listen()函数侦听连接。
l 调用accept()函数接受所有引入的请求。
l 调用recv()函数获取引入的信息然后调用send()回答。
6.6.4 listen() 函数
listen()函数是等待别人连接,进行系统侦听请求的函数。当有人连接你的时候,你有
两步需要做:通过listen()函数等待连接请求,然后使用accept()函数来处理。(accept()函数
在下面介绍)。
listen()函数调用是非常简单的。函数声明如下:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen()函数的参数意义如下:
l sockfd 是一个套接字描述符,由socket()系统调用获得。
l backlog 是未经过处理的连接请求队列可以容纳的最大数目。
backlog 具体一些是什么意思呢?每一个连入请求都要进入一个连入请求队列,等待
listen 的程序调用accept()(accept()函数下面有介绍)函数来接受这个连接。当系统还没有
调用accept()函数的时候,如果有很多连接,那么本地能够等待的最大数目就是backlog 的
数值。你可以将其设成5 到10 之间的数值(推荐)。
像上面的所有函数一样, listen()如果返回–1 ,那么说明在listen()的执行过程中发生
了错误。全局变量errno 中存储了错误代码。
那么我们需要指定本地端口了,因为我们是等待别人的连接。所以,在listen()函数调
用之前,我们需要使用bind() 函数来指定使用本地的哪一个端口数值。
- 152 - Linux网络编程
如果你想在一个端口上接受外来的连接请求的话,那么函数的调用顺序为:
socket() ;
bind() ;
listen() ;
/* 在这里调用accept()函数*/
??
下面将不给出例程,因为listen()是非常容易理解的。下面的accept()函数说明中的例
程中,有listen()的使用。
6.6.5 accept()函数
函数accept()有一些难懂。当调用它的时候,大致过程是下面这样的:
l 有人从很远很远的地方尝试调用connect()来连接你的机器上的某个端口(当然是
你已经在listen()的)。
l 他的连接将被listen 加入等待队列等待accept()函数的调用(加入等待队列的最多
数目由调用listen()函数的第二个参数backlog 来决定)。
l 你调用accept()函数,告诉他你准备连接。
l accept()函数将回返回一个新的套接字描述符,这个描述符就代表了这个连接!
好,这时候你有了两个套接字描述符,返回给你的那个就是和远程计算机的连接,而
第一个套接字描述符仍然在你的机器上原来的那个端口上listen()。
这时候你所得到的那个新的套接字描述符就可以进行send()操作和recv()操作了。
下面是accept()函数的声明:
#include <sys/socket.h>
int accept(int sockfd, void *addr, int *addrlen);
accept()函数的参数意义如下:
l sockfd 是正在listen() 的一个套接字描述符。
l addr 一般是一个指向struct sockaddr_in 结构的指针;里面存储着远程连接过来的
计算机的信息(比如远程计算机的IP 地址和端口)。
l addrlen 是一个本地的整型数值,在它的地址传给accept() 前它的值应该是
sizeof(struct sockaddr_in);accept()不会在addr 中存储多余addrlen bytes 大小的数据。如果
accept()函数在addr 中存储的数据量不足addrlen,则accept()函数会改变addrlen 的值来反
应这个情况。
读者现在应该想到:如果调用accept()失败的话,accept()函数会返回–1 来表明调用
失败,同时全局变量errno 将会存储错误代码。
下面我们来看一段程序片段:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
/* 用户连接的端口号*/
第6 章berkeley 套接字- 153 -
#define MYPORT 4000
/* 等待队列中可以存储多少个未经过accept()处理的连接*/
#define BACKLOG 10
main()
{
/* 用来监听网络连接的套接字sock_fd,用户连入的套接字使用new_fd */
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) ;
/* 自动赋值为自己的IP */
my_addr.sin_addr.s_addr = INADDR_ANY ;
/* 将结构中未使用部分全部清零*/
bzero(&(my_addr.sin_zero), 8) ;
/* 不要忘记在你自己的程序中下面的程序调用需要进行错误检测!!*/
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
listen(sockfd, BACKLOG);
sin_size = sizeof(struct sockaddr_in);
new_fd = accept(sockfd, &their_addr, &sin_size);
??
??
注意:我们使用了套接字描述符new_fd 用来进行所有的send() 和recv()调用。如果你
只想获得一个单独的连接,那么你可以将原来的sock_fd 关掉(调用close()),这样的话就
可以阻止以后的连接了。
在面向连接的通信中客户机要做如下一些事:
- 154 - Linux网络编程
l 调用socket()函数创建一个套接字。
l 调用connect()函数试图连接服务。
l 如果连接成功调用write()函数请求数据,调用read()函数接收引入的应答。
6.6.6 send()、recv()函数
这两个函数是最基本的,通过连接的套接字流进行通讯的函数。
如果你想使用无连接的使用者数据报的话,请参考下面的sendto() 和recvfrom() 函数。
send() 函数的声明:
#include <sys/types.h>
#include <sys/socket.h>
int send(int sockfd, const void *msg, int len, int flags);
send 的参数含义如下:
l sockfd 是代表你与远程程序连接的套接字描述符。
l msg 是一个指针,指向你想发送的信息的地址。
l len 是你想发送信息的长度。
l flags 发送标记。一般都设为0(你可以查看send 的man pages 来获得其他的参数
值并且明白各个参数所代表的含义)。
下面看看有关send()函数的代码片段:
char *msg = “Hello! World!”;
int len, bytes_sent;
??
??
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
??
??
??
send()函数在调用后会返回它真正发送数据的长度。
注意:send() 所发送的数据可能少于你给它的参数所指定的长度!
因为如果你给send()的参数中包含的数据的长度远远大于send()所能一次发送的数据,则send()函数
只发送它所能发送的最大数据长度,然后它相信你会把剩下的数据再次调用它来进行第二次发送。
所以,记住如果send()函数的返回值小于len 的话,则你需要再次发送剩下的数据。幸运的是,如果
包足够小(小于1K),那么send()一般都会一次发送光的。
像上面的函数一样,send()函数如果发生错误,则返回–1 ,错误代码存储在全局变
量errno 中。
下面我们来看看recv()函数。
函数recv()调用在许多方面都和send()很相似,下面是recv()函数的声明:
#include <sys/types.h>
#include <sys/socket.h>
第6 章berkeley 套接字- 155 -
int recv(int sockfd, void *buf, int len, unsigned int flags);
recv()的参数含义如下:
l sockfd 是你要读取数据的套接字描述符。
l buf 是一个指针,指向你能存储数据的内存缓存区域。
l len 是缓存区的最大尺寸。
l flags 是recv() 函数的一个标志,一般都为0 (具体的其他数值和含义请参考recv()
的man pages)。
recv() 返回它所真正收到的数据的长度。(也就是存到buf 中数据的长度)。如果返回
–1 则代表发生了错误(比如网络以外中断、对方关闭了套接字连接等),全局变量errno 里
面存储了错误代码。
很简单,不是吗?现在你已经可以使用套接字连接进行网络发送数据和接受数据了!
Ya! 你现在已经成为了一个Linux 下的网络程序员了!
6.6.7 sendto() 和recvfrom() 函数
这两个函数是进行无连接的UDP 通讯时使用的。使用这两个函数,则数据会在没有
建立过任何连接的网络上传输。因为数据报套接字无法对远程主机进行连接,想想我们在
发送数据前需要知道些什么呢?
对了!是远程主机的IP 地址和端口!
下面是sendto()函数和recvfrom()函数的声明:
#include <sys/types.h>
#include <sys/socket.h>
int sendto(int sockfd, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, int tolen);
和你所看到的一样,这个函数和send()函数基本一致。
l sockfd 是代表你与远程程序连接的套接字描述符。
l msg 是一个指针,指向你想发送的信息的地址。
l len 是你想发送信息的长度。
l flags 发送标记。一般都设为0。(你可以查看send 的man pages 来获得其他的参
数值并且明白各个参数所代表的含义)
l to 是一个指向struct sockaddr 结构的指针,里面包含了远程主机的IP 地址和端口
数据。
l tolen 只是指出了struct sockaddr 在内存中的大小sizeof(struct sockaddr)。
和send()一样,sendto()返回它所真正发送的字节数(当然也和send()一样,它所真正
发送的字节数可能小于你所给它的数据的字节数)。当它发生错误的时候,也是返回–1 ,
同时全局变量errno 存储了错误代码。
同样的,recv()函数和recvfrom()函数也基本一致。
recvfrom()的声明为:
#include <sys/types.h>
- 156 - Linux网络编程
#include <sys/socket.h>
int recvfrom(int sockfd, void *buf, int len, unsigned int flags
struct sockaddr *from, int *fromlen);
其参数含义如下:
l sockfd 是你要读取数据的套接字描述符。
l buf 是一个指针,指向你能存储数据的内存缓存区域。
l len 是缓存区的最大尺寸。
l flags 是recv() 函数的一个标志,一般都为0 (具体的其他数值和含义请参考recv()
的man pages)。
l from 是一个本地指针,指向一个struct sockaddr 的结构(里面存有源IP 地址和端
口数).
l fromlen 是一个指向一个int 型数据的指针,它的大小应该是sizeof ( struct
sockaddr).当函数返回的时候,formlen 指向的数据是form 指向的struct sockaddr 的实际
大小.
recvfrom() 返回它接收到的字节数,如果发生了错误,它就返回–1 ,全局变量errno
存储了错误代码.
如果一个信息大得缓冲区都放不下,那么附加信息将被砍掉。该调用可以立即返回,也
可以永久的等待。这取决于你把flags 设置成什么类型。你甚至可以设置超时(timeout)值。
在说明书(man pages)中可以找到recvfrom 的更多信息。
注意:如果你使用cnnect()连接到了一个数据报套接字的服务器程序上,那么你就可
以使用send() 和recv() 函数来传输你的数据.不要以为你在使用一个流式的套接字,你所
使用的仍然是一个使用者数据报的套接字,只不过套接字界面在send() 和recv()的时候自
动帮助你加上了目标地址,目标端口的信息.
6.6.8 close()和shutdown()函数
程序进行网络传输完毕后,你需要关闭这个套接字描述符所表示的连接。实现这个非
常简单,只需要使用标准的关闭文件的函数:close()。
使用方法:
close(sockfd);
执行close()之后,套接字将不会在允许进行读操作和写操作。任何有关对套接字描述
符进行读和写的操作都会接收到一个错误。
如果你想对网络套接字的关闭进行进一步的操作的话,你可以使用函数shutdown()。
它允许你进行单向的关闭操作,或是全部禁止掉。
shutdown()的声明为:
#include <sys/socket.h>
int shutdown(int sockfd, int how);
它的参数含义如下:
l sockfd 是一个你所想关闭的套接字描述符.
l how 可以取下面的值。0 表示不允许以后数据的接收操;1 表示不允许以后数据
第6 章berkeley 套接字- 157 -
的发送操作;2 表示和close()一样,不允许以后的任何操作(包括接收,发送数据)
shutdown() 如果执行成功将返回0,如果在调用过程中发生了错误,它将返回–1,全
局变量errno 中存储了错误代码.
如果你在一个未连接的数据报套接字上使用shutdown() 函数(还记得可以对数据报套
接字UDP 进行connect()操作吗?),它将什么也不做.
6.6.9 setsockopt() 和getsockopt() 函数
Linux 所提供的socket 库含有一个错误(bug)。此错误表现为你不能为一个套接字重
新启用同一个端口号,即使在你正常关闭该套接字以后。例如,比方说,你编写一个服务
器在一个套接字上等待的程序.服务器打开套接字并在其上侦听是没有问题的。无论如何,
总有一些原因(不管是正常还是非正常的结束程序)使你的程序需要重新启动。然而重启
动后你就不能把它绑定在原来那个端口上了。从bind()系统调用返回的错误代码总是报告
说你试图连接的端口已经被别的进程所绑定。
问题就是Linux 内核在一个绑定套接字的进程结束后从不把端口标记为未用。在大多
数Linux/UNIX 系统中,端口可以被一个进程重复使用,甚至可以被其它进程使用。
在Linux 中绕开这个问题的办法是,当套接字已经打开但尚未有连接的时候用
setsockopt()系统调用在其上设定选项(options)。setsockopt() 调用设置选项而getsockopt()
从给定的套接字取得选项。
这里是这些调用的语法:
#include<sys/types.h>
#include<sys/socket.h>
int getsockopt(int sockfd, int level, int name, char *value, int *optlen);
int setsockopt(int sockfd, int level, int name, char *value, int *optlen);
下面是两个调用的参数说明:
l sockfd 必须是一个已打开的套接字。
l level 是函数所使用的协议标准(protocol level)(TCP/IP 协议使用IPPROTO_TCP,
套接字标准的选项实用SOL_SOCKET)。
l name 选项在套接字说明书中(man page)有详细说明。
l value 指向为getsockopt()函数所获取的值,setsockopt()函数所设置的值的地址。
l optlen 指针指向一个整数,该整数包含参数以字节计算的长度。
现在我们再回到Linux 的错误上来.当你打开一个套接字时必须同时用下面的代码段
来调用setsockopt()函数:
/* 设定参数数值*/
opt = 1; len = sizeof(opt);
/* 设置套接字属性*/
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,&len);
setsockopt()函数还有很多其他用法,请参考帮助页(man pages).
6.6.10 getpeername()函数
这个函数可以取得一个已经连接上的套接字的远程信息(比如IP 地址和端口),告诉
- 158 - Linux网络编程
你在远程和你连接的究竟是谁.
它的声明为:
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
下面是参数说明:
l sockfd 是你想取得远程信息的那个套接字描述符。
l addr 是一个指向struct sockaddr (或是struct sockaddr_in)的指针。
l addrlen 是一个指向int 的指针,应该赋于sizeof(struct sockaddr)的大小。
如果在函数执行过程中出现了错误,函数将返回–1 ,并且错误代码储存在全局变量
errno 中。
当你拥有了远程连接用户的IP 地址,你就可以使用inet_ntoa() 或gethostbyaddr()来输
出信息或是做进一步的处理。
6.6.11 gethostname()函数
gethostname()函数可以取得本地主机的信息.它比getpeername()要容易使用一些。
它返回正在执行它的计算机的名字。返回的这个名字可以被gethostbyname()函数使用,
由此可以得到本地主机的IP 地址。
下面是它的声明:
#include <unistd.h>
int gethostname(char *hostname, size_t size);
参数说明如下:
l hostname 是一个指向字符数组的指针,当函数返回的时候,它里面的数据就是本
地的主机的名字.
l size 是hostname 指向的数组的长度.
函数如果成功执行,它返回0,如果出现错误,则返回–1,全局变量errno 中存储着错
误代码。
6.7 DNS 的操作
6.7.1 理解DNS
你应该知道DNS 吧?DNS 是“Domain Name Service”(域名服务)的缩写。有了它,
你就可以通过一个可读性非常强的因特网名字得到这个名字所代表的IP 地址。转换为IP
地址后,你就可以使用标准的套接字函数(bind(),connect() ,sendto() ,或是其他任何需
要使用的函数)。
在这里,如果你输入命令:
$ telnet bbs.tsinghua.edu.cn
Telnet 可以知道它需要连往202.112.58.200。这就是通过DNS 来实现的。
6.7.2 和DNS 有关的函数和结构
DNS 是怎样工作的呢?你可以使用gethostbyname()函数。
第6 章berkeley 套接字- 159 -
它的声明如下:
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
正如你所看见的,它返回了一个指向struct hostent 的指针.Struct hostent 是这样定义
的:
struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
};
#define h_addr h_addr_list[0]
下面是上面各个域代表含义的解释:
l h_name 是这个主机的正式名称。
l h_aliases 是一个以NULL(空字符)结尾的数组,里面存储了主机的备用名称。
l h_addrtype 是返回地址的类型,一般来说是“AF_INET”。
l h_length 是地址的字节长度。
l h_addr_list 是一个以0 结尾的数组,存储了主机的网络地址。
注意:网络地址是以网络字节顺序存储的。
l h_addr - h_addr_list 数组的第一个成员.
gethostbyname() 返回的指针指向结构struct hostent ,如果发生错误,它将会返回NULL
(但是errno 并不代表错误代码,h_errno 中存储的才识错误代码。参考下面的herror()函数)。
应该如何使用这个函数呢?它看起来有一点点吓人。相信我,它使用起来远远要比它
看起来容易。
6.7.3 DNS 例程
下面我们来看一段例程:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
int
main (int argc, char *argv[])
{
struct hostent *h;
/* 检测命令行中的参数是否存在*/
- 160 - Linux网络编程
if (argc != 2)
/* 如果没有参数,给出使用方法*/
fprintf (stderr “usage: getip address/n”);
/* 然后退出*/
exit(1);
}
/* 取得主机信息*/
if((h=gethostbyname(argv[1])) == NULL)
{
/* 如果gethostbyname 失败,则给出错误信息*/
herror(“gethostbyname”);
/* 然后退出*/
exit(1);
}
/* 列印程序取得的信息*/
printf(“Host name : %s/n”, h->h_name);
printf(“IP Address : %s/n”, inet_ntoa (*((struct in_addr *)h->h_addr)));
/* 返回*/
return 0;
}
使用gethostbyname()函数,你不能使用perror()来输出错误信息(因为错误代码存储在
h_errno 中而不是errno 中。所以,你需要调用herror()函数。
上面的程序是不是很神奇呢? 你简单的传给gethostbyname() 一个机器名
(“bbs.tsinghua.edu.cn”),然后就从返回的结构struct hostent 中得到了IP 等其他信息.
程序中输出IP 地址的程序需要解释一下:
h->h_addr 是一个char*,但是inet_ntoa()函数需要传递的是一个struct in_addr 结构。
所以上面将h->h_addr 强制转换为struct in_addr*,然后通过它得到了所有数据。
6.8 套接字的Client/Server 结构实现的例子
现在是一个服务器/客户端的世界.几乎网络上的所有工作都是由客户端向服务器端
发送请求来实现的.比如Telnet ,当你向一个远程主机的23 端口发出连接请求的时候,
远程主机上的服务程序(Telnetd)就会接受这个远程连接请求。允许你进行login 操作。
等等。
服务器和客户机之间可以使用任何方式通讯,包括SOCK_STREAM, SOCK_DGRAM,
或是其他任何方式(只要他们使用相同的方法).
第6 章berkeley 套接字- 161 -
一些服务器/客户机的例子是telnet/telnetd,ftp/ftpd,bootp/bootpd。每次你使用ftp,
你同时都使用了远程主机上的ftpd 服务。一般来说,服务器上有一个程序等待连接。当接
收到一个连接的时候,服务器程序调用系统函数fork()来得到一个子进程,专门处理这个
连接的操作。
下面我们来看一个简单的流服务器:
6.8.1 简单的流服务器
这个服务器所有的工作就是给远程的终端发送一个字符串:“Hello,World!”你所需要
做的就是在命令行上启动这个服务器,然后在另外一台机器上使用telnet 连接到这台我们
自己写的服务器上:
$ telnet remotehostname 4000
remotehostname 就是你运行我们自己写的服务器的那台机器名。
服务器代码:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
/* 服务器要监听的本地端口*/
#define MYPORT 4000
/* 能够同时接受多少没有accept 的连接*/
#define BACKLOG 10
main()
{
/* 在sock_fd 上进行监听,new_fd 接受新的连接*/
int sock_fd, new_fd ;
/* 自己的地址信息*/
struct sockaddr_in my_addr;
/* 连接者的地址信息*/
struct sockaddr_in their_addr;
int sin_size;
/* 这里就是我们一直强调的错误检查.如果调用socket() 出错,则返回*/ if ((sockfd =
- 162 - Linux网络编程
socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
/* 输出错误提示并退出*/
perror(“socket”);
exit(1);
}
/* 主机字节顺序*/
my_addr.sin_family = AF_INET;
/* 网络字节顺序,短整型*/
my_addr.sin_port = htons(MYPORT);
/* 将运行程序机器的IP 填充入s_addr */
my_addr.sin_addr.s_addr = INADDR_ANY;
/* 将此结构的其余空间清零*/
bzero(&(my_addr.sin_zero), 8);
/* 这里是我们一直强调的错误检查!! */ if (bind(sockfd, (struct sockaddr *)&my_addr,
sizeof(struct sockaddr)) == -1)
{
/* 如果调用bind()失败,则给出错误提示,退出*/
perror(“bind”);
exit(1);
}
/* 这里是我们一直强调的错误检查!! */
if (listen(sockfd, BACKLOG) == -1)
{
/* 如果调用listen 失败,则给出错误提示,退出*/
perror(“listen”);
exit(1);
}
while(1)
{
/* 这里是主accept()循环*/
sin_size = sizeof(struct sockaddr_in);
/* 这里是我们一直强调的错误检查!! */
第6 章berkeley 套接字- 163 -
if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1)
{
/* 如果调用accept()出现错误,则给出错误提示,进入下一个循环*/
perror(“accept”);
continue;
}
/* 服务器给出出现连接的信息*/
printf(“server: got connection from %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);
}
/* 关闭new_fd 代表的这个套接字连接*/
close(new_fd);
}
}
/* 等待所有的子进程都退出*/
while(waitpid(-1,NULL,WNOHANG) > 0);
}
为了更清楚的描述这个套接字服务器的运行过程,我把所有的代码都写在了这个大大
的main()主函数中。如果你觉得分成几个子程序会清楚一些,你可以自己式着把这个程序
改成几个小函数。
你可以使用下面这个套接字客户端来得到"Hello, World!"这个字符串。
6.8.2 简单的流式套接字客户端程序
这个程序比起服务器端程序要简单一些。它所做的工作就是connect()到服务器的4000
端口,然后把服务器发送的字符串给显示出来。
客户端程序:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
- 164 - Linux网络编程
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
/* 服务器程序监听的端口号*/
#define PORT 4000
/* 我们一次所能够接收的最大字节数*/
#define MAXDATASIZE 100
int
main(int argc, char *argv[])
{
/* 套接字描述符*/
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
/* 连接者的主机信息*/
struct sockaddr_in their_addr;
/* 检查参数信息*/
if (argc != 2)
{
/* 如果没有参数,则给出使用方法后退出*/
fprintf(stderr,“usage: client hostname/n”);
exit(1);
}
/* 取得主机信息*/
if ((he=gethostbyname(argv[1])) == NULL)
/* 如果gethostbyname()发生错误,则显示错误信息并退出*/
herror(“gethostbyname”);
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
/* 如果socket()调用出现错误则显示错误信息并退出*/
perror(“socket”);
exit(1);
}
第6 章berkeley 套接字- 165 -
/* 主机字节顺序*/
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), 8);
if(connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1)
{
/* 如果connect()建立连接错误,则显示出错误信息,退出*/
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;
}
注意:显然,你必须在运行client 之前先启动server。否则client 的执行会出错(显示“Connection
refused”)。
6.8.3 数据报套接字例程(DatagramSockets)
在这里我不对数据报做过多的描述,下面你将看见另外一对例程(使用数据报):talker.c
和listener.c。
listener 在一台机器上作为服务器程序运行,它监听端口5000 .
talker 发送UDP 数据包到服务器的5000 端口,传送使用者的数据。
下面是listener.c 的源码:
#include <stdio.h>
- 166 - Linux网络编程
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
/* 要连接到的端口号*/
#define MYPORT 5000
/* 能够接收的最长数据*/
#define MAXBUFLEN 100
main()
{
int sockfd;
/* 本机的地址信息*/
struct sockaddr_in my_addr;
/* 连接这的地址信息*/
struct sockaddr_in their_addr;
int addr_len, numbytes;
char buf[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);
/* 自动设置为自己的IP */
my_addr.sin_addr.s_addr = INADDR_ANY;
第6 章berkeley 套接字- 167 -
/* 将结构的其余空间清零*/
bzero(&(my_addr.sin_zero), 8);
/* 绑定端口*/
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1)
{
/* 如果绑定端口出错,则显示错误信息然后退出*/
perror(“bind”);
exit(1);
}
addr_len = sizeof(struct sockaddr);
/* 接收数据*/
if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0,
(struct sockaddr *)&their_addr, &addr_len)) == -1)
{
/* 如果recvfrom()调用出错,则显示错误信息后退出*/
perror(“recvfrom”);
exit(1);
}
/* 显示接收到的数据*/
printf(“got packet from %s/n”,inet_ntoa(their_addr.sin_addr));
printf(“packet is %d bytes long/n”,numbytes);
buf[numbytes] = ‘/0’;
printf(“packet contains /”%s/“/n”,buf);
/* 关闭套接字连接*/
close(sockfd);
}
注意我们调用socket()函数的时候使用的是SOCK_DGRAM 为参数。而且,我们并不
需要listen()或是accept()。这是因为我们使用了无连接的使用者数据报套接字!
下面的是talker.c 的源码:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
- 168 - Linux网络编程
#include <netdb.h>
#include <sys/socket.h>
#include <sys/wait.h>
/* 要连接的端口*/
#define MYPORT 5000
int main(int argc, char *argv[])
{
int sockfd;
/* 连接者的地址信息*/
struct sockaddr_in their_addr;
struct hostent *he;
int numbytes;
if (argc != 3)
{
/* 检测是否有所须参数,如没有,则显示使用方法后退出*/
fprintf(stderr,“usage: talker hostname message/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;
/* 网络字节顺序,短整型*/
第6 章berkeley 套接字- 169 -
their_addr.sin_port = htons(MYPORT);
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
/* 将结构中未用的部分清零*/
bzero(&(their_addr.sin_zero), 8);
if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,
(struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1)
{
/* 把信息发送到指定的主机指定端口,如出错则提示退出*/
perror(“recvfrom”);
exit(1);
}
printf(“sent %d bytes to %s/n”,numbytes,inet_ntoa(their_addr.sin_addr));
/* 关闭套接字描述符后退出*/
close(sockfd);
return 0;
}
上面这两个程序,你需要在一台主机上首先运行listener,然后在另外一台主机上运行
talker。现在看到它们之间的通讯了吗?
最后,我们要注意一点:使用连接的数据报套接字。因为我们在讲使用数据报,所以
我们需要了解它。如果我们的talker 程序使用了connect()函数来连接listener 的地址,那么
talker 程序就能够使用sent()和recv()来处理数据了。因为talker 程序在connect()函数中已
经知道了远程主机的地址和端口号.
6.9 保留端口
6.9.1 简介
大多数网络应用程序使用两个协议:传输控制协议(TCP)和用户数据包协议(UDP)。
他们都使用一个端口号以识别应用程序。端口号为主机上所运行之程序所用,这样就可以
通过号码象名字一样来跟踪每个应用程序。端口号让操作系统更容易的知道有多少个应用
程序在使用系统,以及哪些服务有效。
理论上,端口号可由每台主机上的管理员自由的分配。但为了更好的通信通常采用一
些约定的协议。这些协议使能通过端口号识别一个系统向另一个系统所请求的服务的类
型。基于如此理由,大多数系统维护一个包含端口号及它们所提供哪些服务的文件。
端口号被从1 开始分配。通常端口号超出255 的部分被本地主机保留为私有用途。1
到255 之间的号码被用于远程应用程序所请求的进程和网络服务。每个网络通信循环地进
出主计算机的TCP 应用层。它被两个所连接的号码唯一地识别。这两个号码合起来叫做套
接字.组成套接字的这两个号码就是机器的IP 地址和TCP 软件所使用的端口号。
因为网络通讯至少包括两台机器,所以在发送和接收的机器上都存在一个套接字。由
于每台机器的IP 地址是唯一的。端口号在每台机器中也是唯一的,所以套接字在网络中应
- 170 - Linux网络编程
该是唯一的。这样的设置能使网络中的两个应用程序完全的基于套接字互相对话。
发送和接收的机器维护一个端口表,它列出了所有激活的端口号。两台机器都包括一
个进程叫做绑定,这是每个任务的入口,不过在两台机器上恰恰相反。换句话说,如果一
台机器的源端口号是23 而目的端口号被设置成25,那么另一台机器的源端口号设置成25
目的端口号设置成23。
6.9.2 保留端口
系统留有1024 个保留端口。这些端口是留给系统使用的,在系统中,只有具有Root
权利的人才可以使用1024 以下的端口(包括1024)
这里是RedHat 6.0 中/etc/services 文件:
[root@bbs /etc]# cat /etc/services
# /etc/services:
# $Id: services,v 1.4 1997/05/20 19:41:21 tobias Exp $
#
# Network services, Internet style
#
# Note that it is presently the policy of IANA to assign a single well-known
# port number for both TCP and UDP; hence, most entries here have two entries
# even if the protocol doesn‘t support UDP operations.
# Updated from RFC 1700, “Assigned Numbers” (October 1994). Not all ports
# are included, only the more common ones.
tcpmux 1/tcp # TCP port service multiplexer
ztelnet 2/tcp
echo 7/tcp
echo 7/udp
discard 9/tcp sink null
discard 9/udp sink null
systat 11/tcp users
daytime 13/tcp
daytime 13/udp
netstat 15/tcp
qotd 17/tcp quote
msp 18/tcp # message send protocol
msp 18/udp # message send protocol
chargen 19/tcp ttytst source
chargen 19/udp ttytst source
ftp-data 20/tcp
ftp 21/tcp
第6 章berkeley 套接字- 171 -
fsp 21/udp fspd
ssh 22/tcp # SSH Remote Login Protocol
ssh 22/udp # SSH Remote Login Protocol
telnet 23/tcp
#stelnet 30/tcp
# 24 - private
smtp 25/tcp mail
# 26 - unassigned
time 37/tcp timserver
time 37/udp timserver
rlp 39/udp resource # resource location
nameserver 42/tcp name # IEN 116
whois 43/tcp nicname
re-mail-ck 50/tcp # Remote Mail Checking Protocol
re-mail-ck 50/udp # Remote Mail Checking Protocol
domain 53/tcp nameserver # name-domain server
domain 53/udp nameserver
mtp 57/tcp # deprecated
bootps 67/tcp # BOOTP server
bootps 67/udp
bootpc 68/tcp # BOOTP client
bootpc 68/udp
tftp 69/udp
gopher 70/tcp # Internet Gopher
gopher 70/udp
rje 77/tcp netrjs
finger 79/tcp
www 80/tcp http # WorldWideWeb HTTP
www 80/udp # HyperText Transfer Protocol
link 87/tcp ttylink
kerberos 88/tcp kerberos5 krb5 # Kerberos v5
kerberos 88/udp kerberos5 krb5 # Kerberos v5
supdup 95/tcp
# 100 - reserved
hostnames 101/tcp hostname # usually from sri-nic
iso-tsap 102/tcp tsap # part of ISODE.
csnet-ns 105/tcp cso-ns # also used by CSO name server
csnet-ns 105/udp cso-ns
# unfortunately the poppassd (Eudora) uses a port which has already
- 172 - Linux网络编程
# been assigned to a different service. We list the poppassd as an
# alias here. This should work for programs asking for this service.
# (due to a bug in inetd the 3com-tsmux line is disabled)
#3com-tsmux 106/tcp poppassd
#3com-tsmux 106/udp poppassd
rtelnet 107/tcp # Remote Telnet
rtelnet 107/udp
pop-2 109/tcp postoffice # POP version 2
pop-2 109/udp
pop-3 110/tcp # POP version 3
pop-3 110/udp
sunrpc 111/tcp portmapper # RPC 4.0 portmapper TCP
sunrpc 111/udp portmapper # RPC 4.0 portmapper UDP
#by zixia RPC 111/tcp portmapper # RPC 4.0 portmapper TCP
#RPC 111/udp portmapper # RPC 4.0 portmapper UDP
auth 113/tcp authentication tap ident
sftp 115/tcp
uucp-path 117/tcp
nntp 119/tcp readnews untp # USENET News Transfer Protocol
ntp 123/tcp
ntp 123/udp # Network Time Protocol
netbios-ns 137/tcp # NETBIOS Name Service
netbios-ns 137/udp
netbios-dgm 138/tcp # NETBIOS Datagram Service
netbios-dgm 138/udp
netbios-ssn 139/tcp # NETBIOS session service
netbios-ssn 139/udp
imap2 143/tcp imap # Interim Mail Access Proto v2
imap2 143/udp imap
snmp 161/udp # Simple Net Mgmt Proto
snmp-trap 162/udp snmptrap # Traps for SNMP
cmip-man 163/tcp # ISO mgmt over IP (CMOT)
cmip-man 163/udp
cmip-agent 164/tcp
cmip-agent 164/udp
xdmcp 177/tcp # X Display Mgr. Control Proto
xdmcp 177/udp
nextstep 178/tcp NeXTStep NextStep # NeXTStep window
nextstep 178/udp NeXTStep NextStep # server
bgp 179/tcp # Border Gateway Proto.
第6 章berkeley 套接字- 173 -
bgp 179/udp
prospero 191/tcp # Cliff Neuman‘s Prospero
prospero 191/udp
irc 194/tcp # Internet Relay Chat
irc 194/udp
smux 199/tcp # SNMP UNIX Multiplexer
smux 199/udp
at-rtmp 201/tcp # AppleTalk routing
at-rtmp 201/udp
at-nbp 202/tcp # AppleTalk name binding
at-nbp 202/udp
at-echo 204/tcp # AppleTalk echo
at-echo 204/udp
at-zis 206/tcp # AppleTalk zone information
at-zis 206/udp
qmtp 209/tcp # The Quick Mail Transfer Protocol
qmtp 209/udp # The Quick Mail Transfer Protocol
z3950 210/tcp wais # NISO Z39.50 database
z3950 210/udp wais
ipx 213/tcp # IPX
ipx 213/udp
imap3 220/tcp # Interactive Mail Access
imap3 220/udp # Protocol v3
rpc2portmap 369/tcp
rpc2portmap 369/udp # Coda portmapper
codaauth2 370/tcp
codaauth2 370/udp # Coda authentication server
ulistserv 372/tcp # UNIX Listserv
ulistserv 372/udp
https 443/tcp # MCom
https 443/udp # MCom
snpp 444/tcp # Simple Network Paging Protocol
snpp 444/udp # Simple Network Paging Protocol
saft 487/tcp # Simple Asynchronous File Transfer
saft 487/udp # Simple Asynchronous File Transfer
npmp-local 610/tcp dqs313_qmaster # npmp-local / DQS
npmp-local 610/udp dqs313_qmaster # npmp-local / DQS
npmp-gui 611/tcp dqs313_execd # npmp-gui / DQS
npmp-gui 611/udp dqs313_execd # npmp-gui / DQS
hmmp-ind 612/tcp dqs313_intercell# HMMP Indication / DQS
- 174 - Linux网络编程
hmmp-ind 612/udp dqs313_intercell# HMMP Indication / DQS
#
# UNIX specific services
#
exec 512/tcp
biff 512/udp comsat
login 513/tcp
who 513/udp whod
shell 514/tcp cmd # no passwords used
syslog 514/udp
printer 515/tcp spooler # line printer spooler
talk 517/udp
ntalk 518/udp
route 520/udp router routed # RIP
timed 525/udp timeserver
tempo 526/tcp newdate
courier 530/tcp rpc
conference 531/tcp chat
netnews 532/tcp readnews
netwall 533/udp # -for emergency broadcasts
uucp 540/tcp uucpd # uucp daemon
afpovertcp 548/tcp # AFP over TCP
afpovertcp 548/udp # AFP over TCP
remotefs 556/tcp rfs_server rfs # Brunhoff remote filesystem
klogin 543/tcp # Kerberized ‘rlogin’ (v5)
kshell 544/tcp krcmd # Kerberized ‘rsh’ (v5)
kerberos-adm 749/tcp # Kerberos ‘kadmin’ (v5)
#
webster 765/tcp # Network dictionary
webster 765/udp
#
# From “Assigned Numbers”:
#
#> The Registered Ports are not controlled by the IANA and on most systems
#> can be used by ordinary user processes or programs executed by ordinary
#> users.
#
#> Ports are used in the TCP [45,106] to name the ends of logical
#> connections which carry long term conversations. For the purpose of
第6 章berkeley 套接字- 175 -
#> providing services to unknown callers, a service contact port is
#> defined. This list specifies the port used by the server process as its
#> contact port. While the IANA can not control uses of these ports it
#> does register or list uses of these ports as a convienence to the
#> community.
#
ingreslock 1524/tcp
ingreslock 1524/udp
prospero-np 1525/tcp # Prospero non-privileged
prospero-np 1525/udp
datametrics 1645/tcp old-radius # datametrics / old radius entry
datametrics 1645/udp old-radius # datametrics / old radius entry
sa-msg-port 1646/tcp old-radacct # sa-msg-port / old radacct entry
sa-msg-port 1646/udp old-radacct # sa-msg-port / old radacct entry
radius 1812/tcp # Radius
radius 1812/udp # Radius
radacct 1813/tcp # Radius Accounting
radacct 1813/udp # Radius Accounting
cvspserver 2401/tcp # CVS client/server operations
cvspserver 2401/udp # CVS client/server operations
venus 2430/tcp # codacon port
venus 2430/udp # Venus callback/wbc interface
venus-se 2431/tcp # tcp side effects
venus-se 2431/udp # udp sftp side effect
codasrv 2432/tcp # not used
codasrv 2432/udp # server port
codasrv-se 2433/tcp # tcp side effects
codasrv-se 2433/udp # udp sftp side effect
mysql 3306/tcp # MySQL
mysql 3306/udp # MySQL
rfe 5002/tcp # Radio Free Ethernet
rfe 5002/udp # Actually uses UDP only
cfengine 5308/tcp # CFengine
cfengine 5308/udp # CFengine
bbs 7000/tcp # BBS service
# #
# Kerberos (Project Athena/MIT) services
# Note that these are for Kerberos v4, and are unofficial. Sites running
# v4 should uncomment these and comment out the v5 entries above.
- 176 - Linux网络编程
#
kerberos4 750/udp kerberos-iv kdc # Kerberos (server) udp
kerberos4 750/tcp kerberos-iv kdc # Kerberos (server) tcp
kerberos_master 751/udp # Kerberos authentication
kerberos_master 751/tcp # Kerberos authentication
passwd_server 752/udp # Kerberos passwd server
krb_prop 754/tcp # Kerberos slave propagation
krbupdate 760/tcp kreg # Kerberos registration
kpasswd 761/tcp kpwd # Kerberos “passwd”
kpop 1109/tcp # Pop with Kerberos
knetd 2053/tcp # Kerberos de-multiplexor
zephyr-srv 2102/udp # Zephyr server
zephyr-clt 2103/udp # Zephyr serv-hm connection
zephyr-hm 2104/udp # Zephyr hostmanager
eklogin 2105/tcp # Kerberos encrypted rlogin
#
# Unofficial but necessary (for NetBSD) services
#
supfilesrv 871/tcp # SUP server
supfiledbg 1127/tcp # SUP debugging
#
# Datagram Delivery Protocol services
#
rtmp 1/ddp # Routing Table Maintenance Protocol
nbp 2/ddp # Name Binding Protocol
echo 4/ddp # AppleTalk Echo Protocol
zip 6/ddp # Zone Information Protocol
#
# Services added for the Debian GNU/Linux distribution
poppassd 106/tcp # Eudora
poppassd 106/udp # Eudora
mailq 174/tcp # Mailer transport queue for Zmailer
mailq 174/tcp # Mailer transport queue for Zmailer
ssmtp 465/tcp # SMTP over SSL
gdomap 538/tcp # GNUstep distributed objects
gdomap 538/udp # GNUstep distributed objects
snews 563/tcp # NNTP over SSL
ssl-ldap 636/tcp # LDAP over SSL
omirr 808/tcp omirrd # online mirror
第6 章berkeley 套接字- 177 -
omirr 808/udp omirrd # online mirror
rsync 873/tcp # rsync
rsync 873/udp # rsync
simap 993/tcp # IMAP over SSL
spop3 995/tcp # POP-3 over SSL
socks 1080/tcp # socks proxy server
socks 1080/udp # socks proxy server
rmtcfg 1236/tcp # Gracilis Packeten remote config
server
xtel 1313/tcp # french minitel
support 1529/tcp # GNATS
cfinger 2003/tcp # GNU Finger
ninstall 2150/tcp # ninstall service
ninstall 2150/udp # ninstall service
afbackup 2988/tcp # Afbackup system
afbackup 2988/udp # Afbackup system
icp 3130/tcp # Internet Cache Protocol (Squid)
icp 3130/udp # Internet Cache Protocol (Squid)
postgres 5432/tcp # POSTGRES
postgres 5432/udp # POSTGRES
fax 4557/tcp # FAX transmission service
(old)
hylafax 4559/tcp # HylaFAX client-server protocol
(new)
noclog 5354/tcp # noclogd with TCP (nocol)
noclog 5354/udp # noclogd with UDP (nocol)
hostmon 5355/tcp # hostmon uses TCP (nocol)
hostmon 5355/udp # hostmon uses TCP (nocol)
ircd 6667/tcp # Internet Relay Chat
ircd 6667/udp # Internet Relay Chat
webcache 8080/tcp # WWW caching service
webcache 8080/udp # WWW caching service
tproxy 8081/tcp # Transparent Proxy
tproxy 8081/udp # Transparent Proxy
mandelspawn 9359/udp mandelbrot # network mandelbrot
amanda 10080/udp # amanda backup services
kamanda 10081/tcp # amanda backup services (Kerberos)
kamanda 10081/udp # amanda backup services (Kerberos)
amandaidx 10082/tcp # amanda backup services
amidxtape 10083/tcp # amanda backup services
- 178 - Linux网络编程
isdnlog 20011/tcp # isdn logging system
isdnlog 20011/udp # isdn logging system
vboxd 20012/tcp # voice box system
vboxd 20012/udp # voice box system
binkp 24554/tcp # Binkley
binkp 24554/udp # Binkley
asp 27374/tcp # Address Search Protocol
asp 27374/udp # Address Search Protocol
tfido 60177/tcp # Ifmail
tfido 60177/udp # Ifmail
fido 60179/tcp # Ifmail
fido 60179/udp # Ifmail
# Local services
linuxconf 98/tcp
swat 901/tcp # Add swat service used via inetd
[root@bbs /etc]#
下面,我们以以Web Server 的端口80 做例子来看看这份单子说明了些什么?:
它在services 文件中的那一行是这样的:
www 80/tcp http # WorldWideWeb HTTP
大家可以看到这一行分3 部分:
l www 代表HTTP 协议的端口名(也就是缺省的Web Browser 连接服务器时的端
口)。
l 80/tcp 这一部分是用"/" 号分开的,前半部分表示的是端口号(这里的HTTP
协议的端口是80),后半部分表示是一个TCP 连接(也就是有连接的套接字,相对应的是
UDP)。
l http 代表是HTTP 协议。
l # WorldWideWeb HTTP 最后大家看到的这个是以“#”号打头的,是一些注释。
我们所在意的其实只有HTTP 和80 .通过这个规律,大家可以看到这个RedHat 6.0
自己所定义的保留端口(其中包括一些大于1024 的端口)
这个文件只是定义了每个服务所使用的端口和它的别名。假如你运行
$telnet 127.0.0.1 www
那么你就连接到了本地的Web 服务器上(当然,前提是你已经启动了这个Web 服务
器)。
自己编程的时候应该尽量避免自己的服务器所使用的端口和系统的Services 文件中已
经声明的端口重叠。避免的方法除了参考系统的Services 文件以外,你还可以直接对系统
进行telnet 来进行测试。
比如你的程序想使用4000 端口进行监听网络连接,你为了确定是否已经有程序使用
了4000 端口,可以像下面这样操作:
$telnet 127.0.0.1 4000
第6 章berkeley 套接字- 179 -
如果系统给出了错误信息:
[root@bbs /etc]# telnet 127.0.0.1 4000
Trying 127.0.0.1...
telnet: Unable to connect to remote host: Connection refused
那么说明系统中没有程序使用4000 端口,你可以放心的使用了。
技巧:如果你自己写了一个Server 和Client ,但是Client 却无法连上Server 而你又不知道究竟是哪
个有问题的时候,你可以使用系统的工具telnet 来帮助你。如果你的Server 监听的端口是4000 ,那么可
以直接使用telnet 去连接4000 端口。如果使用telnet 连接正常,那么你就可以确定你的Server 运行正常.
6.10 五种I/O 模式
下面我们简单的介绍一个各种I/O 操作模式。在Linux/UNIX 下,有下面这五种I/O 操
作方式:
l 阻塞I/O
l 非阻塞I/O
l I/O 多路复用
l 信号驱动I/O(SIGIO)
l 异步I/O
这章讲述了一些I/O 的细节,你可以在第一次阅读的时候跳过这部分,然后在第二次
阅读本书的时候再来读这一节。
一般来说,程序进行输入操作有两步:
1.等待有数据可以读
2.将数据从系统内核中拷贝到程序的数据区。
对于一个对套接字的输入操作,第一步一般来说是等待数据从网络上传到本地。当数
据包到达的时候,数据将会从网络层拷贝到内核的缓存中;第二步是从内核中把数据拷贝
到程序的数据区中。
6.10.1 阻塞I/O 模式
阻塞I/O 模式是最普遍使用的I/O 模式。大部分程序使用的都是阻塞模式的I/O 。缺
省的,一个套接字建立后所处于的模式就是阻塞I/O 模式。
对于一个UDP 套接字来说,数据就绪的标志比较简单:
l 已经收到了一整个数据报
l 没有收到。
而TCP 这个概念就比较复杂,需要附加一些其他的变量。
在图6-4 中,一个进程调用recvfrom ,然后系统调用并不返回知道有数据报到达本地
系统,然后系统将数据拷贝到进程的缓存中。(如果系统调用收到一个中断信号,则它的
调用会被中断)
我们称这个进程在调用recvfrom 一直到从recvfrom 返回这段时间是阻塞的。当recvfrom
正常返回时,我们的进程继续它的操作。
- 180 - Linux网络编程
图6-4 tcp 连接的简单示例
6.10.2 非阻塞模式I/O
当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的
I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返
回一个错误给我。”
我们可以参照图6-5 来描述非阻塞模式I/O 。
我们开始对recvfrom 的三次调用,因为系统还没有接收到网络数据,所以内核马上返
回一个EWOULDBLOCK的错误。第四次我们调用recvfrom 函数,一个数据报已经到达了,
内核将它拷贝到我们的应用程序的缓冲区中,然后recvfrom 正常返回,我们就可以对接收
到的数据进行处理了。
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否
一个文件描述符有数据可读(称做polling)。应用程序不停的polling 内核来检查是否I/O
操作已经就绪。这将是一个极浪费CPU 资源的操作。这种模式使用中不是很普遍。
第6 章berkeley 套接字- 181 -
图6-5 非阻塞模式I/O
6.10.3 I/O 多路复用
在使用I/O 多路技术的时候,我们调用select()函数和poll()函数,在调用它们的时候
阻塞,而不是我们来调用recvfrom(或recv)的时候阻塞。图6-6 说明了它的工作方式。
当我们调用select 函数阻塞的时候,select 函数等待数据报套接字进入读就绪状态。当
select 函数返回的时候,也就是套接字可以读取数据的时候。这时候我们就可以调用recvfrom
函数来将数据拷贝到我们的程序缓冲区中。
和阻塞模式相比较,select()和poll()并没有什么高级的地方,而且,在阻塞模式下只需
要调用一个函数:读取或发送,在使用了多路复用技术后,我们需要调用两个函数了:先
调用select()函数或poll()函数,然后才能进行真正的读写。
多路复用的高级之处在于,它能同时等待多个文件描述符,而这些文件描述符(套接
字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
- 182 - Linux网络编程
图6-6 I/O 多路复用
假设我们运行一个网络客户端程序,要同时处理套接字传来的网络数据又要处理本地
的标准输入输出。在我们的程序处于阻塞状态等待标准输入的数据的时候,假如服务器端
的程序被kill(或是自己Down 掉了),那么服务器程端的TCP 协议会给客户端(我们这端)
的TCP 协议发送一个FIN 数据代表终止连接。但是我们的程序阻塞在等待标准输入的数
据上,在它读取套接字数据之前(也许是很长一段时间),它不会看见结束标志.我们就
不能够使用阻塞模式的套接字。
IO 多路技术一般在下面这些情况中被使用:
l 当一个客户端需要同时处理多个文件描述符的输入输出操作的时候(一般来说是
标准的输入输出和网络套接字), I/O 多路复用技术将会有机会得到使用。
l 当程序需要同时进行多个套接字的操作的时候。
l 如果一个TCP 服务器程序同时处理正在侦听网络连接的套接字和已经连接好的套
接字。
l 如果一个服务器程序同时使用TCP 和UDP 协议。
l 如果一个服务器同时使用多种服务并且每种服务可能使用不同的协议(比如inetd
就是这样的)。
I/O 多路服用技术并不只局限与网络程序应用上。几乎所有的程序都可以找到应用I/O
多路复用的地方。
6.10.4 信号驱动I/O 模式
我们可以使用信号,让内核在文件描述符就绪的时候使用SIGIO 信号来通知我们。我
们将这种模式称为信号驱动I/O 模式。
使用这种模式,我们首先需要允许套接字使用信号驱动I/O ,还要安装一个SIGIO 的
第6 章berkeley 套接字- 183 -
处理函数。在这种模式下,系统调用将会立即返回,然后我们的程序可以继续做其他的事
情。当数据就绪的时候,系统会向我们的进程发送一个SIGIO 信号。这样我们就可以在SIGIO
信号的处理函数中进行I/O 操作(或是我们在函数中通知主函数有数据可读)。
我们现在还不必对SIGIO 信号处理函数做过多的了解(在下一章中我们会介绍信号的
有关内容)。对于信号驱动I/O 模式,它的先进之处在于它在等待数据的时候不会阻塞,程
序可以做自己的事情。当有数据到达的时候,系统内核会向程序发送一个SIGIO 信号进行
通知,这样我们的程序就可以获得更大的灵活性,因为我们不必为等待数据进行额外的编
码。
图6-7 信号驱动I/O
信号I/O 可以使内核在某个文件描述符发生改变的时候发信号通知我们的程序。异步
I/O 可以提高我们程序进行I/O 读写的效率。通过使用它,当我们的程序进行I/O 操作的时
候,内核可以在初始化I/O 操作后立即返回,在进行I/O 操作的同时,我们的程序可以做
自己的事情,直到I/O 操作结束,系统内核给我们的程序发消息通知。
基于Berkeley 接口的Socket 信号驱动I/O 使用信号SIGIO。有的系统SIGPOLL 信号,
它也是相当于SIGIO 的。
为了在一个套接字上使用信号驱动I/O 操作,下面这三步是所必须的。
(1)一个和SIGIO 信号的处理函数必须设定。
(2)套接字的拥有者必须被设定。一般来说是使用fcntl 函数的F_SETOWN 参数来
进行设定拥有者。
(3)套接字必须被允许使用异步I/O。一般是通过调用fcntl 函数的F_SETFL 命令,
O_ASYNC 为参数来实现。
注意:我们在设置套接字的属主之前必须将SIGIO 的信号处理函数设好,SIGIO 的缺省动作是被忽
略。因此我们如果以相反的顺序调用这两个函数调用,那么在fcntl 函数调用之后,signal 函数调用之前就
- 184 - Linux网络编程
有一小段时间程序可能接收到SIGIO 信号。那样的话,信号将会被丢弃。在SVR4 系统中,SIGIO 在
<sys/signal.h> 头文件中被定义为SIGPOLL,而SIGPOLL 信号的缺省动作是终止这个进程。所以我们一
定要保证这两个函数的调用顺序:先调用signal 设置好SIGIO 信号处理函数,然后在使用fcntl 函数设置
套接字的属主。
虽然设定套接字为异步I/O 非常简单,但是使用起来困难的部分是怎样在程序中断定
产生SIGIO 信号发送给套接字属主的时候,程序处在什么状态。
1.UDP 套接字的SIGIO 信号
在UDP 协议上使用异步I/O 非常简单.这个信号将会在这个时候产生:
l 套接字收到了一个数据报的数据包。
l 套接字发生了异步错误。
当我们在使用UDP 套接字异步I/O 的时候,我们使用recvfrom()函数来读取数据报数
据或是异步I/O 错误信息。
2.TCP 套接字的SIGIO 信号
不幸的是,异步I/O 几乎对TCP 套接字而言没有什么作用。因为对于一个TCP 套接
字来说, SIGIO 信号发生的几率太高了,所以SIGIO 信号并不能告诉我们究竟发生了什
么事情。在TCP 连接中, SIGIO 信号将会在这个时候产生:
l 在一个监听某个端口的套接字上成功的建立了一个新连接。
l 一个断线的请求被成功的初始化。
l 一个断线的请求成功的结束。
l 套接字的某一个通道(发送通道或是接收通道)被关闭。
l 套接字接收到新数据。
l 套接字将数据发送出去。
l 发生了一个异步I/O 的错误。
举例来说,如果一个正在进行读写操作的TCP 套接字处于信号驱动I/O 状态下,那么
每当新数据到达本地的时候,将会产生一个SIGIO 信号,每当本地套接字发出的数据被远
程确认后,也会产生一个SIGIO 信号。对于我们的程序来讲,是无法区分这两个SIGIO 有
什么区别的。在这种情况下使用SIGIO,TCP 套接字应当被设置为无阻塞模式来阻止一个
阻塞的read 和write(recv 和send)操作。我们可以考虑在一个只进行监听网络连接操作
的套接字上使用异步I/O,这样当有一个新的连接的时候,SIGIO 信号将会产生。
一个对信号驱动I/O 比较实用的方面是NTP(网络时间协议Network Time Protocol)
服务器,它使用UDP。这个服务器的主循环用来接收从客户端发送过来的数据报数据包,
然后再发送请求。对于这个服务器来说,记录下收到每一个数据包的具体时间是很重要的。
因为那将是返回给客户端的值,客户端要使用这个数据来计算数据报在网络上来回所花费
的时间。图6-8 表示了怎样建立这样的一个UDP 服务器。
第6 章berkeley 套接字- 185 -
图6-8 NTP 服务器
大多数的UDP 服务都被设计成图左边的模式。但是NTP 服务器使用的是图右边的技
术。当有一个新的数据报到达的时候,SIGIO 的处理函数会取出它放入一个程序等待读取
的队列,主程序会从这个队列中读取数据。虽然这样会增加程序代码的长度,但是它能够
获取数据包到达服务器程序的准确时间.
6.10.5 异步I/O 模式
当我们运行在异步I/O 模式下时,我们如果想进行I/O 操作,只需要告诉内核我们要
进行I/O 操作,然后内核会马上返回。具体的I/O 和数据的拷贝全部由内核来完成,我们
的程序可以继续向下执行。当内核完成所有的I/O 操作和数据拷贝后,内核将通知我们的
程序。
异步I/O 和信号驱动I/O 的区别是:
l 信号驱动I/O 模式下,内核在操作可以被操作的时候通知给我们的应用程序发送
SIGIO 消息。
l 异步I/O 模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的
应用程序。
如下图,当我们进行一个IO 操作的时候,我们传递给内核我们的文件描述符,我们
的缓存区指针和缓存区的大小,一个偏移量offset,以及在内核结束所有操作后和我们联
系的方法。这种调用也是立即返回的,我们的程序不需要阻塞住来等待数据的就绪。我们
可以要求系统内核在所有的操作结束后(包括从网络上读取信息,然后拷贝到我们提供给
内核的缓存区中)给我们发一个消息。
- 186 - Linux网络编程
图6-9 异步I/O
6.10.6 几种I/O 模式的比较
下面这个表格对这几种I/O 模式进行了对比。
表6-1 几种I/O 模式的对比
阻塞模式非阻塞模式I/O 多路复用信号驱动I/O 异步I/O
初始化
结束
 检查
 检查
 检查
 检查
 检查
 检查
 检查
 检查
 检查
 结束
 检查
 就绪
 初始化
 结束
 信号通知
 初始化
  结束
 初始化
信号通知
我们可以从中清楚的看出各个模式的差别,自己的程序可以挑选合适的模式来使用。
6.10.7 fcntl()函数
阻塞.你应该明白它的意思。简单的说,阻塞就是"睡眠"的同义词.你也许注意到
阻塞
等待
数据
将数据
从内核
拷贝给
用户程

第6 章berkeley 套接字- 187 -
你运行上面的listener 的时候,它只不过是简单的在那里等待接收数据。它调用recvfrom()
函数,但是那个时候(listener 调用recvfrom()函数的时候),它并没有数据可以接收.所以
recvfrom()函数阻塞在那里(也就是程序停在recvfrom()函数处睡大觉)直到有数据传过来。
很多函数都可以阻塞。像accept()函数是阻塞的,所有以recv 开头的函数也都是阻塞
的。它们这样做的原因是他们需要这样做。
当你一开始建立一个套接字描述符的时候,系统内核就被设置为阻塞状态。如果你不
想你的套接字描述符是处于阻塞状态的,那么你可以使用函数fcntl()。
fcntl()函数声明如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl (int fd, int cmd, long arg);
下面我们看一段程序片段:
#include<unistd.h>
#include<fcntl.h>
sockfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
??
??
这样将一个套接字设置为无阻塞模式后,你可以对套接字描述符进行有效的“检测”.如
果你尝试从一个没有接收到任何数据的无阻塞模式的套接字描述符那里读取数据,那么读
取函数会马上返回–1 代表发生错误,全局变量errno 中的值为EWOULDBLOCK。
一般来说,这种无阻塞模式在某些情况下不是一个好的选择。假如你的程序一直没有
接收到传过来的数据,那么你的程序就会进行不停的循环来检查是否有数据到来,浪费了
大量的CPU 时间,而这些CPU 时间本来可以做其他事情的。
另外一个比较好的检测套接字描述符的方法是调用select()函数。
6.10.8 套接字选择项select()函数
这个技术有一点点奇怪但是它对我们的程序确是非常有用的。
假想一下下面的情况:
你写的服务器程序想监听客户端的连接,但是你同时又想从你以前已经建立过的连接
中来读取数据。你可能会说:“没有问题,我不就是需要使用一个accept()函数和一对儿recv()
函数吗?”。不要这么着急,你要想想,当你调用accept()函数阻塞的时候,你还能调用recv()
函数吗?“使用非阻塞套接字!”你可能会这么说。是的,你可以。但是如果你又不想浪
费宝贵的CPU 时间,该怎么办呢?
Select()函数可以帮助你同时监视许多套接字。它会告诉你哪一个套接字已经可以读取
数据,哪个套接字已经可以写入数据,甚至你可以知道哪个套接字出现了错误,如果你想
知道的话。
下面是select()函数的声明:
- 188 - Linux网络编程
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
下面是select()函数的参数说明:
l numfds 是readfds,writefds,exceptfds 中fd 集合中文件描述符中最大的数字加上
1。
l readfds 中的fd 集合将由select 来监视是否可以读取。
l writefds 中的fds 集合将由select 来监视是否可以写入。
l exceptfds 中的fds 集合将由select 来监视是否有例外发生。
如果你想知道是是否可以从标准输入和一些套接字(sockfd)中读取数据,你就可以
把文件描述符和sockfd 加入readfds 中。numfds 的数值设成readfds 中文件描述符中最大的
那个加上一,也就是sockfd+1(因为标准输入的文件描述符的值为0 ,所以其他任何的文
件描述符都会比标准输入的文件描述符大)。
当select()函数返回的时候,readfds 将会被修改用来告诉你哪一个文件描述符你可以用
来读取数据。使用FD_ISSET() 宏,你可以选出select()函数执行的结果。
在进行更深的操作前,我们来看一看怎样处理这些fd_sets。下面这些宏可以是专门进
行这类操作的:
l FD_ZERO(fd_set *set)将一个文件描述符集合清零
l FD_SET(int fd, fd_set *set)将文件描述符fd 加入集合set 中。
l FD_CLR(int fd, fd_set *set)将文件描述符fd 从集合set 中删除.
l FD_ISSET(int fd, fd_set *set)测试文件描述符fd 是否存在于文件描述符set 中.
那么,struct timeval 是什么呢?是这样的,一般来说,如果没有任何文件描述符满足
你的要求,你的程序是不想永远等下去的.也许每隔1 分钟你就想在屏幕上输出信息:
“hello!”。这个代表时间的结构将允许你定义一个超时。在调用select()函数中,如果时间
超过timeval 参数所代表的时间长度,而还没有文件描述符满足你的要求,那么select()函
数将回返回,允许你进行下面的操作。
这个timeval 结构定义如下:
struct timeval
{
int tv_sec ; /* 秒数*/
int tv_usec ; /* 微秒*/
} ;
只需要将tv_sec 设置为你想等待的秒数,然后设置tv_usec 为想等待的微秒数(真正
的时间就是tv_sec 所表示的秒数加上tv_usec 所表示的微秒数).注意,是微秒(百万分之
一)而不是毫秒.一秒有1,000 毫秒,一毫秒有1,000 微秒。所以,一秒有1,000,000 微秒.
当select()函数返回的时候,timeval 中的时间将会被设置为执行为select()后还剩下的
时间。
第6 章berkeley 套接字- 189 -
现在,我们拥有了一个以微秒为单位的记时器!但是因为Linux 和UNIX 一样,最小
的时间片是100 微秒,所以不管你将tv_usec 设置的多小,实质上记时器的最小单位是100
微秒.
另外需要注意的是:
l 如果你将struct timeval 设置为0,则select()函数将会立即返回,同时返回在你的
集合中的文件描述符的状态。
l 如果你将timeout 这个参数设置为NULL,则select()函数进入阻塞状态,除了等
待到文件描述符的状态变化,否则select()函数不会返回。
下面这段代码演示了从标准输入等待输入等待2.5 秒.
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
/* 标准输入的文件描述符数值*/
#define STDIN 0
main()
{
struct timeval tv;
fd_set readfds;
/* 设置等待时间为2 秒零500,000 微秒*/
tv.tv_sec = 2;
tv.tv_usec = 500000;
FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);
/* 因为我们只想等待输入,所以将writefds 和execeptfds 设为NULL */
/* 程序将会在这里等待2 秒零500,000 微秒,除非在这段时间中标准输入有操作*/
select(STDIN+1, &readfds, NULL, NULL, &tv);
/* 测试STDIN 是否在readfds 集合中*/
if (FD_ISSET(STDIN, &readfds))
{
/* 在,则在标准输入有输入*/
printf(“A key was pressed!/n”);
}
else
{ /* 不在,则在标准输入没有任何输入*/
- 190 - Linux网络编程
printf(“Timed out./n”);
}
}
在标准输入上,你需要输入回车后终端才会将输入的信息传给你的程序。所以如果你
没有输入回车的话,程序会一直等待到超时。
对select()函数需要注意的最后一点:如果你的套接字描述符正在通过listen()函数侦听
等待一个外来的网络连接,则你可以使用select()函数(将套接字描述符加入readfds 集合
中)来测试是否存在一个未经处理的新连接。
上面是对select()函数的一些简单介绍。
6.11 带外数据
许多传输层都支持带外数据(Out-Of-Band data),有时候也称为快速数据(Expedited
Data).之所以有带外数据的概念,是因为有时候在一个网络连接的终端想“快速”的告诉
网络另一边的终端一些信息.这个“快速”的意思是我们的“提示”信息会在正常的网络
数据(有时候称为带内数据In-Band data)之前到达网络另一边的终端.这说明,带外数
据拥有比一般数据高的优先级.但是不要以为带外数据是通过两条套接字连接来实现的.事
实上,带外数据也是通过以有的连接来传输。
不幸的是,几乎每个传输层都有不同的带外数据的处理方法。我们下面研究的是TCP
模型的带外数据,提供一个小小的例子来看看它是怎样处理套接字的带外数据,及调用套
接字API 的方法。
流套接字的抽象中包括了带外数据这一概念,带外数据是相连的每一对流套接字间一
个逻辑上独立的传输通道。带外数据是独立于普通数据传送给用户的,这一抽象要求带外
数据设备必须支持每一时刻至少一个带外数据消息被可靠地传送。这一消息可能包含至少
一个字节;并且在任何时刻仅有一个带外数据信息等候发送。对于仅支持带内数据的通讯
协议来说(例如紧急数据是与普通数据在同一序列中发送的),系统通常把紧急数据从普
通数据中分离出来单独存放。这就允许用户可以在顺序接收紧急数据和非顺序接收紧急数
据之间作出选择(非顺序接收时可以省去缓存重叠数据的麻烦)。在这种情况下,用户也
可以“偷看一眼”紧急数据。
某一个应用程序也可能喜欢线内处理紧急数据,即把其作为普通数据流的一部分。这
可以靠设置套接字选项中的SO_OOBINLINE 来实现。在这种情况下,应用程序可能希望
确定未读数据中的哪一些是“紧急”的(“紧急”这一术语通常应用于线内带外数据)。为
了达到这个目的,在Sockets 的实现中就要在数据流保留一个逻辑记号来指出带外数据从
哪一点开始发送.
select()函数可以用于处理对带外数据到来的通知。
6.11.1 TCP 的带外数据
TCP 上没有真正意义上的“带外数据”。TCP 是由一种叫做“紧急模式”的方法来传
输带外数据的。假设一个进程向一个TCP 套接字写入了N 个字节的数据,数据被TCP 套
接字的发送缓冲区缓存,等待被发送到网络上面.我们在图6-10 可以看见数据的排列。
第6 章berkeley 套接字- 191 -
图6-10 TCP 数据的排列
现在进程使用以MSG_OOB 为参数的send()函数写入一个单字节的"带外数据",包
含一个ASCII 字符"a":
send(fd, “a”, 1, MSG_OOB);
TCP 将数据放在下一个可用的发送缓冲区中,并设置这个连接的"紧急指针"(urgent
pointer)指向下一个可用的缓冲区空间.图6-11 表示了我们描述的这个状态,并将带外数
据(Out-Of-Band)表示为"OOB"。
图6-11 ODB 数据
TCP 的紧急指针的指向的位置是在程序发送的OOB 数据的后面。
由图6-11 所表示的TCP 套接字的状态,得知下一个将要发送的数据是TCP 的URG
(Urgent pointer)标志,发送完URG 标志,TCP 才会发送下面的带外数据的那个字节。
但是TCP 所一次发送的数据中,可能只包含了TCP 的URG 标志,却没有包含我们所发送
的OOB 数据.是否会发生这种情况而取决于TCP 将要发送的数据队列中,在OOB 数据
之前的数据的多少。如果在一次发送中,OOB 前的数据已经占满了名额,则TCP 只会发
送URG 标志,不会发送OOB数据
这是一个TCP 紧急数据状态的重要特性:TCP 的信息头指出发送者进入了紧急模式(比
方说,在紧急偏移处设置了URG 标志),但是紧急偏移处的数据并没有必要一定要发送出
去.事实上,如果一个TCP 套接字的流传送停止后(可能是接收方的套接字的接收缓冲区
没有空余空间),为了发送带外数据,系统会发送不包含数据的TCP 数据包,里面标明这
是一个带外数据.这也是我们使用带外数据的一个有利点:TCP 连接就算是在不能向对方
- 192 - Linux网络编程
发送数据的时候,也可以发送出一个带外数据的信号。
如果我们像下面这样发送一个多字节的带外数据:
send(fd, “abc”, 3, MSG_OOB);
在这个例子中, TCP 的紧急指针指向了数据最后一位的后面, 所以只有最后一位数
据(“c”)才被系统认为是“带外数据”。
我们上面大致了解了发送方是怎样发送“带外数据”的了,下面我们来看一看接收方
是怎样接收“带外数据”的。
1.当TCP 收到一个包含URG 标志的数据段时,TCP 会检查“紧急指针”来验证是
否紧急指针所指的数据已经到达本地。也就是说,无论这次是否是TCP 紧急模式从发送方
到接收方的第一次传输带外数据。一般来说,TCP 传输数据会分成许多小的数据包来传输
(每个包的到达时间也不同)。可能有好几个数据包中都包含紧急指针,但是这几个包中
的紧急指针都是指向同一个位置的,也就是说多个紧急指针指向一个数据。需要注意的是,
对于这一个带外数据,虽然有多个指针指向它,但是只有第一个紧急指针会通知程序注意。
2.接收进程收到另外一个带外数据的通知的条件是:有另外一个带外数据的指针到
达.注意这里是“另外一个带外数据”的指针,不是上面的“一个带外数据”的另外一个
指针。首先, SIGURG 信号回发送给套接字的属主,这个取决于是否已经使用fcntl()函数
或ioctl()函数设定套接字的属主和这个程序对SIGURG 信号的具体操作函数。其次,如果
一个程序正阻塞与对这个套接字描述符的select()函数的调用中,则select()函数会产生一个
例外,然后返回。
注意:进程收到带外数据的通知的时候,并不会在乎带外数据的真正数据是否到达。
3.当紧急指针所指的真正的带外数据通过TCP 网络到达接收端的时候,数据或者被
放入带外数据缓冲区或是只是简单的和普通的网络数据混合在一起。在缺省的条件下,
SO_OOBINLINE 套接字选项是不会被设置的,所以这个单字节的带外数据并没有被防在
套接字的接收缓存区中,而是被放入属于这个套接字的一个单独的带外数据缓存区中。如
果这个进程想读取这个带外数据的具体内容的话,唯一的办法就是调用recv,recvfrom,
或是recvmsg 函数,并且一定要指定MSG_OOB 标志。
4.如果一个进程将套接字设置为SO_OOBINLINE 属性,则由紧急指针所指的,代表
带外数据的那个字节将回被放在正常套接字缓冲区中的最左边.在这种情况下,进程不能
指定MSG_OOB 来读取这个一个字节的带外数据,但是它可以知道带外数据的到达时间:
通过检查套接字的带外数据标记.
有可能发生的一些错误:
5.如果当连接没有发送带外数据的时候进程来读取带外数据(比如说,通过MSG_OOB
参数来接收函数),则EINVAL 将会被返回。
6.当真正的带外数据到达之前的时候,进程被通知(SIGURG 或是select 函数)有带
外数据到达(也就是说带外数据的通知信号已经到达),如果进程尝试读取带外数据,则
返回EWOULDFBLOCK .进程所能做的只是去读取套接字的接收缓存区.(也许,由于
缓存区的数据以满,带外数据的那个字节信息无法传输过来,这样的话也许你需要清理一
下接收缓存区来给带外数据空出一些空间)
7.如果进程尝试多次读取同一个带外数据,则EINVAL 将会被返回。
8.如果进程将套接字属性设置为SO_OOBINLINE ,然后尝试通过指定MSG_OOB
第6 章berkeley 套接字- 193 -
标志来读取带外数据,则EINVAL 将会被返回。
下面我们将前面的套接字例程做一些变动来测试带外数据的发送与接收.
6.11.2 OOB 传输套接字例程(服务器代码Server.c)
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
/* 服务器要监听的本地端口*/
#define MYPORT 4000
/* 能够同时接受多少没有accept 的连接*/
#define BACKLOG 10
void
sig_urg(int signo);
main()
{
/* 在sock_fd 上进行监听,new_fd 接受新的连接*/
int sock_fd, new_fd ;
/* 用于存储以前系统缺省的SIGURL 处理器的变量*/ void * old_sig_urg_handle ;
/* 自己的地址信息*/
struct sockaddr_in my_addr;
/* 连接者的地址信息*/
struct sockaddr_in their_addr;
int sin_size;
int n ;
char buff[100] ;
/* 这里就是我们一直强调的错误检查.如果调用socket() 出错,则返回*/
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
- 194 - Linux网络编程
/* 输出错误提示并退出*/
perror(“socket”);
exit(1);
}
/* 主机字节顺序*/
my_addr.sin_family = AF_INET;
/* 网络字节顺序,短整型*/
my_addr.sin_port = htons(MYPORT);
/* 将运行程序机器的IP 填充入s_addr */
my_addr.sin_addr.s_addr = INADDR_ANY;
/* 将此结构的其余空间清零*/
bzero(&(my_addr.sin_zero), 8);
/* 这里是我们一直强调的错误检查!! */ if (bind(sockfd, (struct sockaddr *)&my_addr,
sizeof(struct sockaddr)) == -1)
{
/* 如果调用bind()失败,则给出错误提示,退出*/
perror(“bind”);
exit(1);
}
/* 这里是我们一直强调的错误检查!! */
if (listen(sockfd, BACKLOG) == -1)
{
/* 如果调用listen 失败,则给出错误提示,退出*/
perror(“listen”);
exit(1);
}
/* 设置SIGURG 的处理函数 sig_urg */
old_sig_urg_handle = signal(SIGURG, sig_urg);
/* 更改connfd 的属主*/
fcntl(sockfd, F_SETOWN, getpid());
while(1)
{
第6 章berkeley 套接字- 195 -
/* 这里是主accept()循环*/
sin_size = sizeof(struct sockaddr_in);
/* 这里是我们一直强调的错误检查!! */
if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1)
{
/* 如果调用accept()出现错误,则给出错误提示,进入下一个循环*/
perror(“accept”);
continue;
}
/* 服务器给出出现连接的信息*/
printf(“server: got connection from %s/n”, inet_ntoa(their_addr.sin_addr));
/* 这里将建立一个子进程来和刚刚建立的套接字进行通讯*/
if (!fork())
/* 这里是子进程*/
while(1)
{
if((n = recv(new_fd, buff, sizeof(buff)–1)) == 0)
{
printf(“received EOF/n”);
break ;
}
buff[n] = 0 ;
printf(“Recv %d bytes: %s/n”, n, buff);
}
/* 关闭new_fd 代表的这个套接字连接*/
close(new_fd);
}
}
/* 等待所有的子进程都退出*/
while(waitpid(-1,NULL,WNOHANG) > 0);
/* 恢复系统以前对SIGURG 的处理器*/
signal(SIGURG, old_sig_urg_handle);
}
void
sig_urg(int signo)
{
- 196 - Linux网络编程
int n;
char buff[100] ;
printf(“SIGURG received/n”);
n = recv(new_fd, buff, sizeof(buff)– 1, MSG_OOB);
buff [ n ] = 0 ;
printf(“recv %d OOB byte: %s/n” , n, buff);
}
6.11.3 OOB 传输套接字例程(客户端代码Client.c)
下面是客户端程序:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
/* 服务器程序监听的端口号*/
#define PORT 4000
/* 我们一次所能够接收的最大字节数*/
#define MAXDATASIZE 100
int
main(int argc, char *argv[])
{
/* 套接字描述符*/
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
/* 连接者的主机信息*/
struct sockaddr_in their_addr;
/* 检查参数信息*/
if (argc != 2)
{
第6 章berkeley 套接字- 197 -
/* 如果没有参数,则给出使用方法后退出*/
fprintf(stderr,“usage: client hostname/n”);
exit(1);
}
/* 取得主机信息*/
if ((he=gethostbyname(argv[1])) == NULL)
/* 如果gethostbyname()发生错误,则显示错误信息并退出*/
herror(“gethostbyname”);
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
/* 如果socket()调用出现错误则显示错误信息并退出*/
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), 8);
if(connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1)
{
/* 如果connect()建立连接错误,则显示出错误信息,退出*/
perror(“connect”);
exit(1);
}
/* 这里就是我们说的错误检查! */
if (send(new_fd, “123”, 3, 0) == -1)
{
/* 如果错误,则给出错误提示,然后关闭这个新连接,退出*/
perror(“send”);
close(new_fd);
- 198 - Linux网络编程
exit(0);
}
printf(“Send 3 byte of normal data/n”);
/* 睡眠1 秒*/
sleep(1);
if (send(new_fd, “4”, 1, MSG_OOB)== -1)
{
perror(“send”);
close(new_fd);
exit(0);
}
printf(“Send 1 byte of OOB data/n”);
sleep(1);
if (send(new_fd, “56”, 2, 0) == -1)
{
perror(“send”);
close(new_fd);
exit(0);
}
printf(“Send 2 bytes of normal data/n”);
sleep(1);
if (send(new_fd, “7”, 1, MSG_OOB)== -1)
{
perror(“send”);
close(new_fd);
exit(0);
}
printf(“Send 1 byte of OOB data/n”);
sleep(1);
if (send(new_fd, “89”, 2, MSG_OOB)== -1)
{
perror(“send”);
close(new_fd);
exit(0);
}
printf(“Send 2 bytes of normal data/n”);
sleep(1);
第6 章berkeley 套接字- 199 -
close(sockfd);
return 0;
}
6.11.4 编译例子
注意:你显然需要在运行client 之前启动server.否则client 会执行出错(显示“Connection
refused”).
当只有一个连接的时候(因为这个服务器是多进程的,所以如果有多个连接同时存在
可能会导致屏幕输出混乱),可以得到下面的结果:(注意是使用我们下面的客户程序来连
接的,并且假设你运行我们的服务器程序是在本地机器上面)
root@bbs# gcc –o server server.c
root@bbs# gcc –o client client.c
root@bbs# ./server
root@bbs# ./client 127.0.0.1
Send 3 bytes of normal data <- Client输出
Recv 3 bytes: 123 <- Server输出
Send 1 byte of OOB data <- Client输出
SIGURG received <- Server输出
Recv 1 OOB byte: 4 <- Server输出
Send 2 bytes of normal data <- Client输出
Recv 2 bytes: 56 <- Server输出
Send 1 byte of OOB data <- Client输出
SIGURG Received <- Server输出
Recv 1 OOB byte: 7 <- Server输出
received EOF <- Server输出
这个结果正是我们想要的。每一个客户端发送的带外数据都导致服务器端产生了
SIGURG 信号,服务器端收到SIGURG 信号后,就去读取带外数据了。
6.12 使用Inetd(Internet 超级服务器)
6.12.1 简介
利用inetd 来做网路程序设计是个既简单又稳定的设计方法,您不需要考虑到复杂的
socket programming。您的设计工作几乎在设计好通讯协定後就完成了,所需要的技巧,仅
为简单的文字分析技巧。
6.12.2 一个简单的inetd 使用的服务器程序hello inet service
首先,我们先来撰写一个称为hello 的服务程序。
hello.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
- 200 - Linux网络编程
void main(void)
{
/* 作为一般的程序,这个printf 将输出到标准输出*/
printf(“Welcome!/n Hello! World! /n”);
}
这个程序很简单,不是吗?
编译。
root@bbs$ gcc -o hello hello.c
好了,现在我们已经拥有这个程序的可执行版本了。如果你直接执行这个程序的话,
会出现下面这样的结果:
root@bbs#./hello
Welcome!
Hello! World!
root@bbs#
啊,程序将输出写出来了,我们成功了!注意!我们需要的是进行网络上的传输操作,
现在我们做到的只能够在本地给你显示一些字符而已,我们需要的是能够传输到网络的另
一端。
6.12.3 /etc/services 和/etc/inetd.conf 文件
我们必须通过设置系统的两个文件:/etc/services 和/etc/inetd.conf 来对系统进行配置,
从而将我们的hello 程序变成网络可访问的。
下面我们来看看如何设定/etc/services 及/etc/inetd.conf。
在我们更改系统的/etc/services 文件前我们先来做一个测试,以便帮助各位读者能够对
它的作用有更清楚的理解。
我们在本地机器上输入下面的命令:
root@bbs# telnet localhost hello
hello: bad port number
注意系统给出的错误信息:“hello: bad port number”。因为telnet 命令的第二个参数应
该是想登陆系统的端口,我们给出的hello,系统不知道hello 是什么端口,所以它说:“错
误的端口数字”。
那么下面我们这样做:
在/etc/services 中加入以下这一行
hello 20001/tcp
其意义为hello 这项服务是在port 20001、是一个TCP 连接。
当我们进行到这一步的时候,你可以再试试进行刚才给出“hello: bad port number”错
误信息的操作:
root@bbs# telnet lcoalhost hello
Trying 127.0.0.1...
telnet: Unable to connect to remote host: Connection refused
信息变了: Unable to connect to remote host: Connection refused.
第6 章berkeley 套接字- 201 -
这说明系统已经知道了hello 代表的是哪个端口(以为我们上面在/etc/services 文件中
指定了hello 是一个tcp 连接,在20001 端口),但是系统无法和hello 端口建立连接,因为
没有任何程序在监听20001 端口来等待连接。
OK,现在我们已经告诉了系统我们的hello 程序使用什么端口了,可是当我们连接hello
的端口的时候系统还没有将我们的程序执行.下面:
在/etc/inetd.conf 中加入以下这一行:
goodie stream tcp nowait root /full_goodie_path_name/goodie
各个参数的意义为
<service_name><sock_type><proto><flags><user><server_path><args>
l service_name 是需要在系统服务中存在的名称。
l sock_type 有很多种,大多用的是stream/dgram。
l proto 一般用tcp/udp。
l flags 有wait/nowait。
l user 是您指定该程序要以那一个使用者来启动,这个例子中用的是root,如果有
安全性的考量,应该要改用nobody。一般来说,建议您用低权限的使用者,除非必要,不
开放root 使用权。
l server_path 及args,这是您的服务程序的位置及您所想加入的叁数。
接下来重新启动inetd
root@bbs# killall inetd
root@bbs# inetd
root@bbs#_
这样我们便建立起一个port 20001 的hello service。
现在我们来检验一下goodie 是否可以执行:
telnet localhost 20001

telnet your_host_name 20001

telnet locahost hello
执行结果如下:
Trying 127.0.0.1...
Connected to localhost.
Escape character is ‘^)’.
Welcome!
Hello! World!
root@bbs#_
Yahoo!! 我们现在连接成功了!!原来一个简单的,只能显示两行欢迎信息的,没有涉
及到任何网络连接的小程序,现在可以进行网络服务了!很神奇吧!
6.12.4 一个复杂一些的inetd 服务器程序
很简单不是吗? 信不信由您,telnet/pop3/imap/ftp 都是靠这种方式建立起来的服务。当
- 202 - Linux网络编程
然, telnet/pop3/imap/ftp 各项服务都有复杂的命令处理过程,我们现在这个程序只能显示
欢迎信息,但是至少我们已经可以让它做网络上的一个服务程序了。下面我们现在来建立
一点小小的“网络协定”,这个协定使我们可以输入“exit”时,离开程序,而其他的指令
都是输出与输入相同的字串。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(void)
{
/* 网络接受缓存区*/
char buf[1024];
/* 是否接收到了exit 字符串的标志*/
int ok;
/* 输出欢迎信息*/
printf(“Welcome! This is hello service!/n”);
/* 因为Linux/UNIX 系统具有缓存作用, fflush 函数将缓存中的数据立即送出,
防止网络连线的另外一边无法接收到少量的数据*/
fflush(stdout);
/* 初始化OK, 设置为没有接收到exit */
ok=0;
do
/* 如果标准输入没有数据输入,则程序在此处循环等待*/
while (fgets(buf,1023,stdin)==NULL);
/* 检查当前的输入是否为“exit” */ if (strncasecmp(buf,“exit”,4)==0)
{
/* 设置标志位*/
ok=1;
}
/* 将接收到的字符串原样送出*/
printf(buf);
/* 将缓存区中的数据立即发送*/ fflush(stdout);
} while(!ok);
}
因为inetd 将网络的输入作为程序的标准输入,而将程序的输出作为程序的网络输出,
第6 章berkeley 套接字- 203 -
所以程序中的stdin 相当于对网络套接字进行读而stdout 相当于是对网络套接字进行send
操作。
执行:
telnet localhost hello

telnet your_host_name 20001
运行结果如下:
Trying 127.0.0.1...
Connected to localhost.
Escape character is ‘^)’.
Welcome! This is hello service!
输入“help”
help
help
输入“exit”
exit
exit
Connection closed by foreign host.
6.12.5 一个更加复杂的inetd 服务器程序
我们现在已经可以简单的处理网络远程发送过来的命令了,而我们程序所做的处理只
是对stdin 和stdout 进行操作!下面,我们将设计一个稍微复杂一点点的通讯协定,比较通
用于一般用途。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 所支持的命令的数组*/
char *cmds[]={ “help”, “say”, “hello”, “bye”, “exit”, NULL};
/* 判断一个字符串是命令数组中的第几个命令,如果不存在则返回–1 */
int getcmd(char *cmd)
{
int n=0;
while (cmds[n]!=NULL)
{
if (strncasecmp(cmd,cmds[n],strlen(cmds[n]))==0)
return n;
n++;
} return -1;
- 204 - Linux网络编程
}
/* 主函数*/
void main(void)
{
/* 接收的命令的缓存区*/
char buf[1024];
/* 是否为退出命令的标志*/
int ok;
/* 输出欢迎信息*/
printf(“Welcome! This is hello service!/n”);
/* 清除标准输出的缓存*/
fflush(stdout);
/* 初始设置ok 为没有收到退出命令*/
ok=0;
/* 程序主循环体*/
do
/* 如果程序没有接收到输入则循环等待*/
while (fgets(buf,1023,stdin)==NULL) ;
/* 收到数据后进行命令判断*/
switch (getcmd(buf))
case -1: printf(“Unknown command!/n”); break;
case 0: printf(“How may I help you, sir?/n”); break;
case 1: printf(“I will say %s”,&buf[3]); break;
case 2: printf(“How‘re you doing today?/n”); break;
case 3: printf(“Si ya, mate!/n”); ok=1; break;
case 4: printf(“Go ahead!/n”); ok=1; break;
}
/* 清空输出缓冲区*/
fflush(stdout);
} while(!ok);
}
第6 章berkeley 套接字- 205 -
运行:
telnet localhost hello

telnet your_host_name 2001
试试看输入“help ”、“say”、“hello”、“bye”、“exit”等等指令,及其它一些不在命令
列中的指令。
好了,现在我们知道了,Inetd 就是将我们写的使用标准输出和标准输入的程序转变成
网络程序,这样可以大大的简化我们的编程,避免了和什么socket()、recv()、send() 函数
打交道。
6.12.6 程序必须遵守的安全性准则
注意:在设计inetd 服务程序时,要特别注意buffer overflow(缓存区溢出)的问题,
也就是以下这种状况:
char buffer_overflow[64];
fscanf(stdin,“%s”,buffer_overflow);
历来几乎所有的安全漏洞都是由此而来的。
你一定不可这样用,不论任何理由,类同的用法也不可以。黑客高手可以透过将您的
buffer 塞爆,然後塞进他自己的程序进来执行。
6.12.7 小结
通过Linux 系统提供的inetd 服务,我们可以方便的编写网络程序而从来不用去在乎那
些看起来高深难懂的套接字函数.你所需要做的只是写一个普通的读写标准输入输出的程
序,然后去配置一下系统inetd 的配置文件:/etc/services 文件和/etc/inetd.conf 文件。
6.13 本章总结
BSD UNIX 引入了作为一种机制的套接字抽象,它允许应用程序于操作系统的协议软
件接口。由于许多厂商采纳了套接字,套接字接口已经成了一种事实上的标准。
一个程序调用socket 函数创建一个套接字描述符。Socket 调用的参数指明了所使用的
协议和所要求的服务器.所有的TCP/IP 协议都是Internet 协议族的一部分.系统为套接字
创建了一个内部的数据结构,并把协议族域填上,系统还使用服务类型参数来选择某个指
定的协议(常常是UDP 或TCP)。
其他的系统调用允许应用程序指明一个本地地址(bind),强迫套接字进入被动模式以
便为某个服务器使用(listen),或强迫插口进入主动模式以便为某个客户机使用(connect).服
务器可以进一步使用accept 调用以获得入连接请求(accept),客户机和服务器都可以发送
或接收数据(read 或write)。最后,在结束某个插口后,客户机和服务器都可以撤消该接
口(close)。
套接字有五种I/O 模式:阻塞模式/非阻塞模式/IO 多路复用/信号驱动IO/异步
IO。
带外数据是一种可以快速的通知网络的另一端计算机信息的一种方法.带外数据甚至
可以只告诉远程计算机它的存在而不必将它的具体数据传输过去.带外数据并不是建立两
- 206 - Linux网络编程
个连接来传送数据(至少在TCP 中不是这样),它是将所谓的"带外数据"影射到已经存
在的套接字连接中。
很少有人想写网络应用程序,因此进程的细节最好留给那些想写的人.实践和查阅大
量的例子程序是开始写网络代码的最好的方法.但是要掌握这门技术却要花许多年时间。