Linux高性能服务器编程-自学笔记


前言

提示:本文为作者阅读《Linux高性能服务器编程(游双 著)》时写下的笔记,无参考价值


第5章 Linux网络编程基础API

5.1 socket地址API

要学习socket地址API,先要理解主机字节序网络字节序

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

字节序分为大端字节序(big endian)和小端字节序(little endian)。

  1. 大端字节序是指一个整数的高位字节(23~31 bit)存储在内存的低地址处,低位字节(0~7 bit)存储在内存的高地址处。
  2. 小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。(高高低低)

代码清单5-1可用于检查机器的字节序。【补充:c语言之union】

#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("unknown...\n");
    }
}

int main(){
    byteorder();
    return 0;
}
输出结果:
little endian

现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序

当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误地解释之。
解决问题的方法是:发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。因此大端字节序也称为网络字节序,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证。

同一台机器上的两个进程(比如一个由C语言编写,另一个由JAVA编写)通信,也要考虑字节序的问题(JAVA虚拟机采用大端字节序)。

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

函数名中,h代表主机host;n代表网络net;l代表long;s代表short.
例如:htonl表示“host to network long”,即将长整型的主机字节序数据转化为网络字节序数据。
这4个函数中,长整型函数通常用来转换IP地址,短整型函数用来转换端口号

5.1.2 通用socket地址

socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:

#include<bits/socket.h>
struct sockaddr
{
	sa_family_t sa_family;
	char sa_data[14];
}

sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如表5-1所示。
在这里插入图片描述宏PF_*和AF_*都定义在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用。

sa_data成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度,如表所示。
在这里插入图片描述
由上表可见,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地址

上面这两个通用socket地址结构体显然很不好用,比如设置与获取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协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于IPv4和IPv6:

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;/*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地址转换函数

人们习惯用可读性好的字符串来表示IP地址(比如用点分十进制字符串表示IPv4地址,以及用十六进制字符串表示IPv6地址)。
但编程中我们需要先把它们转化为二进制整数方能使用。而记录日志时则相反,我们要把整数表示的IP地址转化为可读的字符串。

下面3个函数可用于用点分十进制字符串表示的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);
  1. inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。它失败时返回INADDR_NONE。

  2. inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储于参数inp指向的地址结构中。它成功时返回1,失败则返回0。建议使用这个方法,因为它直接将结果存储到地址结构体in_addr中。

  3. inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。(不可重入性是指函数在被调用时,如果被打断,函数的期待值将被改变。)

    //不可重入的inet_ntoa函数
    char*szValue1=inet_ntoa(1.2.3.4);
    char*szValue2=inet_ntoa(10.194.71.60);
    printf(“address 1:%s\n”,szValue1);
    printf(“address 2:%s\n”,szValue2);
    //运行这段代码,得到的结果是:
    address1:10.194.71.60
    address2:10.194.71.60
    

下面这对更新的函数也能完成和前面3个函数同样的功能(更好用),并且它们同时适用于IPv4地址和IPv6地址:

#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);
  1. inet_pton函数将用字符串表示的IP地址src(用点分十进制字符串表示的IPv4地址或用十六进制字符串表示的IPv6地址)转换成用网络字节序整数表示的IP地址,并把转换结果存储于dst指向的内存中。
    其中,af参数指定地址族,可以是AF_INET或者AF_INET6。inet_pton成功时返回1,失败则返回0并设置errno。

  2. inet_ntop函数进行相反的转换,前三个参数的含义与inet_pton的参数相同,最后一个参数cnt指定目标存储单元的大小。
    下面的两个宏能帮助我们指定这个大小(分别用于IPv4和IPv6):
    #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);
  1. domain参数告诉系统使用哪个底层协议族。

     	对TCP/IP协议族而言,该参数应该设置为PF_INET(Protocol Family of Internet,用于IPv4)或PF_INET6(用于IPv6);
     	对于UNIX本地域协议族而言,该参数应该设置为PF_UNIX。
    
  2. type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务)和SOCK_UGRAM(数据报)服务。

     	对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议
     	取SOCK_DGRAM表示传输层使用UDP协议
    

    值得指出的是,自Linux内核版本2.6.17起,type参数可以接受上述服务类型与下面两个重要的标志相与的值:SOCK_NONBLOCK和SOCK_CLOEXEC。它们分别表示将新创建的socket设为非阻塞的,以及用fork调用创建子进程时在子进程中关闭该socket。
    在内核版本2.6.17之前的Linux中,文件描述符的这两个属性都需要使用额外的系统调用(比如fcntl)来设置。

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

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

5.3 命名socket

创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体socket地址。将一个socket与socket地址绑定称为给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。其中两种常见的errno是EACCES和EADDRINUSE,它们的含义分别是:

  • EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口(端口号为0~1023)上时,bind将返回EACCES错误。
  • EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_WAIT状态的socket地址。

5.4 监听socket

socket被命名之后,还不能马上接受客户连接,我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接:

#include<sys/socket.h>
int listen(int sockfd,int backlog);
  • sockfd参数指定被监听的socket。
  • backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。

在内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket的上限。

但自内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。backlog参数的典型值是5。

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


[补充:c语言之signal]
[basename()函数详解]

下面我们编写一个服务器程序,以研究backlog参数对listen系统调用的实际影响。

服务器程序:testlisten.cpp

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>

static bool stop = false;
static void handle_term(int sig){
    stop = true;
}

int main(int argc, char * argv[]){
    //SIGTERM:发送给本程序的终止请求信号。接收到SIGTERM信号,将执行handle_term函数
    signal(SIGTERM, handle_term);

    if (argc <= 3){
        printf("usage: %s id_address port_number backlog\n",
               basename(argv[0]));
        return 1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);
    int backlog = atoi(argv[3]);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    /*
     * 创建一个ipv4的socket地址
     */
    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);

    //将address所指的socket地址分配给未命名的sock文件描述符,addrlen参数指出该socket地址的长度
    int ret = bind(sock,(struct sockaddr*)&address,sizeof (address));
    assert(ret != 1);

    //backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接
    ret = listen(sock,backlog);
    assert(ret != 1);

    while (! stop){
        sleep(1);
    }

    close(sock);
    return 0;
}

编译并启动服务程序

gcc testlisten.cpp -o testlisten
./testlisten 127.0.0.1 12345

客户端执行telnet指令,连接服务端,执行7次

telnet 127.0.0.1 12345 

再执行netstat -nt | grep 12345查看连接情况。

可见,在监听队列中,处于ESTABLISHED状态的连接只有6个(backlog值加1),其他的连接都处于SYN_RCVD状态。我们改变服务器程序的第3个参数并重新运行之,能发现同样的规律,即完整连接最多有(backlog+1)个
在不同的系统上,运行结果会有些差别,不过监听队列中完整连接的上限通常比backlog值略大。

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调用是否成功?我们编写一个简单的服务器程序来测试之。

服务端程序:testaccept.cpp

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>

int main(int argc,char*argv[])
{
    if(argc <= 2)
    {
        printf("usage:%s ip_address port_number\n",basename(argv[0]));
        return 1;
    }

    const char * ip = argv[1];
    int port = atoi(argv[2]);

    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 sock = socket(AF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    int ret = bind(sock,(struct sockaddr *)&address, sizeof (address));
    assert(ret != -1);

    ret = listen(sock,5);
    assert(ret != -1);

    /*暂停20秒以等待客户端连接和相关操作(掉线或者退出)完成*/
    sleep(20);

    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof (client);

    int connfd = accept(sock, (struct sockaddr*)&client,&client_addrlength);
    if(connfd < 0){
        printf("errno is:%d\n",errno);
    }else{
        /**
         * 接受连接成功则打印出客户端的IP地址和端口号
         * 已定义好:#define INET_ADDRSTRLEN 16
         */
        char remote[INET_ADDRSTRLEN];
        printf("connected with ip:%s and port:%d\n",
               /**
                * 参数一:地址族
                * 参数二:sockaddr_id中的IPv4地址结构体,是要转换成点分十进制字符串
                * 参数三:存储点分十进制Pv4地址的储单元
                * 最后一个参数:指定目标存储单元的大小
                * */
               inet_ntop(AF_INET,&client.sin_addr,remote,INET_ADDRSTRLEN),
               ntohs(client.sin_port));
        close(connfd);
    }
    close(sock);
    return 0;
}

编译并执行服务端程序

gcc testaccept.cpp -o testaccept
./testaccept 127.0.0.1 54321

客户端,应在服务端执行后20秒内,执行如下指令,执行后立马关闭终端

telnet 127.0.0.1 54321

20秒后,可以看到服务端程序接收连接,并输出客户端的地址信息

connected with ip:127.0.0.1 and port:53844

说明:accept调用对于客户端网络断开毫不知情accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。

我 们 把 执 行 过 listen 调 用 、 处 于 LISTEN 状 态 的 socket 称 为 监 听socket,而所有处于ESTABLISHED状态的socket则称为连接socket。

5.6 发起连接

如果说服务器通过listen调用来被动接受连接,那么客户端需要通
过如下系统调用来主动与服务器建立连接:

#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地址的长度。

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

connect失败则返回-1并设置errno。其中两种常见的errno是ECONNREFUSED和ETIMEDOUT,它们的含义如下:

  • ECONNREFUSED,目标端口不存在,连接被拒绝。
  • ETIMEDOUT,连接超时。

5.7 关闭连接

关闭一个连接实际上就是关闭该连接对应的socket,这可以通过如下关闭普通文件描述符的系统调用来完成:

#include<unistd.h>
int close(int fd);
  • fd参数是待关闭的socket。

不过,close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接。
多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。

如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用如下的shutdown系统调用(相对于close来说,它是专门为网络编程设计的):

#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数据读写

对文件的读写操作read和write同样适用于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);

