文章目录
一、socket 地址API
1.1 大端、小端字节序
对于同一组数据 0x12 34 56 78
大端字节序:0x12 34 56 78 -------> 网络字节序(为了避免不同字节序的主机间传递信息,统一在网络中使用大端字节序)
小端字节序:0x78 56 34 12-------->主机字节序(由于现代PC采用较多)
对应的转换api有
#include<netline/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);//将短整型的网络字节序转换为主机字节序
1.2 通用 socket 地址结构
所有的socket编程接口使用的地址结构都是以下两个
#include<bits/socket.h>
struct sockadd{
sa_family_t sa_family;//表示这个地址属于什么协议,例如ipv4等
char sa_data[14];//具体的地址数据
}
//这个结构体是在上面的基础上可以存放更长的地址,并且可以做到内存对齐
struct sockaddr_storage{
sa_family_t sa_family;//表示这个地址属于什么协议,例如ipv4等
unsigned long int __ss_align;//用来内存对齐
char __ss_padding[128-sizeof(__ss_align)];//具体的地址数据
}
1.3 专用 socket 地址结构
#include<sys/un.h>
//UNIX本地协议族使用的socket结构体
struct sockaddr_un{
sa_family_t sin_family; //地址族:AF_UNIX
char sun_path[108]; //地址路径
};
//ipv4使用的socket结构体
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地址,用网络字节序表示
};
//ipv6使用的socket结构体
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的地址结构体
};
struct in6_addr{
unsigned char sa_addr[16]; //ipv6地址,用网络字节序表示
}
所有的专用socket结构在使用的时候,都需要去转换成通用地址结构sockaddr(直接强制转换),这是由于所有的socket编程接口使用的地址参数都是sockaddr。
1.4 IP地址转换函数
下面的函数主要用于点分十进制和网络字节序的转换
ipv4地址的函数
#include <arpa/inet.h>
//inet_addr 函数将点分十进制转换为网络字节序,失败返回INADDR_NONE
in_addr_t inet_addr(const char* strptr);
//inet_aton 函数将点分十进制转换为网络字节序存储在inp中,成功返回1,失败放回0
int inet_aton(const char* cp, struct in_addr* inp);
//inet_ntoa 函数将网络字节序转化为点分十进制输出
//函数内部使用静态变量存储结果,所以具有不可重入性
char* inet_ntoa(struct in_addr in);
ipv6地址的函数(ipv4也可以用)
#include <arpa/inet.h>
//inet_pton 表示将点分十进制转化为网络字节序表示的ip地址
//af指定地址族,src是点分十进制的地址,dst表示转化后的网络字节序表示的地址
//成功返回1,失败返回0并设置error
int inet_pton(int af, const char* src, void* dst);
//inet_ntop 进行相反的转换,成功放回目标存储单元的地址,失败返回NULL并设置error
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);
//cnt是使用宏来设置的,如下
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 64
二、创建socket
创建socket主要使用以下方式:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//domain 指出使用的是哪个底层协议族,有PE_INET(ipv4),PF_INET6(ipv6),PE_UNIX
//type 指出服务类型,主要有SOCK_STREAM流服务(TCP)和SOCK_UGRAM数据报(UDP)
//protocol几乎所有情况下设置为0
//创建成功返回一个socket文件描述符,失败返回-1并设置error
三、命名socket
socket命名指的是将socket与socket地址绑定
在服务器中需要命名socket,而在客户端中一般采用匿名的方式,所以不用命名socket
#include <sys/types.h>
#include <sys/socket.h>
//函数主要将my_addr所指向的地址分配给当前的socket文件描述符sockfd,addrlen表示该socket地址的长度
int bind(int sockfd, const struct sockaddr* my_addr,socklen_t addrlen);
//bind成功返回0,失败返回-1,并设置error,两种常见的error如下
//EACCES,普通用户将socket绑定到受保护的地址上了。
//EADDRINUSE,被绑定的地址正在使用中
四、监听socket
监听队列用来存放待处理的客户连接
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//sockfd用来指定被监听的socket句柄
//backlog提示的是内核监听队列的最大长度
//listen失败返回-1并设置error,成功返回0
五、接受连接
#include <sys/types>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
//sockfd表示执行过listen的socket句柄
//addr用来获取被接受连接的远端socket地址
//addrlen用来接收addr的长度
//失败返回-1并设置error,成功返回0
问题:如果监听队列中处于ESTABLISHD状态的连接对应的客户端出现网络异常或者提前退出,那么服务器对这个连接执行的accept调用是否成功?
accept只是从监听队列中取出连接,不关心连接处于何种状态,更不关心任何网络状况的变化,所以不管是出现网络异常还是提前退出,accept都会成功。
六、发起连接
客户端侧用来与服务器建立连接
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
//sockfd是系统调用返回的一个socket
//serv_addr是服务器监听的socket地址
//addrlen指定这个地址的长度
//连接成功返回0,失败返回-1并设置error
七、关闭连接
#include <unistd.h>
int close(int fd);
//fd是待关闭的socket句柄,成功返回0,失败返回-1
//close并不是真正关闭一个socket连接,而是将其引用计数减一,引用计数变成0时才会真正关闭连接
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
//sockfd是待关闭的fd,howto决定了shutdown的方法,有SHUT_RD,SHUT_WR,SHUT_RDWR
//shutdown能够立刻关闭,而不是close那种计数减一
八、数据读写
8.1 TCP数据读写
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
//recv读取sockfd上的数据,buf和len分别代表缓冲区和缓冲区长度,返回0表示对方关闭连接,-1表示recv出错并设置error
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
//send往sockfd上写数据,buf和len指定缓冲区大小以及长度,成功返回实际写入的长度,失败返回-1并设置error
//flag为数据收发设置额外的控制
8.2 UDP数据读写
也可以用于有连接的数据传输(addrlen写为NULL即可)
#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 recvto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr*
src_addr, socklen_t* addrlen);
//读/写socket上的数据,buf和len表示指定缓冲区的位置以及大小,src_addr表示发送端/接收端的socket地址,addrlen表示该地址的长度
8.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);
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;
};
//msg_name用于指定对方的socket地址,对于TCP连接没有意义,必须设置为NULL
//msg_control和msg_controllen用于实现进程间传递文件描述符
//msg_flags无需设定,他会复制recvmsg和sengmsg的对应内容
struct iovec{
void* iov_base; //内存起始地址
size_t iov_len; //内存长度
};
对于recvmsg来说,数据将被读取并保存在msg_iovlen块分散的内存中,这称为分散读。
对于sendmsg来说,msg_iovlen块分散在内存中的数据将被统一发送,这称为集中写。
9、带外标记
#include <sys/socket.h>
int sockatmark(int sockfd);
//sockatmark判断fd是否处在带外标记,即下一个被读取到的数据是否是带外数据
//是的话,sockatmark返回1,并且可以利用带MSG_OOB标志的recv调用来接收带外数据
//不是的话,sockatmark返回0
10、地址信息函数
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
//获取本端sockfd的地址存在address中,address_len表示本端地址长度
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);
//获取远端sockfd的地址存在address中,address_len表示远端地址长度
如果实际socket地址大于address的长度,socket地址将会被截断,上面两个函数成功时会返回0,失败返回-1并设置error
11、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);
//sockfd指定了被操作的目标socket,level指定要操作那个协议的选项,
//option_name指定选项的名字,有SOL_SOCKET,IPPROTO_IP,IPPROTO_IPV6,IPPROTO_TCP
//option_value和option_len表示被操作选项的值和长度
1.SO_REUSEADDR选项
该选项可以重用本地地址,即使得处于TIME_WAIT状态的连接占用的socket地址被强制重用
2.SO_RCVBUF选项和SO_SNDBUF选项
SO_RCVBUF选项和SO_SNDBUF选项分别表示TCP接收缓冲区和发送缓冲区的大小,但是使用setsockopt来设置,系统会给缓冲区设置两倍于我们设置值的大小,例如我们设置2000B的缓冲区,系统实际分配4000B的缓冲区。
但是,接收缓冲区的最小值为256B,发送缓冲区的最小值为2048B,小于这两个值的话,用户的设置便会无效,例如我们将发送缓冲区设置为50B,则系统会无视我们的设置,发送缓冲区仍然为256B。
3.SO_RCVLOWAT选项和SO_SNDLOWAT选项
SO_RCVLOWAT选项和SO_SNDLOWAT选项分别表示TCP接收缓冲区以及发送缓冲区的低水位标志,一般被I\O复用系统调用来判断socket是否可读或者可写。
默认情况下,TCP接收缓冲区以及发送缓冲区的低水位标志均为1B。
3.SO_LINGER选项
若有数据要发送,则延迟关闭socket,设置SO_LINGER的值时,涉及一个结构体
#include <sys/socket.h>
struct linger{
int l_onoff; //是否开启该选项
int l_linger;//滞留时间
};
- l_onoff=0,SO_LINGER不起作用,close默认关闭socket
- l_onoff≠0,l_linger=0,TCP丢弃该socket的发送缓冲区中残留的数据,同时给对方发送一个复位报文段
- l_onoff≠0,l_linger>0,如果socket阻塞,close将等待长为l_linger的时间直到TCP发送完所有的残余数据并且得到对方的确认,如果这段时间没有发送完数据并且得到确认,则close系统调用将返回-1并设置error为EWOULDBLOCK,如果socket非阻塞,close将立即返回。
12、网络信息api
12.1 gethostbyname和gethostbyaddr
#include <netdb.h>
struct hostent* gethostbyname(const char* name);//根据主机名获取主机完整信息
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);//根据主机地址获取主机完整信息
struct hostent{
char* h_name; //主机名
char** h_aliases; //主机别名
int h_addrtype; //地址类型
int h_length; //地址长度
char** h_addr_list;//按网络字节序列出的主机IP地址列表
};
12.2 getservbyname和getservbyport
#include <netdb.h>
struct servnet* getservbyname(const char* name, const char* proto);//根据名称获取整个服务的完整信息
struct servnet* getservbyport(int port, const char* proto);//根据端口号获取整个服务的完整信息
//proto表示服务类型,有tcp,udp,NULL(tcp+udp)
struct servent{
char* s_name; //服务名称
char** s_aliases; //服务的别名
int s_port; //端口号
char* s_proto; //服务类型,通常是tcp或者udp
};
12.3 getaddrinfo
既能使用主机名获取IP地址(内部使用gethostbyname),也能使用服务名获取端口号(内部使用getservbyport)
#include<netdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
//hostname可以接收主机名,也可以接收字符串表示的IP地址
//service可以接收服务名,也可以接收十进制端口号
//hints可以设置为NULL,以接收认可有用的反馈
//result指向一个链表,存储getaddrinfo的反馈
struct addrinfo{
int ai_flags; //
int ai_family; //地址族
int ai_socktype; //服务类型,SOCK_STREAM或者SOCK_DGRAM
int ai_ptotocol; //具体的网络协议
socklen_t ai_addrlen; //socket地址ai_addr的长度
char* ai_canonname; //主机别名
struct sockaddr* ai_addr; //指向socket地址
struct addrinfo* ai_next; //指向下一个addrinfo结构
};
12.4 getnameinfo
既能使用socket地址获取以字符串表示的主机名(内部使用gethostbyaddr),也能使用socket地址获取以字符串表示的服务名(内部使用getservbyport)
#include<netdb.h>
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen,
char* serv, socklen_t servlen, int flags);