Linux高性能服务器编程 第5章 Linux网络编程基础API

5.1 socket 地址 API

  • 现代CPU的累加器一次都能装载(至少)4 字节(这里考虑32位机,下同),即一个整
    数。那么这4 字节在内存中排列的顺序将影响它被累加器装载成的整数的值。这就是字节序
    问题。字节序分为大端字节序(big endian)和小端字节序(little endian)o 大端字节序是指
    一个整数的高位字节(23〜 31 b it)存储在内存的低地址处,低位字节(0 〜 7 b it)存储在 内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则
    存储在内存的低地址处。
  • 现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。
  • 当格式化的数据(比如32 bit整型数和16 bit短整型数)在两台使用不同字节序的主机 之间直接传递时,接收端必然错误地解释之。解决问题的方法是:发送端总是把要发送的
    数据转化成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字
    节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转
    换,大端机不转换)。因此大端字节序也称为网络字节序,它给所有接收数据的主机提供了
    一个正确解释收到的格式化数据的保证

5.1.2 通用socket地址

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

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

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

5.2 创建 socket

  •  domain参数告诉系统使用哪个底层协议族。对 TCP/IP协议族而言,该参数应该设置为PF_INET ( Protocol Family of In tern et,用于 IPv4) 或 PF_INET6 ( 用于 IPv6) ;对于 UNIX本地域协议族而言,该参数应该设置为PF_UNIX。关于socket系统调用支持的所有协议族, 请读者自己参考其man手册。 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) 来设置。
  • protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值 通常都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下,我们都应该把它设置为0 ,表示使用默认协议。
  • socket系统调用成功时返回一个socket文件描述符,失败则返回-1并设置ermo

5.3 命名 socket

  • 创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体 socket地址。将一个socket与 socket地址绑定称为给socket命名.在服务器程序中,我们通 常要命名socket,因为只有命名后客户端才能知道该如何连接它。客户端则通常不需要命名 socket,而是采用匿名方式,即使用操作系统自动分配的socket地址。命名socket的系统调 用是bind,其定义如下:

  •  bind将 my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址的长度。
  • bind成功时返回0 , 失败则返回-1并设置ermo其中两种常见的ermo是 EACCES和EADDRINUSE,它们的含义分别是
  • EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将
    socket绑定到知名服务端口(端口号为0〜1023) 上时,bind将返回EACCES错误
  • EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_ WAIT状态的socket地址。

5.4 监听 socket

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

  •  sockfd参数指定被监听的socketo 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并设置ermo
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <csignal>
#include <cassert>
#include <unistd.h>

#include "include/algorithm_text.h"

