Linux 高性能服务器编程第五章学习

字节序:

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

小端字节序( little endian) : 一个整数的高位字节 (23 ~ 31 bit) 存储在内存的高地址处,低位字节(0 ~ 7 bit) 存储在内存的低地址处 ( 应该可以抽象地理解成普通数字的顺序 – 高位在前,低位在后 )

Linux 字节序转换函数

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

通用 socket 地址

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

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

专用 socket 地址

UNIX本地域

UNIX本地域专用 socket 地址结构体

#include <sys/un.h>
struct sockaddr_un{
    sa_family_t sin_family; /* 地址族: AF_UNIX */
    char sun_path[108]; /* 文件路径名 */
};

TCP/IP 协议族

TCP/IP 协议族有 sockaddr_insockaddr_in6 两个专用 socket 结构体,分别用于 IPv4IPv6

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

IP 地址转换函数

#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_ntoa 函数将用网络字节序整数表示的 IPv4 地址转化为用点分十进制字符粗汉表示的 IPv4地址。但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此 inet_ntoa 是不可重入的

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
    char* ip1 = "10.194.71.60";
    char* ip2 = "1.2.3.4";
    char* szValue1 = inet_ntoa(*(struct in_addr*)ip1); // 强转
    char* szValue2 = inet_ntoa(*(struct in_addr*)ip2);
    printf("address 1: %s\n", szValue1);
    printf("address 2: %s\n", szValue2);
    return 0;
}
/*
address 1: 49.46.50.46
address 2: 49.46.50.46
*/

创建 socket

对于 UNIX/Linux 来说,所有东西都是文件,包括 socket,可读、可写、可控制和可关闭的文件描述符

#include <sys/types.h>
#include <sys/socket.h>
int socket( int domain, int type, int protocol );
  • domain: 使用哪个底层协议族。
    • PF_INET (Protocol Family of Internet, 用于IPv4)
    • PF_INET6 (用于IPv6)
  • type: 指定服务类型
    • SOCK_STREAM (流服务 --> TCP协议)
    • SOCK_UGRAM (数据报)
    • SOCK_DGRAM (UDP协议)
  • 通常为 0

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

命名 socket

明明 socket 的系统调用的是 bind

#include <sys/types.h>
#include <sys/socket.h>
int bind( int sockfd, const struct sockaddr* my_addr, socklen_t addrlen );

bindmy_addr 所指的 socket 地址分配给未命名的 sockfd 文件描述符, addrlen 参数指出该 socket 地址的长度

成功返回 0, 失败返回 -1

监听 socket

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

#include <sys/socket.h>
int listen( int sockfd, int backlog );

sockfd 参数指定被监听的 socketbacklog 参数体制内核监听队列的最大长度。如果队列超过该长度,服务器不接受新的客户连接。典型值为 5. listen 成功返回 0, 失败返回 -1

服务器简洁实现

#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>
#include <stdbool.h>

static bool stop = false;
/* SIGTERM 信号的处理函数, 触发时结束主程序中的循环 */
static void handle_term ( int sig ) {
    stop = true;
}

int main( int argc, char* argv[]){
    signal (SIGTERM, handle_term );
    if(argc <= 3 ){
        printf( "usage: ip_address port_number backlog\n");
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );
    int backlog = atoi( argv[3] );
    int sock = socket( PF_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 );

    int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );
    ret = listen( sock, backlog );
    assert( ret != -1 );
    /* 循环等待连接, 直到有 SIGTERM 信号将它中断 */
    while( stop == false ){
        sleep( 1 );
    }
    close( sock );
    return 0;
}

接受连接

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

#include <sys/types.h>
#Include <sys/socket.h>
int accept( int sockfd, struct sockaddr* addr, socklen_t* addrlen);

注意: accept() 知识从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化

发起连接

客户端通过如下系统调用来主动与服务器建立连接

#include <sys/types.h>
#include <sys/socket.h>
int connect( int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen );
  • sockfd 参数由 socket 系统调用返回一个 socketserv_addr 参数是服务器监听的 socket 地址,addrlen 参数则指定这个地址的长度
  • connect 成功时返回 0 。一旦成功建立连接, sockfd 就唯一地标识了这个连接,客户端就可以通过读写 sockfd 来与服务器通信。失败返回 -1

关闭连接

#include <unistd.h>
int close( int fd );

close 系统调用并非总是立即关闭一个连接,而是将 fd 的引用计数 -1。只有当 fd 计数为 0 时,才真正关闭连接。多进程程序中,一次 fork 系统调用默认将使父进程中打开的 socket 的引用计数加 1 。

可使用

#include <sys/socket.h>
int shutdown( int sockfd, int howto )

进行立即终止连接

数据读写

TCP数据读写

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

flag 参数为数据收发提供额外的控制,如 MSG_OOB 为应用程序提供了发送和接受带外数据的方法

server.c

#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>
#include <stdbool.h>
#include <errno.h>

#define BUF_SIZE 1024
char* basename( char* fullname );

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 );
    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{
        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 );
        ret = recv( connfd, buffer, BUF_SIZE - 1, MSG_OOB );
        printf( "got %d bytes of oob data '%s'\n", ret, buffer );
        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;
}

client.c

#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>
#include <stdbool.h>
#include <errno.h>

char* basename( char* fullname );

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 sockfd = socket( AF_INET, SOCK_STREAM, 0);
    assert( sockfd >= 0 );
    if( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( struct sockaddr_in ) ) < 0 ){
        printf( "connection errno is : %d\n", errno );
    }
    else{
        const char* oob_data = "abc";
        const char* normal_data = "123";
        send( sockfd, normal_data, strlen( normal_data ), 0 );
        send( sockfd, oob_data, strlen( oob_data ), MSG_OOB );
        send( sockfd, normal_data, strlen( normal_data ), 0 );
    }
    close( sockfd );
    return 0;
}

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 上的数据, buflen 参数分别指定读缓冲区的位置和大小。因为 UDP 通信没有连接的概念,所以每次读取数据都需要获取发送端的 socket 地址,即参数 src_addr 所指的内容, addrlen 参数则指定该地址的长度

sendto 类似

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

通用数据读写函数

#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

  • msgmsghdr 结构体类型地指针, 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			/* 这块内存的长度 */
      };
      

      iovec 结构体封装了一块内存的起始位置和长度。msg_iovlen 指定这样的 iovec 结构体对象有多少个。recvmsg 则利用这些信息进行分散读 (scatter read); 对于 sendmsg 而言, msg_iovlen 块分散内存中的数据将被一并发送,即集中写 (gather write)

地址信息函数

想知道一个连接 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 地址将被姐u但。 getsockname 成功时返回 0 , 失败返回 -1 并设置 errno。

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

level 参数指定要操作哪个协议的选项。

leveloption name数据类型说明
SOL_SOCKET( 通用 socket 选项, 与协议无关)SO_DEBUGint打开调试信息
SO_REUSEADDRint重用本地地址
SO_TYPEint获取 socket 类型
SO_ERRORint获取并清除 sokcet 错误状态
SO_DONTROUTEint不查看路由表,直接将数据发送给本地局域网内的主机
SO_RCVBUFintTCP 接受缓冲区大小
SO_SNDBUFintTCP 发送缓冲区大小
SO_KEEPALIVEint发送周期性保活报文以维持连接
SO_OOBINLINEint接受到的带外数据将存留在普通数据的输入队列中(在线存留),此时我们不能使用带 MSG_OOB 标志的读操作来读取带外数据 (应该想读取普通数据那样读取带外数据)
SO_LINGERlinger若有数据待发送,则延迟关闭
SO_RCVLOWATintTCP 接受缓存区低水位标记
SO_SNDLOWATintTCP 发送缓存区低水位标记
SO_RCVTIMEOtimeval接受数据超时 ( 11 章 )
SO_SNDTIMEOtimeval发送数据超时 ( 11 章 )
IPPROTO_IP(IPv4选项)IP_TOSint服务类型
IP_TTLint存活时间
IPPROTO_IP(IPv6选项)IPV6_NEXTHOPsockaddr_in6吓一跳 IP 地址
IPV6_RECVPKTINFOint接收分组信息
IPV6_DONTFRAGint禁止分片
IPV6_RECVTCLASSint接收通信类型
IPPROTO_TCP(IPv6选项)TCP_MAXSEGintTCP 最大报文段大小
TCP_NODELAYint禁止 Nagle 算法

网络信息API

socket 地址的两个要素,即 IP 地址和端口号,都是用数值表示的。不便于记忆和扩展(IPv4 -> IPv6) 。所以常用主机名来访问一台机器,而避免直接使用 IP 地址。同样用服务名称来代替端口号。比如下面两个 telnet 命令具有完全相同的作用:

telnet 127.0.0.1 80
telnet localhost www

上诉例子中, telnet 客户端程序是通过调用某些网络信息 API 来实现主机名到 IP 地址的转换,以及服务名称到端口号的转换的

gethostbyname 和 gethostbyaddr

gethostbyname 根据主机名称获取主机的完整信息,gethostbyaddr 根据 IP 地址获取主机的完整信息。定义如下:

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

getservbyname 和 getservbyport

getservbyname 根据名称获取某个服务的完整信息,getservbyport 根据端口号获取某个服务的完整信息

#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 */
}

getaddrinfo

getaddrinfo 既能通过主机名获得 IP 地址 (内部使用的是 gethostbyname), 也能通过服务名获得端口号 (内部使用的是 getservbyname)。该函数定义如下:

int getaddrinfo( const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result );
  • hostname 接收主机名,也可以接收字符串表示的 IP 地址
  • service 接收服务名,也可以接收字符串表示的十进制端口号
  • hints 是应用程序给 getaddrinfo 的一个提示,以对 getaddrinfo 的输出进行更精确的控制,可以被置为 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 结构的对象 */
};

getnameinfo

getnameinfo 函数通过 socket 地址同时获得以字符串表示的主机名 ( 内部使用的是 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 参数指向的缓存中, hostlenservlen 参数分别指定这两块缓存的长度。 flag 参数控制 getnameinfo 的行为。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Artintel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值