文章目录
- 读取和设置 socket 选项
- SO_REUSEADDR
- SO_RCVBUF 和 SO_SNDBUF
- SO_RCVLOWAT 和 SO_SNDLOWAT
- SO_LINGER 选项
- 网络信息API
- gethostbyname 和 gethostbyaddr
- getservbyname 和 getservbyport
- getaddrinfo
- getnameinfo
读取和设置 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, void* option_value, socklen_t* restrict option_len);
// sockfd 指定被操作的socket。
// level 指定操作哪个协议的选项(属性),如:IPv4、IPv6、TCP等。
// option_name 指定选项的名字。
// option_value 和 option_len 参数分别指定操作选项值和长度。
// 成功返回0,失败返回-1并置errno。
level | option | 数据类型 | 说明 | 必须在 listen/connect 调用之前设置 |
---|---|---|---|---|
SOL_SOCKET | SO_DEBUG | int | 打开调试信息 | YES |
(通用 socket 选项,与协议无关) | SO_REUSEADDR | int | 重用本地地址。如:可以令服务器处于 TIME_WAIT 的端口立刻被使用。 | |
SO_TYPE | int | 获取 socket 类型 | ||
SO_ERROR | int | 获取并清除 socket 错误状态 | ||
SO_DONTROUTE | int | 不查看路由表,直接将数据发送给本地局域网内的主机。类同 send 系统调用的 MSG_DONTROUTE。 | YES | |
SO_RCVBUF | int | TCP接收缓冲区大小(最小值是256字节) | YES | |
SO_SNDBUF | int | TCP发送缓冲区大小(最小值是2048字节) | YES | |
SO_RCVLOWAT | int | TCP接收缓冲区低水位标记 | YES | |
SO_SNDLOWAT | int | TCP发送缓冲区低水位标记 | YES | |
SO_RCVLOWAT | int | 接收数据超时 | ||
SO_SNDLOWAT | int | 发送数据超时 | ||
SO_KEEPALIVE | int | 发送周期性保活报文以维持连接 | YES | |
SO_OBBINLINE | int | 接收到的带外数据将 在线存留 在普通数据的输入队列中,此时我们不能使用带 MSG_OOB 的读操作来读取带外数据,而应该像读取普通数据那样读取带外数据。 | YES | |
SO_LINGER | intger | 若有数据待发送,则延迟关闭。 | YES | |
IPPROTO_IP | IP_TOS | int | 服务类型 | |
(IPv4选项) | IP_TTL | int | 存活时间 | |
IPPROTO_IPV6 | IPV6_NEXTHOP | sockaddr_in6 | 下一跳IP地址 | |
(IPv6选项) | IPV6_RECVPKTINFO | int | 接受分组信息 | |
IPV6_DONTFRAG | int | 禁止分片 | ||
IPV6_RECVTCLASS | int | 接受通信类型 | ||
IPPROTO_TCP | TCP_MAXSEG | int | TCP最大报文段大小 | YES |
(TCP选项) | TCP_NODELAY | int | 禁止Nagle算法 | YES |
对 服务器 而言,部分socket选项
只能在调用 listen 系统调用前
针对 socket
设置才有效。这是因为连接 socket
只能由 accept
调用返回,而 accept
从 listen 监听队列
中接受的连接至少是个 半连接 ,这说明 服务器 已经往 客户端 上发送出了 TCP同步报文段(执行完了三次握手中的前两次)。但 部分 socket
选项只能在 TCP同步报文中设置 ,如:TCP最大报文段选项。对此有两种解决方案:
- 对于服务器而言,执行
listen系统调用
时设置这些socket选项
,那么accept
返回的连接socket
将自动继承这些选项(注1)
。 - 对于客户端而言,这些
socket选项
应该在调用connect函数
之前设置,因为connect
调用成功之后三次握手已完成。
注1
这些选项包括:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OBBINLINE、SO_RCVBUF、SO_RCVLOWAT、TCP_MAXSEG、TCP_NODELAY 。
SO_REUSEADDR
服务器程序可以通过设置本选项来强制使用被处于 TIME_WAIT
状态的连接占用的 socket
地址。此外,我们也可以通过修改内核参数 /proc/sys/net/ipv4/tcp_tw_recycle
来快速回收被关闭的 socket
,甚至使 TCP
连接根本就不进入 TIME_WAIT
状态。
int sock = socket( PF_INET, SOCK_STREAM, 0); // TCP协议,IPv4版本,基于流服务,0:使用默认协议
assert( sock >= 0); // 检测创建sock是否成功
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse) );
// 操作描述符为sock的socket套接字,操作的属性是通用socket选项
// 选项类型为SO_REUSEADDR,SO_REUSEADDR的数据类型为int
// 1即为启用SO_REUSEADDR选项
SO_RCVBUF 和 SO_SNDBUF
这两个选项分别用来表示 TCP
接收缓冲区大小和发送缓冲区大小。不过,当我们 通过setsockopt
来设置 TCP
的接收、发送缓冲区大小时,系统都会将其值加倍,并且不小于某个值。
一般来讲,TCP
接收缓冲区的最小值是 256
字节,发送缓冲区的最小值是 2048
字节(不同操作系统可能有差异)。我们可以直接修改内核参数 /proc/sys/net/ipv4/tcp_rmem
和 /proc/sys/net/ipv4/tcp_wmem
来强制缓冲区没有最小值限制。
/* 先设置 TCP 接收缓冲区的大小,然后立即读取 */
setsockopt( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof(recvbuf) );
getsockopt( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, ( socklen_t* )&len );
// sock 为目标套接字的文件描述符
// recvbuf 为设定的缓冲区大小
// len=sizeof( recvbuf );
SO_RCVLOWAT 和 SO_SNDLOWAT
该两个选项分别表示接收、发送缓冲区的低水位标记。一般被 I/O复用系统调用
用来判断 socket
是否可读或可写:
- 当
接收缓冲区中
可读数据的总数大于其低水位标记时,I/O复用系统调用
将通知应用程序可以从对应的socket
上读取数据 - 当
发送缓冲区
中的空闲空间大于其低水位标记时,I/O复用系统调用
将通知应用程序可以往对应的socket
上写入数据。
默认情况下,两者均为 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; // 滞留时间
};
根据 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 的行为取决于两个条件:
- 被关闭的
socket
对应的发送缓冲区
中是否还有残留的数据; - 该
socket
是阻塞的还是非阻塞的,阻塞:close
将等待一段长为l_linger
的时间,直到TCP模板
发送完所有残留数据并得到对方的确认。如果没发送完并得到确认,则close
返回-1
并设置errno
为EWOULDBLOCK
。非阻塞:close
将立即返回,此时需要根据其返回值
和errno
来判断残留数据是否发送完毕。
- 被关闭的
网络信息API
gethostbyname 和 gethostbyaddr
- gethostbyname: 根据主机名称获取主机的完整信息。通常先在本地的
/etc/hosts
配置文件中查找主机,没有找到再去访问DNS
服务器。 - gethostbyaddr: 根据IP地址获取主机的完整信息。
#include<netdb.h>
struct hostent* gethostbyname( const char* name );
struct hostent* gethostbyaddr( const void* addr, size_t len, int type );
// len IIP地址长度
// type 指定addr所指IP地址的类型,可以是AF_INET或AFINET6
两者的返回类型都是 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
根据名称/端口号获得某个服务的完整信息。实际上都是通过读取 /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
};
不可重入
gethostbyname
、gethostbyaddr
、getservbyname
和 getservbyport
都是不可重入的,即非线程安全的。但是 netdb.h
头文件给出了它们的可重入版本:在原函数名尾部加上 _r(re-entrant)
。
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:可以为NULL,表示允许反馈任何可用的结果。
// result:指向一个用于存储反馈结果的链表
getaddrinfo
将隐式分配堆内存,因此调用结束后,必须释放这块内存:
#include <netdb.h>
void freeaddrinfo( struct addrinfo* res );
getaddrinfo
反馈的每一条结果都是 addinfo
结构体类型的对象:
struct addrinfo
{int ai_flags; // 标志int ai_family; // 地址族int ai_socktype; // 服务类型,SOCK_STREAM或SOCK_DGRAMint ai_protocol; // 具体的网络协议,等同于socket系统调用的第三个参数,常被设为0以表自动匹配对应协议。socklen_t ai_addrlen; // socket地址ai_addr的长度char* ai_canonname; // 主机的别名struct sockaddr* ai_addr; // 指向socket地址struct addrinfo* ai_next; // 指向下一个 sockinfo 结构的对象
};
ai_flags成员可以取下表中的标志的按位或:
选项 | 含义 |
---|---|
AI_PASSIVE | 套接字地址将用于调用 bind 函数,服务器通常需要设置以表接受任何本地 socket 地址上的服务请求。客户端不能设置。 |
AI_CANONNAME | 返回主机的别名 |
AI_NUMERICHOST | hostname 参数必须是IP地址字符串,避免了DNS查询。 |
AI_NUMERICSERV | 强制 service 参数必须是十进制端口号字符串,不能是服务名。 |
AI_V4MAPPED | 如果对 IPv6 地址的 getaddrinfo 请求失败,则将 IPv4 映射为 IPv6 地址格式。 |
AI_ALL | 必须和 AI_V4MAPPED 同时使用,否则将被忽略。同时返回 符合条件 和 由IPv4转换而来 的 IPv6地址 。 |
AI_ADDRCONFIG | 只有至少配置了一个IPv4/IPv6地址(除了回路地址)后,getaddrinfo 才会解析。和 AI_V4MAPPED 互斥。 |
当我们使用 hints
参数时,可以设置 addrinfo
中前四个成员,其他成员必须设置为 NULL
。
getnameinfo
内部使用 gethostbyaddr
和 getservbyport
,是否可重入取决于内部调用的函数版本是否可重入:
#include<netdb.h>
int getnameinfo( const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags );
// 将返回的主机名(服务名)存储在 host(serv) 参数指向的缓存中
// flags控制getnameinfo的行为
getaddrinfo
和 getnameinfo
成功时返回0,失败返回错误码。Linux下 strerror
函数能将数值错误码 error
转换为易读的字符串形式:
#include<netdb.h>
const char* gai_strerror( int error );