【读书笔记】Linux高性能服务器编程(第二篇 第五章)

第五章 Linux网络编程基础API

 

5.1 socket地址API

5.1.1 主机字节序和网络字节序


字节序分为:

1. 大端字节序:一个整数的高位字节(23-31 bit)存储在内存的低地址处,低位字节(0-7 bit)存储在内存的高地址处。

2. 小端字节序:整数的低位字节存储在内存的低地址处,而高位字节则存储在内存的高地址处。

小端字节序又被称为主机字节序。

大端字节序又被称为网络字节序。

判断机器字节序:

union{

short value;

char union_bytes[ sizeof(short) ] ; 

}test ;

赋值给test.value 然后比较 test.union_bytes数组,可得到机器字节序。

 

Linux提供了如下4个函数来完成主机字节序和网络字节序之间的转换:

#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 netshort ) ;

长整型函数通常用来转换IP地址,短整型函数用来转换端口号(当然不限于此,任何格式化的数据通过网络传输时,都应该使用这些函数来转换字节序)。

 

5.1.2   通用socket地址

#include<bits/socket.h>

struct  sockaddr

{

sa_family_t  sa_family ;

char  sa_data[ 14 ] ;

sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称domain)和对应的地址族,如表:

PF_* AF_* 都定义在bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。


sa_data成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度,如表:


由表5-2可见,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 )] ; 

这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)。

 

5.1.3 专用socket地址

Unix 本地域协议族使用如下专用 socket 地址结构体:

#include<sys/un.h>

struct  sockaddr_un

{

sa_family_t  sin_family;   /* 地址族:AF_UNIX  */ 

char  sun_path[ 108 ] ;  /*  文件路径名  */

} ;

 

 TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,分别用于 IPv4 和 IPv6 


struct  sockaddr_in

{

sa_family_t  sin_family ;  /* 地址族:AF_INET */

u_intl6_t  sin_port ;  /* 端口号,要用网络字节序表示 */

struct  in_addr  sin_addr ;  /* IPv4地址结构体  */ 

};


struct  in_addr

{

u_int32_t  s_addr ;  /* IPv4地址,用网络字节序表示*/

};

 

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 ] ; /* IPv6地址,用网络字节序表示*/

};

所有专用socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr 

 

 

 

5.1.4 IP地址转换函数

下面三个函数可用于点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:

#include<arpa/inet.h>

in_addr_t  inet_addr ( const  char *  strptr ) ;

int inet_aton ( const  char *cp  ,  struct  in_addr * inp) ;

char * inet_ntoa ( struct in_addr  in ) ;

inet_addr 函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。失败时返回 INADDR_NONE

 

inet_aton函数完成和inet_addr 同样的功能,但是将转化结果存储于参数inp 指向的地址结构中 。 成功返回 ,失败则返回

 

inet_ntoa 函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。

注意:该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa 是不可重入的 。

 

 

 

 

 

下面这对更新的函数也能完成和前面三个函数同样的功能,并且它们同时适用于IPv4IPv6地址:

#include<arpa/inet.h>

int inet_pton ( int af , const char* src , void* dst ) ;

const char* inet_ntop( int af, ,const void* src , char* dst , socklen_t cnt) ;

inet_pton函数将用字符串表示的IP地址src(用点分十进制字符串表示的IPv4地址或用十六进制字符串表示的IPv6地址)转换成用网络字节序整数表示的IP地址,并把转换结果存储于dst 指向的内存中。

af 参数指定地址族(AF_INET或者AF_INET6

inet_pton成功时返回1,失败则返回0,并设置errno 

 

inet_ntop函数进行相反的转换,前三个参数的含义与inet_pton的参数相同,最后一个cnt指定目标存储单元的大小。

下面两个宏能帮助我们指定这个大小(分别用于IPv4IPv6

#include<netinet/in.h>

#define  INET_ADDRSTRLEN  16

#define  INET6_ADDRSTRLEN  46

inet_ntop成功时返回目标存储单元的地址,失败则返回NULL并设置errno 

 

 

 

5.2 创建socket

UNIX/Linux 的一个哲学:万物皆文件。

socket 就是可读,可写,可控制,可关闭的文件描述符。

下面的socket系统调用可创建一个socket

#include<sys/types.h>

#include<sys/socket.h>

int  socket ( int domain , int type , int protocol ) ;

domain 参数告诉系统使用哪个底层协议族,对TCP/IP协议族而言,该参数设置为PE_INET(用于IPv4PF_INET6(用于IPv6;对于UNIX本地域协议族而言,该参数应为PF_UNIX

type 参数指定服务类型服务类型主要有 SOCK_STREAM服务(流服务)和SOCK_UGRAM(数据报)服务。对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议,取SOCK_DGRAM表示传输层使用UDP协议

注意:type参数可以接受上述服务类型与下面两个重要的标志(SOCK_NONBLOCKSOCK_CLOEXEC)相与的值。

SOCK_NONBLOCK表示将新创建的socket 设为非阻塞的。

SOCK_CLOEXEC表示用fork调用创建子进程时在子进程中关闭该socket 

protocol 参数是在前两个参数构成协议集合下,再选择一个具体的协议,这个值通常唯一(前两个参数已经决定它的值),几乎所有情况下设置为0,表示使用默认协议。

socket系统调用成功时返回socket文件描述符,失败则返回 -1,设置errno

 

5.3 命名(绑定) socket

将一个socketsocket地址绑定称为给socket命名。

在服务器程序中,我们通常要命名socket,因为只有命名后客户端才能知道如何连接它。

客户端通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址。

命名socket的系统调用 bind

#include<sys/types.h>

#include<sys/socket.h>

int bind(int sockfd ,const struct sockaddr* my_addr , socklen_t addrlen ) ;

 

bind将 my_addr 所指的socket 地址分配给未命名的sockfd 文件描述符。

addrlen 参数指出该socket地址的长度。

bind成功时返回0,失败则返回-1并设置errno

其中两种常见的errnoEACCESEADDRINUSE,含义如下:

EACCES:被绑定的地址是受保护的地址,仅超级用户能够访问。

(例如:普通用户将socket绑定到知名服务端口(0-1023)时,bind将返回EACCES错误。)

EADDRINUSE:被绑定的地址正在使用中。

(例如:将socket绑定到一个处于TIME_WAIT状态的socket地址)。

 

5.4 监听socket

#include<sys/socket.h>

int  listen ( int  sockfd , int  backlog) ;

sockfd参数指定被监听的socket

backlog参数提示内核监听队列的最大长度。

监听队列的长度如果超过backlog(监听队列中完整连接的上限通常比backlog值略大),服务器将不受理新的客户连接,客户端也将受到ECONNREFUSED错误信息。

注意:backlog只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由 /proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。

backlog参数的典型值为5

listen成功时返回0,失败则返回-1并设置errno

 

5.5 接受连接

下面的系统调用从listen监听队列中接受一个连接:

#include<sys/types.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来与被接受连接对应的客户端通信。

accept失败时返回-1并设置errno

 

如果监听队列中处于ESTABLISHED状态的连接对应的客户端出现网络异常(掉线),或提早退出,那么服务器对这个连接执行的accept调用依然成功。

因此注意:accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。

 

5.6发起连接

服务器通过listen系统调用被动接受连接,客户端通过connect系统调用来与服务器建立连接。

#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参数指定这个地址的长度。

connect成功时返回0,一旦成功建立连接,sockfd就唯一标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。

connect失败则返回-1并设置errno

其中两种常见的errno是:

1. ECONNREFUSED:目标端口不存在(客户端收到复位报文段),连接被拒绝。

2. ETIMEOUT:连接超时(网络繁忙导致服务器没有发回应答报文,客户端不断重连,多次重连仍然无效则连接超时)。

 

5.7 关闭连接

关闭一个连接实际就是关闭该连接对应的socket,可以通过如下系统调用完成:

#include<unistd.h>

int  close( int  fd ) ;

fd 参数是待关闭的socket。不过,close系统调用并非总是立即关闭一个连接,而是将fd 的引用计数减一。只有当fd 的引用计数为0时,才真正关闭连接。

多进程程序中,一次fork 系统调用默认将使父进程中打开的socket的引用计数加1因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。

如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用如下shutdown系统调用。

#include<sys/socket.h>

int  shutdown ( int  sockfd , int  howto ) ;

sockfd 参数是待关闭的socket 

howto 参数决定了shutdown的行为可取下面表中的某个值:


由此可见,shutdown能够分别关闭socket上的读或写,或者都关闭。

close在关闭连接时只能将socket上的读和写同时关闭。

shutdown成功时返回0,失败则返回-1并设置errno 

 

5.8 数据读写

5.8.1  TCP数据读写

对文件的读写操作readwrite同样适用于socket

socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制,其中用于TCP流数据读写的系统调用是:

#include<sys/types.h> 

#include<sys/socket.h>

ssize_t  recv ( int  sockfd  , void *buf ,  size_t  len ,  int  flags );

ssize_t  send ( int  sockfd  , const void *buf , size_t  len , int  flags);

recv 读取 sockfd上的数据,buflen参数分别指定读缓冲区的位置和大小。

flags 参数含义见后文,通常设置为0即可。

recv成功时返回实际读取到的数据的长度。

recv可能返回0,这意味着通信对方已经关闭连接了。

recv出错时返回-1并设置errno 

flags参数为数据收发提供了额外的控制,它可以取下表中的一个或几个的逻辑或


 

5.8.2 UDP数据读写

socket编程接口中用于UDP数据报读写的系统调用时:

#include<sys/types.h>

#include<sys/socket.h>

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 ) ;

 

recvfrom 读取sockfd上的数据, buf 和 len 参数分别指定读缓冲区的位置和大小。因为UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数src_addr所指的内容,addrlen参数则指定该地址的长度。

 

sendto sockfd 上写入数据,buf 和 len 参数分别指定缓冲区的位置和大小。dest_addr参数指定接收端的socket地址,addrlen参数则指定该地址的长度。

 

这两个系统调用的flags参数以及返回值的含义均与 send/recv系统调用的flags参数以及返回值相同。

注意:recvfrom / sendto 系统调用也可以用于面向流(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略 发送端/接收端的 socket地址(因为已经建立了连接,已经知道socket地址)。


5.8.3  通用数据读写函数

socket编程接口还提供了一对通用的数据读写系统调用。

可用于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 ) ;

sockfd 参数指定被操作的目标socket, msg参数是msghdr结构图类型的指针,msghdr结构体的定义如下:


struct  msghdr{

void *msg_name ;  / * socket 地址 */

socklen_t  msg_namelen  /* socket 地址的长度 */

struct  iovec *msg_iov ;  /* 分散的内存块,见后文 */

int  msg_iovlen ;  /* 分散内存块的数量 */

void  *msg_control ; / * 指向辅助数据的起始位置 */

socklen_t  msg_controllen ;  /* 辅助数据的大小*/ 

int  msg_flags ;/*复制函数中的flag参数,并在调用过程中更新*/

};


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_controlmsg_controllen 成员用于辅助数据的传送。

msg_flags 成员无须设定,它会复制recvmsg/sendmsgflags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。

recvmsg/sendmsg flags参数以及返回值的含义与send/recvflags参数及返回值相同。

 

5.9 带外标记

Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收,内核通知应用程序带外数据到达得两种常见方式是:I/O复用产生的异常事件和SIGURG信号。

但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。

可用如下系统调用实现:

#include<sys/socket.h>

int  sockatmark ( int  sockfd ) ;

sockatmark 判断 sockfd 是否处于带外标记,即下一个读取到的数据是否是带外数据。如果是,sockatmark 返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0

 

5.10 地址信息函数

当我们想知道一个连接socket的本端socket地址,以及远端的socket地址,可调用这两个函数:

#include<sys/socket.h>

int getsockname ( int  sockfd ,  struct  sockaddr  *address ,  socklen_t  *address_len ) ;

int getpeername ( int  sockfd ,  struct  sockaddr  *address ,   socklen_t  *address_len ) ;

getsockname获取sockfd对应的本端socket地址,并将其存储于address参数指定的内存中,该socket地址的长度则存储于

address_len参数指向的变量中。如果实际socket地址的长度大于address所指内存区的大小,那么该socket地址将被截断。

getsockname成功时返回0,失败返回-1并设置errno

getpeername获取sockfd对应的远端socket地址,其参数及返回值的含义与getsockname的参数及返回值相同。

 

5.11    socket选项

下面两个系统调用则是专门用来读取和设置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 );

sockfd参数指定被操作的目标socket。 

level 参数指定要操作哪个协议的选项(即属性,比如IPv4IPv6TCP等)。

option_name 参数则指定选项的名字。

下表列举了socket通信中几个比较常用的socket选项,option_valueoption_len参数分别是被操作选项的值和长度,不同选项具有不同类型的值。



getsockoptsetsockopt 这两个函数成功时返回0,失败时返回-1并设置errno 

注意,对服务器而言,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效。这是因为连接socket只能

accept调用返回,而acceptlisten监听队列中接受的连接至少已经完成了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三次握手已完成 。

 

5.11.1    SO_REUSEDDR选项

TCP连接处于TIME_WAIT状态时,服务器程序可以通过设置socket选项SO_REUSEADDR来强制使用处于TIME_WAIT状态的连接占用的socket地址。

经过setsocketopt的设置之后,即使sock处于TIME_WAIT状态,与之绑定的socket地址也可以立即被重用。此外,我们也可以

通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle来快速回收被关闭的socket,从而使得TCP连接根本就不进入TIME_WAIT

态,进而允许应用程序立即重用本地的socket地址。

 

5.11.2   SO_RCVBUFSO_SNDBUF选项

SO_RCVBUFSO_SNDBUF选项分别表示TCP接受缓冲区和发送缓冲区的大小。

当我们用setsockopt来设置TCP的接受缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。

TCP接受缓冲区的最小值是256字节,而发送缓冲区的最小值是2048字节(不同系统可能有不同的默认最小值)。

系统这样做的目的,主要是确保一个TCP连接拥有足够的空闲缓冲区来处理阻塞(比如快速重传算法就期望TCP接受缓冲区

至少容纳4个大小为SMSSTCP报文段)。

注意:我们可以直接修改内核参数 /proc/sys/net/ipv4/tcp_rmem 

/proc/sys/net/ipv4/tcp_wmem 来强制TCP接收缓冲区和发送缓冲区的大小没有最小值限制。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值