5.1 socket API
5.1.1 字节序
字节序分为大端字节序和小端字节序。
大端:整数的高位字节存储在内存的低地址,低位字节存储在高地址;
小端:反之
现在PC大多采用小端字节序,因此小端字节序又称为主机字节序
网络传输中大多采用大端字节序,因此大端字节序又称为网络字节序 Linux主机字节序和网络字节序转换函数:
#include <netinet/in.h>
unsigned long int htonl( unsigned long int hostlong ); // host to net long
unsigned short int htons( unsigned short int hostshort );
unsigned long int ntohl( unsigned long int netlong ); // net to host long
unsigned short int ntohs( unsigned short int netshort );
5.1.2 通用socket地址
socket网络编程接口中用结构体socketaddr表示socket地址
#include <bits/socket.h>
struct socketaddr
{
sa_family_t sa_family; // 地址族,与协议族相对应
char sa_data[14]; // 存放socket地址值,不同协议族的地址值具有不同含义和长度, 见下表
}
地址族与协议族对应关系如下图:
协议族对应的地址值含义和长度如下图所示:
由上图可见,14字节的sa_data无法容纳多数协议族的地址,因此Linux定义了新的通用socket地址结构体sockaddr_storage
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family; // 地址族,与协议族相对应
unsigned long int __ss_align; // 用于内存对齐
char __ss_padding[128 - sizeof(__ss_aglin)]; // 足够空间存放地址
}
5.1.3 专用socket地址
5.1.2中给出的两个通用socket显然不好用,如将IP和端口号一起存放,设置与获取IP地址和端口号还需进行位操作。
Linux为各个协议族提供了专用的socket地址结构体。
UNIX专用socket地址结构体:
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family; // 地址族:AF_UNIX
char sun_path[108]; // 文件路径名
}
TCP/IP专用socket地址结构体:
IPv4:
struct sockaddr_in
{
sa_family_t sin_family; // 地址族:AF_INET
u_int16_t sin_port; // 端口号 网络字节序
struct in_addr sin_addr; // IPv4地址 网络字节序
};
struct in_addr
{
u_int32_t s_addr;
};
IPv6:
struct sockaddr_in6
{
sa_family_t sin6_family; // 地址族:AF_INET6
u_int16_t sin6_port; // 端口号 网络字节序
u_int32_t sin6_flowinfo; // 流信息 设置为0
struct in6_addr sin6_addr; // IPv6地址 网络字节序
u_int32_t sin6_scope_id; // scope ID 尚处实验阶段
};
struct in6_addr
{
unsigned char sa_addr[16];
};
注意:所有专用地址以及sockaddr_storage类型的地址在实际使用中都需要转换为通用地址类型sockaddr(强制转换即可),因为所有socket编程接口的地址参数类型都是sockaddr。
5.1.4 IP地址转换
通常,展示时IPv4地址用点分十进制字符串表示,IPv4地址用十六进制数字符串,但编程中需将它们转换成二进制数方能使用。
IPv4: 点分十进制与网络字节序整数互相转换
#include <arpa/inet.h>
// 点分十进制字符串地址转为网络字节序整数地址, 失败返回INADDR_NONE
in_addr_t inet_addr( const char* strptr );
// 点分十进制字符串地址转为网络字节序整数地址, 成功返回1,失败返回0
int inet_aton( const char* cp, struct in_addr* inp );
// 网络字节序整数地址转为点分十进制字符串地址。
// 该函数内部用一个静态变量存储转换结果,返回值指向该静态内存,因此该函数不可重入,否则会覆盖上次的结果
char* inet_ntoa( struct in_addr in );
inet_ntoa的不可重入:
IPv4、IPv6: 点分十进制与网络字节序整数互相转换
#include <arpa/inet.h>
// 字符串IP地址转为网络字节序整数IP地址
// af指定地址族,AF_INET/AF_INET6, 成功返回1,失败范围0
int inet_pton( int af, const char* src, void* dst );
// 网络字节序整数IP地址转为字符串IP地址,失败返回NULL。
// cnt指定目标存储单元的大小,可用下面的宏来帮助指定
const char* inet_ntop( int af, const void* src, char* dst, socklen_t cnt );
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
5.1.5 创建socket
#include <sys/types>
#include <sys/socket.h>
int socket( int domain, int type, int protocol );
// domain: 底层协议族 TCP/IP设置为PF_INET或PF_INET6, UNIX设置为PF_UNIX
// type: 服务类型, TCP设置为SOCK_STREAM服务,UDP设置为SOCK_DGRAM服务
// Linux内核2.6.17起,type参数可额外与SOCK_NONBLOCK和SOCK_CLOEXEC相与
// SOCK_NONBLOCK: 创建非阻塞socket SOCK_CLOEXEC:fork后在子线程中关闭该socket
// protocol:在前两个参数构成的协议集下选择具体协议,通常唯一(由前两个参数完全确定),可设置为0选择默认协议
5.1.6 命名socket
将一个socket与socket地址绑定称为命名socket。
在服务器中,需要先命名socket,客户端才知道如何连接它;在客户端中,直接使用操作系统自动分配的socket即可。
命名socket:
#include <sys/types.h>
#include <sys/socket.h>
// 将my_addr所指向的地址分配给未命名的socketfd文件描述符, addrlen指出地址长度
// 成功返回0,失败返回-1并设置errno
// 常见errno:
// EACCESL - 指定地址为受保护地址(0~1023),仅超级用户可使用
// EADDRINUSE - 指定地址已被占用
int bind( int socketfd, const struct socketaddr* my_addr, socket_t addrlen );
5.1.7 监听socket
服务端创建的socket被绑定后,还不能立即处理客户连接,而是需要先建立一个监听队列在存放待处理的客户连接。
监听socket
#include <sys/socket.h>
// sockfd:被监听的socket文件描述符 backlog:监听队列最大长度,超过该长度,服务器将不受理新连接,向客户端反馈ECONNREFUSED错误信息
// Linux内核2.2后,backlog指处于完全连接状态(ESTABLISHED)的socket上限, 半连接状态(SYN_RCVD)的socket上限由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义
int listen( int sockfd, int backlog );
5.1.8 接受连接
服务端监听到连接请求后,使用下面的系统调用从监听队列中接受一个连接
#include <sys/types.h>
#include <sys/socket.h>
// sockfd是服务端进行监听的socket描述符, addr用于获取远端socket地址,addrlen指出该地址长度
// 成功时返回远端连接socket描述符,失败返回-1并设置error
int accept( int sockfd, struct sockaddr *addr, socklen_t *addrlen );
accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心网络状况的变化。
5.1.9 发起连接
客户端通过如下调用发起与服务端的连接
#include <sys/types.h>
#include <sys/socket.h>
// 成功返回0, 失败返回-1并设置errno
// 常见errno:
// ECONNREFUSED - 端口不存在,连接被拒绝; ETIMEOUT - 连接超时
int connect( int socket, const struct sockaddr *serv_addr, socklen_t addrlen );
5.1.10 关闭连接
socket本质也是文件,关闭socket可通过如下关闭普通文件描述符的系统调用来完成
#include <unistd.h>
// close并非立即关闭一个描述符,而是将fd的引用计数减1,当其引用计数为0时才真正关闭
// 在多进程程序中,fork将默认将父进程中的sockfd引用计数加1,因此必须在父子进程中都执行close
int close( int fd );
使用shutdown可立即终止连接
#include <sys/socket.h>
// sockfd指定被关闭的socket; howto决定shotdown的行为方式,取如下值
// SHUT_RD - 关闭sockfd上的读操作,并丢弃socket接收缓冲区中的数据
// SHUT_WR - 关闭sockfd上的写操作,并在真正关闭连接前将socket发送缓冲区中的数据全部发送,连接进入半关闭状态
// SHUT_RDWR - 上两个的集合
// 成功返回0, 失败返回-1并设置errno
int shutdown( int sockfd, int howto );
5.2 数据读写
5.2.1 TCP数据读写
socket本质还是文件,对文件的读写操作read和write也适用于socket。
但socket接口提供了几个专门的系统调用,他们增加了对数据读写的控制。
TCP数据读写:
#include <sys/types.h>
#include <sys/socket.h>
// sockfd:读取目标socket; buf:读缓冲区; len:读缓冲区长度; flags:定义如下,通常设置为0
// 成功时返回实际读取到的数据长度,可能小于len,出错时返回-1并设置errno
// 可能要多次调用recv才能读到完整数据
ssize_t recv( int sockfd, void *buf, size_t len, int flags );
// sockfd:写入目标socket; buf:写缓冲区; len:写缓冲区长度; flags:
// 成功时返回实际写入数据长度,失败返回-1并设置errno
ssize_t send( int sockfd, const void *buf, size_t len, int flags );
flags参数可选值:
flags参数只对send和recv的当前调用生效,后续将介绍通过setsockopt系统调用永久修改socket的某些属性。
5.2.2 UDP数据读写
UDP数据读写:
#include <sys/types.h>
#include <sys/socket.h>
// 前部分参数与前一致,但UDP没有连接的概念,每次都获取对向的socket信息
// 这两个函数也可用于TCP通信,将后两个参数设置NULL即可
ssize_t recvfrom( int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen );
ssize_t sendto( int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen );
5.2.3 通用数据读写
适用于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 );
struct msghdr
{
void* msg_name; // 指向通信对方socket地址,TCP通信时设为NULL
socklen_t msg_namelen; // 对方socket地址的长度
struct iovec* msg_iov; // 分散的内存块。 (分散读,集中写)recv时,数据被读取并分散在msg_iovlen的内存中,这些内存的位置和长度由msg_iov指向的数组指定; send时,msg_iovlen块内存中的数据被一并发送。
int msg_iovlen; // 分散内存块数量
void* msg_control; // 辅助数据的起始地址,后续13章中介绍使用辅助数据实现进程间传递文件描述符
socklen_t msg_controllen; // 辅助数据的大小
int msg_flags; // 无需设定,复制函数中的flags参数,并在调用过程中更新
};
struct iovec
{
void *iov_base; // 内存起始地址
size_t iov_len; // 此块内存长度
};
5.3 带外标记
实际应用中,通常无法预期带外数据何时到来。
Linux内核检测到TCP紧急标记时,将通知应用程序有带外数据需要接收,通知方式有:I/O复用产生的异常事件和SIGURG信号。
应用程序接收到通知后,通过如下系统调用获取带外数据在数据流中的位置:
#include <sys/socket.h>
// 判断sockfd是否处于带外标记,即下一个读取数据是否是带外数据
// 是则返回1, 则用带MSG_OOB的recv调用来接收带外数据
// 不是返回0
int sockatmark( int sockfd );
5.4 地址信息函数
#include <sys/socket.h>
// 获取sockfd对应的本端socket地址,存储于address指向位置中,长度存储于address_len指向位置中。若地址长度大于address指向内存大小,则socket将被截断
// 成功返回0, 失败返回-1并设置errnos
int getsockname( int sockfd, struct sockaddr* address, socklen_t* address_len );
int getpeername( int sockfd, struct sockaddr* address, socklen_t* address_len );
5.5 socket选项
专门用于读取和设置socket文件描述符属性的方法:
#include <sys/socket.h>
// sockfd: 操作目标socket文件描述符; level:协议选项; option_name:属性选项名; option_value:属性选项值; option_len:属性选项长度
// 成功返回0,失败返回-1并设置errno
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 );
socket选项如下:
有部分socket选项只能在TCP同步报文段中设置,因此这些socket选项只能在socket监听之前(TCP三次握手之前)进行设置,这样,accept返回的socket将自动继承设置的选项。
这些选项包括:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG、TCP_NODELAY
对客户端而言,这些选项应在connect之前设置,因此connect调用返回后,TCP握手已经完成。
5.5.1 SO_REUSEADDR
服务器程序可通过设置socket选项SO_REUSEADDR来强制使用处于TIME_WAIT状态的连接占用的socket地址。
重用本地地址:
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
int reuse = 1;
// 设置SO_REUSEADDR后,即使socket处于TIME_WAIT状态,与之绑定的socket地址也可被立即重用
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 ) );
assert( ret != -1 );
此外,我们可通过设置内核参数/proc/sys/net/ipv4/tcp_tw_recycle来快速回收被关闭的socket,使得TCP连接根本不进入TIME_WAIT状态。
5.5.2 SO_RCVBUF和SO_SNDBUF
SO_RCVBUF和SO_SNDBUF分别设置TCP接收缓冲区和发送缓冲区大小。
但是,当使用setsockopt来设置TCP接收缓冲区和发送缓冲区大小时,系统会自动将其值加倍并使其不小于某个最小值,这么做的目的是确保TCP连接有足够的空闲缓冲区来处理拥塞。TCP接收缓冲区最小值为256字节,发送缓冲区最小值为2048字节(不同系统有不同默认值)。
此外可通过设置内核参数/proc/sys/net/ipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem来强制TCP接收缓冲区和发送缓冲区的大小没有最小值限制。(在16章介绍这两个参数)
5.5.3 SO_RCVLOWAT和SO_SNDLOWAT
SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。
它们一般被I/O复用系统调用来判断socket是否可读或可写。当接收缓冲区中可读数据总数大于其低水位标记时,I/O复用系统调用将通知应用程序对应socket可读;当发送缓冲区空间空间大于其低水位标记时,通知应用程序对应socket可写。
默认情况下,这两个低水位标记均为1字节。
5.5.4 SO_LINGER
SO_LINGER选项用于控制close系统调用在关闭TCP连接时的行为。
默认情况下,使用close系统调用来关闭socket时,close将立即返回,TCP模块负责将该socket发送缓冲区中的残留数据发送给对方。
设置(获取)一个SO_LINGER选项值时,需通过setsockopt(getsockopt)系统调用传递一个linger类型的结构体。
#include <sys/socket.h>
/*
根据linger中的两个成员的值组合,close系统调用有如下3中行为:
1、l_onoff = 0
关闭该选项,close用默认行为关闭socket
2、l_onoff != 0; l_linger = 0
close系统调用立即返回,并丢弃对应socket的TCP发送缓冲区残留数据,给对方发送一个复位报文段
3、l_onoff != 0; l_linger != 0
若是阻塞的socket,close系统调用等待l_linger时间,等待TCP模块发送完所有残留数据并得到对方确认。
若l_linger时间内TCP模块未得到对方确认,close返回-1并设置errno为EWOULDBLOCK。
若是非阻塞socket,close立即返回,需根据返回值和errno来判断残留数据是否发送完毕。
(阻塞和非阻塞将在第8章讨论)
*/
struct linger
{
int l_onoff; // 非0:开始 0:关闭
int l_linger; // 滞留时间
}
5.6 网络信息API
5.6.1 获取主机信息
#include <netdb.h>
// 根据主机名获取主机完整信息,先在本地/etc/hosts配置文件中找,没有再访问DNS服务器找
struct hostent* gethostbyname( const char* name );
// 根据IP获取主机完整信息
// addr: 主机IP len: IP长度 type: AF_INET/AF_INET6
struct hostent* geyhostbyaddr( const void* addr, size_t len, int type );
struct hostent
{
char* h_name; // 主机名
char** h_aliases; // 主机别名列表
int h_addrtype; // 地址类型
int h_length; // 地址长度
char** h_addr_list; // 主机IP地址列表(网络字节序)
}
5.6.2 获取服务信息
#include <netdb.h>
// 根据名称获取服务完整信息
// proto: "tcp"/"udp"/NULL-所有
struct servent* getservbyname( const char* name, const char* proto );
// 根据端口号获取服务完整信息
struct servent* getservbyport( int port, const char* proto );
struct servent
{
char* s_name; // 服务名
char** s_aliases; // 服务别名列表
int s_port; // 服务端口号
char* s_proto; // 服务类型 “tcp” "udp"
}
注意,上述4个函数不可重入,非线程安全。<netdb.h>提供了它们的可重入版本,函数名为原函数名加后缀_r。
5.6.3 getaddrinfo
getaddrinfo函数既能通过主机名获得IP地址,也能通过服务名获得端口号。
#include <netdb.h>
/*
hostname: 主机名或字符串IP地址(ipv4使用点分十进制字符串,v6使用十六进制字符串)
service: 服务名或字符串端口号
hints: 提示,以控制函数输出 设置为NULL表示允许getaddrinfo返回任何可用结果
result: 指向返回结果链表
*/
int getaddrinfo( const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result );
struct addinfo
{
int ai_flags; // 见下表
int ai_family; // 地址族
int ai_socktype; // SOCK_STREAM / SOCK_DGRAM
int ai_protocol; // 具体网络协议,通常被设置为0
socklen_t ai_addrlen; // socket地址ad_addr的长度
char* ai_canonname; // 主机别名
struct sockaddr* ai_addr; // 指向socket地址
struct addrinfo* ai_next; // 指向下一个对象
}
ai_flags成员可取下表按位或:
使用hints参数限制函数输出时,可设置其ai_flags, ai_family, ai_socktype, ai_protocol字段,其余字段必须设为NULL。
struct addrinfo hints;
struct addrinfo* res;
bzero( &hints, sizeof(hints) );
hints.ai_socktype = SOCK_STREAM;
// 使用hints参数获取主机myhost上daytime服务的流服务信息
getaddrinfo("myhost", "daytime", &hints, &res);
此外,getaddrinfo将隐式分配堆内存,因此上例代码中不需给res开辟内存空间,当函数调用结束,需使用配对函数freeaddrinfo( struct addrinfo* res )来释放内存;
5.6.4 getnameinfo
getnameinfo函数能通过socket地址同时获得字符串主机名和服务名。
#include <netdb.h>
// flags参数控制函数的行为,见下表
// 成功返回0,失败返回错误码
int getnameinfo( const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags );
flags参数:
getaddrinfo和getnameinfo返回的错误码:
#include <netdb.h>
// 将错误码转换成易读字符串形式
const char* gai_strerror( int error );