1.套接字描述符
套接字是通信端点的抽象。与应用程序使用文件描述符访问文件一样,访问套接字也需要用套接字描述符。要创建一个
套接字,可以调用socket函数。
#include<sys/socket.h>
int socket(int domain, int type, int protocol); //成功返回套接字描述符,出错返回-1.
domain确定通信的特性,包括地址格式。
下表总结了POSIX.1指定的各个域。
参数type确定套接字的类型,进一步确定通信特征。下表总结了POSIX.1定义的套接字类型。
参数protocol通常为0,表示按给定的域和套接字类型选择默认协议。当对同一域和套接字类型支持多个系协议时,可以使用
protocol选择一个特定协议。
在AF_INET通信域中套接字类型SOCK_STREAM的默认协议是TCP,在AF_INET通信中套接字类型SOCK_DGRAM的默认
协议是UDP。
SOCK_SEQPACKET套接字和SOCK_STREAM套接字很类似,但从该套接字得到的是基于报文的服务而不是字节流服务。
这意味着从SOCK_SEQPACKET套接字接收的数据量与对方所发送的一致。流控制传输协议(SCTP)提供了因特网上的
顺序数据报服务。
SOCK_RAW套接字提供一个数据报接口用于直接访问下面的网络层。使用这个接口时,应用程序负责构造自己的协议首部,
这时因为传输协议被绕过了。当创建一个原始套接字时需要有超级用于权限。
调用socket与调用open相类似。在两种情况下均可获得用于输入输出的文件描述符,当不再需要该文件描述符时,调用close
来关闭文件或套接字的访问。并且释放该描述符以重新使用。但不是所有参数为文件描述符的函数都可以接受套接字描述符。
可以使用文件描述符的函数处理套接字的函数如下表:
套接字通信是双向的。可以采用函数shutdown来禁止套接字上的输入输出。
#include<sys/socket.h>
int shutdown(int sockfd, int how); //若成功则返回0,出错则返回-1.
如果how是SHUT_RD(关闭读端),那么无法从套接字读取数据;如果how是SHUT_WR(关闭写端),那么无法使用套接字
发送数据。使用shut_RDWR,则将同时无法读取和发送数据。
能够close套接字,为什么还要使用shutdown呢?理由如下:
1.clsoe只有在最后一个活动引用被关闭时才释放网络端点。这意味着,如果复制一个套接字,套接字直到关闭了最后一个
引用它的文件描述符之后才会被释放。而shutdown允许使一个套接字处于不活动状态,无论引用它的文件描述符数目多少。
其次,有时值关闭套接字双向传输中的一个方向会很方便。
2.寻址
2.1.大小端
处理器有大端小端之分,这方面本文不做详细介绍,下面介绍四个通用函数以实施在处理字节序和网络字节序之间的转换。
#include <arpa/inet.h>
uint32_t htol(uint32_t hostint32); //以网络字节序表示的32未整形数
uint16_t htos(uint16_t hostint16); //以网络字节序表示的16位整型数
uint32_t ntohl(uint32_t netint32); //以主机字节序表示的32位整型数
uint16_t ntohs(uint16_t neting16); //以主机字节序表示的16位整型数
h表示主机,n表示网络,l表示长整型。s表示短整型。
2.2.地址格式
地址标识了特定通信域中的套接字端点,地址格式与特定的通信域相关。为使不同格式地址能传入到套接字函数,地址
被强制转换成通用的地址结构sockaddr表示:
struct sockaddr{
sa_family_t sa_family; //地址family
char sa_data[]; //可变长度地址
...
};
套接字实现可以自由第添加额外的成员并且定义sa_data成员的大小,例如linux中,该结构定义如下:
struct sockaddr{
sa_family_t sa_family;
char sa_data[14];
};
因特网地址定义在<netinet/in.h>中。在IPv4因特网域(AF_INET)中,套接字地址如下结构sockaddr_in表示:
struct in_addr{
in_addr_t s_addr; //IPv4地址
};
struct sockaddr_in{
sa_family_t sin_family; //地址family
in_port_t sin_port; //端口号
struct in_addr sin_adddr; //IPv4地址
};
数据类型in_port_t定义成uint16_t。数据类型in_addr_t定义成uint32_t.这些整数类型定义在<stdint.h>中定义并指定了相应的
位数。
与IPv4因特网域(AF_INET)相比,IPv6因特网域(AF_INET6)套接字使用如下结构sockaddr_in6表示:
struct in6_addr{
unit8_t s6_addr[16]; //IPv6地址
}
struct sockaddr_in6{
sa_family_t sin6_family; //地址family
in_port_t sin6_port; //端口地址
uint32_t sin6_flowinfo; //流量等级和flow信息
struct in6_addr sin6_addr; //IPv6地址
uint32_t sin6_scope_id; //set of interfaces for scope
};
这些是SUS必需的定义,每个实现可以自由地添加额外的字段。例如,在linux中,sockaddr_in定义如下:
struct sockaddr_in{
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
其中成员sin_zero为填充字段,必须全部被置为0。
inet_ntop和inet_pton这2个IP地址转换函数,可以将IP地址在点分十进制和整数之间转换,支持IPv4和IPv6。
#include<arpa/inet.h>
const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socklen_t size);
//成功则返回地址字符串指针,出错则返回NULL。
int inet_pton(int domain, const char *restrict str, void *restrict addr);
//成功则返回1,格式无小则返回0,出错则返回-1.
函数inet_ntop将网络字节序的二进制地址转换成文本字符串格式,inet_pton将文本字符串格式转换成网络字节序的二进制地址。
参数domian仅支持两个值:AF_INET和AF_INET6。
实践:
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include<arpa/inet.h>
int main(void){
char addr_p[16];
struct in_addr addr_n;
if(inet_pton(AF_INET, "192.168.1.2",&addr_n) < 0){
perror("inet_pton");
return -1;
}
printf("address:%x\n",addr_n.s_addr);
if(inet_ntop(AF_INET, &addr_n,addr_p,(socklen_t)sizeof(addr_p)) == NULL){
perror("inet_ntop");
return -1;
}
printf("address:%s\n",addr_p);
}
运行结果:
[root@yanPC apue]# ./a.out
address:201a8c0
address:192.168.1.2
2.3.地址查询
通过调用gethostent,可以找到给定计算机的主机信息。
#include<netdb.h>
struct hostent *gethostent(void); //成功则返回指针,出错则返回NULL。
void sethostent(int stayopen);
void endhostent(void);
如果主机数据文件没有打开,gethostent会打开它。函数gethostent返回文件的下一个条目。函数sethostent会打开文件,
如果文件已经被打开,那么将其回绕。函数endhostent将关闭文件。
当gethostent返回时,得到一个指向hostent结构的指针,该结构可能包含一个静态的数据缓冲区。每次调用gethostent将
会覆盖这个缓冲区。hostent至少包含如下成员:
struct hostent{
char *h_name; //name of host
char **h_aliases; //pointer to alernate 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
.....
};
通过下面的的函数获得网络名字和网络号。
#include<netdb.h>
struct netent *getnetbyaddr(uint32_t net, int type);
struct netent *getnetbyname(const char *name);
struct netent *getnetent(void);
//以上三个函数的返回值:若成功则返回指针,出错则返回NULL。
void setnetent(int stayopen);
void endnetent(void);
结构netent至少包含如下字段:
struct netent{
char *n_name; //network name
char **n_aliases; //alternate network name array pointer
int n_addrtype; //address type
uint32_t n_net; //network number
......
};
可以将协议名称和协议号采用如下函数映射。
#include <netdb.h>
struct protoent *getprotobyname(const char *name);
struct protoent *getprotobynumber(int proto);
struct protoent *getprotoent(void);
//以上函数成功则返回指针,出错则返回NULL
void setprotoent(int stayopen);
void endprotoent(void);
POSIX.1定义的结构protoent至少包含如下成员:
struct protoent{
char *p_name; //protocol name
char **p_aliases; //pointer to alternate protocol name array
int p_proto; //protocol number
......
};
服务是由地址和端口号部分表示的。每个服务由一个唯一的,熟知的端口来提供。采用函数getservbyname可以将一个服务
名字映射到一个端口号,函数getservbyport将一个端口号映射到一个服务名,或者采用函数getservent顺序扫描服务数据库。
#include<netdb.h>
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
struct servent *getservent(void);
//上面三个函数成功则返回指针,出错则返回NULL。
void setservent(int stayopen);
void endservent(void);
结构servent至少包含如下成员:
struct servent{
char *s_name; //service name
char **s_aliases; //pointer to alternate service name array
int s_port; //port number
char *s_proto; //name of protocol
......
};
POSIX.1定义了若干新的函数,允许应用程序将一个主机名字和服务名字映射到一个地址,或者相反。这些函数代替老的函数
gethostbyname和gethostbyaddr。
函数getaddrinfo允许将一个主机名字和服务名字映射到一个地址。
#include<sys/socket.h>
#include<netdb.h>
int getaddrinfo(const char *restrict host,
const char *restrict service,
const struct addrinfo *restrict hint,
struct addrinfo **restrict res);
//若成功则返回0,出错则返回非0错误码。
void freeaddrinfo(struct addrinfo *ai);
需要提供主机名字、服务名字或者两者都提供。如果仅仅提供一个名字,另外一个必须是一个空指针。主机名可以使一个
节点名或点分十进制记法表示的主机地址。
函数getaddrinfo返回一个结构addrinfo的链表。可以用freeaddrinfo来释放一个或多个这种结构。
结构addrinfo的定义至少包含如下成员:
struct addrinfo{
int ai_flags; //customize behavior
int ai_family; //address family
int ai_socktype; //socket type
int ai_protocol; //protocol
socklen_t ai_addrlen; //length int bytes of address
struct sockaddr *ai_addr; //address
char *ai_canonname; //canonical name of host
struct addrinfo *ai_next; //next in list
......
};
如果getaddrinfo失败,不能使用perror或strerror来生成错误消息,替代使用gai_strerror将返回的错误代码转换成错误消息。
#include<netdb.h>
const char *gai_strerror(int error); //指向描述错误的字符串的指针
函数getnameinfo将地址转换成主机名或者服务名。
#include <sys/socket.h>
#include<netdb.h>
int getnameinfo(const struct sockaddr *restrict addr,
socklen_t alen, char *restrict host,
socklen_t hostlen, char *restrict service,
socklen_t servlen, unsigned int flags);
//成功则返回0,出错则返回非0值。
addr被转换成主机名或者服务名,如果host非空,它指向一个长度为hostlen字节的缓冲区用于存储返回的主机名。同样,
如果service非空,它指向一个长度为servlen字节缓冲区用于存储返回的服务名。
参数flags指定一些转换的控制方式,具体如下表:
2.4.将套接字与地址绑定
对于服务器,需要给一个接受客户端请求的套接字绑定一个众所周知的地址,可以使用bind函数将地址绑定要一个套接字。
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len); //成功则返回0,出错则返回-1.
对于所使用的地址有一些限制:
1.在进程所运行的机器上,指定的地址必须有效,不能指定一个其他机器的地址。
2.地址必须和创建套接字时的地址族所支持的格式相匹配。
3.端口号必须不小于1024,除非该进程具有相应的特权。
4.一般只有套接字端点能够与地址绑定,尽管有些协议允许多重绑定。
对于因特网域,如果指定IP地址为INADDR_ANY,套接字端点可以被绑定到所有的系统网络接口。这意味着可以收到这个系统
所安装的所有网卡的数据包。如果调用connect或listen,但是没有绑定地址到一个套接字,系统会选一个地址并将其绑定到
套接字。
可以调用函数getsockname来发现绑定到一个套接字地址。
#include<sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp); //成功则返回0,出错则返回-1.
调用getsockname之前,设置alenp为一个指向整数的指针,该整数指向缓冲区sockaddr的大小。返回时,该整数会被设置
成返回地址的大小。如果该地址和提供的缓冲区长度不匹配,则将其截断而不报错。如果当前没有绑定到该套接字的地址,其
结果没有定义。
如果套接字已经和对方连接,调用getpeername来找到对方的地址。
#include<sys/socket.h>
int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp); //成功则返回0,出错则返回-1.
3.建立连接
如果处理的是面向连接的网路服务(SOCK_STREAM或SOCK_SEQPACKET),在开始交换数据以前,需要在请求服务的
进程套接字和提供服务进程套接字之间建立一个连接,可以使用connect函数
#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen *len); //成功则返回0,出错则返回-1
在connect中所指定的地址是想与之通信的服务地址,如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址。
如果套接字描述符处于非阻塞模式下,那么在连接不能马上建立时,connect将会返回-1,并且将errno设为特殊的错误码
EINPROGRESS。
函数connect还可以用于无连接的网络服务(SOCK_DGAM),如果在SOCK_DGAM套接字上调用connect,所有发送报文的
目标地址设为connect调用中所指向的地址,这样每次传送报文时就不需要再提供地址。另外,仅能接受来自指定地址的报文。
服务器调用listen来宣告可以接受连接请求。
#include<sys/socket.h>
int listen(int sockfd, int backlog); //成功则返回0,出错则返回-1.
参数backlog用于表示该进程所要入队的连接请求数量。
一旦队列满,系统会拒绝多余连接请求。
一旦服务器调用了listen,套接字就能接受连接请求。使用函数accept获得连接请求并建立连接。
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len); //成功则返回文件描述符,出错返回-1.
函数accept所返回的文件描述符时套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字
sockfd具有相同的套接字类型和地址族。传给accept的原始套接字没有关联到这个连接,而是继续保持可以状态并接受其他
连接请求。
如果不关系客户端标识,可以将参数addr和len设置为NULL。
如果没有连接请求等待处理,accept会阻塞到一个请求到来。如果sockfd处于非阻塞模式,accept会返回-1并将errno设置
为EAGAIN或EWOULDBLOCK。