1.recv

  • recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,flags参数的含义见后文,通常设置为0即可
  • recv成功时返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能要多次调用recv,才能读取到完整的数据
  • recv可能返回0,这意味着通信对方已经关闭连接了。recv出错时返回-1并设置errno

2.send

  • send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小
  • send成功时返回实际写入的数据的长度,失败则返回-1并设置errno

flags参数为数据收发提供了额外的控制,它可以取表5-4所示选项中的一个或几个的逻辑或
在这里插入图片描述


我们举例来说明如何使用这些选项。MSG_OOB选项给应用程序提供了发送和接收带外数据的方法

带外数据比普通数据(也称为带内数据)有更高的优先级,它应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。

接收方:testoobrecv.cpp

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#define BUF_SIZE 1024

int main(int argc,char*argv[])
{
    if(argc <= 2)
    {
        printf("usage:%s ip_address port_number\n",basename(argv[0]));
        return 1;
    }

    const char * ip = argv[1];
    int port = atoi(argv[2]);

    struct sockaddr_in server_address;
    bzero(&server_address, sizeof (server_address));
    server_address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &server_address.sin_addr);
    server_address.sin_port = ntohs(port);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    int ret = bind(sock, (struct sockaddr *)&server_address, sizeof (server_address));
    assert(ret != -1);

    ret = listen(sock, 5);
    assert(ret != -1);


    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    socklen_t client_addrlength=sizeof(client);
    int connfd = accept(sock,(struct sockaddr *)&client, &client_addrlength);
    if (connfd < 0){
        printf("errno is:%d\n",errno);
    }else{
        char buffer[BUF_SIZE];
        memset(buffer,'\0',BUF_SIZE);
        ret = recv(connfd, buffer, BUF_SIZE-1, 0);
        printf("got %d bytes of normal data'%s'\n",ret,buffer);

        memset(buffer,'\0',BUF_SIZE);
        ret=recv(connfd,buffer,BUF_SIZE-1,MSG_OOB);
        printf("got %d bytes of oob data'%s'\n",ret,buffer);

        memset(buffer,'\0',BUF_SIZE);
        ret=recv(connfd,buffer,BUF_SIZE-1,0);
        printf("got %d bytes of normal data'%s'\n",ret,buffer);

        close(connfd);
    }
    close(sock);
    return 0;
}

发送方:testoobsend.cpp

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>

int main(int argc,char*argv[])
{
    if(argc <= 2){
        printf("usage:%s ip_address port_number\n",basename(argv[0]));
        return 1;
    }

    const char * ip = argv[1];
    int port = atoi(argv[2]);
    struct sockaddr_in server_address;
    bzero(&server_address, sizeof (server_address));
    server_address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &server_address.sin_addr);
    server_address.sin_port = htons(port);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    int ret = connect(sock, (struct sockaddr*)&server_address, sizeof (server_address));
    if (ret < 0){
        printf("connection failed\n");
    }else{
        const char*oob_data="abc";
        const char*normal_data="123";

        send(sock, normal_data,  strlen(normal_data), 0);
        send(sock, oob_data,  strlen(oob_data), MSG_OOB);
        send(sock,normal_data,strlen(normal_data),0);
    }
    close(sock);
    return 0;
}

编译并执行服务程序

gcc testoobrecv.cpp -o testoobrecv
./testoobrecv 127.0.0.1 12345

编译并执行客户程序

gcc testoobsend.cpp -o testoobsend
/testoobsend 127.0.0.1 12345

服务程序输出结果

got 5 bytes of normal data'123ab'
got 1 bytes of oob data'c'
got 3 bytes of normal data'123'

由此可见,客户端发送给服务器的3字节的带外数据“abc”中,仅有最后一个字符“c”被服务器当成真正的带外数据接收。并且,服务器对正常数据的接收将被带外数据截断,即前一部分正常数据“123ab”和后续的正常数据“123”是不能被一个recv调用全部读出的。

【带外数据】
带外数据比普通数据(也称为带内数据)有更高的优先级,它应该==总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据。==带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。实际应用中,带外数据的使用很少见,已知的仅有telnet、ftp等远程非活跃程序。
TCP发送带外数据的过程:假设一个进程已经往某个TCP连接的发送缓冲区中写入了N字节的普通数据,并等待其发送。在数据被发送前,该进程又向这个连接写入了3字节的带外数据“abc”。此时,待发送的TCP报文段的头部将被设置URG标志,并且紧急指针被设置为指向最后一个带外数据的下一字节(进一步减去当前TCP报文段的序号值得到其头部中的紧急偏移值)
紧急偏移值,它指出带外数据在字节流中的位置的下一字节位置是。因此,(紧急偏移值-1)为带外数据最后一个字符。

flags参数只对send和recv的当前调用生效,而后面我们将看到如何通过setsockopt系统调用永久性地修改socket的某些属性。

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,structsockaddr*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;/*复制函数中的flags参数,并在调用过程中更新*/
};
  • msg_name成员指向一个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被设置为NULL。这是因为对数据流socket而言,对方的地址已经知道。

  • msg_namelen成员则指定了msg_name所指socket地址的长度。

  • msg_iov成员是iovec结构体类型的指针,iovec结构体的定义如下:

    //iovec结构体封装了一块内存的起始位置和长度。
    struct iovec
    {
    	void*iov_base;/*内存起始地址*/
    	size_t iov_len;/*这块内存的长度*/
    };
    
  • msg_iovlen指定这样的iovec结构对象有多少个。
    对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读(scatter read);
    对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写(gather write)。

  • msg_control和msg_controllen成员用于辅助数据的传送。我们不详细讨论它们,仅在第13章介绍如何使用它们来实现在进程间传递文件描述符。

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

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参数指定要操作哪个协议的选项(即属性),比如IPv4、IPv6、TCP等。
  • option_name参数则指定选项的名字。我们在表5-5中列举了socket通信中几个比较常用的
    socket选项。
  • option_value和option_len参数分别是被操作选项的值和长度。不同的选项具有不同类型的值,如表5-5中“数据类型”一列所示。

在这里插入图片描述
getsockopt和setsockopt这两个函数成功时返回0,失败时返回-1并设置errno。


服务器而言,有部分socket选项只能在调用listen系统调用前针对监听socket 设置才有效。这是因为连接socket只能由accept调用返回,而accept从listen监听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤(因为listen监听队列中的连接至少已进入SYN_RCVD状态,参见代码清单5-4),这说明服务器已经往被接受连接上发送出了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_REUSEADDR选项

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

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地址。

5.11.2 SO_RCVBUF 和 SO_SNDBUF选项

SO_RCVBUF和SO_SNDBUF选项分别表示TCP接收缓冲区和发送缓冲区的大小。不过,当我们用setsockopt来设置TCP的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。 TCP接收缓冲区的最小值是256字节,而发送缓冲区的最小值是2048字节(不过,不同的系统可能有不同的默认最小值)。

系统这样做的目的,主要是确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞(比如快速重传算法就期望TCP接收缓冲区能至少容纳4个大小为SMSS的TCP报文段)。

此外,我们可以直接修改内核参数/proc/sys/net/ipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem来强制TCP接收缓冲区和发送缓冲区的大小没有最小值限制。我们将在第16章讨论这两个内核参数。


下面我们编写一对客户端和服务器程序,如代码清单5-10和代码清单5-11所示,它们分别修改TCP发送缓冲区和接收缓冲区的大小。

服务端程序:set_recv_buffer.cpp

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>

#define BUFFER_SIZE 1024

int main(int argc, char*argv[]){
    if (argc <= 2){
        printf("usage:%s ip_address port_number recv_buffer_size\n",basename(argv[0]));
        return 1;
    }

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    const char* ip = argv[1];
    int port = atoi(argv[2]);
    struct sockaddr_in server_address;
    bzero(&server_address, sizeof(server_address));
    server_address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &server_address.sin_addr);
    server_address.sin_port = htons(port);


    int recvbuf = atoi(argv[3]);
    int len = sizeof(recvbuf);
    /*先设置TCP接收缓冲区的大小,然后立即读取之*/
    setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, len);
    getsockopt(sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, (socklen_t*)&len);


    int ret = bind(sock, (struct sockaddr*)&server_address, sizeof(server_address));
    assert(ret != -1);
    ret = listen(sock, 5);
    assert(ret != -1);

    struct sockaddr_in client_address;
    bzero(&client_address, sizeof(client_address));
    socklen_t  client_addrlength = sizeof (client_address);
    int connfd = accept(sock, (struct sockaddr *)&client_address, &client_addrlength);
    if (connfd < 0){
        printf("erron is %d",errno);
    }else{
        char buffer[BUFFER_SIZE];
        memset(buffer, '\0', BUFFER_SIZE);
        while(recv(connfd,buffer,BUFFER_SIZE-1,0) > 0){
            printf("%s\n",buffer);
        }
        close(connfd);
    }
    close(sock);
    return 0;
}

客户端程序:set_send_buf.cpp

#include<sys/socket.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>

#define BUFFER_SIZE 512

int main(int argc, char * argv[]){
    if (argc <= 2){
        printf("usage:%s ip_address port_number send_bufer_size\n",basename(argv[0]));
        return 1;
    }

    const char * ip = argv[1];
    int port = atoi(argv[2]);

    struct sockaddr_in server_address;
    bzero(&server_address, sizeof(server_address));
    server_address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &server_address.sin_addr);
    server_address.sin_port = htons(port);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    int sendbuf = atoi(argv[3]);
    int len = sizeof(sendbuf);

    /*先设置TCP发送缓冲区的大小,然后立即读取之*/
    setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, len);
    getsockopt(sock,SOL_SOCKET,SO_SNDBUF,&sendbuf,(socklen_t*)&len);
    printf("the tcp send buffer size after setting is%d\n",sendbuf);

    if (connect(sock,(struct sockaddr*)&server_address,sizeof(server_address)) != -1){
        char buffer[BUFFER_SIZE];
        memset(buffer,'a',BUFFER_SIZE);
        send(sock,buffer,BUFFER_SIZE,0);
    }

    close(sock);
    return 0;
}

编译并执行服务程序

gcc set_recv_buffer.cpp  -o set_recv_buffer
./set_send_buffer 127.0.0.1 12345 2000

编译并执行客户程序

gcc set_send_buffer.cpp  -o set_send_buffer
./set_send_buffer 127.0.0.1 12345 2000

结果

  • 客户端的输出结果

    the tcp send buffer size after setting is4608
    

    设置的TCP发送缓冲区的大小被系统增加了一倍

  • 系统允许的TCP接收缓冲区最小为256字节

5.11.3 SO_RCVLOWAT 和 SO_SNDLOWAT选项

SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。
它们一般被I/O复用系统调用(见第9章)用来判断socket是否可读或可写。

当TCP接收缓冲区中可读数据的总数大于其低水位标记时,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据;
当TCP发送缓冲区中的空闲空间(可以写入数据的空间)大于其低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socke上写入数据。

默认情况下,TCP接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1字节。

5.11.4 SO_LINGER选项

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系统调用可能产生如下3种行为之一:

  1. l_onoff等于0。此时SO_LINGER选项不起作用,close用默认行为来关闭socket
  2. l_onoff不为0,l_linger等于0。此时close系统调用立即返回,TCP模块将丢弃被关闭的socket对应的TCP发送缓冲区中残留的数据,同时给对方发送一个复位报文段。因此,这种情况给服务器提供了异常终止一个连接的方法。
  3. l_onoff不为0,l_linger大于0。此时close的行为取决于两个条件:
    一是被关闭的socket对应的TCP发送缓冲区中是否还有残留的数据;
    二是该socket是阻塞的,还是非阻塞的。
    对于阻塞的socket,close将等待一段长为l_linger的时间,直到TCP模块发送完所有残留数据并得到对方的确认。
    如果这段时间内TCP模块没有发送完残留数据并得到对方的确认,那么close系统调用将返回-1并设置errno为EWOULDBLOCK。
    如果socket是非阻塞的,close将立即返回,此时我们需要根据其返回值和errno来判断残留数据是否已经发送完毕。关于阻塞和非阻塞,我们将在第8章讨论。

