socket地址API
主机字节序和网络字节序
字节序问题:(32位机)现代CPU累加器一次能装载4字节【一个整数】,这4字节在内存中的排列顺序会影响其被累加器装载成整数的值
字节序分为大端字节序和小端字节序:
字节序 | 大端字节序 | 小端字节序 |
---|---|---|
存储 | 一个整数的高位字节存储在内存的低地址位低位字节存储在内存的高地址处 | 整数的高位字节存储在内存的高地址处低位字节存储在内存的低地址处 |
别称 | 网络字节序 | 主机字节序 |
使用 | 为所有接收数据的主机提供一个正确解释的格式化数据的保证 | 现代PC大多采用小端字节序 |
判断机器的字节序
#include <stdio.h>
void byteorder()
{
union
{
short value;
char union_bytes[sizeof(short)];
}test;
test.value=0x0102;
if((test.union_bytes[0]==1)&&(test,union_bytes[1]==2))
printf("大端字节序[big endian]\n");
else if((test.union_bytes[0]==2)&&(test,union_bytes[1]==1))
printf("小端字节序[little endian]\n");
else
printf("不确定\n");
}
大端字节序的使用
当格式化的数据(eg:32bit整型数、16bit短整型数)在两台用不同字节序的主机间直接传递必然错误。
解决:发送端总是把要发送的数据转换为大端字节序再发送,接收端会认为收到的数据总为大端字节序,接收端根据自己的字节序来决定是否对接收到的数据进行转换【小端机转换,大端机不转换】
注意:即使同一机器上两进程通讯也要考虑字节序问题
操作:主机字节序与网络字节序间转换
#include <netinet/in.h>
//长整型的主机字节序转化为网络字节序
unsigned long int htonl(unsigned long int hostlong);
//短整形的主机字节序转换为网络字节序
unsigned short int htons(unsigned short int hostshort)
//长整型的网络字节序转换为主机字节序
unsigned long int ntohl(unsigned long int netlong)
//短整形的网络字节序转换为主机字节序
unsigned short int ntohs(unsigned short int netlong)
通用socket地址
socket网络编程接口表示socket地址的是结构体sockaddr:
#include <bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
其中sa_family_t为地址族类型,常与协议族类型对应。sa_data成员用于存放socket地址值,不同协议地址值有不同的含义和长度。关系如下:
协议族 | 地址族 | 描述 | 地址值含义/长度 |
---|---|---|---|
PF_UNIX | AF_UNIX | UNIX本地域协议族 | 文件路径名/长可达108字节 |
PF_INET | AF_INET | TCP/IPv4协议族 | 16bit端口号、32bitIPv4地址/6字节 |
PF_INET6 | AF_INET6 | TCP/IPv6协议族 | 16bit端口号、32bit流标识、128bitIPv6地址、32bit范围ID/共26字节 |
根据上表地址值长度,14字节的sa_data无法完全容纳多数协议值的地址值,Linux定义了新的通用socket结构体:
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;//内存对齐
char __ss_padding[128-sizeof(__ss_align)];
};
这两个通用socket地址结构体在设置与获取IP地址时要进行繁琐的位运算,专用socket地址进行改善。
专用socket地址
UNIX本地协议族使用的专用socket地址:
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family; //地址族:AF_UNIX
char sun_path[108]; //文件路径名
};
TCP/IP协议族中:
- IPv4:sockaddr_in
struct in_addr
{
u_int32_t s_addr;//IPv4地址,网络字节序表示
};
struct sockaddr_in
{
sa_family_t sin_family;//地址族:AF_INET
u_int16_t sin_port; //端口号,网络字节序标识
struct in_addr sin_addr;
};
- IPv6:sockaddr_in6
struct in6_addr
{
unsigned char sa_addr[16];//IPv6地址,网络字节序表示
};
struct sockaddr_in6
{
sa_family_t sin6_family;//地址族:AF_INET6
u_int16_t sin6_port; //端口号,网络字节序表示
u_int32_t sin6_flowinfo;//流信息
struct in6_addr sin6_addr;
u_int32_t sin6_scope_id;
};
所有socket编程接口使用的地址参数类型都是sockaddr,所有专用socket地址(含上面改进的)类型的变量在使用时要转化为sockaddr类型【强制转换】
IP地址转换函数
#include <arpa/inet.h>
//用点分十进制字符串表示的IPv4地址转换为用网络字节序整数表示的IPv4地址
//失败返回INADDR_NONE
in_addr_t inet_addr(const char *strptr);
//用点分十进制字符串表示的IPv4地址转换为用网络字节序整数表示的IPv4地址
//将转化结果存在参数inp指向的地址结构中
//成功返回1,失败返回0
int inet_aton(const char *cp,struct in_addr *inp);
//将用网络字节序表示的IPv4地址转化为点分十进制字符串表示的IPv4地址
char* inet_ntoa(struct in_addr in);
注意:inet_ntoa内部用一个静态变量存储转化结果,函数返回值指向静态内存——>inet_ntoa是不可重入的。
下面更新的函数可完成相同功能且同时适用于IPv4、IPv6地址:
#include <arpa/inet.h>
//将用字符串表示的IP地址src转换为用网络字节序整数表示的IP地址,
//并把转化结果存在dst指向的内存中,af指定地址族
//src——点分十进制表示的IPv4地址/十六进制字符串表示的IPv6地址
//成功返回1,失败返回0并设置errno
int inet_pton(int af,const char* src,void* dst);
//进行相反的转换,cnt指定目标存储单元大小,其他参数同上
//成功返回目标存储单元,失败返回NULL并设置errno
const char* inet_ntop(int af,const void* src,char* dst,socklen_t cnt);
//用于指定cnt的两宏
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16;
#define INET6_ADDRSTRLEN 46;
创建socket
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
- domain:告诉系统使用哪个底层协议【TCP/IP协议:参数为PF_INET/PF_INET6;UNIX本地协议族:参数为PF_UNIX】
- type:指定服务类型。服务类型有SOCK_STREAM服务【流服务】和SOCK_UGRAM【数据报服务】。对TCP/IP协议族而言,SOCK_STREAM表示传输层使用TCP协议;SOCK_UGRAM表示传输层使用UDP协议
- protocol:在前两参数构成的协议集合下再选择一个协议。常设置为0表示默认协议
- socket调用成功返回一个socket文件描述符,失败返回-1并设置errno
仅指定地址族,没有指定使用地址族中哪个具体socket接口
命名socket
命名socket:将一个socket与socket地址绑定。
只有命名后客户端才知道如何连接socket。客户端通常不需要命名socket,而是匿名,即使用OS自动分配的socket地址。
操作:
#include <sys/types.h>
#include <sys/socket.h>
//将my_addr所指向的socket地址分配给未命名的sockfd文件描述符
//addrlen指出该socket地址的长度
//成功返回0,失败返回-1并设置errno
int bind(int sockfd,const struct sockaddr* my_addr,socklen_t addrlen);
常见errno:
- EACCES:被绑定地址受保护,仅超级用户能访问
- EADDRINUSE:被绑定地址正在使用
监听socket
命名socket后不能马上接受客户连接,需要创建一个监听队列存放待处理的客户连接
#include <sys/socket.h>
int listen(int sockfd,int backlog);
- sockfd:指定被监听的socket
- backlog:提示内核监听队列的最大长度。如果监听队列长度超过该值,服务器不再受理新的客户连接,客户端收到ECONNREFUSED错误信息。典型值为5
内核版本2.2前Linux:backlog指所有处于半连接状态【SYN_RCVD】、完全连接状态【ESTABLISHEN】的socket的上限
内核版本2.2后:仅表示处于完全连接状态的socket上限,处于半连接状态socket上限由内核参数【/proc/sys/net/ipv4/tcp_max_sys_backlog】定义
接受连接
从listen监听队列中接受一个连接:
(服务器被动接受连接)
#include <sys/type.h>
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
- sockfd:执行过listen系统调用的监听socket
- addr:获取被接受连接的远端socket地址【该socket地址长度由addrlen指出】
- accept成功返回一个新连接的socket,该socket唯一的标识被接受的此连接,服务器可通过该socket与被接受连接对应客户端通信;失败返回-1并设置errno
accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。
发起连接
(客户端主动与服务器建立连接)
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addrlen);
- sockfd:由socket系统调用返回一个socket
- serv_addr:服务器监听的socket地址
- addrlen:指定socket地址的长度
- 成功返回0,且一旦成功连接,sockfd就唯一标识该连接,客户端通过读写sockfd与服务器进行通信。connect失败返回-1并设置errno。常见errno:
ECONNREFUSED:目标端口不存在
ETIMEDOUT:连接超时
关闭连接
关闭连接对应的socket,可使用关闭普通文件描述符的系统调用:
#include <unistd.h>
int close(int fd);
- fd:带关闭的socket。【并非总是立即关闭一个连接,而是将fd的引用计数器减一,减到0时为关闭连接】
如果在多进程程序中调用fork,会使父进程中打开的socket的引用计数加1,那么在关闭连接时要在父进程、子进程中都对该socket执行close。
如果必须立即直接终止连接而非计数器减一,使用shutdown系统调用【专门为网络编程设计】
#include <sys/socket.h>
int shutdown(int sockfd,int howto);
- sockfd:待关闭的socket
- howto:决定shutdown的行为,如下表:
- 成功返回0,失败返回-1并设置errno
数据读写
对文件的读写操作同样适用于socket。不过·socket编程接口提供了专门用于socket数据读写的系统调用
TCP数据读写
TCP流数据读写调用接口:
#include <sys/types.h>
#include <sys/socket.h>
//读取sockfd上数据,buf、len分别指定读缓冲区的位置和大小,flags常为0
//成功返回读到的数据长度,可能小于期望的len,需多次调用才能读取到完整数据
//可能返回0:通信对方关闭连接。出错返回-1并设置errno
ssize_t recv(int sockfd,void *buf,size_t len,int flags);
//往sockfd上写入数据,buf和len指定写缓冲区的位置和大小。
//成功返回写入的数据长度,失败返回-1并设置errno
ssize_t send(int sockfd,const void *buf,size_t len,int flags);
flags为数据收发提供额外的控制,可取下列选项中的一个或多个的逻辑或
UDP数据读写
#include <sys/types.h>
#include <sys/socket.h>
//读取sockfd上数据,buf、len指定缓冲区位置长度
//UDP没有连接,每次读取数据都要获取发送端socket地址,addrlen指定该地址长度
//成功返回读到的数据长度,可能小于期望的len,需多次调用才能读取到完整数据
//可能返回0:通信对方关闭连接。出错返回-1并设置errno
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen);
//src_addr指定接收端socket地址
//成功返回写入的数据长度,失败返回-1并设置errno
ssize sendto(int sockfd,const void *buf,size_t len,int flags,const struct sockaddr *src_addr,socklen_t *addrlen);
虽然UDP不面向连接,但recvfrom/sendto系统调用也可用于面向连接的socket数据读写【将后面两个参数置NULL】
通用数据读写函数
不仅用于TCP流数据,也用于UDP数据报
#include <sys/socket.h>
ssize_t recvmsg(int sockfd,struct msghdr* msg,int flags);
ssize_t sendmsg(int sockfd,struct msghdr* msg,int flags);
其中msghdr结构体定义:
struct msghdr
{
void* mag_name;//socket地址
socklen_t mag_namelen;//socket地址长度
struct iovec* msg_iov;//分散的内存块
int msg_iovlen;//分散内存块的数量
void* msg_control;//指向辅助位置的起始数据
socklen_t msg_controllen;//辅助数据的大小
int msg_flags;//复制函数中flags参数,在调用过程中更新
};
- msg_name指向一个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被设置为NULL.这是因为对数据流socket而言,对方的地址已经知道。
- msg_namelen 成员指定了msg_name 所指socket地址的长度。.
- msg_iov成员是iovec结构体类型的指针,iovec结构体的定义:
struct iovec
{
void *iov_base;//内存地址起始位置
size_t iov_len;//地址长度
}
msg_iovlen指定这样的iovec结构对象有多少个。对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读;对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写
- msg_control和msg_controllen用于辅助数据的传送。
- msg_flags成员无须设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags 中。
带外标记
在实际应用中,我们通常无法预期带外数据何时到来。在Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是: I/O复用产生的异常事件和SIGURG信号。即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。系统调用实现:
#include <sys/socket.h>
//判断sockfd是否处于带外标记
//【下一个被读到的数据是否为带外数据,是返回1,不是返回0】
int sockatmark(int sockfd);
若是带外数据,可利用MSG_OOB标志的recv调用来接受带外数据
地址信息函数
如果想知道一个连接socket的本端socket地址,以及远端的socket地址。使用下面这两个函数:
#include <sys/socket.h>
//获取sockfd对应的本端socket地址并存在address指定的内存中
//该socket地址长度存在address_len指向变量中
//如果实际socket地址长度大于address所指内存大小,该socket地址将被截断
//成功返回0,失败返回-1并设置errno
int getsockname(int sockfd,struct sockaddr* address,socklen_t* address_len);
//获取sockfd对应的远端socket地址
//成功返回0,失败返回-1并设置errno
int getpeername(int sockfd,struct sockaddr* address,socklen_t* address_len);
socket选项
如果说fcntl系统调用是控制文件描述符属性的通用POSIX方法,那么下面两个系统调用则是专门用来读取和设置socket文件描述符属性的方法:
#include <sys/socket.h>
int getsockopt(int sockfd,int level,int option_name,void *option_value,socklen_t* restrict option_len);
int setsockopt(int sockfd,int level,int option_name,const void *option_value,socklen_t option_len);
//两函数成功返回0,失败返回-1并设置errno
- sockfd指定被操作的目标socket
- level指定要操作哪个协议的选项【属性】如IPv4、IPv6、TCP等
- option_name参数则指定选项的名字
- option_value和option_len 参数是被操作选项的值和长度。(不同的选项具有不同类型的值,如表中数据类型列)
对服务器而言,部分socket选项只能在调用listen系统调用前针对监听socket设置才有效。【将执行listen调用的socket】这是因为连接socket只能由accept调用返回,而accept从listen监听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤(因为listen监听队列中的连接至少已进入SYN_RCVD状态),这说明服务器已经向被接受连接上发送出了TCP同步报文段。
有的socket选项应该在TCP同步报文段中设置,比如TCP最大报文段选项。对这种情况,Linux 给开发人员提供的解决方案是:对监听socket设置这些socket选项,那么accept返回的连接socket将自动继承这些选项。这些socket选项包括: SO_DEBUG、SO_DONTROUTE、SO_ KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_ SNDBUF、SO_SNDLOWAT、TCP_MAXSEG和TCP_NODELAY.
对客户端而言,这些socket选项则应该在调用connect函数之前设置,因为connect调用成功返回之后,TCP三次握手已完成。
SO_REUSEADDR选项
服务器程序可通过设置socket选项SO_REUSEADDR强制使用被处于TIME_WAIT状态的连接占用的socket地址。
实现:
int sock=socket(PF_INET,SOCK_STREAM,0);
assert(sock>=0);
int reuse=1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family=AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr);
address.sin_port=htons(port);
int ret=bind(sock,(struct sockaddr*)&address,sizeof(address));
setsockopt设置后,即使sock处于TIME_WAIT状态,其绑定的socket地址也能立刻被重用。
可通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle快速回收关闭的socket,使得TCP连接不进入TIME_WAIT状态,从而允许程序立即重用本地的socket地址。
SO_RCVBUF、SO_SNDBUF选项
两选项分别表示TCP接收缓冲区和发送缓冲区的大小。不过,当使用setsockopt来设置TCP的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某最小值。TCP接收缓冲区的最小值是256字节,而发送缓冲区的最小值是2048字节(不同的系统可能有不同的默认最小值)。
系统这样做的目的,主要是确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞(eg:快速重传算法就期望TCP接收缓冲区能至少容纳4个大小为SMSS的TCP报文段)
此外,也可以直接修改内核参数/proc/sys/netipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem来强制TCP接收缓冲区和发送缓冲区的大小没有最小值限制。
SO_RCVLOWAT、SO_SNDLOWAT选项
两选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。它们一般被I/O复用系统调用用来判断socket是否可读或可写。
当TCP接收缓冲区中可读数据的总数大于其低水位标记时,I/O 复用系统调用将通知应用程序可以从对应的socket上读取数据;当TCP发送缓冲区中的空闲空间(可写入数据的空间)大于其低水位标记时,I/O 复用系统调用将通知应用程序可以往对应的socke上写入数据。
默认情况下,TCP接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1字节。
SO_LINGER选项
用于控制close系统调用在关闭TCP连接时的行为。默认情况下,当使用close系统调用来关闭一个socket时,close将立即返回,TCP模块负责把该socket对应的TCP发送缓冲区中残留的数据发送给对方
设置(获取)SO_LINGER选项的值时,需要给setsockopt/getsockopt系统调用传递一-个linger类型的结构体:
#include <sys/socket.h>
struct linger
{
int l_onoff;//开启——非0;关闭——0
int l_linger;//滞留时间
};
根据结构体中变量的不同,close系统调用可能产生以下行为:
- l_onoff == 0,SO_LINGER选项不起作用。close用默认行为关闭socket
- l_onoff != 0,l_linger == 0。close系统调用立即返回,TCP模块将丢弃被关闭的socket对应的TCP发送缓冲区中残留数据,同时给对方发送复位报文段——给服务器提供异常终止一个连接的方法
- l_onoff != 0,l_linger > 0。close行为取决于两个条件:1.被关闭socket对应的TCP发送缓冲区中是否有残留数据;2.该socket是阻塞还是非阻塞的。对于阻塞的socket,close将等待一段长为l_linger的时间,直到TCP模块发送完所有残留数据并得到对方的确认。如果这段时间内TCP模块没有发送完残留数据并得到对方的确认,那么close将返回-1并设置ermo为EWOULDBLOCK。如果socket是非阻塞的,close将立即返回,此时需要根据其返回值和errno来判断残留数据是否已经发送完毕。
网络信息API
gethostbyname&gethostbyaddr
gethostbyname函数根据主机名称获取主机的完整信息,gethostbyaddr函数根据IP地址获取主机的完整信息。gethostbyname函数通常先在本地的/etc/hosts配置文件中查找主机,如果没有找到,再去访问DNS服务器。函数定义:
#include <netdb.h>
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void*addr,size_t len,int type);
- name:指定目标主机的主机名
- addr:指定目标主机的IP地址
- len:指定addr所指IP地址的长度
- type:指定addr所指IP地址的类型,其合法取值包括AF_INET (用于IPv4地址)和AF_INET6 (用于IPv6地址)。
- hostent结构体:
#include <netdb.h>
struct hostent
{
char* h_name;//主机名
char** h_aliases;//主机别名列表
int h_addrtype;//地址类型/地址族
int h_length;//地址长度
char** h_addr_list;//按网络字节序列出的主机IP地址列表
};
getservbyname&getservbyport
getservbyname兩数根据名称获取某个服务的完整信息,getservbyport 函数根据端口号获取某个服务的完整信息
它们实际上都是通过读取/etc/services文件来获取服务的信息的
定义:
#include <netdb.h>
struct servent* getservbyname( const char* name, const char* proto);
struct servent* getservbyport( int port,const char* proto ):
- name:指定目标服务的名字
- port:指定目标服务对应的端口号
- proto:指定服务类型(给它传递“tcp" 表示获取流服务,给它传递“udp"表示获取数据报服务,给它传递NULL则表示获取所有类型的服务)
结构体servent:
#include <netdb.h>
struct servent
{
char* s_name;//服务名称
char** s_aliases;//服务的别名列表
int s_port;//端口号
char* s_proto;//服务类型(tcp、udp)
};
getaddrinfo
既能通过主机名获得IP地址(内部使用gethostbyname函数),也能通过服务名获得端口号(内部使用getservbyname函数)。它是否可重入取决于其内部调用的gethostbyname和getservbyname函数是否是它们的可重入版本。函数的定义:
#include <netdb.h>
int getaddrinfo(const char* hostname,const char* service,const struct addrinfo* hints,struct addrinfo** result);
- hostname可以接收主机名,也可以接收字符串表示的IP地址(IPv4 采用点分十进制字符串,IPv6 则采用十六进制字符串)
- service可以接收服务名,也可以接收字符串表示的十进制端口号
- hints是应用程序给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制。hints 参数可以被设置为NULL,表示允许getaddrinfo反馈任何可用的结果
- result指向一个链表,该链表用于存储getaddrinfo反馈的结果【结构体addrinfo类型的对象】
结构体addrinfo:
struct addrinfo
{
int ai_flags;
int ai_family;//地址族
int ai_socktype;//服务类型【SOCK_STREAM/SOCK_DGRAM】
int ai_protocol;//具体的网络协议,常设为0
socklen_t ai_addrlen;//socket地址ai_addr长度
char* ai_canonname;//主机别名
struct sockaddr* ai_addr;//指向socket地址
struct addrinfo* ai_next;//指向下一条sockinfo结构的对象
};
使用时可设置前四个字段,其他字段置NULL
ai_flags成员可按表按位或
该函数会隐式分配堆内存(res原来没有指向一块合法空间),所以调用结束后要释放内存:
#include <netdb.h>
void freeaddrinfo(struct addrinfo* res);
getnameinfo
能通过socket地址同时获得以字符串表示的主机名(内部使用gethostbyaddr函数)和服务名(内部使用getservbyport函数)。它是否可重入取决于其内部调用的gethostbyaddr和getservbyport函数是否是它们的可重入版本
函数的定义:
#include <netdb.h>
int getnameinfol const struct sockaddr* sockaddr, socklen. t addrlen, char" host,socklen_ t hostlen, char" serv, socklen_ t servlen, int flags ); .
- 返回的主机名存储在host参数指向的缓存中
- 返回服务名存储在serv参数指向的缓存中
- hostlen和servlen:指定这两块缓存的长度。
- flags:控制getnameinfo的行为,它可以接收下表中的选项。
- 成功返回0,失败返回错误码:
将表中错误码转换为字符串形式:
#include <netdb.h>
const char* gai_strerror(int error);