0
点赞
收藏
分享

微信扫一扫

Linux 网络编程基础 API——《Linux高性能服务器编程》读书笔记

迪莉娅1979 2022-04-13 阅读 97
linux网络

1、socket地址API

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

主机字节序:
在这里插入图片描述
现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。

大端字节序和小端字节序会产生一个问题,当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然会进行错误的解释。解决办法是:发送端总把要发送的数据转换成大端字节序,再发送,而接收端总采用大端字节序接收,并根据需要转换成自己使用的字节序。

因此,大端字节序也称为网络字节序

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

#include<netinet/in.h>
unsigned long int htonl( unsigned long int hostlong );		——"host to networt long"
unsigned short int htons( unsigned short int hostshort );	——"host to network short"
unsigned long int ntohl( unsigned long int netlong );		——"network to host long"
unsigned short int ntohs( unsigned short int netshort );	——"network to host short"
在windows下用C++需要添加下面的头文件:
#include <winsock2.h>
#pragma comment(lib,“ws2_32.lib”)

看函数名就能知道功能,如htonl,即将长整型的主机字节序转化为网络字节序数据。这四个函数中,长整型通常用来转换IP地址,短整型函数用来转换端口号。当然,它们的功能远远不止于此,任何格式化的数据通过网络传输时,都应该使用这些函数来转换字节序。

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)的变量。地址组类型通常与协议族类型对应。常见的协议族和对应的地址族如下表所示:
在这里插入图片描述
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 __sspadding[128-sizeof(__ss_align)];
}

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

理解socket地址

假设小明同学想给女神打电话需要知道对方的电话号码,而我们进行网络通信也要知道对方的socket地址。 电话号码以北京地区为例,如010-82512345,有两部分组成,3位区号,8位号码,3-8组合。区号用于标示这在北京,号码用于标示北京具体哪个电话机。

而在网络通信中,采用类似方法标示socket地址。 socket地址最关键的两部分为:(ip,port),即IP地址和 port 端口号,比如一个网络地址为192.168.130.55:8000, 里面的IP地址用于区分计算机,而端口号8000用于区分具体是哪一个socket

1.3 专用socket地址

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.4 IP地址转换函数

在这里插入图片描述

#include<arpa/inet.h>
in_addr_t inet_addr( const char* strptr );
int inet_aton( const char* cp, struct in_addr* int );
char* inet_ntoa( struct in_addr in );
  • net_addr函数把点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。他失败时返回INADDR_NONE
  • inet_aton函数的功能和inet_addr一样,但是将转化结果存储于参数inp指向的结构体中。他成功转化返回1,失败返回0。
  • inet_ntoa函数把网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。但是要注意,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。下面的代码揭示了其不可重入性:
char* p1 = inet_ntoa( "1.2.3.4" );
char* p2 = inet_ntop( "10.194.71.60" );
printf("address 1: %s\n", p1);
printf("address 2: %s\n", p2);

输出:

address 1: 10.194.71.60
address 2: 10.194.71.60

在这里插入图片描述

2、创建socket(文件描述符)

说是创建一个socket,但创建出来的其实只是一个socket文件描述符,连端口都还没有确定,他携带的信息仅有:该socket文件描述符使用哪种协议(TCP、UDP等)。
在这里插入图片描述

  • domain告诉系统使用哪个协议族。对TCP/IP协议族而言,其值设置为PF_INETPF_INET6;对于UNIX本地与协议族而言,设置为PF_UNIX
  • type参数指定服务类型。服务类型主要有SOCK_STREAM(流服务) 和 SOCK_UGRAM(数据报服务)。分别适用于TCP/IP协议族 和 UDP协议。
    在这里插入图片描述
  • protoaol是在前两个参数构成的集合下,再选一个具体的协议。几乎在所有情况下,都应该把它设置为0,表示使用默认协议。

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

3、命名socketbind

有了 socket地址socket文件描述符还做不成任何事情,需要把他俩绑定在一起,这也就是bind的工作:

  • 服务器程序中,通常要给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 );

bindmy_addr指向的socket地址分配给未命名的文件描述符sockfdaddrlen参数指出socket地址的长度。
在这里插入图片描述

4、(服务器)监听socketlisten)(被动接受连接)

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

