Linux高性能服务器编程学习笔记——Linux网络编程基础API

Linux网络编程基础API

  • Linux网络编程基础API
    • socket地址API
      • 主机字节序和网络字节序
      • 通用socket地址
      • 专用socket地址
      • IP地址转换函数
    • 创建socket
    • 命名socket
    • 监听socket
    • 接受连接
    • 发起连接
    • 关闭连接
    • 数据读写
      • TCP数据读写
      • UDP数据读写
      • 通用数据读写函数
    • 带外标记
    • 地址信息函数
    • socket选项
      • SO_REUSEADDR选项
      • SO_RECVBUF和SO_SNDBUF选项
      • SO_RCVLOWAT和SO_SNDLOWAT选项
      • SO_LINGER选项
    • 网络信息API
      • gethostbyname和gethostbyaddr
      • getservbyname和getservbyport
      • getaddrinfo
      • getnameinfo

Linux网络编程基础API

应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口,区分不同应用程序进程间的网络通信和连接

socket地址API

主机字节序和网络字节序

现代CPU的累加器一次都能装载4字节,即一个整数。这4字节在内存中排列的顺序将影响它被累加器装载成的整数值。这就是字节序问题。字节序分为大端字节序小端字节序。大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。小端字节序则相反。

现代PC多采用小端字节序,因此小端字节序又被称为主机字节序。在两台使用不同字节序的主机之间直接传递数据时,接收端必然错误地解释之。解决办法就是:发送端总是要把发送的数据转化成大端字节序数据后再发送,而接收端知道接受的数据是大端字节序,可以根据自身的字节序决定是否对接收到的数据进行转换。因此大端字节序也称为网络字节序

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

#include 
unsigned long int htonl( unsigned long int hostlong );
unsigned short int htons( unsigned short int hostshort );
unsigned long int ntonl( unsigned long int netlong );
unsigned short int ntohs( unsigned short int netshort );

通用socket地址

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

#include 
struct sockaddr{
	sa_family sa_family;
	char sa_data[14];
}

sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称domain)和对应的地址族如下表
Linux高性能服务器编程学习笔记——Linux网络编程基础API_第1张图片
sa_data成员用于存放socket地址值。

专用socket地址

通用socket地址结构体很不好用,比如设置与获取IP地址和端口号就需要执行烦琐的位操作。所以Linux为各个协议族提供了专门的socket地址结构体。如IPv4:

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

所有专用socket地址类型的变量在实际使用时都需要转化为通用socket地址类型,因为socket编程接口使用的地址参数类型都是sockaddr。

IP地址转换函数

下面3个函数用于用点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:

#include 
int_addr_t inet_addr( const char* strptr );  //点分十进制字符串转化成网络字节序整数。
int inet_aton( const char* cp, struct in_addr* inp );  //同上,将结果存储于参数inp指向的地址结构中。
int inet_pton( int af, const char* src, void* dst );  //同上,af指定地址族,结果存到dst中。
char* inet_ntoa( struct in_addr in ); //不同以上,函数内部用静态变量存储转化结果,不可重入。

创建socket

下面的socket系统调用可创建一个socket:

#include 
#include 
int socket( int domain, int type, int protocol );

domain参数告诉系统使用哪个底层协议族。PF_INET用于IPv4。
type参数指定服务类型。对于TCP/IP协议族而言,取SOCK_STREAM表示传输层使用TCP协议。
protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议,一般置0。

命名socket

创建socket时,我们给它指定了地址族,但是没有指定具体socket地址。将一个socket与socket地址绑定称为给socket命名。在服务器程序中,我们通常要命名socket,因为只有命名后客户端才知道如何连接它。客户端通常不需要命名。命名socket的系统调用是bind,定义如下:

#include 
#include 
int bind( int sockfd, const struct sockaddr* my_addr, socklen_t addrlen );

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

监听socket

socket被命名后,还不能马上接受客户连接,需要创建一个监听队列以存放待处理的客户连接:

#include 
int listen( int sockfd, int backlog );

sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度。

接受连接

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

#include 
#include 
int accept( int sockfd, struct sockaddr *addr, socklen_t *addrlen );

sockfd参数是执行过listen系统调用的监听socket。addr参数用来获取被接受连接的远端socket地址,该地址长度有addrlen参数指出。返回一个新的连接的socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。

