📄 50.htm
字号:
为SOCK_DGRAM,如果是可以直接访问IP协议的原始套接字则type应设置为SOCK_RAW。参 <br>
数protocol一般设置为"0",表示使用默认协议。当socket()函数成功执行时,返回一个 <br>
标志这个套接字的描述符,如果出错则返回"-1",并设置errno为相应的错误类型。 <br>
设置服务器套接字地址结构 <br>
在通常情况下,首先要将描述服务器信息的套接字地址结构清零,然后在地址结构 <br>
中填入相应的内容,准备接受客户机送来的连接建立请求。这个清零操作可以用多种字 <br>
节处理函数来实现,例如bzero()、bcopy()、memset()、memcpy()等,以字母"b"开始的 <br>
两个函数是和BSD系统兼容的,而后面两个是ANSI C提供的函数。这段代码中使用的bze <br>
ro()其描述为: <br>
void bzero(void *s, int n); <br>
函数的具体操作是将参数s指定的内存的前n个字节清零。memset()同样也很常用, <br>
其描述为: <br>
void *memset(void *s, int c, size_t n); <br>
具体操作是将参数s指定的内存区域的前n个字节设置为参数c的内容。 <br>
下一步就是在已经清零的服务器套接字地址结构中填入相应的内容。Linux系统的套 <br>
接字是一个通用的网络编程接口,它应该支持多种网络通信协议,每一种协议都使用专 <br>
门为自己定义的套接字地址结构(例如TCP/IP网络的套接字地址结构就是struct socka <br>
ddr_in)。不过为了保持套接字函数调用参数的一致性,Linux系统还定义了一种通用的 <br>
套接字地址结构: <br>
----------------------------------------------------------------- <br>
<linux/socket.h> <br>
struct sockaddr <br>
{ <br>
unsigned short sa_family; /* address type */ <br>
char sa_data[14]; /* protocol address */ <br>
} <br>
----------------------------------------------------------------- <br>
其中sa_family意指套接字使用的协议族地址类型,对于我们的TCP/IP网络,其值应 <br>
该是AF_INET,sa_data中存储具体的协议地址,不同的协议族有不同的地址格式。这个 <br>
通用的套接字地址结构一般不用做定义具体的实例,但是常用做套接字地址结构的强制 <br>
类型转换,如我们经常可以看到这样的用法: <br>
bind(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr)) <br>
用于TCP/IP协议族的套接字地址结构是sockaddr_in,其定义为: <br>
----------------------------------------------------------------- <br>
<linux/in.h> <br>
struct in_addr <br>
{ <br>
__u32 s_addr; <br>
}; <br>
struct sochaddr_in <br>
{ <br>
short int sin_family; <br>
unsigned short int sin_port; <br>
struct in_addr sin_addr; <br>
/*This part has not been taken into use yet*/ <br>
nsigned char_ _ pad[_ _ SOCK_SIZE__- sizeof(short int) -sizeof(unsigne <br>
d short int) - sizeof(struct in_addr)]; <br>
}; <br>
#define sin_zero_ - pad <br>
----------------------------------------------------------------- <br>
其中sin_zero成员并未使用,它是为了和通用套接字地址struct sockaddr兼容而特 <br>
意引入的。在编程时,一般都通过bzero()或是memset()将其置零。其他成员的设置一 <br>
般是这样的: <br>
servaddr.sin_family = AF_INET; <br>
表示套接字使用TCP/IP协议族。 <br>
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); <br>
设置服务器套接字的IP地址为特殊值INADDR_ANY,这表示服务器愿意接收来自任何 <br>
网络设备接口的客户机连接。htonl()函数的意思是将主机顺序的字节转换成网络顺序的 <br>
字节。 <br>
servaddr.sin_port = htons(PORT); <br>
设置通信端口号,PORT应该是我们已经定义好的。在本例中servaddr.sin_port = <br>
proxy_port;这是表示端口号是函数的返回值proxy_port。 <br>
另外需要说明的一点是,在本例中,我们并没有看到在预编译部分中包含有<linux <br>
/socket.h>和<linux/in.h>这两个头文件,那是因为这两个头文件已经分别被包含在<s <br>
ys/types.h>和<sys/types.h>中了,而且后面这两个头文件是与平台无关的,所以在网 <br>
络通信中一般都使用这两个头文件。 <br>
服务器公开地址 <br>
如果服务器要接受客户机的连接请求,那么它必须先要在整个网络上公开自己的地 <br>
址。在设置了服务器的套接字地址结构之后,可以通过调用函数bind()绑定服务器的地 <br>
址和套接字来完成公开地址的操作。函数bind()的详细描述为: <br>
----------------------------------------------------------------- <br>
#include <sys/types.h> <br>
#include <sys/socket.h> <br>
int bind(int sockfd, struct sockaddr *addr, int addrlen); <br>
----------------------------------------------------------------- <br>
参数sockfd是我们通过调用socket()创建的套接字描述符。参数addr是本机地址, <br>
参数addrlen是套接字地址结构的长度。函数执行成功时返回"0",否则返回"-1",并设置 <br>
errno变量为EADDRINUAER。 <br>
如果是服务器调用bind()函数,如果设置了套接字的IP地址为某个本地IP地址,那 <br>
么这表示服务器只接受来自于这个IP地址的特定主机发出的连接请求。不过一般情况下 <br>
都是将IP地址设置为INADDR_ANY,以便接受所有网络设备接口送来的连接请求。 <br>
客户机一般是不会调用bind()函数的,因为客户机在连接时不用指定自己的套接字 <br>
地址端口号,系统会自动为客户机选择一个未用端口号,并且用本地IP地址自动填充客 <br>
户机套接字地址结构中的相应项。但是在某些特定的情况下客户机需要使用特定的端口 <br>
号,例如Linux中的rlogin命令就要求使用保留端口号,而系统是不能为客户机自动分配 <br>
保留端口号的,这就需要调用bind()来绑定一个保留端口号了。不过在一些特殊的环境 <br>
下,这样绑定特定端口号也会带来一些负面影响,如在HTTP服务器进入TIME_WAIT状态后 <br>
,客户机如果要求再次与服务器建立连接,则服务器会拒绝这一连接请求。如果客户机 <br>
最后进入TIME_WAIT状态,则马上再次执行bind()函数时会返回出错信息"-1",原因是 <br>
系统会认为同时有两次连接绑定同一个端口。 <br>
转换Listening套接字 <br>
接下来,服务器需要将我们刚才与IP地址和端口号完成绑定的套接字转换成倾听li <br>
stening套接字。只有服务器程序才需要执行这一步操作。我们通过调用函数listen()实 <br>
现这一操作。listen()的详细描述为: <br>
----------------------------------------------------------------- <br>
#include <sys/socket.h> <br>
int listen(int sockfd, int backlog); <br>
----------------------------------------------------------------- <br>
参数sockfd指定我们要求转换的套接字描述符,参数backlog设置请求队列的最大长 <br>
度。函数listen()主要完成以下操作。 <br>
首先是将套接字转换成倾听套接字。因为函数socket()创建的套接字都是主动套接 <br>
字,所以客户机可以通过调用函数connect()来使用这样的套接字主动和服务器建立连接 <br>
。而服务器的情况恰恰相反,服务器需要通过套接字接收客户机的连接请求,这就需要 <br>
一个"被动"套接字。listen()就可将一个尚未连接的主动套接字转换成为这样的"被动" <br>
套接字,也就是倾听套接字。在执行了listen()函数之后,服务器的TCP就由CLOSED变成 <br>
LISTEN状态了。 <br>
另外listen()可以设置连接请求队列的最大长度。虽然参数backlog的用法非常简单,只 <br>
是一个简单的整数。但搞清楚请求队列的含义对理解TCP协议的通信过程建立非常重要。 <br>
TCP协议为每个倾听套接字实际上维护两个队列,一个是未完成连接队列,这个队列中的 <br>
成员都是未完成3次握手的连接;另一个是完成连接队列,这个队列中的成员都是虽然已 <br>
经完成了3次握手,但是还未被服务器调用accept()接收的连接。参数backlog实际上指 <br>
定的是这个倾听套接字完成连接队列的最大长度。在本例中我们是这样用的:listen(s <br>
ockfd,5);表示完成连接队列的最大长度为5。 <br>
接收连接 <br>
接下来我们在主程序中看到通过名为daemonize()的自定义函数创建一个守护进程, <br>
关于这个daemonize()以及守护进程的相关概念,我们等一会儿再做详细介绍。然后服务 <br>
器程序进入一个无条件循环,用于监听接收客户机的连接请求。在此过程中如果有客户 <br>
机调用connect()请求连接,那么函数accept()可以从倾听套接字的完成连接队列中接受 <br>
一个连接请求。如果完成连接队列为空,这个进程就睡眠。accept()的详细描述为: <br>
----------------------------------------------------------------- <br>
#include <sys/socket.h> <br>
int accept(int sockfd, struct sockaddr *addr, int *addrlen); <br>
----------------------------------------------------------------- <br>
参数sockfd是我们转换成功的倾听套接字描述符;参数addr是一个指向套接字地址 <br>
结构的指针,参数addrlen为一个整型指针。当函数成功执行时,返回3个结果,函数返 <br>
回一个新的套接字描述符,服务器可以通过这个新的套接字描述符和客户机进行通信。 <br>
参数addr所指向的套接字地址结构中将存放客户机的相关信息,addrlen指针将描述前述 <br>
套接字地址结构的长度。在通常情况下服务器对这些信息不是很感兴趣,因此我们经常 <br>
可以看到一些源代码中将accept()函数的后两个参数都设置为NULL。不过在这段proxy源 <br>
代码中需要用到有关的客户机信息,因此我们看到通过执行 <br>
newsockfd = accept(sockfd, (struct sockaddr_in *) &cliaddr, &clilen); <br>
将客户机的详细信息存放在地址结构cliaddr中。而proxy就通过套接字newsockfd与 <br>
客户机进行通信。值得注意的是这个返回的套接字描述符与我们转换的倾听套接字是不 <br>
同的。在一段服务器程序中,可以始终只用一个倾听套接字来接收多个客户机的连接请 <br>
求;而如果我们要和客户机建立一个实际的连接的话,对每一个请求我们都需要调用ac <br>
cept()返回一个新的套接字。当服务器处理完毕客户机的请求后,一定要将相应的套接 <br>
字关闭;如果整个服务器程序将要结束,那么一定要将倾听套接字关闭。 <br>
如果accept()函数执行失败,则返回"-1",如果accept()函数阻塞等待客户机调用 <br>
connect()建立连接,进程在此时恰好捕捉到信号,那么函数在返回"-1"的同时将变量e <br>
rrno的值设置为EINTR。这和accept()函数执行失败是有区别的。因此我们在代码中可以 <br>
看到这样的语句: <br>
----------------------------------------------------------------- <br>
if (newsockfd < 0 && errno == EINTR) <br>
continue; <br>
/* a signal might interrupt our accept() call */ <br>
else if (newsockfd < 0) <br>
/* something quite amiss -- kill the server */ <br>
errorout("failed to accept connection"); <br>
----------------------------------------------------------------- <br>
可以看出程序在处理这两种情况时操作是完全不同的,同样是accept()返回"-1", <br>
如果有errno == EINTR,那么系统将再次调用accept()接受连接请求,否则服务器进程 <br>
将直接结束。 <br>
处理客户机请求 <br>
处理客户机请求 <br>
当服务器与客户机建立连接以后,就可以处理客户机的请求了。一般情况下服务器 <br>
程序都要创建一个子进程用于处理客户机请求;而父进程则继续监听,时刻准备接受其 <br>
它客户机的连接请求。我们这段proxy程序也不例外。它通过调用fork()创建处理客户机 <br>
请求的子进程。我想在linux/Unix编程中,fork()的重要性不用我再多说什么了,在大 <br>
型的服务器程序中,一般都要在子进程里,根据客户机请求的不同而通过exec()系列函 <br>
数调用不同的处理程序,这也是在学习linux/Unix编程中一个非常重要的地方。不过我 <br>
们这个proxy程序旨在讲述一些linux网络编程的基本概念,因此在子程序部分就直接调 <br>
用了一个完成proxy功能的函数do_proxy(),其实际参数newsockfd就是accept()返回的 <br>
套接字描述符。另外值得注意的一点就是,因为子进程继承了所有父进程中可用的文件 <br>
描述符,所以我们必须在子进程中关闭倾听套接字(代码中子进程部分的close(sockfd <br>
);),同时在父进程中关闭accept()返回的套接字描述符(例如代码中父进程部分的cl <br>
ose(newsockfd);)。 <br>
◆函数parse_args() <br>
此函数的定义是:void parse_args (int argc, char **argv); <br>
----------------------------------------------------------------- <br>
/**************************************************************** <br>
function: parse_args <br>
description: parse the command line args. <br>
arguments: argc,argv you know what these are. <br>
return value: none. <br>
calls: none. <br>
globals: writes proxy_port, writes hostaddr. <br>
****************************************************************/ <br>
void parse_args (argc,argv) <br>
int argc; <br>
char **argv; <br>
{ <br>
int i; <br>
struct hostent *hostp; <br>
struct servent *servp; <br>
unsigned long inaddr; <br>
struct { <br>
char proxy_port [16]; <br>
char isolated_host [64]; <br>
char service_name [32]; <br>
} pargs; <br>
if (argc < 4) { <br>
printf("usage: %s <proxy-port> <host> <service-name|port-number>\r <br>
\n", argv[0]); <br>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -