Linux网络编程(二) socket编程及其仿真

本节内容介绍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
  • 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:想要绑定的地址和端口

  • addrlenmy_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地址

  • addrlenaddr的长度

  • 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_WAITALLrecv 函数只有在读取到指定数据量的子节后才返回

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_controlmsg_controllen成员用于辅助数据的传送。

    • msg_flags成员无须设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。

recvmsg/sendmsgflags参数以及返回值的含义均与send/recvflags参数及返回值相同。

九、带外标记

在实际应用中,我们通常无法预期带外数据何时到来。通常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、仿真结果

fangzhen1231231

十一、地址信息函数

在某些情况下,我们想知道一个连接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_valueoption_len参数分别是被操作选项的值和长度。不同的选项具有不同类型的值。

kalsjdklajs

  • 成功时返回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_RCVBUFSO_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_RCVLOWATSO_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_INETAF_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的行为,

asdklkl

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3015010.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

暖心又实用!母亲节教会妈妈这4招才是最贴心的礼物

母亲节就要到了&#xff0c;这个特殊的日子&#xff0c;我们总是想要为妈妈送上最真挚的祝福和关怀。在这个数字化时代&#xff0c;一部智能手机就能成为我们表达爱意的桥梁。今天&#xff0c;就让我们一起来看看华为手机的四个功能&#xff0c;让妈妈的手机使用体验更加便捷、…

C语言中的关键字static和extern

Hello,亲爱的小伙伴们&#xff0c;我又来了&#xff0c;上一期作者菌讲解了C语言中函数的知识点&#xff0c;得到了很好的反馈&#xff0c;这里作者菌感谢每一个至此我的小伙伴&#xff01;&#xff01;今天作者菌又来补充一些很有用的知识&#xff0c;感兴趣的uu们不要吝啬手中…

Jmeter 命令行压测 生成 HTML 测试报告,你真的会?

通常 Jmeter 的 GUI 模式仅用于调试&#xff0c;在实际的压测项目中&#xff0c;为了让压测机有更好的性能&#xff0c;多用 Jmeter 命令行来进行压测。 同时&#xff0c;JMeter 也支持生成 HTML 测试报告&#xff0c; 以便从测试计划中获得图表和统计信息。 以上定义的文件路…

Elementui的el-footer标签使用报错

Elementui的el-footer标签使用报错 其余标签的使用没有报错信息 el-footer的报错信息 原因: ​ 警告信息表示 Vue 不识别 <el-footer> 解决方式: 在组件中进行引入和暴露

Flume 的安装和使用方法

一、Flume的安装 1.下载压缩包 https://www.apache.org/dyn/closer.lua/flume/1.7.0/apache-flume-1.7.0-bin.tar.gz 2.上传到linux中 3.解压安装包 cd #进入加载压缩包目录sudo tar -zxvf apache-flume-1.7.0-bin.tar.gz -C /usr/local # 将 apache-flume-1.7.0-bin.tar.g…

119. 再谈接口幂等性

文章目录 0. 前言1. insert前先select2. 加悲观锁3. 加乐观锁5. 加唯一索引【配合 &#xff08;1. insert前先select &#xff09;最常用 】6. 建防重表6. 根据状态机7. 加分布式锁8. 获取token 0. 前言 在 93. 通用防重幂等设计 一文中&#xff0c;已经介绍过幂等的使用。该文…

力扣:63. 不同路径 II

63. 不同路径 II 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish”&#xff09;。 现在考虑网格中有障碍物。那么…

C# Web控件与数据感应之 BaseDataList 类

目录 关于数据感应 BaseDataList 类 范例运行环境 pageview 方法 设计 实现 调用示例 数据源 调用 小结 关于数据感应 数据感应也即数据捆绑&#xff0c;是一种动态的&#xff0c;Web控件与数据源之间的交互&#xff0c;本文将继续介绍以与数据库提取数据并捆绑控件…

CI522/CI523电动车NFC一键启动开发资料

Ci522是一颗工作在13.56MHz频率下的非接触式读写芯片&#xff0c;支持读A卡&#xff08;CI523支持读A/B卡&#xff09;&#xff0c;可做智能门锁、电动车NFC一键启动、玩具NFC开锁等应用。为部分要求低成本&#xff0c;PCB小体积的产品提供了可靠的选择。 Ci522与Si522/MFRC52…

第3章 WebServer重构

3.1 重构原生Web服务框架 3.1.1 分析原生Web服务框架 在服务端代码的 ClientHandler 中&#xff0c;请求解析、处理请求、返回响应的代码混杂在一起&#xff0c;这样的设计会导致代码难以维护和理解。为了提高代码的可读性、可维护性和可扩展性&#xff0c;我们需要对这些代码…

UDP广播

1、UDP广播 1.1、广播的概念 广播&#xff1a;由一台主机向该主机所在子网内的所有主机发送数据的方式 例如 &#xff1a;192.168.3.103主机发送广播信息&#xff0c;则192.168.3.1~192.168.3.254所有主机都可以接收到数据 广播只能用UDP或原始IP实现&#xff0c;不能用TCP…

漏洞挖掘 | EDU证书站任意密码重置

1.前言&#xff1a; 挖了一段时间EDU老破小的站&#xff0c;也该拿证书站下手了。下手的第一个目标&#xff0c;那必然是漏洞排行榜第一的某交大&#xff01;&#xff01;&#xff01; 2.信息搜集 想快速挖到漏洞&#xff0c;必须信息搜集全面。如果信息搜集不到位不全面&…

明星中药企业系列洞察(二)丨百年御药同仁堂,为什么被称为我国最“硬”的老字号?

从最初的同仁堂药室、同仁堂药店到现在的北京同仁堂集团&#xff0c;经历了清王朝由强盛到衰弱、几次外敌入侵、军阀混战到新民主主义革命的历史沧桑&#xff0c;其所有制形式、企业性质、管理方式也都发生了根本性的变化&#xff0c;但同仁堂经历数代而不衰&#xff0c;在海内…

蓝桥杯练习系统(算法训练)ALGO-947 贫穷的城市

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述 某城市有n个小镇&#xff0c;编号是1~n。由于贫穷和缺乏城市规划的人才&#xff0c;每个小镇有且仅有一段单向的公路通往别…

[Linux] GDB使用指南----包含CentOS7下安装以及使用

什么是GDB&#xff1f; GDB 是由 GUN 软件系统社区提供的调试工具&#xff0c;同 GCC 配套组成了一套完整的开发环境&#xff0c;GDB 是 Linux 和许多 类Unix系统的标准开发环境。可以用来调试C、C、Go、java、 objective-c、PHP等语言。 GDB的作用 程序启动时&#xff0c;可…

400 Bad Request问题

总结&#xff1a;请求路径写错了 400 问题 原地址&#xff0c;deleteSetmeal的参数应该改为param 更改请求地址正确后即可

视频质量评估

视频质量评估 一、全参考客观视频质量评价方法三、MSSIM四、STRRED五、VMAF六、MOS 一、全参考客观视频质量评价方法 全参考客观视频质量评价方法是指把原始参考视频与失真视频在每一个对应帧中的每一个对应像素之问进行比较。准确的讲&#xff0c;这种方法得到的并不是真正的…

Chromium编译指南2024 Windows11篇-Git工具准备(四)

前言 在《Chromium编译指南2024&#xff08;三&#xff09;》中&#xff0c;我们已经完成了对 Chromium 编译环境的其他相关环境变量的设置&#xff0c; 接下来&#xff0c;我们将进一步探讨如何初始化配置 Git&#xff0c;为获取 Chromium 源代码做好准备。 1. 配置Git 用户…

AI伦理和安全风险管理终极指南

人工智能&#xff08;AI&#xff09;正在迅速改变各个领域的软件开发和部署。驱动这一转变的两个关键群体为人工智能开发者和人工智能集成商。开发人员处于创建基础人工智能技术的最前沿&#xff0c;包括生成式人工智能&#xff08;GenAI&#xff09;模型、自然语言处理&#x…

VBA在Excel中字母、数字的相互转化

VBA在Excel中字母、数字的相互转化 字母转数字的方法 数字转字母的方法 众所周知,Excel表中的行以数字展示,列用字母展示,如下图: 编程时,很多时候需要将列的字母转变为数字使用,如cells(num1,num2).value等,不知大家是怎么将字母转化为数字的,Excel是否有其他方式…