5.12 网络信息API

socket地址的两个要素,即IP地址和端口号,都是用数值表示的。这不便于记忆,也不便于扩展(比如从IPv4转移到IPv6)。

因此在前面的章节中,我们用主机名来访问一台机器,而避免直接使用其IP地址。同样,我们用服务名称来代替端口号。比如,下面两条telnet命令具有完全相同的作用:

telnet 127.0.0.1 80
telnet localhost www

上面的例子中,telnet客户端程序是通过调用某些网络信息API来实现主机名到IP地址的转换,以及服务名称到端口号的转换的。下面我们将讨论网络信息API中比较重要的几个。

5.12.1 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结构体类型的指针,hostent结构体的定义如下:

#include<netdb.h>
struct hostent
{
	char*h_name;/*主机名*/
	char**h_aliases;/*主机别名列表,可能有多个*/
	int h_addrtype;/*地址类型(地址族)*/
	int h_length;/*地址长度*/
	char**h_addr_list/*按网络字节序列出的主机IP地址列表*/
};

5.12.2 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结构体类型的指针,结构体servent的定义如下:

#include<netdb.h>
struct servent
{
	char*s_name;/*服务名称*/
	char**s_aliases;/*服务的别名列表,可能有多个*/
	int s_port;/*端口号*/
	char*s_proto;/*服务类型,通常是tcp或者udp*/
};

5.12.3 案例

下面我们通过主机名和服务名来访问目标服务器上的daytime服务,以获取该机器的系统时间。

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<stdio.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>

int main(int argc, char * argv[]){
    if (argc != 2){
        printf("usage: ./%s ip \n",basename(argv[0]));
        return 1;
    }

    char * host = argv[1];

    /*根据主机名称获取主机的完整信息*/
    struct hostent * hostinfo =  gethostbyname(host);
    assert(hostinfo);

    /*获取daytime服务信息.根据名称获取某个服务的完整信息*/
    struct servent * servinfo = getservbyname("daytime","tcp");
    assert(servinfo);
    printf("daytime port is %d\n", ntohs(servinfo->s_port));

    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_port =  servinfo->s_port;
    /*注意下面的代码,因为h_addr_list本身是使用网络字节序的地址列表,所以使用其中的IP地址时,无须对目标IP地址转换字节序*/
    address.sin_addr = *(struct in_addr*)*hostinfo->h_addr_list;

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    int result = connect(sock, (struct sockaddr *)&address, sizeof(address));
    assert(result != -1);

    char  buffer[128];
    result = read(sock, buffer, sizeof(buffer));
    buffer[result]='\0';
    printf("the day tiem is:%s",buffer);

    close(sock);
    return 0;
}

需要指出的是,上面讨论的4个函数都是不可重入的,即非线程安全的。不过netdb.h头文件给出了它们的可重入版本。正如Linux下所有其他函数的可重入版本的命名规则那样,这些函数的函数名是在原函数名尾部加上_r(re-entrant)。

5.12.4 getaddrinfo

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反馈的结果。

getaddrinfo反馈的每一条结果都是addrinfo结构体类型的对象,结构体addrinfo的定义如下:

struct addrinfo
{
	int ai_flags;/*见后文*/
	int ai_family;/*地址族*/
	int ai_socktype;/*服务类型,SOCK_STREAM或SOCK_DGRAM*/
	int ai_protocol;/*见后文*/
	socklen_t ai_addrlen;/*socket地址ai_addr的长度*/
	char*ai_canonname;/*主机的别名*/
	struct sockaddr*ai_addr;/*指向socket地址*/
	struct addrinfo*ai_next;/*指向下一个sockinfo结构的对象*/
};
  • ai_protocol成员是指具体的网络协议,其含义和socket系统调用的第三个参数相同,它通常被设置为0。
  • ai_flags成员可以取表5-6中的标志的按位或
    在这里插入图片描述

我们使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段则必须被设置为NULL。


代码清单5-13利用了hints参数获取主机ernest-laptop上的“daytime”流服务信息。

struct addrinfo hints;
struct addrinfo*res;
bzero(&hints,sizeof(hints));
hints.ai_socktype=SOCK_STREAM;
getaddrinfo("ernest-laptop","daytime",&hints,&res);

