已经知道了如何创建和销毁一个socket, 那么如何确定一个目标通信进程呢?
进程的标识由两个部分组成:
- 网络地址: 标识网络上通信的目标计算机.
- 服务: 标识计算机上的特定进程.
1. 处理器的字节序:
字节序是一个处理器架构特性, 用于指示象整数这样的大数据类型的内部字节顺序.
先介绍一下大端/小端的概念:
- 大端: 最大字节地址对应于数字的最低有效字节(LSB)上.
- 小端: 最小字节地址对应于数字的最低有效字节上.
不管字节如何排序, 数字最高位总是在左边, 最低位总是在右边.
因此, 如果想给一个32位整数赋值0x04030201, 不管字节如何排序, 数字最高位包含4(确切说是04), 数字最低位包含1(确切说是01).
接着, 如果想将一个字符指针(cp)强制转换到这个整数地址, 将看到字节序带来的不同:
- 小端处理器: cp[0]指向数字最低位因而包含1, cp[3]指向数字最高位因而包含4.
- 大端处理器: cp[0]指向数字最高位因而包含4, cp[3]指向数字最低位因而包含1.
2. 网络上的字节序:
网络协议指定了字节序, 因此不同架构的计算机系统在网络上交换协议信息时不会混淆字节序, 但在应用程序交换格式化数据时, 字节序问题就会出现.
TCP/IP协议栈采用大端字节序. 对于TCP/IP, 地址用网络字节序来表示, 所以应用程序有时需要在处理器的字节序与网络字节序之间的转换.
对于TCP/IP应用程序, Linux socket提供了4个函数以实现在处理器字节序和网络字节序之间的转换.
头文件: <arpa/inet.h>
原型:
- uint32_t htonl(uint32_t hostint32); 返回以网络字节序表示的32位整数.
- uint16_t htons(uint16_t hostint16); 返回以网络字节序表示的16位整数.
- uint32_t ntohl(uint32_t netint32); 返回以主机字节序表示的32位整数.
- uint16_t ntohs(uint16_t netint16); 返回以主机字节序表示的16位整数.
说明:
- h: 表示主机(host)字节序.
- n: 表示网络(network)字节序.
- l: 表示长(long)整数.
- s: 表示短(short)整数.
3. 通用地址格式:
地址标识了特定通信域中的socket端点, 地址格式与特定的通信域有关. 为使不同格式的地址能够被传入到socket函数, 地址将被强制转换成通用地址结构sockaddr表示:
... {
sa_family_t sa_family; /**//* address family */
char sa_data[]; /**//* variable-length address */
.
.
.
} ;
socket实现可以自由的添加额外成员并且定义sa_data成员的大小, 在Linux中, 该结构定义如下:
... {
sa_family_t sa_family; /**//* address family */
char sa_data[14]; /**//* variable-length address */
} ;
4. Internet地址(INET地址):
在IPv4(AF_INET)中, socket由如下结构sockaddr_in表示:
- 头文件: <netinet/in.h>
- 结构:
... {
sa_family_t sin_family; /**//* address family */
in_port_t sin_port; /**//* port number */
struct in_addr sin_addr; /**//* IPv4 address */
} ;
struct in_addr
... {
in_addr_t s_addr; /**//* IPv4 address */
} ;
- 说明: 数据类型in_port_t定义成uint16_t, 数据类型in_addr_t定义成uint32_t. 这些整数类型在<stdint.h>中定义并指定了相应的位数.
在IPv6(AF_INET6)中, socket由如下结构sockaddr_in6表示:
... {
sa_family_t sin6_family; /**//* address family */
in_port_t sin6_port; /**//* port number */
uint32_t sin6_flowinfo; /**//* traffic class and flow info */
struct in6_addr sin_addr; /**//* IPv6 address */
uint32_t sin6_scope_id; /**//* set of interface for scope */
} ;
struct in6_addr
... {
uint8_t s6_addr[16]; /**//* IPv6 address */
} ;
在Linux中, sockaddr_in定义如下:
... {
sa_family_t sin_family; /**//* address family */
in_port_t sin_port; /**//* port number */
struct in_addr sin_addr; /**//* IPv4 address */
unsigned char sin_zero[8]; /**//* filler */
} ;
其中成员sin_zero[8]为填充字段, 必须全部被置为0.
尽管这些结构相差比较大, 它们都会被强制转换为sockaddr结构传入到socket中.
有时, 需要输出被人理解的地址格式, 而不只是被计算机理解的格式, 这就需要二进制地址格式与点分十进制地址(a.b.c.d)格式字符串之间的相互转换. Linux提供了这样的3个函数:
inet_addr (将网络地址转成二进制的数字)
- 头文件: <sys/socket.h>, <netinet/in.h>, <arpa/inet.h>
- 原型: unsigned long int inet_addr(const char *cp);
- 返回值: 成功则返回对应的网络二进制数字, 失败返回-1.
- 参数: cp指向网络地址字符串, 如"192.168.1.1"
inet_aton (将网络地址转换成网络二进制的数字)
- 头文件: <sys/socket.h>, <netinet/in.h>, <arpa/inet.h>
- 原型: int inet_aton(const char *cp, struct in_addr *inp);
- 返回值: 成功则返回非0, 失败返回0.
- 参数:
- cp: 指向网络地址字符串.
- inp: 指向一个struct in_addr结构, 用于保存转换结果.
- 说明: 将cp所指向的网络地址字符串转换为网络二进制数字, 然后存于参数inp中.
inet_ntoa (将网络二进制的数字转换成网络地址)
- 头文件: <sys/socket.h>, <netinet/in.h>, <arpa/inet.h>
- 原型: char *inet_ntoa(struct in_addr in);
- 返回值: 成功则返回字符串指针, 失败返回NULL.
- 说明: 将参数in所指的网络二进制数字转换成网络地址, 并返回该网络地址的字符串指针.
以上仅用于IPv4地址, 若是IPv6地址, 则使用inet_ntop和inet_pton, 详细见APUE 442页.
5. 地址查询:
下面介绍一些用来查询寻址信息的函数, 这些函数返回的网络配置信息可能存放在许多地方, 可以是文件(如/etc/hosts, /etc/services等), 也可以被DNS或NIS管理. 但无论信息存放在哪里, 这些函数同样能够访问它们.
通过调用gethostent系列函数, 可以找到给定计算机的host信息, 它们是:
struct hostent * gethostent(); /**/ /* 返回文件的下一个条目 */
void sethostent( int stayopen); /**/ /* 打开文件或回绕 */
void endhostent(); /**/ /* 关闭文件 */
/**/ /* hostent */
struct hostent
... {
char *h_name; /**//* name of host */
char **h_aliases; /**//* pointer to alternate host name array */
int h_addrtype; /**//* address type */
int h_length; /**//* length in bytes of address */
char **h_addr_list; /**//* pointer to array of network addresses */
} ;
具体相关内容查看APUE 443页, 这包括可以查找:
- 计算机的主机信息(gethostent).
- 网络名字和网络号(getnetent).
- 协议名字和协议号(getprotoent).
- 服务名(getservent).
此外还有一些通过映射方式查找的函数接口, 也可以参考APUE.
6. socket与地址的绑定:
对于服务器来说, 需要给一个接受客户端请求的socket绑定一个(IP)地址. 客户端也应该存在一种方法发现已连接的服务器地址.
在网络IPC的各种操作中(如send, recv...), 操作的目标参数是socket, 而不是地址, 但在这些操作开始之前, 要先把socket与地址绑定(bind). 使用bind函数将地址绑定到一个socket:
- 头文件: <sys/socket.h>
- 原型: int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
- 返回值: 成功则返回0, 出错则返回-1.
- 参数:
- sockfd: 已创建的socket.
- addr: 已填充好的地址结构.
- len: sockaddr的结构长度.
对于所能使用的地址有一些限制:
- 在进程所运行的机器上, 指定的地址必须有效.
- 地址必须和创建socket时的地址族所支持的格式相匹配.
- 端口号必须不小于1024, 除非该进程具有特权.
- 一般只有socket能够与地址绑定, 少数协议允许多重绑定.
对于Internet域, 如果指定IP地址为INADDR_ANY, socket端点可以被绑定到艘有的系统网络接口. 这意味着可以收到这个系统所安装的所有网卡的数据包.
可以调用函数getsockname来发现绑定到一个socket的地址:
- 头文件: <sys/socket.h>
- 原型: int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
- 返回值: 成功则返回0, 出错则返回-1.
- 参数:
- sockfd: 已创建的socket.
- addr: 指向用于保存返回地址的结构.
- alenp: 指向整数的指针, 该整数指向sockaddr的大小. 返回时, 该整数会被设置成返回地址的大小. 如果该地址与sockaddr长度不匹配, 则将其截断而不报错.
如果socket已经和对方连接, 可以调用函数getpeername来找到对方的地址, 该函数除了会返回对方的地址外, 其他的都和getsockname一样.