读书笔记_《Linux高性能服务器编程》_第 5 章:网络编程基础API

第 5 章 Linux网络编程基础API

知识要点:

  • socket 地址 API
  • socket 基础 API
  • 网络信息 API

1、socket 地址API

主机字节序和网络字节序

CPU (32位)的累加器一次至少可以装载 4 字节,即一个整数。该 4 字节在内存中的排列顺序将影响其被累加器装载成的整数的值 => 字节序问题。

大端字节序(网络字节序): 数据的高字节数据存储在内存的低地址处。当数据在使用不同字节序的机器间传递数据时,对于数据的解析将会出错,因此,规定在发送端总是将数据转化为大端字节序数据后再进行发送,接收端知道数据总是大端字节序,则根据自身本机采用的字节序决定是否需要转化,确定了数据收发的正确性。

小端字节序(主机字节序): 数据的低字节数据存储在内存的低地址处。一般的 PC 都是采用小端字节序,因此又称主机字节序。

通用 socket 地址

#include <bits/socket.h>
struct sockaddr
{
    //地址族变量 与协议族类型对应
    sa_family_t sa_family;
    //存放 socket 地址值
    //不同的协议的含义和长度不同,后续针对具体协议专用的结构体
    char sa_data[14];
};

常见协议族(protocol family,也称 domin)与对应地址族如下图,两者具有相同的值,可以混用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uLSC5UEN-1623659442704)(./Pic/table-5-1.png)]

sa_data 成员存放的 socket 地址值 对应关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cca5W0EI-1623659442709)(./Pic/table-5-2.png)]

14 字节的 sa_data 成员无法容纳多数协议族的地址值,提出新的通用 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作用)

专用 socket 地址

Linux 为各协议族提供专门的 socket 地址结构体,方便设置与获取 IP 地址和端口。

Unix:

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

IPV4:

struct in_addr
{
    u_int32_t s_addr;//IPV4地址,网络字节序表示
};

struct sockaddr_in
{
    sa_family_t sin_family;//地址族:AF_INET
    u_int16_t sin_port;//端口号,网络字节序表示
    struct in_addr sin_addr;//IPV4地址结构体
};

IPV6:

struct in6_addr
{
    unsigned char sa_addr[16];//IPV6地址,网络字节序表示
};

struct sockaddr_in
{
    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
};

注意: 所有专用 socket 地址在使用 socket 编程接口时都需要转换成通用 socket 地址(sockaddr 类型)。

IP 地址转换

IPV4:

#include <arpa/inet.h>
//将用点分十进制字符串表示的 IPV4 地址转换成网络字节序整数表示的 IPV4 地址,
//失败返回 INADDR_NONE
in_addr_t inet_addr(const char* strptr);
//与 inet_addr 功能一致,将结果存入 inp 指向的结构体中
//失败返回 0,成功返回 1
int inet_aton(const char* cp, struct in_addr* inp);
//将网络字节序整数表示的 IPV4 地址转化为用点分十进制表示的 IPV4 地址
//内部使用静态变量存储转化结果,返回值指向该静态内存,不可重入
//失败返回 0,成功返回 1
char* inet_ntoa(struct in_addr in);

IPV4 与 IPV6 同时适用:

#include <arpa/inet.h>
//10进制点分字符串的IPV4地址或16进制IPV6地址转换成网络字节序整数的IP地址,并存入dst内存
//af:对应的协议族 AF_INET 或 AF_INET6
//失败返回 0,成功返回 1
int inet_pton(int af, const char* src, void* dst);

//与inet_pton函数相反,socklen_t 指定目标存储单元的大小见后续宏
//成功返回目标存储单元地址,失败返回NULL并设置errno
const char* inet_ntop(int af, const char* src, char* dst, socklen_t cnt);

#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

2、创建 socket

#include <sys/types.h>
#include <sys/socket.h>
/**
	domin:底层协议族 PF_INET/PF_INET6/PF_UNIX
	type:服务类型 SOCK_STREAM/SOCK_DGRAM
	protocol: 具体协议,唯一值,一般设为0表示使用默认协议
	成功返回 socket 文件描述符,失败返回 -1 并置 errno
*/
int socket(int domin, int type, int protocol);

拓展: type值可以和 SOCK_NONBLOCK 及 SOCK_CLOEXEC 标志相与,表示新创建的 socket 为非阻塞,以及 fork 调用创建子进程时在子进程中关闭该socket

3、命名 socket

#include <sys/types.h>
#include <sys/socket.h>
/**
	fd: 需要命名的socket的文件描述符
	myaddr: 分配的 socket 地址
	addrlen: sizeof(myaddr)
	成功返回0,失败返回 -1 置 errno
	EACCESS: 被绑定的地址是受保护的地址,仅超级用户能够访问,如:绑定到知名服务端口(0~1023)
	EADDRINUSE: 被绑定的地址正在使用,如:处于TIME_WAIT状态的 socket 地址
*/
int bind(int fd, const struct sockaddr* myaddr, socklen_t addrlen);

4、监听 socket

socket 被命名后,需要调用系统调用来创建一个监听队列存放待处理的客户连接。

#include <sys/socket.h>
/**
	sockfd: 被监听的 socket
	backlog: 监听队列中处于ESTABLISED状态的最大值,当超过时服务器不再受理新的客户连接,客户	端收到 ECONNREFUSED 错误信息(但实际值往往可能为backlog+1)
	成功返回0,失败返回 -1 置 errno
*/
int listen(int sockfd, int backlog);

5、接受连接

#include <sys/types.h>
#include <sys/socket.h>
/**
	sockfd: listen系统调用的监听socket
	addr: 获取接受连接的远程socket地址
	addrlen: socket长度
	成功返回0,失败返回 -1 置 errno
*/
int accept(int sockfd,struct sockaddr* addr, socklen_t* addrlen);

//accept只是从监听队列中取出连接,而不论连接处于何种状态(如CLOSE_WAIT),更不关心网络状况的变化

6、发起连接

客户端主动的发起连接,服务端被动的接受连接。

#include <sys/types.h>
#include <sys/socket.h>
/**
	sockfd: 创建socket返回的文件描述符,通过读取该sockfd来与服务端进行通信
	serv_addr: 需要连接的服务端socket地址
	addrlen: socket地址长度
	成功返回0,失败返回 -1 置 errno
*/
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t* addrlen);

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

7、关闭连接

#include <unistd.h>
/**
	并非是立刻关闭一个连接,而是将fd的引用计数减1,当fd的引用计数为0才真正关闭连接
	多进程时,一次fork默认会将父进程打开的socket的引用加1,必须在父子进程中都对该socket执行	close调用才能将连接关闭。
*/
int close(int fd);

立即终止连接!

#include <sys/socket.h>
/**
	成功返回0,失败返回 -1 置 errno
*/
int shutdown(int sockfd, int howto);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mmLxOTnN-1623659442711)(./Pic/table-5-3.png)]

8、数据读写

TCP 数据读写

#include <sys/types.h>
#include <sys/socket.h>
/**
	sockfd: 从sockfd读取、往sockfd写入
	buf: 读/写缓冲区
	len: 缓冲区长度
	flags: 额外控制的标志
*/

//成功返回实际读取到的长度,可能小于期望值len则可以多次调用读取完整
//返回0时,以为通信对方已经关闭连接
//失败返回 -1 置 errno
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
//成功返回实际写入的长度,失败返回 -1 置 errno
ssize_t send(int sockfd, const void* buf, size_t len, int flags);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BnO3HIFk-1623659442717)(./Pic/table-5-4.png)]

send 与 recv 设置的flags是一次性的,可以通过 setsockopt 系统调用永久性的修改 socket 某些属性。

UDP 数据读写

#include <sys/types.h>
#include <sys/socket.h>
/**
	UDP协议不是面向连接的,
	接收时都要记录发送方的地址和端口
	发送时需要指明发送给哪个地址和端口
	返回值与send/recv一致,TCP使用该接口时可以把最后两个参数置为 NULL
*/
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr_in* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, struct sockaddr_in* dest_addr, socklen_t* addrlen);

通用数据读写函数

socket 编程接口提供即支持 TCP 又支持 UDP 的读写系统调用。

#include <sys/socket.h>

struct iovec
{
    void* iov_base;//内存起始地址
    size_t iov_len;//内存的长度
};

struct msghdr
{
    //socket地址,指向 socket 地址结构变量,指定通信对方socket地址,对于TCP无用置为NULL
    void* msg_name;
    //socket地址的长度
    socklen_t msg_namelen;
    //分散的内存块
    struct iovec* msg_iov;
    //分散内存块的数量
    int msg_iovlen;
    //指向辅助数据的起始位置
    void* msg_control;
    //辅助数据的大小
    socklen_t msg_controllen;
    //复制函数中flags参数,在调用过程中更新
    int msg_flags;
};

//返回值与send/recv一致
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);

分散读: 对于recvmsg读取的数据将存放在msg_iovlen块分散的内存中,内存的地址及长度由msg_iov指向的数组决定

集中写: 对于sendmsg而言,msg_iovlen块分散内存中的数据将一并发送

9、带外数据

Linux 内核检测到 TCP 紧急标志时,将会通过 IO 复用产生的异常事件和 SIGURG 信号 来通知应用程序有带外数据需要接收。

#include <sys/socket.h>
/**
	判断 sockfd 下一个被读取到的数据是否是带外数据
	是:返回 1 ,否:返回0
*/
int sockatmark(int sockfd);

10、获取地址信息函数

#include <sys/socket.h>
/**
	获取本端连接的socket地址信息存入address,长度存入address_len
	若实际socket地址长度比address所指向的内存区大,则地址将被截断
	成功返回0,失败返回 -1 置 errno
*/
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
/**
	获取通信连接另一端的socket地址
	参数和返回值含义与getsockname同
*/
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);

11、socket 选项

fcntl 系统调用专门用来控制文件描述符属性,socket 编程接口也提供了设置 socket 文件描述符属性的方法:

#include <sys/socket.h>
int getsockopt(int sockfd,//指定被操作的 目标socket
               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);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tCv2cjpH-1623659442721)(./Pic/table-5-5.png)]

注意: 对于服务端而言, listen 之后,accept 从监听队列中返回的连接至少完成了 TCP 三次握手的前两阶段即进入 SYN_RECVD 状态,通信双方的同步报文段都已经发送给对方,而有些属性如:TCP最大报文段,则只能由同步报文段来发送,所以需要在 listen 之前设置属性;对于客户端则需要在 connect 之前设置属性。

对 listen 之前设置的属性,accept 返回的连接将自动继承:SO_DEBUG、SO_DONTROUTE、SO_ KEEPALIVE、SO_ LINGER、 SO_ OOBINLINE、SO RCVBUF、SO_RCVLOWAT、SO SNDBUF、SO SNDLOWAT、TCP_ MAXSEG 和 TCP_ NODELAY

11、网络信息 API

gethostbyname 和 gethostbyaddr

gethostbyname 通过主机名称获取主机的完整信息,函数通常先在本地的 /etc/hosts 配置文件中查找主机,如果没有找到再去访问 DNS 服务器。

gethostbyaddr 通过地址名获取主机的完整信息。

#include <netdb.h>

struct hostent
{
	char* h_name;//主机名
	char** h_aliases;//主机别名列表
	int h_addrtype;//地址族类型
	int h_length;//地址长度
	char** h_addr_list;//网络字节序列出的主机IP地址列表
};

/**
	name: 目标主机名称
*/
struct hostent* gethostbyname(const char* name);

/**
	name: 目标主机IP地址
	len: 指定IP地址的长度
	type: AF_INET/AF_INET6
*/
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);

getservbyname 和 getservbyport

通过服务名称获取某个服务的完整信息 及 通过端口获取某个服务的完整信息。

都是通过读取 /etc/services 文件获取服务的信息。

#include <netdb.h>

struct servent
{
    char* s_name;//服务的名称
    char** s_aliases;//服务的别名列表
    int s_port;//服务的端口号
    char* s_proto;//服务类型,tcp或udp
};
/**
	name: 目标服务的名称
	proto: 服务类型,"tcp"表示获取数据流服务,“udp”表示获取数据报服务
*/
struct servent* getservbyname (const char* name, const char* proto);
/**
	name: 目标服务的端口
	proto: 服务类型,"tcp"表示获取数据流服务,“udp”表示获取数据报服务
*/
struct servent* getservbyport (int port, const char* proto);

getaddrinfo

即能通过主机名获取IP地址(内部gethostbyname),也能通过服务名获取端口号(内部getservbyname)

#include <netdb.h>

struct addrinfo
{
    int ai_flags;//取表5-6中标志的按位或
    int ai_family;//地址族
    int ai_socktype;//服务类型,SOCK_STREAM/SOCK_DGRAM
    int ai_protocol;//指具体协议,与socket系统调用一致,一般设为0
    socklen_t ai_addrlen;//ai_addr的长度
    char* ai_canonname;//主机的别名
    struct sockaddr* ai_addr;//指向socket地址
    struct addrinfo* ai_next;//指向下一个sockinfo结构的对象
};

int getaddrinfo(
    			//即可接收主机名又可字符串表示的IP地址(IPV4: 点分十进制,IPV6: 十六进制)
    			const char* hostname,
    			//即可接收服务名也可接收字符串表示的十进制端口号
                const char* service, 
    			//精确控制输出,NULL表示反馈任何可用的结果;
    			//使用时可以设置ai_flags、ai_family、ai_socktype、ai_protocol四个字段,其余必须置为NULL
                const struct addrinfo* hints,
    			//指向一个反馈结果的链表
                struct addrinfo** result);

//getaddrinfo 将隐式的分配堆内存(result),需要配对的函数释放内存
void freeaddrinfo(struct addrinfo* res);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5mGbK3V0-1623659442724)(./Pic/table-5-6.png)]

getnameinfo

通过socket地址同时获取字符串表示的主机名(内部gethostbyaddr)和服务名(内部getservbyport)

#include <netdb.h>
int getnameinfo(const struct sockaddr* sockaddr,//传入的socket地址
                socklen_t addrlen,//地址长度
                char* host,//返回的主机名
              	socklen_t hostlen,//长度
                char* serv, //返回的服务名
                socklen_t servlen,//长度
                int flags//精确控制行为
               );

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRwmFVha-1623659442725)(./Pic/table-5-7.png)]

**getaddrinfo 和 getnameinfo 函数成功返回 0,失败返回错误码 **

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A9R9lrWV-1623659442727)(./Pic/table-5-8.png)]

#include <netdb.h>
//将数值错误码转换成易读字符串
const char* gai_strerror(int error);
名称作用特点
gethostbyname通过主机名称获取主机的完整信息不可重入(xxx_r版本可以)
gethostbyaddr通过地址名称获取主机的完整信息不可重入(xxx_r版本可以)
getservbyname通过服务名称获取某个服务的完整信息不可重入(xxx_r版本可以)
getservbyport通过端口获取某个服务的完整信息不可重入(xxx_r版本可以)
getaddrinfo即能通过主机名获取IP地址(内部gethostbyname),也能通过服务名获取端口号(内部getservbyname)是否可重入取决于调用的内部函数是否可重入
getnameinfo通过socket地址同时获取字符串表示的主机名(内部gethostbyaddr)和服务名(内部getservbyport)是否可重入取决于调用的内部函数是否可重入
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值