1、Linux 是一个可靠性非常高的操作系统,但是所有用过 Linux 的朋友都会感觉到, Linux 和Windows 这样的“ 傻瓜“操作系统(这里丝毫没有贬低 Windows 的意思,相反这应该是Windows 的优点)相比,后者无疑在易操作性上更胜一筹。但是为什么又有那么多的爱好者钟情于 Linux 呢,当然自由是最吸引人的一点,另外 Linux 强大的功能也是一个非常重要的原因,尤其是 Linux 强大的网络功能更是引人注目。放眼今天的 WAP 业务、银行网络业务和曾经红透半边天的电子商务,都越来越倚重基于 Linux 的解决方案。因此 Linux 网络编程是非常重要的,而且当我们一接触
2、到 Linux 网络编程,我们就会发现这是一件非常有意思的事情,因为以前一些关于网络通信概念似是而非的地方,在这一段段代码面前马上就豁然开朗了。在刚开始学习编程的时候总是让人感觉有点理不清头绪,不过只要多读几段代码,很快我们就能体会到其中的乐趣了。下面我就从一段 Proxy 源代码开始,谈谈如何进行 Linux 网络编程。 首先声明,这段源代码不是我编写的,让我们感谢这位名叫 Carl Harris 的大虾,是他编写了这段代码并将其散播到网上供大家学习讨论。这段代码虽然只是描述了最简单的 proxy 操作,但它的确是经典,它不仅清晰地描述了客户机/服务器系统的概念,而且几乎包括了 Linux网
3、络编程的方方面面,非常适合 Linux 网络编程的初学者学习。这段 Proxy 程序的用法是这样的,我们可以使用这个 proxy 登录其它主机的服务端口。假如编译后生成了名为 Proxy 的可执行文件,那么命令及其参数的描述为:./Proxy 其中参数 proxy_port 是指由我们指定的代理服务器端口。参数 remote_host 是指我们希望连接的远程主机的主机名,IP 地址也同样有效。这个主机名在网络上应该是唯一的,如果您不确定的话,可以在远程主机上使用 uname -n 命令查看一下。参数 service_port 是远程主机可提供的服务名,也可直接键入服务对应的端口号。这个命令的相
4、应操作是将代理服务器的 proxy_port 端口绑定到 remote_host 的 service_port 端口。然后我们就可以通过代理服务器的proxy_port 端口访问 remote_host 了。例如一台计算机,网络主机名是 legends,IP 地址为10.10.8.221,如果在我的计算机上执行:rootlee /root#./proxy 8000 legends telnet那么我们就可以通过下面这条命令访问 legends 的 telnet 端口。-rootlee /root#telnet legends 8000Trying 10.10.8.221.Connected t
5、o legends(10.10.8.221).Escape character is Red Hat Linux release 6.2(Zoot)Kernel 2.2.14-5.0 on an i686Login:-上面的绑定操作也可以使用下面的命令:rootlee /root#./proxy 8000 10.10.8.221 2323 是 telnet 服务的标准端口号,其它服务的对应端口号我们可以在/etc/services 中查看。下面我就从这段代码出发谈谈我对 Linux 网络编程的一些粗浅的认识,不对的地方还请各位大虾多多批评指正。main()函数-#include #includ
6、e #include #include #include #include #include #include #include #include #include #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 *
7、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. Ill tell you why we must do this later*/typedef void Signal(int);/*function: main description: Main level
8、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_
9、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.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_
10、ANY); servaddr.sin_port = proxy_port; /* get a socket. */ if (sockfd = socket(AF_INET,SOCK_STREAM,0) #include int socket(int domain, int type, int protocol);-参数 domain 指定套接字使用的协议族,AF_INET 表示使用 TCP/IP 协议族,AF_UNIX 表示使用 Unix 协议族, AF_ISO 表示套接字使用 ISO 协议族。type 指定套接字类型,一般的面向连接通信类型(如 TCP)设置为 SOCK_STREAM,当套接
11、字为数据报类型时, type 应设置为SOCK_DGRAM,如果是可以直接访问 IP 协议的原始套接字则 type 应设置为 SOCK_RAW。参数 protocol 一般设置为 “0“,表示使用默认协议。当 socket()函数成功执行时,返回一个标志这个套接字的描述符,如果出错则返回“-1“,并设置 errno 为相应的错误类型。设置服务器套接字地址结构在通常情况下,首先要将描述服务器信息的套接字地址结构清零,然后在地址结构中填入相应的内容,准备接受客户机送来的连接建立请求。这个清零操作可以用多种字节处理函数来实现,例如 bzero()、bcopy() 、memset()、memcpy()
12、等,以字母“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 系统的套接字是一个通用的网络编程接口,它应该支持多种网络通信协议,每一种协议都使用
13、专门为自己定义的套接字地址结构(例如 TCP/IP 网络的套接字地址结构就是 struct sockaddr_in)。不过为了保持套接字函数调用参数的一致性,Linux 系统还定义了一种通用的套接字地址结构:-struct sockaddrunsigned short sa_family; /* address type */char sa_data14; /* protocol address */-其中 sa_family 意指套接字使用的协议族地址类型,对于我们的 TCP/IP 网络,其值应该是AF_INET,sa_data 中存储具体的协议地址,不同的协议族有不同的地址格式。这个通用的
14、套接字地址结构一般不用做定义具体的实例,但是常用做套接字地址结构的强制类型转换,如我们经常可以看到这样的用法:bind(sockfd,(struct sockaddr *) ;struct sochaddr_inshort 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
15、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,这表示服务器愿意接收来自任何网络
16、设备接口的客户机连接。htonl()函数的意思是将主机顺序的字节转换成网络顺序的字节。servaddr.sin_port = htons(PORT);设置通信端口号,PORT 应该是我们已经定义好的。在本例中 servaddr.sin_port = proxy_port;这是表示端口号是函数的返回值 proxy_port。另外需要说明的一点是,在本例中,我们并没有看到在预编译部分中包含有和这两个头文件,那是因为这两个头文件已经分别被包含在和中了,而且后面这两个头文件是与平台无关的,所以在网络通信中一般都使用这两个头文件。服务器公开地址如果服务器要接受客户机的连接请求,那么它必须先要在整个网络上
17、公开自己的地址。在设置了服务器的套接字地址结构之后,可以通过调用函数 bind()绑定服务器的地址和套接字来完成公开地址的操作。函数 bind()的详细描述为:-#include #include int bind(int sockfd, struct sockaddr *addr, int addrlen);-参数 sockfd 是我们通过调用 socket()创建的套接字描述符。参数 addr 是本机地址,参数addrlen 是套接字地址结构的长度。函数执行成功时返回 “0“,否则返回“-1“,并设置 errno 变量为 EADDRINUAER。如果是服务器调用 bind()函数,如果设置
18、了套接字的 IP 地址为某个本地 IP 地址,那么这表示服务器只接受来自于这个 IP 地址的特定主机发出的连接 请求。不过一般情况下都是将 IP 地址设置为 INADDR_ANY,以便接受所有网络设备接口送来的连接请求。客户机一般是不会调用 bind()函数的,因为客户机在连接时不用指定自己的套接字地址端口号,系统会自动为客户机选择一个未用端口号,并且用本地 IP 地址自动填充客户机套接字地址结构中的相应项。但是在某些特定的情况下客户机需要使用特定的端口号,例如 Linux 中的rlogin 命令就要求使用保留端口号,而系统是不能为客户机自动分配保留端口号的,这就需要调用 bind()来绑定一
19、个保留端口号了。不过在一些特殊的环境下,这样绑定特定端口号也会带来一些负面影响,如在 HTTP 服务器进入 TIME_WAIT 状态后,客户机如果要求再次与服务器建立连接,则服务器会拒绝这一连接请求。如果客户机最后进入 TIME_WAIT 状态,则马上再次执行 bind()函数时会返回出错信息“-1“,原因是系统会认为同时有两次连接绑定同一个端口。转换 Listening 套接字接下来,服务器需要将我们刚才与 IP 地址和端口号完成绑定的套接字转换成倾听 listening 套接字。只有服务器程序才需要执行这一步操作。我们通过调用函数 listen()实现这一操作。listen()的详细描述为
20、:-#include int listen(int sockfd, int backlog);-参数 sockfd 指定我们要求转换的套接字描述符,参数 backlog 设置请求队列的最大长度。函数listen()主要完成以下操作。首先是将套接字转换成倾听套接字。因为函数 socket()创建的套接字都是主动套接字,所以客户机可以通过调用函数 connect()来使用这样的套接字主动和服务器建立连接。而服务器的情况恰恰相反,服务器需要通过套接字接收客户机的连接请求,这就需要一个“被动“ 套接字。listen()就可将一个尚未连接的主动套接字转换成为这样的“被动“套接字,也就是倾听套接字。在执行
21、了listen()函数之后,服务器的 TCP 就由 CLOSED 变成 LISTEN 状态了。另外 listen()可以设置连接请求队列的最大长度。虽然参数 backlog 的用法非常简单,只是一个简单的整数。但搞清楚请求队列的含义对理解 TCP 协议的通信过程建立非常重要。TCP 协议为每个倾听套接字实际上维护两个队列,一个是未完成连接队列,这个队列中的成员都是未完成 3 次握手的连接;另一个是完成连接队列,这个队列中的成员都是虽然已经完成了 3 次握手,但是还未被服务器调用 accept()接收的连接。参数 backlog 实际上指定的是这个倾听套接字完成连接队列的最大长度。在本例中我们是
22、这样用的:listen(sockfd,5);表示完成连接队列的最大长度为 5。接收连接接下来我们在主程序中看到通过名为 daemonize()的自定义函数创建一个守护进程,关于这个daemonize()以及守护进程的相关概念,我们等一会儿再做详细介绍。然后服务器程序进入一个无条件循环,用于监听接收客户机的连接请求。在此过程中如果有客户机调用 connect()请求连接,那么函数 accept()可以从倾听套接字的完成连接队列中接受一个连接请求。如果完成连接队列为空,这个进程就睡眠。accept()的详细描述为:-#include int accept(int sockfd, struct so
23、ckaddr *addr, int *addrlen);-参数 sockfd 是我们转换成功的倾听套接字描述符;参数 addr 是一个指向套接字地址结构的指针,参数 addrlen 为一个整型指针。当函数成功执行时,返回 3 个结果,函数返回一个新的套接字描述符,服务器可以通过这个新的套接字描述符和客户机进行通信。参数 addr 所指向的套接字地址结构中将存放客户机的相关信息,addrlen 指针将描述前述套接字地址结构的长度。在通常情况下服务器对这些信息不是很感兴趣,因此我们经常可以看到一些源代码中将 accept()函数的后两个参数都设置为 NULL。不过在这段 proxy 源代码中需要用
24、到有关的客户机信息,因此我们看到通过执行newsockfd = accept(sockfd, (struct sockaddr_in *) 将客户机的详细信息存放在地址结构 cliaddr 中。而 proxy 就通过套接字 newsockfd 与客户机进行通信。值得注意的是这个返回的套接字描述符与我们转换的倾听套接字是不同的。在一段服务器程序中,可以始终只用一个倾听套接字来接收多个客户机的连接请求;而如果我们要和客户机建立一个实际的连接的话,对每一个请求我们都需要调用 accept()返回一个新的套接字。当服务器处理完毕客户机的请求后,一定要将相应的套接字关闭;如果整个服务器程序将要结束,那么
25、一定要将倾听套接字关闭。如果 accept()函数执行失败,则返回“-1“,如果 accept()函数阻塞等待客户机调用 connect()建立连接,进程在此时恰好捕捉到信号,那么函数在返回“-1“的同时将变量 errno 的值设置为EINTR。这和 accept()函数执行失败是有区别的。因此我们在代码中可以看到这样的语句:-if (newsockfd 0 /* 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()接受连接请求,否则服务器进程将直接结束。