static bool stop = false;
static void handle_term(int sig){
    stop = true;
}
int main(int argc,char* argv[]) {
    //参考链接 https://blog.51cto.com/u_15284125/2988992
    //signal函数 第一参数是指需要进行处理的信号,第二个参数是指 处理的方式(系统默认/忽略/捕获)
    //SIGTERM 请求中止进程,kill命令缺省发送 交给handle_term函数进行处理
    signal(SIGTERM,handle_term);
    if (argc < 3){
        //basename 参考链接 https://blog.csdn.net/Draven_Liu/article/details/38235585
        //假设路径为 nihao/nihao/jhhh/txt.c
        //basename函数并不会关心路径是否正确,文件是否存在,只不过是把路径上除了最后的txt.c 这个文件名字其他的东西都删除了然后返回txt.c而已
        std::cout << "usage:" <<basename(argv[0]) << "ip_address port_number backlog\n"<<std::endl;
    }
    //argv[1] ip地址
    //argv[2] 端口号
    //argv[3] 日志级别
    const char* ip = argv[1];
    //atoi 把字符串转换成一个整数
    //参考链接 https://www.runoob.com/cprogramming/c-function-atoi.html
    int port = atoi(argv[2]);
    int backlog = atoi(argv[3]);
    //socket编程 第一个参数表示使用哪个底层协议族,对 TCP/IP协议族而言,该参数应该设置为PF_INET
    //对 TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议
    //第三个参数是在前两个参数构成的协议集合下,再选择一个具体的协议  设置为0 ,表示使用默认协议
    int sock = socket(PF_INET,SOCK_STREAM,0);
    //断言 如果不正确 不会往下继续执行
    assert(sock>=0);
    //创建一个IPv4 socket地址
    //TCP/IP 协议族sockaddr_in 表示IPv4专用socket地址结构体
    struct sockaddr_in address;
    // bzero() 会将内存块(字符串)的前n个字节清零;
    // s为内存(字符串)指针,n 为需要清零的字节数。
    // 在网络编程中会经常用到。
    bzero(&address,sizeof (address));
    address.sin_family = AF_INET;
    //int inet_pton(int af,const char* src,void* dst)
    //af 指定地址族 AF_INET 或者 AF_INET6
    //inet_pton函数成功返回1 失败返回0,并且设置errno
    //errno 表示各种错误
    // inet_pton 将字符串表示的IP地址src(使用点分十进制表示的IPv4地址和使用十六进制表示的IPv6)转换成网络字节序整数表示的IP地址,并把转换的结果存储在dst指向的内存中
    inet_pton(AF_INET,ip,&address.sin_addr);
    //const char* inet_ntop(int af,const char* src,void* dst,socklen_t cnt)
    //inet_tpon函数和inet_pton进行相反的转换,前三个参数的含义与其相同,最后一个参数cnt指定目标存储单元的大小
    //成功 返回目标单元的地址 失败返回NULL 并且设置errno
    address.sin_port = htons(port);
    //bind将 my_addr所指的socket地址(2)分配给未命名的sockfd(1)文件描述符,addrlen参数(3)指出该socket地址的长度。
    int ret = bind(sock,(struct sockaddr*)&address,sizeof (address));
    assert(ret != -1);
    ret = listen(sock,backlog);
    //循环等待连接 直到有SIGTERM信号将其中断
    while(!stop){
        sleep(1);
    }
    //关闭 socket
    close(sock);
    return 0;
}

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

5 . 5 接受连接

  • sockfd参数是执行过listen系统调用的监听socket,addr参数用来获取被接受连接的远 端 socket地址,该 socket地址的长度由addrlen参数指出。accept成功时返回一个新的连接 socket,该 socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受 连接对应的客户端通信accept失败时返回-1并设置ermo
  • 们把执行过listen调用、处于LISTEN状态的socket称为监听socket,而所有处于ESTABLISHED状态的 socket则称为连接socket
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <csignal>
#include <cassert>
#include <unistd.h>

#include "include/algorithm_text.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);
    //htons  将一个无符号短整型数值转换为网络字节序,即大端模式(big-endian)
    address.sin_port = htons(port);
    int sock = socket(PF_INET,SOCK_STREAM,0);
    assert(sock >= 0);
    int ret = bind(sock,(struct sockaddr*)&address,sizeof (address));
    assert(ret != -1);
    ret = listen(sock,5);
    //等待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地址和端口号
        char remote[INET_ADDRSTRLEN];
        printf("connected with ip:%s and port:%d\n",
               inet_ntop(AF_INET,&client.sin_addr,remote,INET_ADDRSTRLEN),
               // 将一个无符号短整形数从网络字节顺序转换为主机字节顺序
               ntohs(client.sin_port));
        close(connfd);
    }
    close(sock);
    return 0;
}
  • accept是从监听队列中取出连接,而不论连接处于何种状态(如上面的 ESTABLISHED状态和CLOSE_WAIT状态),更不关心任何网络状况的变化

5 .6 发起连接

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

  •  sockfd参数由socket系统调用返回一个socket。serv addr参数是服务器监听的socket地
    址,addrlen参数则指定这个地址的长度。 
  • connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就 可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置e rm s 其中两种常见的
    ermo是 ECONNREFUSED和 ETIMEDOUT,它们的含义如下:
  •  ECONNREFUSED,目标端口不存在,连接被拒绝
  • ETIMEDOUT,连接超时

