Linux高性能服务器-第五章-网络编程基础

111 阅读15分钟

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地址值,不同协议族的地址值具有不同含义和长度, 见下表
}

地址族与协议族对应关系如下图:

协议族_地址族.png 协议族对应的地址值含义和长度如下图所示:

协议族_地址值.png

由上图可见,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的不可重入:

不可重入-1709607515452.png

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.png

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选项.png

有部分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成员可取下表按位或:

ai_flags.png

使用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参数:

flags参数.png getaddrinfogetnameinfo返回的错误码:

错误码.png

#include <netdb.h>
// 将错误码转换成易读字符串形式
const char* gai_strerror( int error );