从代码清单5-13中我们能分析出,getaddrinfo将隐式地分配堆内存(可以通过valgrind等工具查看),因为res指针原本是没有指向一块合法内存的,所以,getaddrinfo调用结束后,我们必须使用如下配对函数来释放这块内存:

#include<netdb.h>
void freeaddrinfo(struct addrinfo*res);

5.12.5 getnameinfo

getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。它是否可重入取决于其内部调用的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);

getnameinfo将返回的主机名存储在host参数指向的缓存中, 将服务名存储在serv参数指向的缓存中, hostlen和servlen参数分别指定这两块缓存的长度。flags参数控制getnameinfo的行为,它可以接收表5-7中的选项。
在这里插入图片描述



getaddrinfo和getnameinfo函数成功时返回0,失败则返回错误码,可能的错误码如表5-8所示。
在这里插入图片描述
Linux下strerror函数能将数值错误码errno转换成易读的字符串形式。同样,下面的函数可将表5-8中的错误码转换成其字符串形式:

#include<netdb.h>
const char*gai_strerror(int error);

第6章 高级I/O函数

6.1 pipe函数

pipe函数可用于创建一个管道,以实现进程间通信。
pipe函数的定义如下:

#include<unistd.h>
int pipe(int fd[2]);
  • pipe函数的参数是一个包含两个int型整数的数组指针。

该函数成功时返回0,并将一对打开的文件描述符值填入其参数指向的数组。如果失败,则返回-1并设置errno。

通过pipe函数创建的这两个文件描述符fd[0]和fd[1]分别构成管道的两端,往fd[1]写入的数据可以从fd[0]读出。并且,fd[0]只能用于从管道读出数据,fd[1]则只能用于往管道写入数据,而不能反过来使用。如果要实现双向的数据传输,就应该使用两个管道。

默认情况下,这一对文件描述符都是阻塞的。此时如果我们用read系统调用来读取一个空的管道,则read将被阻塞,直到管道内有数据可读;如果我们用write系统调用来往一个满的管道(见后文)中写入数据,则write亦将被阻塞,直到管道有足够多的空闲空间可用。

但如果应用程序将fd[0]和fd[1]都设置为非阻塞的,则read和write会有不同的行为。

  • 如果管道的写端文件描述符fd[1]的引用计数(见5.7节)减少至0,即没有任何进程需要往管道中写入数据,则针对该管道的读端文件描述符fd[0]的read操作将返回0,即读取到了文件结束标记(End Of File,EOF);
  • 反之,如果管道的读端文件描述符fd[0]的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对该管道的写端文件描述符fd[1]的write操作将失败,并引发SIGPIPE信号。关于SIGPIPE信号。

管道内部传输的数据是字节流,这和TCP字节流的概念相同。但二者又有细微的区别。应用层程序能往一个TCP连接中写入多少字节的数据,取决于对方的接收通告窗口的大小和本端的拥塞窗口的大小。而管道本身拥有一个容量限制,它规定如果应用程序不将数据从管道读走的话,该管道最多能被写入多少字节的数据。自Linux 2.6.11内核起,管道容量的大小默认是65536字节。我们可以使用fcntl函数来修改管道容量(见后文)。

此外,socket的基础API中有一个socketpair函数。它能够方便地创建双向管道。其定义如下:

#include<sys/types.h>
#include<sys/socket.h>
int socketpair(int domain,int type,int protocol,int fd[2]);

socketpair前三个参数的含义与socket系统调用的三个参数完全相同,但domain只能使用UNIX本地域协议族AF_UNIX,因为我们仅能在本地使用这个双向管道。最后一个参数则和pipe系统调用的参数一样,只不过socketpair创建的这对文件描述符都是既可读又可写的。socketpair成功时返回0,失败时返回-1并设置errno。

6.2 dup函数和dup2函数

有时我们希望把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接(比如CGI编程)。这可以通过下面的用于复制文件描述符的dup或dup2函数来实现:

#include<unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one,int file_descriptor_two);

dup函数创建一个新的文件描述符,该新文件描述符和原有文件描述符file_descriptor指向相同的文件、管道或者网络连接。并且dup返回的文件描述符总是取系统当前可用的最小整数值。

dup2和dup类似,不过它将返回第一个不小于file_descriptor_two的整数值

dup和dup2系统调用失败时返回-1并设置errno。

