本节内容介绍Linux下进行网络编程所必须得socket接口的一些知识
一、socket地址函数
1.1、主机字节序和网络字节序
现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。
为了避免由于字节序导致的错误,发送端总是将字节序转换为大端字节序后再发送。
需要指出的是,即使是同一台机器上的两个进程(比如一个由C语言编写,另一个由JAVA编写)通信,也要考虑字节序的问题(JAVA虚拟机采用大端字节序)。
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);
1.2、通用socket地址
socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:
#include <bits/socket.h>
struct sockaddr {sa_family_t sa_family;char sa_data[14];
}
sa_family
成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称domain)如下:
#define PF_LOCAL 1 /* Local to host (pipes and file-domain). */
#define PF_UNIX 1 /* POSIX name for PF_LOCAL. */
#define PF_FILE 1 /* Another non-standard name for PF_LOCAL. */
#define PF_INET 2 /* IP protocol family. */
#define PF_INET6 10 /* IP version 6. */
其中 PF_INET
为IPV4,PF_INET6
为IPV6。
sa_data成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度。
PF_UNIX:文件路径名,长度可达108字节
PF_INET:16bit端口号和32bit地址,共6字节
PF_INET6:16bit端口号,32bit流标识,128bit地址,32bit范围ID,共26字节
14字节的sa_data
根本无法完全容纳多数协议族的地址值。因此,Linux定义了下面这个新的通用socket地址结构体:
#include <bits/socket.h>
struct sockaddr_storage {sa_family_t sa_family;unsigned long int __ss_align;char__ss_padding[128-sizeof(__ss_align)];
}
这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)。
1.3、专用socket地址
上面这两个通用socket地址结构体显然很不好用,比如设置与获取IP地址和端口号就需要执行烦琐的位操作。所以Linux为各个协议族提供了专门的socket地址结构体。
UNIX本地域协议族使用如下专用socket地址结构体:
#include <sys/un.h>struct sockaddr_un {sa_family_t sin_family;/*地址族:AF_UNIX*/char sun_path[108];/*文件路径名*/
};
TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于IPv4和IPv6:
#include <netinet/in.h>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地址,要用网络字节序表示*/
};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地址结构体,见下面*/u_int32_t sin6_scope_id; /*scope ID,尚处于实验阶段*/
};struct in6_addr {unsigned char sa_addr[16]; /*IPv6地址,要用网络字节序表示*/
};
这两个专用socket地址结构体各字段的含义都很明确,我们只在右边稍加注释。
所有专用socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr
。
1.4、IP地址转换函数
通常,人们习惯用可读性好的字符串来表示IP地址,比如用点分十进制字符串表示IPv4地址,以及用十六进制字符串表示IPv6地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。下面3个函数可用于字符串表示的IPv4地址和网络字节序整数表示的IPv4地址之间的转换:
#include <arpa/inet.h>// 将点分十进制的IPv4地址字符串转换为in_addr_t类型的整数。
in_addr_t inet_addr(const char*strptr);
// 将点分十进制的IPv4地址字符串转换为struct in_addr结构。
int inet_aton(const char*cp,struct in_addr*inp);
// 将struct in_addr结构转换为其点分十进制的字符串表示。
char* inet_ntoa(struct in_addr in);
二、创建socket
UNIX/Linux的一个哲学是:所有东西都是文件。socket也不例外,它就是可读、可写、可控制、可关闭的文件描述符。下面的socket系统调用可创建一个socket:
#include <sys/types.h>
#include <sys/socket.h>int socket(int domain,int type,int protocol);
-
domain
指定协议族- 对TCP/IP协议族而言,该参数应该设置为
PF_INET
(Protocol Family of Internet,用于IPv4)或PF_INET6
(用于IPv6) - 对于UNIX本地域协议族而言,该参数应该设置为
PF_UNIX
- 对TCP/IP协议族而言,该参数应该设置为
-
type
参数指定服务类型- SOCK_STREAM 流服务,对于TCP/IP协议族表示传输层使用TCP协议
- SOCK_UGRAM 数据报服务,对于TCP/IP协议族表示传输层使用UDP协议
- SOCK_NONBLOCK将新创建的socket创建为非阻塞
- SOCK_CLOEXEC用fork调用创建子进程时在子进程中关闭该socket。
-
protocol
参数是在前两个参数构成的协议集合下,再选择一个具体的协议。几乎在所有情况下,我们都应该把它设置为0,表示使用默认协议 -
调用成功时返回一个socket文件描述符,失败则返回-1并设置errno
三、命名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);
-
sockfd
:想要绑定的套接字的文件描述符 -
my_addr
:想要绑定的地址和端口 -
addrlen
:my_addr
参数指向的结构的长度 -
调用成功时返回0,失败则返回-1并设置errno
-
EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问
-
EADDRINUSE,被绑定的地址正在使用中
-
四、监听socket
socket
被命名之后,还不能马上接受客户连接,需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接:
#include <sys/socket.h>int listen(int sockfd,int backlog);
sockfd
参数指定被监听的socket。backlog
参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog
,服务器将不受理新的客户连接。backlog
只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog
内核参数定义。backlog
参数的典型值是5。- 调用成功时返回0,失败则返回-1并设置errno
五、接受连接
下面的系统调用从listen监听队列中接受一个连接:
#include<sys/types.h>
#include<sys/socket.h>int accept(int sockfd, struct sockaddr* addr,socklen_t* addrlen);
-
sockfd
:执行过listen
系统调用的监听socket -
addr
:获取被接受连接的远端socket地址 -
addrlen
:addr
的长度 -
accept成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。accept失败时返回-1并设置errno。
六、发起连接
如果说服务器通过listen调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接:
#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
:地址的长度。 -
connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno。其中两种常见的errno是ECONNREFUSED和ETIMEDOUT,它们的含义如下:
-
ECONNREFUSED,目标端口不存在
-
ETIMEDOUT,连接超时
-
七、关闭连接
关闭一个连接实际上就是关闭该连接对应的socket,这可以通过如下关闭普通文件描述符的系统调用来完成:
#include <unistd.h>int close(int fd);
fd
参数是待关闭的socket。不过,close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1。只有当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 关闭sockfd上读的这一半,应用程序不能再针对socket文件描述符执行读操作。
-
SHUT_WR 关闭sockfd上写的这一半,应用程序不能再针对socket文件描述符执行写操作。
-
SHUT_RDWR 同时关闭sockfd上的读与写。
-
-
shutdown成功时返回0,失败则返回-1并设置errno。
八、数据读写
8.1、TCP数据读写
对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:
#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);
sockfd
:要读取或写入的socket文件描述符buf
:要写入或缓存的地址len
:要写入的长度,或准备缓存的缓存空间长度flags
:通常为0。下面列出一些可选项- MSG_DONTWAIT:此次操作是非阻塞的
- MSG_OOB:发送或接收带外数据(比较紧急的数据,优先级较高)
- MSG_PEEK:窥视消息。使
recv
函数可以读取数据而不从输入队列中移除它 - MSG_WAITALL:
recv
函数只有在读取到指定数据量的子节后才返回
8.2、UDP数据读写
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);
sockfd
:套接字描述符。buf
:指向接收数据缓冲区的指针。len
:缓冲区的大小。flags
:接收操作的标志(与recv/send
函数的flags
参数类似)。src_addr
:存储发送方的地址信息,UDP通信没有连接的概念,每次读取数据都需要获取发送端的socket地址,发送数据也一样。addrlen
:地址的长度
值得一提的是,recvfrom/sendto
也可以用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因为我们已经和对方建立了连接,所以已经知道其socket地址了)。
8.3、通用数据读写
socket编程接口还提供了一对通用的数据读写系统调用。它们不仅能用于TCP流数据,也能用于UDP数据报:
#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
:目标socket -
msg
:msghdr结构体类型的指针,msghdr结构体的定义如下: -
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参数,并在调用过程中更新*/};
-
msg_name
成员指向一个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被设置为NULL。这是因为对数据流socket而言,对方的地址已经知道。 -
msg_namelen
成员则指定了msg_name所指socket地址的长度。 -
msg_iov
成员是iovec结构体类型的指针,iovec结构体的定义如下:-
struct iovec {void* iov_base;/*内存起始地址*/size_t iov_len;/*这块内存的长度*/};
-
-
msg_iovlen
指定这样的iovec
结构对象有多少个。 -
msg_control
和msg_controllen
成员用于辅助数据的传送。 -
msg_flags
成员无须设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。
-
recvmsg/sendmsg
的flags
参数以及返回值的含义均与send/recv
的flags
参数及返回值相同。
九、带外标记
在实际应用中,我们通常无法预期带外数据何时到来。通常Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是:I/O复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这一点可通过如下系统调用实现:
#include <sys/socket.h>int sockatmark(int sockfd);
sockatmark
判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。如果是,sockatmark
返回1,此时我们就可以利用带MSG_OOB标志的recv
调用来接收带外数据。如果不是,则sockatmark
返回0。
十、仿真
10.1、服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> #define PORT 8080
#define BUFFER_SIZE 20 int main() { int server_socket, client_socket; struct sockaddr_in server_addr, client_addr; socklen_t client_addr_len = sizeof(client_addr); char buffer[BUFFER_SIZE]; ssize_t bytes_read; // 创建socket server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket == -1) { perror("socket creation failed"); exit(EXIT_FAILURE); } // 命名socket memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = INADDR_ANY; if (bind(server_socket, (struct sockaddr *) &server_addr, sizeof(server_addr)) == -1) { perror("bind failed"); close(server_socket); exit(EXIT_FAILURE); } // 监听socket if (listen(server_socket, 5) == -1) { perror("listen failed"); close(server_socket); exit(EXIT_FAILURE); } printf("Server is listening on port %d...\n", PORT); while (1) { // 接受客户端连接 client_socket = accept(server_socket, (struct sockaddr *) &client_addr, &client_addr_len); if (client_socket == -1) { perror("accept failed"); continue; } printf("Client connected from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 接收消息 bytes_read = recv(client_socket, buffer, BUFFER_SIZE - 1, 0); if (bytes_read == -1) { perror("recv failed"); } else if (bytes_read == 0) { printf("Client disconnected\n"); } else { buffer[bytes_read] = '\0'; // 确保字符串以null结尾 printf("Received from client: %s\n", buffer); // 返回给客户端一个信息 const char *response = "Hello client!\n"; send(client_socket, response, strlen(response), 0); } // 关闭客户端套接字 close(client_socket); } // 关闭服务器套接字 close(server_socket); return 0;
}
10.2、客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> #define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024 int main() { int client_socket; struct sockaddr_in server_addr; char buffer[BUFFER_SIZE]; ssize_t bytes_read; // 创建socket client_socket = socket(AF_INET, SOCK_STREAM, 0); if (client_socket == -1) { perror("socket creation failed"); exit(EXIT_FAILURE); } // 设置服务器地址信息 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) { perror("invalid server address"); close(client_socket); exit(EXIT_FAILURE); } // 连接到服务器 if (connect(client_socket, (struct sockaddr *) &server_addr, sizeof(server_addr)) == -1) { perror("connection failed"); close(client_socket); exit(EXIT_FAILURE); } printf("Connected to server\n"); // 发送消息给服务器 const char *message = "Hello server!\n";send(client_socket, message, strlen(message), 0); // 接收消息bytes_read = recv(client_socket, buffer, BUFFER_SIZE - 1, 0); buffer[bytes_read] = '\0'; // 确保字符串以null结尾 printf("Received from Server: %s\n", buffer); // 关闭连接close(client_socket);
}
10.3、仿真结果
十一、地址信息函数
在某些情况下,我们想知道一个连接socket的本端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
获取sockfd对应的本端socket地址,getsockname
成功时返回0,失败返回-1并设置errno。
getpeername
获取sockfd对应的远端socket地址,getpeername
成功时返回0,失败返回-1并设置errno。
十二、socket属性设置
如果说fcntl
系统调用是控制文件描述符属性的通用POSIX方法,那么下面两个系统调用则是专门用来读取和设置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
:指定协议,eg:IPV4 -
option_name
:指定选项 -
option_value
和option_len
参数分别是被操作选项的值和长度。不同的选项具有不同类型的值。
- 成功时返回0,失败时返回-1并设置errno。
对服务器而言,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效。这是因为连接socket只能由accept调用返回,而accept从listen监听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤(因为listen监听队列中的连接至少已进入SYN_RCVD状态),这说明服务器已经往被接受连接上发送出了TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设置,比如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三次握手已完成。
12.1、SO_REUSEADDR占用wait状态地址
选项SO_REUSEADDR
强制使用被处于TIME_WAIT
状态的连接占用的socket地址
int sock=socket(PF_INET, SOCK_STREAM, 0);
// 设置了 reuse 变量为 1,用于启用地址重用功能。
int reuse=1;
// 设置了套接字选项 SO_REUSEADDR
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
// 定义ipv4结构体地址
struct sockaddr_in address;
// 将 address 结构体清零,以确保其中的字段没有残留值
bzero(&address,sizeof(address));
// 设置地址族为 IPv4
address.sin_family=AF_INET;
// 将字符串格式的 IP 地址转换为网络字节序的二进制形式,并存储在 address.sin_addr 中
inet_pton(AF_INET,ip,&address.sin_addr);
// 设置服务器端口号,并将其转换为网络字节序
address.sin_port=htons(port);
// 将套接字与指定的 IP 地址和端口绑定
int ret=bind(sock,(struct sockaddr*)&address,sizeof(address));
设置后即使sock处于TIME_WAIT状态,与之绑定的socket地址也可以立即被重用。
12.2、SO_RCVBUF和SO_SNDBUF修改缓冲区
SO_RCVBUF
和SO_SNDBUF
选项分别表示TCP接收缓冲区和发送缓冲区的大小。系统都会将设置值加倍,并且不小于某个值。Linux下TCP接收缓冲区的最小值是256字节,而发送缓冲区的最小值是2048字节。
可以直接修改内核参数/proc/sys/net/ipv4/tcp_rmem
和/proc/sys/net/ipv4/tcp_wmem
来强制TCP接收缓冲区和发送缓冲区的大小没有最小值限制。
// 修改发送缓冲区大小
int sendbuf = 4096;
int len = sizeof(sendbuf);
setsockopt(server_fd, SOL_SOCKET, SO_SNDBUF, &sendbuf, len);
getsockopt(server_fd, SOL_SOCKET, SO_SNDBUF, &sendbuf, (socklen_t*)& len);
12.3、SO_RCVLOWAT和SO_SNDLOWAT缓冲区低水位标记
SO_RCVLOWAT
和SO_SNDLOWAT
选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。它们一般被I/O复用系统调用用来判断socket是否可读或可写。当TCP接收缓冲区中可读数据的总数大于其低水位标记时,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据;当TCP发送缓冲区中的空闲空间(可以写入数据的空间)大于其低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socke上写入数据。
Linux下,TCP接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1字节。
12.4、SO_LINGER修改close行为
SO_LINGER
选项用于控制close系统调用在关闭TCP连接时的行为。默认情况下,当我们使用close系统调用来关闭一个socket时,close将立即返回,TCP模块负责把该socket对应的TCP发送缓冲区中残留的数据发送给对方。
设置(获取)SO_LINGER选项的值时,我们需要给setsockopt(getsockopt)系统调用传递一个linger类型的结构体,其定义如下:
#include <sys/socket.h>struct linger {int l_onoff; /*开启(非0)还是关闭(0)该选项*/int l_linger; /*滞留时间*/
};
根据linger结构体中两个成员变量的不同值,close系统调用可能产生如下3种行为之一
-
l_onoff等于0。此时SO_LINGER选项不起作用,close用默认行为来关闭socket。
-
l_onoff不为0,l_linger等于0。此时close系统调用立即返回,TCP模块将丢弃被关闭的socket对应的TCP发送缓冲区中残留的数据,同时给对方发送一个复位报文段。因此,这种情况给服务器提供了异常终止一个连接的方法。
-
l_onoff不为0,l_linger大于0。此时close的行为取决于两个条件:一是被关闭的socket对应的TCP发送缓冲区中是否还有残留的数据;二是该socket是阻塞的,还是非阻塞的。对于阻塞的socket,close将等待一段长为l_linger的时间,直到TCP模块发送完所有残留数据并得到对方的确认。如果这段时间内TCP模块没有发送完残留数据并得到对方的确认,那么close系统调用将返回-1并设置errno为EWOULDBLOCK。如果socket是非阻塞的,close将立即返回,此时我们需要根据其返回值和errno来判断残留数据是否已经发送完毕。
十三、网络信息API
socket地址的两个要素,即IP地址和端口号,都是用数值表示的。这不便于记忆,也不便于扩展(比如从IPv4转移到IPv6)。因此在前面的章节中,我们用主机名来访问一台机器,而避免直接使用其IP地址。同样,我们用服务名称来代替端口号。
13.1、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
:IP地址的长度 -
type
:addr所指IP地址的类型,eg: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地址列表*/};
13.2、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);
-
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*/};
-
需要指出的是,上面讨论的4个函数都是不可重入的,即非线程安全的。它们的可重入版本正如Linux下所有其他函数的可重入版本那样,在原函数名尾部加上_r
。
13.3、getaddrinfo获取ip地址
getaddrinfo
函数既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。它是否可重入取决于其内部调用的gethostbyname和getservbyname函数是否是它们的可重入版本。该函数的定义如下:
#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反馈的结果。 -
getaddrinfo
反馈的每一条结果都是addrinfo结构体类型的对象,结构体addrinfo的定义如下:-
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_canonname;/*主机的别名*/struct sockaddr*ai_addr;/*指向socket地址*/struct addrinfo*ai_next;/*指向下一个sockinfo结构的对象*/}
-
getaddrinfo将隐式地分配堆内存,所以,getaddrinfo调用结束后,我们必须使用如下配对函数来释放这块内存:
#include <netdb.h>void freeaddrinfo(struct addrinfo* res);
13.4、getnameinfo获取主机名
getnameinfo
函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。它是否可重入取决于其内部调用的gethostbyaddr和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);
getnameinfo将返回的主机名存储在host参数指向的缓存中,将服务名存储在serv参数指向的缓存中,hostlen和servlen参数分别指定这两块缓存的长度。flags参数控制getnameinfo的行为,