网络socket编程汇总

更新时间:2024-01-07 09:52:01 阅读量: 教育文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

[精华] 网络socket编程指南

http://www.chinaunix.net 作者:流浪者 发表于:2008-04-20 15:21:55

Beej网络socket编程指南

-------------------------------------------------------------------------------- 介绍

Socket 编程让你沮丧吗?从man pages中很难得到有用的信息吗?你想跟上时代去编Internet相关的程序,但是为你在调用 connect() 前的bind() 的结构而不知所措?等等… 好在我已经将这些事完成了,我将和所有人共享我的知识了。如果你了解 C 语言并想穿过网络编程的沼泽,那么你来对地方了。

-------------------------------------------------------------------------------- 读者对象

这个文档是一个指南,而不是参考书。如果你刚开始 socket 编程并想找一本入门书,那么你是我的读者。但这不是一本完全的 socket 编程书。 -------------------------------------------------------------------------------- 平台和编译器

这篇文档中的大多数代码都在 Linux 平台PC 上用 GNU 的 gcc 成功编译过。而且它们在 HPUX平台 上用 gcc 也成功编译过。但是注意,并不是每个代码片段都独立测试过。 -------------------------------------------------------------------------------- 目录:

1) 什么是套接字?

2) Internet 套接字的两种类型 3) 网络理论 4) 结构体 5) 本机转换

6) IP 地址和如何处理它们 7) socket()函数 8) bind()函数 9) connect()函数 10) listen()函数 11) accept()函数

12) send()和recv()函数

13) sendto()和recvfrom()函数 14) close()和shutdown()函数 15) getpeername()函数 16) gethostname()函数 17) 域名服务(DNS) 18) 客户-服务器背景知识 19) 简单的服务器 20) 简单的客户端

21) 数据报套接字Socket

22) 阻塞

23) select()--多路同步I/O 24) 参考资料

-------------------------------------------------------------------------------- 什么是 socket?

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

-------------------------------------------------------------------------------- Internet 套接字的两种类型

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

那么这两种类型是什么呢?一种是\(流格式),另外一种是\(数据包格式)。 我们以后谈到它们的时候也会用到 \和 \。数据报套接字有时也叫―无连接套接字‖(如果你确实要连接 的时候可以用connect()。) 流式套接字是可靠的双向通讯的数据流。如果你向套接字按顺序输出―1,2‖,那么它们将按顺序―1,2‖到达另一 边。它们是无错误的传递的,有自己的错误控制,在此不讨论。

有什么在使用流式套接字?你可能听说过 telnet,不是吗?它就使用流式套接字。你需要你所输入的字符按顺序到达,不是吗?同样, WWW浏览器使用的 HTTP 协议也使用它们来下载页面。实际上,当你通过端口80 telnet 到一个 WWW 站点,然后输入 ―GET pagename‖ 的时候,你也可以得到 HTML 的内容。为什么流式套接字可以达到高质量的数据传输?这是因为它使用了―传输控制协议 (The Transmission Control Protocol)‖,也叫 ―TCP‖ (请参考 RFC-793 获得详细资料。)TCP 控 制你的数据按顺序到达并且没有错 误。你也许听到 ―TCP‖ 是因为听到过 ―TCP/IP‖。这里的 IP 是指―Internet 协议‖(请参考 RFC-791。) IP 只是处理 Internet 路由而已。

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

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

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

-------------------------------------------------------------------------------- 网络理论

既然我刚才提到了协议层,那么现在是讨论网络究竟如何工作和一些 关于 SOCK_DGRAM 包是如何建立的例子。当然,你也可以跳过这一段, 如果你认为已经熟悉的话。

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

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

不说其它废话了,我现在列出整个层次模型。如果你要参加网络考试, 可一定要记住: 应用层 (Application) 表示层 (Presentation) 会话层 (Session) 传输层(Transport) 网络层(Network)

数据链路层(Data Link) 物理层(Physical)

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

这个模型如此通用,如果你想,你可以把它作为修车指南。把它对应 到 Unix,结果是: 应用层(Application Layer) (telnet, ftp,等等)

传输层(Host-to-Host Transport Layer) (TCP, UDP) Internet层(Internet Layer) (IP和路由)

网络访问层 (Network Access Layer) (网络层,数据链路层和物理层) 现在,你可能看到这些层次如何协调来封装原始的数据了。

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

现在结束我们的网络理论速成班。哦,忘记告诉你关于路由的事情了。 但是我不准备谈它,如果你真的关心,那么参考 IP RFC。

-------------------------------------------------------------------------------- 结构体

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

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

仅仅是一个常见的 int。

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

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

unsigned short sa_family; /* 地址家族, AF_xxx */ char sa_data[14]; /*14字节协议地址*/ };

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

为了处理struct sockaddr,程序员创造了一个并列的结构: struct sockaddr_in (\代表 \。) struct sockaddr_in {

short int sin_family; /* 通信类型 */ unsigned short int sin_port; /* 端口 */ struct in_addr sin_addr; /* Internet 地址 */

unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/ };

用这个数据结构可以轻松处理套接字地址的基本元素。注意 sin_zero (它被加入到这个结构,并且长度和 struct sockaddr 一样) 应该使用函数 bzero() 或 memset() 来全部置零。 同时,这一重要的字节,一个指向 sockaddr_in结构体的指针也可以被指向结构体sockaddr并且代替它。这 样的话即使 socket() 想要的是 struct sockaddr *,你仍然可以使用 struct sockaddr_in,并且在最后转换。同时,注意 sin_family 和 struct sockaddr 中的 sa_family 一致并能够设置为 \。最后,sin_port和 sin_addr 必 须是网络字节顺序 (Network Byte Order)! 你也许会反对道:\但是,怎么让整个数据结构 struct in_addr sin_addr 按照网络字节顺序呢?\要知道这个问题的答案,我们就要仔细的看一看这 个数据结构: struct in_addr, 有这样一个联合 (unions):

/* Internet 地址 (一个与历史有关的结构) */ struct in_addr {

unsigned long s_addr; };

