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地址之间的转换,以及服务名称和端口号之间的转换。
一、 socket 地址API
1、主机字节序和网络字节序
现代CPU的累加器一次都能装载(至少) 4字节(这里考虑32位机,下同),即一个整数。那么这4字节在内存中排列的顺序将影响它被累加器装载成的整数的值。这就是字节序问题。字节序分为大端字节序(big endian)和小端字节序(little endian)。大端字节序是指一个整数的高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0~ 7bit)存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。
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_UNIX | AF_UNIX | UNIX本地域协议族 |
PF_INET | AF_INET | TCP/IPV4协议族 |
PF_INET6 | AF_INET6 | TCP/IPV6协议族 |
二、创建socket
Linux上一切皆文件。socket也不例外,他就是可读、可写、可控制、可关闭的文件描述符。下面的socket系统调用可创建一个socket。
#include <sys.types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数含义:
domain参数告诉系统使用哪个底层协议族。对TCP/IP协议族而言,该参数应该设置为PF_ INET (Protocol Family of Internet,用于IPv4)或PF_ INET6 (用于IPv6);对于UNIX本地域协议族而言,该参数应该设置为PF_ UNIX。
type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务)和SOCK_UGRAM (数据报)服务。对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议,取SOCK_DGRAM表示传输层使用UDP协议。
protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下,我们都应该把它设置为0,表示使用默认协议。
socket系统调用成功时返回-个socket文件描述符,失败则返回-1并设置errno。
三、命名socket
创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体socket地址。将一个socket与socket地址绑定称为给socket命名。在服务器程序中,我们通常要命名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);
参数含义:
bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址的长度。
bind成功时返回0,失败则返回-1并设置ermo。其中两种常见的errno是EACCES和EADDRINUSE,它们的含义分别是:
- EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口(端口号为0~1023)上时, bind 将返回EACCES错误。
- EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到-一个处于TIME_WAIT状态的socket地址。
四、监听socket
socket被命名之后,还不能马上接受客户连接,我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户端连接。
#include <sys/socket.h>
int listen(int socket, int backlog);
参数含义:sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。在内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态(SYN RCVD)和完全连接状态( ESTABLISHED)的socket的上限。但自内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/netipv4/tcp_ max_ syn_ backlog 内核参数定义。backlog 参数的典型值是5。
listen成功时返回0,失败则返回-1并设置errno。
五、接受连接
下面的系统调用从listen监听队列中接受一个链接:
#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。
六、发起连接
如果说服务器通过listen调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
参数含义:
sockfd参数由socket系统调用返回- -个socket. serv_ addr 参数是服务器监听的socket 地址,addrlen 参数则指定这个地址的长度。
connct成功时返回0。一旦成功建立连接,sockfd 就唯-地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect 失败则返回-1并设置erno.其中两种常见的errno是ECONNREFUSED和ETIMEDOUT,它们的含义如下:
- ECONNREFUSED,目 标端口不存在,连接被拒绝。
- ETIMEDOUT,连接超时。
七、关闭连接
关闭连接实际就是关闭一个连接的socket,这可以通过如下关闭普通文件描述符的系统调用来完成:
#include <unistd.h>
int close(int fd);
参数含义:
fd参数是待关闭的socket。不过,close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1,只有将fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程打开的socket的引用计数加1,因此我们必须在父进程和子进程中,对该socket进行close调用才能将连接关闭。
如果无论如何都要立即关闭连接(而不是将socket引用计数减1),可以使用如下的shutdown系统调用:
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
参数含义:
sockfd是待关闭的socket。howto参数决定了shutdown的行为。
具体流程如下图: