字节序转换函数:
记住:主机字节序(小端字节序,“高字节高地址”)
网络字节序(大端字节序,“高字节低地址”)
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); // converts the unsigned integer hostlong from host byte order to network byte order. uint16_t htons(uint16_t hostshort); // converts the unsigned short integer hostshort from host byte order to network byte order. uint32_t ntohl(uint32_t netlong); // converts the unsigned integer netlong from network byte order to host byte order. uint16_t ntohs(uint16_t netshort); // converts the unsigned short integer netshort from network byte order to host byte order. 这里,长整型函数通常用于转换IP地址,而短整型函数通常用于转换port。任何格式化的数据通过网络传输时,都应该通过这些函数转换字节序。
专用socket地址
通用的socket地址结构体十分不好用,设置IP与port都需要复杂的位操作。所以Linux为各个协议族制定了专门的socket地址结构体。
下面展示了IPv4的专用socket地址结构体:🐤
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ }; 注意:所有专用socket地址结构体类型的变量在实际使用是需要转换成通用socket地址类型sockaddr(强制转换即可),因为所有的socket编程接口使用的地址参数类型都是sockaddr。
IP地址转换函数
由于人们使用的是点分十进制来记录IPv4,用十六进制字符串表示IPv6的,但是编程中我们要将其转换成二进制才可使用,记录日志正相反。
#include <arpa/inet.h> int inet_pton(int af, const char *src, void *dst); //inet_pton - convert IPv4 and IPv6 addresses from text to binary form DESCRIPTION /*This function converts the character string src into a network address structure in the af address family, then copies the network address structure to dst. The af argument must be either AF_INET or AF_INET6.*/ const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); //inet_ntop - convert IPv4 and IPv6 addresses from binary to text form DESCRIPTION /*This function converts the network address structure src in the af address family into a character string. The resulting string is copied to the buffer pointed to by dst, which must be a non-NULL pointer. The caller specifies the number of bytes available in this buffer in the argument size.*/创建socket🤙
NAME socket - create an endpoint for communication SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); DESCRIPTION //socket() creates an endpoint for communication and returns a descriptor. domain:哪个底层协议族?AF_INET、AF_INET6type:哪个服务?SOCK_STREAM(数据流,TCP)、SOCK_DGRAM(数据报,UDP)protocol:默认为0,因为前两个参数已经足够确定。成功的话,返回一个socket fd;失败的话返回-1并设置errno绑定(命名)socket
SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //bind - bind a name to a socket /*DESCRIPTION When a socket is created with socket(2), it exists in a name space (address family) but has no address assigned to it. bind() assigns the address specified by addr to the socket referred to by the file descriptor sockfd. addrlen specifies the size, in bytes, of the address structure pointed to by addr. Traditionally, this operation is called “assigning a name to a socket”.*/ 如何查看函数定义:比如一个bind(2),就需要在命令行里面敲 man 2 bind ,回车。sockfd:一个创建成功的socket文件描述符;addr:将addr所指的socket地址分配给未命名的sockfd,也就是将IP+port与sockfd绑定addrlen:该socket地址的长度。成功返回0,失败返回1并设置errno。监听socket:设置最大同时连接数,并不阻塞于此
SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); //listen for connections on a socket DESCRIPTION /*listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).*/ socket被绑定之后,还不可以马上接受客户连接,还需要创建一个监听队列存放待处理的客户连接。
sockfd:被监听的socket;backlog:提示内核监听队列的最大长度。成功返回0,失败返回-1并设置errno。接受连接🃏
从listen监听队列中接受一个连接 SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //accept a connection on a socket DESCRIPTION The accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET). /*It extracts the first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket.*/ The newly created socket is not in the listening state. The original socket sockfd is unaffected by this call. The argument sockfd is a socket that has been created with socket(2), bound to a local address with bind(2), and is listening for connections after a listen(2).The argument addr is a pointer to a sockaddr structure. This structure is filled in with the address of the peer socket, as known to the communications layer. The exact format of the address returned addr is determined by the socket’s address family.
The addrlen argument is a value-result argument: the caller must initialize it to contain the size (in bytes) of the structure pointed to by addr; on return it will contain the actual size of the peer address.成功返回一个与新的连接的socket,该socket唯一标识了被接受的这个连接,服务器可以通过读写该socket与被接受连接对应的客户端通信;失败返回-1并设置errno。accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。发起连接💛
服务器是通过listen调用来被动接受连接,客户端则是通过connect主动与服务器建立连接。 NAME connect - initiate a connection on a socket SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); DESCRIPTION The connect() system call connects the socket referred to by the file descriptor sockfd to the address specified by addr. The addrlen argument specifies the size of addr. The format of the address in addr is determined by the address space of the socket sockfd; sockfd是系统调用返回的一个socket;addr:服务器监听的socket地址;addrlen:指定这个地址的长度。成功返回0,一旦成功,sockfd就唯一标识了这个连接,客户端就可以通过读写sockfd来与服务器通信;失败返回-1并设置errno。关闭连接
关闭该连接对应的socket。不过close并非总是立即关闭一个连接,而是将fd引用计数减一,直到为0才算真正关闭。多进程程序中,一次fork使得父进程打开的socket计数加一,因此要在父进程与子进程都对该socket执行close才算关闭连接。 NAME close - close a file descriptor SYNOPSIS #include <unistd.h> int close(int fd); DESCRIPTION close() closes a file descriptor, so that it no longer refers to any file and may be reused. Any record locks (see fcntl(2)) held on the file it was associated with, and owned by the process, are removed (regardless of the file descriptor that was used to obtain the lock).TCP数据读写🚛
对文件的read和write操作同样适用于socket,但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。 NAME recv - receive a message from a socket SYNOPSIS #include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t send(int sockfd, const void *buf, size_t len, int flags); DESCRIPTION The recv() call is normally used only on a connected socket (see connect(2)) and is identical to recvfrom() with a NULL src_addr argu‐ ment. it returns the length of the message on successful completion. recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,成功返回实际读取到的数据长度,可能小于我们期待的len,因此需要多次调用recv,才能收到完整数据。recv可能返回0,意味着连接已关断,出错时返回-1并设置errno。
send往sockfd上写数据,buf和len分别指定写缓冲区的位置和大小。成功返回实际写入的数据长度,失败返回-1并设置errno。
flags默认设置为0即可。
UDP数据读写
SYNOPSIS #include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom(int sockfd, const void *buf, size_t len, int flags, struct sockaddr *dest_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读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,因为UDP没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即dest_addr所指的内容,addrlen指定地址长度;
sendto往sockfd上写入数据,buf和len参数分别指定读缓冲区的位置和大小,dest_addr指定接收方的socket地址,addrlen指定地址长度。
返回值与TCP的一样。
由之前内容可知,socket地址的两大要素是ip 和 Port,但是非常不容易记忆,也不易扩展。所以我们用主机名代替ip,服务名称代替端口号。看下面两条命令
telnet 127.0.0.1 80 telnet localhost www 上述命令中,telnet客户端程序是通过调用某些网络信息API来实现主机名到IP以及服务名称到Port的转换的。下面介绍几个API:
SYNOPSIS #include <netdb.h> extern int h_errno; struct hostent *gethostbyname(const char *name); #include <sys/socket.h> /* for AF_INET */ struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);gethostbyname根据主机名获得主机的完整信息,gethostbyaddr根据ip地址获得主机的完整信息。gethostbyname通常先在本地的 /etc/hosts文件中查找主机,如果没有再去访问DNS服务器。
name:指定目标主机的主机名称;
addr:指定目标主机的ip地址;
len:指定addr所指的ip地址长度;
type:指定addr所指的ip地址类型,包括AF_INET和AF_INET6.
这两个函数返回值都是hostent结构体类型的指针。此结构体定义如下:
#include <netdb.h> struct hostent{ char* h_name; //主机名 char** h_aliases; // 主机别名列表,可能有多个 int h_addrtype; //地址列表(地址族) int h_length; // 地址长度 char** h_addr_list; // 按网络字节序列出的主机IP地址列表 }; SYNOPSIS #include <netdb.h> struct servent *getservbyname(const char *name, const char *proto); struct servent *getservbyport(int port, const char *proto);getservbyname根据名称获得某个服务的完整信息,getservbyport根据端口号获得某个服务的完整信息。它们实际上都是读取 /etc/services 文件来获取服务信息的。
name:指定目标服务的名字;
Port:指定目标服务对应的端口号;
proto:指定服务类型,传“tcp”表示获取流服务,传“udp”表示获取数据报服务,传null表示获取所有类型的服务。
返回值都是servent结构体类型的指针,此结构体定义如下:
#include <netdb.h> struct servent{ char* s_name; //服务类型 char** s_aliases; // 服务别名列表,可能有多个 int s_port; //端口号 char* s_proto; // 服务类型,通常是tcp或者udp }; NAME getaddrinfo - network address and service translation SYNOPSIS #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); getaddrinfo既能通过主机名获得IP(靠gethostbyname),也可以通过服务名称获得端口号(靠的是getservbyname)。
node:可以接收主机名,也可以接收字符串表示的IP地址;
service:可以接收服务名,也可以接收字符串表示的十进制端口号;
hints:可设置为NULL;
result:指向一个链表,该链表用于存储getaddrinfo反馈结果;
getaddrinfo反馈的每一条结果都是addrinfo结构体类型的对象,该结构体的定义如下:
struct addrinfo { int ai_flags; int ai_family; //地址族 int ai_socktype; //服务类型,数据流还是数据报 int ai_protocol; // 通常默认为0; socklen_t ai_addrlen; //socket地址长度 struct sockaddr *ai_addr; //指向socket的地址 char* ai_canonname; // 主机别名 struct addrinfo *ai_next; // 指向下一个socketinfo结构对象 }; NAME getnameinfo - address-to-name translation in protocol-independent manner SYNOPSIS #include <sys/socket.h> #include <netdb.h> int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags); 该函数可以通过socket地址同时获得字符串表示的主机名(gethostbyaddr)和服务名(getservbyport);返回的主机名存储在host参数指向的缓存中,将服务名存储在serv参数指向的缓存中,hostlen和servlen分别指定这两块缓存的长度。
[1]: Linux高性能服务器编程 “游双 注” ; [2]: https://www.bilibili.com/video/BV1iJ411S7UA?from=search&seid=3498472485973927732 “Linux网络编程” [3]: centos7.0系统调用