它曾经是个最坏的联合,但是现在那些日子过去了。如果你声明 \是数据结构 struct sockaddr_in 的实例,那么 \就储 存4字节的 IP 地址(使用网络字节顺序)。如果你不幸的系统使用的还是恐 怖的联合 struct in_addr ,你还是可以放心4字节的 IP 地址并且和上面 我说的一样(这是因为使用了―#define‖。) -------------------------------------------------------------------------------- 本机转换

我们现在到了新的章节。我们曾经讲了很多网络到本机字节顺序的转 换,现在可以实践了!

你能够转换两种类型: short (两个字节)和 long (四个字节)。这个函 数对于变量类型 unsigned 也适用。假设你想将 short 从本机字节顺序转 换为网络字节顺序。用 \表示 \本机 (host)\,接着是 \,然后用 \表 示 \网络 (network)\,最后用 \表示 \: h-to-n-s, 或者 htons() (\。 太简单了...

如果不是太傻的话,你一定想到了由\,\,\,和 \形成的正确 组合,例如这里肯定没有stolh() (\函数,不仅在这里 没有,所有场合都没有。但是这里有: htons()--\ htonl()--\ ntohs()--\ ntohl()--\ 现在,你可能想你已经知道它们了。你也可能想:―如果我想改变 char 的顺序要怎么办呢?‖ 但是你也许马上就想到,―用不着考虑的‖。你也 许 会想到:我的 68000 机器已经使用了网络字节顺序,我没有必要去调用 htonl() 转换 IP 地址。你可能是对的,但是当你移植你的程序 到别的机器 上的时候,你的程序将失败。可移植性!这里是 Unix 世界!记住:在你 将数据放到网络上的时候,确信它们是网络字节顺序的。

最后一点:为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺 序,而sin_family 需不需要呢? 答案是: sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须 要 是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节 顺序。同时, sin_family 没有发送到网络上,它们可以是本机字节顺序。 -------------------------------------------------------------------------------- IP 地址和如何处理它们 现在我们很幸运,因为我们有很多的函数来方便地操作 IP 地址。没有 必要用手工计算它们,也没有必要用\操作来储存成长整 字型。 首先,假设你已经有了一个sockaddr_in结构体ina,你有一个IP地 址\要储存在其中,你就要用到函数 inet_addr(),将IP地址从 点数格式转换成无符号长整型。使用方法如下: ina.sin_addr.s_addr = inet_addr(\

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

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

好了,现在你可以将IP地址转换成长整型了。有没有其相反的方法呢? 它可以将一个in_addr结构体输出成点数格式?这样的话,你就要用到函数 inet_ntoa()(\的含义是\,就像这样: printf(\

struct sockaddr_in their_addr; /* connector's address information */ if (argc != 2) {

fprintf(stderr,\ exit(1); }

if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */ herror(\ exit(1); }

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror(\ exit(1); }

their_addr.sin_family = AF_INET; /* host byte order */

their_addr.sin_port = htons(PORT); /* short, network byte order */ their_addr.sin_addr = *((struct in_addr *)he->;h_addr);

bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */ if (connect(sockfd, (struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -1) { perror(\ exit(1); }

if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) { perror(\ exit(1); }

buf[numbytes] = '\\0';

printf(\ close(sockfd); return 0; }

注意,如果你在运行服务器之前运行客户程序,connect() 将返回 \信息,这非常有用。

-------------------------------------------------------------------------------- 数据包 Sockets

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

listener 在机器上等待在端口 4590 来的数据包。talker 发送数据包到 一定的机器,它包含用户在命令行输入的内容。 这里就是 listener.c: #include ;

#include ; #include ;

#include ; #include ; #include ; #include ; #include ;

#define MYPORT 4950 /* the port users will be sending to */ #define MAXBUFLEN 100 main() {

int sockfd;

struct sockaddr_in my_addr; /* my address information */

struct sockaddr_in their_addr; /* connector's address information */ int addr_len, numbytes; char buf[MAXBUFLEN];

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror(\ exit(1); }

my_addr.sin_family = AF_INET; /* host byte order */

my_addr.sin_port = htons(MYPORT); /* short, network byte order */ my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */ bzero(&(my_addr.sin_zero),; /* zero the rest of the struct */

if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) \\

== -1) {

perror(\ exit(1); }

addr_len = sizeof(struct sockaddr);

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

(struct sockaddr *)&their_addr, &addr_len)) == -1) { perror(\ exit(1); }

printf(\ printf(\ buf[numbytes] = '\\0';

printf(\ close(sockfd); }

注意在我们的调用 socket(),我们最后使用了 SOCK_DGRAM。同时,用 listen() 或者 accept()。我们在使用无连接的数据报套接 字! 下面是 talker.c: #include ;

没有必要去使 #include ; #include ; #include ; #include ; #include ; #include ; #include ;

#define MYPORT 4950 /* the port users will be sending to */ int main(int argc, char *argv[]) {

int sockfd;

struct sockaddr_in their_addr; /* connector's address information */ struct hostent *he; int numbytes;

if (argc != 3) {

fprintf(stderr,\ exit(1); }

if ((he=gethostbyname(argv[1])) == NULL) { /* get the host info */ herror(\ exit(1); }

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror(\ exit(1); }

their_addr.sin_family = AF_INET; /* host byte order */

their_addr.sin_port = htons(MYPORT); /* short, network byte order */

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

bzero(&(their_addr.sin_zero),; /* zero the rest of the struct */ if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, \\

(struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) { perror(\ exit(1); }

printf(\

%s\\n\ close(sockfd); return 0;

}

这就是所有的了。在一台机器上运行 listener,然后在另外一台机器上 运行 talker。观察它们的通讯!

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

-------------------------------------------------------------------------------- 阻塞

阻塞,你也许早就听说了。\阻塞\是 \的科技行话。你可能注意 到前面运行的 listener 程序,它在那里不停地运 行,等待数据包的到来。 实际在运行的是它调用 recvfrom(),然后没有数据,因此 recvfrom() 说\阻塞 (block)\,直到 数据的到来。 很多函数都利用阻塞。accept() 阻塞,所有的 recv*() 函数阻塞。它 们之所以能这样做是因为它们被允许这样做。当你第一次调 用 socket() 建 立套接字描述符的时候,内核就将它设置为阻塞。如果你不想套接字阻塞, 你就要调用函数 fcntl(): #include ; #include ; . .

sockfd = socket(AF_INET, SOCK_STREAM, 0); fcntl(sockfd, F_SETFL, O_NONBLOCK); . .

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

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

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

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

select() 让你可以同时监视多个套接字。如果你想知道的话,那么它就 会告诉你哪个套接字准备读,哪个又准备写,哪个套接字又发生了例外 (exception)。 闲话少说,下面是 select(): #include ;

#include ; #include ;

int select(int numfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

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

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

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

int tv_sec; /* seconds */

int tv_usec; /* microseconds */ };

只要将 tv_sec 设置为你要等待的秒数,将 tv_usec 设置为你要等待 的微秒数就可以了。是的,是微秒而不是毫秒。1,000微秒 等于1毫秒,1,000 毫秒等于1秒。也就是说,1秒等于1,000,000微秒。为什么用符号 \呢? 字母 \很象希腊字母 Mu,而 Mu 表示 \微\的意思。当然,函数 返回的时候 timeout 可能是剩余的时间,之所以是可能,是因为它依赖于 你的 Unix 操作系统。

哈!我们现在有一个微秒级的定时器!别计算了,标准的 Unix 系统 的时间片是100毫秒,所以无论你如何设置你的数据结构 struct timeval, 你都要等待那么长的时间。 还有一些有趣的事情:如果你设置数据结构 struct timeval 中的数据为 0,select() 将立即超时,这样就可以有效地轮询 集合中的所有的文件描述 符。如果你将参数 timeout 赋值为 NULL,那么将永远不会发生超时,即 一直等到第一个文件描述符就绪。最后,如果 你不是很关心等待多长时间, 那么就把它赋为 NULL 吧。 下面的代码演示了在标准输入上等待 2.5 秒: #include ;

#include ; #include ;

#define STDIN 0 /* file descriptor for standard input */ main() {

struct timeval tv; fd_set readfds; tv.tv_sec = 2;

tv.tv_usec = 500000; FD_ZERO(&readfds);

FD_SET(STDIN, &readfds);

/* don't care about writefds and exceptfds: */

select(STDIN+1, &readfds, NULL, NULL, &tv); if (FD_ISSET(STDIN, &readfds)) printf(\ else

printf(\ }

如果你是在一个 line buffered 终端上,那么你敲的键应该是回车 (RETURN),否则无论如何它都会超时。

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

最后一件关于 select() 的事情:如果你有一个正在侦听 (listen()) 的套 接字,你可以通过将该套接字的文件描述符加入到 readfds 集合中来看是 否有新的连接。 这就是我关于函数select() 要讲的所有的东西。 参考书目:

Internetworking with TCP/IP, volumes I-III by Douglas E. Comer and David L. Stevens. Published by Prentice Hall. Second edition ISBNs: 0-13-468505-9, 0-13-472242-6, 0-13-474222-2. There is a third edition of this set which covers IPv6 and IP over ATM.

Using C on the UNIX System by David A. Curry. Published by O'Reilly & Associates, Inc. ISBN 0-937175-23-4.

TCP/IP Network Administration by Craig Hunt. Published by O'Reilly & Associates, Inc. ISBN 0-937175-82-X.

TCP/IP Illustrated, volumes 1-3 by W. Richard Stevens and Gary R. Wright. Published by Addison Wesley. ISBNs: 0-201-63346-9, 0-201-63354-X, 0-201-63495-3.

Unix Network Programming by W. Richard Stevens. Published by Prentice Hall. ISBN 0-13-949876-1. On the web:

BSD Sockets: A Quick And Dirty Primer

(http://www.cs.umn.edu/~bentlema/unix/--has other great Unix system programming info, too!) Client-Server Computing

(http://pandonia.canberra.edu.au/ClientServer/socket.html) Intro to TCP/IP (gopher)

(gopher://gopher-chem.ucdavis.edu/11/Index/Internet_aw/Intro_the_Inter net/intro.to.ip/)

Internet Protocol Frequently Asked Questions (France) (http://web.cnam.fr/Network/TCP-IP/) The Unix Socket FAQ

(http://www.ibrado.com/sock-faq/) RFCs--the real dirt:

RFC-768 -- The User Datagram Protocol (UDP) (ftp://nic.ddn.mil/rfc/rfc768.txt) RFC-791 -- The Internet Protocol (IP) (ftp://nic.ddn.mil/rfc/rfc791.txt)

RFC-793 -- The Transmission Control Protocol (TCP) (ftp://nic.ddn.mil/rfc/rfc793.txt) RFC-854 -- The Telnet Protocol

(ftp://nic.ddn.mil/rfc/rfc854.txt)

RFC-951 -- The Bootstrap Protocol (BOOTP) (ftp://nic.ddn.mil/rfc/rfc951.txt)

RFC-1350 -- The Trivial File Transfer Protocol (TFTP) (ftp://nic.ddn.mil/rfc/rfc1350.txt)

Linux下Socket编程

http://www.kuqin.com/networkprog/20080512/8361.html

摘要:网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。

什么是Socket

Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。要学Internet上的TCP/IP网络编程,必须理解Socket接口。

Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和输出的话,就很容易了解Socket了。网络的 Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返 回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。常用的Socket类型有两种:流式Socket (SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据 报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

Socket建立

为了建立Socket,程序可以调用Socket函数,该函数返回一个类似于文件描述符的句柄。socket函数原型为:

int socket(int domain, int type, int protocol); domain指明所使用的协议族,通常为PF_INET,表示互联网协议族(TCP/IP协议族);type参数指定socket的类型: SOCK_STREAM 或SOCK_DGRAM,Socket接口还定义了原始

Socket(SOCK_RAW),允许程序使用低层协议;protocol通常赋值\。 Socket()调用返回一个整型socket描述符,你可以在后面的调用使用它。

Socket描述符是一个指向内部数据结构的指针,它指向描述符表入口。调用Socket函数时,socket执行体将建立一个Socket,实际上\建立一个Socket\意味着为一个Socket数据结构分配存储空间。Socket执行体为你管理描述符表。

两个网络程序之间的一个网络连接包括五种信息:通信协议、本地协议地址、本地主机端口、远端主机地址和远端协议端口。Socket数据结构中包含这五种信息。

Socket配置

通过socket调用返回一个socket描述符后,在使用socket进行网络传输以前,必须配置该socket。面向连接的socket客户端通过 调用Connect函数在socket数据结构中保存本地和远端信息。无连接socket的客户端和服务端以及面向连接socket的服务端通过调用 bind函数来配置本地信息。

Bind函数将socket与本机上的一个端口相关联,随后你就可以在该端口监听服务请求。Bind函数原型为:

int bind(int sockfd,struct sockaddr *my_addr, int addrlen);

Sockfd是调用socket函数返回的socket描述符,my_addr是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针;addrlen常被设置为sizeof(struct sockaddr)。

struct sockaddr结构类型是用来保存socket信息的: struct sockaddr {

unsigned short sa_family; /* 地址族, AF_xxx */ char sa_data[14]; /* 14 字节的协议地址 */ };

sa_family一般为AF_INET,代表Internet(TCP/IP)地址族;sa_data则包含该socket的IP地址和端口号。

另外还有一种结构类型: struct sockaddr_in {

short int sin_family; /* 地址族 */

unsigned short int sin_port; /* 端口号 */ struct in_addr sin_addr; /* IP地址 */

unsigned char sin_zero[8]; /* 填充0 以保持与struct sockaddr同样大小 */ };

这个结构更方便使用。sin_zero用来将sockaddr_in结构填充到与struct sockaddr同样的长度,可以用bzero()或memset()函数将其置为零。指向sockaddr_in 的指针和指向sockaddr的指针可以相互转换,这意味着如果一个函数所需参数类型是sockaddr时,你可以在函数调用的时候将一个指向 sockaddr_in的指针转换为指向sockaddr的指针;或者相反。

使用bind函数时,可以用下面的赋值实现自动获得本机IP地址和随机获取一个没有被占用的端口号:

my_addr.sin_port = 0; /* 系统随机选择一个未被使用的端口号 */ my_addr.sin_addr.s_addr = INADDR_ANY; /* 填入本机IP地址 */

通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。同样,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址。

注意在使用bind函数是需要将sin_port和sin_addr转换成为网络字节优先顺序;而sin_addr则不需要转换。

计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。Internet上数据以高位字节优先顺序在网络上传输,所以对于在内部是以低位字节优先方式存储数据的机器,在Internet上传输数据时就需要进行转换,否则就会出现数据不一致。 下面是几个字节顺序转换函数:

·htonl():把32位值从主机字节序转换成网络字节序 ·htons():把16位值从主机字节序转换成网络字节序 ·ntohl():把32位值从网络字节序转换成主机字节序 ·ntohs():把16位值从网络字节序转换成主机字节序

Bind()函数在成功被调用时返回0;出现错误时返回\并将errno置为相应的错误号。需要注意的是,在调用bind函数时一般不要将端口号置为小于1024的值,因为1到1024是保留端口号,你可以选择大于1024中的任何一个没有被占用的端口号。

连接建立

面向连接的客户程序使用Connect函数来配置socket并与远端服务器建立一个TCP连接,其函数原型为:

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

Sockfd 是socket函数返回的socket描述符;serv_addr是包含远端主机IP地址和端口号的指针;addrlen是远端地质结构的长度。 Connect函数在出现错误时返回-1,并且设置errno为相应的错误码。进行客户端程序设计无须调用bind(),因为这种情况下只需知道目的机器 的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,socket执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候到 打断口。 Connect函数启动和远端主机的直接连接。只有面向连接的客户程序使用socket时才需要将此socket与远端主机相连。无连接协议从不建立直接连接。面向连接的服务器也从不启动一个连接,它只是被动的在协议端口监听客户的请求。

Listen函数使socket处于被动的监听模式,并为该socket建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们。 int listen(int sockfd, int backlog);

Sockfd 是Socket系统调用返回的socket 描述符;backlog指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待accept()它们(参考下文)。Backlog对队列中等待 服务的请求的数目进行了限制,大多数系统缺省值为20。如果一个服务请求到来时,输入队列已满,该socket将拒绝连接请求,客户将收到一个出错信息。 当出现错误时listen函数返回-1,并置相应的errno错误码。

accept()函数让服务器接收客户的连接请求。在建立好输入队列后,服务器就调用accept函数,然后睡眠并等待客户的连接请求。

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

sockfd是被监听的socket描述符,addr通常是一个指向sockaddr_in变量的指针,该变量用来存放提出连接请求服务的主机的信息(某 台主机从某个端口发出该请求);addrten通常为一个指向值为sizeof(struct sockaddr_in)的整型指针变量。出现错误时accept函数返回-1并置相应的errno值。

首先,当accept函数监视的 socket收到连接请求时,socket执行体将建立一个新的socket,执行体将这个新socket和请求连接进程的地址联系起来,收到服务请求的 初始

socket仍可以继续在以前的 socket上监听,同时可以在新的socket描述符上进行数据传输操作。

数据传输

Send()和recv()这两个函数用于面向连接的socket上进行数据传输。 Send()函数原型为:

int send(int sockfd, const void *msg, int len, int flags);

Sockfd是你想用来传输数据的socket描述符;msg是一个指向要发送数据的指针;Len是以字节为单位的数据的长度;flags一般情况下置为0(关于该参数的用法可参照man手册)。 Send()函数返回实际上发送出的字节数,可能会少于你希望发送的数据。在程序中应该将send()的返回值与欲发送的字节数进行比较。当send()返回值与len不匹配时,应该对这种情况进行处理。 char *msg = \int len, bytes_sent; ??

len = strlen(msg);

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

recv()函数原型为:

int recv(int sockfd,void *buf,int len,unsigned int flags); Sockfd是接受数据的socket描述符;buf 是存放接收数据的缓冲区;len是缓冲的长度。Flags也被置为0。Recv()返回实际上接收的字节数,当出现错误时,返回-1并置相应的errno值。

Sendto()和recvfrom()用于在无连接的数据报socket方式下进行数据传输。由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址。 sendto()函数原型为: int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);

该函数比send()函数多了两个参数,to表示目地机的IP地址和端口号信息,而tolen常常被赋值为sizeof (struct sockaddr)。Sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。 Recvfrom()函数原型为:

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

from是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。fromlen常置为sizeof (struct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。Recvfrom()函数返回接收到的字节数或 当出现错误时返回-1,并置相应的errno。

如果你对数据报socket调用了connect()函数时,你也可以利用send()和recv()进行数据传输,但该socket仍然是数据报socket,并且利用传输层的UDP服务。但在发送或接收数据报时,内核会自动为之加上目地和源地址信息。

结束传输

当所有的数据操作结束以后,你可以调用close()函数来释放该socket,从而停止在该socket上的任何数据操作:

close(sockfd);

你也可以调用shutdown()函数来关闭该socket。该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行。如你可以关闭某socket的写操作而允许继续在该socket上接受数据,直至读入所有数据。 int shutdown(int sockfd,int how); Sockfd是需要关闭的socket的描述符。参数 how允许为shutdown操作选择以下几种方式:

·0-------不允许继续接收数据 ·1-------不允许继续发送数据

·2-------不允许继续发送和接收数据, ·均为允许则调用close ()

shutdown在操作成功时返回0,在出现错误时返回-1并置相应errno。

Socket编程实例

代码实例中的服务器通过socket连接向客户端发送字符串\

connected!\。只要在服务器上运行该服务器软件,在客户端运行客户软件,客户端就会收到该字符串。

该服务器软件代码如下: #include #include #include #include #include #include #include #include

#define SERVPORT 3333 /*服务器监听端口号 */ #define BACKLOG 10 /* 最大同时连接请求数 */ main() {

int sockfd,client_fd; /*sock_fd:监听socket;client_fd:数据传输socket */ struct sockaddr_in my_addr; /* 本机地址信息 */

struct sockaddr_in remote_addr; /* 客户端地址信息 */ if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror(\创建出错!\ }

my_addr.sin_family=AF_INET;

my_addr.sin_port=htons(SERVPORT); my_addr.sin_addr.s_addr = INADDR_ANY; bzero(&(my_addr.sin_zero),8);

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

perror(\出错!\ exit(1);

}

if (listen(sockfd, BACKLOG) == -1) { perror(\出错!\ exit(1); }

while(1) {

sin_size = sizeof(struct sockaddr_in);

if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr, &sin_size)) == -1) {

perror(\出错\ continue; }

printf(\ if (!fork()) { /* 子进程代码段 */

if (send(client_fd, \ perror(\出错!\ close(client_fd); exit(0); }

close(client_fd); } } }

服务器的工作流程是这样的:首先调用socket函数创建一个Socket,然后调用bind函数将其与本机地址以及一个本地端口号绑定,然后调用 listen在相应的socket上监听,当accpet接收到一个连接服务请求时,将生成一个新的socket。服务器显示该客户机的IP地址,并通过 新的socket向客户端发送字符串\,you are connected!\。最后关闭该socket。

代码实例中的fork()函数生成一个子进程来处理数据传输部分,fork()语句对于子进程返回的值为0。所以包含fork函数的if语句是子进程代码部分,它与if语句后面的父进程代码部分是并发执行的。

客户端程序代码如下: #include #include #include #include #include #include #include #include #define SERVPORT 3333

#define MAXDATASIZE 100 /*每次最大数据传输量 */ main(int argc, char *argv[]){

int sockfd, recvbytes; char buf[MAXDATASIZE]; struct hostent *host;

struct sockaddr_in serv_addr; if (argc < 2) {

fprintf(stderr,\exit(1); }

if((host=gethostbyname(argv[1]))==NULL) { herror(\出错!\exit(1); }

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){ perror(\创建出错!\exit(1); }

serv_addr.sin_family=AF_INET;

serv_addr.sin_port=htons(SERVPORT);

serv_addr.sin_addr = *((struct in_addr *)host->h_addr); bzero(&(serv_addr.sin_zero),8);

if (connect(sockfd, (struct sockaddr *)&serv_addr, \\ sizeof(struct sockaddr)) == -1) { perror(\出错!\exit(1); }

if ((recvbytes=recv(sockfd, buf, MAXDATASIZE, 0)) ==-1) { perror(\出错!\exit(1); }

buf[recvbytes] = '\\0';

printf(\ close(sockfd); }

客户端程序首先通过服务器域名获得服务器的IP地址,然后创建一个socket,调用connect函数与服务器建立连接,连接成功之后接收从服务器发送过来的数据,最后关闭socket。

函数gethostbyname()是完成域名转换的。由于IP地址难以记忆和读写,所以为了方便,人们常常用域名来表示主机,这就需要进行域名和IP地址的转换。函数原型为: struct hostent *gethostbyname(const char *name); 函数返回为hosten的结构类型,它的定义如下: struct hostent {

char *h_name; /* 主机的官方域名 */

char **h_aliases; /* 一个以NULL结尾的主机别名数组 */

int h_addrtype; /* 返回的地址类型,在Internet环境下为AF-INET */

int h_length; /* 地址的字节长度 */

char **h_addr_list; /* 一个以0结尾的数组,包含该主机的所有地址*/ };

#define h_addr h_addr_list[0] /*在h-addr-list中的第一个地址*/

当 gethostname()调用成功时,返回指向struct hosten的指针,当调用失败时返回-1。当调用gethostbyname时,你不能使用perror()函数来输出错误信息,而应该使用herror()函数来输出。

无连接的客户/服务器程序的在原理上和连接的客户/服务器是一样的,两者的区别在于无连接的客户/服务器中的客户一般不需要建立连接,而且在发送接收数据时,需要指定远端机的地址。

阻塞和非阻塞

阻塞函数在完成其指定的任务以前不允许程序调用另一个函数。例如,程序执行一个读数据的函数调用时,在此函数完成读操作以前将不会执行下一程序语句。当 服务器运行到accept语句时,而没有客户连接服务请求到来,服务器就会停止在accept语句上等待连接服务请求的到来。这种情况称为阻塞 (blocking)。而非阻塞操作则可以立即完成。比如,如果你希望服务器仅仅注意检查是否有客户在等待连接,有就接受连接,否则就继续做其他事情,则 可以通过将Socket设置为非阻塞方式来实现。非阻塞socket在没有客户在等待时就使accept调用立即返回。 #include #include ??

sockfd = socket(AF_INET,SOCK_STREAM,0); fcntl(sockfd,F_SETFL,O_NONBLOCK); ??

通过设置socket为非阻塞方式,可以实现\轮询\若干Socket。当企图从一个没有数据等待处理的非阻塞Socket读入数据时,函数将立即返 回,返回值为-1,并置errno值为EWOULDBLOCK。但是这种\轮询\会使CPU处于忙等待方式,从而降低性能,浪费系统资源。而调用 select()会有效地解决这个问题,它允许你把进程本身挂起来,而同时使系统内核监听所要求的一组文件描述符的任何活动,只要确认在任何被监控的文件 描述符上出现活动,select()调用将返回指示该文件描述符已准备好的信息,从而实现了为进程选出随机的变化,而不必由进程本身对输入进行测试而浪费 CPU开销。Select函数原型为: int select(int numfds,fd_set *readfds,fd_set *writefds, fd_set *exceptfds,struct timeval *timeout);

其中readfds、writefds、exceptfds分别是被select()监视的读、写和异常处理的文件描述符集合。如果你希望确定是否可以 从标准输入和某个socket描述符读取数据,你只需要将标准输入的文件描述符0和相应的sockdtfd加入到readfds集合中;numfds的值 是需要检查的号码最高的文件描述符加1,这个例子中numfds的值应为sockfd+1;当select返回时,readfds将被修改,指示某个文件 描述符已经准备被读取,你可以通过FD_ISSSET()来测试。为了实现fd_set中对应的文件描述符的设置、复位和测试,它提供了一组宏: FD_ZERO(fd_set *set)----清除一个文件描述符集;

FD_SET(int fd,fd_set *set)----将一个文件描述符加入文件描述符集中; FD_CLR(int fd,fd_set *set)----将一个文件描述符从文件描述符集中清除; FD_ISSET(int fd,fd_set *set)----试判断是否文件描述符被置位。

Timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout长时间后没有文件描述符准备好即返回。struct timeval数据结构为: struct timeval {

int tv_sec; /* seconds */

int tv_usec; /* microseconds */ };

POP3客户端实例

下面的代码实例基于POP3的客户协议,与邮件服务器连接并取回指定用户帐号的邮件。与邮件服务器交互的命令存储在字符串数组POPMessage中,程序通过一个do-while循环依次发送这些命令。 #include #include #include #include #include #include #include #include #define POP3SERVPORT 110 #define MAXDATASIZE 4096 main(int argc, char *argv[]){ int sockfd;

struct hostent *host;

struct sockaddr_in serv_addr; char *POPMessage[]={ \\\\\\\NULL };

int iLength; int iMsg=0; int iEnd=0;

char buf[MAXDATASIZE];

if((host=gethostbyname(\perror(\exit(1); }

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){ perror(\exit(1); }

serv_addr.sin_family=AF_INET;

serv_addr.sin_port=htons(POP3SERVPORT);

serv_addr.sin_addr = *((struct in_addr *)host->h_addr); bzero(&(serv_addr.sin_zero),8);

if (connect(sockfd, (struct sockaddr *)&serv_addr,sizeof(struct sockaddr))==-1){ perror(\exit(1); }

do {

send(sockfd,POPMessage[iMsg],strlen(POPMessage[iMsg]),0); printf(\iLength=recv(sockfd,buf+iEnd,sizeof(buf)-iEnd,0); iEnd+=iLength; buf[iEnd]='\\0';

printf(\iMsg++;

} while (POPMessage[iMsg]); close(sockfd); }

Linux socket 编程入门(一)TCP server 端

http://www.cppblog.com/lf426/archive/2008/07/08/55641.html

1、建模

绝大部分关于socket编程的教程总是从socket的概念开始讲起的。要知道,socket的初衷是个庞大的体系,TCP/IP只是这个庞大体系下一 个很小的子集,而我们真正能用上的更是这个子集中的一小部分:运输层(Host-to-Host Transport Layer)的TCP和UDP协议,以及使用这两个协议进行应用层(Application Layer)的开发。即使是socket的核心部分,网络层(Internet Layer)的IP协议,在编程的时候我们也很少会感觉到它的存在——因为已经被封装好了,我们唯一需要做的事情就是传入一个宏。第一节我想介绍的概念就 这么多,当然,既然我们已经说了3个层了,我想最好还是把最后一个层也说出来,即所谓链路层(Network Access Layer),它包括了物理硬件和驱动程序。这四个层从底到高的顺序是:链路层--网络层--运输层--应用层。 好,说实话我们现在并不清楚所谓TCP到底是什么东东,不过我们知道这东东名气很大。或许你早就知道,另外一个声名狼藉建立在TCP协议基础上的应用程 序,它曾经几乎是统治了一个时代,即使是今天,我们依然无法消除他的影响力的——恩,是的,就是telnet。

在这个教程中,我使用的环境是Debian GNU/Linux 4.0 etch。传说中的stable -_-!!!,恩,我是很保守的人。如果你不是自己DIY出来的系统,相信默认安装里面就应该有telnet(/usr/bin/telnet,要是没装 就自己aptitude install吧)。telnet可以与所有遵循TCP协议的服务器端进行通讯。通常,socket编程总是Client/Server形式的,因为有了 telnet,我们可以先不考虑client的程序,我们先写一个支持TCP协议的server端,然后用telnet作为client验证我们的程序就 好了。

server端的功能,我们也考虑一种最简单的反馈形式:echo。就如同你在终端输入echo 'Hello World',回车后shell就会给你返回Hello World一样,我们的第一个TCP server就用以实现这个功能。

什么样的模型适合描述这样的一种server呢?我相信,一个很2的例子会有助于我们记忆TCP server端的基本流程。 想象你自己是个小大佬,坐办公室(什么样的黑社会做办公室啊?可能是讨债公司吧^^)你很土,只有一个小弟帮你接电话(因为你自己的号码是不敢对外公开 的)。一次通讯的流程大概应该是这样的:小弟那里的总机电话响了;小弟接起电话;对方说是你女朋友A妹;小弟转达说,“老大,你马子电话”;你说,接过 来;小弟把电话接给你;你和你女朋友聊天半小时;挂电话。

我们来分析一下整个过程中的元素。先分析成员数据(请注意,这里开始用C++术语了):你小弟(listenSock),你需要他来监听(listen, 这是socket编程中的术语)电话;你自己(communicationSock),实际上打电话进行交流的是你自己;你的电话号码 (servAddr),否则你女朋友怎么能找到你?你女朋友的电话号码(clntAddr),这个比喻有点牵强,因为事实上你接起电话,不需要知道对方的 号码也可以通话(虽然事实上你应该是知道的,你不会取消了来电显示功能吧^^),但是,难道你是只接女朋友电话从来不打过去的牛人吗?这个过程中的行为 (成员函数):你小弟接电话并转接给你(isAccept());你自己的通话(handleEcho())(这个行为确实比较土,只会乌鸦学舌的 echo,呵呵)。

简单的说,就是这些了。根据这个模型,我们可以很容易写出实现我们需要的echo功能的TCP server的类:

class TcpServer { private:

int listenSock; int communicationSock; sockaddr_in servAddr; sockaddr_in clntAddr; public:

TcpServer(int listen_port); bool isAccept(); void handleEcho(); };

这里面有些简写,比如,sock实际上就是socket,addr就是address。serv和clnt我想你一定能猜到是server和client 吧。还有一个socket中的结构体sockaddr_in,实际上就是这个意思:socket address internet(网络嵌套字地址),具体解说,请看下回分解。

2、socket与文件描述符

UNIX中的一切事物都是文件(everything in Unix is a file!)

当我在这篇教程中提到UNIX的时候,其意思专指符合UNIX标准的所谓―正统‖UNIX的衍生系统(其实我就用来带指那些买了最初UNIX源代码的商业 系统)操作系统和类似Linux,BSD这些类UNIX系统。如果某些要点是Linux特有的,或者因为本人孤陋寡闻暂时搞不清楚是Linux特有的还是 UNIX通用的,我就会指明是Linux,甚至其发行版(我本人在写这篇教程的时候是以Debian GNU/Linux 4.0 etch为测试平台的)。

我们学习UNIX的时候,恐怕听到的第一句话就是这句:UNIX中一切都是文件。这是UNIX的基本理念之一,也是一句很好的概括。比如,很多UNIX老 鸟会举出个例子来,―你看,/dev/hdc是个文件,它实际上也是我的光盘……‖UNIX中的文件可以是:网络连接(network connection),输入输出(FIFO),管道(a pipe),终端(terminal),硬盘上的实际文件,或者其它任何东东。

文件与文件描述符(file & file descriptor)

你可能对上一章中建模类中的int还记忆犹新。我们用int在描述socket,实际上,所有的文件描述符都是int,没错,用的是一个整数类型。如果你 觉得这样让你很难接受,那么恭喜你,你跟我一样,也许是深中C++面向对象思想的毒了^^。因为是int,所以文件描述符不可能是C++概念中的对象,因 为int无法发出行为,但是,这并不代表也不能接受一个动作哈。

PASCAL之父在批判面向对象思想教条的时候,曾经生动的举了个例子,―在OOP的概念中,绝对不应该接受a+b这种表达的, OOP对这个问题的表达应该是a.add(b)‖。fd(file descriptor)可以作为接受动作的对象,但是本身却无法发出动作,这就如同一个只能做宾语不能做主语的名词,是个不完整的对象。但是,请别忘了 Linux和socket本身是C语言的

产物,我们必须接受在面向过程时代下的产物,正视历史——当然,这与我们自己再进行OOP的封装并不矛盾。

我们应该记住3个已经打开的fd,0:标准输入(STDIN_FILENO);1:标准输出(STDOUT_FILENO);2:标准错误 (STDERR_FILENO)。(以上宏定义在中)一个最简单的使用fd的例子,就是使用< unistd.h>中的函数:write(1, \,在标准输出上显示―Hello, World!‖。 另外一个需要注意的问题是,file和fd并非一定是一一对应的。当一个file被多个程序调用的时候,会生成相互独立的fd。这个概念可以类比于C++中的引用(eg: int& rTmp = tmp;)。

socket与file descriptor

文件是应用程序与系统(包括特定硬件设备)之间的桥梁,而文件描述符就是应用程序使用这个―桥梁‖的接口。在需要的时候,应用程序会向系统申请一个文件, 然后将文件的描述符返回供程序使用。返回socket的文件通常被创建在/tmp或者/usr/tmp中。我们实际上不用关心这些文件,仅仅能够利用返回 的socket描述符就可以了。

好了,说了这么多,实际上就解释了一个问题,―为什么socket的类型是int?‖ -_-!!!

3、sockaddr与sockaddr_in

收件人地址

一家化妆品公司将一批新产品的样品,准备发给某学校某个班的女生们免费试用。通常情况下,这件邮包的地址上可以这么写: 收件人:全体女生。

地址:A省B市C学校,X级Y班。

但是,如果在描述地址的时候这样写呢: 收件人:全体女生。

地址:请打电话xxxxxxxx,找他们学校一个叫Lucy的女生,然后把东西送到她的班上。 这种文字是相当的诡异啊-_-!!!,但是并不等于就没有表述清楚邮包的去向和地址。事实上邮局看到这样的地址一定会发飙的,然而对于电脑,如果你的地址描述形式是他可以接受和执行的,他就会老老实实的按你的要求去做?? 所以,如何描述地址不是问题的关键,关键在于这样的表述是不是能够表述清楚一个地址。一种更加通用的表达形式可能是这样的: 收件人:全体女生。

地址:<一种地址描述方式> 事实上,在socket的通用address描述结构sockaddr中正是用这样的方式来进行地址描述的:

struct sockaddr {

unsigned short sa_family; char sa_data[14]; };

这是一个16字节大小的结构(2+14),sa_family可以认为是socket address family的缩写,也可能被简写成AF(Address Family),他就好像我们例子中那个“收件人:全体女生”一样,虽然事实上有很多AF的种类,但是我们这个教程中只用得上大名鼎鼎的

internet 家族AF_INET。另外的14字节是用来描述地址的。这是一种通用结构,事实上,当我们指定sa_family=AF_INET之后,sa_data的 形式也就被固定了下来:最前端的2字节用于记录16位的端口,紧接着的4字节用于记录32位的IP地址,最后的8字节清空为零。这就是我们实际在构造 sockaddr时候用到的结构sockaddr_in(意指socket address internet):

struct sockaddr_in {

unsigned short sin_family; unsigned short sin_port; struct in_addr sin_addr; char sin_zero[8]; };

我想,sin_的意思,就是socket (address) internet吧,只不过把address省略掉了。sin_addr被定义成了一个结构,这个结构实际上就是:

struct in_addr {

unsigned long s_addr; };

in_addr显然是internet address了,s_addr是什么意思呢?说实话我没猜出值得肯定的答案,也许就是socket address的意思吧,尽管跟更广义的sockaddr结构意思有所重复了。哎,这些都是历史原因,也许我是没有精力去考究了。

sockaddr和sockaddr_in在Linux中的实现

你可能还记得我之前说过,UNIX和Linux上的socket实现都是从BSD的socket实现演变过来的。事实上,socket这个词本来的意思,就是Berkeley Socket interface的简单说法。Linux上的socket与原本的socket的应该是完全兼容的,不过发展到今天,在代码实现上可能有些小的差别。我们就吹毛求疵的来看看这些区别在什么地方。

#include

/* Structure describing a generic socket address. */ struct sockaddr {

__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */ char sa_data[14]; /* Address data. */ };

//==============

/* POSIX.1g specifies this type name for the `sa_family' member. */ typedef unsigned short int sa_family_t;

/* This macro is used to declare the initial common members of the data types used for socket addresses, `struct sockaddr', `struct sockaddr_in', `struct sockaddr_un', etc. */

#define __SOCKADDR_COMMON(sa_prefix) \\ sa_family_t sa_prefix##family

#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

可以看到,转了几次typedef,几次宏定义,实际效果是与标准socket一样的。

#include

/* Internet address. */ typedef uint32_t in_addr_t; struct in_addr {

in_addr_t s_addr; };

//=================

/* Structure describing an Internet socket address. */ struct sockaddr_in {

__SOCKADDR_COMMON (sin_);

in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */

/* Pad to size of `struct sockaddr'. */

unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; };

同样的,看起来挺复杂,实际上与标准socket的定义是一样的。

头文件依赖关系

是包含在中的,是包含在中的,实际上我们在程序中往往就是: #include #include

值得知道的是,ARPA是 Advanced research project agency(美国国防部高级研究计划暑)的所写,ARPANET是当今互联网的前身,所以我们就可以想象,为什么inet.h会在arpa目录下了。

4、构造函数涉及的概念

话题回到“黑社会办公室”的例子,讲概念已经扯得比较远了,不过,这一节我们还得讲概念,不过好在有些程序的例子。如果大家不想翻回去看TcpServer类的原型,我这里直接给出这个头文件的完整源代码:

//Filename: TcpServerClass.hpp

#ifndef TCPSERVERCLASS_HPP_INCLUDED #define TCPSERVERCLASS_HPP_INCLUDED

#include #include #include #include

class TcpServer { private:

int listenSock; int communicationSock; sockaddr_in servAddr; sockaddr_in clntAddr; public:

TcpServer(int listen_port); bool isAccept(); void handleEcho(); };

#endif // TCPSERVERCLASS_HPP_INCLUDED

我们已经解释了为什么listenSock和communicationSock的类型是int,以及sockaddr_in是什么结构,现在来写这个类的构造函数:

TcpServer::TcpServer(int listen_port) {

if ( (listenSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 ) { throw \ }

memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET;

servAddr.sin_addr.s_addr = htonl(INADDR_ANY); servAddr.sin_port = htons(listen_port);

if ( bind(listenSock, (sockaddr*)&servAddr, sizeof(servAddr)) < 0 ) { throw \ }

if ( listen(listenSock, 10) < 0 ) { throw \ } }

好,先看看程序培养一下感觉,我们还得说概念。

数据封装(Data Encapsutation)

我们前面说到了网络分层:链路——网络——传输——应用。数据从应用程序里诞生,传送到互联网上每一层都会进行一次封装:

Data>>Application>>TCP/UDP>>IP>>OS(Driver, Kernel & Physical Address) 我们用socket重点描述的是协议,包括网络协议(IP)和传输协议(TCP/UDP)。 sockaddr重点描述的是地址,包括IP地址和TCP/UDP端口。

socket()函数

我们从TcpServer::TcpServer()函数可以看到,socket和sockaddr的产生是可以相互独立的。socket()的函数原型是:

int socket(int protocolFamily, int type, int protocol);

在Linux中的实现为:

#include

/* Create a new socket of type TYPE in domain DOMAIN, using

protocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically. Returns a file descriptor for the new socket, or -1 for errors. */ extern int socket (int __domain, int __type, int __protocol) __THROW;

第一个参数是协议簇(Linux里面叫作域,意思一样的),还是那句话,我们这篇教程用到的就仅仅是一个PF_INET(protocol family : internet),很多时候你会发现人们也经常在这里赋值为AF_INET,事实上,当前,AF_INET就是PF_INET的一个#define,但 是,写成PF_INET从语义上会更加严谨。这也就是TCP/IP协议簇中的IP协议(Internet Protocol),网络层的协议。

后面两个参数定义传输层的协议。

第二个参数是传输层协议类型,我们教程里用到的宏,只有两个:SOCK_STREAM(数据流格式)和SOCK_DGRAM(数据报格式);(具体是什么我们以后讨论)

第 三个参数是具体的传输层协议。当赋值为0的时候,系统会根据传输层协议类型自动匹配和选择。事实上,当前,匹配SOCK_STREAM的就是TCP协议; 而匹配SOCK_DGRAM就是UDP协议。所以,我们指定了第二个参数,第三个就可以简单的设置为0。不过,为了严谨,我们最好还是把具体协议写出来, 比如,我们的例子中的TCP协议的宏名称:IPPROTO_TCP。

数据的“地址”

从数据封装的模型,我们可以看到数据是怎么从应用程序传递到互联网的。我们说过,数据的传送是通过socket进行的。但是socket只描述了协议类 型。要让数据正确的传送到某个地方,必须添加那个地方的sockaddr地址;同样,要能接受网络上的数据,必须有自己的sockaddr地址。

可见,在网络上传送的数据包,是socket和sockaddr共同“染指”的结果。他们共同封装和指定了一个数据包的网络协议(IP)和IP地址,传输协议(TCP/UDP)和端口号。

网络字节和本机字节的相互转换

sockaddr结构中的IP地址(sin_addr.s_addr)和端口号(sin_port)将被封装到网络上传送的数据包中,所以,它的结构形式需要保证是网络字节形式。我们这里用到的函数是htons()和htonl(),这些缩写的意思是: h: host,主机(本机) n: network,网络 to: to转换

s: short,16位(2字节,常用于端口号) l: long, 32位(4字节,常用于IP地址)

“反过来”的函数也是存在的ntohs()和ntohl()。

动作与持续行为

本节最后的一个概念可以跟计算机无关。作为动词,有些可以描述动作,有些是描述一重持续的行为状态的(就如同一般动词和be动词一样)。扯到C++来说, 我们可以把持续行为封装到函数内部,只留出动作的接口。事实上,构造函数中的bind()和listen()就是这种描述持续状态的行为函数。

5、创建监听嵌套字

前面一小节,我们已经写出了TcpServer的构造函数。这个函数的实际作用,就是创建了listen socket(监听嵌套字)。这一节,我们来具体分析这个创建的过程。

socket和sockaddr的创建是可以相互独立的

在函数中,我们首先通过socket()系统调用创建了listenSock,然后通过为结构体赋值的方法具体定义了服务器端的sockaddr。 (memset()函数的作用是把某个内存段的空间设定为某值,这里是清零。)其他的概念已经在前一小节讲完了。这里需要补充的是说明宏定义 INADDR_ANY。这里的意思是使用本机所有可用的IP地址。当然,如果你机器绑定了多个IP地址,你也可以指定使用哪一个。

数据流简易模型(SOCK_STREAM)

我们的例子以电话做的比喻,实际上,socket stream模型不完全类似电话,它至少有以下这些特点:

1、一种持续性的连接。这点跟电话是类似的,也可以想象成流动着液体的水管。一旦断开,这种流动就会中断。

2、 数据包的发送实际上是非连续的。这个世界上有什么事物是真正的线性连续的?呵呵,扯远了,这貌似一个哲学问题。我们仅仅需要知道的是,一个数据包不可能是 无限大的,所以,总是一个小数据包一个小数据包这样的发送的。这一点,又有点像邮包的传递。这些数据包到达与否,到达的先后次序本身是无法保证的,即是 说,是IP协议无法保证的。但是stream形式的TCP协议,在IP之上,做了一定到达和到达顺序的保证。

3、传送管道实际上是非封闭的。要不干嘛叫“网络”-_-!!!。我们之所以能保证数据包的“定点”传送,完全是依靠每个数据包都自带了目的地址信息。

由此可见,虽然socket和sockaddr可以分别创建,并无依赖关系。但是在实际使用的时候,一个socket至少会绑定一个本机的sockaddr,没有自己的“地址信息”,就不能接受到网络上的数据包(至少在TCP协议里面是这样的)。

socket与本机sockaddr的绑定

有时候绑定是系统的任务,特别是当你不需要知道自己的IP地址和所使用的端口号的时候。但是,我们现在是建立服务器,你必须告诉客户端你的连接信息:IP和Port。所以,我们需要指明IP和Port,然后进行绑定。

int bind(int socket, struct sockaddr* localAddress, unsigned int addressLength);

作为C++的程序员,也许你会觉得这个函数很不友好,它似乎更应该写成:

int bind_cpp_style(int socket, const sockaddr& localAddress);

我们需要通过函数原型指明两点:

1、我们仅仅使用sockaddr结构的数据,但并不会对原有的数据进行修改;

2、我们使用的是完整的结构体,而不仅仅是这个结构体的指针。(很显然光用指针是无法说明结构体大小的)

幸运的是,在Linux的实现中,这个函数已经被写为:

#include

/* Give the socket FD the local address ADDR (which is LEN bytes long). */ extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len) __THROW;

看到亲切的const,我们就知道这个指针带入是没有“副作用”的。

监听:listen()

stream流模型形式上是一种“持续性”的连接,这就是要求信息的流动是“可来可去”的。也就是说,stream流的socket除了绑定本机的 sockaddr,还应该拥有对方sockaddr的信息。在listen()中,这“对方的sockaddr”就可以不是某一个特定的 sockaddr。实际上,listen socket的目的是准备被动的接受来自“所有”sockaddr的请求。所以,listen()反而就不能指定某个特定的sockaddr。

int listen(int socket, int queueLimit);

其中第二个参数是等待队列的限制,一般设置在5-20。Linux中实现为:

#include

/* Prepare to accept connections on socket FD.

N connection requests will be queued before further requests are refused. Returns 0 on success, -1 for errors. */ extern int listen (int __fd, int __n) __THROW;

完成了这一步,回到我们的例子,就像是让你小弟在电话机前做好了接电话的准备工作。需要再次强调的是,这些行为仅仅是改变了socket的状态,实际上我想强调的是,为什么这些函数不会造成block(阻塞)的原因。(block的概念以后再解释)

6、创建―通讯 ‖嵌套字

这里的“通讯”加上了引号,是因为实际上所有的socket都有通讯的功能,只是在我们的例子中,之前那个socket只负责listen,而这个socket负责接受信息并echo回去。 我们现看看这个函数:

bool TcpServer::isAccept() {

unsigned int clntAddrLen = sizeof(clntAddr);

if ( (communicationSock = accept(listenSock, (sockaddr*)&clntAddr, &clntAddrLen)) < 0 ) {

return false; } else {

std::cout << \ return true; } }

用accept()创建新的socket

在我们的例子中,communicationSock实际上是用函数accept()创建的。

int accept(int socket, struct sockaddr* clientAddress, unsigned int* addressLength);

在Linux中的实现为:

/* Await a connection on socket FD.

When a connection arrives, open a new socket to communicate with it, set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting peer and *ADDR_LEN to the address's actual length, and return the new socket's descriptor, or -1 for errors.

This function is a cancellation point and therefore not marked with __THROW. */

extern int accept (int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len);

这个函数实际上起着构造socket作用的仅仅只有第一个参数(另外还有一个不在这个函数内表现出来的因素,后面会讨论到),后面两个指针都有副作用,在socket创建后,会将客户端sockaddr的数据以及结构体的大小传回。

当程序调用accept()的时候,程序有可能就停下来等accept()的结果。这就是我们前一小节说到的block(阻塞)。这如同我们调用 std::cin的时候系统会等待输入直到回车一样。accept()是一个有可能引起block的函数。请注意我说的是“有可能”,这是因为 accept()的block与否实际上决定与第一个参数socket的属性。这个文件描述符如果是block的,accept()就block,否则就 不block。默认情况下,socket的属性是“可读可写”,并且,是阻塞的。所以,我们不修改socket属性的时候,accept()是阻塞的。

accept()的另一面connect()

accept()只是在server端被动的等待,它所响应的,是client端connect()函数:

int connect(int socket, struct sockaddr* foreignAddress, unsigned int addressLength);

虽然我们这里不打算详细说明这个client端的函数,但是我们可以看出来,这个函数与之前我们介绍的bind()有几分相似,特别在Linux的实现中:

/* Open a connection on socket FD to peer at ADDR (which LEN bytes long). For connectionless socket types, just set the default address to send to and the only address from which to accept transmissions. Return 0 on success, -1 for errors.

This function is a cancellation point and therefore not marked with __THROW. */

extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);

connect() 也使用了const的sockaddr,只不过是远程电脑上的而非bind()的本机。 accept()在server端表面上是通过listen socket创建了新的socket,实际上,这种行为是在接受对方客户机程序中connect()函数的请求后发生的。综合起看,被创建的新 socket实际上包含了listen socket的信息以及客户端connect()请求中所包含的信息——客户端的sockaddr地址。

新socket与sockaddr的关系

accept()创建的新socket(我们例子中的communicationSock,这里我们简单用newSock来带指)首先包含了listen socket的信息,所以,newSock具有本机sockaddr的信息;其次,因为它响应于client端connect()函数的请求,所以,它还 包含了clinet端sockaddr的信息。

我们说过,stream流形式的TCP协议实际上是建立起一个“可来可去”的通道。用于listen的通道,远程机的目标地址是不确定的;但是 newSock却是有指定的本机地址和远程机地址,所以,这个socket,才是我们真正用于TCP“通讯”的socket。

inet_ntoa()

#include

/* Convert Internet number in IN to ASCII representation. The return value is a pointer to an internal array containing the string. */ extern char *inet_ntoa (struct in_addr __in) __THROW;

对于这个函数,我们可以作为一种,将IP地址,由in_addr结构转换为可读的ASCII

形式的固定用法。

7、接收与发送

现在,我们通过accept()创建了新的socket,也就是我们类中的数据成员communicationSock,现在,我们就可以通过这个socket进行通讯了。

TCP通讯模型

在介绍函数之前,我们应该了解一些事实。TCP的Server/Client模型类似这样: ServApp——ServSock——Internet——ClntSock——ClntApp

当 然,我们这里的socket指的就是用于“通讯”的socket。TCP的server端至少有两个socket,一个用于监听,一个用于通讯;TCP的 server端可以只有一个socket,这个socket同时“插”在server的两个socket上。当然,插上listen socket的目的只是为了创建communication socket,创建完备后,listen是可以关闭的。但是,如果这样,其他的client就无法再连接上server了。 我们这个模型,是client的socket插在server的communication socket上的示意。这两个socket,都拥有完整的本地地址信息以及远程计算机地址信息,所以,这两个socket以及之间的网络实际上形成了一条 形式上“封闭”的管道。数据包只要从一端进来,就能知道出去的目的地,反之亦然。这正是TCP协议,数据流形式抽象化以及实现。因为不再需要指明“出处” 和“去向”,对这样的socket(实际上是S/C上的socket对)的操作,就如同对本地文件描述符的操作一样。但是,尽管我们可以使用read() 和write(),但是,为了完美的控制,我们最好使用recv()和send()。

recv()和send()

int send(int socket, const void* msg, unsigned int msgLength, int flags); int recv(int socket, void* rcvBuffer, unsigned int bufferLength, int flags);

在Linux中的实现为:

#include

/* Send N bytes of BUF to socket FD. Returns the number sent or -1.

This function is a cancellation point and therefore not marked with __THROW. */

extern ssize_t send (int __fd, __const void *__buf, size_t __n, int __flags);

/* Read N bytes into BUF from socket FD. Returns the number read or -1 for errors.

This function is a cancellation point and therefore not marked with __THROW. */

extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);

这两个函数的第一个参数是用于“通讯”的socket,第二个参数是发送或者接收数据的起始点指针,第三个参数是数据长度,第四个参数是控制符号(默认属性设置为0就可以了)。

失败时候传回-1,否则传回实际发送或者接收数据的大小,返回0往往意味着连接断开了。

处理echo行为

void TcpServer::handleEcho() {

const int BUFFERSIZE = 32; char buffer[BUFFERSIZE]; int recvMsgSize; bool goon = true;

while ( goon == true ) {

if ( (recvMsgSize = recv(communicationSock, buffer, BUFFERSIZE, 0)) < 0 ) { throw \ } else if ( recvMsgSize == 0 ) { goon = false; } else {

if ( send(communicationSock, buffer, recvMsgSize, 0) != recvMsgSize ) { throw \ } } }

close(communicationSock); }

本小节最后要讲的函数是close(),它包含在

#include

/* Close the file descriptor FD.

This function is a cancellation point and therefore not marked with __THROW. */

extern int close (int __fd);

这个函数用于关闭一个文件描述符,自然,也就可以用于关闭socket。 下一小节是完整的源代码。默认的监听端口是5000。我们可以通过 $telnet 127.0.0.1 5000

验证在本机运行的echo server程序。

8、本章的完整源代码

//Filename: TcpServerClass.hpp

#ifndef TCPSERVERCLASS_HPP_INCLUDED #define TCPSERVERCLASS_HPP_INCLUDED

#include #include #include #include

class TcpServer { private:

int listenSock; int communicationSock; sockaddr_in servAddr; sockaddr_in clntAddr; public:

TcpServer(int listen_port); bool isAccept(); void handleEcho(); };

#endif // TCPSERVERCLASS_HPP_INCLUDED

//Filename: TcpServerClass.cpp

#include \

TcpServer::TcpServer(int listen_port) {

if ( (listenSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 ) { throw \ }

memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET;

servAddr.sin_addr.s_addr = htonl(INADDR_ANY); servAddr.sin_port = htons(listen_port);

if ( bind(listenSock, (sockaddr*)&servAddr, sizeof(servAddr)) < 0 ) { throw \ }

if ( listen(listenSock, 10) < 0 ) { throw \ } }

bool TcpServer::isAccept() {

unsigned int clntAddrLen = sizeof(clntAddr);

if ( (communicationSock = accept(listenSock, (sockaddr*)&clntAddr, &clntAddrLen)) < 0 ) {

return false; } else {

std::cout << \ return true; } }

void TcpServer::handleEcho() {

const int BUFFERSIZE = 32; char buffer[BUFFERSIZE]; int recvMsgSize; bool goon = true;

while ( goon == true ) {

if ( (recvMsgSize = recv(communicationSock, buffer, BUFFERSIZE, 0)) < 0 ) { throw \ } else if ( recvMsgSize == 0 ) { goon = false; } else {

if ( send(communicationSock, buffer, recvMsgSize, 0) != recvMsgSize ) { throw \ } } }

close(communicationSock); }

演示程序:

//Filename: main.cpp

//Tcp Server C++ style, single work

#include

#include \

int echo_server(int argc, char* argv[]);

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

int mainRtn = 0; try {

mainRtn = echo_server(argc, argv); }

catch ( const char* s ) { perror(s);

exit(EXIT_FAILURE); }

return mainRtn; }

int echo_server(int argc, char* argv[]) {

int port;

if ( argc == 2 ) { port = atoi(argv[1]); } else {

port = 5000; }

TcpServer myServ(port);

while ( true ) {

if ( myServ.isAccept() == true ) { myServ.handleEcho(); } }

return 0; }

[精华] socket编程原理

http://www.chinaunix.net/jh/4/198859.html

socket编程原理 socket编程原理 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.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 服务方式

在网络分层结构中,各层之间是严格单向依赖的,各层次的分工和协作集中体现在相量纪纪之间的界面上。―服务‖是描述相量纪纪之间关系的抽 象概念,即网络中各层向紧

本文来源:https://www.bwwdw.com/article/l16x.html

Top