1 套接字描述
套接字是通信端点的抽象,创建一个套接字使用如下函数:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
返回值:若成功,返回套接字描述符;若出错,返回-1.
参数:
domain: 指定通信的特征,包括地址格式,以AF_开头的常数表示地址族(address family):
域 | 描述 |
AF_INET | IPv4因特网域 |
AF_INET6 | IPv6因特网域 |
AF_UNIX | UNIX域 |
AF_UPSPEC | 未指定(可以代表任何域) |
type: 指定套接字的类型。如下为POSIX.1的套接字类型,也可以增加其他类型的支持:
类型 | 描述 |
SOCK_DGRAM | 固定长度、无连接、不可靠 |
SOCK_RAM | IP协议的数据报接口 |
SOCK_SEQPACKET | 固定长度、面向连接、有序、可靠 |
SOCK_STREAM | 有序、可靠、双向、面向连接 |
protocol: 根据指定的domain和type所提供的默认协议,通常设为0即可。在AF_INET通信域中,SOCK_STREAM默认的协议为TCP;SOCK_DGRAM默认的协议为UDP.
使用无连接的数据报通信类似于邮寄信件,你不能保证传递的次序,信件可能会丢失,每封信都包含收信人地址;相反地,使用面向连接的通信协议类似于打电话,首先需要建立连接,连接建立好以后,彼此可以双向地通信,对话中不需要包含地址信息,连接本身指定了通话的源和目的,就像是端与端之间有一条虚拟链路一样。
SOCK_STREAM套接字提供了字节流服务,应用程序不能分辨出报文的界限;SOCK_SEQPACKET套接字提供了基于报文的服务,这意味着接受的数据量和发送的数据量完全一致;SOCK_RAW套接字提供了一个数据报接口,用于直接访问下面的网络层,使用该套接字时,必须有root用户权限,并且需要应用程序自己负责构造协议头。
套接字通信是双向的,可以使用shutdown函数来禁止一个套接字I/O:
#include <sys/socket.h>
int shutdown(int sockfd, int how);
返回值:若成功,返回0;若出错,返回-1.
参数:
how: SHUT_RD(关闭读),SHUT_WR(关闭写),SHUT_RDWR(关闭读写).
套接字本质上是一个文件描述符,如下为适用于文件描述符的函数在套接字中的表现行为:
文件描述符函数 | 使用套接字时的行为 |
close | 释放套接字 |
dup / dup2 | 复制套接字 |
fchdir | 失败,并且将errno设置为ENOTDIR |
fchomod | 未指定 |
fchown | 由实现定义 |
fcntl | 支持某些命令 |
fdatasync / fsync | 由实现定义 |
fstat | 支持一些stat结构成员,如何支持由实现定义 |
ftruncate | 未指定 |
ioctl | 依赖于底层设备驱动 |
lseek | 由实现定义,失败时将errno设置为ESPIPE |
mmap | 未指定 |
poll | 正常工作 |
pread / pwrite | 失败时,将errno设置为ESPIPE |
read / readv | 与没有任何标志位的recv等价 |
select | 正常工作 |
write / writev | 与没有任何标志位的send等价 |
2 寻址
:: 不同的处理器架构有着不同的字节序,如果需要实现异构通信,必须统一字节序方式。网络协议指定了字节序(网络字节序),TCP/IP协议栈使用大端方式,对于使用TCP/IP的应用程序,有4个函数可以用来在处理器字节序和网络字节序之间进行转换:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32); // 32位主机-->32位网络
uint16_t htons(uint16_t hostint16); // 16位主机-->16位网络
uint32_t ntohl(uint32_t netint32); // 32位网络-->32位主机
uint16_t ntohs(uint16_t netint16); // 16位网络-->16位主机
说明:
h: 主机字节序
n: 网络字节序
l: 4字节的长整型
s: 2字节的短整型
1 #include <arpa/inet.h> 2 #include <stdio.h> 3 4 int main(void) 5 { 6 unsigned int bytes = 0x12345678; 7 const char* p = (const char*)&bytes; 8 printf("%x_%x_%x_%x\n", p[0], p[1], p[2], p[3]); 9 10 unsigned int netbytes = htonl(bytes); /* 将主机字节序转换为网络字节序 */ 11 p = (const char*)&netbytes; 12 printf("%x_%x_%x_%x\n", p[0], p[1], p[2], p[3]); 13 14 return 0; 15 }
由于地址格式与特定的通信域相关,因此,为了使得不同格式的地址都能够传入套接字函数,所有的地址必须先被强制转换为一个通用的地址结构sockaddr:
struct sockaddr
{
unsigned char sa_len; /* total length */
sa_family_t sa_family; /* address family */
char sa_data[14]; /* variable-length address */
};
因特网地址定义在<netinet/in.h>中:
struct in_addr
{
in_addr_t s_addr; /* IPv4 address */
};
struct sockaddr_in
{
sa_family_t sin_family; /* address_family */
in_port_t sin_port; /* port number */
struct in_addr sin_addr; /* IPv4 address */
};
数据类型in_port_t为uint16_t,in_addr_t为uint32_t,在<stdint.h>中定义了这些类型。
:: 如下函数可以在数值地址和文本格式字符串地址之间进行转换:
#include <arpa/inet.h>
/* 将数值地址转换为文本字符串格式 */
const char* inet_ntop(int domain,
const void *restrict addr,
char* restrict str, socklen_t size);
/* 将文本字符串格式转换为数值地址 */
int inet_pton(int domain,
const char* restrict str,
void* restrict addr);
参数:
domain: AF_INET或者AF_INET6.
size: 指定保存文本字符串缓冲区str的大小,该参数为INET_ADDRSTREAM或者INET6_ADDRSTREAM时,表明使用足够大的空间来存放该地址。
说明:
inet_pton的输出为网络字节序,inet_ntop的输入为网络字节序,要注意转换。
1 #include <arpa/inet.h> 2 #include <stdio.h> 3 4 int main(void) 5 { 6 char dotaddr[] = "192.168.8.128"; 7 struct in_addr ipaddr; 8 9 inet_pton(AF_INET, dotaddr, (void*)&ipaddr); 10 11 ipaddr.s_addr = ntohl(ipaddr.s_addr); /* 将网络字节序转换为主机字节序 */ 12 printf("addr = %x\n", ipaddr.s_addr); 13 14 ipaddr.s_addr = htonl(ipaddr.s_addr); /* 将主机字节序转换为网络字节序 */ 15 inet_ntop(AF_INET, (void*)&ipaddr, dotaddr, 16); 16 printf("addr = %s\n", dotaddr); 17 18 return 0; 19 }
地址查询:
#include <netdb.h>
struct hostent* gethostent(void); /* 返回下一个文件 */
void sethostent(int stayopen); /* 打开文件 */
void endhostent(void); /* 关闭文件 */
:: gethostent返回一个指向hostent结构体的指针,hostent结构如下:
struct hostent
{
char *h_name; /* 主机名 */
char **h_aliases; /* 可选的别名列表 */
int h_addrtype; /* 地址类型,一般为AF_INET */
int h_length; /* 地址长度 */
char **h_addr_list; /* 网络地址列表 */
#define h_addr h_addr_list[0]; /* 第一个网络地址 */
};
说明:
之所以主机的地址是一个列表的形式,其原因是一个主机可能有多个网络接口。
返回的地址为网络字节序。
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <netdb.h> 4 #include <netinet/in.h> 5 6 int main() 7 { 8 struct hostent* h; 9 h = gethostbyname("benxintuzi"); 10 printf("host: %s\n", h->h_name); 11 printf("addr: %s\n", inet_ntoa(*((struct in_addr*)h->h_addr))); 12 13 return 0; 14 }
:: 如下函数用来获得网络名字和网络编号:
#include <netdb.h>
struct netent* getnetbyaddr(uint32_t net, int type);
struct netent* getnetbyname(const char* name);
struct netent* getnetent(void);
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; /* net address type */
uint32_t n_net; /* network number*/
};
1 #include <stdio.h> 2 #include <netdb.h> 3 4 void printnet(struct netent* net) 5 { 6 char** p = net->n_aliases; 7 printf("net name: %s\n", net->n_name); 8 9 while(*p != NULL) 10 { 11 printf("alias name: %s\n", *p); 12 p++; 13 } 14 15 printf("address type: %d\n", net->n_addrtype); 16 17 printf("net number: %u\n", net->n_net); 18 } 19 20 int main() 21 { 22 struct netent* net = NULL; 23 setnetent(1); 24 while((net = getnetent()) != NULL) 25 { 26 printnet(net); 27 printf("\n"); 28 } 29 endnetent(); 30 31 return 0; 32 }
:: 如下函数用于操作协议名和协议编号:
#include <netdb.h>
struct protoent* getprotobyname(const char* name);
struct protoent* getprotobynumber(int proto);
struct protoent* getprotoent(void);
void setprotoent(int stayopen);
void endprotoent(void);
struct protoent
{
char *p_name; /* Official protocol name. */
char **p_aliases; /* Alias list. */
int p_proto; /* Protocol number. */
};
1 #include <netdb.h> 2 3 void printproto(struct protoent* proto) 4 { 5 char** p = proto->p_aliases; 6 printf("proto name: %s\n", proto->p_name); 7 8 while(*p != NULL) 9 { 10 printf("alias name: %s\n", *p); 11 p++; 12 } 13 14 15 printf("proto number: %d\n", proto->p_proto); 16 } 17 18 int main() 19 { 20 struct protoent* proto = NULL; 21 setprotoent(1); 22 while((proto = getprotoent()) != NULL) 23 { 24 printproto(proto); 25 printf("\n"); 26 } 27 endprotoent(); 28 29 return 0; 30 }
:: 如下函数用于操作服务于端口号:
#include <netdb.h>
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
struct servent* getservent(void);
void setservent(int stayopen);
void endservent(void);
struct servent
{
char *s_name; /* Official service name. */
char **s_aliases; /* Alias list. */
int s_port; /* Port number. */
char *s_proto; /* Protocol to use. */
};
1 #include <stdio.h> 2 #include <netdb.h> 3 4 void printservent(struct servent* serv) 5 { 6 char** p = serv->s_aliases; 7 printf("servent name: %s\n", serv->s_name); 8 9 while(*p != NULL) 10 { 11 printf("alias name: %s\n", *p); 12 p++; 13 } 14 15 printf("port number: %d\n", serv->s_port); 16 17 printf("proto to use: %s\n", serv->s_proto); 18 } 19 20 int main() 21 { 22 struct servent* serv = NULL; 23 setservent(1); 24 while((serv = getservent()) != NULL) 25 { 26 printservent(serv); 27 printf("\n"); 28 } 29 endservent(); 30 31 return 0; 32 }
:: POSIX.1中定义的函数getaddrinfo用来代替过时的gethostbyname和gethostbyaddr:
#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);
void freeaddrinfo(sruct addrinfo* ai);
struct addrinfo
{
int ai_flags; /* Input flags. */
int ai_family; /* Protocol family for socket. */
int ai_socktype; /* Socket type. */
int ai_protocol; /* Protocol for socket. */
socklen_t ai_addrlen; /* Length of socket address. */
struct sockaddr *ai_addr; /* Socket address for socket. */
char *ai_canonname; /* Canonical name for service location. */
struct addrinfo *ai_next; /* Pointer to next in list. */
};
getaddrinfo函数根据提供的主机名或服务名返回一个addrinfo链表;freeaddrinfo用来释放一个addrinfo结构体。如果getaddrinfo失败了,必须调用gai_strerror将返回的错误码转换成错误消息:
const char* gai_strerror(int error);
如果host非空,则指向一个长度为hostlen字节的缓冲区,用于存放返回的主机名;同样,如果service非空,则指向一个长度为servlen字节的缓冲区,用于返回的服务名。
具体的flags参数如下:
标志 | 描述 |
NI_DGRAM | 服务基于数据报而非基于流 |
NI_NAMEREQD | 如果找不到主机名,将其作为一个错误对待 |
NI_NOFQDN | 对于本地主机,仅返回全限定域名的节点名部分 |
NI_NUMERICHOST | 返回主机地址的数字形式,而非主机名 |
NI_NUMERICSCOPE | 对于IPv6,返回数字形式 |
NI_NUMERICSERV | 返回服务地址的数字形式(即端口号) |
1 #include <stdio.h> 2 #include <arpa/inet.h> 3 #include <netdb.h> 4 #include <sys/socket.h> 5 #include <netinet/in.h> 6 7 int main(void) 8 { 9 struct addrinfo* ailist, *aip; 10 struct addrinfo hint; 11 struct sockaddr_in* sinp; 12 const char* addr; 13 int err; 14 char abuf[INET_ADDRSTRLEN]; 15 16 hint.ai_flags = AI_CANONNAME; 17 hint.ai_family = 0; 18 hint.ai_socktype = 0; 19 hint.ai_protocol = 0; 20 hint.ai_addrlen = 0; 21 hint.ai_canonname = NULL; 22 hint.ai_addr = NULL; 23 hint.ai_next = NULL; 24 25 if((err = getaddrinfo("benxintuzi", "nfs", &hint, &ailist)) != 0) 26 printf("getaddrinfo error: %s", gai_strerror(err)); 27 for(aip = ailist; aip != NULL; aip = aip->ai_next) 28 { 29 printf("host: %s\n", aip->ai_canonname ? aip->ai_canonname : "-"); 30 if(aip->ai_family == AF_INET) 31 { 32 sinp = (struct sockaddr_in*)aip->ai_addr; 33 addr = inet_ntop(AF_INET, &sinp->sin_addr, abuf, INET_ADDRSTRLEN); 34 printf("address: %s\n", addr ? addr : "Unknown"); 35 36 } 37 printf("\n"); 38 } 39 40 return 0; 41 }
:: getnameinfo将一个地址转换为一个主机名和一个服务名:
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr* addr,
socklen_t alen,
char* restrict host,
socklen_t hostlen,
char* restrict service,
socklen_t servlen,
int flags);
struct sockaddr
{
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
套接字与地址的关联:
给某个服务器关联一个众所周知的地址,使得客户端可以访问该服务器,最简单的一个办法是服务器保留一个地址并且将其注册在/etc/services中,使用bind函数:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t len);
地址中的端口号一般不小于1024,并且一般只能将一个套接字端口绑定在一个给定的地址上。
使用getsockname找到绑定到套接字上的地址:
int getsockname(int sockfd,
struct sockaddr* restrict addr,
socklen_t* restrict alenp);
alenp指向缓冲区addr的长度。返回时,该整数会被设置成返回地址的大小,如果地址和提供的缓冲区长度不匹配,则地址会被自动截断而不报错。
如果套接字已经和连接上,则可以使用getpeername来找到对方的地址:
int getpeername(int sockfd,
struct sockaddr* restrict addr,
socklen_t* restrict alenp);