一、socket地址API
1、主机字节序和网络字节序
小端字节序(主机字节序)是指一个整数的高位字节存储在内存的高地址处
大端字节序(网络字节序)是指一个整数的高位字节存储在内存的低地址处
判断机器字节序
#include <stdio.h>void byteorder(){union {short value;char union_bytes[sizeof(short)];}test;test.value = 0x0102;if ((test.union_bytes[0] == 1)&& (test.union_bytes[1] == 2)) {printf("big endian\n");}else if((test.union_bytes[0] == 2) && (test.union_bytes[1] == 1)){printf("little endian\n");}else{printf("unknown\n");}
}
主机字节序和网络字节序之间的转换
#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);
功能:短整型的网络字节序转化为主机字节序
2、通用socket地址
#include <bits/socket.h>
struct sockaddr{sa_family_t sa_family; // 地址族类型char sa_data[14]; // 存放socket地址值
}
// 协议族(protocol family 也称domain)
协议族 地址族 描述 地址值含义和长度
PF_UNIX AF_UNIX UNIX本地域协议族 文件的路径名,长度可到达108字节
PF_INET AF_INET TCP/IPv4协议族 16bit端口号和32bitV IPv4地址,共6个字节
PF_INET6 AF_INET6 TCP/IPv6协议族 16bit端口号,32bit流标识,128bit IPv6地址,32bit范围ID,共有26字节// 为解决sa_data无法容纳多数协议族的地址值,定义新的通用socker地址结构体
struct sockaddr_storage{sa_family_t sa_family;unsigned long int __ss_align;char __ss_padding[128-sizeof(__ss_align)];
}
3、专用socket地址
#include <sys/un.h>
struct sockaddr_un{sa_family_t sin_family; // AF_UNIXchar sun_path[108]; // 文件路径名
}
struct sockaddr_in{sa_family_t sin_family; // 地址族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_INET6u_int16_t sin6_port; // 端口号,要用网络字节序表示u_int32_t sin6_flowinfo; // 流信息,应设置为0struct 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(强制转换即可)
4、IP地址转换函数
#include<arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
功能:将用点分十进制字符串表示的IPv4的地址转化为用网络字节序整数表示的IPv4.
返回值:失败:INADDR_NONEint inet_aton(const char* cp, struct in_addr* ip);
功能:同inet_addr同样的功能。将转化的结果存储于参数inp指向的地址结构中
返回值:成功 1失败 0char *inet_ntoa(struct in_addr in);
功能:将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址(函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的int inet_pton(int af, const char* src. void* dst);
功能:将用字符串表示的IP地址src转换成网络字节序整数表示的IP地址,并将转化结果存储于dst指向的内存中
参数:af 指定地址族 AF_INET 或者AF_INET6src IP地址字符串dst 转化结果存储于dst指向的内存中
返回值 成功 1失败 0 设置errnoconst char* inet_ntop(int af, void *src, char *dst, socklen_t cnt);
功能:af,src,dst同上cnt指定目标存储单元的大小 #define INET_ADDRSTRLEN 16#define INET6_ADDRSTRLEN 46
返回值:成功 目标存储单元的地址失败 NULL并设置errno
二、创建socket socket
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:
参数:domin:告诉系统使用底层协议族IPv4 PF_INET IPv6 PF_INET6本地协议族 PF_UNIXtype 指定服务类型SOCK_STREAM TCPSOCK_DGRAM UDPSOCK_NONBLOCK 设置为非阻塞的SOCK_CLOEXEC 用fork调用创建子进程时在子进程中关闭该socketprotocol 通常设置为0,表示默认协议
返回值:成功 返回一个socket文件描述符失败 -1并设置errno
三、绑定socket bind
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
功能:将sockfd与my_addr进行绑定
参数:sockfd:socket创建出来的文件描述符my_addr:服务器的地址addrlen:my_addr的大小 sizeof(my_addr)
返回值:成功: 0失败:-1并设置errnoEACCES:被绑定的地址是受保护的地址,仅超级用户能够访问EADDRINUSE:被绑定的地址正在使用中/*注意:服务器中,就需要为sockfd与my_addr进行绑定,因为只有绑定后客户端才能知道如何连接它客户端不需要绑定,而是采用匿名方式,也就是操作系统自动分配socket地址
*/
四、监听socket listen
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:创建监听队列以存放待处理的客户连接
参数:sockfd:指定被监听的socketbacklog:内核监听队列的最大长度,连接数超过backlog,客户端收到ECONNREFUSED错误信息
返回值:成功 0失败 -1并设置errno
五、接受连接 accept
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:从listen监听队列中接受一个了连接
参数:sockfd:执行过里listen系统调用的文件描述符addr:客户端socket地址addrlen:addr的长度
返回值:成功:与客户端通信的文件描述符,读写都是通过这个描述符来进行的失败:-1并设置errno
六、发起连接 connect
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
功能:客户端发起连接服务器
参数:sockfd:客户端的sock文件描述符serv_addr:服务器地址addrlen:serv_addr的长度
返回值:成功 0失败 -1并设置errnoECONNREFUSED:目标端口不存在,连接被拒绝ETIMEDOUT:连接超时
七、关闭连接 close
#include<unistd.h>
int close(int fd);
功能:关闭fd,并不是立即关闭一个连接,而是将fd的引用计数减1,只有当fd的引用计数为0时,才真正关闭连接#include<sys/socket.h>
int shutdown(int sockfd, int howto);
功能:立即终止连接
参数:howtoSHUT_RD:关闭读SHUT_WR:关闭写SHUT_RDWR:关闭读写
返回值:成功 0失败 -1并设置errno
八、数据读写
1、TCP数据读写
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:读取sockfd上的数据
参数:sockfd:文件描述buf:指定缓冲区的位置len:指定缓冲区的大小flags:0
返回值:成功:实际读取的数据的长度失败:0 对方已经关闭连接错误 -1并设置errnossize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:往sockfd上写入数据
参数:sockfd:文件描述符buf:指定缓冲区的位置len:缓冲区的大小flags:0
返回值:成功 返回实际写入数据的长度失败 -1并设置errno
flags可以取以下选项的一个或几个的逻辑或
选项名 | 含义 | send | recv |
---|---|---|---|
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数据读写
#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, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
参数:sockfd 读写数据文件描述符buf:读写缓冲区的位置len:缓冲区的大小flags:0src_addr:获取发送端的socket地址dest_addr:指定接收端的socket地址addrlen:地址长度// 注意:recvform/sendto也可以用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接受端的地址
3、通用数据读写函数
#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 读写数据文件描述符msg msghdr结构体类型指针flags 0struct 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参数,并在调用过程中更新
};struct iovec
{void *iov_base; // 内存起始地址size_t iov_len; // 这块内存的长度
};
iovec结构体封装了一块内存的起始位置和长度,msg_iovlen指定这样的分散的iovec结构对象有多少个
msg_control和msg_controllen 用于辅助数据的传送
msg_flags 无须设定,它会复制函数的第三个参数flags,recvmsg还会在调用结束强,将某些更新后的标志设置到msg_flags中
九、带外标记
#include <sys/socket.h>
int sockatmark(int sockfd);
功能:判断sockfd是否处于带外标记,下一个被读取到的数据是否是带外数据
参数:sockfd 文件描述符
返回值:1 是0 不是
用sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。如果是,sockatmark返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0。
十、地址信息函数
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* address, socklen_t *address_len);
功能:获取sockfd对应的本端socket地址
参数:sockfd 文件描述符address 存储地址的内存address_len 地址长度
返回值:0 成功-1 失败int getpeername(int sockfd, struct sockaddr* address, socklen_t *address_len);
功能:获取sockfd对应的远端socket地址
参数:同getsockname
十一、socket选项
#include <sys/socket.h>
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 option_len);
功能:读取和设置socket文件描述符属性
参数:sockfd 指定被操作目标socketlevel 参数指定要操作的那个协议的选项SOL_SOCKET 通用socket选项,与协议无关IPPROTO_IP IPv4选项IPPROTO_IPV6 IPv6选项IPPROTO_TCP TCP选项option_name:指定选项的名字,常用以下几个SO_REUSEADDR:重用socket地址 intSO_RCVBUF:TCP接收缓冲区的大小 intSO_SNDBUF:TCP发送缓冲区的大小 intSO_RCVLOWAT:TCP接收缓冲区的低水位标记,默认1字节 intSO_SNDLOWAT:TCP发送缓冲区的低水位标记,默认1字节 intSO_LINGER:如下 lingeroption_value:被操作选项的值option_len:被操作选项的长度
返回值:0 成功-1 失败并设置errnoSO_LINGER:
// SO_LINGER用于控制close系统调用在关闭TCP连接时的行为。
// 通常,使用close系统调用来关闭socket时,close将立即返回,TCP模块负责把该socket对应TCP发送缓冲区中残留的数据发送给对方。
#include <sys/socket.h>
struct linger{int l_onoff; // 开启(非0)还是关闭(0)该选项int l_linger; // 滞留时间
};
- l_onoff = 0,此时SO_LINGER选项不起作用,close用默认行为来关闭socket
- l_onoff != 0,l_linger = 0,此时close系统立即返回,TCP模块丢弃被关闭的socket对应的TCP发送缓冲区中残留的数据,同时给对方发送一个复位报文段,,提供了异常终止一个连接的方法
- l_onoff != 0,l_linger大于0,此时close的行为取决于两个条件:1.TCP缓冲区中是否残留都数据,2. socket是阻塞的,还是非阻塞的- 阻塞:close将等待一段长为l_linger的时间,如果没有发送往并得到对方确认,close系统调用返回-1并设置errno为EWOULDBLOCK- 非阻塞:close将立即返回,根据返回值和errno判断数据是否发送完毕
十二、网络信息API
1、gethostbyname和gethostbyaddr
//
#include <netdb.h>
struct hostent* gethostbyname(const char *name);
功能:根据主机名称获取主机的完整信息
参数:name:目的主机的主机名
返回值:hostent结构体类型的指针struct hostent* gethostbyaddr(const void *addr, size_t len, int type);
功能:根据IP地址获取主机的完整信息
参数:addr:目标主机的IP地址len:addr所只IP地址的长度type:IP地址的类型, AF_INET(IPv4) AF_INEF69(IPv6)
返回值:hostent结构体类型的指针struct hostent{char* h_name; // 主机名char** h_aliases; // 主机别名列表,可能有多个int h_addrtype; // 地址类型int h_length: // 地址长度char** h_addr_list; // 按网络字节序列出的主机IP地址列表
};
2、getservbyname和getservbyaddr
//
#include <netdb.h>
struct servent* getservbyname(const* name, const char* proto);
功能:根据名称获取某个服务的完整信息
参数:name:目标服务的名字proto:指定服务类型,“tcp”获取流服务 “udp”获取数据报服务 NULL获取所有类型的服务
返回值:servent结构体类型的指针struct servent* getservbyport(int port, const char* proto);
功能:根据端口号获取某个服务的完整信息
参数:port:目标服务的端口号proto:指定服务类型,“tcp”获取流服务 “udp”获取数据报服务 NULL获取所有类型的服务
返回值:servent结构体类型的指针struct servent{char* s_name; // 服务名称char** s_aliases; // 服务的别名列表,可能有多个int s_port; // 端口号char* s_proto; // 服务类型,通常是tcp或者udp
}
注意:以上4个函数都是不可重入的,即非线程安全的,不过netdb.h头文件给出它们的可重入版本,正如Linux下所有其他函数的可重入的命名规则那样。这些函数的函数名是在原函数名尾部加上_r(re-entrant)。
3、getaddrinfo
#include <netdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
功能:既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)参数:hostname:接收主机名,也可以接收字符串表示的IP地址(IPv4采用点分十进制字符串,IPv6则采用十六进制字符串)service:接收服务名,也可以接收字符串表示的十进制端口号hints:应用程序给getaddrinfo的一个提示,对getaddrinfo的输出进行更精确的控制。NULL,允许getaddrinfo反馈任何结果result:参数指向一个链表,该链表用于存储getaddrinfo反馈的结果
返回值:成功 0失败 返回错误码struct addrinfo{int ai_flags; // 下面说明int ai_family; // 地址族int ai_socktype; // 服务类型,SOCK_STREAM或SOCK_DGRAMint ai_protocol; // 下面说明socklen_t ai_addrlen; // socket地址ai_addr的长度char* ai_canonname; // 主机的别名struct sockaddr* ai_addr; // 指向socket地址4struct addrinfo* ai_next; // 指向下一个sockinfo结构的对象
};
// ai_protocol:具体的网络协议,其含义和socket系统调用的第三个参数相同,通常设置为0
// 使用hints参数的时候,可以设置ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段必须设置为NULL// result需要调用下面函数来释放这块内存
void freeaddrinfo(struct addrinfo* res);// 使用案例:
struct addrinfo hints;
struct addrinfo *res;
bzero(&hints, sizeof(hints));
hints.ai_socktype = SOCK_STREAM;
getaddinfo("ernest-laptop","daytime", &hists, &res);
...
freeaddrinfo(res);
ai_flags成员可以取以下的标志的按位或
选项 | 含义 |
---|---|
AI_PASSIVE | hists参数中设置,表示调用者是否会将取得的socket地址用于被动打开,服务器通常设置它,表示接受任何本地socket地址上的服务请求。客户端程序不能设置它 |
AI_CANONNANE | 在hists参数中设置,告诉getaddrinfo函数返回主机的别名 |
AI_NUMERICHOST | 在hists参数中设置,表示hostnam必须是用字符串表示的IP地址,从而避免了DNS查询 |
AI_NUMERICSERV | 在hists参数中设置,强制service参数使用十进制端口号的字符串形式,而不能是服务名 |
AI_V4MAPPED | 在hists参数中设置,如果ai_family被设置为AF_INET6,那么当没有满足条件的IPv6地址被找到时,将IPv4地址映射为IPv6地址 |
AI_ALL | 必须和AI_V4MAPPED同时使用,否则将被忽略。表示同时返回符合条件的IPv6地址以及由IPv4地址映射得到的IPv6地址 |
AI_ADDRCONGIG | 仅当至少配置有一个IPv4地址(除了回路地址)时,才返回IPv4地址信息;同样,仅当至少配置有一个IPv6地址(除了回路地址)时,才返回IPv6地址信息。它和AI_V4MAPPED时互斥的 |
4、getnameinfo
int getnameinfo(const struct sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);
功能:通过socket地址同时获得以字符串表示的主机名和服务名
参数:sockaddr:socket地址host:主机名hostlen:主机名的长度serv:服务号servlen:服务号的长度flags:控制getnameinfo的行为
返回值:成功 0 失败 返回错误号
// 将错误码转换成易读的字符串形式
const char* gai_strerror(int error);
flags参数
选项 | 含义 |
---|---|
NI_NAMEREQD | 如果通过socket地址不能获取主机名,则返回一个错误 |
NI_DGRAM | 返回数据服务。大部分同时支持流和数据报的服务使用相同的端口号来提供这两种服务。但端口512~514是例外。比如TCP的514端口提供的是shell登录服务,而UDP的514端口提供的是syslog服务 |
NI_NUMERICHOST | 返回字符串表示的IP地址,而不是主机名 |
NI_NUMERICSERV | 返回字符串表示的十进制端口号,而不是服务名 |
NI_NOFQDN | 仅返回主机域名的第一部分。比如对主机名nebula.testing.com,getnameinfo只将nebula写入host缓存中 |
错误码表
选项 | 含义 |
---|---|
EAI_AGAIN | 调用临时失败,提示应用程序过后再试 |
EAI_BADFLAGS | 非法的ai_flags值 |
EAI_FAIL | 名称解析失败 |
EAI_FAMILY | 不支持的ai_family参数 |
EAI_MEMORY | 内存分配失败 |
EAI_NONAME | 非法的主机名或服务名 |
EAI_OVERFLOW | 用户提供的缓冲区溢出。仅发生在getnameinfo调用中 |
EAI_SERVICE | 没有支持的服务,比如用数据报服务类型来查找ssh服务。因为ssh服务只能使用流服务 |
EAI_SOCKTYPE | 不支持的服务类型。如果hints.ai_socktype和hists.ai_protocol不一致,比如前者指定SOCK_DGRAM,而后者使用的是IPROTO_TCP,则会触发这类错误 |
EAI_SYSSTEM | 系统错误,错误值存储在errno中 |