Linux网络编程中的API
- socket地址API:socket的含义是一个IP地址和端口对 (ip,port)。它唯一地表示了使用TCP通信的一端。
- socket基础API:socket的主要API都定义在sys/socket.h头文件中, 包括创建socket、命名socket、监听socket、接受连接、发起连接、读写 数据、获取地址信息、检测带外标记,以及读取和设置socket选项。
- 网络信息API:Linux提供了一套网络信息API,以实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。这些API都定义在netdb.h头文件中。
socket地址API
主机字节序:字节序分为大端字节序和小端字节序。大端字节序是指一个整数的高位字节(23~31 bit)存储在内存的低地址处,低位字节(0~7 bit)存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。
现代主机大多采用小端字节序,因此小端字节序又被称为主机字节序。当格式化数据在两台使用不同字节的主机之间传递时,需要其中一方对数据进行转换。大端字节序也称为网络字节序,它给所 有接收数据的主机提供了一个正确解释收到的格式化数据的保证。Linux提供了如下四个函数来完成主机字节序和网络字节序之间的转换:
#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”;htons表示“host to network short”;ntohl表示“network to host long”;ntohs表示“network to host short”;长整型函数通常用来转换IP地址,短整型函数用来转换端口号
通用socket地址:socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下
#include<bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
}
专用socket地址:这里介绍TCP/IP协议的专用地址结构,sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于IPv4和IPv6。
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地址,要用网络字节序表示*/
};
struct sockaddr_in6
{
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,尚处于实验阶段*/
};
struct in6_addr
{
unsigned char sa_addr[16];/*IPv6地址,要用网络字节序表示*/
};
所有专用socket地址类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr。
IP地址转换函数:
编程中我们需要先把IP地址转化为整数(二进制数)方能使用,但是记录日志时则相反,要把整数表示的IP地址转化为可读的字符串,下面3个函数可用于点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:
#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地址。它失败时返回INADDR_NONE;
- inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储于参数inp指向的地址结构中。它成功时返回1,失败则返回0。
- inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十 进制字符串表示的IPv4地址
下面这对更新的函数也能完成和前面3个函数同样的功能,并且它们同时适用于IPv4地址和IPv6地址
#include<arpa/inet.h>
int inet_pton(int af,const char* src,void* dst);
const char* inet_ntop(int af,const void* src,char* dst,socklen_t cnt);
- inet_pton函数将用字符串表示的IP地址转换成用网络字节序整数表示的IP地址,并把转换结果存储于dst指向的内存中。其中,af 参数指定地址族,可以是AF_INET或者AF_INET6。inet_pton成功时返回1,失败则返回0并设置errno;
- inet_ntop函数进行相反的转换,前三个参数的含义与inet_pton的参 数相同,最后一个参数cnt指定目标存储单元的大小。inet_ntop成功时返回目标存储单元的地址,失败则返回NULL并设 置errno。
socket基础API
创建socket
Linux系统中万物皆文件,socket就是可读,可写,可控制,可关闭的一个文件描述符。创建一个socket:
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
- domain参数告诉系统使用哪个底层协议族。对TCP/IP协议族而言, 该参数应该设置为PF_INET(用于IPv4)或 PF_INET6(用于IPv6);
- type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务/TCP协议)和SOCK_UGRAM(数据报/UDP协议)服务;
- protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下,我们都应该把它设置为0,表示使用默认协议。
socket系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno。
命名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并设置errno。其中两种常见的 errno是EACCES和EADDRINUSE,它们的含义分别是:
- EACCES:被绑定的地址是受保护的地址,仅超级用户能够访问
- EADDRINUSE:被绑定的地址正在使用中
监听socket
socket被命名之后,还不能马上接受客户连接,我们需要使用如下 系统调用来创建一个监听队列以存放待处理的客户连接
#include<sys/socket.h>
int listen(int sockfd,int backlog);
- sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息
listen成功时返回0,失败则返回-1并设置errno
实操
编写一个服务器程序,研究 backlog参数对listen系统调用的实际影响
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include<string.h>
#include<stdbool.h>
#include<libgen.h>
static bool stop=false;
/*SIGTERM信号的处理函数,触发时结束主程序中的循环*/
static void handle_term(int sig)
{
stop=true;
}
int main(int argc,char* argv[])
{
signal(SIGTERM,handle_term);//处理信号
if(argc <= 3)
{
printf("usage:%s ip_address port_number backlog\n",basename(argv[0]));
return 1;
}
const char* ip=argv[1];//ip地址
int port=atoi(argv[2]);//端口号
int backlog=atoi(argv[3]);//字符串转整型,监听队列最大长度
int sock=socket(PF_INET,SOCK_STREAM,0);//创建socket指定地址族和协议,返回文件描述符
assert(sock>0);//查看返回的文件描述符是否为0,若是0,表示socket创建失败,直接终止程序
/*创建一个IPv4 socket地址*/
struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family=AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr);//将字符串表示的ip地址转换成网络字节序,转换结果储存在address.sin_addr中
address.sin_port=htons(port);//端口号的主机字节转换成网络字节序
int ret=bind(sock,(struct sockaddr*)&address,sizeof(address));//命名socket
printf("value of ret:%d\n",ret);
assert(ret != -1);
ret=listen(sock,backlog);//创建一个监听队列以存放待处理的客户链接
assert(ret != -1);
/*循环等待链接,直到有SIGTERM信号将他中断*/
while(!stop)
{
sleep(1);
}
/*关闭socket*/
close(sock);
return 0;
}
该程序(名为testlisten)接收3个参数:IP地址、端口号和 backlog值。我们在Aliyun上运行该服务器程序,并在Ubuntu上多次执行telnet命令来连接该服务器程序。同时,每使用telnet命令建立一个连接,就执行一次netstat命令来查看服务器上连接的状态,具体操作过程如下:
./testlisten 172.18.190.194 35555 5 //在服务器端执行该程序
telnet 8.130.123.201 35555 //在客户端多次执行该命令
netstat -nt | grep 35555 //每次执行完第二行命令,紧接着执行这条命令
netstat显示了这一时刻listen 监听队列的内容
这个结果是在客户端上执行了六次telnet操作,可以看到在监听队列中,处于ESTABLISHED状态的连接有6个 (backlog值加1,完整连接多有backlog+1个);
当继续执行telnet命令时,执行结果显示已经无法连接了,队列已经满了
阿里云服务器绑定端口时出现的问题
刚开始在执行./testlisten 8.130.123.201 xxxxx 5 时,失败!!!查阅了好多博客,发现网址有问题,阿里云服务器绑定端口号的不能用公网IP,要用私网IP!修改之后,还失败!!!然后继续查啊查,发现我端口选择的是12345,一直提示错误,端口绑定失败,因为不是所有端口都可以用的,查看一下服务器端可用的端口是 32768 ~60999,然后换一下端口号,换成35555;改了之后发现还是不能telnet,又是阿里云安全组问题!!!在阿里云服务器控制台出配置一下安全组规则,允许外部telnet它。注意,服务器端IP+端口时要用阿里云的私网IP,当时外面的客户端要是想连接阿里云服务器,需要telnet阿里云的公网IP哦!!!唉,搞了一下午,只能说,阿里云,“你牛逼!”。
接受连接
下面的系统调用从listen监听队列中接受一个连接:
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr*addr,socklen_t*addrlen);
sockfd参数是执行过listen系统调用的监听socket [1] 。addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。accept成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信,accept失败时返回-1并设置errno。
accept只是从监听队列中取出连接,而不论连接处于何种状态(如上面的ESTABLISHED状态和CLOSE_WAIT状态),更不关心任何网络状况的变化。
发起连接
服务器通过listen调用来被动接受连接,客户端需要通过如下系统调用来主动与服务器建立连接:
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd,const struct sockaddr*serv_addr,socklen_t addrlen);
sockfd参数由socket系统调用返回一个socket。serv_addr参数是服务 器监听的socket地址,addrlen参数则指定这个地址的长度。connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno。两种常见的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调用才能将连接关闭。