发起连接

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

#include 
#include 
int connect( int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen );

sockfd参数由socket系统调用返回一个socket。serv_addr参数是服务器监听的socket地址,addrlen参数则指定这个地址的长度。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。

关闭连接

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

#include 
int close( int fd );

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

如果无论如何都要立即终止连接,可以使用如下shutdown系统调用:

#include 
int shutdown( int sockfd, int howto );

sockfd参数是待关闭的socket。

数据读写

TCP数据读写

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

#include 
#include 
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读取sockfd上的数据,buf和len参数分别指定都缓冲区的位置和大小,flags参数通常设为0。
send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。

UDP数据读写

socket编程接口中用于UDP数据报读写的系统调用是:

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

sendto往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。dest参数指定接收端的socket地址,addrlen参数则指定该地址的长度。

通用数据读写函数

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

#include 
ssize_t recvmesg( int sockfd, struct msghdr* msg, int flags );
ssize_t sendmsg( int sockfd, struct msghdr* msg, int flags );

带外标记

在Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是:I/O复用产生的异常事件和SIGURG信号。可以通过系统调用找到带外数据在数据流中的具体位置:

#include 
int sockatmark( int sockfd );

sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。

地址信息函数

有些情况,想知道一个连接socket的本段socket地址,以及远端的socket地址。可以使用如下函数:

#include 
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参数指向的变量中。
getpeername获取sockfd对应的远端socket地址,其参数同上。

socket选项

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

#include 
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参数则指定选项的名字。option_value和option_len参数分别是被操作选项的值和长度。

SO_REUSEADDR选项

服务器程序可以通过设置socket选项SO_REUSEADDR来强制使用被处于TIME_WAIT状态的连接占用的socket地址。
经过setsockopt的设置之后,及时sock处于TIME_WAIT状态,与之绑定的socket地址也可以立即被重用。

SO_RECVBUF和SO_SNDBUF选项

SO_RECVBUF和SO_SNDBUF选项分别表示TCP接收缓冲区和发送缓冲区的大小。

SO_RCVLOWAT和SO_SNDLOWAT选项

SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。它们一般被I/O复用系统调用用来判断socket是否可读或可写。当TCP接收缓冲区中可读数据的总数大于其低水位标记时,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据;当TCP发送缓冲区中的空闲空间大于其低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socket上写入数据。

SO_LINGER选项

SO_LINGER选项用于控制close系统调用在关闭TCP连接时的行为。

网络信息API

socket地址的两个要素,即IP地址和端口号,都是用数值表示的。不便于记忆,也不便于扩展。在前面在章节中,我们用主机名来访问一台机器,避免直接使用其IP地址。同样,我们用服务名称来代替端口号。

gethostbyname和gethostbyaddr

gethostbyname函数根据主机名称获取主机完整信息,gethostbyaddr函数根据IP地址获取主机的完整信息。

#include 
struct hosten* gethostbyname( const char* name );
strcut hosten* 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 
struct hostent{
	char* h_name;			// 主机名
	char** h_aliases;		// 主机别名列表,可能有多个
	int h_addrtype;			// 地址类型
	int h_length;			// 地址长度
	char** h_addr_list;     // 网络字节序列出的主机IP地址列表
};

getservbyname和getservbyport

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

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

以上四个函数均是不可重入的,即线程非安全的。

getaddrinfo

getaddrinfo函数既能通过主机名获得IP地址,也能通过服务名获得端口号。该函数的定义如下:

#include 
int getaddrinfo( const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result );

hostname参数可以接受主机名,也可以接受字符串表示的IP地址。同样,service参数可以接受服务名,也可以接受字符串表示的十进制端口号。hints参数使应用程序给geraddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制。result参数指向一个链表,该链表用于存储geraddrinfo反馈的结果。
getaddrinfo反馈的每一条结果都是addrinfo结构体类型的对象,结构体addrinfo的定义如下:

struct addrinfo{
	int ai_flags;				// 
	int ai_family;				// 地址族
	int ai_socktype;			// 服务类型
	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结构的对象
}

getnameinfo

getnameinfo函数能通过socket地址同时获得以字符串表示的主机名和服务名。

#include 
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参数控制genameinfo的行为。

你可能感兴趣的:(服务器开发,网络,linux,socket,c++,tcpip)