相关API笔记(一)
Linux网络编程基础API
1. 主机字节序和网络字节序
代码语言:javascript复制#include <netinet/in.h>
unsigned long int htonl( unsigned long int hostlong );
unsigned short int htons( unsigned long int hostlong );
unsigned long int ntohl( unsigned long int netlong );
unsigned short int ntohs( unsigned long int netlong );
htonl即”host to network long”, 即长整型(32bit)的主机字节序转换未网络字节序数据。
长整型函数 (htonl,ntohl)通常用来转换IP地址
短整型函数 (htonl,ntohl)通常用来转换端口号
2. 通用socket地址
这个比较少用
socket网络编程接口中表示socket地址的是结构体sockaddr
代码语言:javascript复制#include <bits/socket.h>
struct sockaddr{
sa_family_t sa_family; //地址族类型变量
char sa_data[14]; //存放socket地址值
}
地址族类型通常与协议族类型对应。
协议族 | 地址族 | 描述 |
---|---|---|
PF_UNIX | AF_UNIX | UNIX本地域协议族 |
PF_INET | AF_INET | TCP/IPv4协议族 |
PF_INET6 | AF_INET6 | TCP/Ipv6协议族 |
宏PF_*和AF_*都定在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用
sa_data存放socket地址值,不同的协议族的地址具有不同的长度
协议族 | 地址值含义和长度 |
---|---|
PF_UNIX | 文件的路径名,长度可达到108字节 |
PF_INET | 16bit端口号和32bit IPv4地址,共6字节 |
PF_INET6 | 16bit端口号,32bit流标识,128bitIpv6地址,32bit范围ID,共26字节 |
14字节的sa_data不能容纳协议族的地址值。定义如下新的通用socket地址结构体:
代码语言:javascript复制#include <bits/socket.h>
struct sockaddr_storage {
sa_family_t sa_family; //地址族类型变量
unsigned long int __ss_align; //用来内存对齐
char __ss_padding[128-sizeof(__ss_align)];
}
3.专用socket地址
这个比较常用
UNIX本地域协议族使用如下专用socket地址结构体:
代码语言:javascript复制#include <sys/un.h>
struct sockaddr_un {
sa_family_t sin_family; //地址族: AF_UNIX
char sun_path[108]; //文件路径名
}
TCP/IP协议族有sockaddr_in和socketaddr_in6,分别用于Ipv4和Ipv6
代码语言:javascript复制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地址,要用网络字节序表示
}
代码语言:javascript复制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_scopt_16; //scope ID,尚处于实验阶段
};
struct in6_addr {
unsigned char sa_addr[16]; //IPv6地址,要用网络字节序表示
}
实际使用时(包括sockaddr_storage)都需要将其转化为通用的socket地址类型sockaddr(强制转换即可),所以的socket编程接口使用的类型都是sockaddr。
4. IP地址转换函数
我们习惯上都是使用点分十进制(Dotted Decimal Notation)表示IP地址,实际上使用的得把它们转换为整数(二进制数)
代码语言:javascript复制#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。但是该函数内部使用一个静态变量来保存结果的,函数的返回值是这个静态内存,多次调用会覆盖到之前调用产生的结果。
下面的更好用,适应于IPv6
代码语言:javascript复制#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, socketlen_t cnt);
inet_pton(IP地址src->网络字节序IP,成功返回1,失败返回0并设置errno)
参数:
af: 地址族,AF_INET或者AF_INET6
src: 点分十进制表示的IPv4地址或者十六进制表示的**IPv6地址
dst: 转换的结果指向dst指向的内存
inet_ntop:(与inet_pton相反,成功返回目标存储单元的地址,失败返回NULL并设置errno)
前三个参数与上述相同
cnt: 指定目标存储单元的大小,使用如下两个宏能指定这个大小
代码语言:javascript复制#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
5. 创建socket
代码语言:javascript复制#include <sys/types.h>
#include <sys/socket.h>
//成功返回socket文件描述符,失败返回-1并设置errno
int socket(int domain, int type, int protocol);
参数:
domain: 使用哪个底层协议族,TCP/IP协议: PF_INET,PF_INET6, UNIX本地协议族: PF_UNIX
type: 服务类型,取值有SOCK_STREAM,SOCK_DGRAM,在TCP/IP中,SOCK_STREAM表示使用TCP,SOCK_DGRAM表示使用UDP。同时也可以传入上述服务类型与下面两个标志的相与的值: SOCK_NONBLOCK和SOCK_CLOEXEC。分别表示非阻塞,fork调用创建子进程后在子进程关闭该socket。
protocol: 在前两个参数构成的协议集合下再选择一个具体协议。一般情况设置为0即可。
6. 命名socket
创建了socket后,我们还需要将对应的地址与其绑定。
代码语言:javascript复制#include <sys/types.h>
#include <sys/socket.h>
//成功返回0,失败返回-1并设置errno
//errno的类型: EACCES,表示被绑定地址是受保护的
// EADDRINUSE, 被绑定地址正在使用中
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
参数:
sockfd: 要绑定的socket文件描述符
my_addr: socket地址,一般来说为sockaddr_un, sockaddr_in, sockaddr_in6的地址,传入参数时要强制转换为sockaddr*指针类型。
addrlen: 第二个参数my_addr所指向的socket地址的长度。通常使用sizeof()来获取。
7. 监听socket
socket被命名,即绑定后要使用listen函数创建监听队列存放待处理的用户连接
代码语言:javascript复制#include <sys/socket.h>
//成功返回0,失败返回-1并设置errno
int listen(int sockfd, int backlog);
参数:
sockfd: 被监听的socket文件描述符。
backlog: 内核监听队列的最大长度。
8. 接受连接
下面系统调用从listen监听队列中接受一个连接
代码语言:javascript复制#include <sys/types.h>
#include <sys/socket.h>
//成功返回一个新的socket文件描述符,用来唯一标识被接受的这个连接,服务器可以通过读写该socket来与被连接的客户端通信
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd: 执行过listen系统调用的socket文件描述符。
addr: 获取被接受连接的远端socket地址
addrlen: 第二个参数my_addr所指向的socket地址的长度,可以提前声明一个socklen_t类型变量并赋值socket地址的长度,然后传入这个变量的地址。
代码语言:javascript复制//例
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
9. 发起连接
服务器通过listen被动接受连接,那么客户端就需要通过connect来主动与服务器建立连接
代码语言:javascript复制#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: 参数二的地址长度
10. 关闭连接
代码语言:javascript复制#include <unistd.h>
int close(int fd);
参数:
fd: 要关闭的socket文件描述符
close并非立刻关闭一个连接,只是是把fd的引用计数减1,当fd的引用数完全减为0时,才算真正关闭连接。
多进程中,一个fork系统调用默认会让父进程中打开的socket文件描述符的引用数加一,因此得再父子进程中都调用close才能真正关闭一个连接。
下面系统调用可以立刻终止连接
代码语言:javascript复制#include <sys/socket.h>
//成功返回0,失败返回-1并设置errno
int shutdown(int sockfd, int howto);
参数:
sockfd: 要立刻关闭的socket文件描述符
howto: 决定了shutdown的行为,取值如下
可选值 | 含义 |
---|---|
SHUT_RD | 关闭sockfd读的这一半。应用程序不能再针对socket文件描述符执行读操作,并且该socket接收缓冲区中的数据都被丢弃 |
SHUT_WR | 关闭sockfd写的这一半。sockfd的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该socket文件描述符执行写操作。这种情况下,连接处于半关闭状态。 |
SHUT_RDWR | 同时关闭sockfd上的读和写 |
11. 数据读写
1. TCP数据读写
对文件的读写read和write同样适用于socket。下列系统调用专门用于对socket数据读写
代码语言:javascript复制#include <sys/types.h>
#include <sys/socket.h>
//函数返回实际读(写)的数据长度,返回0的话意味对方已关闭连接,出错时返回-1并设置errno
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, cons void *buf, size_t len, int flags);
参数:
sockfd: 要读或写的socker文件描述符
buf: 读写缓冲区的位置
len: 读写缓冲区的大小
flags: 控制参数,如下
选项名 | 含义 | send | revc |
---|---|---|---|
MSG_CONFIRM | 指示数据链路层协议持续监听对方的回应,直到得到发育。它仅能用于SOCK_DGRAM和SOCK_RAW类型的socket | Y | N |
MSG_DONTROUTE | 不查看路由表,直接将数据发送给本地局域网络内的主机。这表示发送者确切地知道目标主机就在本地网络上 | Y | N |
MSG_DONTWAIT | 对socket的此次操作将是非阻塞的 | Y | Y |
MSG_MORE | 告诉内核应用程序还有更多数据要发送,内核将超时等待新数据写入TCP发送缓冲区后一并发送。这样可以防止TCP发送过多小的报文段,从而提高传输效率 | Y | N |
MSG_WAITALL | 读操作仅在读取到指定数量的字节后才返回 | N | Y |
MSG_PEEK | 窥探读缓存中的数据,此次读操作不会导致这些数据被清楚 | N | Y |
MSG_OOB | 发送或接收紧急数据 | Y | Y |
MSG_NOSIGNAL | 往读端关闭的管道或者socket连接中写数据时不引发SIGPIPE信号 | Y | N |
2. UDP数据读写
代码语言:javascript复制#include <sys/types.h>
#include <sys/socket.h>
//函数返回实际读(写)的数据长度
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd, cons void *buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
参数:
前四个同TCP读写函数一样
src_addr: UDP没有连接的概念,所以每次读数据都要获取发送端(接收端)的socket地址
addrlen: src_addr或dest_addr地址的长度
这两个函数也可用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数设置为NULL即可。
3. 通用数据读写函数
下面这组接口可以用于TCP流数据,也可用于UDP数据报
代码语言:javascript复制#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
参数:
sockfd: 被操作的目标socket文件描述符
msg: msgaddr结构体类型指针
flags: 控制参数,具体取值同上
msgaddr结构体定义如下:
代码语言:javascript复制struct msghdr{
void* msg_name; //socket地址
socklen_t msg_namelen; //socket地址的长度
struct iovec* msg_iov; //分散的内存块,见后文
int msg_iovlen; //分散内存块的数量
void* msg_control; //指向辅助数据的起使位置
socklen_t msg_controllen; //辅助数据的大小
int msg_flags; //复制函数中的flags参数,并在调用过程中更新
};``
//iovec封装了一块内存的起始位置和长度
struct iovec{
void* iov_base; //内存起始地址
size_t iov_len; //这块内存的长度
};
msghdr成员变量:
msg_name: 指向一个socket地址结构变量,指定通信对方的socket地址。对于面向连接的TCP协议他必须设置为NULL。
msg_name: socket地址的长度
msg_iovlen: iovec结构对象的个数
msg_control和msg_controllen: 用于辅助数据的传送
msg_flags: 会复制recvmsg和sendmsg的flags参数的内容。
12. 带外标记
内核通知应用程序带外数据到达的两种常见方法:I/O复用产生的异常事件,SIGURG信号。
应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置。
sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据
代码语言:javascript复制#include <sys/socket.h>
//成功返回1,失败返回0
//若成功了,我们就可以利用带MSG_OOB标志的recv调用来接收带外数据
int sockatmark(int sockfd);
13. 地址信息函数
想知道连接socket的本端socket地址,以及远端的socket地址,可以使用如下函数
代码语言:javascript复制#include <sys/socket.h>
//获取sockfd对应的本端socket地址,成功返回1,失败返回-1并设置errno
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
//获取sockfd对应的远端socket地址,成功返回1,失败返回-1并设置errno
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);
参数:
sockfd: socket文件描述符
address: 存储对应的socket地址
address_len: socket地址的长度,注意其要传入一个socklen_t的指针变量
如果实际socket的地址长度大于address所指内存区的大小,则地址会被截断。
14. socket选项
下面两个函数用来读取和设置socket文件描述符属性
代码语言:javascript复制#include <sys/socket.h>
//两个函数都是成功返回1,失败返回0并设置errno
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* restrict option_len);
参数:
sockfd: socket文件描述符
level: 要操作哪个协议的选项(属性),如IPv4,IPv6,TCP等
option_name: 指定选项的名字
option_value: 被操作的选项的值
option_len: 被操作的选项的长度
15. 网络信息API
- gethostbyname和gethostbyaddr
#include <netdb.h>
//根据主机名获取主机的完整信息
struct hostnet* gethostbyname(const char* name);
//根据IP地址获取主机的完整信息
struct hostnet* gethostbyaddr(const void* addr, size_t len, int type);
struct hostnet{
char* h_name; //主机名
char** h_aliases; //主机别名列表,可能有多个
int h_addrtype; //地址类型(地址族)
int h_length; //地址长度
char** h_addr_list; //按网络字节序 列出的主机IP地址列表
}
参数:
name: 指定目标主机的主机名(如localhost)
addr: 指定目标主机的IP地址
len: addr所指IP地址的长度
type: addr所指IP地址的类型,包括AF_INET和AF_INET6
- getservbyname和getservbyport
#include <netdb.h>
//根据服务名称获取某个服务的完整信息
struct servent* getservbyname(const char* name, const char* proto);
//根据服务端口号获取某个服务的完整信息
struct servent* getservbyport(int port, const char* proto);
struct servent{
char* s_name; //服务名称
char** s_aliases; //服务的别名列表,可能有多个
int s_port; //服务对应的端口号
char* s_proto; //服务的类型,通常是tcp或者udp
}
参数:
name: 目标服务的名字
proto: 指定服务类型,如传递”tcp“表示获取流服务,传递“udp”表示获取数据报服务,传递NULL表示获取所有类型的服务
port: 目标服务对应的端口号
- getaddrinfo
getaddrinfo函数能通过主机名获得IP地址(内部使用gethostbyname),也能通过服务名获得端口号(内部使用getservbyname)
代码语言:javascript复制#include <netdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
//由于getaddrin隐式分配堆内存到result,因此需要使用这个函数释放内存
void freeaddrinfo(struct addrinfo* res);
struct addrinfo{
int ai_flags; //见后文
int ai_family; //地址族
int ai_socktype; //服务类型,SOCK_STREAM或SOCK_DGRAM
int ai_protocal; //见后文
socklen_t ai_addrlen; //socket地址ai_addr的长度
char* ai_canonname; //主机的别名
struct sockaddr* ai_addr; //指向socket地址
struct addrinfo* ai_next; //指向下一个socketinfo结构对象
}
- getnameinfo
getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用gethostbyaddr)和服务名(内部使用的是getservbyport)
代码语言:javascript复制#include <netdb.h>
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);