前言
socket地址API:socket最开始的含义是一个P地址和端口对(ip,port)。它唯一地表示了使用TCP通信的一端。本书称其为socket地址。
socket基础API:ocket的主要API都定义在sys/socket.h头文件中,包括创建socket、命名socket、监听 socket、接受连接、发起连接、读写数据、获取地址信息、检测带外标记,以及读取和设置socket选项。
网络信息API:Linux提供了一套网络信息API,以实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。这些API都定义在netdb.h头文件中,我们将讨论其中几个主要的函数。
socket地址API
主机字节序和网络字节序
现代CPU的累加器一次都能装载(至少)4字节(这里考虑32位机,下同),即一个整数。那么这4字节在内存中排列的顺序将影响它被累加器装载成的整数的值。这就是字节序问题。字节序分为大端字节序(big endian)和小端字节序(little endian)。大端字节序是指一个整数的高位字节(23~31 bit)存储在内存的低地址处,低位字节(0~7 bit〉存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。
现在PC大多采用小端字节序,因此小端字节序又被称为主机字节序
当格式化的数据(比如 32 bit整型数和16 bit短整型数)在两台使用不同字节序的主机之间直接传递时,接收端必然错误地解释之。解决问题的方法是﹔发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。因此大端字节序也称为网络字节序,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证。
API host byte_order->net byte_order
#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);
通用socket地址
#include<bits/socket.h>struct sockaddr{sa_family_t sa_family;char sa_data[14];}//sa_family_t(地址变量)//sa_data[16];(地址值)
常见的协议族和对应的地址族
协议族对应的地址值
由此可见更新
#include<bits/socket.h>struct sockaddr_storage{sa_family_t sa_family;unsignned long int __ss_align;char __ss_paddeing[128-sizeof(__ss_align)];}//新增的__ss_align[对齐内存](/qq_43516928/article/details/119680760)
专用socket地址
重点讲ipv4
struct sockaddr_in{sa_family_t sin_family;//地址族 AF_INETu_int16_t sin_port;//port net——bytesstruct in_addr sin_addr;//ipv4 结构体 地址值}struct in_addr{u_int32_t s_addr;}
IP地址转换函数
前言:
可读性好的字符串来表示IP地址 点分十进制表示 IPV4
#include<arpa/inet.h>int_addr_t inet_addr(const char* strptr);//点分十进制->big endianint inet_aton(const char* cp,struct in_addr* inp);char* inet_ntoa(struct in_addr in);//big endian->点分十进制
更新函数
#includ<arpa/inet.h>int inet_pton(int af,const char* src,void* dst);//点分十进制->big endian//src Dotted decimal notation//af:协议族//dst相当一个buffer区 const char* inet_ntop(int af,const void* src,char* dst,socklen_t cnt)//RETURN dst//cnt 指定dst的大小//官方//#define INET_ADDRSTRLEN 16;//#define INET6_ADDRSTRLEN 46;
创建socket
一切东西都是文件
socket是一个可读 可写 可控制 可关闭的文件描述符
#include<sys/types.h>#include<sys/socket.h>int socket(int domain,int type,int protocol);//domain 是使用什么底层协议//type sock_stream sock_ugram //protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常//都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下,我们都应该把它设//置为0,表示使用默认协议。
命名socket
创建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);//常见的error:EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口(端口号为0~1023)上时,bind将返回EACCES错误。EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_WAIT状态的socket地址。
监听socket
sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。在内核版本2.2之前的Linux 中,backlog参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket的上限。但自内核版本2.2之后,它只表示处于完全连接状态的socket 的上限,处于半连接状态的 socket的上限则由/proc/svs/net/ipv4/tcp max syn backlog内核参数定义。backlog参数的典型值是5。
#include<sys/socket.h>int listen(int socketfd,int backlog)
接受连接
sockfd参数是执行过listen系统调用的监听 socket。addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。accept成功时返回-一-个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。accept 失败时返回-1并设置errno.
#include<sys/types.h>#include<sys/socket.h>int accept(int sockfd,struct sockaddr* addr,socklent_t addrlen);
由此可见,accept只是从监听队列中取出连接,而不论连接处于何种状态(如上面的ESTABLISHED状态和CLOSE_WAIT 状态),更不关心任何网络状况的变化。
发起连接(客户端)
#include<sys/types.h>#include<sys/socket.h>int connect(int sockfd,const struct sockaddr* serv_addr,socklent addrlen);
serr_addr 监听的socket地址
关闭连接
#include<unistd.h>int close(int fd);int shutdown(int sockfd,int howto);
fd参数是待关闭的 socket。不过,close系统调用并非总是立即关闭一个连接,而是将fl的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的 socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。
需要立即关闭:shutdown
数据读写
TCP数据读写
#include<sys/types.h>#include<sys/socket.h>ssize_t recv(int sockfd,void* buf,size_t len,int flags);//buf->lenssize_t send(int sockfd,const void* buf,size_t len,int flags);
flags 参数
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 sockelen_t addrlen);ssize_t sendto(int sockfd,void* buf,size_t len,int flags,struct sockaddr* src_addr sockelen_t addrlen);
通用数据读写
#include<sys/socket.h>ssize_t recvmsg(int sockfd,struct msgdr* msg,int flags);ssize_t sendmsg(int sockfd,struct msgdr* msg,int flags);struct msghdr {void *msg_name; /* optional address */socklen_tmsg_namelen; /* size of address */struct iovec *msg_iov; /* scatter/gather array */size_t msg_iovlen;/* # elements in msg_iov */void *msg_control; /* ancillary data, see below */size_t msg_controllen; /* ancillary data buffer len */int msg_flags;/* flags on received message */};struct iovec {/* Scatter/gather array items */void *iov_base; /* Starting address */size_t iov_len;/* Number of bytes to transfer */};
带外标记
#include<sys/socket.h>int socketmark(int sockfd);
sockatmark 判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。如果是。sockatmark 返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则 sockatmark返回0。
地址信息函数
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);if len>addrlen 会被截断int getpeername(int sockfd,struct sockaddr* addr,socklent_t* addrlen);
socket选项
#include<sys/socket.h>int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);level:指定协议名称option_name:指定选项名字option_value and option_len :值与长度
problem:
值得指出的是,对服务器而言,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效。这是因为连接socket 只能由accept调用返回,而accept从 listen 监听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤(因为listen 监听队列中的连接至少已进入SYN_RCVD状态,参见,这说明服务器已经往被接受连接上发送啦TCP同步报文
Linux给开发人员提供的解决方案是:对监听socket 设置这些socket选项,那么accept返回的连接socket将自动继承这些选项。这些socket选项包括:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、So_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、so_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG和TCP_NODELAY。而对客户端而言,这些socket选项则应该在调用connect函数之前设置,因为connect调用成功返回之后,TCP三次握手已完成。
SO_RCVBUF and SO_SNDBUF
言简意赅:RCVEICE BUFFER
SEND BUFFER
当我们用setsockopt来设置TCP的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。TCP接收缓冲区的最小值是256字节,而发送缓冲区的最小值是2048字节(不过,不同的系统可能有不同的默认最小值)。系统这样做的目的,主要是确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞(比如快速重传算法就期望TCP接收缓冲区能至少容纳4个大小为SMSS的TCP报文段)。此外,我们可以直接修改内核参数/proc/sys/net/ipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem来强制TCP接收缓冲区和发送缓冲区的大小没有最小值限制。
SO_RCVLOWAT and SO_SNDLOWAT
SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。它们一般被I/O复用系统调用(见第9章)用来判断socket 是否可读或可写。当TCP接收缓冲区中可读数据的总数大于其低水位标记时,IO复用系统调用将通知应用程序可以从对应的socket 上读取数据﹔当TCP发送缓冲区中的空闲空间(可以写入数据的空间)大于其低水位标记时,IO复用系统调用将通知应用程序可以往对应的socke 上写入数据。
SO_LINGER
SO_LINGER选项用于控制close系统调用在关闭TCP连接时的行为。默认情况下,当我们使用close系统调用来关闭一个socket时,close将立即返回,TCP模块负责把该socket对应的TCP发送缓冲区中残留的数据发送给对方。
#inckclude<sys/socket.h>struct linger{int l_onoff;//off 0int l_linger;//滞留时间}
l_onoff等于0:此时SO_LINGER选项不起作用,close用默认行为来关闭socket。
l_onoff不为0,l_linger等于0:此时close系统调用立即返回,TCP模块将丢弃被关闭的socket对应的TCP发送缓冲区中残留的数据,同时给对方发送一个复位报文段。因此,这种情况给服务器提供了异常终止一个连接的方法。
l_onoff不为0,1_linger大于0:此时close的行为取决于两个条件:一是被关闭的socket对应的TCP发送缓冲区中是否还有残留的数据﹔二是该socket是阻塞的,还是非阻塞的。对于阻塞的socket,close将等待一段长为l_linger的时间,直到TCP模块发送完所有残留数据并得到对方的确认。如果这段时间内TCP模块没有发送完残留数据并得到对方的确认,那么close系统调用将返回-1并设置errno为EWOULDBLOCK。如果 socket是非阻塞的,close将立即返回,此时我们需要根据其返回值和errno来判断残留数据是否已经发送完毕。(重点)
网络信息API
gethostbyname and gethostbyaddr
gethostbyname函数根据主机名称获取主机的完整信息,gethostbyaddr函数根据IP地址获取主机的完整信息。gethostbyname函数通常先在本地的/etc/hosts 配置文件中查找主机,如果没有找到,再去访问DNS服务器。这些在前面章节中都讨论过。这两个函数的定义如下:
#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; /* official name of host */char **h_aliases; /* alias list */int h_addrtype; /* host address type */int h_length;/* length of address */char **h_addr_list; /* list of addresses */}
getservbyname and getservbyport
name参数指定目标服务的名字,port参数指定目标服务对应的端口号。proto参数指定服务类型,给它传递“tcp”表示获取流服务,给它传递“udp”表示获取数据报服务,给它传递NULL则表示获取所有类型的服务。
struct servent *getservbyname(const char *name, const char *proto);struct servent *getservbyport(int port, const char *proto);struct servent {char *s_name; /* official service name */char **s_aliases; /* alias list */int s_port; /* port number */char *s_proto;/* protocol to use */}
以上都是非线程安全
getaddrinfo
getaddrinfo函数既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。它是否可重人取决于其内部调用的 gethostbyname和getservbyname函数是否是它们的可重入版本。该函数的定义:
int getaddrinfo(const char *node, const char *service,const struct addrinfo *hints,struct addrinfo **res);/*hostname参数可以接收主机名,也可以接收字符串表示的IP地址(IPv4采用点分十进制字符串,IPv6则采用十六进制字符串)。同样,service参数可以接收服务名,也可以接收字符串表示的十进制端口号。hints参数是应用程序给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制。hints参数可以被设置为NULL,表示允许getaddrinfo反馈任何可用的结果。result参数指向一个链表,该链表用于存储getaddrinfo反馈的结果。*/struct addrinfo {int ai_flags;//标志int ai_family;//地址族int ai_socktype;//服务类型int ai_protocol;//具体网络协议socklen_t ai_addrlen;//struct sockaddr *ai_addr;//char *ai_canonname;//主机别名struct addrinfo *ai_next;//指向sockinfo的结构的对象};e.struct addrinfo hints;struct addrinfo* res;bzero(&hints,sizeof(hints));hints.ai_socktype=SOCK_STREAM;getaddrinfo(”localhost","daytime",&hints,&res)void freeaddrinfo(struct addrinfo* res);
getnameinfo
getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。它是否可重入取决于其内部调用的gethostbyaddr和getservbyport函数是否是它们的可重入版本。该函数的定义如下:
int getnameinfo(const struct sockaddr *sa, socklen_t salen,char *host, size_t hostlen,char *serv, size_t servlen, int flags);