海阔天空

I'm on my way!
随笔 - 17, 文章 - 69, 评论 - 21, 引用 - 0
数据加载中……

2009年11月17日

经典的Linux Socket 编程 示例代码 (下)


摘自:http://fanqiang.chinaunix.net/a4/b7/20010810/1200001101.html


在例程main()函数快要结束时,我们看到,在服务器接受了客户机的连接请求后,将为其创建子进程,并在子进程中执行代理服务程序do_proxy()。
-----------------------------------------------------------------/****************************************************************
function:    do_proxy
description:  does the actual work of virtually connecting a client to the telnet service on the          isolated host.
arguments:   usersockfd socket to which the client is connected. return value: none.
calls:     none.
globals:     reads hostaddr.
****************************************************************/
void do_proxy (usersockfd)
int usersockfd;
{
int isosockfd;
fd_set rdfdset;
int connstat;
int iolen;
char buf[2048];
/* open a socket to connect to the isolated host */
if ((isosockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
errorout("failed to create socket to host");
/* attempt a connection */
connstat = connect(isosockfd,(struct sockaddr *) &hostaddr, sizeof(hostaddr));
switch (connstat) {
case 0:
break;
case ETIMEDOUT:
case ECONNREFUSED:
case ENETUNREACH:
strcpy(buf,sys_myerrlist[errno]);
strcat(buf,"\r\n");
write(usersockfd,buf,strlen(buf));
close(usersockfd);
exit(1);
/* die peacefully if we can't establish a connection */
break;
default:
errorout("failed to connect to host");
}
/* now we're connected, serve fall into the data echo loop */
while (1) {
/* Select for readability on either of our two sockets */
FD_ZERO(&rdfdset);
FD_SET(usersockfd,&rdfdset);
FD_SET(isosockfd,&rdfdset);
if (select(FD_SETSIZE,&rdfdset,NULL,NULL,NULL) < 0)
errorout("select failed");
/* is the client sending data? */
if (FD_ISSET(usersockfd,&rdfdset)) {
if ((iolen = read(usersockfd,buf,sizeof(buf))) <= 0)
break; /* zero length means the client disconnected */
rite(isosockfd,buf,iolen);
/* copy to host -- blocking semantics */
}
/* is the host sending data? */
if (FD_ISSET(isosockfd,&rdfdset)) {
f ((iolen = read(isosockfd,buf,sizeof(buf))) <= 0)
break; /* zero length means the host disconnected */
rite(usersockfd,buf,iolen);
/* copy to client -- blocking semantics */
}
}
/* we're done with the sockets */
close(isosockfd);
lose(usersockfd);
}
-----------------------------------------------------------------
在 我们这段代理服务器例程中,真正连接用户主机和远端主机的一段操作,就是由这个do_proxy()函数来完成的。回想一下我们一开始对这段 proxy程序用法的介绍。先将我们的proxy与远端主机绑定,然后用户通过proxy的绑定端口与远端主机建立连接。而在main()函数中,我们的 proxy由一段服务器程序与用户主机建立了连接,而在这个do_proxy()函数中,proxy将与远端主机的相应服务端口(由用户在命令行参数中指 定)建立连接,并负责传递用户主机和远端主机之间交换的数据。
由于要和远端主机建立连接,所以我们看到do_proxy()函数的前半部分实际上相当于一段标准的客户机程序。首先创建一个新的套接字描述符 isosockfd,然后调用函数connect()与远端主机之间建立连接。函数connect()的定义为:
-----------------------------------------------------------------
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
-----------------------------------------------------------------
参数sockfd是调用函数socket()返回的套接字描述符,参数servaddr指向远程服务器的套接字地址结构,参数addrlen指定这个 套接字地址结构的长度。函数connect()执行成功时返回"0",如果执行失败则返回"-1",并将全局变量errno设置为相应的错误类型。在例程 中的switch()函数调用中对以下三种出错类型进行了处理: ETIMEDOUT、ECONNREFUSED和ENETUNREACH。这三个出错类型的意思分别为:ETIMEDOUT代表超时,产生这种情况的原因 有很多,最常见的是服务器忙,无法应答客户机的连接请求;ECONNREFUSED代表连接拒绝,即服务器端没有准备好的倾听套接字,或是没有对倾听套接 字的状态进行监听;ENETUNREACH表示网络不可达。
在本例中,connect()函数的第二个参数servaddr是全局变量hostaddr,其中存储着函数parse_args()转换好的命令行 参数。如果连接建立失败,在例程中就调用我们自定义的函数errorout()输出信息"failed to connect to host"。errorout()函数的定义为:
-----------------------------------------------------------------
/****************************************************************
function:  errorout
description: displays an error message on the console and kills the current process.
arguments:  msg -- message to be displayed.
return value: none -- does not return.
calls:    none.
globals:   none.
****************************************************************/
void errorout (msg)
char *msg;
{
FILE *console;
console = fopen("/dev/console","a");
fprintf(console,"proxyd: %s\r\n",msg);
fclose(console);
exit(1);
}
-----------------------------------------------------------------
do_proxy()函数的后半部分是通过proxy建立用户主机与远端主机之间的连接。我们既有proxy与用户主机连接的套接字 (do_proxy()函数的参数usersockfd),又有proxy与远端主机连接的套接字isosockfd,那么最简单直接的通信建立方式就是 从一个套接字读,然后直接写到另一个套接字去。如:
-----------------------------------------------------------------
int n;
char buf[2048];
while((n=read(usersockfd, buf, sizeof(buf))>0)
if(write(isosockfd, buf, n)!=n)
err_sys("write wrror\n");
-----------------------------------------------------------------
这种形式的阻塞I/O在单向数据传递的时候是非常有效的,但是在我们的proxy操作中是要求用户主机和远端主机双向通信的,这样就要求我们对两个套 接字描述符既能够读由能够写。如果还是采用这种方式的阻塞I/O的话,很有可能长时间阻塞在一个描述符上。因此例程在处理这个问题的时候调用了 select()函数,这个函数允许我们执行I/O多路转接。其具体含义就是select()函数可以构造一个表,在这个表中包含了我们所有要用到的文件 描述符。然后我们可以调用一个函数,这个函数可以检测这些文件描述符的状态,当某个(我们指定的)文件描述符准备好进行I/O操作时,此函数就返回,告知 进程哪个文件描述符已经可以执行I/O操作了。这样就避免了长时间的阻塞。
还有一个函数poll()可以实现I/O多路转接,由于在例程中调用的是select(),我们就只对select()进行一下比较详细的介绍。select()系列函数的详细描述为:
-----------------------------------------------------------------
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int n, fd_set *readfds, fd_set *writefds, fd_est *exceptfds, struct timeval *timeout);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
-----------------------------------------------------------------
select()函数将创建一个我们所关心的文件描述符表,它的参数将在内核中为这些文件描述符设置我们所关心的条件,例如是否是可读、是否可写以及 是否异常,而且在参数中还可以设置我们希望等待的最大时间。在select()成功执行时,它将返回目前已经准备好的描述符数量,同时内核可以告诉我们各 个描述符的状态信息。如果超时,则返回"0",如果出错,则函数返回"-1",并同时设置errno为相应的值。
select()的最后一个参数timeout将设置等待时间。其中结构timeval是在文件<bits/time.h>中定义的。
-----------------------------------------------------------------
struct timeval
{
__time_t tv_sec; /* Seconds */
__time_t tv_usec; /* Microseconds */
};
-----------------------------------------------------------------
参数timeout的设置有三种情况。象例程中这样timeout==NULL时,这表示用户希望永远等待,直到我们指定的文件描述符中的一个已准备 好,或者是捕捉到一个信号。如果是由于捕捉到信号而中断了这个无限期的等待过程的话,select()将返回"-1",同时设置errno的值为 EINTR。
如果timeout->tv_sec==0&&timeout->tv_usec==0,那么这表示完全不等待。 Select()测试了所有指定文件描述符后立即返回。这是得到多个描述符状态而不阻塞select()函数的轮询方法。
如果timeout->tv_sec!=0||timeout->tv_usec!=0,那么这两个参数的值即为我们希望函数等待的时 间。其中tv_sec设置时间单位为秒,tv_usec设置时间单位为微秒。如果在超时的时候,在我们指定的所有文件描述符里面仍然没有任何一个准备好的 话,则select()将返回"0"。
中间三个参数的数据类型是fd_set,它的意思是文件描述符集,而readfds, writefds和exceptfds则分别是指向文件描述符集的指针,他们分别描述了我们所关心的可读、可写以及状态异常的各个文件描述符。之所以我们 称select()可以创建一个文件描述符"表",那个所谓的表就是由这三个参数指向的数据结构组成的。其具体结构如图1所示。其中在每个set_fd数 据类型中都为我们关心的所有文件描述符保留了一位。所以在监测文件描述符状态的时候,就在这些set_fd数据结构中查询相关的位。
第一个参数n用来说明到底需要遍历多少个描述符位。n的值一般是这样设置的,从我们关心的所有文件描述符中选出最大值再加1。例如我们设置的所有文件 描述符中最大的为6,那么将n设置为7,则系统在检测描述符状态的时候,就只用遍历前7位(fd0~fd6)的状态。不过如果不想这样麻烦的话,我们可以 象例程中那样将n的值直接设置为FD_SETSIZE。这是系统中设定的最大文件描述符个数,不同的系统这个值也不相同,一般是256或是1024。这样 在检测描述符状态的时候,函数将遍历所有的描述符位。
在调用select()函数实现多路I/O转接时,首先我们要声明一个新的文件描述符集,就象例程中这样:
fd_set rdfdset;
然后调用FD_ZERO()清空此文件描述符集的所有位,以免下面检测描述符位的时候返回错误结果:
FD_ZERO(&rdfdset);
然后调用FD_SET()在文件描述符集中设置我们关心的位。在本例中,我们关心的就是分别与用户主机和远端主机连接的两个套接字描述符,所以执行这样的语句:
FD_SET(usersockfd,&rdfdset);
FD_SET(isosockfd,&rdfdset);
然后调用select()返回描述符状态,此时描述符状态被存储进描述符集,也就是set_fd数据结构中。在图1中我们看到所有的描述符位状态都是 "0",在select()返回后,例如fd0可读,则在readfds描述符集中fd0对应的位上将状态标志设置为"1",如果fd1可写,则 writefds描述符集中fd1对应的位上将状态标志设置为"1",状态异常的情况也也与此相同。在本例中,我们只关心两个套接字描述符是否可写,因此 执行这样的select()函数:
select(FD_SETSIZE,&rdfdset,NULL,NULL,NULL)
那么在select()返回后怎样检测set_fd数据结构中描述符位的状态呢?这就要调用函数FD_ISSET(),如果对应文件描述符的状态为"已准备好"(即描述符位为"1"),则FD_ISSET()返回"1",否则返回"0"。
-----------------------------------------------------------------
if (FD_ISSET(usersockfd,&rdfdset)) {
if ((iolen = read(usersockfd,buf,sizeof(buf))) <= 0)
break; /* zero length means the host disconnected */
write(isosockfd,buf,iolen);
-----------------------------------------------------------------
这一段代码就实现从套接字usersockfd(用户主机)到套接字isosockfd(远端主机)的无阻塞传输。而下一段代码实现反方向的无阻塞传输:
-----------------------------------------------------------------
if (FD_ISSET(isosockfd,&rdfdset)) {
if ((iolen = read(isosockfd,buf,sizeof(buf))) <= 0)
break; /* zero length means the host disconnected */
write(usersockfd,buf,iolen);
-----------------------------------------------------------------
这样就通过proxy实现了用户主机与远端主机之间的通信。
对这段proxy代码我只是写了一些自己的理解,大多数是一些函数的用法,这些都是linux网络编程中一些最基础的知识,如果有不对的地方,还请各位大虾批评指正。





posted @ 2009-11-17 21:14 石头@ 阅读(745) | 评论 (0)编辑 收藏

经典的Linux Socket 编程 示例代码 (上)

http://fanqiang.chinaunix.net/a4/b7/20010810/1200001101.html


Linux是一个可靠性非常高的操作系统,但是所有用过Linux的朋友都会感 觉到, Linux和Windows这样的"傻瓜"操作系统(这里丝毫没有贬低Windows的意思,相反这应该是Windows的优点)相比,后者无疑在易操作 性上更胜一筹。但是为什么又有那么多的爱好者钟情于Linux呢,当然自由是最吸引人的一点,另外Linux强大的功能也是一个非常重要的原因,尤其是 Linux强大的网络功能更是引人注目。放眼今天的WAP业务、银行网络业务和曾经红透半边天的电子商务,都越来越倚重基于Linux的解决方案。因此 Linux网络编程是非常重要的,而且当我们一接触到Linux网络编程,我们就会发现这是一件非常有意思的事情,因为以前一些关于网络通信概念似是而非 的地方,在这一段段代码面前马上就豁然开朗了。在刚开始学习编程的时候总是让人感觉有点理不清头绪,不过只要多读几段代码,很快我们就能体会到其中的乐趣 了。下面我就从一段Proxy源代码开始,谈谈如何进行Linux网络编程。

   首先声明,这段源代码不是我编写的,让我们感谢这位名叫Carl Harris的大虾,是他编写了这段代码并将其散播到网上供大家学习讨论。这段代码虽然只是描述了最简单的proxy操作,但它的确是经典,它不仅清晰地 描述了客户机/服务器系统的概念,而且几乎包括了Linux网络编程的方方面面,非常适合Linux网络编程的初学者学习。
这段Proxy程序的用法是这样的,我们可以使用这个proxy登录其它主机的服务端口。假如编译后生成了名为Proxy的可执行文件,那么命令及其参数的描述为:
./Proxy <proxy_port> <remote_host> <service_port>
其中参数proxy_port是指由我们指定的代理服务器端口。参数remote_host是指我们希望连接的远程主机的主机名,IP地址也同样有 效。这个主机名在网络上应该是唯一的,如果您不确定的话,可以在远程主机上使用uname -n命令查看一下。参数service_port是远程主机可提供的服务名,也可直接键入服务对应的端口号。这个命令的相应操作是将代理服务器的 proxy_port端口绑定到remote_host的service_port端口。然后我们就可以通过代理服务器的proxy_port端口访问 remote_host了。例如一台计算机,网络主机名是legends,IP地址为10.10.8.221,如果在我的计算机上执行:
[root@lee /root]#./proxy 8000 legends telnet
那么我们就可以通过下面这条命令访问legends的telnet端口。
-----------------------------------------------------------------
[root@lee /root]#telnet legends 8000
Trying 10.10.8.221...
Connected to legends(10.10.8.221).
Escape character is '^]'

Red Hat Linux release 6.2(Zoot)
Kernel 2.2.14-5.0 on an i686
Login:
-----------------------------------------------------------------
上面的绑定操作也可以使用下面的命令:
[root@lee /root]#./proxy 8000 10.10.8.221 23
23是telnet服务的标准端口号,其它服务的对应端口号我们可以在/etc/services中查看。

下面我就从这段代码出发谈谈我对Linux网络编程的一些粗浅的认识,不对的地方还请各位大虾多多批评指正。

◆main()函数
-----------------------------------------------------------------
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <netdb.h>
#define TCP_PROTO   "tcp"
int proxy_port;    /* port to listen for proxy connections on */
struct sockaddr_in hostaddr;   /* host addr assembled from gethostbyname() */
extern int errno;   /* defined by libc.a */
extern char *sys_myerrlist[];
void parse_args (int argc, char **argv);
void daemonize (int servfd);
void do_proxy (int usersockfd);
void reap_status (void);
void errorout (char *msg);
/*This is my modification.
I'll tell you why we must do this later*/
typedef void Signal(int);
/****************************************************************
function:    main
description:   Main level driver. After daemonizing the process, a socket is opened to listen for         connections on the proxy port, connections are accepted and children are spawned to         handle each new connection.
arguments:    argc,argv you know what those are.
return value:  none.
calls:      parse_args, do_proxy.
globals:     reads proxy_port.
****************************************************************/
main (argc,argv)
int argc;
char **argv;
{
int clilen;
int childpid;
int sockfd, newsockfd;
struct sockaddr_in servaddr, cliaddr;
parse_args(argc,argv);
/* prepare an address struct to listen for connections */
bzero((char *) &servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = proxy_port;
/* get a socket... */
if ((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0) {
fputs("failed to create server socket\r\n",stderr);
exit(1);
}
/* ...and bind our address and port to it */
if   (bind(sockfd,(struct sockaddr_in *) &servaddr,sizeof(servaddr)) < 0) {
fputs("faild to bind server socket to specified port\r\n",stderr);
exit(1);
}
/* get ready to accept with at most 5 clients waiting to connect */
listen(sockfd,5);
/* turn ourselves into a daemon */
daemonize(sockfd);
/* fall into a loop to accept new connections and spawn children */
while (1) {
/* accept the next connection */
clilen = sizeof(cliaddr);
newsockfd = accept(sockfd, (struct sockaddr_in *) &cliaddr, &clilen);
if (newsockfd < 0 && errno == EINTR)
continue;
/* a signal might interrupt our accept() call */
else if (newsockfd < 0)
/* something quite amiss -- kill the server */
errorout("failed to accept connection");
/* fork a child to handle this connection */
if ((childpid = fork()) == 0) {
close(sockfd);
do_proxy(newsockfd);
exit(0);
}
/* if fork() failed, the connection is silently dropped -- oops! */
lose(newsockfd);
}
}
-----------------------------------------------------------------
上面就是Proxy源代码的主程序部分,也许您在网上也曾经看到过这段代码,不过细心的您会发现在上面这段代码中我修改了两个地方,都是在预编译部分。一个地方是在定义外部字符型指针数组时,我将原代码中的
extern char *sys_errlist[];
修改为
extern char *sys_myerrlist[];原因是在我的Linux环境下头文件"stdio.h"已经对sys_errlist[]进行了如下定义:
extern __const char *__const sys_errlist[];
也许Carl Harris在94年编写这段代码时系统还没有定义sys_errlist[],不过现在我们不修改一下的话,编译时系统就会告诉我们sys_errlist发生了定义冲突。
另外我添加了一个函数类型定义:
typedef void Sigfunc(int);
具体原因我将在后面向大家解释。

套接字和套接字地址结构定义

这段主程序是一段典型的服务器程序。网络通讯最重要的就是套接字的使用,在程序的一开始就对套接字描述符sockfd和newsockfd进行了定义。 接下来定义客户机/服务器的套接字地址结构cliaddr和servaddr,存储客户机/服务器的有关通信信息。然后调用parse_args (argc,argv)函数处理命令参数。关于这个parse_args()函数我们待会儿再做介绍。

创建通信套接字

  下面就是建立一个服务器的详细过程。服务器程序的第一个操作是创建一个套接字。这是通过调用函数socket()来实现的。socket()函数的具体描述为:
-----------------------------------------------------------------
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-----------------------------------------------------------------
参数domain指定套接字使用的协议族,AF_INET表示使用TCP/IP协议族,AF_UNIX表示使用Unix协议族,AF_ISO表示套接 字使用ISO协议族。type指定套接字类型,一般的面向连接通信类型(如TCP)设置为SOCK_STREAM,当套接字为数据报类型时,type应设 置为SOCK_DGRAM,如果是可以直接访问IP协议的原始套接字则type应设置为SOCK_RAW。参数protocol一般设置为"0",表示使 用默认协议。当socket()函数成功执行时,返回一个标志这个套接字的描述符,如果出错则返回"-1",并设置errno为相应的错误类型。

设置服务器套接字地址结构

在通常情况下,首先要将描述服务器信息的套接字地址结构清零,然后在地址结构中填入相应的内容,准备接受客户机送来的连接建立请求。这个清零操作可以用 多种字节处理函数来实现,例如bzero()、bcopy()、memset()、memcpy()等,以字母"b"开始的两个函数是和BSD系统兼容 的,而后面两个是ANSI C提供的函数。这段代码中使用的bzero()其描述为:
void bzero(void *s, int n);
函数的具体操作是将参数s指定的内存的前n个字节清零。memset()同样也很常用,其描述为:
void *memset(void *s, int c, size_t n);
具体操作是将参数s指定的内存区域的前n个字节设置为参数c的内容。
下一步就是在已经清零的服务器套接字地址结构中填入相应的内容。Linux系统的套接字是一个通用的网络编程接口,它应该支持多种网络通信协议,每一 种协议都使用专门为自己定义的套接字地址结构(例如TCP/IP网络的套接字地址结构就是struct sockaddr_in)。不过为了保持套接字函数调用参数的一致性,Linux系统还定义了一种通用的套接字地址结构:
-----------------------------------------------------------------
<linux/socket.h>
struct sockaddr
{
unsigned short sa_family; /* address type */
char sa_data[14]; /* protocol address */
}
-----------------------------------------------------------------
其中sa_family意指套接字使用的协议族地址类型,对于我们的TCP/IP网络,其值应该是AF_INET,sa_data中存储具体的协议地 址,不同的协议族有不同的地址格式。这个通用的套接字地址结构一般不用做定义具体的实例,但是常用做套接字地址结构的强制类型转换,如我们经常可以看到这 样的用法:
bind(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))
用于TCP/IP协议族的套接字地址结构是sockaddr_in,其定义为:
-----------------------------------------------------------------
<linux/in.h>
struct in_addr
{
__u32 s_addr;
};
struct sochaddr_in
{
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
/*This part has not been taken into use yet*/
nsigned char_ _ pad[_ _ SOCK_SIZE__- sizeof(short int) -sizeof(unsigned short int) -       sizeof(struct in_addr)];
};
#define sin_zero_ - pad
-----------------------------------------------------------------
其中sin_zero成员并未使用,它是为了和通用套接字地址struct sockaddr兼容而特意引入的。在编程时,一般都通过bzero()或是memset()将其置零。其他成员的设置一般是这样的:
servaddr.sin_family = AF_INET;
表示套接字使用TCP/IP协议族。
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
设置服务器套接字的IP地址为特殊值INADDR_ANY,这表示服务器愿意接收来自任何网络设备接口的客户机连接。htonl()函数的意思是将主机顺序的字节转换成网络顺序的字节。
servaddr.sin_port = htons(PORT);
设置通信端口号,PORT应该是我们已经定义好的。在本例中servaddr.sin_port = proxy_port;这是表示端口号是函数的返回值proxy_port。
另外需要说明的一点是,在本例中,我们并没有看到在预编译部分中包含有<linux/socket.h>和< linux/in.h>这两个头文件,那是因为这两个头文件已经分别被包含在<sys/types.h>和< sys/types.h>中了,而且后面这两个头文件是与平台无关的,所以在网络通信中一般都使用这两个头文件。

服务器公开地址

  如果服务器要接受客户机的连接请求,那么它必须先要在整个网络上公开自己的地址。在设置了服务器的套接字地址结构之后,可以通过调用函数bind()绑定服务器的地址和套接字来完成公开地址的操作。函数bind()的详细描述为:
-----------------------------------------------------------------
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *addr, int addrlen);
-----------------------------------------------------------------
参数sockfd是我们通过调用socket()创建的套接字描述符。参数addr是本机地址,参数addrlen是套接字地址结构的长度。函数执行成功时返回"0",否则返回"-1",并设置errno变量为EADDRINUAER。
如果是服务器调用bind()函数,如果设置了套接字的IP地址为某个本地IP地址,那么这表示服务器只接受来自于这个IP地址的特定主机发出的连接 请求。不过一般情况下都是将IP地址设置为INADDR_ANY,以便接受所有网络设备接口送来的连接请求。
客户机一般是不会调用bind()函数的,因为客户机在连接时不用指定自己的套接字地址端口号,系统会自动为客户机选择一个未用端口号,并且用本地 IP地址自动填充客户机套接字地址结构中的相应项。但是在某些特定的情况下客户机需要使用特定的端口号,例如Linux中的rlogin命令就要求使用保 留端口号,而系统是不能为客户机自动分配保留端口号的,这就需要调用bind()来绑定一个保留端口号了。不过在一些特殊的环境下,这样绑定特定端口号也 会带来一些负面影响,如在HTTP服务器进入TIME_WAIT状态后,客户机如果要求再次与服务器建立连接,则服务器会拒绝这一连接请求。如果客户机最 后进入TIME_WAIT状态,则马上再次执行bind()函数时会返回出错信息"-1",原因是系统会认为同时有两次连接绑定同一个端口。

转换Listening套接字

接下来,服务器需要将我们刚才与IP地址和端口号完成绑定的套接字转换成倾听listening套接字。只有服务器程序才需要执行这一步操作。我们通过调用函数listen()实现这一操作。listen()的详细描述为:
-----------------------------------------------------------------
#include <sys/socket.h>
int listen(int sockfd, int backlog);
-----------------------------------------------------------------
参数sockfd指定我们要求转换的套接字描述符,参数backlog设置请求队列的最大长度。函数listen()主要完成以下操作。
首先是将套接字转换成倾听套接字。因为函数socket()创建的套接字都是主动套接字,所以客户机可以通过调用函数connect()来使用这样的 套接字主动和服务器建立连接。而服务器的情况恰恰相反,服务器需要通过套接字接收客户机的连接请求,这就需要一个"被动"套接字。listen()就可将 一个尚未连接的主动套接字转换成为这样的"被动"套接字,也就是倾听套接字。在执行了listen()函数之后,服务器的TCP就由CLOSED变成 LISTEN状态了。
另外listen()可以设置连接请求队列的最大长度。虽然参数backlog的用法非常简单,只是一个简单的整数。但搞清楚请求队列的含义对理解TCP 协议的通信过程建立非常重要。TCP协议为每个倾听套接字实际上维护两个队列,一个是未完成连接队列,这个队列中的成员都是未完成3次握手的连接;另一个 是完成连接队列,这个队列中的成员都是虽然已经完成了3次握手,但是还未被服务器调用accept()接收的连接。参数backlog实际上指定的是这个 倾听套接字完成连接队列的最大长度。在本例中我们是这样用的:listen(sockfd,5);表示完成连接队列的最大长度为5。

接收连接

接下来我们在主程序中看到通过名为daemonize()的自定义函数创建一个守护进程,关于这个daemonize()以及守护进程的相关概念,我 们等一会儿再做详细介绍。然后服务器程序进入一个无条件循环,用于监听接收客户机的连接请求。在此过程中如果有客户机调用connect()请求连接,那 么函数accept()可以从倾听套接字的完成连接队列中接受一个连接请求。如果完成连接队列为空,这个进程就睡眠。accept()的详细描述为:
-----------------------------------------------------------------
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, int *addrlen);
-----------------------------------------------------------------
参数sockfd是我们转换成功的倾听套接字描述符;参数addr是一个指向套接字地址结构的指针,参数addrlen为一个整型指针。当函数成功执 行时,返回3个结果,函数返回一个新的套接字描述符,服务器可以通过这个新的套接字描述符和客户机进行通信。参数addr所指向的套接字地址结构中将存放 客户机的相关信息,addrlen指针将描述前述套接字地址结构的长度。在通常情况下服务器对这些信息不是很感兴趣,因此我们经常可以看到一些源代码中将 accept()函数的后两个参数都设置为NULL。不过在这段proxy源代码中需要用到有关的客户机信息,因此我们看到通过执行
newsockfd = accept(sockfd, (struct sockaddr_in *) &cliaddr, &clilen);
将客户机的详细信息存放在地址结构cliaddr中。而proxy就通过套接字newsockfd与客户机进行通信。值得注意的是这个返回的套接字描 述符与我们转换的倾听套接字是不同的。在一段服务器程序中,可以始终只用一个倾听套接字来接收多个客户机的连接请求;而如果我们要和客户机建立一个实际的 连接的话,对每一个请求我们都需要调用accept()返回一个新的套接字。当服务器处理完毕客户机的请求后,一定要将相应的套接字关闭;如果整个服务器 程序将要结束,那么一定要将倾听套接字关闭。
如果accept()函数执行失败,则返回"-1",如果accept()函数阻塞等待客户机调用connect()建立连接,进程在此时恰好捕捉到 信号,那么函数在返回"-1"的同时将变量errno的值设置为EINTR。这和accept()函数执行失败是有区别的。因此我们在代码中可以看到这样 的语句:
-----------------------------------------------------------------
if (newsockfd < 0 && errno == EINTR)
continue;
/* a signal might interrupt our accept() call */
else if (newsockfd < 0)
/* something quite amiss -- kill the server */
errorout("failed to accept connection");
-----------------------------------------------------------------
可以看出程序在处理这两种情况时操作是完全不同的,同样是accept()返回"-1",如果有errno == EINTR,那么系统将再次调用accept()接受连接请求,否则服务器进程将直接结束。

posted @ 2009-11-17 21:13 石头@ 阅读(2371) | 评论 (0)编辑 收藏