【摘自《linux高性能服务器编程》】
现代PC大多采用小端字节序,因此称为主机字节序; 大端字节序称为网络字节序。
Linux提供了如下4个函数来完成主机字节序和网络字节序之间的转换。
#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);
Htonl表示“host to network long”,即将长整型(32bit)的主机字节序数据转化为网络字节序数据。这4个函数中,长整型函数通常用来转换IP地址,短整型函数用来转换端口号(当然不限于此。任何格式化的数据通过网络传输时,都应该使用这些函数来转换字节序)。
点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:
#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_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制表示的IPv4地址。但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回该静态内存,因此
Inet_ntoa是不可重入的。
Char *szValue1 = inet_ntoa(“1.2.3.4”);
Char * szValue2 = inet_ntoa(“10.194.71.60”);
Printf(“address 1: %s\n”, szValue1);
Printf(“address 2: %s\n” szValue2);
运行结果为:address1: 10.194.71.60
Address2:10.194.71.60
同时适用IPv4和IPv6的转换函数:
#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,socklen_t cnt);
Cnt指定目标存储单元的大小。适用以下两个宏:
#include<netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
Inet_ntop成功时返回目标存储单元的地址,失败则返回NULL并设置errno.
创建socket
#include<sys/types.h>
#include<sys/socket.h>
Int socket(int domain, int type , int protocol);
Domain使用那个协议族,PF_INET、PF_INET6
Type SOCK_STREAM SOCK_UGRAM
Protocol 前面已经确认了,一般默认为0,使用默认协议。
成功返回socket文件符
服务器程序需要命名socket,客户端使用自动分配的socket地址。
服务器端适用bind函数
#include<sys/types.h>
#include<sys/socket.h>
Int bind(int sockfd,const struct sockaddr * my_addr,socklen_t addrlen);
监听socket
#include<sys/socket.h>
Int listen(int socket,int backlog);
Backlog参数提示内核监听队列的最大长度。ESTABLISHED,SYS_RCVD。
接受连接
#include<sys/types.h>
#include<sys/socket.h>
int accept(int socket,struct sockaddr *addr,socklen_t *addrlen);
accept只是从监听队列中取出连接,而不论连接处于何种状态(如上面的ESTABLISED
状态和CLOSE_WAIT状态),更不关心任何网络状况的变化。
发起连接
服务器通过Listen调用来被动接受连接,那么客户端需要通过主动与服务器建立连接:
#include<sys/types.h>
#include<sys/socket.h>
Int connect(int sockfd,const struct sockaddr * serv_addr,socklen_t addrlne);
sockfd参数由socket系统调用返回一个socket。Serv_addr参数是服务器监听的socket地址,addrlen参数则制定这个地址的长度。
关闭连接
#include<unistd.h>
int close(int fd);
fd参数是待关闭的socket。不过,close系统调用并非总是立即关闭一个连接,而是将fd引用计数减一。只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1,因此必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。
如果需要立即终止连接(而不是将socket的引用计数减1),使用shutdown系统调用(相对close来说,它是专门为网络编程设计的):
#include<sys/socket.h>
Int shutdown(int sockfd ,int howto);
sockfd参数是待关闭的socket。Howto参数决定了shutdown的行为。
SHUT_RD:关闭读,舍弃缓冲区
SHUT_WR:关闭写,在真正关闭前将缓冲区的数据发送出去
SHUT_RDWR:同时关闭sockfd上的读和写
Close在关闭连接时只能将socket上的读和写同时关闭。
Shutdown成功时返回0。
数据读写:
TCP数据读写
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv(int sockfd , void *buf, size_t len, int flags);
sszie_t send(int sockfd,const void *buf, size_t len, int flags);
Flags: MSG_CONFIRM, MSG_DONTROUTE, MSG_DONTWAIT, MSG_MORE,
MSG_WAITALL, MSG_PEEK, MSG_OOB, MSG_NOSIGNAL
其中MSG_OOB是带外紧急数据,只接受最后一个字节。如“abc”,只接收’c’
Flags参数只对send和recv的当前调用有效,可以通过setsockopt系统调用永久性修改socket的某些属性。
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);
Recvfrom/sendto系统调用也可以用于面向连接(STREAM)的 socket的数据读写,
只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因为我们已经和对方建立了连接,所以已经知道其socket地址了)。
通用数据读写函数:
#include<sys/socket.h>
Ssize_t recvmsg(int sockfd,struct msghdr* msg, int flags);
Ssize_t sendmsg(int sockfd, structmsghdr* 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; //复制函数中的flags参数,并在调用过程中更新
};
Struct iovec
{
Void* iov_base;
Size_t iov_len;
};
带外标记:
#include<sys/socket.h>
Int sockatmark(int sockfd);
Sockatmark判断sockfd是否处于带外标记,即下一个被取到的数据是否是带外数据。
如果是,sockatmark返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0。
内核通知应用程序带外数据到达的两种常见方式是:I/O复用产生的异常事件和SIGURG信号。
地址信息函数:
通过socket获取socket地址
#include<sys/socket.h>
Int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
Int getpeername(int sockfd, struct sockaddr* address,socklen_t* address_len);
Getsockname获取本端的socket地址。
Getpeername获取sockfd对应的远端socket地址。
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参数指定要操作哪个协议的选项(即属性),
比如IPv4,IPv6,TCP等。option_name参数则指定选项的名字。
注意:对服务器而言,有部分socket只能由accept调用返回,而accept从listen监听socket设置才有效。
监听socket选项特性:对监听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。而对客户端而言,这些选项要在调用connect函数之前设置,因为connect调用成功返回之后,TCP三次握手已经完成。
SO_REUSEADDR选项:
TCP连接TIME_WAIT状态,并提到服务器程序可以通过设置socket选项SO_REUSEADDR来强制使用被处于TIME_WAIT状态的连接占用的socket地址。
重用本地地址
Int sock = socket(PF_INET, SOCK_STREAM,0);
Assert(sock >= 0);
In reuse = 1;
Setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
Struct sockaddr_in address;
Bzero(&address, sizeof(address));
Address.sin_family = AF_INET;
Inet_pton(AF_INET, ip ,&address.sin_addr);
Address.sin_port = htons(port);
Int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
经过setsockopt的设置之后,即使sock处于TIME_WAIT状态,与之绑定的socket地址也可以立即被重用。此外我们也可以通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle来快速回收被关闭的socket,从而使得TCP连接根本就不进入TIME_WAIT状态,进而允许应用程序立即重用本地socket地址。
SO_RCVLOWAT和SO_SNDLOWAT
当TCP接收缓冲区中可读数据的总数大于其低水位标记时,I/O复用系统调用将通知应用程序可以从对应的socket上来读取数据;当TCP发送缓冲区中的空闲空间大于其低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socket上写入数据。默认情况下,该值都为1字节。
SO_LINGER
#include<sys/socket.h>
Struct linger
{
Int l_onoff;//开启非0,关闭0
Int l_linger;//滞留时间
};
L_onoff == 0;//不起作用close用默认方法关闭socket
L_onoff != 0,l_linger == 0;//close系统调用立即返回,TCP模块将丢弃被关闭的socket对应的TCP发送缓冲区中残留的数据,同时给对方发送一个复位报文段,这种情况服务器提供了异常终止一个连接的方法。
L_onoff != 0, l_linger > 0;一是被关闭的socket对应的TCP发送缓冲区中是否还有残留的数据;二是该socket是阻塞的,还是非阻塞的socket,close将等待一段长为l_linger的时间,直到TCP模块发送完所有的残留和数据。否则报错。
以下为网络信息API:
Gethostbyname和gethostbyaddr
Gethostbyname函数根据主机名称获取主机的完整信息,gethostbyaddr函数根据IP地址获取主机的完整信息。
#include<netdb.h>
Struct hostent * gethostbyname(const char* name);
Struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
Name参数指定目标主机名,addr参数目标主机IP地址,len参数指定addr所指的IP地址长度,type类型,包括AF_INET和AF_INET6(用于IPv6);
#include<netdb.h>
Struct hostent
{
Char* h_name;
Char** h_aliases;//主机别名列表
Int h_addrtype;//地址类型
Int h_length; //地址长度
Char ** h_addr_list;//按网络字节序列出的主机IP地址列表
};
Getservbyname和getservbyport
Getservbyname函数根据名称获取某个服务的完整信息,getservbyport函数根据端口号获取某个服务的完整信息,实际上通过读取/etc/services来获取服务信息。
#include<netdb.h>
Struct servent* getservbyname(const char* name, const char* proto);
Struct servent* getservbyport(int port, const char* proto);
Proto参数指服务类型,tcp,udp,null获取所有类型的服务。
#include<netdb.h>
Struct servent
{
Char* s_name;//服务名称
Char** s_aliases;//服务的别名列表,可能多个
Int s_port;//端口号
Char* s_proto;//服务类型,通常是tcp或者udp
};
这四个函数都是不可重入的,线程不安全的。
Getaddrinfo
函数既能通过主机名获得IP地址
#include<netdb.h>
Int getaddrinfo(const char* hostname, const char* service,const struct
Addrinfo* hints, struct addrinfo** result);
Hostname参数可以接收主机名,IP地址,service接收服务名或十进制字符串,hints参数是应用程序给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制。Hints参数是可以被设置为NULL,表示允许getaddrinfo反馈任何可用的结果,result参数指向一个链表,存储getaddrinfo反馈的结果。
Struct addrinfo
{
Int ai_flags;//见后文
Int ai_family;//地址族
Int ai_socktype;//服务类型,SOCK_STREAM或SOCK_DGRAM
Int ai_protocol;//见后文
Socklen_t ai_addrlen;//socket地址ai_addr的长度
Char* ai_cannonname;//主机的别名
Struct sockaddr* ai_addr;//指向socket地址
Struct addrinfo* ai_next;//指向下一个sockinfo结构的对象
};
Ai_flags成员可以取表中的标志按位或。
AI_PASSIVE,AI_NUMERICHOST,AI_CANONNAME,AI_NUMERICSERV,
AI_V4MAPPED,AI_ALL,AI_ADDRCONFIG。
使用hinst时可以设置其ai_flags,ai_family,ai_socktype和ai_protocal四个字段,其他设置为NULL。
Struct addrinfo hinst;
Struct addrinfo* res;
Bzero(&hisnt,sizeof(hinst));
Hinst.ai_socktype = SOCK_STREAM;
Getaddrinfo(“ernest-laptop”,”daytime”,&hinst,&res);
Getaddrinfo将隐式分配堆内存,在调用结束 后使用
#include<netdb.h>
Void freeaddrinfo(struct addrinfo* res);来释放内存。
Getnameinfo函数通过socket地址同时获得以字符串表示的主机名和服务名。
#include<netdb.h>
Int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host,
Socklen_t hostlen, char* serv, socklen_t servlen, int flags);
Flags参数:
NI_NAMEREQD,NI_DGRAM,NI_NUMERICHOST,NI_NUMERICSERV,
NI_NOFQDN
Getaddrinfo和getnameinfo函数调用返回的错误码
EAI_AGAIN,BAI_BADFLAGS,EAI_FAIL,EAI_FAMILY,EAI_MEMORY,
EAI_NONAME,EAI_OVERFLOW,EAI_SERVICE,EAI_SOCKTYPE,EAI_SYSTEM。
Linux下strerror函数能够将数值错误码errno转换成易读的字符串形式。
#include<netdb.h>
Const char*gai_strerror(int error);