Linux网络编程API(一)

2022-09-26 17:39:05 浏览数 (1)

相关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_insocketaddr_in6,分别用于Ipv4Ipv6

代码语言: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_STREAMSOCK_DGRAM,在TCP/IP中,SOCK_STREAM表示使用TCP,SOCK_DGRAM表示使用UDP。同时也可以传入上述服务类型与下面两个标志的相与的值: SOCK_NONBLOCKSOCK_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_controlmsg_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

  1. gethostbyname和gethostbyaddr
代码语言:javascript复制
#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_INETAF_INET6

  1. getservbyname和getservbyport
代码语言:javascript复制
#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: 目标服务对应的端口号

  1. 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结构对象
}
  1. 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);

0 人点赞