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

本章节主要从三个方面讨论Linux网络API:

  • socket地址API。socket最开始的含义是一个IP地址和端口对(ip,port)。它唯一地表示了使用TCP通信的一端。本书称其为socket地址。
  • socket基础API。socket的主要API都定义在sys/socket.h头文件中,包括创建socket、命名socket、监听socket、接受连接、发起连接、读写数据、获取地址信息、检测带外标记,以及读取和设置socket选项。
  • 网络信息API。Linux提供了一套网络信息API,以实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。这些API都定义在netdb.h头文件中,我们将讨论其中几个主要的函数。

socket地址API

主机字节序和网络字节序

现在CPU一次都能装载4字节(至少),那么这四个字节排列顺序将影响他被累加器装载成的整数值。

字节序分为大端字节序和小端字节序,大端序指的是一个整数的高位字节在内存的低地址处,低位字节在高地址处,小端序则相反。现代PC都用小端序,所以小端序又称主机字节序,与此同时发送端总是将发送端数据转化为大端字节序再发送,因此大端字节序又叫网络字节序。

Linux提供了四个函数完成主机字节序和网络字节序的转换:

1
2
3
4
5
#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 );

它们的含义很明确,比如htonl表示“host to network long”,即将长整型(32 bit)的主机字节序数据转化为网络字节序数据。这4个函数中,长整型函数通常用来转换IP地址,短整型函数用来转换端口号(当然不限于此。任何格式化的数据通过网络传输时,都应该使用这些函数来转换字节序)。

通用socket地址

1
2
3
4
5
6
#include <bits/ socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data [ 14];
}

专用socket地址

这里只看IPV4的结构体

1
2
3
4
5
6
7
8
9
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地址类型的变量在实际使用的时候都需要转化为通用sockaddr(强制转换即可)。

IP地址转换函数

通常用可读性好的字符串来表示IP地址,比如用点分十进制来表示IPV4地址,编程中我们需要先把他们转化为整数(二进制)

1
2
3
4
#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_addr函数将用点分十进制字符串表示的IPV4地址,转化为网络字节序整数表示的IPv4地址

inet_aton完成和上面函数一样的功能,但是将值保存在第二个参数代表的结构体中

inet_ntoa是将整数表示转化为点分十进制的IPv4地址

创建socket

UNIX/Linux的一个哲学是万物皆文件,socket也不例外,他就是可读可写可控制可关闭的文件描述符,下面的系统调用可创建一个socket:

1
2
3
4
#include<sys/types.h>
#include<sys/socket.h>

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

domain参数告诉系统使用哪个底层协议族,TCP/IP应该设置为PF_INET

type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务)和SOCK_UGRAM(数据报)服务。对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议,取SOCK_DGRAM表示传输层使用UDP协议。

protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议,通常这个值都是唯一的,几乎在所有情况下,我们都应该把这个值设为0,表示默认协议

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

命名socket

我们创建socket的时候指定了地址族,但是没有指定使用该地址族的哪个具体地址,只有将一个socket和socket地址绑定才能命名。在服务器程序中,我们需要给socket命名,客户端才知道该如何连接他,客户端不需要命名,而是采用匿名方式,即使用操作系统自动分配的socket地址

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

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

监听socket

被命名后还不能马上接收客户连接,我们需要使用如下系统调用:

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

backlog提示内核监听队列的最大长度,一般设置为5

接受连接

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

sockfd参数是执行过listen系统调用的监听socket。addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。accept成功时返回—个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。accept失败时返回-1并设置errno。

经过实验,accept只是从监听队列中取出连接,而不论连接处与何种状态,也不关心任何网络变化

发起连接

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

fd参数是待关闭的socket,但是他不是总是立即关闭一个连接,而是将fd的引用计数-1,当引用计数为0时才关闭。

多进程程序中,一次fork系统调用都将使父进程打开的socket的引用计数+1.


后面的具体方法不再赘述,如果有用到了再提