Linux网络编程基础API
Posted _Karry
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
主机字节序和网络字节序
大端字节序是指一个整数的高位字节存储在内存的低位地址,低位字节存储在内存的高位地址。大端字节序也成为网络字节序。
小端字节序是指整数的高位字节序存储在内存的高位地址,低位字节序存储在内存的低位地址。现代PC大多采用小端字节序,因此小段字节序又称为主机字节序。
Linux提供了4个函数来完成主机字节序和网络字节序的转换:
#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",即将长整型的主机字节序转换为网络字节序。
这4个函数中,长整型函数常用来转换IP地址,短整型函数常用来转换端口地址。
通用socket地址
socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:
#include <bits/socket.h>
struct sockaddr{
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
}
sa_family_t是地址族类型,地址族类型通常和协议族类型对应。
常见的协议族(protocol family,也称domain)和对应的地址族如表所示:
sa_data成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度,如表5-2所示:
由表5-2所示,14字节的sa_data根本无法完全容纳多数协议族的地址值。因此Linux定义了下面这个新的通用socket地址结构体:
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[128-sizeof(__ss_align)];
}
这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)
专用socket地址
上面两个通用socket不太好用,所以Linux为各个协议族提供了专门的socket地址结构体。
UNIX本地域协议族使用如下专用socket地址结构体:
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family; /* 地址族:AF_UNIX */
char sun_path[108]; /* 文件路径名 */
};
TCP/IP协议族有socketaddr_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; /*IPv4地址结构体,见下面*/
u_int32_t sin6_scope_id; /*socpe ID,尚处于实验阶段*/
};
struct in6_addr
{
unsigned char sa_addr[16]; /*IPv6地址,要用网络字节序表示*/
};
所有专用socket地址类型的变量在实际使用时都需要转换为通用socket地址类型sockaddr(强制转换即可),因为所有的socket编程接口使用的地址参数类型都是sockaddr。
IP地址转换函数
通常人们用点分十进制来表示IPv4地址,用十六进制字符串表示IPv6地址。但编程中我们需要转换为整数(二进制数)才能使用。
而记录日志时,我们需要把整数表示的IP地址转换为点分十进制
下面三个函数可以用于点分十进制表示的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函数将点分十进制地址转换为网络字节序整数表示的地址,失败返回INADDR_NONE。
inet_aton函数和inet_addr函数功能相同,但将转换结果存储于参数inp指向的地址结构中。成功返回1,失败返回0。
inet_ntoa函数将网络字节序表示的地址转换为点分十进制地址。
下面这对更新的函数也能完成上面三个函数的功能,同时也适用于IPv4和IPv6
#include <arpa/inet>
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地址src转换为网络字节序整数表示的IP地址,并存储在dst指向的内存中;af参数指定地址族,可以是AF_INET或者AF_INET6。成功返回1,失败返回0,并设置errno
inet_ntop函数进行相反的转换,前三个参数具有相同的含义,最后一个cnt指定目标存储单元的大小,下面两个宏用于指定大小
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
inet_ntop成功返回目标存储单元的地址,失败返回NULL并设置errno。
创建socket(socket函数)
在Linux里,socket是一个可读、可写、可控制、可关闭的文件描述符。下面创建一个socket:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain参数告诉系统使用哪个底层协议族。对TCP/IP协议,参数设置为AF_INET(IPv4)或AF_INET6(IPv6);对于UNIX本地域协议族,参数设置为AF_UNIX
type参数指定服务类型。对TCP/IP而言,取SOCKET_STREAM;对UDP协议,取SOCKET_DGRAM。
protocol一般设置为0,表示使用默认协议。
socket系统调用成功返回一个socket文件描述符,失败返回-1并设置errno。
命名socket(bind函数)
创建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地址的长度,即sizeof(my_addr)。
bind成功返回0,失败返回-1并设置errno
例子:
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666
监听socket(listen函数)
socket被命名后,还不能马上接受客户端的连接,需要使用如下系统调用创建一个监听队列以存放待处理的客户连接:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd参数指定被监听的socket
backlog指定内核监听队列的最大长度,监听队列长度如果超过backlog,服务器将不受理新的客户连接,客户端也收到ECONNREFUSED的错误。
listen成功返回0,失败返回-1并设置errno
接受连接(accept函数)
accpet函数从listen监听队列中接受一个连接:
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf: 文件描述符
addr: 传出参数,返回连接客户端地址信息,含IP地址和端口号
addrlen: 传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
addr是一个传出参数,accept()返回时传出客户端的地址和端口号。
addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址
使用举例:
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
......
close(connfd);
}
整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len
是传入传出参数,每次调用accept()
之前应该重新赋初值。accept()
的参数listenfd
是先前的监听文件描述符,而accept()
的返回值是另外一个文件描述符connfd
,之后与客户端之间就通过这个connfd
通讯,最后关闭connfd
断开连接,而不关闭listenfd
,再次回到循环开头listenfd
仍然用作accept
的参数。accept()
成功返回一个文件描述符,出错返回-1
发起连接(connect函数)
服务器通过listen调用来被动接受连接;客户端通过connect函数调用来主动与服务器建立连接:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
sockfd: 文件描述符
serv_addr: 传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen: 传入参数,传入sizeof(addr)大小
返回值: 成功返回0,失败返回-1,设置errno
客户端需要调用connect()
连接服务器,connect
和bind
的参数形式一致,区别在于bind
的参数是自己的地址,而connect
的参数是对方的地址。connect()
成功返回0,出错返回-1。
关闭连接(close函数)
关闭一个连接实际上就是关闭该连接对应的socket
#include <unistd.h>
int close(int fd);
fd是待关闭的socket。
不过close系统调用并非立即关闭一个连接,而是将fd的引用计数-1,当fd的计数为0时,才真正关闭连接。
在多进程程序中,一次fork系统调用默认使父进程中打开的socket的引用计数+1,因此我们必须在父进程和子进程中都对该socket执行close才能将连接关闭。
如果要立即终止连接,可以使用shutdown系统调用
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
sockfd是待关闭的socket
howto参数决定shutdown行为,可取值如下表
数据读写
TCP数据读写
对文件的读写read和write同样适用于socket。但是socket提供了几个专门用于socket数据读写的系统调用。用于TCP流数据读写的系统调用是:
#include <sys/types.h>
#include <sys/socket.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 读取sockfd上的数据,buf和len分别指读缓冲区的位置和大小,flags常设为0。
recv成功时返回实际读取到的数据长度,出错返回-1并设置errno。
send往sockfd上写入数据,buf和len分别指定写缓冲区的位置和大小。
send成功时返回实际写入的数据的长度,失败返回-1并设置errno。
flags参数为数据收发提供了额外的控制,它的取值如表:(一般取0)
例子:
发送带外数据(服务器端)
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
int main()
{
// 1.创建监听套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1){
perror("socket");
return -1;
}
// 2.将服务器端的IP地址和端口号绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999); //从本地字节序转换为网络字节序
// INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
// 这个宏可以代表任意一个IP地址
// 这个宏一般用于本地的绑定操作
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1){
perror("bind");
return -1;
}
// 3.设置监听
ret = listen(fd, 100);
if(ret == -1){
perror("listen");
return -1;
}
// 4.阻塞等待并接受客户端的连接
struct sockaddr_in caddr;
int caddrlen = sizeof(caddr);
int cfd = accept(fd, (struct sockaddr*)&caddr, &caddrlen);
if(cfd == -1)
{
perror("accept");
return -1;
}
// 输出客户端的IP和端口号
char ip[32];
printf("client IP: %s, port: %d\\n",
inet_ntop(AF_INET, &caddr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(caddr.sin_port));
// 5.和客户端通信
while(1)
{
// 接收数据
char buf[1024];
int len = recv(cfd, buf, sizeof(buf), 0);
if(len > 0){
printf("client says: %s\\n", buf);
// 发送数据
send(cfd, buf, len, 0);
}
else if(len == 0){
printf("client break connection\\n");
break;
}
else{
perror("read");
break;
}
}
close(fd);
close(cfd);
return 0;
}
接收带外数据:
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
int main()
{
// 1.创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1){
perror("socket");
return -1;
}
// 2.连接服务器
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999); // 本地字节序转换为网络字节序
inet_pton(AF_INET, "192.168.143.141", &saddr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1){
perror("connect");
return -1;
}
// 和服务器通信
int number = 0;
while(1)
{
// 发送数据
char buf[1024];
sprintf(buf, "Hello, world, %d...\\n", number++);
send(fd, buf, (strlen(buf)+1), 0);
// 接收数据
memset(buf, 0, sizeof(buf));
int len = recv(fd, buf, sizeof(buf), 0);
if(len > 0)
{
printf("server says: %s\\n", buf);
}
else if(len == 0)
{
printf("server break connection\\n");
break;
}
else
{
perror("recv");
break;
}
sleep(2);
}
close(fd);
return 0;
}
以上是关于Linux网络编程基础API的主要内容,如果未能解决你的问题,请参考以下文章