#include<sys/socket.h>
int listen(int sockfd, int backlog);
  • sockfd参数指定被监听的socket。(socket地址 和 socket文件描述符已经被bind在一起了,其中 socket文件描述符是主体,他俩绑定在一起后,我们只用文件描述符来指示某个具体的socket
  • backlog参数限定 监听队列中处于完全连接状态(ESTABLISHED)的连接的最大数量。也就是说,监听队列中,最多只能有backlog个连接完成了三报文握手过程(也就是ESTABLISHED状态),其他连接只能停留在SYN_RECV状态。(这两个状态的详情请参考这篇博客的3.4小节)不过,在实际实验中,处于完全连接状态的最大连接数一般是backlog+1
  • listen成功时返回0,失败返回-1,并设置errno

调用过listen之后,传入listensocket文件描述符自动变成了一个 监听socket他就代表着该socket的监听队列。这一性质,将在下一小节:接受连接中使用。

5、(服务器)接受连接(accept

只有接受了某个客户端的连接,并获取到新的socket,服务器才可通过读写该socket来与被接受连接对应的客户端通信accept系统调用正是用来解决这个问题,他从listen产生的监听队列中接受一个连接:

#include<sys/socket.h>
#include<sys/types.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
  • sockfd参数是执行过listen系统调用的监听socket
  • addr参数用来获取被接受连接的远端socket地址。
  • addrlen参数指出远端socket地址的长度。

accept成功时返回一个新的socket ,该socket唯一标识了被接受的连接。服务器可通过读写该socket与被接受连接对应的客户端通信。
accept失败时返回-1并设置errno

accept对客户端的实时情况毫不知情

accept只是自以为是的从监听队列中取出连接,而不论连接出于何种状态,更不关心网络状况如何变化。
什么??不懂?好,我给你细细道来:
我们不是用accept从监听队列里拿出来一个连接吗?只要监听队列里面有ESTABLISHED的连接,accept就不管三七二十一,返回一个socket,说:“拿去吧!你可以用这个socket和客户端通信了。”
这哪行啊?玩意这时候客户端那边断网了呢?怎么可能和他通信?

6、(客户端)发起连接

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

#include<sys/socket.h>
#include<sys/types.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
  • sockfdsocket系统调用返回文件描述符
  • serv_addr是使用服务器的IP地址和端口号创建的 “服务器” 的socket地址
  • socklen_tsocket地址的长度
  • connect成功返回0,失败返回-1并设置errno。两种常见的errno如下:在这里插入图片描述

一旦执行了这个函数之后,sockfd就唯一标识了该连接,客户端通过读写该sockfd与服务器通信。

7、关闭连接

关闭普通文件描述符的系统调用如下:

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

在这里插入图片描述
在这里插入图片描述

8、数据读写

8.1 TCP数据读写

用于TCP读写的系统调用:

#icnlude<sys/socket.h>
#include<sys/types.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

  • recv读取sockfd上的数据
  • buf指针指向存储读入数据的起始地址
  • len指定读入数据的大小
  • flags参数见后文
  • recv成功时,返回实际读取到的数据的长度,他可能小于期望的长度len。因此可能要多次调用recv,才能读取到完整的数据。recv可能返回0,意味着对方已经关闭连接了。recv出错时返回-1,并设置errno

对于send

  • sendsockfd上写入数据
  • buf指针指向被写数据的起始地址
  • len指定要写的数据的大小
  • send成功时饭hi实际写入数据的长度,失败返回-1并设置errno

在这里插入图片描述
在这里插入图片描述

8.2 UDP数据读写

用于UDP数据报读写的系统调用是:

#include<sys/socketh.>
#include<sys/types.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

  • 前四个参数都和TCP的recv函数相同
  • src_addr参数是指向发送端的socket地址的指针
  • addrlen参数指出发送端socket地址的长度

对于sendto

  • 前四个参数都和TCP的send函数相同
  • src_addr参数是指向接收端的socket地址的指针
  • addrlen参数指出接收端socket地址的长度

这两个系统调用的返回值含义与recvsend的返回值含义相同。
由于UDP提供非连接服务,所以每次读取和发送都要获取对方的socket地址,及其长度。
在这里插入图片描述

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结构体类型的指针,其定义如下:在这里插入图片描述
  • ——msg_name指向socket地址的指针,它指定对方的socket地址。对于面向连接的TCP协议,该成员无意义必须设置为NULL
  • ——msg_namenlen指定了msg_name地址所指的socket地址的长度
  • ——msg_iov成员是iovec结构体类型的指针(其实就是个结构体数组,msg_iov是指向其首元素的指针),iovec结构体定义如下在这里插入图片描述
    iovec结构体封装了一块内存的起始地址和长度。
  • ——msg_iovlen指定了这样的 iovec结构体对象有多少个。(结构体数组的长度
    • 对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度由msg_iov指向的iovec数组指定,这称为分散读
    • 对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写

在这里插入图片描述

9、带外标记

在这里插入图片描述

#include<sys/socket.h>
int sockatmark(int sockfd);

在这里插入图片描述

10、地址信息函数

某些情况下,想获取一个连接的本端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对应的本端IP地址,并把它存储到address指向的socket地址中,该socket地址的长度存储于address_len指向的变量中。如果实际socket地址的长度大于address所指内存区的大小,那么socket地址将被截断getsockname成功返回0,失败返回-1,并设置errno
  • getpeername获取sockfd对应的远端IP地址,参数及返回值含义与getsockname相同。

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 opiton_len);
  • sockfd参数指定被操作的目标socket.
  • level参数指定要操作哪个协议的属性(或选项),比如IPv4、IPv6、TCP等。
  • option_name参数指定属性的名字。下表中列举了socket通信中几个比较常用的socket属性。在这里插入图片描述
  • option_valueoption_len参数分别是被操作选项的值和长度。不同选项具有不同类型的值,如上表中数据类型一列所示。
    setsockoptsocket的相应属性设置为option_value的值
    getsockopt获取socket的相应属性,并将其值写入option_value

在这里插入图片描述在这里插入图片描述

11.1 SO_REUSEADDR选项

在这里插入图片描述
当然,SO_REUSEADDR的使用情景远不止于此(详细用法可以参考《UNIX网络编程》P179)。
现在先记住SO_REUSEADDR的作用是:可以重用已经处于连接状态的端口。

11.2 SO_RCVBUFSO_SNDBUF选项

SO_RCVBUFSO_SNDBUF分别表示TCP接收缓冲区 和 发送缓冲区的大小。不过,当调用setsockopt设置TCP的接收缓冲区大小时,系统会将其值加倍(或者其他变大方法),总之一般都会比我们传入的值大,并且不小于最小值。

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

11.3 SO_RCVlOWATSO_SNDLOWAT选项

在这里插入图片描述

11.4 SO_LINGER选项

在这里插入图片描述

#include<sys/socket.h>
struct linger
{
	int l_onoff;		值非0表示开启,值为0表示关闭 该选项
	int l_linger;		滞留时间
};

在这里插入图片描述

12、网络信息 API

12.1 什么是 网络信息API?

通过一个例子来讲。我们用用主机名访问一台机器,从而避免直接使用其IP地址。同样,我们用服务名称代替端口号。比如下面两条命令完全等价:

telnet 127.0.0.1 80
telnet localhost www

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

下面列举几个较为重要的网络信息API。

12.2 gethostbynamegethostbyaddr

在这里插入图片描述

#include<netdb.h>
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
注:这两个版本的函数是不可重入的

在这里插入图片描述

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

12.3 getservbunamegetservbyport

在这里插入图片描述

#include<netdb.h>
struct servent* getservbuname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
注:这两个版本的函数是不可重入的

在这里插入图片描述

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

12.4 getaddrinfo

getaddrinfo函数既能通过主机名获得IP地址(内部调用gethostbyname函数),也能通过服务名获得端口号(内部调用getservbyname函数)。她是否可重入取决于其内部调用的gethostbyname getservbyname是否是他们的可重入版本,该函数定义如下:

#include<netnetdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
  • hostname参数可以接收主机名,也可以接收字符串表示的IP地址。
  • service参数可以接收服务名,也可以接收字符串表示的十进制端口号。
  • hints参数是应用程序给getaddrinfo的一个提示,用来对其行为进行更精准的控制。hints可以被设置为NULL,表示允许函数返回任何可用结果。
  • result参数指向一个链表,该链表存储了该函数的返回结果。链表的每一个元素都是一个addrinfo结构体,其定义如下:
struct addrinfo
{
	int ai_flags;				其值可取下表中标志的按位异或
	int ai_family;				地址族
	int ai_socktype;			服务类型,SOCK_STREAM 或 SOCK_DGRAM
	int ai_protocol;			具体的网络协议,与socket系统调用中第三个参数相同,通常被置为0
	socklen_t ai_addrlen;		socket地址 ai_addr 的长度
	char* ai_canonname;			主机的别名
	struct sockaddr* ai_addr;	指针,指向存储了socket地址的结构体
	struct addrinfo* ai_next;	指针,指向链表中下一个 sockinfo 结构体
};

在这里插入图片描述
当想要使用hints参数时,可以设置其aiflagsai_familyai_socktypeai_protocol四个字段,其他字段必须被设置为NULL。

下面的代码利用hints参数获取主机ernest_laptop上的daytime流服务信息:
在这里插入图片描述
在这里插入图片描述

12.5 getnameinfo

getnameinfo函数可以通过socket地址获得以字符串表示主机名服务名(内部分别使用gethostbyaddrgethostbyport函数)。她是否可重入取决于其内部调用的gethostbyaddrgtehostbyport是否是他们的可重入版本。该函数定义如下:

#include<netdb.h>
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);

共7个参数:

  • 前两个参数分别是我们想要获取其信息的socket地址及其长度
  • 第3、4、5、6个参数用于存储函数返回的结果:getnameinfo将其返回的主机名存储在host参数指向的缓存中,将服务名存储在serv指向的缓存中,hostlenservlen参数分别指定这两块缓存的长度。
  • 第7个参数:flags用于控制getnameinfo的行为,他可以接收下表中的选项:在这里插入图片描述

getaddrinfo(上一小节的函数)和 getnameinfo(本小节的函数)的返回值

在这里插入图片描述

举报

相关推荐

0 条评论