【注意】
通过dup和dup2创建的文件描述符并不继承原文件描述符的属性,比如close-on-exec和non-blocking等。


代码清单6-1 利用dup函数实现了一个基本的CGI服务器。

服务端的代码:cgi_server.cpp

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>

int main(int argc, char* argv[]){
    if (argc <= 2){
        printf("usage:%s ip_address port_number\n",basename(argv[0]));
        return 1;
    }

    const char * ip = argv[1];
    int port = atoi(argv[2]);
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET,
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = port;

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    int ret = bind(sock, (struct sockaddr *)&address, sizeof (address));
    assert( ret != -1);

    ret = listen(sock, 5);
    assert( ret != -1);

    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof (client);
    int connfd = accept(sock, (struct sockaddr *)&client, &client_addrlength);

    if (connfd < 0){
        printf("errno is:%d\n",errno);
    }else{
    	//先关闭标准输出文件描述符STDOUT_FILENO(其值是1)
        close(STDOUT_FILENO);
        //复制socket文件描述符connfd。因为dup总是返回系统中最小的可用文件描述符,所以它的返回值实际上是1
        dup(connfd);
        printf("abcd\n");
        close(connfd);
    }
    close(sock);
    return 0;
}

客户端代码:cgi_client.cpp

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>

#define BUF_SIZE 1024

int main(int argc, char* argv[]){
    if (argc <= 2){
        printf("usage:%s ip_address port_number\n",basename(argv[0]));
        return 1;
    }

    const char * ip = argv[1];
    int port = atoi(argv[2]);
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET,
            inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = port;

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    int conn = connect(sock, (struct sockaddr *)&address, sizeof (address));

    if (conn != -1){
        printf("连接成功!!!\n");
        char buffer[BUF_SIZE];
        memset(buffer,'\0',BUF_SIZE);
        int count=recv(sock,buffer,BUF_SIZE-1,0);
        printf("got %d bytes of normal data'%s'\n",count,buffer);
    }

    close(sock);
    return  0;
}

编译并运行服务端

gcc cgi_server.cpp -o cgi_server
./cgi_server 127.0.0.1 12345

编译并运行客户端

gcc cgi_server.cpp -o cgi_client
./cgi_client 127.0.0.1 12345

客户端终端上的运行结果

连接成功!!!
got 5 bytes of normal data'abcd

在代码清单6-1中,我们先关闭标准输出文件描述符STDOUT_FILENO(其值是1),然后复制socket文件描述符connfd。因为dup总是返回系统中最小的可用文件描述符,所以它的返回值实际上是1,即之前关闭的标准输出文件描述符的值。这样一来,服务器输出到标准输出的内容(这里是“abcd”)就会直接发送到与客户连接对应的socket上,因此printf调用的输出将被客户端获得(而不是显示在服务器程序的终端上)。这就是CGI服务器的基本工作原理。

6.3 readv函数和writev函数

  1. readv函数将数据从文件描述符读到分散的内存块中,即分散读;
  2. writev函数则将多块分散的内存数据一并写入文件描述符中,即集中写。
#include<sys/uio.h>
ssize_t readv(int fd,const struct iovec*vector,int count);
ssize_t writev(int fd,const struct iovec*vector,int count);
  • fd参数是被操作的目标文件描述符。

  • vector参数的类型是iovec结构数组。我们在第5章讨论过结构体iovec,该结构体描述一块内存区。

    //iovec结构体封装了一块内存的起始位置和长度。
    struct iovec
    {
    	void*iov_base;/*内存起始地址*/
    	size_t iov_len;/*这块内存的长度*/
    };
    
  • count参数是vector数组的长度,即有多少块内存数据需要从fd读出或写到fd

readv和writev在成功时返回读出/写入fd的字节数,失败则返回-1并设置errno。它们相当于简化版的recvmsg和sendmsg函数


考虑Web服务器。当Web服务器解析完一个HTTP请求之后,如果目标文档存在且客户具有读取该文档的权限,那么它就需要发送一个HTTP应答来传输该文档。这个HTTP应答包含1个状态行、多个头部字段、1个空行和文档的内容。其中,前3部分的内容可能被Web服务器放置在一块内存中,而文档的内容则通常被读入到另外一块单独的内存中(通过read函数或mmap函数)。我们并不需要把这两部分内容拼接到一起再发送,而是可以使用writev函数将它们同时写出,如代码所示。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值