5 .7 关闭连接

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

  •  fd参数是待关闭的socketo不过,close系统调用并非总是立即关闭一个连接,而是将fd 的引用计数减1。只有当fd的引用计数为。时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1 ,因此我们必须在父进程和子进程 中都对该socket执行close调用才能将连接关闭。 
  • 如果无论如何都要立即终止连接(而不是将socket的引用计数减1 ) ,可以使用如下的 shutdown系统调用(相对于close来说,它是专门为网络编程设计的)

  •  sockfd参数是待关闭的socketo howto参数决定了 shutdown的行为,它可取表5・ 3中的
    某个值。

  •  由此可见,shutdown能够分别关闭socket上的读或写,或者都关闭°而 close在关闭连 接时只能将socket上的读和写同时关闭o
  • shutdown成功时返回0 , 失败则返回-1并设置ermoo

5 . 8 数据读写
5.8.1 TCP数据读写

  • 对文件的读写操作read和 write同样适用于sockets但是socket编程接口提供了几个专 门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读 写的系统调用是:

  • recv读取sockfd上的数据,buf和 Ien 参数分别指定读缓冲区的位置和大小,flags参数的含义见后文,通常设置为0 即可。即recv成功时返回实际读取到的数据的长度,它可能小于 我们期望的长度len。因此我们可能要多次调用recv ,才能读取到完整的数据recv可能返回 0 , 这意味着通信对方已经关闭连接了。recv出错时返回-1并设置ermo
  • send往 sockfd上写入数据,buf和 len参数分别指定写缓冲区的位置和大小。send成功 时返回实际写入的数据的长度,失败则返回并设置errno
  • flags参数为数据收发提供了额外的控制,它可以取表所示选项中的一个或几个的逻辑或

 发送端代码

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>

#include "include/algorithm_text.h"


