比尔~盖茨最聪明的地方不是他做了什么,而是他没做什么。他可以做许许多多的事情,却只专注在自己的操作系统,软件研发二不被市场中别的诱惑吸引。
做人要谦卑,做事要学会不断找问题--比尔·盖茨
在真理面前的谦卑,是比尔·盖茨一种内心态度,远比外面的风光无限、备受世人崇敬更重要。
有了在真理面前的谦卑,就可以在这个浮躁的世界中保持一颗安静的心灵,有更大的创造力和影响力
做人要谦卑,做事要学会不断找问题--比尔·盖茨
在真理面前的谦卑,是比尔·盖茨一种内心态度,远比外面的风光无限、备受世人崇敬更重要。
有了在真理面前的谦卑,就可以在这个浮躁的世界中保持一颗安静的心灵,有更大的创造力和影响力
从大学毕业,现在工作了,钱也稍稍宽松点,为此特定此计划,四年必学的科目:
1。必须学好c语言Bc++,计算机基础和windows用户界面开发。
2.必须深入学习php的构架和他的php模块开发,这个作为前台开发的利器,通读mambo系统的代码和核心设计思想。
3.必须学会freebsd的优化
3.必须学会Apache模块的编写和相关代码的阅读
4.必须学会python,这将为几年后的后台开发打下基础
5.必须学会lucene这个开源搜索引擎的使用,以及它的中文本土化的处理,这个是直接关系到海量数据的存储打下基础
6.必须学好mysql的优化和rpc调用socket深入编程。
7.必须学好英语。
8.寻找一帮牛人朋友,打下打天下的基础,包括关注互联网的需求和销售市场随时把握。
最后,有空安排好先后顺序。。。。(千里之行始于足下)
渐渐发现这些东西太多:
去掉如下内容:
去掉python的学习。
1。必须学好c语言Bc++,计算机基础和windows用户界面开发。
2.必须深入学习php的构架和他的php模块开发,这个作为前台开发的利器,通读mambo系统的代码和核心设计思想。
3.必须学会freebsd的优化
3.必须学会Apache模块的编写和相关代码的阅读
4.必须学会python,这将为几年后的后台开发打下基础
5.必须学会lucene这个开源搜索引擎的使用,以及它的中文本土化的处理,这个是直接关系到海量数据的存储打下基础
6.必须学好mysql的优化和rpc调用socket深入编程。
7.必须学好英语。
8.寻找一帮牛人朋友,打下打天下的基础,包括关注互联网的需求和销售市场随时把握。
最后,有空安排好先后顺序。。。。(千里之行始于足下)
渐渐发现这些东西太多:
去掉如下内容:
去掉python的学习。
FreeBSD套接字模型
BSD套接字构建在基本的UNIX®模型上: 一切都是文件。那么,在我们的例子中,套接字将使我接收一个HTTP文件,就这么说。然后我们要负责将 PNG文件从中提取出来。
由于联网的复杂性,我们不能只使用 open系统调用,或open() C 函数。而是我们需要分几步 “打开”一个套接字。
一旦我们做了这些,我们就能以处理任何文件描述符 的方式处理套接字。我们从它读取 (read),向它写入(write),建立管道(pipe), 必定还要关闭(close)它。
重要的套接字函数
FreeBSD提供了与套接字相关的不同函数, “打开”一个套接字我们只需要四个函数。 有时我们只需要两个。
1 客户端-服务器差异
典型情况中,以套接字为基础的数据通信一端是一个 服务器,另一端是一个客户端。
1.1 通用元素
1.1.1 socket
这一个函数在客户端和服务器都要使用:socket(2)。它是这样被声明的:
int socket(int domain, int type, int protocol);
返回值的类型与open的相同,一个整数。 FreeBSD从和文件句柄相同的池中分配它的值。这就是允许套接字被以对文件相同的方式处理的原因。
参数domain告诉系统你需要使用什么协议族。有许多种协议族存在,有些是某些厂商专有的,其它的都非常通用。协议族的声明在 sys/socket.h中
使用PF_INET是对于 UDP, TCP 和其它网间协议(IPv4)的情况。
对于参数type有五个定义好的值,也在 sys/socket.h中。这些值都以 “SOCK_”开头。 其中最通用的是SOCK_STREAM, 它告诉系统你正需要一个可靠的流传送服务 (和PF_INET一起使用时是指 TCP)。
如果指定SOCK_DGRAM, 你是在请求无连接报文传送服务 (在我们的情形中是UDP)。
如何你需要处理基层协议 (例如IP),或者甚至是网络接口 (例如,以太网),你就需要指定 SOCK_RAW。
最后,参数protocol取决于前两个参数,并非总是有意义。在以上情形中,使用取值0。
未连接的套接字: 对于函数socket 我们还没有指定我们要连往什么其它(主机)系统。 我们新建的套接字还是未连接的。这是有意的:拿电话类比,我们刚把调制解调器接在电话线上。我们既没有告诉调制解调器发起一个呼叫,也不会应答电话振铃。
1.1.2 sockaddr
各种各样的套接字函数需要指定地址,那是一小块内存空间 (用C语言术语是指向一小块内存空间的指针)。在 sys/socket.h中有各种各样如 struct sockaddr的声明。 这个结构是这样被声明的:
/*
* 内核用来存储大多数种类地址的结构
*/
struct sockaddr {
unsigned char sa_len; /* 总长度 */
sa_family_t sa_family; /* 地址族 */
char sa_data[14]; /* 地址值,实际可能更长 */
};
#define SOCK_MAXADDRLEN 255 /* 可能的最长的地址长度 */
注意对于sa_data域的定义具有不确定性。 那只是被定义为14字节的数组, 注释暗示内容可能超过14字节
这种不确定性是经过深思熟虑的。套接字是个非常强大的接口。多数人可能认为比Internet接口强不到哪里 ──大多数应用现在很可能都用它 ──套接字可被用于几乎任何种类的进程间通信, Internet(更精确的说是IP)只是其中的一种。
sys/socket.h提到的各种类型的协议 将被按照地址族对待,并把它们就列在 sockaddr定义的前面:
/*
* 地址族
*/
#define AF_UNSPEC 0 /* 未指定 */
#define AF_LOCAL 1 /* 本机 (管道,portal) */
#define AF_UNIX AF_LOCAL /* 为了向前兼容 */
#define AF_INET 2 /* 网间协议: UDP, TCP, 等等 */
#define AF_IMPLINK 3 /* arpanet imp 地址 */
#define AF_PUP 4 /* pup 协议: 例如BSP */
#define AF_CHAOS 5 /* MIT CHAOS 协议 */
#define AF_NS 6 /* 施乐(XEROX) NS 协议 */
#define AF_ISO 7 /* ISO 协议 */
#define AF_OSI AF_ISO
#define AF_ECMA 8 /* 欧洲计算机制造商协会 */
#define AF_DATAKIT 9 /* datakit 协议 */
#define AF_CCITT 10 /* CCITT 协议, X.25 等 */
#define AF_SNA 11 /* IBM SNA */
#define AF_DECnet 12 /* DECnet */
#define AF_DLI 13 /* DEC 直接数据链路接口 */
#define AF_LAT 14 /* LAT */
#define AF_HYLINK 15 /* NSC Hyperchannel */
#define AF_APPLETALK 16 /* Apple Talk */
#define AF_ROUTE 17 /* 内部路由协议 */
#define AF_LINK 18 /* 协路层接口 */
#define pseudo_AF_XTP 19 /* eXpress Transfer Protocol (no AF) */
#define AF_COIP 20 /* 面向连接的IP, 又名 ST II */
#define AF_CNT 21 /* Computer Network Technology */
#define pseudo_AF_RTIP 22 /* 用于识别RTIP包 */
#define AF_IPX 23 /* Novell 网间协议 */
#define AF_SIP 24 /* Simple 网间协议 */
#define pseudo_AF_PIP 25 /* 用于识别PIP包 */
#define AF_ISDN 26 /* 综合业务数字网(Integrated Services Digital Network) */
#define AF_E164 AF_ISDN /* CCITT E.164 推荐 */
#define pseudo_AF_KEY 27 /* 内部密钥管理功能 */
#define AF_INET6 28 /* IPv6 */
#define AF_NATM 29 /* 本征ATM访问 */
#define AF_ATM 30 /* ATM */
#define pseudo_AF_HDRCMPLT 31 /* 由BPF使用,就不必在接口输出例程
* 中重写头文件了
*/
#define AF_NETGRAPH 32 /* Netgraph 套接字 */
#define AF_SLOW 33 /* 802.3ad 慢速协议 */
#define AF_SCLUSTER 34 /* Sitara 集群协议 */
#define AF_ARP 35
#define AF_BLUETOOTH 36 /* 蓝牙套接字 */
#define AF_MAX 37
用于指定IP的是 AF_INET。这个符号对应着常量 2。
在sockaddr中的域 sa_family指定地址族, 从而决定预先只确定下大致字节数的 sa_data的实际大小。
特别是当地址族 是AF_INET时,我们可以使用 struct sockaddr_in,这可在 netinet/in.h中找到,任何需要 sockaddr的地方都以此作为实际替代。
/*
* 套接字地址,Internet风格
*/
struct sockaddr_in {
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
我们可这样描绘它的结构:
三个重要的域是: sin_family,结构体的字节1; sin_port,16位值,在字节2和3; sin_addr,一个32位整数,表示 IP地址,存储在字节4-7。
现在,让我们尝试填满它。让我们假设我们正在写一个 daytime协议的客户端,这个协议只是简单的规定服务器写出一个代表当前日期和时间文本字符串到端口13。 我们需要使用 TCP/IP,所以我们需要指定在地址族域指定 AF_INET。 AF_INET被定义为 2。让我们使用 IP地址192.43.244.18,这指向 美国联邦政府(time.nist.gov)的服务器。
顺便说一下,域sin_addr被声明为类型 struct in_addr,这个类型定义在 netinet/in.h之中:
/*
* Internet 地址 (由历史原因而形成的结构)
*/
struct in_addr {
in_addr_t s_addr;
};
而in_addr_t是一个32位整数。
192.43.244.18 只是为了表示32位整数的方便写法,按每个八位字节列出, 以最高位的字节开始。
到目前为止,我已经看见了sockaddr。我们的计算机并不将短整数存储为一个16位实体,而是一个2字节序列。同样的,计算机将32位整数存储为4字节序列。
想象我们这样写程序:
sa.sin_family = AF_INET;
sa.sin_port = 13;
sa.sin_addr.s_addr = (((((192 << 8) | 43) << 8) | 244) << 8) | 18;
结果会是什么样的呢?
好,那当然是要依赖于其它因素的。在Pentium®或其它x86 为基础的计算机上,它会像这样:
在另一个不同的系统上,它可能会是:
在一台PDP计算机上,它可能又是另一个样子。 不过上面两种情况是今天最常用的了。
译者注: PDP的字节顺序在英语中称为middle-endian或mixed-endian。例如,原数0x44332211会被PDP存储为0x33441122。 VAX也采用这种字节顺序。
通常,要书写可移植的代码,程序员假设不存在那些差异。他们回避这种差异(除了他们使用汇编语言写代码的时候)。唉,可你不能在为套接字写代码时那样轻易的回避这种差异。
为什么?
因为当与另一台计算机通信时, 你通常不知道对方存储数据时是先存放最高位字节 (MSB)还是最低位字节 (LSB)。
你可能会有问题,“那么,套接字可以为我把握这种差异吗?”
它不能。
这个回答可能先是让你感到惊讶, 请记住通用的套接字接口只明白结构体sockaddr 中的域sa_len和sa_family。 你不必担心那里的字节顺序(当然, 在FreeBSD上sa_family只有一个字节, 但是许多其它的 UNIX® 系统没有 sa_len 并使用2字节给 sa_family,而且数据使用何种顺序都取决于计算机(译者注:此处英文原文的用词为“is native to”))。
其余的数据,也就只剩下sa_data[14]。 依照地址族,套接字只是将那些数据转发到目的地。
事实上,我们输入一个端口号, 是为了让其它计算机知道我们需要什么服务。并且,当我们提供服务时, 只有读取了端口号我们才知道其它计算机期望从我们这里获得什么服务。另一方面,套接字只将端口号作为数据转发,完全不去理会(译者注:此处英文原文用词为“interpret”)其中的内容。
同样的,我们输入IP地址,告诉途经的每台计算机要将我们的数据发送到哪里。 套接字依然只将其按数据转发。
那就是为什么我们(指程序员,而不是套接字)不得不把使用在我们的计算机上的字节顺序和发送给其它计算机时使用的传统字节顺序区分开来。
我们将把我们的计算机上使用的字节顺序称为 主机字节顺序, 或者就是主机顺序.
有一个在IP发送多字节数据的传统: 最高位字节(MSB)优先。 这,我们将用网络字节顺序提及, 或者简单的称为网络顺序。
现在,如果我们在Intel计算机上编译上面的代码, 我们的主机字节顺序将产生:
但是网络字节顺序 要求我们先存储数据的最高位字节(MSB):
不幸的是,我们的主机顺序 恰恰与网络顺序相反。
我们有几种方法解决这个问题。一种是在我们的代码中 倒置数值:
sa.sin_family = AF_INET;
sa.sin_port = 13 << 8;
sa.sin_addr.s_addr = (((((18 << 8) | 244) << 8) | 43) << 8) | 192;
这将欺骗我们的编译器把数据按网络字节顺序存储。在一些情形中,这的确是个有效的办法 (例如,用汇编语言编程)。然而,在多数情形中,这会导致一个问题。
想象一下,你用C语言写了一个套接字程序。 你知道它将运行在一台Pentium计算机上, 于是你倒着输入你的所有常量,并且把它们强置为 网络字节顺序。 它工作正常。
然而,有一台,你所信任的旧 Pentium 变成一台生了锈的旧 Pentium。你把它更换为一个 主机顺序与 网络顺序相同的系统。 你需要重新编译你的所有软件。你的所有软件中除了你写的那个程序,都继续工作正常。
你早已经忘记你将全部常量强置为与 主机顺序相反。你花费宝贵时间拽头发,呼唤你曾经听到过的(有些是你编造的)所有上帝的名字, 用击球棍敲打你的显示器,还上演所有其它的传统仪式 试图找到一个原本好端端的程序突然完成不能工作的原因。
最终,你找到了原因,发了一通誓言, 开始重写你的代码。
幸运的是,你不是第一个面对这个问题的人。 其它人已经创建 htons(3) 和 htonl(3) C 语言函数分别将 short and long 从主机字节顺序转换为 网络字节顺序, 并且还有 ntohs(3) 和 ntohl(3) C 语言函数进行着另外的转换。
在最高位字节(MSB)-最前 的系统上,这些函数什么都不做。在 最低位字节(LSB)-最前的系统上它们将值转换为正确的顺序。
这样一来,无论你的软件在什么系统上编译, 如果你使用这些函数,你的数据最终都将是正确的顺序。
1.2 客户端函数
典型情况中,客户端初始化到服务器的连接。 客户端知道要呼叫哪台服务器:它知道服务器的IP地址,并且知道服务器驻守的 端口。这就好比你拿起电话拨号码 (地址),然后,有人应答, 呼叫负责狂欢的人 (端口)。
1.2.1 connect
一旦一个客户端已经建立了一个套接字,就需要把它连接到一个远方系统的一个端口上。这使用 connect(2):
int connect(int s, const struct sockaddr *name, socklen_t namelen);
参数 s 是套接字, 那是由函数socket返回的值。 name 是一个指向 sockaddr的指针,这个结构体我们已经展开讨论过了。 最后,namelen通知系统 在我们的sockaddr结构体中有多少字节。
如果 connect 成功, 返回 0。否则返回 -1 并将错误码存放于 errno之中。
有许多种connect可能失败的原因。例如,试图发起一个Internet连接时, IP 地址可能不存在,或可能停机, 或者就是太忙,或者可能没有在指定端口上有服务器监听。或者直接拒绝任何特定代码的请求。
1.2.2 我们的第一个客户端
现在我们知道足够多去写一个非常简单的客户端, 一个从192.43.244.18获取当前时间并打印到 stdout的程序。
/*
* daytime.c
*
* G. Adam Stanislav 编程
*/
#include
#include
#include
#include
int main() {
register int s;
register int bytes;
struct sockaddr_in sa;
char buffer[BUFSIZ+1];
if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return 1;
}
bzero(&sa, sizeof sa);
sa.sin_family = AF_INET;
sa.sin_port = htons(13);
sa.sin_addr.s_addr = htonl((((((192 << 8) | 43) << 8) | 244) << 8) | 18);
if (connect(s, (struct sockaddr *)&sa, sizeof sa) < 0) {
perror("connect");
close(s);
return 2;
}
while ((bytes = read(s, buffer, BUFSIZ)) > 0)
write(1, buffer, bytes);
close(s);
return 0;
}
继续,把它输入到你的编辑器中,保存为 daytime.c,然后编译并运行:
% cc -O3 -o daytime daytime.c
% ./daytime
52079 01-06-19 02:29:25 50 0 1 543.9 UTC(NIST) *
%
在这一情形中,日期是2001年6月19日,时间是 02:29:25 UTC。你的结果会很自然的变化。
1.3 服务器函数
典型的服务器不初始化连接。 相反,服务器等待客户端呼叫并请求服务。服务器不知道客户端什么时候会呼叫, 也不知道有多少客户端会呼叫。服务器就是这样静坐在那儿,耐心等待,一会儿,又一会儿, 它突然发觉自身被从许多客户端来的请求围困,所有的呼叫都同时来到。
套接字接口提供三个基本的函数处理这种情况。
1.3.1 bind
端口像是电话线分机:在你拨一个号码后, 你拨分机到一个特定的人或部门。
有65535个 IP 端口,但是一台服务器通常只处理从其中一个端口进入的请求。这就像告诉电话室操作员我们处于工作状态并在一个特定分机应答电话。 我们使用 bind(2) 告诉套接字我们要服务的端口。
int bind(int s, const struct sockaddr *addr, socklen_t addrlen);
除了在 addr 中指定端口, 服务器还可以包含其自身的 IP 地址。不过,也可以就使用符号常量 INADDR_ANY,指示服务于无论哪个 IP上的指定端口上的请求。 这个符号和几个相同的常量,声明在 netinet/in.h之中。
#define INADDR_ANY (u_int32_t)0x00000000
想象我们正在为 daytime协议在 TCP/IP的基础上写一个服务器。 回想起使用端口13。我们的sockaddr_in 结构应当像这样:
1.3.2 listen
继续我们的办公室电话类比, 在你告诉电话中心操作员你会在哪个分机后,现在你走进你的办公室,确认你自己的电话已插上并且振铃已被打开。还有,你确认呼叫等待功能开启,这样即使你正在与其它人通话, 也可听见电话振铃。
服务器执守所有经过函数 listen(2) 操作的套接字。
int listen(int s, int backlog);
在这里,变量backlog 告诉套接字在忙于处理上一个请求时还可以接受多少个进入的请求。换句话说,这决定了挂起连接的队列的最大大小。
7.5.1.3.3 accept
在你听见电话铃响后,你应答呼叫接起电话。 现在你已经建立起一个与你的客户的连接。这个连接保持到你或你的客户挂线。
服务器通过使用函数 accept(2) 接受连接。
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
注意,这次 addrlen 是一个指针。这是必要的,因为在此情形中套接字要 填上 addr,这是一个 sockaddr_in 结构体。
返回值是一个整数。其实, accept 返回一个 新套接字。你将使用这个新套接字与客户通信。
老套接字会发生什么呢?它继续监听更多的请求 (想起我们传给listen的变量 backlog了吗?),直到我们 close(关闭) 它。
现在,新套接字仅对通信有意义,是完全接通的。 我们不能再把它传给 listen接受更多的连接。
1.3.4 我们的第一个服务器
我们的第一个服务器会比我们的第一个客户端复杂一些:我们不仅用到了更多的套接字函数, 还需要把程序写成一个守护程序。
这最好写成:在绑定端口后建立一个子进程。 主进程随后退出,将控制权交回给 shell (或者任何调用主进程的程序)。
子进程调用 listen,然后启动一个无休止循环。这个循环接受连接,提供服务, 最后关闭连接的套接字。
/*
* daytimed - 端口 13 的服务器
*
* G. Adam Stanislav 编程
* 2001年6月19日
*/
#include
#include
#include
#include
#include
#include
#define BACKLOG 4
int main() {
register int s, c;
int b;
struct sockaddr_in sa;
time_t t;
struct tm *tm;
FILE *client;
if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return 1;
}
bzero(&sa, sizeof sa);
sa.sin_family = AF_INET;
sa.sin_port = htons(13);
if (INADDR_ANY)
sa.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(s, (struct sockaddr *)&sa, sizeof sa) < 0) {
perror("bind");
return 2;
}
switch (fork()) {
case -1:
perror("fork");
return 3;
break;
default:
close(s);
return 0;
break;
case 0:
break;
}
listen(s, BACKLOG);
for (;;) {
b = sizeof sa;
if ((c = accept(s, (struct sockaddr *)&sa, &b)) < 0) {
perror("daytimed accept");
return 4;
}
if ((client = fdopen(c, "w")) == NULL) {
perror("daytimed fdopen");
return 5;
}
if ((t = time(NULL)) < 0) {
perror("daytimed time");
return 6;
}
tm = gmtime(&t);
fprintf(client, "%.4i-%.2i-%.2iT%.2i:%.2i:%.2iZ\n",
tm->tm_year + 1900,
tm->tm_mon + 1,
tm->tm_mday,
tm->tm_hour,
tm->tm_min,
tm->tm_sec);
fclose(client);
}
}
我们开始于建立一个套接字。然后我们填好 sockaddr_in 类型的结构体 sa。注意, INADDR_ANY的特定使用方法:
if (INADDR_ANY)
sa.sin_addr.s_addr = htonl(INADDR_ANY);
这个常量的值是0。由于我们已经使用 bzero于整个结构体, 再把成员设为0将是冗余。 但是如果我们把代码移植到其它一些 INADDR_ANY可能不是0的系统上, 我们就需要把实际值指定给 sa.sin_addr.s_addr。多数现在C语言 编译器已足够智能,会注意到 INADDR_ANY是一个常量。由于它是0,他们将会优化那段代码外的整个条件语句。
在我们成功调用bind后, 我们已经准备好成为一个 守护进程:我们使用 fork建立一个子进程。 同在父进程和子进程里,变量s都是套接字。 父进程不再需要它,于是调用了close, 然后返回0通知父进程的父进程成功终止。
此时,子进程继续在后台工作。 它调用listen并设置 backlog 为 4。这里并不需要设置一个很大的值, 因为 daytime 不是个总有许多客户请求的协议,并且总可以立即处理每个请求。
最后,守护进程开始无休止循环,按照如下步骤:
调用accept。 在这里等待直到一个客户端与之联系。在这里,接收一个新套接字,c, 用来与其特定的客户通信。
使用 C 语言函数 fdopen 把套接字从一个 低级 文件描述符 转变成一个 C语言风格的 FILE 指针。 这使得后面可以使用 fprintf。
检查时间,按 ISO 8601格式打印到 “文件” client。 然后使用 fclose 关闭文件。这会把套接字一同自动关闭。
我们可把这些步骤 概括 起来,作为模型用于许多其它服务器:
这个流程图很好的描述了顺序服务器, 那是在某一时刻只能服务一个客户的服务器,就像我们的daytime服务器能做的那样。这只能存在于客户端与服务器没有真正的“对话”的时候:服务器一检测到一个与客户的连接,就送出一些数据并关闭连接。整个操作只花费若干纳秒就完成了。
这张流程图的好处是,除了在父进程 fork之后和父进程退出前的短暂时间内, 一直只有一个进程活跃:我们的服务器不占用许多内存和其它系统资源。
注意我们已经将初始化守护进程 加入到我们的流程图中。我们不需要初始化我们自己的守护进程 (译者注:这里仅指上面的示例程序。一般写程序时都是需要的。), 但这是在程序流程中设置signal 处理程序、 打开我们可能需要的文件等操作的好地方。
几乎流程图中的所有部分都可以用于描述许多不同的服务器。 条目 serve 是个例外,我们考虑为一个 “黑盒子”,那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。
并非所有协议都那么简单。许多协议收到一个来自客户的请求,回复请求,然后接收下一个来自同一客户的请求。 因此,那些协议不知道将要服务客户多长时间。这些服务器通常为每个客户启动一个新进程 当新进程服务它的客户时,守护进程可以继续监听更多的连接。
现在,继续,保存上面的源代码为 daytimed.c (用字母d 结束守护程序名是个风俗)。在你编译好后,尝试运行:
% ./daytimed
bind: Permission denied
%
这里发生了什么?正如你将回想起的, daytime协议使用端口13。 但是所有1024以下的端口保留给超级用户 (否则,任何人都可以启动一个守护进程伪装一个常用端口的服务, 这就导致了一个安全漏洞)。
再试一次,这次以超级用户的身份:
# ./daytimed
#
怎么……什么都没有?让我们再试一次:
# ./daytimed
bind: Address already in use
#
在一个时刻,每个端口只能被一个程序绑定。我们的第一个尝试真的成功了:启动了守护子进程并安静的返回。守护子进程仍然在运行,并且继续运行到你关闭它,或是它使用的系统调用失败,或是你重启计算机时。
好,我们知道它正在后台运行着。 但是它正在正常工作吗?我们如何知道它是个正常的 daytime 服务器?只需简单的:
% telnet localhost 13
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
2001-06-19T21:04:42Z
Connection closed by foreign host.
%
telnet 尝试新协议 IPv6,失败了。又重新尝试 IPv4,而后成功了。守护进程工作正常。
如果你可以通过telnet 访问另一个 UNIX 系统,你可以用测试远程访问服务器。 我们计算机没有静态 IP 地址, 所以我这样做:
% who
whizkid ttyp0 Jun 19 16:59 (216.127.220.143)
xxx ttyp1 Jun 19 16:06 (xx.xx.xx.xx)
% telnet 216.127.220.143 13
Trying 216.127.220.143...
Connected to r47.bfm.org.
Escape character is '^]'.
2001-06-19T21:31:11Z
Connection closed by foreign host.
%
又工作正常了。使用域名还会工作正常吗?
% telnet r47.bfm.org 13
Trying 216.127.220.143...
Connected to r47.bfm.org.
Escape character is '^]'.
2001-06-19T21:31:40Z
Connection closed by foreign host.
%
顺序说一句,telnet 在我们的守护进程关闭套接字之后打印消息 Connection closed by foreign host (连接被外部主机关闭)。这告诉我们,实际上,在我们的代码中使用 fclose(client); 的工作情况就像前面说的一样。
BSD套接字构建在基本的UNIX®模型上: 一切都是文件。那么,在我们的例子中,套接字将使我接收一个HTTP文件,就这么说。然后我们要负责将 PNG文件从中提取出来。
由于联网的复杂性,我们不能只使用 open系统调用,或open() C 函数。而是我们需要分几步 “打开”一个套接字。
一旦我们做了这些,我们就能以处理任何文件描述符 的方式处理套接字。我们从它读取 (read),向它写入(write),建立管道(pipe), 必定还要关闭(close)它。
重要的套接字函数
FreeBSD提供了与套接字相关的不同函数, “打开”一个套接字我们只需要四个函数。 有时我们只需要两个。
1 客户端-服务器差异
典型情况中,以套接字为基础的数据通信一端是一个 服务器,另一端是一个客户端。
1.1 通用元素
1.1.1 socket
这一个函数在客户端和服务器都要使用:socket(2)。它是这样被声明的:
int socket(int domain, int type, int protocol);
返回值的类型与open的相同,一个整数。 FreeBSD从和文件句柄相同的池中分配它的值。这就是允许套接字被以对文件相同的方式处理的原因。
参数domain告诉系统你需要使用什么协议族。有许多种协议族存在,有些是某些厂商专有的,其它的都非常通用。协议族的声明在 sys/socket.h中
使用PF_INET是对于 UDP, TCP 和其它网间协议(IPv4)的情况。
对于参数type有五个定义好的值,也在 sys/socket.h中。这些值都以 “SOCK_”开头。 其中最通用的是SOCK_STREAM, 它告诉系统你正需要一个可靠的流传送服务 (和PF_INET一起使用时是指 TCP)。
如果指定SOCK_DGRAM, 你是在请求无连接报文传送服务 (在我们的情形中是UDP)。
如何你需要处理基层协议 (例如IP),或者甚至是网络接口 (例如,以太网),你就需要指定 SOCK_RAW。
最后,参数protocol取决于前两个参数,并非总是有意义。在以上情形中,使用取值0。
未连接的套接字: 对于函数socket 我们还没有指定我们要连往什么其它(主机)系统。 我们新建的套接字还是未连接的。这是有意的:拿电话类比,我们刚把调制解调器接在电话线上。我们既没有告诉调制解调器发起一个呼叫,也不会应答电话振铃。
1.1.2 sockaddr
各种各样的套接字函数需要指定地址,那是一小块内存空间 (用C语言术语是指向一小块内存空间的指针)。在 sys/socket.h中有各种各样如 struct sockaddr的声明。 这个结构是这样被声明的:
/*
* 内核用来存储大多数种类地址的结构
*/
struct sockaddr {
unsigned char sa_len; /* 总长度 */
sa_family_t sa_family; /* 地址族 */
char sa_data[14]; /* 地址值,实际可能更长 */
};
#define SOCK_MAXADDRLEN 255 /* 可能的最长的地址长度 */
注意对于sa_data域的定义具有不确定性。 那只是被定义为14字节的数组, 注释暗示内容可能超过14字节
这种不确定性是经过深思熟虑的。套接字是个非常强大的接口。多数人可能认为比Internet接口强不到哪里 ──大多数应用现在很可能都用它 ──套接字可被用于几乎任何种类的进程间通信, Internet(更精确的说是IP)只是其中的一种。
sys/socket.h提到的各种类型的协议 将被按照地址族对待,并把它们就列在 sockaddr定义的前面:
/*
* 地址族
*/
#define AF_UNSPEC 0 /* 未指定 */
#define AF_LOCAL 1 /* 本机 (管道,portal) */
#define AF_UNIX AF_LOCAL /* 为了向前兼容 */
#define AF_INET 2 /* 网间协议: UDP, TCP, 等等 */
#define AF_IMPLINK 3 /* arpanet imp 地址 */
#define AF_PUP 4 /* pup 协议: 例如BSP */
#define AF_CHAOS 5 /* MIT CHAOS 协议 */
#define AF_NS 6 /* 施乐(XEROX) NS 协议 */
#define AF_ISO 7 /* ISO 协议 */
#define AF_OSI AF_ISO
#define AF_ECMA 8 /* 欧洲计算机制造商协会 */
#define AF_DATAKIT 9 /* datakit 协议 */
#define AF_CCITT 10 /* CCITT 协议, X.25 等 */
#define AF_SNA 11 /* IBM SNA */
#define AF_DECnet 12 /* DECnet */
#define AF_DLI 13 /* DEC 直接数据链路接口 */
#define AF_LAT 14 /* LAT */
#define AF_HYLINK 15 /* NSC Hyperchannel */
#define AF_APPLETALK 16 /* Apple Talk */
#define AF_ROUTE 17 /* 内部路由协议 */
#define AF_LINK 18 /* 协路层接口 */
#define pseudo_AF_XTP 19 /* eXpress Transfer Protocol (no AF) */
#define AF_COIP 20 /* 面向连接的IP, 又名 ST II */
#define AF_CNT 21 /* Computer Network Technology */
#define pseudo_AF_RTIP 22 /* 用于识别RTIP包 */
#define AF_IPX 23 /* Novell 网间协议 */
#define AF_SIP 24 /* Simple 网间协议 */
#define pseudo_AF_PIP 25 /* 用于识别PIP包 */
#define AF_ISDN 26 /* 综合业务数字网(Integrated Services Digital Network) */
#define AF_E164 AF_ISDN /* CCITT E.164 推荐 */
#define pseudo_AF_KEY 27 /* 内部密钥管理功能 */
#define AF_INET6 28 /* IPv6 */
#define AF_NATM 29 /* 本征ATM访问 */
#define AF_ATM 30 /* ATM */
#define pseudo_AF_HDRCMPLT 31 /* 由BPF使用,就不必在接口输出例程
* 中重写头文件了
*/
#define AF_NETGRAPH 32 /* Netgraph 套接字 */
#define AF_SLOW 33 /* 802.3ad 慢速协议 */
#define AF_SCLUSTER 34 /* Sitara 集群协议 */
#define AF_ARP 35
#define AF_BLUETOOTH 36 /* 蓝牙套接字 */
#define AF_MAX 37
用于指定IP的是 AF_INET。这个符号对应着常量 2。
在sockaddr中的域 sa_family指定地址族, 从而决定预先只确定下大致字节数的 sa_data的实际大小。
特别是当地址族 是AF_INET时,我们可以使用 struct sockaddr_in,这可在 netinet/in.h中找到,任何需要 sockaddr的地方都以此作为实际替代。
/*
* 套接字地址,Internet风格
*/
struct sockaddr_in {
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
我们可这样描绘它的结构:
三个重要的域是: sin_family,结构体的字节1; sin_port,16位值,在字节2和3; sin_addr,一个32位整数,表示 IP地址,存储在字节4-7。
现在,让我们尝试填满它。让我们假设我们正在写一个 daytime协议的客户端,这个协议只是简单的规定服务器写出一个代表当前日期和时间文本字符串到端口13。 我们需要使用 TCP/IP,所以我们需要指定在地址族域指定 AF_INET。 AF_INET被定义为 2。让我们使用 IP地址192.43.244.18,这指向 美国联邦政府(time.nist.gov)的服务器。
顺便说一下,域sin_addr被声明为类型 struct in_addr,这个类型定义在 netinet/in.h之中:
/*
* Internet 地址 (由历史原因而形成的结构)
*/
struct in_addr {
in_addr_t s_addr;
};
而in_addr_t是一个32位整数。
192.43.244.18 只是为了表示32位整数的方便写法,按每个八位字节列出, 以最高位的字节开始。
到目前为止,我已经看见了sockaddr。我们的计算机并不将短整数存储为一个16位实体,而是一个2字节序列。同样的,计算机将32位整数存储为4字节序列。
想象我们这样写程序:
sa.sin_family = AF_INET;
sa.sin_port = 13;
sa.sin_addr.s_addr = (((((192 << 8) | 43) << 8) | 244) << 8) | 18;
结果会是什么样的呢?
好,那当然是要依赖于其它因素的。在Pentium®或其它x86 为基础的计算机上,它会像这样:
在另一个不同的系统上,它可能会是:
在一台PDP计算机上,它可能又是另一个样子。 不过上面两种情况是今天最常用的了。
译者注: PDP的字节顺序在英语中称为middle-endian或mixed-endian。例如,原数0x44332211会被PDP存储为0x33441122。 VAX也采用这种字节顺序。
通常,要书写可移植的代码,程序员假设不存在那些差异。他们回避这种差异(除了他们使用汇编语言写代码的时候)。唉,可你不能在为套接字写代码时那样轻易的回避这种差异。
为什么?
因为当与另一台计算机通信时, 你通常不知道对方存储数据时是先存放最高位字节 (MSB)还是最低位字节 (LSB)。
你可能会有问题,“那么,套接字可以为我把握这种差异吗?”
它不能。
这个回答可能先是让你感到惊讶, 请记住通用的套接字接口只明白结构体sockaddr 中的域sa_len和sa_family。 你不必担心那里的字节顺序(当然, 在FreeBSD上sa_family只有一个字节, 但是许多其它的 UNIX® 系统没有 sa_len 并使用2字节给 sa_family,而且数据使用何种顺序都取决于计算机(译者注:此处英文原文的用词为“is native to”))。
其余的数据,也就只剩下sa_data[14]。 依照地址族,套接字只是将那些数据转发到目的地。
事实上,我们输入一个端口号, 是为了让其它计算机知道我们需要什么服务。并且,当我们提供服务时, 只有读取了端口号我们才知道其它计算机期望从我们这里获得什么服务。另一方面,套接字只将端口号作为数据转发,完全不去理会(译者注:此处英文原文用词为“interpret”)其中的内容。
同样的,我们输入IP地址,告诉途经的每台计算机要将我们的数据发送到哪里。 套接字依然只将其按数据转发。
那就是为什么我们(指程序员,而不是套接字)不得不把使用在我们的计算机上的字节顺序和发送给其它计算机时使用的传统字节顺序区分开来。
我们将把我们的计算机上使用的字节顺序称为 主机字节顺序, 或者就是主机顺序.
有一个在IP发送多字节数据的传统: 最高位字节(MSB)优先。 这,我们将用网络字节顺序提及, 或者简单的称为网络顺序。
现在,如果我们在Intel计算机上编译上面的代码, 我们的主机字节顺序将产生:
但是网络字节顺序 要求我们先存储数据的最高位字节(MSB):
不幸的是,我们的主机顺序 恰恰与网络顺序相反。
我们有几种方法解决这个问题。一种是在我们的代码中 倒置数值:
sa.sin_family = AF_INET;
sa.sin_port = 13 << 8;
sa.sin_addr.s_addr = (((((18 << 8) | 244) << 8) | 43) << 8) | 192;
这将欺骗我们的编译器把数据按网络字节顺序存储。在一些情形中,这的确是个有效的办法 (例如,用汇编语言编程)。然而,在多数情形中,这会导致一个问题。
想象一下,你用C语言写了一个套接字程序。 你知道它将运行在一台Pentium计算机上, 于是你倒着输入你的所有常量,并且把它们强置为 网络字节顺序。 它工作正常。
然而,有一台,你所信任的旧 Pentium 变成一台生了锈的旧 Pentium。你把它更换为一个 主机顺序与 网络顺序相同的系统。 你需要重新编译你的所有软件。你的所有软件中除了你写的那个程序,都继续工作正常。
你早已经忘记你将全部常量强置为与 主机顺序相反。你花费宝贵时间拽头发,呼唤你曾经听到过的(有些是你编造的)所有上帝的名字, 用击球棍敲打你的显示器,还上演所有其它的传统仪式 试图找到一个原本好端端的程序突然完成不能工作的原因。
最终,你找到了原因,发了一通誓言, 开始重写你的代码。
幸运的是,你不是第一个面对这个问题的人。 其它人已经创建 htons(3) 和 htonl(3) C 语言函数分别将 short and long 从主机字节顺序转换为 网络字节顺序, 并且还有 ntohs(3) 和 ntohl(3) C 语言函数进行着另外的转换。
在最高位字节(MSB)-最前 的系统上,这些函数什么都不做。在 最低位字节(LSB)-最前的系统上它们将值转换为正确的顺序。
这样一来,无论你的软件在什么系统上编译, 如果你使用这些函数,你的数据最终都将是正确的顺序。
1.2 客户端函数
典型情况中,客户端初始化到服务器的连接。 客户端知道要呼叫哪台服务器:它知道服务器的IP地址,并且知道服务器驻守的 端口。这就好比你拿起电话拨号码 (地址),然后,有人应答, 呼叫负责狂欢的人 (端口)。
1.2.1 connect
一旦一个客户端已经建立了一个套接字,就需要把它连接到一个远方系统的一个端口上。这使用 connect(2):
int connect(int s, const struct sockaddr *name, socklen_t namelen);
参数 s 是套接字, 那是由函数socket返回的值。 name 是一个指向 sockaddr的指针,这个结构体我们已经展开讨论过了。 最后,namelen通知系统 在我们的sockaddr结构体中有多少字节。
如果 connect 成功, 返回 0。否则返回 -1 并将错误码存放于 errno之中。
有许多种connect可能失败的原因。例如,试图发起一个Internet连接时, IP 地址可能不存在,或可能停机, 或者就是太忙,或者可能没有在指定端口上有服务器监听。或者直接拒绝任何特定代码的请求。
1.2.2 我们的第一个客户端
现在我们知道足够多去写一个非常简单的客户端, 一个从192.43.244.18获取当前时间并打印到 stdout的程序。
/*
* daytime.c
*
* G. Adam Stanislav 编程
*/
#include
#include
#include
#include
int main() {
register int s;
register int bytes;
struct sockaddr_in sa;
char buffer[BUFSIZ+1];
if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return 1;
}
bzero(&sa, sizeof sa);
sa.sin_family = AF_INET;
sa.sin_port = htons(13);
sa.sin_addr.s_addr = htonl((((((192 << 8) | 43) << 8) | 244) << 8) | 18);
if (connect(s, (struct sockaddr *)&sa, sizeof sa) < 0) {
perror("connect");
close(s);
return 2;
}
while ((bytes = read(s, buffer, BUFSIZ)) > 0)
write(1, buffer, bytes);
close(s);
return 0;
}
继续,把它输入到你的编辑器中,保存为 daytime.c,然后编译并运行:
% cc -O3 -o daytime daytime.c
% ./daytime
52079 01-06-19 02:29:25 50 0 1 543.9 UTC(NIST) *
%
在这一情形中,日期是2001年6月19日,时间是 02:29:25 UTC。你的结果会很自然的变化。
1.3 服务器函数
典型的服务器不初始化连接。 相反,服务器等待客户端呼叫并请求服务。服务器不知道客户端什么时候会呼叫, 也不知道有多少客户端会呼叫。服务器就是这样静坐在那儿,耐心等待,一会儿,又一会儿, 它突然发觉自身被从许多客户端来的请求围困,所有的呼叫都同时来到。
套接字接口提供三个基本的函数处理这种情况。
1.3.1 bind
端口像是电话线分机:在你拨一个号码后, 你拨分机到一个特定的人或部门。
有65535个 IP 端口,但是一台服务器通常只处理从其中一个端口进入的请求。这就像告诉电话室操作员我们处于工作状态并在一个特定分机应答电话。 我们使用 bind(2) 告诉套接字我们要服务的端口。
int bind(int s, const struct sockaddr *addr, socklen_t addrlen);
除了在 addr 中指定端口, 服务器还可以包含其自身的 IP 地址。不过,也可以就使用符号常量 INADDR_ANY,指示服务于无论哪个 IP上的指定端口上的请求。 这个符号和几个相同的常量,声明在 netinet/in.h之中。
#define INADDR_ANY (u_int32_t)0x00000000
想象我们正在为 daytime协议在 TCP/IP的基础上写一个服务器。 回想起使用端口13。我们的sockaddr_in 结构应当像这样:
1.3.2 listen
继续我们的办公室电话类比, 在你告诉电话中心操作员你会在哪个分机后,现在你走进你的办公室,确认你自己的电话已插上并且振铃已被打开。还有,你确认呼叫等待功能开启,这样即使你正在与其它人通话, 也可听见电话振铃。
服务器执守所有经过函数 listen(2) 操作的套接字。
int listen(int s, int backlog);
在这里,变量backlog 告诉套接字在忙于处理上一个请求时还可以接受多少个进入的请求。换句话说,这决定了挂起连接的队列的最大大小。
7.5.1.3.3 accept
在你听见电话铃响后,你应答呼叫接起电话。 现在你已经建立起一个与你的客户的连接。这个连接保持到你或你的客户挂线。
服务器通过使用函数 accept(2) 接受连接。
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
注意,这次 addrlen 是一个指针。这是必要的,因为在此情形中套接字要 填上 addr,这是一个 sockaddr_in 结构体。
返回值是一个整数。其实, accept 返回一个 新套接字。你将使用这个新套接字与客户通信。
老套接字会发生什么呢?它继续监听更多的请求 (想起我们传给listen的变量 backlog了吗?),直到我们 close(关闭) 它。
现在,新套接字仅对通信有意义,是完全接通的。 我们不能再把它传给 listen接受更多的连接。
1.3.4 我们的第一个服务器
我们的第一个服务器会比我们的第一个客户端复杂一些:我们不仅用到了更多的套接字函数, 还需要把程序写成一个守护程序。
这最好写成:在绑定端口后建立一个子进程。 主进程随后退出,将控制权交回给 shell (或者任何调用主进程的程序)。
子进程调用 listen,然后启动一个无休止循环。这个循环接受连接,提供服务, 最后关闭连接的套接字。
/*
* daytimed - 端口 13 的服务器
*
* G. Adam Stanislav 编程
* 2001年6月19日
*/
#include
#include
#include
#include
#include
#include
#define BACKLOG 4
int main() {
register int s, c;
int b;
struct sockaddr_in sa;
time_t t;
struct tm *tm;
FILE *client;
if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return 1;
}
bzero(&sa, sizeof sa);
sa.sin_family = AF_INET;
sa.sin_port = htons(13);
if (INADDR_ANY)
sa.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(s, (struct sockaddr *)&sa, sizeof sa) < 0) {
perror("bind");
return 2;
}
switch (fork()) {
case -1:
perror("fork");
return 3;
break;
default:
close(s);
return 0;
break;
case 0:
break;
}
listen(s, BACKLOG);
for (;;) {
b = sizeof sa;
if ((c = accept(s, (struct sockaddr *)&sa, &b)) < 0) {
perror("daytimed accept");
return 4;
}
if ((client = fdopen(c, "w")) == NULL) {
perror("daytimed fdopen");
return 5;
}
if ((t = time(NULL)) < 0) {
perror("daytimed time");
return 6;
}
tm = gmtime(&t);
fprintf(client, "%.4i-%.2i-%.2iT%.2i:%.2i:%.2iZ\n",
tm->tm_year + 1900,
tm->tm_mon + 1,
tm->tm_mday,
tm->tm_hour,
tm->tm_min,
tm->tm_sec);
fclose(client);
}
}
我们开始于建立一个套接字。然后我们填好 sockaddr_in 类型的结构体 sa。注意, INADDR_ANY的特定使用方法:
if (INADDR_ANY)
sa.sin_addr.s_addr = htonl(INADDR_ANY);
这个常量的值是0。由于我们已经使用 bzero于整个结构体, 再把成员设为0将是冗余。 但是如果我们把代码移植到其它一些 INADDR_ANY可能不是0的系统上, 我们就需要把实际值指定给 sa.sin_addr.s_addr。多数现在C语言 编译器已足够智能,会注意到 INADDR_ANY是一个常量。由于它是0,他们将会优化那段代码外的整个条件语句。
在我们成功调用bind后, 我们已经准备好成为一个 守护进程:我们使用 fork建立一个子进程。 同在父进程和子进程里,变量s都是套接字。 父进程不再需要它,于是调用了close, 然后返回0通知父进程的父进程成功终止。
此时,子进程继续在后台工作。 它调用listen并设置 backlog 为 4。这里并不需要设置一个很大的值, 因为 daytime 不是个总有许多客户请求的协议,并且总可以立即处理每个请求。
最后,守护进程开始无休止循环,按照如下步骤:
调用accept。 在这里等待直到一个客户端与之联系。在这里,接收一个新套接字,c, 用来与其特定的客户通信。
使用 C 语言函数 fdopen 把套接字从一个 低级 文件描述符 转变成一个 C语言风格的 FILE 指针。 这使得后面可以使用 fprintf。
检查时间,按 ISO 8601格式打印到 “文件” client。 然后使用 fclose 关闭文件。这会把套接字一同自动关闭。
我们可把这些步骤 概括 起来,作为模型用于许多其它服务器:
这个流程图很好的描述了顺序服务器, 那是在某一时刻只能服务一个客户的服务器,就像我们的daytime服务器能做的那样。这只能存在于客户端与服务器没有真正的“对话”的时候:服务器一检测到一个与客户的连接,就送出一些数据并关闭连接。整个操作只花费若干纳秒就完成了。
这张流程图的好处是,除了在父进程 fork之后和父进程退出前的短暂时间内, 一直只有一个进程活跃:我们的服务器不占用许多内存和其它系统资源。
注意我们已经将初始化守护进程 加入到我们的流程图中。我们不需要初始化我们自己的守护进程 (译者注:这里仅指上面的示例程序。一般写程序时都是需要的。), 但这是在程序流程中设置signal 处理程序、 打开我们可能需要的文件等操作的好地方。
几乎流程图中的所有部分都可以用于描述许多不同的服务器。 条目 serve 是个例外,我们考虑为一个 “黑盒子”,那是你要为你自己的服务器专门设计的东西, 并且 “接到其余部分上”。
并非所有协议都那么简单。许多协议收到一个来自客户的请求,回复请求,然后接收下一个来自同一客户的请求。 因此,那些协议不知道将要服务客户多长时间。这些服务器通常为每个客户启动一个新进程 当新进程服务它的客户时,守护进程可以继续监听更多的连接。
现在,继续,保存上面的源代码为 daytimed.c (用字母d 结束守护程序名是个风俗)。在你编译好后,尝试运行:
% ./daytimed
bind: Permission denied
%
这里发生了什么?正如你将回想起的, daytime协议使用端口13。 但是所有1024以下的端口保留给超级用户 (否则,任何人都可以启动一个守护进程伪装一个常用端口的服务, 这就导致了一个安全漏洞)。
再试一次,这次以超级用户的身份:
# ./daytimed
#
怎么……什么都没有?让我们再试一次:
# ./daytimed
bind: Address already in use
#
在一个时刻,每个端口只能被一个程序绑定。我们的第一个尝试真的成功了:启动了守护子进程并安静的返回。守护子进程仍然在运行,并且继续运行到你关闭它,或是它使用的系统调用失败,或是你重启计算机时。
好,我们知道它正在后台运行着。 但是它正在正常工作吗?我们如何知道它是个正常的 daytime 服务器?只需简单的:
% telnet localhost 13
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
2001-06-19T21:04:42Z
Connection closed by foreign host.
%
telnet 尝试新协议 IPv6,失败了。又重新尝试 IPv4,而后成功了。守护进程工作正常。
如果你可以通过telnet 访问另一个 UNIX 系统,你可以用测试远程访问服务器。 我们计算机没有静态 IP 地址, 所以我这样做:
% who
whizkid ttyp0 Jun 19 16:59 (216.127.220.143)
xxx ttyp1 Jun 19 16:06 (xx.xx.xx.xx)
% telnet 216.127.220.143 13
Trying 216.127.220.143...
Connected to r47.bfm.org.
Escape character is '^]'.
2001-06-19T21:31:11Z
Connection closed by foreign host.
%
又工作正常了。使用域名还会工作正常吗?
% telnet r47.bfm.org 13
Trying 216.127.220.143...
Connected to r47.bfm.org.
Escape character is '^]'.
2001-06-19T21:31:40Z
Connection closed by foreign host.
%
顺序说一句,telnet 在我们的守护进程关闭套接字之后打印消息 Connection closed by foreign host (连接被外部主机关闭)。这告诉我们,实际上,在我们的代码中使用 fclose(client); 的工作情况就像前面说的一样。
socket编程原理
2.1 问题的引入
Unix系统的I/O命令集,是从Maltics和早期系统中的命令演变出来的,其模式为打开一读/写一关闭(open-write-read-close)。在一个用户进程进行I/O操作时,它首先调用“打开”获得对指定文件或设备的使用权,并返回称为文件描述符的整型数,以描述用户在打开的文件或设备上进行I/O操作的进程。然后这个用户进程多次调用“读/写”以传输数据。当所有的传输操作完成后,用户进程关闭调用,通知操作系统已经完成了对某对象的使用。
TCP/IP协议被集成到UNIX内核中时,相当于在UNIX系统引入了一种新型的I/O操作。UNIX用户进程与网络协议的交互作用比用户进程与传统的I/O设备相互作用复杂得多。首先,进行网络操作的两个进程在不同机器上,如何建立它们之间的联系?其次,网络协议存在多种,如何建立一种通用机制以支持多种协议?这些都是网络应用编程界面所要解决的问题。
在UNIX系统中,网络应用编程界面有两类:UNIX BSD的套接字(socket)和UNIX System V的TLI。由于Sun公司采用了支持TCP/IP的UNIX BSD操作系统,使TCP/IP的应用有更大的发展,其网络应用编程界面──套接字(socket)在网络软件中被广泛应用,至今已引进微机操作系统Dos和Windows系统中,成为开发网络应用软件的强有力工具,本章将要详细讨论这个问题。
2.2 套接字编程基本概念
在开始使用套接字编程之前,首先必须建立以下概念。
2.2.1 网间进程通信
进程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如UNIX BSD中的管道(pipe)、命名管道(named pipe)和软中断信号(signal),UNIX system V的消息(message)、共享存储区(shared memory)和信号量(semaphore)等,但都仅限于用在本机进程之间通信。网间进程通信要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例)。为此,首先要解决的是网间进程标识问题。同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋于某进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。
其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。
为了解决上述问题,TCP/IP协议引入了下列几个概念。
端口
网络中可以被命名和寻址的通信端口,是操作系统可分配的一种资源。
按照OSI七层协议的描述,传输层与网络层在功能上的最大区别是传输层提供进程通信能力。从这个意义上讲,网络通信的最终地址就不仅仅是主机地址了,还包括可以描述进程的某种标识符。为此,TCP/IP协议提出了协议端口(protocol port,简称端口)的概念,用于标识通信的进程。
端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序(即进程)通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应进程所接收,相应进程发给传输层的数据都通过该端口输出。在TCP/IP协议的实现中,端口操作类似于一般的I/O操作,进程获取一个端口,相当于获取本地唯一的I/O文件,可以用一般的读写原语访问之。
类似于文件描述符,每个端口都拥有一个叫端口号(port number)的整数型标识符,用于区别不同端口。由于TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立,如TCP有一个255号端口,UDP也可以有一个255号端口,二者并不冲突。
端口号的分配是一个重要问题。有两种基本分配方式:第一种叫全局分配,这是一种集中控制方式,由一个公认的中央机构根据用户需要进行统一分配,并将结果公布于众。第二种是本地分配,又称动态连接,即进程需要访问传输层服务时,向本地操作系统提出申请,操作系统返回一个本地唯一的端口号,进程再通过合适的系统调用将自己与该端口号联系起来(绑扎)。TCP/IP端口号的分配中综合了上述两种方式。TCP/IP将端口号分为两部分,少量的作为保留端口,以全局方式分配给服务进程。因此,每一个标准服务器都拥有一个全局公认的端口(即周知口,well-known port),即使在不同机器上,其端口号也相同。剩余的为自由端口,以本地方式进行分配。TCP和UDP均规定,小于256的端口号才能作保留端口。
地址
网络通信中通信的两个进程分别在不同的机器上。在互连网络中,两台机器可能位于不同的网络,这些网络通过网络互连设备(网关,网桥,路由器等)连接。因此需要三级寻址:
1. 某一主机可与多个网络相连,必须指定一特定网络地址;
2. 网络上每一台主机应有其唯一的地址;
3. 每一主机上的每一进程应有在该主机上的唯一标识符。
通常主机地址由网络ID和主机ID组成,在TCP/IP协议中用32位整数值表示;TCP和UDP均使用16位端口号标识用户进程。
网络字节顺序
不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低价先存),有的存高位字节(高价先存)。为保证数据的正确性,在网络协议中须指定网络字节顺序。TCP/IP协议使用16位整数和32位整数的高价先存格式,它们均含在协议头文件中。
连接
两个进程间的通信链路称为连接。连接在内部表现为一些缓冲区和一组协议机制,在外部表现出比无连接高的可靠性。
半相关
综上所述,网络中用一个三元组可以在全局唯一标志一个进程:
(协议,本地地址,本地端口号)
这样一个三元组,叫做一个半相关(half-association),它指定连接的每半部分。
全相关
一个完整的网间进程通信需要由两个进程组成,并且只能使用同一种高层协议。也就是说,不可能通信的一端用TCP协议,而另一端用UDP协议。因此一个完整的网间通信需要一个五元组来标识:
(协议,本地地址,本地端口号,远地地址,远地端口号)
这样一个五元组,叫做一个相关(association),即两个协议相同的半相关才能组合成一个合适的相关,或完全指定组成一连接。
2.2.2 服务方式 www.it55.com在线教程
在网络分层结构中,各层之间是严格单向依赖的,各层次的分工和协作集中体现在相邻层之间的界面上。“服务”是描述相邻层之间关系的抽象概念,即网络中各层向紧邻上层提供的一组操作。下层是服务提供者,上层是请求服务的用户。服务的表现形式是原语(primitive),如系统调用或库函数。系统调用是操作系统内核向网络应用程序或高层协议提供的服务原语。网络中的n层总要向n+1层提供比n-1层更完备的服务,否则n层就没有存在的价值。
在OSI的术语中,网络层及其以下各层又称为通信子网,只提供点到点通信,没有程序或进程的概念。而传输层实现的是“端到端”通信,引进网间进程通信概念,同时也要解决差错控制,流量控制,数据排序(报文排序),连接管理等问题,为此提供不同的服务方式:
面向连接(虚电路)或无连接
面向连接服务是电话系统服务模式的抽象,即每一次完整的数据传输都要经过建立连接,使用连接,终止连接的过程。在数据传输过程中,各数据分组不携带目的地址,而使用连接号(connect ID)。本质上,连接是一个管道,收发数据不但顺序一致,而且内容相同。TCP协议提供面向连接的虚电路。
无连接服务是邮政系统服务的抽象,每个分组都携带完整的目的地址,各分组在系统中独立传送。无连接服务不能保证分组的先后顺序,不进行分组出错的恢复与重传,不保证传输的可靠性。UDP协议提供无连接的数据报服务。
下面给出这两种服务的类型及应用中的例子:
服务类型 www.it55.com在线教程
服 务
例 子
面向连接
可靠的报文流
可靠的字节流
不可靠的连接
文件传输(FTP)
远程登录(Telnet)
数字话音
无连接
不可靠的数据报
有确认的数据报
请求-应答
电子邮件(E-mail)
电子邮件中的挂号信
网络数据库查询
顺序
在网络传输中,两个连续报文在端-端通信中可能经过不同路径,这样到达目的地时的顺序可能会与发送时不同。“顺序”是指接收数据顺序与发送数据顺序相同。TCP协议提供这项服务。
差错控制
保证应用程序接收的数据无差错的一种机制。检查差错的方法一般是采用检验“检查和(Checksum)”的方法。而保证传送无差错的方法是双方采用确认应答技术。TCP协议提供这项服务。
流控制
在数据传输过程中控制数据传输速率的一种机制,以保证数据不被丢失。TCP协议提供这项服务。
字节流
字节流方式指的是仅把传输中的报文看作是一个字节序列,不提供数据流的任何边界。TCP协议提供字节流服务。
报文
接收方要保存发送方的报文边界。UDP协议提供报文服务。
全双工/半双工
端-端间数据同时以两个方向/一个方向传送。
缓存/带外数据
在字节流服务中,由于没有报文边界,用户进程在某一时刻可以读或写任意数量的字节。为保证传输正确或采用有流控制的协议时,都要进行缓存。但对某些特殊的需求,如交互式应用程序,又会要求取消这种缓存。
在数据传送过程中,希望不通过常规传输方式传送给用户以便及时处理的某一类信息,如UNIX系统的中断键(Delete或Control-c)、终端流控制符(Control-s和Control-q),称为带外数据。逻辑上看,好象用户进程使用了一个独立的通道传输这些数据。该通道与每对连接的流相联系。由于Berkeley Software Distribution中对带外数据的实现与RFC 1122中规定的Host Agreement不一致,为了将互操作中的问题减到最小,应用程序编写者除非与现有服务互操作时要求带外数据外,最好不使用它。
2.2.3 客户/服务器模式
IT资讯之家 www.it55.com
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器模式(ClIEnt/Server model),即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。客户/服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于客户/服务器模式的TCP/IP。
客户/服务器模式在操作过程中采取的是主动请求方式:
首先服务器方要先启动,并根据请求提供相应服务:
1. 打开一通信通道并告知本地主机,它愿意在某一公认地址上(周知口,如FTP为21)接收客户请求;
2. 等待客户请求到达该端口;
3. 接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,要激活一新进程来处理这个客户请求(如UNIX系统中用fork、exec)。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。
4. 返回第二步,等待另一客户请求。
5. 关闭服务器
客户方:
1. 打开一通信通道,并连接到服务器所在主机的特定端口;
2. 向服务器发服务请求报文,等待并接收应答;继续提出请求......
3. 请求结束后关闭通信通道并终止。
从上面所描述过程可知:
1. 客户与服务器进程的作用是非对称的,因此编码不同。
2. 服务进程一般是先于客户请求而启动的。只要系统运行,该服务进程一直存在,直到正常或强迫终止。
2.2.4 套接字类型 it55.com
TCP/IP的socket提供下列三种类型套接字。
流式套接字(SOCK_STREAM)
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议(FTP)即使用流式套接字。
数据报式套接字(SOCK_DGRAM)
提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。
原始式套接字(SOCK_RAW)
该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
2.3 基本套接字系统调用
为了更好地说明套接字编程原理,下面给出几个基本套接字系统调用说明。
2.3.1 创建套接字──socket()
应用程序在使用套接字前,首先必须拥有一个套接字,系统调用socket()向应用程序提供创建套接字的手段,其调用格式如下:
SOCKET PASCAL FAR socket(int af, int type, int protocol);
该调用要接收三个参数:af、type、protocol。参数af指定通信发生的区域,UNIX系统支持的地址族有:AF_UNIX、AF_INET、AF_NS等,而DOS、WINDOWS中仅支持AF_INET,它是网际网区域。因此,地址族与协议族相同。参数type 描述要建立的套接字的类型。参数protocol说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此,socket()系统调用实际上指定了相关五元组中的“协议”这一元。
有关socket()的详细描述参看5.2.23。
2.3.2 指定本地地址──bind()
http://www.it55.com/
当一个套接字用socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以指定本地半相关。其调用格式如下:
int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen);
参数s是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。参数name 是赋给套接字s的本地地址(名字),其长度可变,结构随通信域的不同而不同。namelen表明了name的长度。
如果没有错误发生,bind()返回0。否则返回值SOCKET_ERROR。
地址在建立套接字通信过程中起着重要作用,作为一个网络应用程序设计者对套接字地址结构必须有明确认识。例如,UNIX BSD有一组描述套接字地址的数据结构,其中使用TCP/IP协议的地址结构为:
struct sockaddr_in{
short sin_family; /*AF_INET*/
u_short sin_port; /*16位端口号,网络字节顺序*/
struct in_addr sin_addr; /*32位IP地址,网络字节顺序*/
char sin_zero[8]; /*保留*/
}
有关bind()的详细描述参看5.2.2。
2.3.3 建立套接字连接──connect()与accept()
www.it55.com在线教程
这两个系统调用用于完成一个完整相关的建立,其中connect()用于建立连接。无连接的套接字进程也可以调用connect(),但这时在进程之间没有实际的报文交换,调用将从本地操作系统直接返回。这样做的优点是程序员不必为每一数据指定目的地址,而且如果收到的一个数据报,其目的端口未与任何套接字建立“连接”,便能判断该端口不可操作。而accept()用于使服务器等待来自某客户进程的实际连接。
connect()的调用格式如下:
int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
参数s是欲建立连接的本地套接字描述符。参数name指出说明对方套接字地址结构的指针。对方套接字地址长度由namelen说明。
如果没有错误发生,connect()返回0。否则返回值SOCKET_ERROR。在面向连接的协议中,该调用导致本地系统和外部系统之间连接实际建立。
由于地址族总被包含在套接字地址结构的前两个字节中,并通过socket()调用与某个协议族相关。因此bind()和connect()无须协议作为参数。
有关connect()的详细描述参看5.2.4。
accept()的调用格式如下:
SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
参数s为本地套接字描述符,在用做accept()调用的参数前应该先调用过listen()。addr 指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。addrlen 为客户方套接字地址的长度(字节数)。如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回值INVALID_SOCKET。
accept()用于面向连接服务器。参数addr和addrlen存放客户方的地址信息。调用前,参数addr 指向一个初始值为空的地址结构,而addrlen 的初始值为0;调用accept()后,服务器等待从编号为s的套接字上接受客户连接请求,而连接请求是由客户方的connect()调用发出的。当有连接请求到达时,accept()调用将请求连接队列上的第一个客户方套接字地址及长度放入addr 和addrlen,并创建一个与s有相同特性的新套接字号。新的套接字可用于处理服务器并发请求。
有关accept()的详细描述参看5.2.1。
四个套接字系统调用,socket()、bind()、connect()、accept(),可以完成一个完全五元相关的建立。socket()指定五元组中的协议元,它的用法与是否为客户或服务器、是否面向连接无关。bind()指定五元组中的本地二元,即本地主机地址和端口号,其用法与是否面向连接有关:在服务器方,无论是否面向连接,均要调用bind();在客户方,若采用面向连接,则可以不调用bind(),而通过connect()自动完成。若采用无连接,客户方必须使用bind()以获得一个唯一的地址。
以上讨论仅对客户/服务器模式而言,实际上套接字的使用是非常灵活的,唯一需遵循的原则是进程通信之前,必须建立完整的相关。
2.3.4 监听连接──listen()
IT资讯之家 www.it55.com
此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下:
int PASCAL FAR listen(SOCKET s, int backlog);
参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。
listen()在执行调用过程中可为没有调用过bind()的套接字s完成所必须的连接,并建立长度为backlog的请求连接队列。
调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。
有关listen()的详细描述参看5.2.13。
2.2.3节中提到在客户/服务器模式中,有两种类型的服务:重复服务和并发服务。accept()调用为实现并发服务提供了极大方便,因为它要返回一个新的套接字号,其典型结构为:
int initsockid, newsockid;
if ((initsockid = socket(....)) < 0)
error(“can't create socket”);
if (bind(initsockid,....) < 0)
error(“bind error”);
if (listen(initsockid , 5) < 0)
error(“listen error”);
for (;;) {
newsockid = accept(initsockid, ...) /* 阻塞 */
if (newsockid < 0)
error(“accept error“);
if (fork() == 0){ /* 子进程 */
closesocket(initsockid);
do(newsockid); /* 处理请求 */
exit(0);
}
closesocket(newsockid); /* 父进程 */
}
这段程序执行的结果是newsockid与客户的套接字建立相关,子进程启动后,关闭继承下来的主服务器的initsockid,并利用新的newsockid与客户通信。主服务器的initsockid可继续等待新的客户连接请求。由于在Unix等抢先多任务系统中,在系统调度下,多个进程可以同时进行。因此,使用并发服务器可以使服务器进程在同一时间可以有多个子进程和不同的客户程序连接、通信。在客户程序看来,服务器可以同时并发地处理多个客户的请求,这就是并发服务器名称的来由。
面向连接服务器也可以是重复服务器,其结构如下:
int initsockid, newsockid;
if ((initsockid = socket(....))<0)
error(“can't create socket”);
if (bind(initsockid,....)<0)
error(“bind error”);
if (listen(initsockid,5)<0)
error(“listen error”);
for (;;) {
newsockid = accept(initsockid, ...) /* 阻塞 */
if (newsockid < 0)
error(“accept error“);
do(newsockid); /* 处理请求 */
closesocket(newsockid);
}
重复服务器在一个时间只能和一个客户程序建立连接,它对多个客户程序的处理是采用循环的方式重复进行,因此叫重复服务器。并发服务器和重复服务器各有利弊:并发服务器可以改善客户程序的响应速度,但它增加了系统调度的开销;重复服务器正好与其相反,因此用户在决定是使用并发服务器还是重复服务器时,要根据应用的实际情况来定。
2.3.5 数据传输──send()与recv() http://www.it55.com/
当一个连接建立以后,就可以传输数据了。常用的系统调用有send()和recv()。
send()调用用于在参数s指定的已连接的数据报或流套接字上发送输出数据,格式如下:
int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);
参数s为已连接的本地套接字描述符。buf 指向存有发送数据的缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否发送带外数据等。如果没有错误发生,send()返回总共发送的字节数。否则它返回SOCKET_ERROR。
有关send()的详细描述参看5.2.19。
recv()调用用于在参数s指定的已连接的数据报或流套接字上接收输入数据,格式如下:
int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);
参数s 为已连接的套接字描述符。buf指向接收输入数据缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否接收带外数据等。如果没有错误发生,recv()返回总共接收的字节数。如果连接被关闭,返回0。否则它返回SOCKET_ERROR。
有关recv()的详细描述参看5.2.16。
2.3.6 输入/输出多路复用──select() 免费资源www.it55.com
select()调用用来检测一个或多个套接字的状态。对每一个套接字来说,这个调用可以请求读、写或错误状态方面的信息。请求给定状态的套接字集合由一个fd_set结构指示。在返回时,此结构被更新,以反映那些满足特定条件的套接字的子集,同时, select()调用返回满足条件的套接字的数目,其调用格式如下:
int PASCAL FAR select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);
参数nfds指明被检查的套接字描述符的值域,此变量一般被忽略。
参数readfds指向要做读检测的套接字描述符集合的指针,调用者希望从中读取数据。参数writefds 指向要做写检测的套接字描述符集合的指针。exceptfds指向要检测是否出错的套接字描述符集合的指针。timeout指向select()函数等待的最大时间,如果设为NULL则为阻塞操作。select()返回包含在fd_set结构中已准备好的套接字描述符的总数目,或者是发生错误则返回SOCKET_ERROR。
有关select()的详细描述参看5.2.18。
2.3.7 关闭套接字──closesocket()
closesocket()关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。closesocket()的调用格式如下:
BOOL PASCAL FAR closesocket(SOCKET s);
参数s待关闭的套接字描述符。如果没有错误发生,closesocket()返回0。否则返回值SOCKET_ERROR。
有关closesocket()的详细描述参看5.2.3。 45398 www.it55.com it55学习IT知识,享受IT生活 4dfkjn
2.4 典型套接字调用过程举例
如前所述,TCP/IP协议的应用一般采用客户/服务器模式,因此在实际应用中,必须有客户和服务器两个进程,并且首先启动服务器,其系统调用时序图如下。
it55.com
面向连接的协议(如TCP)的套接字系统调用如图2.1所示:
服务器必须首先启动,直到它执行完accept()调用,进入等待状态后,方能接收客户请求。假如客户在此前启动,则connect()将返回出错代码,连接不成功。
图2.1 面向连接的套接字系统调用时序图
无连接协议的套接字调用如图2.2所示:
图2.2 无连接协议的套接字调用时序图
无连接服务器也必须先启动,否则客户请求传不到服务进程。无连接客户不调用connect()。因此在数据发送之前,客户与服务器之间尚未建立完全相关,但各自通过socket()和bind()建立了半相关。发送数据时,发送方除指定本地套接字号外,还需指定接收方套接字号,从而在数据收发过程中动态地建立了全相关。
实例
本实例使用面向连接协议的客户/服务器模式,其流程如图2.3所示:
图2.3 面向连接的应用程序流程图
服务器方程序:
/* File Name: streams.c */
#include
#include
#define TRUE 1
/* 这个程序建立一个套接字,然后开始无限循环;每当它通过循环接收到一个连接,则打印出一个信息。当连接断开,或接收到终止信息,则此连接结束,程序再接收一个新的连接。命令行的格式是:streams */
main( )
{
int sock, length;
struct sockaddr_in server;
struct sockaddr tcpaddr;
int msgsock;
char buf[1024];
int rval, len;
/* 建立套接字 */
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror(“opening stream socket”);
exit(1);
}
/* 使用任意端口命名套接字 */
server.sin_family = AF_INET;
server.sin_port = INADDR_ANY;
if (bind(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror(“binding stream socket”);
exit(1);
}
/* 找出指定的端口号并打印出来 */
length = sizeof(server);
if (getsockname(sock, (struct sockaddr *)&server, &length) < 0) {
perror(“getting socket name”);
exit(1);
}
printf(“socket port #%d/n”, ntohs(server.sin_port));
/* 开始接收连接 */
listen(sock, 5);
len = sizeof(struct sockaddr);
do {
msgsock = accept(sock, (struct sockaddr *)&tcpaddr, (int *)&len);
if (msgsock == -1)
perror(“accept”);
else do{
memset(buf, 0, sizeof(buf));
if ((rval = recv(msgsock, buf, 1024)) < 0)
perror(“reading stream message”);
if (rval == 0)
printf(“ending connection /n”);
else
printf(“-->%s/n”, buf);
}while (rval != 0);
closesocket(msgsock);
} while (TRUE);
/* 因为这个程序已经有了一个无限循环,所以套接字“sock”从来不显式关闭。然而,当进程被杀死或正常终止时,所有套接字都将自动地被关闭。*/
exit(0);
}
客户方程序:
/* File Name: streamc.c */
#include
#include
#define DATA “half a league, half a league ...”
/* 这个程序建立套接字,然后与命令行给出的套接字连接;连接结束时,在连接上发送
一个消息,然后关闭套接字。命令行的格式是:streamc 主机名 端口号
端口号要与服务器程序的端口号相同 */
main(argc, argv)
int argc;
char *argv[ ];
{
int sock;
struct sockaddr_in server;
struct hostent *hp, *gethostbyname( );
char buf[1024];
/* 建立套接字 */
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror(“opening stream socket”);
exit(1);
}
/* 使用命令行中指定的名字连接套接字 */
server.sin_family = AF_INET;
hp = gethostbyname(argv[1]);
if (hp == 0) {
fprintf(stderr, “%s: unknown host /n”, argv[1]);
exit(2);
}
memcpy((char*)&server.sin_addr, (char*)hp->h_addr, hp->h_length);
sever.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) {
perror(“connecting stream socket”);
exit(3);
}
if (send(sock, DATA, sizeof(DATA)) < 0)
perror(“sending on stream socket”);
closesocket(sock);
exit(0);
} sflj www.it55.com kg^&fgd
2.5 一个通用的实例程序
在上一节中,我们介绍了一个简单的socket程序实例。从这个例子我们可以看出,使用socket编程几乎有一个模式,即所有的程序几乎毫无例外地按相同的顺序调用相同的函数。因此我们可以设想,设计一个中间层,它向上提供几个简单的函数,程序只要调用这几个函数就可以实现普通情况下的数据传输,程序设计者不必太多地关心socket程序设计的细节。
本节我们将介绍一个通用的网络程序接口,它向上层提供几个简单的函数,程序设计者只要使用这几个函数就可以完成绝大多数情况下的网络数据传输。这些函数将socket编程和上层隔离开来,它使用面向连接的流式套接字,采用非阻塞的工作机制,程序只要调用这些函数查询网络消息并作出相应的响应即可。这些函数包括:
l InitSocketsStruct:初始化socket结构,获取服务端口号。客户程序使用。
l InitPassiveSock:初始化socket结构,获取服务端口号,建立主套接字。服务器程序使用。
l CloseMainSock:关闭主套接字。服务器程序使用。
l CreateConnection:建立连接。客户程序使用。
l AcceptConnection:接收连接。服务器程序使用。
l CloseConnection:关闭连接。
l QuerySocketsMsg:查询套接字消息。
l SendPacket:发送数据。
l RecvPacket:接收数据。
2.5.1 头文件 www.it55.com在线教程
/* File Name: tcpsock.h */
/* 头文件包括socket程序经常用到的系统头文件(本例中给出的是SCO Unix下的头文件,其它版本的Unix的头文件可能略有不同),并定义了我们自己的两个数据结构及其实例变量,以及我们提供的函数说明。*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct SocketsMsg{ /* 套接字消息结构 */
int AcceptNum; /* 指示是否有外来连接等待接收 */
int ReadNum; /* 有外来数据等待读取的连接数 */
int ReadQueue[32]; /* 有外来数据等待读取的连接队列 */
int WriteNum; /* 可以发送数据的连接数 */
int WriteQueue[32]; /* 可以发送数据的连接队列 */
int ExceptNum; /* 有例外的连接数 */
int ExceptQueue[32]; /* 有例外的连接队列 */
} SocketsMsg;
typedef struct Sockets { /* 套接字结构 */
int DaemonSock; /* 主套接字 */
int SockNum; /* 数据套接字数目 */
int Sockets[64]; /* 数据套接字数组 */
fd_set readfds, writefds, exceptfds; /* 要被检测的可读、可写、例外的套接字集合 */
int Port; /* 端口号 */
} Sockets;
Sockets Mysock; /* 全局变量 */
SocketsMsg SockMsg;
int InitSocketsStruct(char * servicename) ;
int InitPassiveSock(char * servicename) ;
void CloseMainSock();
int CreateConnection(struct in_addr *sin_addr);
int AcceptConnection(struct in_addr *IPaddr);
int CloseConnection(int Sockno);
int QuerySocketsMsg();
int SendPacket(int Sockno, void *buf, int len);
int RecvPacket(int Sockno, void *buf, int size);
2.5.2 函数源文件
it55.com
/* File Name: tcpsock.c */
/* 本文件给出九个函数的源代码,其中部分地方给出中文注释 */
#include "tcpsock.h"
int InitSocketsStruct(char * servicename)
/* Initialize Sockets structure. If succeed then return 1, else return error code (<0) */
/* 此函数用于只需要主动套接字的客户程序,它用来获取服务信息。服务的定义
在/etc/services文件中 */
{
struct servent *servrec;
struct sockaddr_in serv_addr;
if ((servrec = getservbyname(servicename, "tcp")) == NULL) {
return(-1);
}
bzero((char *)&Mysock, sizeof(Sockets));
Mysock.Port = servrec->s_port; /* Service Port in Network Byte Order */
return(1);
}
int InitPassiveSock(char * servicename)
/* Initialize Passive Socket. If succeed then return 1, else return error code (<0) */
/* 此函数用于需要被动套接字的服务器程序,它除了获取服务信息外,还建立
一个被动套接字。*/
{
int mainsock, flag=1;
struct servent *servrec;
struct sockaddr_in serv_addr;
if ((servrec = getservbyname(servicename, "tcp")) == NULL) {
return(-1);
}
bzero((char *)&Mysock, sizeof(Sockets));
Mysock.Port = servrec->s_port; /* Service Port in Network Byte Order */
if((mainsock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
return(-2);
}
bzero((char *)&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* 任意网络接口 */
serv_addr.sin_port = servrec->s_port;
if (bind(mainsock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
close(mainsock);
return(-3);
}
if (listen(mainsock, 5) == -1) { /* 将主动套接字变为被动套接字,准备好接收连接 */
close(mainsock);
return(-4);
}
/* Set this socket as a Non-blocking socket. */
if (ioctl(mainsock, FIONBIO, &flag) == -1) {
close(mainsock);
return(-5);
}
Mysock.DaemonSock = mainsock;
FD_SET(mainsock, &Mysock.readfds); /* 申明对主套接字“可读”感兴趣 */
FD_SET(mainsock, &Mysock.exceptfds); /* 申明对主套接字上例外事件感兴趣 */
return(1);
}
void CloseMainSock()
/* 关闭主套接字,并清除对它上面事件的申明。在程序结束前关闭主套接字是一个好习惯 */
{
close(Mysock.DaemonSock);
FD_CLR(Mysock.DaemonSock, &Mysock.readfds);
FD_CLR(Mysock.DaemonSock, &Mysock.exceptfds);
}
int CreateConnection(struct in_addr *sin_addr)
/* Create a Connection to remote host which IP address is in sin_addr.
Param: sin_addr indicates the IP address in Network Byte Order.
if succeed return the socket number which indicates this connection,
else return error code (<0) */
{
struct sockaddr_in server; /* server address */
int tmpsock, flag=1, i;
if ((tmpsock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return(-1);
server.sin_family = AF_INET;
server.sin_port = Mysock.Port;
server.sin_addr.s_addr = sin_addr->s_addr;
/* Set this socket as a Non-blocking socket. */
if (ioctl(tmpsock, FIONBIO, &flag) == -1) {
close(tmpsock);
return(-2);
}
/* Connect to the server. */
if (connect(tmpsock, (struct sockaddr *)&server, sizeof(server)) < 0) {
if ((errno != EWOULDBLOCK) && (errno != EINPROGRESS)) {
/* 如果错误代码是EWOULDBLOCK和EINPROGRESS,则不用关闭套接字,因为系统将在之后继续为套接字建立连接,连接是否建立成功可用select()函数来检测套接字是否“可写”来确定。*/
close(tmpsock);
return(-3); /* Connect error. */
}
}
FD_SET(tmpsock, &Mysock.readfds);
FD_SET(tmpsock, &Mysock.writefds);
FD_SET(tmpsock, &Mysock.exceptfds);
i = 0;
while (Mysock.Sockets[i] != 0) i++; /* look for a blank sockets position */
if (i >= 64) {
close(tmpsock);
return(-4); /* too many connections */
}
Mysock.Sockets[i] = tmpsock;
Mysock.SockNum++;
return(i);
}
int AcceptConnection(struct in_addr *IPaddr)
/* Accept a connection. If succeed, return the data sockets number, else return -1. */
{
int newsock, len, flag=1, i;
struct sockaddr_in addr;
len = sizeof(addr);
bzero((char *)&addr, len);
if ((newsock = accept(Mysock.DaemonSock, &addr, &len)) == -1)
return(-1); /* Accept error. */
/* Set this socket as a Non-blocking socket. */
ioctl(newsock, FIONBIO, &flag);
FD_SET(newsock, &Mysock.readfds);
FD_SET(newsock, &Mysock.writefds);
FD_SET(newsock, &Mysock.exceptfds);
/* Return IP address in the Parameter. */
IPaddr->s_addr = addr.sin_addr.s_addr;
i = 0;
while (Mysock.Sockets[i] != 0) i++; /* look for a blank sockets position */
if (i >= 64) {
close(newsock);
return(-4); /* too many connections */
}
Mysock.Sockets[i] = newsock;
Mysock.SockNum++;
return(i);
}
int CloseConnection(int Sockno)
/* Close a connection indicated by Sockno. */
{
int retcode;
if ((Sockno >= 64) || (Sockno < 0) || (Mysock.Sockets[Sockno] == 0))
return(0);
retcode = close(Mysock.Sockets[Sockno]);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.readfds);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.writefds);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.exceptfds);
Mysock.Sockets[Sockno] = 0;
Mysock.SockNum--;
return(retcode);
}
int QuerySocketsMsg()
/* Query Sockets Message. If succeed return message number, else return -1.
The message information stored in struct SockMsg. */
{
fd_set rfds, wfds, efds;
int retcode, i;
struct timeval TimeOut;
rfds = Mysock.readfds;
wfds = Mysock.writefds;
efds = Mysock.exceptfds;
TimeOut.tv_sec = 0; /* 立即返回,不阻塞。*/
TimeOut.tv_usec = 0;
bzero((char *)&SockMsg, sizeof(SockMsg));
if ((retcode = select(64, &rfds, &wfds, &efds, &TimeOut)) == 0)
return(0);
if (FD_ISSET(Mysock.DaemonSock, &rfds))
SockMsg.AcceptNum = 1; /* some client call server. */
for (i=0; i<64; i++) /* Data in message */
{
if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &rfds)))
SockMsg.ReadQueue[SockMsg.ReadNum++] = i;
}
for (i=0; i<64; i++) /* Data out ready message */
{
if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &wfds)))
SockMsg.WriteQueue[SockMsg.WriteNum++] = i;
}
if (FD_ISSET(Mysock.DaemonSock, &efds))
SockMsg.AcceptNum = -1; /* server socket error. */
for (i=0; i<64; i++) /* Error message */
{
if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &efds)))
SockMsg.ExceptQueue[SockMsg.ExceptNum++] = i;
}
return(retcode);
}
int SendPacket(int Sockno, void *buf, int len)
/* Send a packet. If succeed return the number of send data, else return -1 */
{
int actlen;
if ((Sockno >= 64) || (Sockno < 0) || (Mysock.Sockets[Sockno] == 0))
return(0);
if ((actlen = send(Mysock.Sockets[Sockno], buf, len, 0)) < 0)
return(-1);
return(actlen);
}
int RecvPacket(int Sockno, void *buf, int size)
/* Receive a packet. If succeed return the number of receive data, else if the connection
is shutdown by peer then return 0, otherwise return 0-errno */
{
int actlen;
if ((Sockno >= 64) || (Sockno < 0) || (Mysock.Sockets[Sockno] == 0))
return(0);
if ((actlen = recv(Mysock.Sockets[Sockno], buf, size, 0)) < 0)
return(0-errno);
return(actlen); /* actlen是接收的数据长度,如果为零,指示连接被对方关闭。*/
}
2.5.3 简单服务器程序示例 www.it55.com
/* File Name: server.c */
/* 这是一个很简单的重复服务器程序,它初始化好被动套接字后,循环等待接收连接。如果接收到连接,它显示数据套接字序号和客户端的IP地址;如果数据套接字上有数据到来,它接收数据并显示该连接的数据套接字序号和接收到的字符串。*/
#include "tcpsock.h"
main(argc, argv)
int argc;
char **argv;
{
struct in_addr sin_addr;
int retcode, i;
char buf[32];
/* 对于服务器程序,它经常是处于无限循环状态,只有在用户主动kill该进程或系统关机时,它才结束。对于使用kill强行终止的服务器程序,由于主套接字没有关闭,资源没有主动释放,可能会给随后的服务器程序重新启动产生影响。因此,主动关闭主套接字是一个良好的变成习惯。下面的语句使程序在接收到SIGINT、SIGQUIT和SIGTERM等信号时先执行CloseMainSock()函数关闭主套接字,然后再结束程序。因此,在使用kill强行终止服务器进程时,应该先使用kill -2 PID给服务器程序一个消息使其关闭主套接字,然后在用kill -9 PID强行结束该进程。*/
(void) signal(SIGINT, CloseMainSock);
(void) signal(SIGQUIT, CloseMainSock);
(void) signal(SIGTERM, CloseMainSock);
if ((retcode = InitPassiveSock("TestService")) < 0) {
printf("InitPassiveSock: error code = %d/n", retcode);
exit(-1);
}
while (1) {
retcode = QuerySocketsMsg(); /* 查询网络消息 */
if (SockMsg.AcceptNum == 1) { /* 有外来连接等待接收?*/
retcode = AcceptConnection(&sin_addr);
printf("retcode = %d, IP = %s /n", retcode, inet_ntoa(sin_addr.s_addr));
}
else if (SockMsg.AcceptNum == -1) /* 主套接字错误?*/
printf("Daemon Sockets error./n");
for (i=0; i
if ((retcode = RecvPacket(SockMsg.ReadQueue[i], buf, 32)) > 0)
printf("sockno %d Recv string = %s /n", SockMsg.ReadQueue[i], buf);
else /* 返回数据长度为零,指示连接中断,关闭套接字。*/
CloseConnection(SockMsg.ReadQueue[i]);
}
} /* end while */
}
2.5.4 简单客户程序示例 45398 www.it55.com it55学习IT知识,享受IT生活 4dfkjn
/* File Name: client.c */
/* 客户程序在执行时,先初始化数据结构,然后等待用户输入命令。它识别四个命令:
conn(ect): 和服务器建立连接;
send: 给指定连接发送数据;
clos(e): 关闭指定连接;
quit: 退出客户程序。
*/
#include "tcpsock.h"
main(argc, argv)
int argc;
char **argv;
{
char cmd_buf[16];
struct in_addr sin_addr;
int sockno1, retcode;
char *buf = "This is a string for test.";
sin_addr.s_addr = inet_addr("166.111.5.249"); /* 运行服务器程序的主机的IP地址 */
if ((retcode = InitSocketsStruct("TestService")) < 0) { /* 初始化数据结构 */
printf("InitSocketsStruct: error code = %d/n", retcode);
exit(1);
}
while (1) {
printf(">");
gets(cmd_buf);
if (!strncmp(cmd_buf, "conn", 4)) {
retcode = CreateConnection(&sin_addr); /* 建立连接 */
printf("return code: %d/n", retcode);
}
else if(!strncmp(cmd_buf, "send", 4)) {
printf("Sockets Number:");
scanf("%d", &sockno1);
retcode = SendPacket(sockno1, buf, 26); /* 发送数据 */
printf("return code: %d/n", retcode, sizeof(buf));
}
else if (!strncmp(cmd_buf, "close", 4)) {
printf("Sockets Number:");
scanf("%d", &sockno1);
retcode = CloseConnection(sockno1); /* 关闭连接 */
printf("return code: %d/n", retcode);
}
else if (!strncmp(cmd_buf, "quit", 4))
exit(0);
else
putchar('/007');
} /* end while */
}
2.1 问题的引入
Unix系统的I/O命令集,是从Maltics和早期系统中的命令演变出来的,其模式为打开一读/写一关闭(open-write-read-close)。在一个用户进程进行I/O操作时,它首先调用“打开”获得对指定文件或设备的使用权,并返回称为文件描述符的整型数,以描述用户在打开的文件或设备上进行I/O操作的进程。然后这个用户进程多次调用“读/写”以传输数据。当所有的传输操作完成后,用户进程关闭调用,通知操作系统已经完成了对某对象的使用。
TCP/IP协议被集成到UNIX内核中时,相当于在UNIX系统引入了一种新型的I/O操作。UNIX用户进程与网络协议的交互作用比用户进程与传统的I/O设备相互作用复杂得多。首先,进行网络操作的两个进程在不同机器上,如何建立它们之间的联系?其次,网络协议存在多种,如何建立一种通用机制以支持多种协议?这些都是网络应用编程界面所要解决的问题。
在UNIX系统中,网络应用编程界面有两类:UNIX BSD的套接字(socket)和UNIX System V的TLI。由于Sun公司采用了支持TCP/IP的UNIX BSD操作系统,使TCP/IP的应用有更大的发展,其网络应用编程界面──套接字(socket)在网络软件中被广泛应用,至今已引进微机操作系统Dos和Windows系统中,成为开发网络应用软件的强有力工具,本章将要详细讨论这个问题。
2.2 套接字编程基本概念
在开始使用套接字编程之前,首先必须建立以下概念。
2.2.1 网间进程通信
进程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如UNIX BSD中的管道(pipe)、命名管道(named pipe)和软中断信号(signal),UNIX system V的消息(message)、共享存储区(shared memory)和信号量(semaphore)等,但都仅限于用在本机进程之间通信。网间进程通信要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例)。为此,首先要解决的是网间进程标识问题。同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋于某进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。
其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。
为了解决上述问题,TCP/IP协议引入了下列几个概念。
端口
网络中可以被命名和寻址的通信端口,是操作系统可分配的一种资源。
按照OSI七层协议的描述,传输层与网络层在功能上的最大区别是传输层提供进程通信能力。从这个意义上讲,网络通信的最终地址就不仅仅是主机地址了,还包括可以描述进程的某种标识符。为此,TCP/IP协议提出了协议端口(protocol port,简称端口)的概念,用于标识通信的进程。
端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序(即进程)通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应进程所接收,相应进程发给传输层的数据都通过该端口输出。在TCP/IP协议的实现中,端口操作类似于一般的I/O操作,进程获取一个端口,相当于获取本地唯一的I/O文件,可以用一般的读写原语访问之。
类似于文件描述符,每个端口都拥有一个叫端口号(port number)的整数型标识符,用于区别不同端口。由于TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立,如TCP有一个255号端口,UDP也可以有一个255号端口,二者并不冲突。
端口号的分配是一个重要问题。有两种基本分配方式:第一种叫全局分配,这是一种集中控制方式,由一个公认的中央机构根据用户需要进行统一分配,并将结果公布于众。第二种是本地分配,又称动态连接,即进程需要访问传输层服务时,向本地操作系统提出申请,操作系统返回一个本地唯一的端口号,进程再通过合适的系统调用将自己与该端口号联系起来(绑扎)。TCP/IP端口号的分配中综合了上述两种方式。TCP/IP将端口号分为两部分,少量的作为保留端口,以全局方式分配给服务进程。因此,每一个标准服务器都拥有一个全局公认的端口(即周知口,well-known port),即使在不同机器上,其端口号也相同。剩余的为自由端口,以本地方式进行分配。TCP和UDP均规定,小于256的端口号才能作保留端口。
地址
网络通信中通信的两个进程分别在不同的机器上。在互连网络中,两台机器可能位于不同的网络,这些网络通过网络互连设备(网关,网桥,路由器等)连接。因此需要三级寻址:
1. 某一主机可与多个网络相连,必须指定一特定网络地址;
2. 网络上每一台主机应有其唯一的地址;
3. 每一主机上的每一进程应有在该主机上的唯一标识符。
通常主机地址由网络ID和主机ID组成,在TCP/IP协议中用32位整数值表示;TCP和UDP均使用16位端口号标识用户进程。
网络字节顺序
不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低价先存),有的存高位字节(高价先存)。为保证数据的正确性,在网络协议中须指定网络字节顺序。TCP/IP协议使用16位整数和32位整数的高价先存格式,它们均含在协议头文件中。
连接
两个进程间的通信链路称为连接。连接在内部表现为一些缓冲区和一组协议机制,在外部表现出比无连接高的可靠性。
半相关
综上所述,网络中用一个三元组可以在全局唯一标志一个进程:
(协议,本地地址,本地端口号)
这样一个三元组,叫做一个半相关(half-association),它指定连接的每半部分。
全相关
一个完整的网间进程通信需要由两个进程组成,并且只能使用同一种高层协议。也就是说,不可能通信的一端用TCP协议,而另一端用UDP协议。因此一个完整的网间通信需要一个五元组来标识:
(协议,本地地址,本地端口号,远地地址,远地端口号)
这样一个五元组,叫做一个相关(association),即两个协议相同的半相关才能组合成一个合适的相关,或完全指定组成一连接。
2.2.2 服务方式 www.it55.com在线教程
在网络分层结构中,各层之间是严格单向依赖的,各层次的分工和协作集中体现在相邻层之间的界面上。“服务”是描述相邻层之间关系的抽象概念,即网络中各层向紧邻上层提供的一组操作。下层是服务提供者,上层是请求服务的用户。服务的表现形式是原语(primitive),如系统调用或库函数。系统调用是操作系统内核向网络应用程序或高层协议提供的服务原语。网络中的n层总要向n+1层提供比n-1层更完备的服务,否则n层就没有存在的价值。
在OSI的术语中,网络层及其以下各层又称为通信子网,只提供点到点通信,没有程序或进程的概念。而传输层实现的是“端到端”通信,引进网间进程通信概念,同时也要解决差错控制,流量控制,数据排序(报文排序),连接管理等问题,为此提供不同的服务方式:
面向连接(虚电路)或无连接
面向连接服务是电话系统服务模式的抽象,即每一次完整的数据传输都要经过建立连接,使用连接,终止连接的过程。在数据传输过程中,各数据分组不携带目的地址,而使用连接号(connect ID)。本质上,连接是一个管道,收发数据不但顺序一致,而且内容相同。TCP协议提供面向连接的虚电路。
无连接服务是邮政系统服务的抽象,每个分组都携带完整的目的地址,各分组在系统中独立传送。无连接服务不能保证分组的先后顺序,不进行分组出错的恢复与重传,不保证传输的可靠性。UDP协议提供无连接的数据报服务。
下面给出这两种服务的类型及应用中的例子:
服务类型 www.it55.com在线教程
服 务
例 子
面向连接
可靠的报文流
可靠的字节流
不可靠的连接
文件传输(FTP)
远程登录(Telnet)
数字话音
无连接
不可靠的数据报
有确认的数据报
请求-应答
电子邮件(E-mail)
电子邮件中的挂号信
网络数据库查询
顺序
在网络传输中,两个连续报文在端-端通信中可能经过不同路径,这样到达目的地时的顺序可能会与发送时不同。“顺序”是指接收数据顺序与发送数据顺序相同。TCP协议提供这项服务。
差错控制
保证应用程序接收的数据无差错的一种机制。检查差错的方法一般是采用检验“检查和(Checksum)”的方法。而保证传送无差错的方法是双方采用确认应答技术。TCP协议提供这项服务。
流控制
在数据传输过程中控制数据传输速率的一种机制,以保证数据不被丢失。TCP协议提供这项服务。
字节流
字节流方式指的是仅把传输中的报文看作是一个字节序列,不提供数据流的任何边界。TCP协议提供字节流服务。
报文
接收方要保存发送方的报文边界。UDP协议提供报文服务。
全双工/半双工
端-端间数据同时以两个方向/一个方向传送。
缓存/带外数据
在字节流服务中,由于没有报文边界,用户进程在某一时刻可以读或写任意数量的字节。为保证传输正确或采用有流控制的协议时,都要进行缓存。但对某些特殊的需求,如交互式应用程序,又会要求取消这种缓存。
在数据传送过程中,希望不通过常规传输方式传送给用户以便及时处理的某一类信息,如UNIX系统的中断键(Delete或Control-c)、终端流控制符(Control-s和Control-q),称为带外数据。逻辑上看,好象用户进程使用了一个独立的通道传输这些数据。该通道与每对连接的流相联系。由于Berkeley Software Distribution中对带外数据的实现与RFC 1122中规定的Host Agreement不一致,为了将互操作中的问题减到最小,应用程序编写者除非与现有服务互操作时要求带外数据外,最好不使用它。
2.2.3 客户/服务器模式
IT资讯之家 www.it55.com
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器模式(ClIEnt/Server model),即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。客户/服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于客户/服务器模式的TCP/IP。
客户/服务器模式在操作过程中采取的是主动请求方式:
首先服务器方要先启动,并根据请求提供相应服务:
1. 打开一通信通道并告知本地主机,它愿意在某一公认地址上(周知口,如FTP为21)接收客户请求;
2. 等待客户请求到达该端口;
3. 接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,要激活一新进程来处理这个客户请求(如UNIX系统中用fork、exec)。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。
4. 返回第二步,等待另一客户请求。
5. 关闭服务器
客户方:
1. 打开一通信通道,并连接到服务器所在主机的特定端口;
2. 向服务器发服务请求报文,等待并接收应答;继续提出请求......
3. 请求结束后关闭通信通道并终止。
从上面所描述过程可知:
1. 客户与服务器进程的作用是非对称的,因此编码不同。
2. 服务进程一般是先于客户请求而启动的。只要系统运行,该服务进程一直存在,直到正常或强迫终止。
2.2.4 套接字类型 it55.com
TCP/IP的socket提供下列三种类型套接字。
流式套接字(SOCK_STREAM)
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议(FTP)即使用流式套接字。
数据报式套接字(SOCK_DGRAM)
提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。
原始式套接字(SOCK_RAW)
该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
2.3 基本套接字系统调用
为了更好地说明套接字编程原理,下面给出几个基本套接字系统调用说明。
2.3.1 创建套接字──socket()
应用程序在使用套接字前,首先必须拥有一个套接字,系统调用socket()向应用程序提供创建套接字的手段,其调用格式如下:
SOCKET PASCAL FAR socket(int af, int type, int protocol);
该调用要接收三个参数:af、type、protocol。参数af指定通信发生的区域,UNIX系统支持的地址族有:AF_UNIX、AF_INET、AF_NS等,而DOS、WINDOWS中仅支持AF_INET,它是网际网区域。因此,地址族与协议族相同。参数type 描述要建立的套接字的类型。参数protocol说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此,socket()系统调用实际上指定了相关五元组中的“协议”这一元。
有关socket()的详细描述参看5.2.23。
2.3.2 指定本地地址──bind()
http://www.it55.com/
当一个套接字用socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以指定本地半相关。其调用格式如下:
int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen);
参数s是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。参数name 是赋给套接字s的本地地址(名字),其长度可变,结构随通信域的不同而不同。namelen表明了name的长度。
如果没有错误发生,bind()返回0。否则返回值SOCKET_ERROR。
地址在建立套接字通信过程中起着重要作用,作为一个网络应用程序设计者对套接字地址结构必须有明确认识。例如,UNIX BSD有一组描述套接字地址的数据结构,其中使用TCP/IP协议的地址结构为:
struct sockaddr_in{
short sin_family; /*AF_INET*/
u_short sin_port; /*16位端口号,网络字节顺序*/
struct in_addr sin_addr; /*32位IP地址,网络字节顺序*/
char sin_zero[8]; /*保留*/
}
有关bind()的详细描述参看5.2.2。
2.3.3 建立套接字连接──connect()与accept()
www.it55.com在线教程
这两个系统调用用于完成一个完整相关的建立,其中connect()用于建立连接。无连接的套接字进程也可以调用connect(),但这时在进程之间没有实际的报文交换,调用将从本地操作系统直接返回。这样做的优点是程序员不必为每一数据指定目的地址,而且如果收到的一个数据报,其目的端口未与任何套接字建立“连接”,便能判断该端口不可操作。而accept()用于使服务器等待来自某客户进程的实际连接。
connect()的调用格式如下:
int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
参数s是欲建立连接的本地套接字描述符。参数name指出说明对方套接字地址结构的指针。对方套接字地址长度由namelen说明。
如果没有错误发生,connect()返回0。否则返回值SOCKET_ERROR。在面向连接的协议中,该调用导致本地系统和外部系统之间连接实际建立。
由于地址族总被包含在套接字地址结构的前两个字节中,并通过socket()调用与某个协议族相关。因此bind()和connect()无须协议作为参数。
有关connect()的详细描述参看5.2.4。
accept()的调用格式如下:
SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
参数s为本地套接字描述符,在用做accept()调用的参数前应该先调用过listen()。addr 指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。addrlen 为客户方套接字地址的长度(字节数)。如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回值INVALID_SOCKET。
accept()用于面向连接服务器。参数addr和addrlen存放客户方的地址信息。调用前,参数addr 指向一个初始值为空的地址结构,而addrlen 的初始值为0;调用accept()后,服务器等待从编号为s的套接字上接受客户连接请求,而连接请求是由客户方的connect()调用发出的。当有连接请求到达时,accept()调用将请求连接队列上的第一个客户方套接字地址及长度放入addr 和addrlen,并创建一个与s有相同特性的新套接字号。新的套接字可用于处理服务器并发请求。
有关accept()的详细描述参看5.2.1。
四个套接字系统调用,socket()、bind()、connect()、accept(),可以完成一个完全五元相关的建立。socket()指定五元组中的协议元,它的用法与是否为客户或服务器、是否面向连接无关。bind()指定五元组中的本地二元,即本地主机地址和端口号,其用法与是否面向连接有关:在服务器方,无论是否面向连接,均要调用bind();在客户方,若采用面向连接,则可以不调用bind(),而通过connect()自动完成。若采用无连接,客户方必须使用bind()以获得一个唯一的地址。
以上讨论仅对客户/服务器模式而言,实际上套接字的使用是非常灵活的,唯一需遵循的原则是进程通信之前,必须建立完整的相关。
2.3.4 监听连接──listen()
IT资讯之家 www.it55.com
此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下:
int PASCAL FAR listen(SOCKET s, int backlog);
参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。
listen()在执行调用过程中可为没有调用过bind()的套接字s完成所必须的连接,并建立长度为backlog的请求连接队列。
调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。
有关listen()的详细描述参看5.2.13。
2.2.3节中提到在客户/服务器模式中,有两种类型的服务:重复服务和并发服务。accept()调用为实现并发服务提供了极大方便,因为它要返回一个新的套接字号,其典型结构为:
int initsockid, newsockid;
if ((initsockid = socket(....)) < 0)
error(“can't create socket”);
if (bind(initsockid,....) < 0)
error(“bind error”);
if (listen(initsockid , 5) < 0)
error(“listen error”);
for (;;) {
newsockid = accept(initsockid, ...) /* 阻塞 */
if (newsockid < 0)
error(“accept error“);
if (fork() == 0){ /* 子进程 */
closesocket(initsockid);
do(newsockid); /* 处理请求 */
exit(0);
}
closesocket(newsockid); /* 父进程 */
}
这段程序执行的结果是newsockid与客户的套接字建立相关,子进程启动后,关闭继承下来的主服务器的initsockid,并利用新的newsockid与客户通信。主服务器的initsockid可继续等待新的客户连接请求。由于在Unix等抢先多任务系统中,在系统调度下,多个进程可以同时进行。因此,使用并发服务器可以使服务器进程在同一时间可以有多个子进程和不同的客户程序连接、通信。在客户程序看来,服务器可以同时并发地处理多个客户的请求,这就是并发服务器名称的来由。
面向连接服务器也可以是重复服务器,其结构如下:
int initsockid, newsockid;
if ((initsockid = socket(....))<0)
error(“can't create socket”);
if (bind(initsockid,....)<0)
error(“bind error”);
if (listen(initsockid,5)<0)
error(“listen error”);
for (;;) {
newsockid = accept(initsockid, ...) /* 阻塞 */
if (newsockid < 0)
error(“accept error“);
do(newsockid); /* 处理请求 */
closesocket(newsockid);
}
重复服务器在一个时间只能和一个客户程序建立连接,它对多个客户程序的处理是采用循环的方式重复进行,因此叫重复服务器。并发服务器和重复服务器各有利弊:并发服务器可以改善客户程序的响应速度,但它增加了系统调度的开销;重复服务器正好与其相反,因此用户在决定是使用并发服务器还是重复服务器时,要根据应用的实际情况来定。
2.3.5 数据传输──send()与recv() http://www.it55.com/
当一个连接建立以后,就可以传输数据了。常用的系统调用有send()和recv()。
send()调用用于在参数s指定的已连接的数据报或流套接字上发送输出数据,格式如下:
int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);
参数s为已连接的本地套接字描述符。buf 指向存有发送数据的缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否发送带外数据等。如果没有错误发生,send()返回总共发送的字节数。否则它返回SOCKET_ERROR。
有关send()的详细描述参看5.2.19。
recv()调用用于在参数s指定的已连接的数据报或流套接字上接收输入数据,格式如下:
int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);
参数s 为已连接的套接字描述符。buf指向接收输入数据缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否接收带外数据等。如果没有错误发生,recv()返回总共接收的字节数。如果连接被关闭,返回0。否则它返回SOCKET_ERROR。
有关recv()的详细描述参看5.2.16。
2.3.6 输入/输出多路复用──select() 免费资源www.it55.com
select()调用用来检测一个或多个套接字的状态。对每一个套接字来说,这个调用可以请求读、写或错误状态方面的信息。请求给定状态的套接字集合由一个fd_set结构指示。在返回时,此结构被更新,以反映那些满足特定条件的套接字的子集,同时, select()调用返回满足条件的套接字的数目,其调用格式如下:
int PASCAL FAR select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);
参数nfds指明被检查的套接字描述符的值域,此变量一般被忽略。
参数readfds指向要做读检测的套接字描述符集合的指针,调用者希望从中读取数据。参数writefds 指向要做写检测的套接字描述符集合的指针。exceptfds指向要检测是否出错的套接字描述符集合的指针。timeout指向select()函数等待的最大时间,如果设为NULL则为阻塞操作。select()返回包含在fd_set结构中已准备好的套接字描述符的总数目,或者是发生错误则返回SOCKET_ERROR。
有关select()的详细描述参看5.2.18。
2.3.7 关闭套接字──closesocket()
closesocket()关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。closesocket()的调用格式如下:
BOOL PASCAL FAR closesocket(SOCKET s);
参数s待关闭的套接字描述符。如果没有错误发生,closesocket()返回0。否则返回值SOCKET_ERROR。
有关closesocket()的详细描述参看5.2.3。 45398 www.it55.com it55学习IT知识,享受IT生活 4dfkjn
2.4 典型套接字调用过程举例
如前所述,TCP/IP协议的应用一般采用客户/服务器模式,因此在实际应用中,必须有客户和服务器两个进程,并且首先启动服务器,其系统调用时序图如下。
it55.com
面向连接的协议(如TCP)的套接字系统调用如图2.1所示:
服务器必须首先启动,直到它执行完accept()调用,进入等待状态后,方能接收客户请求。假如客户在此前启动,则connect()将返回出错代码,连接不成功。
图2.1 面向连接的套接字系统调用时序图
无连接协议的套接字调用如图2.2所示:
图2.2 无连接协议的套接字调用时序图
无连接服务器也必须先启动,否则客户请求传不到服务进程。无连接客户不调用connect()。因此在数据发送之前,客户与服务器之间尚未建立完全相关,但各自通过socket()和bind()建立了半相关。发送数据时,发送方除指定本地套接字号外,还需指定接收方套接字号,从而在数据收发过程中动态地建立了全相关。
实例
本实例使用面向连接协议的客户/服务器模式,其流程如图2.3所示:
图2.3 面向连接的应用程序流程图
服务器方程序:
/* File Name: streams.c */
#include
#include
#define TRUE 1
/* 这个程序建立一个套接字,然后开始无限循环;每当它通过循环接收到一个连接,则打印出一个信息。当连接断开,或接收到终止信息,则此连接结束,程序再接收一个新的连接。命令行的格式是:streams */
main( )
{
int sock, length;
struct sockaddr_in server;
struct sockaddr tcpaddr;
int msgsock;
char buf[1024];
int rval, len;
/* 建立套接字 */
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror(“opening stream socket”);
exit(1);
}
/* 使用任意端口命名套接字 */
server.sin_family = AF_INET;
server.sin_port = INADDR_ANY;
if (bind(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror(“binding stream socket”);
exit(1);
}
/* 找出指定的端口号并打印出来 */
length = sizeof(server);
if (getsockname(sock, (struct sockaddr *)&server, &length) < 0) {
perror(“getting socket name”);
exit(1);
}
printf(“socket port #%d/n”, ntohs(server.sin_port));
/* 开始接收连接 */
listen(sock, 5);
len = sizeof(struct sockaddr);
do {
msgsock = accept(sock, (struct sockaddr *)&tcpaddr, (int *)&len);
if (msgsock == -1)
perror(“accept”);
else do{
memset(buf, 0, sizeof(buf));
if ((rval = recv(msgsock, buf, 1024)) < 0)
perror(“reading stream message”);
if (rval == 0)
printf(“ending connection /n”);
else
printf(“-->%s/n”, buf);
}while (rval != 0);
closesocket(msgsock);
} while (TRUE);
/* 因为这个程序已经有了一个无限循环,所以套接字“sock”从来不显式关闭。然而,当进程被杀死或正常终止时,所有套接字都将自动地被关闭。*/
exit(0);
}
客户方程序:
/* File Name: streamc.c */
#include
#include
#define DATA “half a league, half a league ...”
/* 这个程序建立套接字,然后与命令行给出的套接字连接;连接结束时,在连接上发送
一个消息,然后关闭套接字。命令行的格式是:streamc 主机名 端口号
端口号要与服务器程序的端口号相同 */
main(argc, argv)
int argc;
char *argv[ ];
{
int sock;
struct sockaddr_in server;
struct hostent *hp, *gethostbyname( );
char buf[1024];
/* 建立套接字 */
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror(“opening stream socket”);
exit(1);
}
/* 使用命令行中指定的名字连接套接字 */
server.sin_family = AF_INET;
hp = gethostbyname(argv[1]);
if (hp == 0) {
fprintf(stderr, “%s: unknown host /n”, argv[1]);
exit(2);
}
memcpy((char*)&server.sin_addr, (char*)hp->h_addr, hp->h_length);
sever.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) {
perror(“connecting stream socket”);
exit(3);
}
if (send(sock, DATA, sizeof(DATA)) < 0)
perror(“sending on stream socket”);
closesocket(sock);
exit(0);
} sflj www.it55.com kg^&fgd
2.5 一个通用的实例程序
在上一节中,我们介绍了一个简单的socket程序实例。从这个例子我们可以看出,使用socket编程几乎有一个模式,即所有的程序几乎毫无例外地按相同的顺序调用相同的函数。因此我们可以设想,设计一个中间层,它向上提供几个简单的函数,程序只要调用这几个函数就可以实现普通情况下的数据传输,程序设计者不必太多地关心socket程序设计的细节。
本节我们将介绍一个通用的网络程序接口,它向上层提供几个简单的函数,程序设计者只要使用这几个函数就可以完成绝大多数情况下的网络数据传输。这些函数将socket编程和上层隔离开来,它使用面向连接的流式套接字,采用非阻塞的工作机制,程序只要调用这些函数查询网络消息并作出相应的响应即可。这些函数包括:
l InitSocketsStruct:初始化socket结构,获取服务端口号。客户程序使用。
l InitPassiveSock:初始化socket结构,获取服务端口号,建立主套接字。服务器程序使用。
l CloseMainSock:关闭主套接字。服务器程序使用。
l CreateConnection:建立连接。客户程序使用。
l AcceptConnection:接收连接。服务器程序使用。
l CloseConnection:关闭连接。
l QuerySocketsMsg:查询套接字消息。
l SendPacket:发送数据。
l RecvPacket:接收数据。
2.5.1 头文件 www.it55.com在线教程
/* File Name: tcpsock.h */
/* 头文件包括socket程序经常用到的系统头文件(本例中给出的是SCO Unix下的头文件,其它版本的Unix的头文件可能略有不同),并定义了我们自己的两个数据结构及其实例变量,以及我们提供的函数说明。*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct SocketsMsg{ /* 套接字消息结构 */
int AcceptNum; /* 指示是否有外来连接等待接收 */
int ReadNum; /* 有外来数据等待读取的连接数 */
int ReadQueue[32]; /* 有外来数据等待读取的连接队列 */
int WriteNum; /* 可以发送数据的连接数 */
int WriteQueue[32]; /* 可以发送数据的连接队列 */
int ExceptNum; /* 有例外的连接数 */
int ExceptQueue[32]; /* 有例外的连接队列 */
} SocketsMsg;
typedef struct Sockets { /* 套接字结构 */
int DaemonSock; /* 主套接字 */
int SockNum; /* 数据套接字数目 */
int Sockets[64]; /* 数据套接字数组 */
fd_set readfds, writefds, exceptfds; /* 要被检测的可读、可写、例外的套接字集合 */
int Port; /* 端口号 */
} Sockets;
Sockets Mysock; /* 全局变量 */
SocketsMsg SockMsg;
int InitSocketsStruct(char * servicename) ;
int InitPassiveSock(char * servicename) ;
void CloseMainSock();
int CreateConnection(struct in_addr *sin_addr);
int AcceptConnection(struct in_addr *IPaddr);
int CloseConnection(int Sockno);
int QuerySocketsMsg();
int SendPacket(int Sockno, void *buf, int len);
int RecvPacket(int Sockno, void *buf, int size);
2.5.2 函数源文件
it55.com
/* File Name: tcpsock.c */
/* 本文件给出九个函数的源代码,其中部分地方给出中文注释 */
#include "tcpsock.h"
int InitSocketsStruct(char * servicename)
/* Initialize Sockets structure. If succeed then return 1, else return error code (<0) */
/* 此函数用于只需要主动套接字的客户程序,它用来获取服务信息。服务的定义
在/etc/services文件中 */
{
struct servent *servrec;
struct sockaddr_in serv_addr;
if ((servrec = getservbyname(servicename, "tcp")) == NULL) {
return(-1);
}
bzero((char *)&Mysock, sizeof(Sockets));
Mysock.Port = servrec->s_port; /* Service Port in Network Byte Order */
return(1);
}
int InitPassiveSock(char * servicename)
/* Initialize Passive Socket. If succeed then return 1, else return error code (<0) */
/* 此函数用于需要被动套接字的服务器程序,它除了获取服务信息外,还建立
一个被动套接字。*/
{
int mainsock, flag=1;
struct servent *servrec;
struct sockaddr_in serv_addr;
if ((servrec = getservbyname(servicename, "tcp")) == NULL) {
return(-1);
}
bzero((char *)&Mysock, sizeof(Sockets));
Mysock.Port = servrec->s_port; /* Service Port in Network Byte Order */
if((mainsock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
return(-2);
}
bzero((char *)&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* 任意网络接口 */
serv_addr.sin_port = servrec->s_port;
if (bind(mainsock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
close(mainsock);
return(-3);
}
if (listen(mainsock, 5) == -1) { /* 将主动套接字变为被动套接字,准备好接收连接 */
close(mainsock);
return(-4);
}
/* Set this socket as a Non-blocking socket. */
if (ioctl(mainsock, FIONBIO, &flag) == -1) {
close(mainsock);
return(-5);
}
Mysock.DaemonSock = mainsock;
FD_SET(mainsock, &Mysock.readfds); /* 申明对主套接字“可读”感兴趣 */
FD_SET(mainsock, &Mysock.exceptfds); /* 申明对主套接字上例外事件感兴趣 */
return(1);
}
void CloseMainSock()
/* 关闭主套接字,并清除对它上面事件的申明。在程序结束前关闭主套接字是一个好习惯 */
{
close(Mysock.DaemonSock);
FD_CLR(Mysock.DaemonSock, &Mysock.readfds);
FD_CLR(Mysock.DaemonSock, &Mysock.exceptfds);
}
int CreateConnection(struct in_addr *sin_addr)
/* Create a Connection to remote host which IP address is in sin_addr.
Param: sin_addr indicates the IP address in Network Byte Order.
if succeed return the socket number which indicates this connection,
else return error code (<0) */
{
struct sockaddr_in server; /* server address */
int tmpsock, flag=1, i;
if ((tmpsock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return(-1);
server.sin_family = AF_INET;
server.sin_port = Mysock.Port;
server.sin_addr.s_addr = sin_addr->s_addr;
/* Set this socket as a Non-blocking socket. */
if (ioctl(tmpsock, FIONBIO, &flag) == -1) {
close(tmpsock);
return(-2);
}
/* Connect to the server. */
if (connect(tmpsock, (struct sockaddr *)&server, sizeof(server)) < 0) {
if ((errno != EWOULDBLOCK) && (errno != EINPROGRESS)) {
/* 如果错误代码是EWOULDBLOCK和EINPROGRESS,则不用关闭套接字,因为系统将在之后继续为套接字建立连接,连接是否建立成功可用select()函数来检测套接字是否“可写”来确定。*/
close(tmpsock);
return(-3); /* Connect error. */
}
}
FD_SET(tmpsock, &Mysock.readfds);
FD_SET(tmpsock, &Mysock.writefds);
FD_SET(tmpsock, &Mysock.exceptfds);
i = 0;
while (Mysock.Sockets[i] != 0) i++; /* look for a blank sockets position */
if (i >= 64) {
close(tmpsock);
return(-4); /* too many connections */
}
Mysock.Sockets[i] = tmpsock;
Mysock.SockNum++;
return(i);
}
int AcceptConnection(struct in_addr *IPaddr)
/* Accept a connection. If succeed, return the data sockets number, else return -1. */
{
int newsock, len, flag=1, i;
struct sockaddr_in addr;
len = sizeof(addr);
bzero((char *)&addr, len);
if ((newsock = accept(Mysock.DaemonSock, &addr, &len)) == -1)
return(-1); /* Accept error. */
/* Set this socket as a Non-blocking socket. */
ioctl(newsock, FIONBIO, &flag);
FD_SET(newsock, &Mysock.readfds);
FD_SET(newsock, &Mysock.writefds);
FD_SET(newsock, &Mysock.exceptfds);
/* Return IP address in the Parameter. */
IPaddr->s_addr = addr.sin_addr.s_addr;
i = 0;
while (Mysock.Sockets[i] != 0) i++; /* look for a blank sockets position */
if (i >= 64) {
close(newsock);
return(-4); /* too many connections */
}
Mysock.Sockets[i] = newsock;
Mysock.SockNum++;
return(i);
}
int CloseConnection(int Sockno)
/* Close a connection indicated by Sockno. */
{
int retcode;
if ((Sockno >= 64) || (Sockno < 0) || (Mysock.Sockets[Sockno] == 0))
return(0);
retcode = close(Mysock.Sockets[Sockno]);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.readfds);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.writefds);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.exceptfds);
Mysock.Sockets[Sockno] = 0;
Mysock.SockNum--;
return(retcode);
}
int QuerySocketsMsg()
/* Query Sockets Message. If succeed return message number, else return -1.
The message information stored in struct SockMsg. */
{
fd_set rfds, wfds, efds;
int retcode, i;
struct timeval TimeOut;
rfds = Mysock.readfds;
wfds = Mysock.writefds;
efds = Mysock.exceptfds;
TimeOut.tv_sec = 0; /* 立即返回,不阻塞。*/
TimeOut.tv_usec = 0;
bzero((char *)&SockMsg, sizeof(SockMsg));
if ((retcode = select(64, &rfds, &wfds, &efds, &TimeOut)) == 0)
return(0);
if (FD_ISSET(Mysock.DaemonSock, &rfds))
SockMsg.AcceptNum = 1; /* some client call server. */
for (i=0; i<64; i++) /* Data in message */
{
if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &rfds)))
SockMsg.ReadQueue[SockMsg.ReadNum++] = i;
}
for (i=0; i<64; i++) /* Data out ready message */
{
if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &wfds)))
SockMsg.WriteQueue[SockMsg.WriteNum++] = i;
}
if (FD_ISSET(Mysock.DaemonSock, &efds))
SockMsg.AcceptNum = -1; /* server socket error. */
for (i=0; i<64; i++) /* Error message */
{
if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &efds)))
SockMsg.ExceptQueue[SockMsg.ExceptNum++] = i;
}
return(retcode);
}
int SendPacket(int Sockno, void *buf, int len)
/* Send a packet. If succeed return the number of send data, else return -1 */
{
int actlen;
if ((Sockno >= 64) || (Sockno < 0) || (Mysock.Sockets[Sockno] == 0))
return(0);
if ((actlen = send(Mysock.Sockets[Sockno], buf, len, 0)) < 0)
return(-1);
return(actlen);
}
int RecvPacket(int Sockno, void *buf, int size)
/* Receive a packet. If succeed return the number of receive data, else if the connection
is shutdown by peer then return 0, otherwise return 0-errno */
{
int actlen;
if ((Sockno >= 64) || (Sockno < 0) || (Mysock.Sockets[Sockno] == 0))
return(0);
if ((actlen = recv(Mysock.Sockets[Sockno], buf, size, 0)) < 0)
return(0-errno);
return(actlen); /* actlen是接收的数据长度,如果为零,指示连接被对方关闭。*/
}
2.5.3 简单服务器程序示例 www.it55.com
/* File Name: server.c */
/* 这是一个很简单的重复服务器程序,它初始化好被动套接字后,循环等待接收连接。如果接收到连接,它显示数据套接字序号和客户端的IP地址;如果数据套接字上有数据到来,它接收数据并显示该连接的数据套接字序号和接收到的字符串。*/
#include "tcpsock.h"
main(argc, argv)
int argc;
char **argv;
{
struct in_addr sin_addr;
int retcode, i;
char buf[32];
/* 对于服务器程序,它经常是处于无限循环状态,只有在用户主动kill该进程或系统关机时,它才结束。对于使用kill强行终止的服务器程序,由于主套接字没有关闭,资源没有主动释放,可能会给随后的服务器程序重新启动产生影响。因此,主动关闭主套接字是一个良好的变成习惯。下面的语句使程序在接收到SIGINT、SIGQUIT和SIGTERM等信号时先执行CloseMainSock()函数关闭主套接字,然后再结束程序。因此,在使用kill强行终止服务器进程时,应该先使用kill -2 PID给服务器程序一个消息使其关闭主套接字,然后在用kill -9 PID强行结束该进程。*/
(void) signal(SIGINT, CloseMainSock);
(void) signal(SIGQUIT, CloseMainSock);
(void) signal(SIGTERM, CloseMainSock);
if ((retcode = InitPassiveSock("TestService")) < 0) {
printf("InitPassiveSock: error code = %d/n", retcode);
exit(-1);
}
while (1) {
retcode = QuerySocketsMsg(); /* 查询网络消息 */
if (SockMsg.AcceptNum == 1) { /* 有外来连接等待接收?*/
retcode = AcceptConnection(&sin_addr);
printf("retcode = %d, IP = %s /n", retcode, inet_ntoa(sin_addr.s_addr));
}
else if (SockMsg.AcceptNum == -1) /* 主套接字错误?*/
printf("Daemon Sockets error./n");
for (i=0; i
if ((retcode = RecvPacket(SockMsg.ReadQueue[i], buf, 32)) > 0)
printf("sockno %d Recv string = %s /n", SockMsg.ReadQueue[i], buf);
else /* 返回数据长度为零,指示连接中断,关闭套接字。*/
CloseConnection(SockMsg.ReadQueue[i]);
}
} /* end while */
}
2.5.4 简单客户程序示例 45398 www.it55.com it55学习IT知识,享受IT生活 4dfkjn
/* File Name: client.c */
/* 客户程序在执行时,先初始化数据结构,然后等待用户输入命令。它识别四个命令:
conn(ect): 和服务器建立连接;
send: 给指定连接发送数据;
clos(e): 关闭指定连接;
quit: 退出客户程序。
*/
#include "tcpsock.h"
main(argc, argv)
int argc;
char **argv;
{
char cmd_buf[16];
struct in_addr sin_addr;
int sockno1, retcode;
char *buf = "This is a string for test.";
sin_addr.s_addr = inet_addr("166.111.5.249"); /* 运行服务器程序的主机的IP地址 */
if ((retcode = InitSocketsStruct("TestService")) < 0) { /* 初始化数据结构 */
printf("InitSocketsStruct: error code = %d/n", retcode);
exit(1);
}
while (1) {
printf(">");
gets(cmd_buf);
if (!strncmp(cmd_buf, "conn", 4)) {
retcode = CreateConnection(&sin_addr); /* 建立连接 */
printf("return code: %d/n", retcode);
}
else if(!strncmp(cmd_buf, "send", 4)) {
printf("Sockets Number:");
scanf("%d", &sockno1);
retcode = SendPacket(sockno1, buf, 26); /* 发送数据 */
printf("return code: %d/n", retcode, sizeof(buf));
}
else if (!strncmp(cmd_buf, "close", 4)) {
printf("Sockets Number:");
scanf("%d", &sockno1);
retcode = CloseConnection(sockno1); /* 关闭连接 */
printf("return code: %d/n", retcode);
}
else if (!strncmp(cmd_buf, "quit", 4))
exit(0);
else
putchar('/007');
} /* end while */
}
http://www.56.com/u64/v_MjExODgyNTM.html
多明显的机腹进气道!!!这模型做的也真是厉害。尾冲、失速尾旋、眼镜蛇。牛啊
多明显的机腹进气道!!!这模型做的也真是厉害。尾冲、失速尾旋、眼镜蛇。牛啊
没想到打我电话了,说我没有还钱,那就这周末吧,到中国银行还钱去。。。
帐号:4050007018880280382
质询电话:62139051
估计的交500,欠款3个月了。。。该交点了。。。
帐号:4050007018880280382
质询电话:62139051
估计的交500,欠款3个月了。。。该交点了。。。
人生成功的17条金律
1、保持积极的心态
人与人之间只有很小的差别,但这种很小的差别却往往造成巨大的差异,很小的差别就是所具备的心态是积极的还是消极的,巨大的差异就是成功与失败。也就是说,心态是命运的控制塔,心态决定我们人生的成败。我们生存的外部环境,也许不能选择,但另一个环境,即心理的、感情的、精神的内在环境,是可以由自己去改造的。成功的不一定都是企业家、领袖人物。成功,是指方方面面取得的成功,其标志在于人的心态,即积极、乐观地面对人生的各种挑战。一个人如果在一生中都不具有积极的心态就可能深陷泥淖,不能自觉,不能醒悟,不能自拔,当你发现身处困境时,机会已经失去。这种败局,不仅限于事业的失败,还包括人生中为人处事的失败,心理情绪的失败、婚恋家庭的失败、人的感受的失败等。总之,凡人生感受不如意,不幸福,都可视为你人生的失败,这些失败多半源于我们与生俱来的弱者的消极心态。如果我们能够调整心态,改变处事方法,就可以避免或扭转败局,甚至可以成为推动事业成功的伟人和把握幸福人生的智者。人成功不是指拥有什么(权力、财富),而是做了什么。如果能每天在一点一滴的努力中去实现自己的目标,就可以帮助和影响他人。成功等于每天进步一点点。积极的心态包括诚恳、忠诚、正直、乐观、勇敢、奋发、创造、机智、亲切、友善、积极、向善、向上、进取、努力、愉快、自信、自勉和有安全感等。
2、要有明确的目标
有了目标,内心的力量才会找到方向,漫无目标的努力或漂荡终归会迷路,而你心中的那座无价的金矿,也因得不到开采而与平凡的尘土无异。你过去和现在的情况并不重要,你将来想获得什么成就才是最重要的。有目标才会成功,如果你对未来没有理想,就做不出什么大事来。设定目标后订出中长期计划来,而且还要怀着迫切要求进步的愿望。成功是需要完全投入的,只有完全投入到你所从事的职业中去,才会有成功的一天;只有全身心地热爱你的生活,才会有成功的一天。
3、多走些路
做个主动的人。要勇于实践,你的成功也就是因为多走了些路,找到了别人未找到的另外一点东西。抓住机会,掌握机会,做个积极主动的人,并养成及时行动的好习惯。
4、正确的思考方法
成功等于正确的思想方法加信念加行动。要想成为思想方法正确的人,必须具备顽强坚定的性格,挖掘潜力,进行“我行”、“我是优秀的”、“还须再改进”的心理暗示。
5、高度的自制力
自制是一种最艰难的美德,有自制力才能抓住成功的机会。成功的最大敌人是自己,缺乏对自己情绪的控制,会把许多稍纵即逝的机会白白浪费掉。如愤怒时不能遏制怒火,使周围的合作者望而却步;消沉时,放纵自己的萎靡。
6、培养领导才能
衡量一个领导人物成就的大小,要看他信念的深度、雄心的高度、理想的广度和他对下属关爱的程度。一个人的领导能力唯有靠同事和下属的支持和合作才能成功。领导要练习赞美的艺术,对人要公正,管理要合乎人性。每一件事情都要精益求精,每一件事都要研究如何改善,每一件事都要订出更高的标准。认真工作并不断改进的人才会成为一个卓越的领导。
7、建立自信心
一个人能否做成、做好一件事,首先看他是否有一个好的心态,以及是否能认真、持续地坚持下去。信心大、心态好,办法才多。所以,信心多一分,成功多十分;投入才能收获,付出才能杰出。永远不要被缺点所迷惑。当然,成功卓越的人只有少数,失败平庸的人却很多。成功的人在遭受挫折和危机的时候,仍然是顽强、乐观和充满自信,而失败者往往是退却,甚至是甘于退却。我们应该学会自信,成功的程度取决于信念的程度。
8、迷人的个性
人生的美好在于人情的美好,人情的美好,在于人性的美好,人性的美,在于迷人的、能够吸引人的个性。对他人的生活、工作表示深切的关心;与人交往中求同存异,避免冲突;学会倾听别人的观点;学会夸奖别人;有微笑的魅力;别吝啬自己的同情;要学会认错,学会宽容大度。
9、创新致胜
创造力是最珍贵的财富。如果你有这种能力,就能把握事业成功的最佳时机,从而创造伟大的奇迹。创新思维比常规思维更具明显的优势特点:A、具有独创性;B、机动灵活;C、有风险意识。创新思维无论取得什么样的成果,都具有重要的认识论和方法论的意义,因为即便他的不成功结果,也会向人们提供了以后少走弯路的教训。常规性思维虽然看起来“稳妥”,但它的根本缺陷是不能为人们提供新的启示。创新必胜,保守必败。
10、充满热忱
你有信仰就年轻,绝望就年老。失去了热忱,就损伤了灵魂。热忱是一种最重要的力量,有史以来没有任何一件伟大的事业不是因为热忱而成功的。热忱要有高尚的信念,如果热忱出于贪婪和自私,成功也会昙花一现。唯有热忱的态度,才是成功推销自己的重要因素。热忱的心态,是做任何事情都必需的条件。热忱是一种积极意识和状态,能够鼓励和激励他人采取行动,而且还具有感染和鼓舞他人的力量
1、保持积极的心态
人与人之间只有很小的差别,但这种很小的差别却往往造成巨大的差异,很小的差别就是所具备的心态是积极的还是消极的,巨大的差异就是成功与失败。也就是说,心态是命运的控制塔,心态决定我们人生的成败。我们生存的外部环境,也许不能选择,但另一个环境,即心理的、感情的、精神的内在环境,是可以由自己去改造的。成功的不一定都是企业家、领袖人物。成功,是指方方面面取得的成功,其标志在于人的心态,即积极、乐观地面对人生的各种挑战。一个人如果在一生中都不具有积极的心态就可能深陷泥淖,不能自觉,不能醒悟,不能自拔,当你发现身处困境时,机会已经失去。这种败局,不仅限于事业的失败,还包括人生中为人处事的失败,心理情绪的失败、婚恋家庭的失败、人的感受的失败等。总之,凡人生感受不如意,不幸福,都可视为你人生的失败,这些失败多半源于我们与生俱来的弱者的消极心态。如果我们能够调整心态,改变处事方法,就可以避免或扭转败局,甚至可以成为推动事业成功的伟人和把握幸福人生的智者。人成功不是指拥有什么(权力、财富),而是做了什么。如果能每天在一点一滴的努力中去实现自己的目标,就可以帮助和影响他人。成功等于每天进步一点点。积极的心态包括诚恳、忠诚、正直、乐观、勇敢、奋发、创造、机智、亲切、友善、积极、向善、向上、进取、努力、愉快、自信、自勉和有安全感等。
2、要有明确的目标
有了目标,内心的力量才会找到方向,漫无目标的努力或漂荡终归会迷路,而你心中的那座无价的金矿,也因得不到开采而与平凡的尘土无异。你过去和现在的情况并不重要,你将来想获得什么成就才是最重要的。有目标才会成功,如果你对未来没有理想,就做不出什么大事来。设定目标后订出中长期计划来,而且还要怀着迫切要求进步的愿望。成功是需要完全投入的,只有完全投入到你所从事的职业中去,才会有成功的一天;只有全身心地热爱你的生活,才会有成功的一天。
3、多走些路
做个主动的人。要勇于实践,你的成功也就是因为多走了些路,找到了别人未找到的另外一点东西。抓住机会,掌握机会,做个积极主动的人,并养成及时行动的好习惯。
4、正确的思考方法
成功等于正确的思想方法加信念加行动。要想成为思想方法正确的人,必须具备顽强坚定的性格,挖掘潜力,进行“我行”、“我是优秀的”、“还须再改进”的心理暗示。
5、高度的自制力
自制是一种最艰难的美德,有自制力才能抓住成功的机会。成功的最大敌人是自己,缺乏对自己情绪的控制,会把许多稍纵即逝的机会白白浪费掉。如愤怒时不能遏制怒火,使周围的合作者望而却步;消沉时,放纵自己的萎靡。
6、培养领导才能
衡量一个领导人物成就的大小,要看他信念的深度、雄心的高度、理想的广度和他对下属关爱的程度。一个人的领导能力唯有靠同事和下属的支持和合作才能成功。领导要练习赞美的艺术,对人要公正,管理要合乎人性。每一件事情都要精益求精,每一件事都要研究如何改善,每一件事都要订出更高的标准。认真工作并不断改进的人才会成为一个卓越的领导。
7、建立自信心
一个人能否做成、做好一件事,首先看他是否有一个好的心态,以及是否能认真、持续地坚持下去。信心大、心态好,办法才多。所以,信心多一分,成功多十分;投入才能收获,付出才能杰出。永远不要被缺点所迷惑。当然,成功卓越的人只有少数,失败平庸的人却很多。成功的人在遭受挫折和危机的时候,仍然是顽强、乐观和充满自信,而失败者往往是退却,甚至是甘于退却。我们应该学会自信,成功的程度取决于信念的程度。
8、迷人的个性
人生的美好在于人情的美好,人情的美好,在于人性的美好,人性的美,在于迷人的、能够吸引人的个性。对他人的生活、工作表示深切的关心;与人交往中求同存异,避免冲突;学会倾听别人的观点;学会夸奖别人;有微笑的魅力;别吝啬自己的同情;要学会认错,学会宽容大度。
9、创新致胜
创造力是最珍贵的财富。如果你有这种能力,就能把握事业成功的最佳时机,从而创造伟大的奇迹。创新思维比常规思维更具明显的优势特点:A、具有独创性;B、机动灵活;C、有风险意识。创新思维无论取得什么样的成果,都具有重要的认识论和方法论的意义,因为即便他的不成功结果,也会向人们提供了以后少走弯路的教训。常规性思维虽然看起来“稳妥”,但它的根本缺陷是不能为人们提供新的启示。创新必胜,保守必败。
10、充满热忱
你有信仰就年轻,绝望就年老。失去了热忱,就损伤了灵魂。热忱是一种最重要的力量,有史以来没有任何一件伟大的事业不是因为热忱而成功的。热忱要有高尚的信念,如果热忱出于贪婪和自私,成功也会昙花一现。唯有热忱的态度,才是成功推销自己的重要因素。热忱的心态,是做任何事情都必需的条件。热忱是一种积极意识和状态,能够鼓励和激励他人采取行动,而且还具有感染和鼓舞他人的力量
其实int中的4代表4个字节,1个字节是8个二进制串,于是实际上int型可以表示的最大整数就是0111 1111 1111 1111 1111 1111 1111 1111共32位,最前面的一位是符号位,计算机中通常用0代表整数,1代表负数,于是这个数转换成10进制就是2^0 + 2^1 + 2^2 + .........2^31 = 2^32 - 1 = 2147483647于是,我又想当然的认为最小的负整数就一定是1111 1111 1111 1111 1111 1111 1111 1111我们知道,负数在计算机中是以补码的形式存储的,而负数的补码= 其绝对值的原码取反 + 1,我们可以反向来推算嘛先减1.
取浮点数的后2位的简单示例代码。
[codes=C]
int temperatur; // "return" of temperature in degrees Celsius * 100
int temperatur2;
float temp; // temperature in degrees Celsius
float temp2; // temperature in degrees Celsius
temp=temperatur/100.0;
temp2=temperatur2/100.0;
sprintf( tempStr, "%.2f", temp );
sprintf( temp2Str, "%.2f", temp2 );
[/codes]
sprintf.c
[codes=C]
#include <stdio.h>
int main(){
float fstr=21.0056;
char pszstr[16]={0};
sprintf( pszstr, "%.2f", fstr );
printf("pszstr=%s\n",pszstr);
}
[/codes]
在将各种类型的数据构造成字符串时,sprintf 的强大功能很少会让你失望。由于sprintf 跟printf 在用法上几乎一样,只是打印的目的地不同而已,前者打印到字符串中,后者则直接在命令行上输出。这也导致sprintf 比printf 有用得多。
sprintf 是个变参函数,定义如下:
int sprintf( char *buffer, const char *format [, argument] ... );
除了前两个参数类型固定外,后面可以接任意多个参数。而它的精华,显然就在第二个参数:
格式化字符串上。
printf 和sprintf 都使用格式化字符串来指定串的格式,在格式串内部使用一些以“%”开头的格式说明符(format specifications)来占据一个位置,在后边的变参列表中提供相应的变量,最终函数就会用相应位置的变量来替代那个说明符,产生一个调用者想要 的字符串。
格式化数字字符串
sprintf 最常见的应用之一莫过于把整数打印到字符串中,所以,spritnf 在大多数场合可以替代
itoa。
如:
//把整数123 打印成一个字符串保存在s 中。
sprintf(s, "%d", 123); //产生"123"
可以指定宽度,不足的左边补空格:
sprintf(s, "%8d%8d", 123, 4567); //产生:" 123 4567"
当然也可以左对齐:
sprintf(s, "%-8d%8d", 123, 4567); //产生:"123 4567"
也可以按照16 进制打印:
sprintf(s, "%8x", 4567); //小写16 进制,宽度占8 个位置,右对齐
sprintf(s, "%-8X", 4568); //大写16 进制,宽度占8 个位置,左对齐
这样,一个整数的16 进制字符串就很容易得到,但我们在打印16 进制内容时,通常想要一种左边补0 的等宽格式,那该怎么做呢?很简单,在表示宽度的数字前面加个0 就可以了。
sprintf(s, "%08X", 4567); //产生:"000011D7"
上面以”%d”进行的10 进制打印同样也可以使用这种左边补0 的方式。
这里要注意一个符号扩展的问题:比如,假如我们想打印短整数(short)-1 的内存16 进制表示形式,在Win32 平台上,一个short 型占2 个字节,所以我们自然希望用4 个16 进制数字来打印它:
short si = -1;
sprintf(s, "%04X", si);
产生“FFFFFFFF”,怎么回事?因为spritnf 是个变参函数,除了前面两个参数之外,后面的参数都不是类型安全的,函数更没有办法仅仅通过一个“%X”就能得知当初函数调用前参数压栈时被压进来的到底 是个4 字节的整数还是个2 字节的短整数,所以采取了统一4 字节的处理方式,导致参数压栈时做了符号扩展,扩展成了32 位的整数-1,打印时4 个位置不够了,就把32 位整数-1 的8 位16 进制都打印出来了。
如果你想看si 的本来面目,那么就应该让编译器做0 扩展而不是符号扩展(扩展时二进制左边补0 而不是补符号位):
sprintf(s, "%04X", (unsigned short)si);
就可以了。或者:
unsigned short si = -1;
sprintf(s, "%04X", si);
sprintf 和printf 还可以按8 进制打印整数字符串,使用”%o”。注意8 进制和16 进制都不会打
印出负数,都是无符号的,实际上也就是变量的内部编码的直接的16 进制或8 进制表示。
控制浮点数打印格式
浮点数的打印和格式控制是sprintf 的又一大常用功能,浮点数使用格式符”%f”控制,默认保
留小数点后6 位数字,比如:
sprintf(s, "%f", 3.1415926); //产生"3.141593"
但有时我们希望自己控制打印的宽度和小数位数,这时就应该使用:”%m.nf”格式,其中m 表
示打印的宽度,n 表示小数点后的位数。比如:
sprintf(s, "%10.3f", 3.1415626); //产生:" 3.142"
sprintf(s, "%-10.3f", 3.1415626); //产生:"3.142 "
sprintf(s, "%.3f", 3.1415626); //不指定总宽度,产生:"3.142"
注意一个问题,你猜
int i = 100;
sprintf(s, "%.2f", i);
会打出什么东东来?“100.00”?对吗?自己试试就知道了,同时也试试下面这个:
sprintf(s, "%.2f", (double)i);
第一个打出来的肯定不是正确结果,原因跟前面提到的一样,参数压栈时调用者并不知道跟i相对应的格式控制符是个”%f”。而函数执行时函数本身则并不知道 当年被压入栈里的是个整数,于是可怜的保存整数i 的那4 个字节就被不由分说地强行作为浮点数格式来解释了,整个乱套了。不过,如果有人有兴趣使用手工编码一个浮点数,那么倒可以使用这种方法来检验一下你手工编 排的结果是否正确。
字符/Ascii 码对照
我们知道,在C/C++语言中,char 也是一种普通的scalable 类型,除了字长之外,它与short,
int,long 这些类型没有本质区别,只不过被大家习惯用来表示字符和字符串而已。(或许当年该把
这个类型叫做“byte”,然后现在就可以根据实际情况,使用byte 或short 来把char 通过typedef 定义出来,这样更合适些)于是,使用”%d”或者”%x”打印一个字符,便能得出它的10 进制或16 进制的ASCII 码;反过来,使用”%c”打印一个整数,便可以看到它所对应的ASCII 字符。以下程序段把所有可见字符的ASCII 码对照表打印到屏幕上(这里采用printf,注意”#”与”%X”合用时自动为16 进制数增加”0X”前缀):
for(int i = 32; i < 127; i++) {
printf("[ %c ]: %3d 0x%#04X
", i, i, i);
}
连接字符串
sprintf 的格式控制串中既然可以插入各种东西,并最终把它们“连成一串”,自然也就能够连
接字符串,从而在许多场合可以替代strcat,但sprintf 能够一次连接多个字符串(自然也可以同时
在它们中间插入别的内容,总之非常灵活)。比如:
char* who = "I";
char* whom = "CSDN";
sprintf(s, "%s love %s.", who, whom); //产生:"I love CSDN. "
strcat 只能连接字符串(一段以’’结尾的字符数组或叫做字符缓冲,null-terminated-string),但有时我们有两段字符缓冲区,他们并不是以 ’’结尾。比如许多从第三方库函数中返回的字符数组,从硬件或者网络传输中读进来的字符流,它们未必每一段字符序列后面都有个相应的’’来结尾。如果直接 连接,不管是sprintf 还是strcat 肯定会导致非法内存操作,而strncat 也至少要求第一个参数是个null-terminated-string,那该怎么办呢?我们自然会想起前面介绍打印整数和浮点数时可以指定宽度,字符串 也一样的。比如:
char a1[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
char a2[] = {'H', 'I', 'J', 'K', 'L', 'M', 'N'};
如果:
sprintf(s, "%s%s", a1, a2); //Don't do that!
十有八九要出问题了。是否可以改成:
sprintf(s, "%7s%7s", a1, a2);
也没好到哪儿去,正确的应该是:
sprintf(s, "%.7s%.7s", a1, a2);//产生:"ABCDEFGHIJKLMN"
这可以类比打印浮点数的”%m.nf”,在”%m.ns”中,m 表示占用宽度(字符串长度不足时补空格,超出了则按照实际宽度打印),n 才表示从相应的字符串中最多取用的字符数。通常在打印字符串时m 没什么大用,还是点号后面的n 用的多。自然,也可以前后都只取部分字符:
sprintf(s, "%.6s%.5s", a1, a2);//产生:"ABCDEFHIJKL"
在许多时候,我们或许还希望这些格式控制符中用以指定长度信息的数字是动态的,而不是静态指定的,因为许多 时候,程序要到运行时才会清楚到底需要取字符数组中的几个字符,这种动态的宽度/精度设置功能在sprintf 的实现中也被考虑到了,sprintf 采用”*”来占用一个本来需要一个指定宽度或精度的常数数字的位置,同样,而实际的宽度或精度就可以和其它被打印的变量一样被提供出来,于是,上面的例子 可以变成:
sprintf(s, "%.*s%.*s", 7, a1, 7, a2);
或者:
sprintf(s, "%.*s%.*s", sizeof(a1), a1, sizeof(a2), a2);
实际上,前面介绍的打印字符、整数、浮点数等都可以动态指定那些常量值,比如:
sprintf(s, "%-*d", 4, 'A'); //产生"65 "
sprintf(s, "%#0*X", 8, 128); //产生"0X000080","#"产生0X
sprintf(s, "%*.*f", 10, 2, 3.1415926); //产生" 3.14"
打印地址信息
有时调试程序时,我们可能想查看某些变量或者成员的地址,由于地址或者指针也不过是个32 位的数,你完全可以使用打印无符号整数的”%u”把他们打印出来:
sprintf(s, "%u", &i);
不过通常人们还是喜欢使用16 进制而不是10 进制来显示一个地址:
sprintf(s, "%08X", &i);
然而,这些都是间接的方法,对于地址打印,sprintf 提供了专门的”%p”:
sprintf(s, "%p", &i);
我觉得它实际上就相当于:
sprintf(s, "%0*x", 2 * sizeof(void *), &i);
利用sprintf 的返回值
较少有人注意printf/sprintf 函数的返回值,但有时它却是有用的,spritnf 返回了本次函数调用
最终打印到字符缓冲区中的字符数目。也就是说每当一次sprinf 调用结束以后,你无须再调用一次
strlen 便已经知道了结果字符串的长度。如:
int len = sprintf(s, "%d", i);
对于正整数来说,len 便等于整数i 的10 进制位数。
下面的是个完整的例子,产生10 个[0, 100)之间的随机数,并将他们打印到一个字符数组s 中,
以逗号分隔开。
#include
#include
#include
int main() {
srand(time(0));
char s[64];
int offset = 0;
for(int i = 0; i < 10; i++) {
offset += sprintf(s + offset, "%d,", rand() % 100);
}
s[offset - 1] = '
';//将最后一个逗号换成换行符。
printf(s);
return 0;
}
设想当你从数据库中取出一条记录,然后希望把他们的各个字段按照某种规则连接成一个字
符串时,就可以使用这种方法,从理论上讲,他应该比不断的strcat 效率高,因为strcat 每次调用
都需要先找到最后的那个’’的位置,而在上面给出的例子中,我们每次都利用sprintf 返回值把这
个位置直接记下来了。
使用sprintf 的常见问题
sprintf 是个变参函数,使用时经常出问题,而且只要出问题通常就是能导致程序崩溃的内存访
问错误,但好在由sprintf 误用导致的问题虽然严重,却很容易找出,无非就是那么几种情况,通
常用眼睛再把出错的代码多看几眼就看出来了。
?? 缓冲区溢出
第一个参数的长度太短了,没的说,给个大点的地方吧。当然也可能是后面的参数的问
题,建议变参对应一定要细心,而打印字符串时,尽量使用”%.ns”的形式指定最大字符数。
?? 忘记了第一个参数
低级得不能再低级问题,用printf 用得太惯了。//偶就常犯。:。(
?? 变参对应出问题
通常是忘记了提供对应某个格式符的变参,导致以后的参数统统错位,检查检查吧。尤
其是对应”*”的那些参数,都提供了吗?不要把一个整数对应一个”%s”,编译器会觉得你
欺她太甚了(编译器是obj 和exe 的妈妈,应该是个女的,:P)。
strftime
sprnitf 还有个不错的表妹:strftime,专门用于格式化时间字符串的,用法跟她表哥很像,也
是一大堆格式控制符,只是毕竟小姑娘家心细,她还要调用者指定缓冲区的最大长度,可能是为
了在出现问题时可以推卸责任吧。这里举个例子:
time_t t = time(0);
//产生"YYYY-MM-DD hh:mm:ss"格式的字符串。
char s[32];
strftime(s, sizeof(s), "%Y-%m-%d %H:%M:%S", localtime(&t));
sprintf 在MFC 中也能找到他的知音:CString::Format,strftime 在MFC 中自然也有她的同道:
CTime::Format,这一对由于从面向对象哪里得到了赞助,用以写出的代码更觉优雅。
C语言标准库函数::strncmp()
函数原型
int strncmp(char *str1, char *str2, int maxlen);
函数功能 比较字符串s1和s2的前n个字符。 当s1<s2时,返回值<0 当s1=s2时,返回值=0 当s1>s2时,返回值>0声明文件 <string.h>
用法示例 #include <stdio.h> #include <string.h> int main(void) { char *buf1 = "aaabbb", *buf2 = "bbbccc", *buf3 = "ccc"; int ptr; ptr = strncmp(buf2,buf1,3); if (ptr > 0) printf("buffer 2 is greater than buffer 1\n"); else printf("buffer 2 is less than buffer 1\n"); ptr = strncmp(buf2,buf3,3); if (ptr > 0) printf("buffer 2 is greater than buffer 3\n"); else printf("buffer 2 is less than buffer 3\n"); return(0); }
[codes=C]
int temperatur; // "return" of temperature in degrees Celsius * 100
int temperatur2;
float temp; // temperature in degrees Celsius
float temp2; // temperature in degrees Celsius
temp=temperatur/100.0;
temp2=temperatur2/100.0;
sprintf( tempStr, "%.2f", temp );
sprintf( temp2Str, "%.2f", temp2 );
[/codes]
sprintf.c
[codes=C]
#include <stdio.h>
int main(){
float fstr=21.0056;
char pszstr[16]={0};
sprintf( pszstr, "%.2f", fstr );
printf("pszstr=%s\n",pszstr);
}
[/codes]
在将各种类型的数据构造成字符串时,sprintf 的强大功能很少会让你失望。由于sprintf 跟printf 在用法上几乎一样,只是打印的目的地不同而已,前者打印到字符串中,后者则直接在命令行上输出。这也导致sprintf 比printf 有用得多。
sprintf 是个变参函数,定义如下:
int sprintf( char *buffer, const char *format [, argument] ... );
除了前两个参数类型固定外,后面可以接任意多个参数。而它的精华,显然就在第二个参数:
格式化字符串上。
printf 和sprintf 都使用格式化字符串来指定串的格式,在格式串内部使用一些以“%”开头的格式说明符(format specifications)来占据一个位置,在后边的变参列表中提供相应的变量,最终函数就会用相应位置的变量来替代那个说明符,产生一个调用者想要 的字符串。
格式化数字字符串
sprintf 最常见的应用之一莫过于把整数打印到字符串中,所以,spritnf 在大多数场合可以替代
itoa。
如:
//把整数123 打印成一个字符串保存在s 中。
sprintf(s, "%d", 123); //产生"123"
可以指定宽度,不足的左边补空格:
sprintf(s, "%8d%8d", 123, 4567); //产生:" 123 4567"
当然也可以左对齐:
sprintf(s, "%-8d%8d", 123, 4567); //产生:"123 4567"
也可以按照16 进制打印:
sprintf(s, "%8x", 4567); //小写16 进制,宽度占8 个位置,右对齐
sprintf(s, "%-8X", 4568); //大写16 进制,宽度占8 个位置,左对齐
这样,一个整数的16 进制字符串就很容易得到,但我们在打印16 进制内容时,通常想要一种左边补0 的等宽格式,那该怎么做呢?很简单,在表示宽度的数字前面加个0 就可以了。
sprintf(s, "%08X", 4567); //产生:"000011D7"
上面以”%d”进行的10 进制打印同样也可以使用这种左边补0 的方式。
这里要注意一个符号扩展的问题:比如,假如我们想打印短整数(short)-1 的内存16 进制表示形式,在Win32 平台上,一个short 型占2 个字节,所以我们自然希望用4 个16 进制数字来打印它:
short si = -1;
sprintf(s, "%04X", si);
产生“FFFFFFFF”,怎么回事?因为spritnf 是个变参函数,除了前面两个参数之外,后面的参数都不是类型安全的,函数更没有办法仅仅通过一个“%X”就能得知当初函数调用前参数压栈时被压进来的到底 是个4 字节的整数还是个2 字节的短整数,所以采取了统一4 字节的处理方式,导致参数压栈时做了符号扩展,扩展成了32 位的整数-1,打印时4 个位置不够了,就把32 位整数-1 的8 位16 进制都打印出来了。
如果你想看si 的本来面目,那么就应该让编译器做0 扩展而不是符号扩展(扩展时二进制左边补0 而不是补符号位):
sprintf(s, "%04X", (unsigned short)si);
就可以了。或者:
unsigned short si = -1;
sprintf(s, "%04X", si);
sprintf 和printf 还可以按8 进制打印整数字符串,使用”%o”。注意8 进制和16 进制都不会打
印出负数,都是无符号的,实际上也就是变量的内部编码的直接的16 进制或8 进制表示。
控制浮点数打印格式
浮点数的打印和格式控制是sprintf 的又一大常用功能,浮点数使用格式符”%f”控制,默认保
留小数点后6 位数字,比如:
sprintf(s, "%f", 3.1415926); //产生"3.141593"
但有时我们希望自己控制打印的宽度和小数位数,这时就应该使用:”%m.nf”格式,其中m 表
示打印的宽度,n 表示小数点后的位数。比如:
sprintf(s, "%10.3f", 3.1415626); //产生:" 3.142"
sprintf(s, "%-10.3f", 3.1415626); //产生:"3.142 "
sprintf(s, "%.3f", 3.1415626); //不指定总宽度,产生:"3.142"
注意一个问题,你猜
int i = 100;
sprintf(s, "%.2f", i);
会打出什么东东来?“100.00”?对吗?自己试试就知道了,同时也试试下面这个:
sprintf(s, "%.2f", (double)i);
第一个打出来的肯定不是正确结果,原因跟前面提到的一样,参数压栈时调用者并不知道跟i相对应的格式控制符是个”%f”。而函数执行时函数本身则并不知道 当年被压入栈里的是个整数,于是可怜的保存整数i 的那4 个字节就被不由分说地强行作为浮点数格式来解释了,整个乱套了。不过,如果有人有兴趣使用手工编码一个浮点数,那么倒可以使用这种方法来检验一下你手工编 排的结果是否正确。
字符/Ascii 码对照
我们知道,在C/C++语言中,char 也是一种普通的scalable 类型,除了字长之外,它与short,
int,long 这些类型没有本质区别,只不过被大家习惯用来表示字符和字符串而已。(或许当年该把
这个类型叫做“byte”,然后现在就可以根据实际情况,使用byte 或short 来把char 通过typedef 定义出来,这样更合适些)于是,使用”%d”或者”%x”打印一个字符,便能得出它的10 进制或16 进制的ASCII 码;反过来,使用”%c”打印一个整数,便可以看到它所对应的ASCII 字符。以下程序段把所有可见字符的ASCII 码对照表打印到屏幕上(这里采用printf,注意”#”与”%X”合用时自动为16 进制数增加”0X”前缀):
for(int i = 32; i < 127; i++) {
printf("[ %c ]: %3d 0x%#04X
", i, i, i);
}
连接字符串
sprintf 的格式控制串中既然可以插入各种东西,并最终把它们“连成一串”,自然也就能够连
接字符串,从而在许多场合可以替代strcat,但sprintf 能够一次连接多个字符串(自然也可以同时
在它们中间插入别的内容,总之非常灵活)。比如:
char* who = "I";
char* whom = "CSDN";
sprintf(s, "%s love %s.", who, whom); //产生:"I love CSDN. "
strcat 只能连接字符串(一段以’’结尾的字符数组或叫做字符缓冲,null-terminated-string),但有时我们有两段字符缓冲区,他们并不是以 ’’结尾。比如许多从第三方库函数中返回的字符数组,从硬件或者网络传输中读进来的字符流,它们未必每一段字符序列后面都有个相应的’’来结尾。如果直接 连接,不管是sprintf 还是strcat 肯定会导致非法内存操作,而strncat 也至少要求第一个参数是个null-terminated-string,那该怎么办呢?我们自然会想起前面介绍打印整数和浮点数时可以指定宽度,字符串 也一样的。比如:
char a1[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
char a2[] = {'H', 'I', 'J', 'K', 'L', 'M', 'N'};
如果:
sprintf(s, "%s%s", a1, a2); //Don't do that!
十有八九要出问题了。是否可以改成:
sprintf(s, "%7s%7s", a1, a2);
也没好到哪儿去,正确的应该是:
sprintf(s, "%.7s%.7s", a1, a2);//产生:"ABCDEFGHIJKLMN"
这可以类比打印浮点数的”%m.nf”,在”%m.ns”中,m 表示占用宽度(字符串长度不足时补空格,超出了则按照实际宽度打印),n 才表示从相应的字符串中最多取用的字符数。通常在打印字符串时m 没什么大用,还是点号后面的n 用的多。自然,也可以前后都只取部分字符:
sprintf(s, "%.6s%.5s", a1, a2);//产生:"ABCDEFHIJKL"
在许多时候,我们或许还希望这些格式控制符中用以指定长度信息的数字是动态的,而不是静态指定的,因为许多 时候,程序要到运行时才会清楚到底需要取字符数组中的几个字符,这种动态的宽度/精度设置功能在sprintf 的实现中也被考虑到了,sprintf 采用”*”来占用一个本来需要一个指定宽度或精度的常数数字的位置,同样,而实际的宽度或精度就可以和其它被打印的变量一样被提供出来,于是,上面的例子 可以变成:
sprintf(s, "%.*s%.*s", 7, a1, 7, a2);
或者:
sprintf(s, "%.*s%.*s", sizeof(a1), a1, sizeof(a2), a2);
实际上,前面介绍的打印字符、整数、浮点数等都可以动态指定那些常量值,比如:
sprintf(s, "%-*d", 4, 'A'); //产生"65 "
sprintf(s, "%#0*X", 8, 128); //产生"0X000080","#"产生0X
sprintf(s, "%*.*f", 10, 2, 3.1415926); //产生" 3.14"
打印地址信息
有时调试程序时,我们可能想查看某些变量或者成员的地址,由于地址或者指针也不过是个32 位的数,你完全可以使用打印无符号整数的”%u”把他们打印出来:
sprintf(s, "%u", &i);
不过通常人们还是喜欢使用16 进制而不是10 进制来显示一个地址:
sprintf(s, "%08X", &i);
然而,这些都是间接的方法,对于地址打印,sprintf 提供了专门的”%p”:
sprintf(s, "%p", &i);
我觉得它实际上就相当于:
sprintf(s, "%0*x", 2 * sizeof(void *), &i);
利用sprintf 的返回值
较少有人注意printf/sprintf 函数的返回值,但有时它却是有用的,spritnf 返回了本次函数调用
最终打印到字符缓冲区中的字符数目。也就是说每当一次sprinf 调用结束以后,你无须再调用一次
strlen 便已经知道了结果字符串的长度。如:
int len = sprintf(s, "%d", i);
对于正整数来说,len 便等于整数i 的10 进制位数。
下面的是个完整的例子,产生10 个[0, 100)之间的随机数,并将他们打印到一个字符数组s 中,
以逗号分隔开。
#include
#include
#include
int main() {
srand(time(0));
char s[64];
int offset = 0;
for(int i = 0; i < 10; i++) {
offset += sprintf(s + offset, "%d,", rand() % 100);
}
s[offset - 1] = '
';//将最后一个逗号换成换行符。
printf(s);
return 0;
}
设想当你从数据库中取出一条记录,然后希望把他们的各个字段按照某种规则连接成一个字
符串时,就可以使用这种方法,从理论上讲,他应该比不断的strcat 效率高,因为strcat 每次调用
都需要先找到最后的那个’’的位置,而在上面给出的例子中,我们每次都利用sprintf 返回值把这
个位置直接记下来了。
使用sprintf 的常见问题
sprintf 是个变参函数,使用时经常出问题,而且只要出问题通常就是能导致程序崩溃的内存访
问错误,但好在由sprintf 误用导致的问题虽然严重,却很容易找出,无非就是那么几种情况,通
常用眼睛再把出错的代码多看几眼就看出来了。
?? 缓冲区溢出
第一个参数的长度太短了,没的说,给个大点的地方吧。当然也可能是后面的参数的问
题,建议变参对应一定要细心,而打印字符串时,尽量使用”%.ns”的形式指定最大字符数。
?? 忘记了第一个参数
低级得不能再低级问题,用printf 用得太惯了。//偶就常犯。:。(
?? 变参对应出问题
通常是忘记了提供对应某个格式符的变参,导致以后的参数统统错位,检查检查吧。尤
其是对应”*”的那些参数,都提供了吗?不要把一个整数对应一个”%s”,编译器会觉得你
欺她太甚了(编译器是obj 和exe 的妈妈,应该是个女的,:P)。
strftime
sprnitf 还有个不错的表妹:strftime,专门用于格式化时间字符串的,用法跟她表哥很像,也
是一大堆格式控制符,只是毕竟小姑娘家心细,她还要调用者指定缓冲区的最大长度,可能是为
了在出现问题时可以推卸责任吧。这里举个例子:
time_t t = time(0);
//产生"YYYY-MM-DD hh:mm:ss"格式的字符串。
char s[32];
strftime(s, sizeof(s), "%Y-%m-%d %H:%M:%S", localtime(&t));
sprintf 在MFC 中也能找到他的知音:CString::Format,strftime 在MFC 中自然也有她的同道:
CTime::Format,这一对由于从面向对象哪里得到了赞助,用以写出的代码更觉优雅。
C语言标准库函数::strncmp()
函数原型
int strncmp(char *str1, char *str2, int maxlen);
函数功能 比较字符串s1和s2的前n个字符。 当s1<s2时,返回值<0 当s1=s2时,返回值=0 当s1>s2时,返回值>0声明文件 <string.h>
用法示例 #include <stdio.h> #include <string.h> int main(void) { char *buf1 = "aaabbb", *buf2 = "bbbccc", *buf3 = "ccc"; int ptr; ptr = strncmp(buf2,buf1,3); if (ptr > 0) printf("buffer 2 is greater than buffer 1\n"); else printf("buffer 2 is less than buffer 1\n"); ptr = strncmp(buf2,buf3,3); if (ptr > 0) printf("buffer 2 is greater than buffer 3\n"); else printf("buffer 2 is less than buffer 3\n"); return(0); }
京城独酌,草木盎然,对影三人,月圆中天,嫦娥轻舞,吾影零乱,举杯遥祝,诸位平安,醒同交欢,醉各分散,六年同职,去留徘徊。金沙滩之大煤场,柳村二场之港口,路距七百,心距漫漫无限,车距七十二时辰,魔鬼般的旅程,炼狱般的煎熬,祈布达拉宫之神灵,借珠穆朗玛之灵气,饮青藏高原之圣水,采贡嘎雪山仙景之灵光,执问四方诸神,可否赐我洗礼后的重生?激情依旧?一切重来!
身累心痛,碌碌无为,今生有悔!
不在沉默中死亡,就在改变中重生!
身累心痛,碌碌无为,今生有悔!
不在沉默中死亡,就在改变中重生!
1. 能做到或不能做到,其实只在一念之间。相信你自己,永远不要让你的猜疑阻碍了你追求梦想的决心。
2. 你认为你行,你就行
你认为你行,你就行!
敢想,敢做!我很勇敢!
每天早上第一件事就是反复对自己说这句话!
相信自己!不要等待别人为你做你应该做的事。不断鼓励你自己,--你认为你行,你就行!
唤醒你潜藏的力量
1. 潜藏在内心的力量令你无所不能
如果你相信上帝是一个慈爱的父亲,上帝愿将他最好的东西给予他的孩子。只要你真的相信自己拥有上帝赐予的神奇的力量,你将会无所不能。
一个人能否成功的决定因素,不在于他拥有多少有利的条件,而在于他对自己如何评价和期待。他怎样评价自己,决定了别人怎样评价他。你如果不相信上帝已经给予你一切,就等于对宇宙的智慧和伟大的能力关闭了大门。
每个人都拥有许多天赋、潜在的能力和成功的力量。这是你最大的资产,你必须使自己成为一名积极的思想者,把个性的力量转化为有效的行动,赋予生命新的意义。
这个世界上最伟大的力量就藏在你的心中!现在你已经知道了!利用这个万能的工具,你可以做成任何事情。毫无疑问,你的每一个梦想都将实现。你期望的都会如你所愿,任你取回:爱情、成功、财富、健康、幸福。“我早已拥有它们了”--只要你这样认为,并且发自内心地相信,你就可以获得自己期望拥有的一切。你唯一需要做的,就是将你真正的个性显露出来。一个令人惊奇的新生活正在等待你的拥抱,你会从此获得新生。因为生活就是一种态度,也可以说是你自己想法的产品,所以不要限制你的想法。思考的力量可以造就一切。你认为你行,你就行!
2. 要相信你自己!对自己的能力拥有绝对的信心!如果你对自己的力量,拥有健全的自信心,成功将是你的囊中之物。如果你有难以克服的自卑感,不相信自己的能力,就会阻碍你获取成功;只有坚定的自信会带你走向自我实现的道路。--态度决定一切!相信你自己,释放出你不可思议的内在力量。
没有什么事可以决定你的一生。能够成功当然好,但如果没有成功,明天仍然是光明的一天。
凭借神所赐的力量,我能够应对一切!
如果你们有信心,没有任何事情是你们不能做的。
只要上帝在我们这一边,谁又能与我们为敌?
我能改变,能变成另外一个人。
建立信心的10条规则:
1. 构思你自己成功的心像,牢牢印在脑海中。不屈不挠地固守这幅心像,不容它褪色。无论情况显得多糟,请随时想像成功的画面。
2. 消极的想法浮上心头,请马上采用一个积极的想法来与之对抗。
3. 有意忽视每一个所谓的障碍,把阻力缩小。研究困难,做有效的处理,消除它,千万不要因为恐惧而把问题看得太严重。
4. 不要过度敬畏别人,培养一种“自以为是”的心态。没有人能比你更好地扮演你的角色。请记住:大多数人虽然外表看来很自信,其实往往跟你一样害怕,一样不信任自己。
5. 每天念10遍下面的语句:“如果上帝帮助我们,谁能阻挡我们呢?”
6. 找一个专家帮你找出自卑的主因。由童年研究起,认清自己对你大有帮助。
7. 如果遇到困难遭到挫败,要拿出一张纸列出所有对自己有利的因素。这些因素不但可以让你变得积极,而且更能使自己冷静、客观地面对问题。
8. 确实评估自己的能力,然后再将它提高10%。别太自负,但要有足够的自尊。
9. 想像你的能力无限之大。时刻不要忘记接受积极的思想,不给空虚、沮丧、疲倦留有侵袭的时间。
10.提醒自己别和你的恐惧商量如何去做,而是采取主动积极的态度去分析问题、解决问题
2. 你认为你行,你就行
你认为你行,你就行!
敢想,敢做!我很勇敢!
每天早上第一件事就是反复对自己说这句话!
相信自己!不要等待别人为你做你应该做的事。不断鼓励你自己,--你认为你行,你就行!
唤醒你潜藏的力量
1. 潜藏在内心的力量令你无所不能
如果你相信上帝是一个慈爱的父亲,上帝愿将他最好的东西给予他的孩子。只要你真的相信自己拥有上帝赐予的神奇的力量,你将会无所不能。
一个人能否成功的决定因素,不在于他拥有多少有利的条件,而在于他对自己如何评价和期待。他怎样评价自己,决定了别人怎样评价他。你如果不相信上帝已经给予你一切,就等于对宇宙的智慧和伟大的能力关闭了大门。
每个人都拥有许多天赋、潜在的能力和成功的力量。这是你最大的资产,你必须使自己成为一名积极的思想者,把个性的力量转化为有效的行动,赋予生命新的意义。
这个世界上最伟大的力量就藏在你的心中!现在你已经知道了!利用这个万能的工具,你可以做成任何事情。毫无疑问,你的每一个梦想都将实现。你期望的都会如你所愿,任你取回:爱情、成功、财富、健康、幸福。“我早已拥有它们了”--只要你这样认为,并且发自内心地相信,你就可以获得自己期望拥有的一切。你唯一需要做的,就是将你真正的个性显露出来。一个令人惊奇的新生活正在等待你的拥抱,你会从此获得新生。因为生活就是一种态度,也可以说是你自己想法的产品,所以不要限制你的想法。思考的力量可以造就一切。你认为你行,你就行!
2. 要相信你自己!对自己的能力拥有绝对的信心!如果你对自己的力量,拥有健全的自信心,成功将是你的囊中之物。如果你有难以克服的自卑感,不相信自己的能力,就会阻碍你获取成功;只有坚定的自信会带你走向自我实现的道路。--态度决定一切!相信你自己,释放出你不可思议的内在力量。
没有什么事可以决定你的一生。能够成功当然好,但如果没有成功,明天仍然是光明的一天。
凭借神所赐的力量,我能够应对一切!
如果你们有信心,没有任何事情是你们不能做的。
只要上帝在我们这一边,谁又能与我们为敌?
我能改变,能变成另外一个人。
建立信心的10条规则:
1. 构思你自己成功的心像,牢牢印在脑海中。不屈不挠地固守这幅心像,不容它褪色。无论情况显得多糟,请随时想像成功的画面。
2. 消极的想法浮上心头,请马上采用一个积极的想法来与之对抗。
3. 有意忽视每一个所谓的障碍,把阻力缩小。研究困难,做有效的处理,消除它,千万不要因为恐惧而把问题看得太严重。
4. 不要过度敬畏别人,培养一种“自以为是”的心态。没有人能比你更好地扮演你的角色。请记住:大多数人虽然外表看来很自信,其实往往跟你一样害怕,一样不信任自己。
5. 每天念10遍下面的语句:“如果上帝帮助我们,谁能阻挡我们呢?”
6. 找一个专家帮你找出自卑的主因。由童年研究起,认清自己对你大有帮助。
7. 如果遇到困难遭到挫败,要拿出一张纸列出所有对自己有利的因素。这些因素不但可以让你变得积极,而且更能使自己冷静、客观地面对问题。
8. 确实评估自己的能力,然后再将它提高10%。别太自负,但要有足够的自尊。
9. 想像你的能力无限之大。时刻不要忘记接受积极的思想,不给空虚、沮丧、疲倦留有侵袭的时间。
10.提醒自己别和你的恐惧商量如何去做,而是采取主动积极的态度去分析问题、解决问题
grep hp_mailchecksize /usr/home/xiangdong2/test_entadmin_src/src/entplatform/ -r
以下是grep查找test文本中行首包含某个单词的情况:
grep -i -E '^ANT' test
对某个目录的grep匹配命令:
grep -rni -E '^ANT' ./
去掉前非0开头的号码:
grep "^[1-9].*" a.txt
以下是grep查找test文本中行首包含某个单词的情况:
grep -i -E '^ANT' test
对某个目录的grep匹配命令:
grep -rni -E '^ANT' ./
去掉前非0开头的号码:
grep "^[1-9].*" a.txt
我是一个粗人,永远不懂女孩的感受;
我是一个粗人,只知道默默忍受;
我是一个粗人,有些事,永远不明白,永远不明白情为何物;
对酒当歌又有何用,我只是一个粗人。
我是一个粗人,不知道逗女孩开心;
我是一个粗人,只知道对女孩要用真心,真心何用;
我是一个粗人,女孩喜欢甜言蜜语,哪怕是谎言,我不会;
我是一个粗人,女孩喜欢风花雪月,就算是故事,我不懂;
我是一个粗人,永远不会说慌,不会花心,只知道痴心如一;
女孩不爱我,我只是一个粗人;
粗人要坚持原则,女孩不理解;
粗人要保持个性,女孩不满意;
粗人要爱女孩,女孩不接受;
女孩听了甜言蜜语,不理会粗人;
女孩听了一派谎言,反对粗人;
粗人真的很伤心,真的很伤心;
粗人苯嘴拙舌,不会替自己辩解;
粗人老老实实,不会替自己开脱;
粗人只知道,一辈子对女孩好;
粗人只知道,永远真心实意,对女孩好;
粗人想哭,可是粗人是男人,哭不出来;
粗人想骂,可是粗人老实,骂不出口;
粗人喝醉了,为了一个小天使;
粗人不明白,永远不明白,因为我是一个粗人;
周末了,粗人守在计算机前,心里难受;
女孩陪别人出去了,粗人寂寞;
粗人只有用工作来安慰自己;
粗人只有用酒精来麻醉自己;
粗人真的很想哭;
粗人心如刀铰,粗人不怪女孩;
因为我只是一个粗人,一个粗人;
粗人也知道哄女孩开心;
粗人也明白需要浪漫;
可是粗人太笨,粗人只知道关心女孩;
为了女孩,粗人肯得罪上司;
为了女孩,粗人肯牺牲时间;
为了女孩,粗人肯替她做一切的一切;
女孩不爱粗人;
女孩不爱粗人;
粗人不后悔,因为粗人爱女孩,粗人真心真意爱女孩;
粗人还是想哭,因为女孩不爱粗人;
粗人今天晚上抽了很多烟;
粗人今天晚上喝了很多酒;
粗人的眼泪流出来了;
粗人永远不明白;
粗人不笨,不是不会骗人;
粗人不傻,不是不会花心;
粗人不丑,不是没有女孩爱;
可是粗人只爱那一个,那一个不爱粗人;
粗人很想陪女孩去逛街,女孩没空;
粗人很想陪女孩去吃好吃的,女孩有事;
粗人很想陪女孩看日出日落,
粗人很想陪女孩去看海;
女孩和别的SmartBoy出去了;
粗人很傻,粗人很天真;
粗人哭了,真的哭了;
粗人似乎不明白,粗人也似乎很明白;
粗人明白自己只是粗人;
粗人真的很想将女孩揽入怀中,抚弄女孩的长发;
粗人真的很想拉拉女孩的手;
粗人真的很想吻女孩的额头;
粗人真的很想娶女孩;
粗人真的哭了;
只怪我是一个粗人;
一个粗人而已,粗人不怪女孩;
粗人只怪自己是一个粗人;
粗人要走了;
女孩你知道吗?
当女孩受人挑拨的时候;
当女孩开始反对粗人的时候;
粗人的心凉了,女孩你知道吗?
粗人现在真的哭了,女孩你知道吗?
女孩,当你因为任性跟自己过不去的时候,
粗人真的心疼了,女孩你知道吗?
粗人真的要走了,很凄凉的走了;
粗人爱你,女孩你知道吗?
粗人愿用一生去爱你,女孩你知道吗?
也许女孩太小,也许女孩真的是个幼稚的小女孩;
粗人真的很伤心,粗人真的很心疼女孩;
粗人为什么,为什么;
粗人的烟烧完了,烫到了手指,烫出了水泡,粗人没感觉;
有谁在乎粗人,有谁明白粗人;
谁让我是一个粗人;
女孩的心思粗人永远不懂,永远不懂;
粗人心碎了;
女孩,有一天你会发现,最适合你的人就是对你最好,最爱你,真心真意爱你的,
那一个粗人;
粗人常用这句话来安慰自己;
现在粗人也只能用这句话来安慰自己;
粗人真的想走了,一个人走了;
粗人的心伤透了。
粗人只怪自己是个粗人;
粗人会记得那双闪亮的眼睛;
粗人会记得那美丽的容颜;
粗人会记得女孩的温柔;
粗人会记得女孩的俏皮;
粗人会记得女孩的倔强;
粗人舍不得,多想女孩对粗人说ILU;
粗人心疼,心疼女孩做的那些伤害自己的傻事;
粗人想对女孩说,一份真挚的感情来直不易;
粗人想走了,粗人想对女孩说留住我好吗?
----
有的人腿断了,可他还站着;
有的人腿没断,可是他装逼,他趴着
我是一个粗人,只知道默默忍受;
我是一个粗人,有些事,永远不明白,永远不明白情为何物;
对酒当歌又有何用,我只是一个粗人。
我是一个粗人,不知道逗女孩开心;
我是一个粗人,只知道对女孩要用真心,真心何用;
我是一个粗人,女孩喜欢甜言蜜语,哪怕是谎言,我不会;
我是一个粗人,女孩喜欢风花雪月,就算是故事,我不懂;
我是一个粗人,永远不会说慌,不会花心,只知道痴心如一;
女孩不爱我,我只是一个粗人;
粗人要坚持原则,女孩不理解;
粗人要保持个性,女孩不满意;
粗人要爱女孩,女孩不接受;
女孩听了甜言蜜语,不理会粗人;
女孩听了一派谎言,反对粗人;
粗人真的很伤心,真的很伤心;
粗人苯嘴拙舌,不会替自己辩解;
粗人老老实实,不会替自己开脱;
粗人只知道,一辈子对女孩好;
粗人只知道,永远真心实意,对女孩好;
粗人想哭,可是粗人是男人,哭不出来;
粗人想骂,可是粗人老实,骂不出口;
粗人喝醉了,为了一个小天使;
粗人不明白,永远不明白,因为我是一个粗人;
周末了,粗人守在计算机前,心里难受;
女孩陪别人出去了,粗人寂寞;
粗人只有用工作来安慰自己;
粗人只有用酒精来麻醉自己;
粗人真的很想哭;
粗人心如刀铰,粗人不怪女孩;
因为我只是一个粗人,一个粗人;
粗人也知道哄女孩开心;
粗人也明白需要浪漫;
可是粗人太笨,粗人只知道关心女孩;
为了女孩,粗人肯得罪上司;
为了女孩,粗人肯牺牲时间;
为了女孩,粗人肯替她做一切的一切;
女孩不爱粗人;
女孩不爱粗人;
粗人不后悔,因为粗人爱女孩,粗人真心真意爱女孩;
粗人还是想哭,因为女孩不爱粗人;
粗人今天晚上抽了很多烟;
粗人今天晚上喝了很多酒;
粗人的眼泪流出来了;
粗人永远不明白;
粗人不笨,不是不会骗人;
粗人不傻,不是不会花心;
粗人不丑,不是没有女孩爱;
可是粗人只爱那一个,那一个不爱粗人;
粗人很想陪女孩去逛街,女孩没空;
粗人很想陪女孩去吃好吃的,女孩有事;
粗人很想陪女孩看日出日落,
粗人很想陪女孩去看海;
女孩和别的SmartBoy出去了;
粗人很傻,粗人很天真;
粗人哭了,真的哭了;
粗人似乎不明白,粗人也似乎很明白;
粗人明白自己只是粗人;
粗人真的很想将女孩揽入怀中,抚弄女孩的长发;
粗人真的很想拉拉女孩的手;
粗人真的很想吻女孩的额头;
粗人真的很想娶女孩;
粗人真的哭了;
只怪我是一个粗人;
一个粗人而已,粗人不怪女孩;
粗人只怪自己是一个粗人;
粗人要走了;
女孩你知道吗?
当女孩受人挑拨的时候;
当女孩开始反对粗人的时候;
粗人的心凉了,女孩你知道吗?
粗人现在真的哭了,女孩你知道吗?
女孩,当你因为任性跟自己过不去的时候,
粗人真的心疼了,女孩你知道吗?
粗人真的要走了,很凄凉的走了;
粗人爱你,女孩你知道吗?
粗人愿用一生去爱你,女孩你知道吗?
也许女孩太小,也许女孩真的是个幼稚的小女孩;
粗人真的很伤心,粗人真的很心疼女孩;
粗人为什么,为什么;
粗人的烟烧完了,烫到了手指,烫出了水泡,粗人没感觉;
有谁在乎粗人,有谁明白粗人;
谁让我是一个粗人;
女孩的心思粗人永远不懂,永远不懂;
粗人心碎了;
女孩,有一天你会发现,最适合你的人就是对你最好,最爱你,真心真意爱你的,
那一个粗人;
粗人常用这句话来安慰自己;
现在粗人也只能用这句话来安慰自己;
粗人真的想走了,一个人走了;
粗人的心伤透了。
粗人只怪自己是个粗人;
粗人会记得那双闪亮的眼睛;
粗人会记得那美丽的容颜;
粗人会记得女孩的温柔;
粗人会记得女孩的俏皮;
粗人会记得女孩的倔强;
粗人舍不得,多想女孩对粗人说ILU;
粗人心疼,心疼女孩做的那些伤害自己的傻事;
粗人想对女孩说,一份真挚的感情来直不易;
粗人想走了,粗人想对女孩说留住我好吗?
----
有的人腿断了,可他还站着;
有的人腿没断,可是他装逼,他趴着