UNP总结 Chapter 3 套接字编程简介
1.套接字地址结构
1).IPv4套接字地址结构
IPv4套接字地址结构通常也称为“网际套接字地址结构”,它以sockaddr_in命名,定义在<netinet.h>头文件中,如下所示
struct in_addr{ in_addr_t s_addr; //32位的IPv4网络字节序 } struct sockaddr_in{ uint8_t sin_len; //带符号8位整数地址结构长度 sa_family_t sin_family; //协议族,IPv4为AF_INET in_port_t sin_port; //端口号 struct in_addr sin_addr; //32位IPv4网络字节序地址 char sin_zero[8]; //填充对齐位,未使用 };
2).通用套接字地址结构
存在通用套接字地址结构的原因是:在调用任何需要套接字地址结构做为参数的函数时,套接字地址结构总是以引用的方式(指针)传递的。不同的协议有不同的套接字地址结构,函数的参数怎么声明这些套接字地址结构的指针类型是一个问题,于是就定义了一个通用套接字地址结构,所有需要套接字地址结构做参数的函数的这个形参都被声明为指向这个通用套接字地址结构的指针的类型。其他套接字地址结构的指针被强制转换为通用套接字地址结构的指针类型,ANSI C 定义了 void * 来解决这个问题
struct sockaddr { uint8_t sa_len; sa_family_t sa_family; /* address family: AF_xxx value */ char sa_data[14]; /* protocol-specific address */ };
强制转换实例:
struct sockaddr_in serv; /* IPv4 socket address structure */ /* fill in serv{} */ bind(sockfd, (struct sockaddr *) &serv, sizeof(serv));
从应用程序开发的角度来看,这些通用套接字结构的唯一用途就是对指向特定于协议的套接字地址结构的指针执行类型强制转换
2).IPv6套接字地址结构
IPv6套接字地址结构在<netinet/in.h>中定义,如下所示
struct in6_addr{ uint8_t s6_addr[16]; //128位IPv6网络字节序地址 }; #define SIN6_LEN struct sockaddr_in6{ uint8_t sin6_len; //这个结构的长度 sa_family_t sin6_family; //协议族 AF_INET6 in_port_t sin6_port; //端口号,网络字节序 uint32_t sin6_flowinfo; //流信息,未定义 struct in6_addr sin6_addr; //IPv6地址 uint32_t sin6_scope_id; };
说明:
- 如果当前系统支持套接字地址结构中的长度字段,则SIN6_LEN常值必须定义
- IPv6的地址族是AF_INET6 而IPv4 是AF_INET.
- 结构体字段的先后顺序做过编排的,使得结果sockaddr_in6结构本身64位对齐,那么128位的sin6_addr字段特是64位对齐的
(其他说明见UNP3)
3).新的通用套接字地址结构
不像struct sockaddr,新的struct sockaddr_storage足以容纳系统所支持的任何套接字地址结构,sockaddr_storage结构在<netinet/in.h>头文件中定义
struct sockaddr_storage { uint8_t ss_len; /* length of this struct (implementation dependent) */ sa_family_t ss_family; /* address family: AF_xxx value */
/*其他字段对用户来说的透明的 故没有列出*/ };
sockaddr_storage和sockaddr的主要差别
- sockaddr_storage通用套接字地址结构满足对齐要求
- sockaddr_storage通用套接字地址结构足够大,能够容纳系统支持的任何套接字地址结构。
4).套接字地址结构的比较
下图给出了给套接字地址结构的直观比较
2.值-结果参数
一个套接字函数传递一个套接字地址结构时候,该结构总以引用形势来传递,也就是说传递的指向该结构的一个指针,该结构的床度也作为一个参数来传递,不过其传递方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程。
1).从进程到内核传递套接字结构函数:bind、connect和sendto,这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构体的整数大小
struct sockaddr_in serv; /* fill in serv{} */ connect (sockfd, (SA *) &serv, sizeof(serv));
2).从内核到进程传递套接字地址结构的函数:accept、recvfrom、getsockname和getpeername。这四个参数的其中两个参数指向某个套接字结构体的指针和指向表示该结构体大小的整数变量的指针,eg:
struct sockaddr_un cli; /* Unix domain */ socklen_t len; len = sizeof(cli); /* len is a value */ getpeername(unixfd, (SA *) &cli, &len); /* len may have changed */
为何将结构大小由整数改为指向整数的指针呢?这是因为:当函数被调用时,结构大小是一个值(value), 此值告诉内核该结构的大小,使内核在写此结构时不至于越界),当函数返回时,结构大小又是一个结果(result,它告诉进程内核在此结构中确切存储了多少信息),这种参数叫做值-结果参数(value-result)。
3.字节排序函数
- 大端字节序:高字节在低地址
- 小端字节序:低字节在高地址
- 网络字节序:网络字节序采用大端字节序
- 主机字节序:本地主机使用的字节序,可能为大端或小端
如下图
因为套接字地址结构中的某些字段必须按照网络字节序进行维护,所以必须关注主机字节序和网络字节序之间的相互转换,这两种字节序之间的转换使用下面的4个函数:
#include <netinet/in.h> uint16_t htons(uint16_t host16bitvalue); //主机to网络 short uint32_t htonl(uint32_t host32bitvalue); //主机to网络 long //均返回:网络字节序的值 uint16_t ntohs(uint16_t net16bitvalue); //网络to主机 short //均返回:主机字节序的值 uint32_t ntohl(uint32_t net32bitvalue); //网络to主机 long
显然在这些函数名字中,h表示network,s代表short,l代表long。
4.字节操纵函数
1).
#include <strings.h> void bzero(void *dest,size_t nbytes); //将目标字符串制定数目的字节置0 void bcopy(const void *src,void *dest,size_t nbytes); //拷贝指定字节 int bcmp(const void*ptrl,const void *ptr2,size_t nbytes); //若相等返回0
2).
#include<string.h> void *memset(void *dest,const void *c,size_t len); void *memcpy(void *dest,const void *src,size_t nbytes); int memcpy(const void *ptrl,const void *ptr2,size_t nbytes); //返回:若相等则为0,否则为<0或者>0
memset把目标字节串指定数目的字节置为值c。memcpy类似bcopy,不过两个指针参数的顺序相反,当源字节串与目标字节串重叠时,bcopy能够正确处理,但是memcpy的操作结果却不可知,memcmp比较两个任意的字符串
5.inet_aton、inet_addr和inet_ntoa函数
inet_aton、inet_addr和inet_ntoa在点分十进制数串(eg:“206.168.112.96”)与它的32位网络字节序二进制值间转换IPv4地址。
#include<arpa/inet.h> int inet_aton(const char *strptr, struct in_addr *addrptr);
//返回:1--串有效, 0--串有错 in_addr_t inet_addr(const char *strptr); //返回:若成功,返回32位二进制的网络字节序地址;若有错,返回INADDR_NONE char *inet ntoa(struct in_addr inaddr); //返回:指向点分十进制数串的指针
第一个函数inet_aton将strptr所指的C字符串转换成32位的网络字节序二进制值,并通过指针addrptr来存储。如果成功返回1,否则返回0,inte_addr进行相同的转换,返回值为32位的网络字节序二进制值,一般用inet_aton来代替代替inet_addr.
函数inet_ntoa将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串。由函数返回值所指的串驻留在静态内存中,这意味着函数是不可重入的。注意这个函数以结构为参数,而不是指向结构的指针。
6.inet_pton和inet_ntop函数
函数名中p和n分别代表表达(presentation)和数值(numeric)地址的表达式通常是ASCII字符串,数值格式则是存放在套接字地址结构中的二进制值
#include <arpa/inet.h> int inet_pton(int family, const char *strptr, void *addrptr); //返回:若成功为1,若输入不是有效的表达式为0,若出错为-1 const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len); //返回:若成功则为指向结果的指针,若出错则为null
inet_pton函数尝试转换由strptr指针指向的字符串,并通过addrptr指针存放二进制结果,inet_ntop函数进行相反的转换,从数值格式(addrptr)转换到表达格式(strptr),len参数是目标存储单元的大小
下图给出了地址转换函数小结
7.readn、writen和readline函数
下面的函数实际上就是read、writen函数增强版
#include "unp.h" ssize_t readn(int filedes, void *buff, size_t nbytes); ssize_t writen(int filedes, const void *buff, size_t nbytes); ssize_t readline(int filedes, void *buff, size_t maxlen); //均返回:读或写的字节数,若出错则为-1
实现代码如下:
//readn #include "unp.h" ssize_t readn(int fd,void *vptr,size_t n) { size_t nleft; ssize_t nread; char *ptr; ptr = vptr; nleft = n; while(nnleft > 0) { if((nread = read(fd,ptr,nleft)) < 0 ) { if(errno == EINTR) { nread = 0; } else { return -1; } } else if(nread == 0) { break; } nleft -= nread; ptr += nread; } return (n-nleft); }
//written #include "unp.h" ssize_t written(int fd,void *vptr,size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; nleft = n; while(nlfet > 0) { if((nwritten = writen(fd,ptr,nleft)) <= 0) { if(nwritten < 0 && errno ==EINTR) { nwritten =0; } else { return -1; } } nleft -= nwritten; ptr += nwritten; } return n; }
//readline #include "unp.h"
ssize_t readline(int fd, void *vptr, size_t maxlen) { ssize_t n,rc; char c,*ptr; ptr = vptr; for(n = 1; n < maxlen; n++)
{ if( (rc = read(fd, &c)) == 1)
{ *ptr ++ = c; if(c == '\n') break; }
else if (r c== 0)
{ *ptr = 0; return (n-1); }else return (-1); } *ptr = 0; return (n); }