int main(int argc,char* argv[]) {
    if (argc <= 2){
        printf("usage:%s ip_server_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);
    //htons  将一个无符号短整型数值转换为网络字节序,即大端模式(big-endian)
    server_address.sin_port = htons(port);
    int sock_fd = socket(PF_INET,SOCK_STREAM,0);
    assert(sock_fd >= 0);
    if (connect(sock_fd,(struct sockaddr*)&server_address,sizeof (server_address))<0){
        printf("connected failed.\n");
    } else{
        const char* oob_data = "abc";
        const char* normal_data = "123";
        send(sock_fd,normal_data, strlen(normal_data),0);
        send(sock_fd,oob_data, strlen(oob_data),MSG_OOB);
        send(sock_fd,normal_data, strlen(normal_data),0);
    }
    
    close(sock_fd);
    return 0;
}

接收端代码

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>

#include "include/algorithm_text.h"
#define BUF_SIZE 1024


int main(int argc,char* argv[]) {
    if (argc <= 2){
        printf("usage:%s ip_server_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);
    //htons  将一个无符号短整型数值转换为网络字节序,即大端模式(big-endian)
    server_address.sin_port = htons(port);
    int sock_fd = socket(PF_INET,SOCK_STREAM,0);
    assert(sock_fd >= 0);
    int ret = bind(sock_fd,(struct sockaddr*)&server_address,sizeof (server_address));
    assert(ret != -1);

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

    struct sockaddr_in client{};
    socklen_t client_addrlength = sizeof (client);
    int connfd = accept(sock_fd,(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_fd);
    return 0;
}

 5.8.2 UDP数据读写

  •  recvfrom读取sockfd上的数据,buf和 len参数分别指定读缓冲区的位置和大小。因为 UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数 src_addr所指的内容,addrlen参数则指定该地址的长度。
  • sendto往 sockfd上写入数据,buf和 len参数分别指定写缓冲区的位置和大小。dest_addr
    参数指定接收端的socket地址,addrlen参数则指定该地址的长度。
  • 这两个系统调用的flags参数以及返回值的含义均与send/recv系统调用的flags参数及返回值相同。
  • 值得一提的是,recvGom/sendto系统调用也可以用于面向连接(STREAM)的 socket的
    数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因 为我们已经和对方建立了连接,所以已经知道其socket地址了)。

5 .8 .3 通用数据读写函数

  • socket编程接口还提供了一对通用的数据读写系统调用。它们不仅能用于TCP流数据, 也能用于UDP数据报:

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

  •  msg name成员指向一个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被设置为 NULL 这是因为对数据流socket而言,对方的地址已经知道。msg namelen成员则指定了 msg_name所 指 socket地址的长度。
  • msg iov成员是iovec结构体类型的指针,iovec结构体的定义如下:

  • 由上可见,iovec结构体封装了一块内存的起始位置和长度。
  • msg_iovlen指定这样的 iovec结构对象有多少个。
  • 对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散 的内存中,这些内存的位置和长度则由msgiov指向的数组指定,这称为分散读(scatter read);
  • 对于sendmsg而言,msg iovlen块分散内存中的数据将被一并发送,这称为集中写
  • msg contro 1和 msg_controllen成员用于辅助数据的传送。我们不详细讨论它们,仅在第 13章介绍如何使用它们来实现在进程间传递文件描述符。
  • msg_flags成员无须设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg flags中。
  • recvmsg/sendmsg的 flags参数以及返回值的含义均与send/recv的 flags参数及返回值
    相同。

5 .9 带外标记

  • 实际应用中,我们通常无法预期带外数据何时到来。好在Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据 需要接收。内核通知应用程序带外数据到达的两种常见方式是:I/O复用产生的异常事件SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这一点可通过如下系统调用实现:

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

5 .1 0 地址信息函数

  • 在某些情况下,我们想知道一个连接socket的本端socket地址,以及远端的socket地 址。下面这两个函数正是用于解决这个问题:

  •  getsockname获 取 sockfd对应的本端socket地址,并将其存储于address参数指定的内
    存中,该 socket地址的长度则存储于addressjen参数指向的变量中。如果实际socket地址 的长度大于address所指内存区的大小,那么该socket地址将被截断。getsockname成功时返
    回0 , 失败返回-1并设置errno
  • getpeername获取sockfd对应的远端socket地址,其参数及返回值的含义与getsockname
    的参数及返回值相同

5.11 socket 选项

  • 如果说fcntl系统调用是控制文件描述符属性的通用POSIX方法,那么下面两个系统调 用则是专门用来读取和设置socket文件描述符属性的方法:

  •  sockfd参数指定被操作的目标socket。level参数指定要操作哪个协议的选项(即属性),
    比如IPv4、IPv6、TCP等。option_iiame参数则指定选项的名字。我们在表5-5中列举了socket通信中几个比较常用的socket选项。option value和 option len参数分别是被操作选项
    的值和长度。不同的选项具有不同类型的值,如表中 “数据类型” 一列所示。

  •  getsockopt和 setsockopt这两个函数成功时返回0 , 失败时返回-1并设置ermo
  • 值得指出的是,对服务器而言,有部分socket选项只能在调用listen系统调用前针对监
    听 socket设置才有效。这是因为连接socket只能由accept调用返回,而 accept从 listen监
    听队列中接受的连接至少已经完成了 TCP三次握手的前两个步骤(因为listen监听队列中 的连接至少已进入SYN_RCVD状态) , 这说明服务器已经往被接受连接上发送出了 TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设 置,比如TCP最大报文段选项(该选项只能由同步报文段来发送)。对这种情况,Linux给开发人员提供的解决方案是:对监听socket设置这些socket选项,那么accept返回的连接socket将自动继承这些选项。这 些 socket选项包括:SO_DEBUG、SO_ DONTROUTE、SO_KEEPALIVE、SO_LINGER、SOJDOBINLINE、SO_RCVBUF> SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT> TCP_MAXSEG 和 TCP_N0DELAYo 而 对客户端而言,这些socket选项则应该在调用connect函数之前设置,因为connect调用成功返回之后,TCP三次握手已完成。

5.11.1 SO_REUSEADDR 选项

  • TCP连接的TIME_WAIT状态,并提到服务器程序可以通过设置 socket选项 SO_REUSEADDR来强制使用被处于TIME_WAIT状态的连接占用的socket地址。具体实现方法如代码所示。
  • 重用本地IP地址
    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章讨论这两个内核参数。

修改TCP发送缓冲区的客户端程序

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>

#include "include/algorithm_text.h"
#define BUF_SIZE 1024


int main(int argc,char* argv[]) {
    if (argc <= 2){
        printf("usage:%s ip_server_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);
    //htons  将一个无符号短整型数值转换为网络字节序,即大端模式(big-endian)
    server_address.sin_port = htons(port);
    int sock_fd = socket(PF_INET,SOCK_STREAM,0);
    assert(sock_fd >= 0);

    int send_buf = atoi(argv[3]);
    int len = sizeof (send_buf);
    //先设置TCP发送缓冲区的大小,然后立即读取数据
    setsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,&send_buf,len);
    getsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,&send_buf,(socklen_t*)&len);
    printf("the tcp send buffer size after setting is %d\n", send_buf);
    if (connect(sock_fd,(struct sockaddr*)&server_address,sizeof (server_address))!=-1){
            char buffer[BUF_SIZE];
            memset(buffer, 'a', BUF_SIZE);
            send(sock_fd, buffer, BUF_SIZE, 0);
    }
    close(sock_fd);
    return 0;
}

 修改TCP接收缓冲区的服务器程序

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>

#include "include/algorithm_text.h"
#define BUF_SIZE 1024


int main(int argc,char* argv[]) {
    if (argc <= 2){
        printf("usage:%s ip_server_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);
    //htons  将一个无符号短整型数值转换为网络字节序,即大端模式(big-endian)
    server_address.sin_port = htons(port);
    int sock_fd = socket(PF_INET,SOCK_STREAM,0);
    assert(sock_fd >= 0);

    int recv_buf = atoi(argv[3]);
    int len = sizeof (recv_buf);
    //先设置TCP接收缓冲区的大小,然后立即读取数据
    setsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,&recv_buf,len);
    getsockopt(sock_fd,SOL_SOCKET,SO_SNDBUF,&recv_buf,(socklen_t*)&len);
    printf("the tcp receive buffer size after setting is %d\n", recv_buf);

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

    struct sockaddr_in client{};
    socklen_t client_addrlength = sizeof (client);
    int connfd = accept(sock_fd,(struct sockaddr*)&client,&client_addrlength);
    if (connfd < 0){
        printf("errno is:%d\n",errno);
    } else{
        char buffer[BUF_SIZE];
        memset(buffer,'\0',BUF_SIZE);
        while (recv(connfd,buffer,BUF_SIZE-1,0)>0){}
        close(connfd);

    }

    close(sock_fd);
    return 0;
}
  •  从服务器的输出来看,系统允许的TCP接收缓冲区最小为256字节。当我们设置TCP
    接收缓冲区的大小为50字节时,系统将忽略我们的设置。从客户端的输出来看,我们设置
    的TCP发送缓冲区的大小被系统增加了一倍。这两种情况和我们前面讨论的一致。

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发送缓冲区中残留的数据发送给对方。
  • 如 表 5・5 所 示 ,设 置 (获 取 )SO_LINGER选 项 的 值 时 , 我 们 需 要 给 setsockopt ( getsockopt) 系统调用传递一个linger类型的结构体,其定义如下:

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

5 .1 2 网络信息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服务器。这些在前面章节中都讨论过。这两个函数的定义如下:

  • name参数指定目标主机的主机名,addr参数指定目标主机的IP地址,len参数指定addr 所指IP地址的长度,type参数指定addi•所指IP地址的类型,其合法取值包括AF_INET (用于 IPv4地 址 )和 AF INET6 (用于IPv6地址)。
  • 这两个函数返回的都是hostent结构体类型的指针,hostent结构体的定义如下:

 5.12.2 getservbyname 和 getservbyport

  • getservbyname函数根据名称获取某个服务的完整信息,getservbyport函数根据端口号获取某个服务的完整信息。它们实际上都是通过读取/etc/services文件来获取服务的信息的。 这两个函数的定义如下:

  •  name参数指定目标服务的名字,port参数指定目标服务对应的端口号。proto参数指定 服务类型,给它传递“tcp” 表示获取流服务,给它传递“udp” 表示获取数据报服务,给它 传递NULL则表示获取所有类型的服务。
  • 这两个函数返回的都是servent结构体类型的指针,结构体servent的定义如下:

  •  下面我们通过主机名和服务名来访问目标服务器上的daytime服务,以获取该机器的系 统时间,如代码清单5-12所示。

访问daytime服务

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <netdb.h>

#include "include/algorithm_text.h"
#define BUF_SIZE 1024


int main(int argc,char* argv[]) {
    assert(argc == 2);
    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 sockfd = socket(AF_INET,SOCK_STREAM,0);
    int result = connect(sockfd,(struct sockaddr*)&address,sizeof (address));
    assert(result != -1);
    char buffer[128];
    result = read(sockfd,buffer,sizeof (buffer));
    assert(result > 0);
    buffer[result] = '\0';
    printf("the day tiem is: %s",buffer);
    close(sockfd);
    return 0;
}
  •  需要指出的是,上面讨论的4个函数都是不可重入的,即非线程安全的。不过netdb.h 头文件给出了它们的可重入版本。正如Linux下所有其他函数的可重入版本的命名规则那样, 这些函数的函数名是在原函数名尾部加上_r ( re-entrant) 。

5.12.3 getaddrinfo

  • getaddrinfo函数既能通过主机名获得IP 地 址 (内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。它是否可重入取决于其内部调用的gethostbyname和 getservbyname函数是否是它们的可重入版本。该函数的定义如下:
  • #include <netdb.h>

  • hostname参数可以接收主机名,也可以接收字符串表示的IP地 址 (IPv4采用点分十 进制字符串,IPv6则采用十六进制字符串)。同样,service参数可以接收服务名,也可以 接收字符串表表示的十进制端口号。hints参数是应用程序给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制。hints参数可以被设置为N U L L ,表示允许getaddrinfo反馈任何可用的结果。result参数指向一个链表,该链表用于存储getaddrinfo反馈的结果。
  • getaddrinfo反馈的每一条结果都是addrinfo结构体类型的对象,结构体addrinfo的定义如下:

 

  •  该结构体中,ai_protocol成员是指具体的网络协议,其含义和socket系统调用的第三个 参数相同,它通常被设置为0。ai_flags成员可以取表5-6中的标志的按位或。

  •  当我们使用hints参数的时候 ,可以设置其ai_flags, ai_family, ai_socktype和 ai_protocol四个字段,其他字段则必须被设置为NULL。例如,代码清单 13利用了 hints参数获取主机emest-laptop 上的 "daytime” 流服务信息

 5.12.4 getnameinfo

  • getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是 gethostbyaddr函数)和 服 务 名 (内部使用的是getservbyport函数)。它是否可重入取决于 其内部调用的gethostbyaddr和 getservbyport函数是否是它们的可重入版本。该函数的定义 如下:

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

  •  getaddrinfo和 getnameinfo函数成功时返回0 , 失败则返回错误码,可能的错误码如表 所示

  •  Linux下 strerror函数能将数值错误码ermo,转换成易读的字符串形式。同样,下面的函 数可将表5-8中的错误码转换成其字符串形式:

​​​​​​​

参考链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值