套接字基础
首先,我们来思考下这样一个问题:为什么要使用套接字进行网络编程?
答:Linux环境下使用套接字进行进程之间的通信。套接字接口(socket interface)是一组函数,也是操作系统提供给应用程序的接口。在Unix系统中,套接字和Unix I/O函数结合起来,用来创建网络应用程序。(也就是说,操作系统对外只提供了套接字作为网络通信的接口,假如想进行网络通信,套接字我们用也得用,不用也得用,而且使用套接字来进行网络通信是十分通用的方法)。这里最典型的就是客户端--服务器模型。
因特网客户端和服务器通过在**连接**上发送的接收字节流来通信。从连接一对进程的意义上而言,连接是**点对点**的。从数据可以同时双向流动的角度来说,它是**全双工**的。并且从(除了一些灾难性的引起的失败以外,如:农民伯伯切断了电缆。)由源进程发出的字节流最终被目的进程以它发出的顺序收到它的角度来说,它也是**可靠的**。这个可靠性,主要是有TCP传输来保证的,至于TCP如何保证可靠传输,请参考TCP相关资料。
一个**套接字**是连接的一个端点。每个套接字都有相应的**套接字地址**,是由一个因特网地址(IP地址)和一个16位的整数**端口**组成的,用"地址:端口"来表示。当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为**临时端口**(ephemeral port)。然而,服务器套接字地址中的端口通常是和某个**知名**的端口,是和这个服务相对应的。例如,Web服务器通常使用端口80,而电子邮件服务器使用端口25.在Unix机器上,文件/etc/services包含一张这台机器提供的服务以及它们的知名端口号的综合列表。
一个连接是由它两端的套接字地址唯一确定的。这对套接字地址叫做**套接字对**(socket pair),由下列元组来表示:
(cliaddr:cliport, servaddr:servport)
其中cliaddr是客户端的IP地址,cliport是客户端的端口,servaddr是服务器的IP地址,而servport是服务器的端口。例如,有如下连接套接字对:
(128.2.194.242:51213, 208.216.181.15:80)
在这个示例中,Web客户端的套接字地址是:
128.2.194.242:51213
其中,端口号51213是内核分配的临时的端口号。
Web服务器的套接字地址是:
208.216.181.15:80
其中,端口号80是和Web服务器的通用的端口号。给定了客户端和服务器套接字的地址和端口号。客户端和服务器之间的连接就由下列套接字对唯一确定了:
(128.2.194.242:51213, 208.216.181.15:80)
(也就是说,IP地址,确定了唯一的主机,而端口号,确定了,主机上唯一的进程,这样,就可以使客户端的某一个指定的进程和服务器某一指定的进程进行网络通信)
套接字的地址结构
从Unix内核的角度来看,一个套接字就是通信的一个端点。从Unix程序的角度来看,套接字就是一个有相应描述符(套接字描述符)的打开文件,应用程序可以像操作文件一样操作一个套接字,因此,在进行网络通信的过程中,用户感觉就好像在操作一个文件一样,这也正是Linux将外部设备抽象为一个文件的好处。
准备工作
字节序
每一台主机由于体系结构的不同,所采用的数据存储方式也不相同。在网络环境中,进程之间的通信是要跨越主机的,这时就有了一个字节序不统一的问题。字节序依赖于具体主机的处理器体系结构,同一台主机的进程之间不存在该问题。但是在网络环境汇总变成,程序员不能对通信两端的主机做强制性要求,这样会降低代码的通用性。
为了解决这个问题,网络协议提供一种字节序,当跨越主机的两个进程进行通信时,先将需要传输的数据转换成网络字节序,待接受方接收数据后,再将其转换为本地主机字节序。
因为因特网主机可以有不同的主机字节顺序,TCP/IP为任意整数数据项定义了统一的网络字节顺序(network byte order)(大端字节序)(big-endian),例如IP地址,它放在包头中跨过网络被携带。在IP地址结构中存放的地址总是以(大端法)网络字节顺序存放的,即使主机字节顺序(host byte order)是小端法。Unix提供了下面这样的函数在网络和主机字节顺序间实现转换:
1 #include <netinet/in.h> 2 3 unsigned long int htonl(unsigned long int hostlong); 4 5 unsigned short int htons(unsigned short int hostshort); 6 7 /* 返回:按照网络字节顺序的值 */ 8 9 unsigned long int ntohl(unsigned long int netlong); 10 11 unsigned short int ntohs(unsigned short int netshort); 12 13 /* 返回:按照主机字节顺序的值*/
备注:
其实big endian是指低地址存放最高有效字节(MSB),而little
endian则是低地址存放最低有效字节(LSB)。用文字说明可能比较抽象,下面用图像加以说明。比如数字0x12345678在两种不同字节序CPU中的存储顺序如下所示:
Big Endian
一个Word中的高位的Byte放在内存中这个Word区域的低地址处
低地址 高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 12 | 34 | 56 | 78 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Little Endian
一个Word中的低位的Byte放在内存中这个Word区域的低地址处
低地址 高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 78 | 56 | 34 | 12 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
获取IP地址
因特网的套接字地址存放在如下结构中,如下图所示的类型为sockaddr_in的16字节结构中。对于因特网应用,sin_family成员是AF_INET,sin_port成员是一个16位的端口号,而sin_addr成员就是一个32位的IP地址。IP地址和端口号总是以网络字节顺序(大端法)存放的。
1 ------------------------sockaddr:socketbits.h (included by socket.h), sockaddr_in:netinet/in.h 2 3 /* Generic socket address structure (for connect, bind, adn accept) */ 4 struct sockaddr { 5 unsigned short sa_family; /* Protocol family */ 6 char sa_data[14]; /* Address data */ 7 }; 8 9 /* Internet-style socket address structure */ 10 struct sockaddr_in { 11 unsigned short sin_family; /* Address family (always AF_INET) */ 12 unsigned short sin_port; /* Port number in network byte order */ 13 struct in_addr sin_addr; /* IP address in network byte order */ 14 unsigned char sin_zero[8]; /* Pad to sizeof(struct sockaddr) */ 15 }; 16 17 --------------------sockaddr:socketbits.h(included by socket.h), sockaddr_in:netinet/in.h
##### _in后缀意味着什么?
_in 后缀是互联网(internet)的缩写,而不是输入(input)的缩写。
**connect、bing和accept函数要求一个指向与协议相关的套接字地址结构的指针。套接字接口的设计者面临的问题是,如何定义这些函数,使之能接受各种类型的套接字地址结构。** **现在,我们可以使用通用的void*指针**,那时在C中并不存在这种类型的指针。解决办法是定义套接字函数要求一个指向通用sockaddr结构的指针,然后要求应用程序将与协议特定的结构的指针强制转换成这个通用结构。为了简化代码示例,我们跟随
Stevens的指导,定义下面的类型:
typedef struct sockaddr SA;
然后无论何时需要将sockaddr_in结构强制转换成通用sockaddr结构,我们都使用这个类型。
为什么已经有了socket_addr_in还要设计socketaddr这样的类型,也就是说为什么connect/bind/之类的不直接把参数定义成sockaddr_in或者直接将下面的sockaddr_in定义为sockaddr?反而要在传参或者初始化过程中通过强制类型转换?
答:主要参考上一段中**包括的部分内容** 通俗说,不同的系统或者说不同的网络协议族(网络通信协议),结构是不一样的,这样通过再加一层封装以后,可以是一个统一的结构,也就是,所有结构都是sockaddr.
sockaddr 结构的定义在/usr/include/x86_64-linux-gnu/bits/socket.h中
sockaddr_in 结构定义在/usr/include/netinet/in.h中
参看in.h中,还有其他的协议族,比如下面的sockaddr_in6, 应该也可以调用操作系统的同一套接口,即:connect/bind之类的接口,但是,应该只需强制类型转换为sockaddr即可。
accept函数
服务器通过调用accept函数来等待来自客户端的连接请求:
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr * addr, int *addrlen);
返回:若成功则为非负连接描述符,若出错则为-1。
accpet函数等待来自客户端的连接请求到达侦听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个**已连接描述符**(connected descriptor),这个描述符可被用来利用Unix I/O函数与客户端通信。
监听描述符和已连接描述符之间的区别使很多人感到迷惑。
监听描述符是作为客户端链接请求的一个端点。典型地,它被创建一次,并存在于服务器的整个生命周期。
已连接描述符是客户端和服务器之间已经建立起来了的连接的一个端点。服务器每次接受连接请求时都会创建一次,它只存在于服务器为一个客户端服务的过程中。
第一步。服务器调用accept,等待连接请求到达监听描述符,具体的我们设定为描述符3.(回忆一下,描述符0~2是预留给了标准文件的)
1.服务器阻塞在accpet,等待监听描述符listenfd上的连接请求。
第二步。客户端调用connect函数,发送一个连接请求到listenfd。
2.客户端通过调用和阻塞在connect,创建连接请求。
第三步。accept函数打开了一个新的已连接描述符connfd(我们假设是描述符4),在clientfd和connfd之间建立连接,并且随后返回connfd给应用程序。客户端也从connect返回,在这一点以后,客户端和服务器就可以分别通过读和写clientfd和connfd来回传送数据了。
3.服务器从accept返回connfd。客户端从connect返回。现在在clientfd和connfd之间已经建立起了链接。
备注:为何要有监听描述符和已连接描述符,他们之间有什么区别?
答:你可能很想知道为什么套接字接口要区别监听描述符和已连接描述符。乍一看,这像是不必要的复杂化。然而,区分这两者其实是非常有用而且有必要的,因为它使得我们可以建立并发服务器,它能够同时处理许多客户端连接。例如,每次一个连接请求到达监听描述符时,我们可以派生一个新的进程,它通过已连接描述符与客户端通信。
上面说的这样的服务器,即是简单的echo服务器,一次只能处理一个客户端。这种类型的服务器一次一个地在客户间迭代,成为迭代服务器(iterative server)
而,同时可以处理多个客户端的socke请求的服务器,我们称之为并发服务器(soncurrent server).
为何要有监听描述符和已连接描述符之间的区别?
因为,区分这两者是很有用的,因为它使得我们可以建立并发服务器,它能够同时处理多个客户端连接。例如,每次一个连接请求到达监听描述符时,我们可以派生(fork)一个新的进程,它通过已连接描述符与客户端通信。
----------------------------
参考资料:
可能部分内容有修改。
《深入理解计算机系统》 Page 625, 623, 629。
http://www.cnblogs.com/lidp/archive/2009/12/02/1697485.html