Linux C 网络基础编程

基础知识

在进行网络编程之前,我们需要简单回顾一下计算机网络五层模型的网络层和传输层,这两层在面向后端编程时用的最多。物理层和链路层过于底层,已经完全由内核协议栈实现,不再细述。

这里假设读者已经对计算机网络有一个大致的了解。

网络层

IP协议是四层模型中的核心协议,所有TCP、UDP、ICMP数据都通过IP数据报传输。IP提
供一种尽力而为(就是)、无连接的数据报交付服务。不可靠意味着如果传递过程不可靠中出现差错,IP层可以选择丢弃数据,并且不会主动重传;无连接意味着IP协议不会记录传递过程中的路径,那同样的两端发生的不同数据报可能会走不同的路径,并且有可能不按顺序到达。

IP地址以及其分类

IP地址用来区分不同的主机在网络层中的位置。IPv4的地址长度为32位,为了方便描述,通常将其按8位一组分隔,并用.号隔开,这种就是点分十进制,比如192.168.1.1。IPv6的地址长度是128位,一般用8个4位十六进制描述,每个十六进制数描述一个段。IPv6的应用可以解决IP地址缺乏的问题,但是随着NAT技术的广泛使用,IPv4目前实际上还是占据了大部分市场。
在早期,每个IP地址会分为两部分,高位是网络号,低位是主机号,并且根据网络号的前缀,将其分为5类地址,A、B、C、D(这是用于组播的)和E(这是保留的地址)类,这种分类方式除了规定了网络号的前缀,还是划定了网络号和主机号的长度。随着Internet逐渐发展,这种死板的分类方式已经不适应人们的需求。一种相对自由的划分方式就是采用子网机制:把主机号的前缀作为子网ID,剩余部分作为主机ID,主机ID的长度由本地网络管理员自行划定。

子网掩码可以用来描述主机ID的长度,其长度和IP地址一样是32,其中高位部分全为1,剩余的部分全为0。前缀部分的长度说明网络号和子网ID的长度,剩余部分自然就是主机ID了。子网掩码可以用在局域网内为路由器的路由决策作出参考,而局域网外的路由决策则只和网络号部分有关。我们可以用点分十进制来描述子网掩码(比如255.255.255.0),或者在IP地址的后缀中说明网络号和子网ID的总长度(比如192.168.3.0/24)

 在每个IPv4子网中,主机部分全为1的IP地址被保留为本地广播地址,比如子网为128.32.1.0/24的子网广播地址是128.32.1.255。除此以外,特殊地址255.255.255.255被保留为本地广播地址,它不会被路由器转发,通常配合UDP/IP和ICMP等协议使用。
随着CIDR技术的引入,传统的5类分类方式被废弃了,IP地址不再按照固定的长度进行划分,在分配IP地址时,除了要指定分配的网络号,还需要说明网络号的长度,可以采用类似子网掩码的方式进行描述。
下面是一些常见的用于特殊用途的IP地址:

前缀用途
0.0.0.0/8作为源地址时表示本地主机||作为目的地址时,表示任意IP地址
10.0.0.0/8局域网IP地址
172.16.0.0/12局域网IP地址
192.168.0.0/16局域网IP地址
127.0.0.0/8回环地址
169.254.0.0/16链路本地地址,通常出现在DHCP自动分配IP未完成时
255.255.255.255/32本地网络广播地址

IP数据报的结构

正常的IPv4头部大小为20个字节(在很少情况会拥有选项,此时头部不只20个字节),IP头部的传输是按照大端法进行的,对于一个32位值而言,首先传输高位8位,然后次高8位,以此类推。因为TCP/IP协议中所有协议的头部都采用大端法进行传输,所以大端法也网络字节序称作。由于大部分PC使用的是小端法,所以在构造完头部之后传输之前需要对其执行大小端转换才行。下图当中描述IPv4中IP数据报的格式。

  • 版本:4位,数值4指IPv4,6指IPv6。
  • 头部字段:4位,用来描述IP数据报头部的长度为多少32位。因此IP数据报头部最多只有60个字节。
  • 服务类型:8位,描述服务质量和拥塞情况
  • 总长度:16位,描述IP数据报的总长度(包括头部)为多少字节。这个字段有助于从带填充的以太网帧取出IP数据报的有效部分(可能这个IP数据报不足46个字节,在以太网帧当中填充了0)。
  • 标识:16位,描述IP数据报的分片编号,同一个大IP数据报分解出来的多个分片拥有相同的标识。
  • 标志:3位,描述是否发生分片,以及是否后续有更多的分片。
  • 片偏移:13位,描述该分片在重组后的大IP数据报当中的位置,以8字节为单位。生存期(TTL):8位,描述一个数据报可以经过路由器的上限,每次路由器转发时该数值会减一。这个属性可以避免在环形路由情况下,数据报在网络中永远循环。
  • 协议:8位,描述上层协议的类型,最常见的是1(ICMP)、17(UDP)和6(TCP)首部校验和:16位,IP数据报头部的校验和,注意并不检查载荷部分内容,所以需要上层协议自己检查。
  • 源IP地址和目的IP地址:各有32位,描述IP数据报的发送者和接收者的IP地址。

 分片和重组

由于IP数据报的总长度限制为65535字节,这远远超过了部分链路层标准的MTU,当数据从网络层准备转移到数据链路层时,网络层会将IP数据报进行分片操作,将分解成若干个独立的IP数据报(分解之后IP数据的总长度字段改变了),并且在网络之间独立传输。一旦到达终点的目的主机之后(中间不会重组),目的主机的网络层会将分片重组成一个大IP数据报。由于重组的过程十分复杂,所以协议设计者应该尽可能避免出现让IP数据报超过MTU的情况。比如DNS、DHCP等就规定UDP报文长度为512字节。

传输层

传输控制协议TCP

传输控制协议(TCP)是整个四层模型当中最重要的协议,它工作在传输层,其目标是在不可靠的逐跳传输的网络层之上,构建一个可靠的、面向连接的、全双工的端到端协议。

建立连接的三次握手

TCP是一个面向连接的协议,在通信双方真正交换数据之前,必须先先相互联系建立一个TCP连接,这个就类似于电话的开头的“喂”的效果。TCP是一个全双工协议,双方都需要对连接状态进行管理。每一个独特的TCP都由一个四元组唯一标识,组内包括通信双方的IP地址和端口号,建立连接的过程通常被称作是3次握手。虽然是全双工的通信,但是有一次建立连接和确认行为可以合并在一起,所以只需要传输3次报文段即可。下面是其步骤:

  • 客户端发起连接,发送一个SYN报文给服务端,然后说明自己连接的端口和客户端初始序列号seq1。
  • 服务端收到SYN报文,也需要发起反方向的连接,所以发送一个SYN报文给服务端,说明自己连接的端口和服务端初始序列号seq2,除此以外,这个报文还可以携带一个确认信息,所以把seq1+1作为ACK返回。
  • 客户端收到服务端的SYN之后,需要确认,所以把seq2+1作为ACK返回给服务端。同时,本次发送的报文可以携带数据。

3次握手最主要的目的是为了建立连接,并且交换初始序列号。在TCP连接过程中,如果存在一个老旧的报文(上一次连接时发送的)到达服务端,服务端可以根据其序列号是否合法来决定是否丢弃。有些情况可能出现双方同时发起连接的情况,这个时候就需要4个报文段来建立连接了。

使用2次握手可不可行?

答案是否定的,因为服务端发起的SYN未确认。一种典型的场景就是客户端发起SYN,第一个SYN超时并重传,第二个SYN到达并建立连接,之后再完成连接并关闭,倘若关闭之后,第一个SYN到达服务端,此时服务端就会认为对方建立连接,并回复SYN+ACK,由于没有确认,所以服务端并不知道客户端的状态,此时客户端完全可能已经关闭,那服务端就会陷入永久等待了

断开连接的四次挥手

断开连接的过程要更加复杂一些,通信双方谁都可以主动断开连接,但是由于TCP是全双工连接,所以一个方向断开并不意味着反向的数据已经传输完成,所以每个方向的断开是相对独立的,这样的话两个方向各有一次断开和确认,总共需要4次报文段的传输,即4次挥手。下面是其具体流程:

  • 主动关闭方发送一个FIN段表示希望断开连接。
  • 被动关闭方收到FIN段,并且回复一个确认信息。其上层应用会收到一个EOF,被动关闭方继续传输剩余的数据。
  • 被动关闭方发送完数据了,发送一个FIN段。
  • 主动关闭方回复一个确认,并且等待一段时间(2MSL,MSL指单个报文在网络中的最长生存时间)。
  • 在第2次挥手之后,TCP连接此时处于一种半关闭的状态。可以任为现在是一个单工通信方式(被动关闭方-->主动关闭方)。
报文头部

上图中描述了TCP报文首部的各个字段,具体含义如下:

  • 序号:即SEQ的值
  • 确认需要:即ACK的值,描述预期接收的下一个序列号。注意发送一个ACK和发送一个普通报文的消耗是一样的。
  • 首部长度:首部的长度是可变的,以32位为单位。首部长度最短为20字节,最长为60字节。
  • URG:紧急。
  • ACK:确认号字段有效。连接建立以后通常一直有效。
  • PSH:推送。
  • RST:重置连接,出现在连接出错时。
  • SYN:发起连接。
  • FIN:发起关闭。
  • 窗口大小:通告一个窗口大小以限制流量。
  • 校验和:校验传输中的比特跳变错误。
  • 紧急指针:向对端提供一种特殊标识。
  • 最大段大小(MSS),用来描述后续希望接收到的报文段
  • 选项:最常见的选项是
  • 的最大值,这个数值通常受限于MTU,比如MTU为1500,IP数据报头部为20字节,TCP头部为20字节,则MSS是1460。

用户数据报协议UDP

UDP是一种保留消息边界的简单的面向数据报的传输层协议。它不提供差错纠正、流量控制和拥塞管理等功能,只提供差错校验,但是一旦发现错误也只是简单地丢弃报文,不会通知对端,更不会有重传。由于功能特别简单,所以UDP的实现和运行消耗特别地小,故UDP协议可以配合一些应用层协议实现在一些低质量网络信道上的高效传输。许多早期的聊天软件或者客户端游戏都采用了基于UDP的应用层协议,这样能最好地利用性能,同时在比较差的网络状态下提供更良好的服务。

报文头部

UDP的报文结构非常简单:

  • 长度:指UDP报文的总长度(包括UDP头部),实际上这个长度是冗余的,报文长度可以根据IP报文长度计算而来。
  • 校验和:用于最终目的方校验,出错的报文会直接丢弃。

Berkeley Socket

TCP/IP协议族标准只规定了网络各个层次的设计和规范,具体实现则需要由各个操作系统厂商完成。最出名的网络库由BSD 4.2版本最先推出,所以称作,这些API随后被移植到各大操作系统中,并成为了网络编程的事实标准。 socket 即套接字是指网络中伯克利套接字
一种用来建立连接、网络通信的设备,用户创建了 socket 之后,可以通过其发起或者接受TCP 连接、可以向 TCP 的发送和接收缓冲区当中读写TCP数据段,或者发送 UDP 文本。

地址信息设置

struct sockaddr

我们主要以IPv4为例介绍网络的地址结构。主要涉及的结构体有 struct in_addr 、 struct sockaddr 、 struct sockaddr_in 。其中 struct sockaddr 是一种通用的地址结构,它可以描述一个IPv4或者IPv6的结构,所有涉及到地址的接口都使用了该类型的参数,但是过于通用的结果是直接用它来描述一个具体的IP地址和端口号十分困难。所以用户一般先使用struct sockaddr_in 来构造地址,再将其进行强制类型转换成 struct sockaddr 以作为网络接口的参数。

sockaddr_in

struct sockaddr_in 是一个在 C 语言中用于网络编程的结构体,它主要用于表示 IPv4 地址和端口号。

struct sockaddr_in {uint8_t sin_len;         // 地址长度(可选字段,不是所有平台都使用)sa_family_t sin_family;  // 地址族uint16_t sin_port;       // 端口号struct in_addr sin_addr; // IPv4 地址char sin_zero[8];        // 填充字段,用于对齐
};

字段说明

  • sin_len

    • 类型uint8_t(无符号8位整数)

    • 作用:指定结构体的长度(以字节为单位)。这个字段在某些系统(如某些版本的 BSD 系统)中是必需的,但在大多数现代系统中(如 Linux)通常不使用。

    • :如果使用,通常设置为 sizeof(struct sockaddr_in)

  • sin_family

    • 类型sa_family_t(通常是无符号16位整数)

    • 作用:指定地址族,用于标识地址类型。

      • AF_INET:表示 IPv4 地址族(这是最常见的值)。

      • 其他值(如 AF_UNIXAF_INET6)通常不用于 sockaddr_in 结构体。

  • sin_port

    • 类型uint16_t(无符号16位整数)

    • 作用:表示网络端口号。

    • :端口号以网络字节序(大端序)存储。在使用时,通常需要通过 htons() 函数将主机字节序转换为网络字节序,例如:

struct sockaddr_in addr;
addr.sin_port = htons(80); // 将主机字节序的80转换为网络字节序

 sin_addr

  • 类型struct in_addr

  • 作用:表示 IPv4 地址。

  • 结构

struct in_addr {uint32_t s_addr; // IPv4 地址
};

s_addr:IPv4 地址,以网络字节序存储。可以使用 inet_addr()inet_pton() 函数将点分十进制字符串(如 "192.168.1.1")转换为网络字节序的整数,例如:

struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("192.168.1.1");
  • sin_zero
  • 类型char[8]
  • 作用:填充字段,用于对齐结构体。在实际使用中,通常不需要手动设置这个字段,它主要用于确保结构体的大小和对齐方式与 struct sockaddr 一致。

大小端转换

网络字节序即大端法。对于应用TCP/IP协议规定,当数据在网络中传输的时候,一律使用层协议的载荷部分,如果不需要第三方工具检测内容,可以不进行大小端转换(因为接收方和发送方都是主机字节序即小端法)。但是对于其他层次的头部部分,在发送之前就一定要进行小端到大端的转换了(因为网络中的通信以及 tcpdump 、 netstat 等命令都是以大端法来解析内容的)。
下面是整数大小端转换相关的函数。 

字节序概述

  1. 主机字节序(Host Byte Order)

    • 依赖于具体计算机系统的架构。

    • 在大多数现代计算机系统(如 x86 和 x86_64 架构的机器)中,主机字节序是小端序(Little-Endian),即低位字节存储在低地址处。

    • 在某些系统(如某些 PowerPC 架构的机器)中,主机字节序是大端序(Big-Endian)。

  2. 网络字节序(Network Byte Order)

    • 标准的网络通信协议(如 TCP/IP)使用大端序(Big-Endian),即高位字节存储在低地址处。

    • 这种字节序在跨平台网络通信中确保数据的一致性。

htonl
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
  • 作用:将32位的主机字节序整数转换为网络字节序。

  • 参数

    • hostlong:一个32位的无符号整数,表示主机字节序的值。

  • 返回值

    • 返回转换后的32位无符号整数,表示网络字节序的值。

  • 使用场景

    • 在发送32位数据(如IP地址)到网络之前,需要将其从主机字节序转换为网络字节序。一般用于转换IP地址

htons
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
  • 作用:将16位的主机字节序整数转换为网络字节序。

  • 参数

    • hostshort:一个16位的无符号整数,表示主机字节序的值。

  • 返回值

    • 返回转换后的16位无符号整数,表示网络字节序的值。

  • 使用场景

    • 在发送16位数据(如端口号)到网络之前,需要将其从主机字节序转换为网络字节序。一般用于转换端口号。

int main() {uint16_t port = 80; // 主机字节序的端口号uint16_t net_port = htons(port); // 转换为网络字节序(小端转大端)uint32_t ip = 0xC0A80101; // 主机字节序的 IP 地址(192.168.1.1)uint32_t net_ip = htonl(ip); // 转换为网络字节序printf("Host port: %d\n", port);printf("Network port: %d\n", net_port);printf("Host IP: 0x%X\n", ip);printf("Network IP: 0x%X\n", net_ip);return 0;
}
ntohl
#include <arpa/inet.h>
uint32_t ntohl(uint32_t netlong);
  • 作用:将32位的网络字节序整数转换为主机字节序。

  • 参数

    • netlong:一个32位的无符号整数,表示网络字节序的值。

  • 返回值

    • 返回转换后的32位无符号整数,表示主机字节序的值。

  • 使用场景

    • 在从网络接收32位数据(如IP地址)后,需要将其从网络字节序转换为主机字节序。一般用于转换IP地址。

ntohs
#include <arpa/inet.h>
uint16_t ntohs(uint16_t netshort);
  • 作用:将16位的网络字节序整数转换为主机字节序。

  • 参数

    • netshort:一个16位的无符号整数,表示网络字节序的值。

  • 返回值

    • 返回转换后的16位无符号整数,表示主机字节序的值。

  • 使用场景

    • 在从网络接收16位数据(如端口号)后,需要将其从网络字节序转换为主机字节序。一般用于转换端口号

示例:

int main() {uint32_t hostlong = 0x12345678; // 主机字节序的32位整数uint16_t hostshort = 0x1234;   // 主机字节序的16位整数// 转换为主机字节序到网络字节序uint32_t netlong = htonl(hostlong);uint16_t netshort = htons(hostshort);printf("Host long: 0x%X\n", hostlong);printf("Network long: 0x%X\n", netlong);printf("Host short: 0x%X\n", hostshort);printf("Network short: 0x%X\n", netshort);// 转换为网络字节序到主机字节序uint32_t converted_hostlong = ntohl(netlong);uint16_t converted_hostshort = ntohs(netshort);printf("Converted host long: 0x%X\n", converted_hostlong);printf("Converted host short: 0x%X\n", converted_hostshort);return 0;
}

IP地址转换

inet_aton

将点分十进制字符串(如 "192.168.1.1")转换为二进制形式的 IPv4 地址,并存储在 struct in_addr 结构体中。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);

参数

  • cp:指向点分十进制字符串的指针(如 "192.168.1.1")。

  • inp指向 struct in_addr 结构体的指针,用于存储转换后的二进制 IPv4 地址。

返回值

  • 成功时返回 1

  • 失败时返回 0(例如,输入的字符串格式不正确)。

inet_ntoa

将二进制形式的 IPv4 地址(存储在 struct in_addr 结构体中)转换为点分十进制字符串。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);

参数

  • in:包含二进制 IPv4 地址的 struct in_addr 结构体。

返回值

  • 返回一个指向静态分配的字符串的指针,该字符串表示点分十进制形式的 IPv4 地址。

  • 注意:返回的字符串是静态分配的,因此每次调用 inet_ntoa 都会覆盖之前的返回值。如果需要保存结果,建议将其复制到其他变量中。

示例:

#include <stdio.h>
#include <arpa/inet.h>int main(int argc,char* argv[]){//./inet_aton 127.0.0.1struct sockaddr_in addr;inet_aton(argv[1],&addr.sin_addr);//将点分十进制转换成32位网络字节序printf("addr = %x\n",addr.sin_addr.s_addr);printf("addr = %s\n",inet_ntoa(addr.sin_addr));//将32位网络字节序转换成点分十进制return 0;
}
/*输出结果
(base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./ip 192.168.1.1
addr = 101a8c0
addr = 192.168.1.1
*/

注意事项

线程安全性

inet_ntoa 不是线程安全的,因为它返回的是静态分配的字符串。如果需要在多线程环境中使用,建议使用 inet_ntop 函数,它允许指定目标缓冲区。

inet_pton

将点分十进制字符串(IPv4)或冒号十六进制字符串(IPv6)转换为二进制形式的 IP 地址。

int inet_pton(int af, const char *src, void *dst);
  • af:地址族,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)。

  • src:指向点分十进制(IPv4)或冒号十六进制(IPv6)字符串的指针。

  • dst:指向存储转换后的二进制 IP 地址的缓冲区。

返回值

  • 成功时返回 1

  • 输入字符串无效时返回 0

  • 地址族不支持时返回 -1

inet_ntop

将二进制形式的 IP 地址转换为点分十进制字符串(IPv4)或冒号十六进制字符串(IPv6)。

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • 参数

    • af:地址族,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)。

    • src:指向二进制 IP 地址的指针。

    • dst:指向存储转换后的字符串的缓冲区。

    • size:缓冲区的大小。

  • 返回值

    • 成功时返回指向 dst 的指针。

    • 失败时返回 NULL

示例:

int main(int argc,char* argv[]){struct sockaddr_in addr;inet_pton(AF_INET ,argv[1],&addr.sin_addr);//将点分十进制转换成32位网络字节序printf("addr = %x\n",addr.sin_addr.s_addr);char ip_addr[20];printf("addr = %s\n",inet_ntop(AF_INET, &addr.sin_addr.s_addr, ip_addr, 20));//将32位网络字节序转换成点分十进制return 0;
}
/*
输出结果:
(base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./ip2 192.168.1.1
addr = 101a8c0
addr = 192.168.1.1
*/

inet_addr 

用于将点分十进制的 IP 地址字符串(如 "192.168.1.1")转换为一个 32 位的二进制形式的 IPv4 地址。

uint32_t inet_addr(const char *cp);

 参数

  • cp

    • 类型const char *

    • 作用:指向点分十进制格式的 IPv4 地址字符串(如 "192.168.1.1")。

返回值

  • 成功时返回 32 位的二进制形式的 IPv4 地址(网络字节序)。

  • 如果输入的字符串格式无效,返回 INADDR_NONE(通常定义为 0xFFFFFFFF)。

使用场景

inet_addr 函数通常用于将人类可读的 IP 地址字符串转换为程序可以使用的二进制形式。这在设置套接字地址结构(如 struct sockaddr_in)时非常有用。

在实际使用中,如果只需要简单的 IPv4 地址转换,inet_addr 是一个方便的选择。可以直接返回 32 位的二进制地址。如果需要更复杂的处理或支持 IPv6,建议使用 inet_pton 或 inet_aton (线程不安全)

域名和IP地址的对应关系

IP层通过IP地址的结构进行路由选择最终找到一条通往目的地的路由,但是一些著名的网站如果采用IP地址的方式提供地址,用户将无法记忆,所以更多的时候需要一个方便人类记忆域名(比如www.kernel.org)作为其实际IP地址(145.40.73.55)的别名,显然我们需要的一种机制去建立域名和IP地址的映射关系,一种方法是修改本机的hosts文件 /etc/hosts ,但是更加通用的方案是利用DNS协议,去访问一个DNS服务器,服务器当中存储了域名和IP地址的映射关系。与这个操作相关的函数是 gethostbyname ,下面是其用法:

struct hostent

用于存储从域名解析服务(如 DNS)中获取的主机信息,包括主机名、别名、IP 地址等。这个结构体定义在 <netdb.h> 头文件中。

#include<netdb.h>struct hostent {char *h_name;        // 主机的官方名称char **h_aliases;    // 主机的别名列表(以 NULL 结尾的数组)int h_addrtype;      // 地址类型(通常是 AF_INET 或 AF_INET6)int h_length;        // 地址的长度(以字节为单位)char **h_addr_list;  // 主机的地址列表(以 NULL 结尾的数组)
};

字段说明

  • h_name

    • 类型char *

    • 作用:主机的官方名称(通常是主机的域名,如 "example.com")。

  • h_aliases

    • 类型char **

    • 作用:主机的别名列表,是一个以 NULL 结尾的字符串数组。主机可能有多个别名,这些别名存储在这个数组中。

  • h_addrtype

    • 类型int

    • 作用:地址类型,通常为 AF_INET(IPv4)或 AF_INET6(IPv6)。

  • h_length

    • 类型int

    • 作用:地址的长度(以字节为单位)。对于 IPv4 地址,长度为 4 字节;对于 IPv6 地址,长度为 16 字节。

  • h_addr_list

    • 类型char **

    • 作用:主机的地址列表,是一个以 NULL 结尾的字符串数组。每个地址都是一个二进制形式的 IP 地址,存储为字节数组。第一个地址通常是最主要的地址。

gethostbyname

根据主机名或域名获取主机的 IP 地址信息。 

struct hostent *gethostbyname(const char *name);

参数

  • name:指向主机名或域名的字符串指针(如 "example.com""localhost")。

返回值

  • 成功时返回一个指向 struct hostent 的指针。

  • 失败时返回 NULL,可以通过 h_errno 获取错误原因(h_errno 是一个全局变量,用于存储主机名解析的错误代码)。

错误代码

  • HOST_NOT_FOUND:主机名未找到。

  • TRY_AGAIN:暂时无法解析主机名(可能是 DNS 服务器未响应)。

  • NO_RECOVERY:无法从错误中恢复。

  • NO_ADDRESS:主机名有效,但没有找到对应的地址。

  • gethostbyname 不是线程安全的,因为它返回的是静态分配的 struct hostent。在多线程环境中,建议使用 getaddrinfo,它返回动态分配的结构体。
  • gethostbyname 仅支持 IPv4 地址。如果需要支持 IPv6,建议使用 getaddrinfo,因为它可以同时处理 IPv4 和 IPv6 地址。

示例:

int main() {struct hostent *host;char *hostname = "www.taobao.com";// 获取主机信息host = gethostbyname(hostname);if (host == NULL) {perror("gethostbyname");return 1;}// 打印主机的官方名称printf("Official name: %s\n", host->h_name);// 打印主机的别名printf("Aliases:\n");for (char **alias = host->h_aliases; *alias != NULL; alias++) {printf("  %s\n", *alias);}// 打印主机的地址类型printf("Address type: %s\n", (host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");// 打印主机的地址printf("Addresses:\n");for (char **addr = host->h_addr_list; *addr != NULL; addr++) {printf("  %s\n", inet_ntoa(*((struct in_addr *)*addr)));}return 0;
}
/*
(base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./dns
Official name: www.taobao.com.danuoyi.tbcache.com
Aliases:www.taobao.com
Address type: AF_INET
Addresses:222.192.186.120222.192.186.122
*/

struct addrinfo

struct addrinfo {int ai_flags;          // 查询标志int ai_family;         // 地址族(如 AF_INET 或 AF_INET6)int ai_socktype;       // 套接字类型(如 SOCK_STREAM 或 SOCK_DGRAM)int ai_protocol;       // 协议(如 IPPROTO_TCP 或 IPPROTO_UDP)socklen_t ai_addrlen;  // 地址长度struct sockaddr *ai_addr; // 地址结构体char *ai_canonname;    // 规范化的主机名struct addrinfo *ai_next; // 指向下一个结果的指针
};
  • ai_flags

    • 类型int

    • 作用:查询标志,用于指定查询的偏好选项。常见的标志包括:

      • AI_PASSIVE:用于服务器端,表示返回的地址适用于 bind 函数。

      • AI_CANONNAME:返回规范化的主机名。

      • AI_NUMERICHOST:要求 node 是一个数字形式的地址(如 IP 地址)。

      • AI_NUMERICSERV:要求 service 是一个数字形式的端口号。

      • AI_V4MAPPED:如果查询 IPv6 地址,但主机只有 IPv4 地址,则返回 IPv4 映射的 IPv6 地址。

      • AI_ALL:返回所有匹配的地址(IPv4 和 IPv6)。

      • AI_ADDRCONFIG:仅返回当前主机支持的地址族。

  • ai_family

    • 类型int

    • 作用:地址族,指定地址的类型。常见的值包括:

      • AF_INET:IPv4 地址。

      • AF_INET6:IPv6 地址。

      • AF_UNSPEC:不指定地址族,允许返回 IPv4 或 IPv6 地址。

  • ai_socktype

    • 类型int

    • 作用:套接字类型,指定套接字的类型。常见的值包括:

      • SOCK_STREAM:TCP 套接字。

      • SOCK_DGRAM:UDP 套接字。

      • SOCK_RAW:原始套接字。

  • ai_protocol

    • 类型int

    • 作用:协议类型,指定使用的协议。常见的值包括:

      • IPPROTO_TCP:TCP 协议。

      • IPPROTO_UDP:UDP 协议。

      • IPPROTO_RAW:原始协议。

  • ai_addrlen

    • 类型socklen_t

    • 作用:地址的长度(以字节为单位)。

  • ai_addr

    • 类型struct sockaddr *

    • 作用:指向地址结构体的指针,存储主机的地址信息。根据 ai_family 的值,可以将其强制转换为 struct sockaddr_in(IPv4)或 struct sockaddr_in6(IPv6)。

  • ai_canonname

    • 类型char *

    • 作用:规范化的主机名。如果设置了 AI_CANONNAME 标志,此字段将包含主机的规范化名称。

  • ai_next

    • 类型struct addrinfo *

    • 作用:指向链表中的下一个 struct addrinfo 结构体的指针。如果为 NULL,表示链表结束。

getaddrinfo

getaddrinfo 用于根据主机名或服务名获取主机的地址信息。它是一个现代的替代品,用于替代传统的 gethostbynamegetservbyname 等函数,因为它支持 IPv4 和 IPv6,并且提供了更灵活的接口。 

int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);

参数

  • node

    • 类型const char *

    • 作用:主机名或 IP 地址字符串(如 "example.com""192.168.1.1")。

    • 可选:如果为 NULL,则表示本地主机。

  • service

    • 类型const char *

    • 作用:服务名或端口号字符串(如 "http""80")。

    • 可选:如果为 NULL,则不解析服务名。

  • hints

    • 类型const struct addrinfo *

    • 作用:一个指向 struct addrinfo 的指针,用于指定查询的偏好选项。

    • 可选:如果为 NULL,则默认查询所有可能的地址和协议。

  • res

    • 类型struct addrinfo **

    • 作用:一个指向指针的指针,用于存储查询结果。查询结果是一个链表,每个节点都是一个 struct addrinfo 结构体。

返回值

  • 成功时返回 0

  • 失败时返回一个非零的错误码(如 EAI_AGAINEAI_NONAME 等)。

#include<54func.h>
int main(int argc, char const *argv[])
{ARGS_CHECK(argc, 2);struct addrinfo hints, *res;int ret;char ipstr[INET6_ADDRSTRLEN];memset(&hints, 0, sizeof(hints));hints.ai_family = AF_UNSPEC;hints.ai_socktype = SOCK_STREAM;ret = getaddrinfo(argv[1], "https", &hints, &res);THREAD_ERROR_CHECK(ret, "getaddrinfo");printf("IP addresses for %s:\n\n", argv[1]);for(struct addrinfo *p = res; p != NULL; p = p->ai_next){void *addr;char *ipver;if(p->ai_family == AF_INET){struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;addr = &(ipv4->sin_addr);ipver = "IPV4";}else{struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;addr = &(ipv6->sin6_addr);ipver = "IPV6";}inet_ntop(p->ai_family, addr, ipstr, sizeof(ipstr));printf("  %s: %s\n", ipver, ipstr);}freeaddrinfo(res); //释放动态分配的内存return 0;
}
/*
(base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./dns2 www.taobao.com
IP addresses for www.taobao.com:IPV6: 2001:da8:20d:40db:3::3d2IPV6: 2001:da8:20d:40db:3::3d1IPV4: 222.192.186.120IPV4: 222.192.186.122
*/

 TCP 通信

下面是使用TCP通信的流程图:

socket

socket 函数用于创建一个 socket 套接字。它是网络通信的端点。通过 socket 函数,程序可以创建一个用于网络通信的套接字,并指定其类型和协议。

int socket(int domain, int type, int protocol);

 参数

  • domain

    • 类型int

    • 作用:指定地址族(协议族),常见的值包括:

      • AF_INET:IPv4 地址族。

      • AF_INET6:IPv6 地址族。

      • AF_UNIXAF_LOCAL:本地通信(Unix 域套接字)。

      • AF_UNSPEC:不指定地址族,通常用于 getaddrinfo 的结果。

  • type

    • 类型int

    • 作用:指定套接字类型,常见的值包括:

      • SOCK_STREAM:面向连接的流式套接字(TCP)。

      • SOCK_DGRAM:无连接的数据报套接字(UDP)。

      • SOCK_RAW:原始套接字,允许直接访问底层协议。

      • SOCK_SEQPACKET:有序的、可靠的、面向连接的、固定大小的数据报套接字。

      • SOCK_RDM:可靠的无连接数据报套接字。

  • protocol

    • 类型int

    • 作用指定协议,通常为 0,表示使用默认协议。常见的协议包括:

      • IPPROTO_TCP:TCP 协议(用于 SOCK_STREAM)。

      • IPPROTO_UDP:UDP 协议(用于 SOCK_DGRAM)。

      • IPPROTO_RAW:原始协议(用于 SOCK_RAW)。

返回值

  • 成功时返回一个非负的套接字描述符(文件描述符)。

  • 失败时返回 -1,并设置 errno 以指示错误原因。

bind

bind 函数是网络编程中用于将一个套接字绑定到一个本地地址和端口的函数。它通常用于服务器端,用于指定服务器监听的本地地址和端口。绑定后,套接字会与指定的地址和端口关联起来,从而允许服务器接收来自客户端的连接请求或数据。服务器建立连接时必须使用bind绑定端口,客户端一般不需要。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数

  • sockfd

    • 类型int

    • 作用:套接字描述符,由 socket 函数创建。它标识了服务器用于通信的套接字。

  • addr

    • 类型const struct sockaddr *

    • 作用:指向 struct sockaddr 或其派生类型(如 struct sockaddr_instruct sockaddr_in6)的指针,存储本地地址信息。对于 IPv4,通常使用 struct sockaddr_in;对于 IPv6,使用 struct sockaddr_in6

  • addrlen

    • 类型socklen_t

    • 作用addr 指向的地址结构体的大小(以字节为单位)。对于 struct sockaddr_in,大小通常为 sizeof(struct sockaddr_in);对于 struct sockaddr_in6,大小通常为 sizeof(struct sockaddr_in6)

返回值

  • 成功时返回 0

  • 失败时返回 -1,并设置 errno 以指示错误原因。

 示例:

int main(int argc, char *argv[]) {ARGS_CHECK(argc, 3);int sockfd;struct sockaddr_in server_addr;// 创建一个 TCP 套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");// 初始化服务器地址memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;          // 地址族server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));        // 服务器端口号// 绑定套接字到本地地址int ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");// 关闭套接字close(sockfd);return 0;
}

connect

客户端使用 connect 来建立和TCP服务端的连接。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数

  • sockfd

    • 类型int

    • 作用:套接字描述符,由 socket 函数创建。它标识了客户端用于通信的套接字。

  • addr

    • 类型const struct sockaddr *

    • 作用:指向 struct sockaddr 或其派生类型(如 struct sockaddr_instruct sockaddr_in6)的指针,存储服务器的地址信息。对于 IPv4,通常使用 struct sockaddr_in;对于 IPv6,使用 struct sockaddr_in6

  • addrlen

    • 类型socklen_t

    • 作用addr 指向的地址结构体的大小(以字节为单位)。对于 struct sockaddr_in,大小通常为 sizeof(struct sockaddr_in);对于 struct sockaddr_in6,大小通常为 sizeof(struct sockaddr_in6)

返回值

  • 成功时返回 0

  • 失败时返回 -1,并设置 errno 以指示错误原因。

listen

listen 函数是网络编程中用于将一个套接字转换为被动套接字(即监听套接字)的函数。它通常用于服务器端,用于使套接字进入监听状态,等待客户端的连接请求。listen 函数是 TCP 服务器编程中的关键步骤之一。

int listen(int sockfd, int backlog);

 一旦启用了 listen 之后,操作系统就知道该套接字是服务端的套接字,操作系统内核就不再启用其发送和接收缓冲区,转而在内核区维护两个队列结构:半连接队列 和 全连接队列。半连接队列用于管理成功第一次握手的连接,全连接队列用于管理已经完成三次握手的队列。 backlog 在有些操作系统用来指明半连接队列和全连接队列的长度之和,一般填一列个正数即可。如果队列已经满了,那么服务端受到任何再发起的连接都会直接丢弃(大部分操作系统中服务端不会回复RST,以方便客户端自动重传)

     参数

    • sockfd

      • 类型int

      • 作用:套接字描述符,由 socket 函数创建。它标识了服务器用于通信的套接字。

    • backlog

      • 类型int

      • 作用:指定未完成连接队列的最大长度。当有多个客户端同时尝试连接时,backlog 参数决定了服务器可以暂存的未完成连接的数量。

    返回值

    • 成功时返回 0

    • 失败时返回 -1,并设置 errno 以指示错误原因。

    accept

    accept 函数由服务端调用,用于从全连接队列中取出下一个已经完成的TCP连接。如果全连接队列为空,那么 accept 会陷入阻塞。一旦全连接队列中到来新的连接,此时 accept 操作就会就绪,这种就绪是读操作就绪,所以可以使用 select 函数的读集合进行监听。当accept 执行完了之后,内核会创建一个新的套接字文件对象 ,该文件对象关联的文件描述符是 accept 的返回值,文件对象当中最重要的结构是一个发送缓冲区和接收缓冲区,可以用于服务端通过TCP连接发送和接收TCP段。

    区分两个套接字是非常重要的。通过把旧的管理连接队列的套接字称作监听套接字,而新的用于发送和接收TCP段的套接字称作已连接套接字。通常来说,监听套接字会一直存在,负责建立各个不同的TCP连接(只要源IP、源端口、目的IP、目的端口四元组任意一个字段有区别,就是一个新的TCP连接),而某一条单独的TCP连接则是由其对应的已连接套接字进行数据通信的。客户端使用 close 关闭套接字或者服务端使用 close 关闭已连接套接字的时候就是主动发起断开连接四次挥手的过程。

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

     参数

    • sockfd

      • 类型int

      • 作用:监听套接字描述符,由 socket 函数创建并经过 listen 转换为被动套接字。它标识了服务器用于接收连接请求的套接字。

    • addr

      • 类型struct sockaddr *

      • 作用:指向 struct sockaddr 或其派生类型(如 struct sockaddr_instruct sockaddr_in6)的指针,用于存储客户端的地址信息。如果不需要客户端的地址信息,可以设置为 NULL

    • addrlen

      • 类型socklen_t *

      • 作用:指向一个 socklen_t 类型的变量,该变量在调用前应设置为 addr 指向的地址结构体的大小(以字节为单位)。调用后,addrlen 指向的变量将被设置为实际存储在 addr 中的地址长度。如果 addrNULL,则 addrlen 也应为 NULL

    返回值

    • 成功时返回一个新的套接字描述符,用于与客户端通信。

    • 失败时返回 -1,并设置 errno 以指示错误原因。

    send

    send 和 recv 用于将数据在用户态空间和内核态的缓冲区之间进行传输,无论是客户端还是服务端均可使用,但是只能用于TCP连接。将数据拷贝到内核态并不意味着会马上传输,而是会根据时机再由内核协议栈按照协议的规范进行分节,通常缓冲区如果数据过多会分节成MSS的大小,然后根据窗口条件传输到网络层之中。

    send 函数是网络编程中用于向已连接的套接字发送数据的函数。它通常用于 TCP 套接字,但也可以用于已连接的 UDP 套接字。send 函数是 write 函数在网络编程中的等价物,用于将数据发送到对端。

    ssize_t send(int sockfd, const void *buf, size_t len, int flags);

    参数

    • sockfd

      • 类型int

      • 作用:套接字描述符,标识了用于发送数据的套接字。该套接字必须已经通过 connect 函数连接到对端(对于 TCP 套接字)或通过 sendto 函数发送数据(对于 UDP 套接字)。

    • buf

      • 类型const void *

      • 作用:指向要发送的数据的缓冲区。

    • len

      • 类型size_t

      • 作用:要发送的数据的长度(以字节为单位)。

    • flags

      • 类型int

      • 作用:发送标志,通常设置为 0。可以设置的标志包括:

        • MSG_OOB:发送带外数据(仅适用于 TCP 套接字)。

        • MSG_DONTROUTE:跳过路由表,直接发送数据(仅适用于 TCP 套接字)。

        • MSG_DONTWAIT:非阻塞发送(仅适用于非阻塞套接字)。

        • MSG_NOSIGNAL:发送数据时不会产生 SIGPIPE 信号(仅适用于 TCP 套接字)。

    返回值

    • 成功时返回实际发送的字节数(可能小于请求的字节数)。

    • 失败时返回 -1,并设置 errno 以指示错误原因。

    recv

    recv 函数是网络编程中用于从已连接的套接字接收数据的函数。它通常用于 TCP 套接字,但也可以用于已连接的 UDP 套接字。recv 函数是 read 函数在网络编程中的等价物,用于从对端接收数据。

    ssize_t recv(int sockfd, void *buf, size_t len, int flags);

    参数

    • sockfd

      • 类型int

      • 作用:套接字描述符,标识了用于接收数据的套接字。该套接字必须已经通过 connect 函数连接到对端(对于 TCP 套接字)或通过 bind 函数绑定到本地地址(对于 UDP 套接字)。

    • buf

      • 类型void *

      • 作用:指向接收数据的缓冲区。

    • len

      • 类型size_t

      • 作用:缓冲区的大小(以字节为单位),表示可以接收的最大数据量。

    • flags

      • 类型int

      • 作用:接收标志,通常设置为 0。可以设置的标志包括:

        • MSG_OOB:接收带外数据(仅适用于 TCP 套接字)。

        • MSG_PEEK:查看数据但不移除它(即“偷看”数据)。

        • MSG_WAITALL:阻塞直到接收到请求的所有数据。

        • MSG_DONTWAIT:非阻塞接收(仅适用于非阻塞套接字)。

    返回值

    • 成功时返回接收到的字节数。

    • 如果对端关闭连接,返回 0

    • 失败时返回 -1,并设置 errno 以指示错误原因。

    示例:使用以上函数完成一次简单的服务端和客户端之间的信息发送

    client.c
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc,3);int sockfd;struct sockaddr_in client_addr;sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");memset(&client_addr, 0, sizeof(client_addr));client_addr.sin_family = AF_INET;client_addr.sin_addr.s_addr = inet_addr(argv[1]);client_addr.sin_port = htons(atoi(argv[2]));int ret = connect(sockfd, (struct sockaddr*)&client_addr, sizeof(client_addr));sleep(3);char *str = "hi I am client\n";char buf[50];ret = send(sockfd, str, strlen(str), 0);ERROR_CHECK(ret, -1, "send");ret = recv(sockfd, buf, sizeof(buf), 0);ERROR_CHECK(ret, -1, "recv");printf("%s\n", buf);return 0;
    }server.c
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc,3);int sockfd;struct sockaddr_in server_addr;sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");printf("waiting connect...\n");ret = listen(sockfd, 10);           //放入监听集合ERROR_CHECK(ret, -1, "listen");int newFd = accept(sockfd, NULL, NULL);  //从就绪集合中取出printf("client connected!\n");char buf[50];char *str = "hello";memset(buf, 0, sizeof(buf));ret = recv(newFd, buf, sizeof(buf), 0);ERROR_CHECK(ret, 0, "recv");printf("%s\n", buf);printf("send hello to client\n");send(newFd, str, strlen(str), 0);close(newFd);close(sockfd);return 0;
    }//client
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client 127.0.0.1 1255
    hello
    //server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server 127.0.0.1 1255
    waiting connect...
    client connected!
    hi I am clientsend hello to client
    

    需要特别注意的是, send 和 recv 的次数和网络上传输的TCP段的数量没有关系,多次的send 和 recv 可能只需要一次TCP段的传输。另外一方面,TCP是一种流式的通信协议,消息是以字节流的方式在信道中传输,这就意味着一个重要的事情,消息和消息之间是没有边界的。在不加额外约定的情况下,通信双方并不知道发送和接收到底有没有接收完一个消息,有可能多个消息会在一次传输中被发送和接收(江湖俗称"粘包"),也有有可能一个消息需要多个传输才能被完整的发送和接收(江湖俗称"半包")。

    实战:使用select实现TCP客户端与服务端即时聊天

    基于TCP的聊天程序的实现思路和之前利用管道实现即时聊天的思路是一致的。客户端和服务端都需要使用 select 这种IO多路复用机制监听读事件,客户端需要监听套接字的读缓冲区以及标准输入,服务端需要监听已连接套接字的读缓冲区以及标准输入。

    //client.c
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc,3);int sockfd;struct sockaddr_in client_addr;sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");memset(&client_addr, 0, sizeof(client_addr));client_addr.sin_family = AF_INET;client_addr.sin_addr.s_addr = inet_addr(argv[1]);client_addr.sin_port = htons(atoi(argv[2]));printf("connecting to server\n");int ret = connect(sockfd, (struct sockaddr*)&client_addr, sizeof(client_addr));printf("connected\n");char buf[1024];fd_set rdset;while (1){FD_ZERO(&rdset);FD_SET(STDIN_FILENO, &rdset);FD_SET(sockfd, &rdset);select(sockfd+1, &rdset, NULL, NULL, NULL);if(FD_ISSET(STDIN_FILENO, &rdset)){memset(buf, 0, sizeof(buf));ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");if(strcmp(buf, "disconnect\n") == 0){printf("disconnected\n");close(sockfd);break;}send(sockfd, buf, strlen(buf)-1, 0);}if(FD_ISSET(sockfd, &rdset)){memset(buf, 0, sizeof(buf));ret = recv(sockfd, buf, sizeof(buf), 0);ERROR_CHECK(ret, -1, "read");if(ret == 0){printf("server disconnected\n");break;}printf("server: %s\n",buf);}}close(sockfd);return 0;
    }
    /server.c
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc,3);int sockfd;struct sockaddr_in server_addr;sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");printf("waiting to connect...\n");ret = listen(sockfd, 10);           //放入监听集合ERROR_CHECK(ret, -1, "listen");int newFd = accept(sockfd, NULL, NULL);  //从就绪集合中取出ERROR_CHECK(newFd, -1, "accept");printf("client connected!\n");char buf[1024];fd_set rdset;while (1){FD_ZERO(&rdset);FD_SET(STDIN_FILENO, &rdset);FD_SET(newFd, &rdset);select(newFd+1, &rdset, NULL, NULL, NULL);if(FD_ISSET(STDIN_FILENO, &rdset)){memset(buf, 0, sizeof(buf));ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");if(strcmp(buf, "disconnect\n") == 0){printf("disconnected\n");close(newFd);close(sockfd);break;}send(newFd, buf, strlen(buf)-1, 0);}if(FD_ISSET(newFd, &rdset)){memset(buf, 0, sizeof(buf));ret = recv(newFd, buf, sizeof(buf), 0);ERROR_CHECK(ret, -1, "read");if(ret == 0){printf("client disconnected\n");close(newFd);close(sockfd);return 0;}printf("client: %s\n",buf);}}return 0;
    }
    /*
    //server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server 192.168.1.2 1236
    waiting to connect...
    client connected!
    client: hello i am client!
    hi i am server!
    what are you doing?
    client: emmm... learning linux c.
    client: i want to offline, byebye!
    ok
    client disconnected
    //client
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client 192.168.1.2 1236
    connecting to server
    connected
    hello i am client! 
    server: hi i am server!
    server: what are you doing?
    emmm... learning linux c.
    i want to offline, byebye!
    server: ok
    ^C
    */
    

    TIME_WAIT和setsockopt

    如果是服务端主动调用 close 断开的连接,即服务端是四次挥手的主动关闭方,由之前的TCP状态转换图可知,主动关闭方在最后会处于一个固定2MSL时长的TIME_WAIT等待时间。在此状态期间,如果尝试使用 bind 系统调用对重复的地址进行绑定操作,那么会报错。

    $ ./server_tcpchat1 192.168.135.132 2778
    bind: Address already in use
    $ netstat -an|grep 2778
    tcp
    0
    0 192.168.135.132:2778
    192.168.135.133:57466
    TIME_WAIT

    使用 select 对 socket 进行断线重连

    服务端除了接收缓冲区和标准输入以外,还有一个操作也会造成阻塞,那就是 accept 操作,实际上服务端可以使用 select 管理监听套接字,检查其全连接队列是否存在已经建好的连接,如果存在连接,那么其读事件即 accept 操作便就绪。将监听套接字加入监听会导致服务端的代码发生一个结构变化:

    • 每次重新调用 select 之前需要提前准备好要监听的文件描述符,这些文件描述符当中可能会包括新的已连接套接字的文件描述符。
    • select 的第一个参数应当足够大,从而避免无法监听到新的已连接套接字的文件描述符(它们的数值可能会比较大)。
    • 需要处理 accept 就绪的情况。
    #include<54func.h>int main(int argc, char const *argv[])
    {ARGS_CHECK(argc,3);int sockfd, newFd;struct sockaddr_in server_addr;sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");printf("waiting to connect...\n");ret = listen(sockfd, 10);           //放入监听集合ERROR_CHECK(ret, -1, "listen");//needMonitorSetorset 是目前需要监听的集合,为rdset提供监听集合,它本身并不会被监听char buf[1024];fd_set rdset, needMonitorSetorset;FD_ZERO(&rdset);FD_ZERO(&needMonitorSetorset);   FD_SET(STDIN_FILENO, &needMonitorSetorset);FD_SET(sockfd, &needMonitorSetorset);while (1){memcpy(&rdset, &needMonitorSetorset, sizeof(rdset));ret = select(10, &rdset, NULL, NULL, NULL);ERROR_CHECK(ret, -1, "select");if(FD_ISSET(STDIN_FILENO, &rdset)){memset(buf, 0, sizeof(buf));ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");if(strcmp(buf, "disconnect\n") == 0){printf("disconnected\n");close(newFd);close(sockfd);break;}send(newFd, buf, strlen(buf)-1, 0);}else if(FD_ISSET(newFd, &rdset)){memset(buf, 0, sizeof(buf));ret = recv(newFd, buf, sizeof(buf), 0);ERROR_CHECK(ret, -1, "read");if(ret == 0){printf("client disconnected\n");close(newFd);FD_CLR(newFd, &needMonitorSetorset);continue;}printf("client: %s\n",buf);}else if(FD_ISSET(sockfd, &rdset)){struct sockaddr_in cliAddr;memset(&cliAddr, 0, sizeof(cliAddr));socklen_t sockLen = sizeof(cliAddr);printf("sockLen=%d\n",sockLen);newFd = accept(sockfd, (struct sockaddr*)&cliAddr, &sockLen);ERROR_CHECK(newFd, -1, "accept");FD_SET(newFd, &needMonitorSetorset);printf("sockLen = %d\n",sockLen);printf("newFd = %d is connected\n", newFd);printf("ip is: %s, port is: %d\n", inet_ntoa(cliAddr.sin_addr), ntohs(cliAddr.sin_port));}}close(newFd);close(sockfd);return 0;
    }
    /*
    client
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client 192.168.1.1 1236
    connecting to server
    connected
    hello server
    server: what are you doing?
    I will go
    ^C
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client 192.168.1.1 1236
    connecting to server
    connected
    I am back!
    byebye!
    ^C
    server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server2 192.168.1.1 1236
    waiting to connect...
    sockLen=16
    sockLen = 16
    newFd = 4 is connected
    ip is: 192.168.1.1, port is: 58290
    client: hello server
    what are you doing?
    client: I will go
    client disconnected
    sockLen=16
    sockLen = 16
    newFd = 4 is connected
    ip is: 192.168.1.1, port is: 60632
    client: I am back!
    client: byebye!
    client disconnected
    ^C
    */

    实战:使用epoll实现TCP客户端与服务端即时聊天

    在之前一节文章中,对IO多路复用的 select 和 epoll 进行了总结,实际开发中,epoll用的更广泛且效率也更高。在实现方面,epoll基本上与select差不多也是进行初始化将监听集合加入即可,不过简化掉了每次循环都需要将监听对象重新加入的步骤。一次加入,即可永久监听。

    client.cint main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);int sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));printf("waiting for connect\n");int ret = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "coonect");printf("connected\n");int epfd = epoll_create(1);ERROR_CHECK(epfd, -1, "epoll_create");struct epoll_event ev, evs[2];ev.events = EPOLLIN;ev.data.fd = STDIN_FILENO;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);ERROR_CHECK(ret, -1, "epoll_ctl");ev.data.fd = sockfd;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);ERROR_CHECK(ret, -1, "epoll_ctl");char buf[1024];int readyNum = 0;while (1){readyNum = epoll_wait(epfd, evs, 2, -1);for(int i = 0; i < readyNum; i++){if(evs[i].data.fd ==  STDIN_FILENO){memset(buf, 0, sizeof(buf));read(STDIN_FILENO, buf, sizeof(buf));send(sockfd, buf, strlen(buf), 0);}else if(evs[i].data.fd == sockfd){memset(buf, 0, sizeof(buf));ret = read(sockfd, buf, sizeof(buf));if(ret == 0){printf("disconnected\n");close(sockfd);return 0;}printf("server: %s", buf);}  }}return 0;
    }server.cint main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);int sockfd = socket(AF_INET, SOCK_STREAM, 0);ERROR_CHECK(sockfd, -1, "socket");struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));printf("waiting for connect\n");int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");ret = listen(sockfd, 10);           //放入监听集合ERROR_CHECK(ret, -1, "listen");int newfd = accept(sockfd, NULL, NULL);ERROR_CHECK(newfd, -1, "accept");printf("connected:%d\n", newfd);int epfd = epoll_create(1);ERROR_CHECK(epfd, -1, "epoll_create");struct epoll_event ev, evs[2];ev.events = EPOLLIN;ev.data.fd = STDIN_FILENO;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);ERROR_CHECK(ret, -1, "epoll_ctl");ev.data.fd = newfd;ret = epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &ev);ERROR_CHECK(ret, -1, "epoll_ctl");char buf[1024];int readyNum = 0;while (1){readyNum = epoll_wait(epfd, evs, 2, -1);for(int i = 0; i < readyNum; i++){if(evs[i].data.fd ==  STDIN_FILENO){memset(buf, 0, sizeof(buf));read(STDIN_FILENO, buf, sizeof(buf));send(newfd, buf, strlen(buf), 0);}else if(evs[i].data.fd == newfd){memset(buf, 0, sizeof(buf));ret = read(newfd, buf, sizeof(buf));if(ret == 0){printf("disconnected\n");close(newfd);close(sockfd);return 0;}printf("client: %s", buf);}  }}return 0;
    }
    //client
    base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client5 172.20.74.205 1236
    waiting for connect
    connected
    hello i am client!
    how do you do?
    server: very good!
    server: thank you!
    server: goodbye
    ok
    ^C
    //server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server5 172.20.74.205 1236
    waiting for connect
    connected:4
    client: hello i am client!
    client: how do you do?
    very good!
    thank you!
    goodbye
    client: ok
    disconnected
    

    UDP通信

    UDP相对于TCP减少了建立连接部分,客户端设置好通信地址后直接使用 sendto 和 recvfrom 即可通信,服务端绑定好套接字后不需要监听。直接阻塞等待客户端。需要注意的是,服务端一般不知道客户端的地址,但客户端知道服务端的信息,需要先等待客户端发送消息才能确定客户端的地址。

     sendto

    sendto 函数是网络编程中用于向指定地址发送数据的函数,通常用于无连接的套接字(如 UDP 套接字)。它允许发送方指定目标地址和端口,而不需要事先建立连接。sendto 函数是 UDP 通信中的关键函数,也可以用于已连接的 TCP 套接字。

    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

    参数

    • sockfd

      • 类型int

      • 作用:套接字描述符,标识了用于发送数据的套接字。该套接字可以是 UDP 套接字或已连接的 TCP 套接字。

    • buf

      • 类型const void *

      • 作用:指向要发送的数据的缓冲区。

    • len

      • 类型size_t

      • 作用:要发送的数据的长度(以字节为单位)。

    • flags

      • 类型int

      • 作用:发送标志,通常设置为 0。可以设置的标志包括:

        • MSG_OOB:发送带外数据(仅适用于 TCP 套接字)。

        • MSG_DONTROUTE:跳过路由表,直接发送数据(仅适用于 TCP 套接字)。

        • MSG_DONTWAIT:非阻塞发送(仅适用于非阻塞套接字)。

        • MSG_NOSIGNAL:发送数据时不会产生 SIGPIPE 信号(仅适用于 TCP 套接字)。

    • dest_addr

      • 类型const struct sockaddr *

      • 作用:指向目标地址结构体的指针,存储目标地址和端口信息。对于 IPv4,通常使用 struct sockaddr_in;对于 IPv6,使用 struct sockaddr_in6

    • addrlen

      • 类型socklen_t

      • 作用dest_addr 指向的地址结构体的大小(以字节为单位)。对于 struct sockaddr_in,大小通常为 sizeof(struct sockaddr_in);对于 struct sockaddr_in6,大小通常为 sizeof(struct sockaddr_in6)

    返回值

    • 成功时返回实际发送的字节数(可能小于请求的字节数)。

    • 失败时返回 -1,并设置 errno 以指示错误原因。

    recvfrom

    recvfrom 函数是网络编程中用于从无连接的套接字(如 UDP 套接字)接收数据的函数。它允许接收方获取发送方的地址信息,而不需要事先建立连接。recvfrom 函数是 UDP 通信中的关键函数,也可以用于已连接的 TCP 套接字。

    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

    参数

    • sockfd

      • 类型int

      • 作用:套接字描述符,标识了用于接收数据的套接字。该套接字可以是 UDP 套接字或已连接的 TCP 套接字。

    • buf

      • 类型void *

      • 作用:指向接收数据的缓冲区。

    • len

      • 类型size_t

      • 作用:缓冲区的大小(以字节为单位),表示可以接收的最大数据量。

    • flags

      • 类型int

      • 作用:接收标志,通常设置为 0。可以设置的标志包括:

        • MSG_PEEK:查看数据但不移除它(即“偷看”数据)。

        • MSG_WAITALL:阻塞直到接收到请求的所有数据。

        • MSG_DONTWAIT:非阻塞接收(仅适用于非阻塞套接字)。

    • src_addr

      • 类型struct sockaddr *

      • 作用:指向 struct sockaddr 或其派生类型(如 struct sockaddr_instruct sockaddr_in6)的指针,用于存储发送方的地址信息。如果不需要发送方的地址信息,可以设置为 NULL

    • addrlen

      • 类型socklen_t *(必须取地址)

      • 作用:指向一个 socklen_t 类型的变量,该变量在调用前应设置为 src_addr 指向的地址结构体的大小(以字节为单位)。调用后,addrlen 指向的变量将被设置为实际存储在 src_addr 中的地址长度。如果 src_addrNULL,则 addrlen 也应为 NULL

    返回值

    • 成功时返回接收到的字节数。

    • 如果对端关闭连接,返回 0

    • 失败时返回 -1,并设置 errno 以指示错误原因。

    在使用UDP进行的通信的时候,要特别注意的是这是一个无连接的协议。一方面调用socket 函数的时候需要设置 SOCK_DGRAM 选项,而且因为没有建立连接的过程,所以必须总是由客户端先调用 sendto 发送消息给服务端,这样服务端才能知道对端的地址信息,从进入后续的通信。下面是使用UDP通信的一个例子:

    //client
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);int sockfd, ret;sockfd = socket(AF_INET, SOCK_DGRAM, 0);ERROR_CHECK(sockfd, -1, "socket");struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));printf("initialize raady\n");char buf[1024];socklen_t len = sizeof(server_addr);ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&server_addr, len);ERROR_CHECK(ret, -1, "sendto");ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&server_addr, &len);ERROR_CHECK(ret, -1, "recvfrom");printf("server:%s\n", buf);ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&server_addr, len);ERROR_CHECK(ret, -1, "sendto");close(sockfd);return 0;
    }
    //server
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);int sockfd, ret;sockfd = socket(AF_INET, SOCK_DGRAM, 0);ERROR_CHECK(sockfd, -1, "socket");struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");printf("initialize raady\n");char buf[1024];struct sockaddr_in client_addr;memset(&client_addr, 0, sizeof(client_addr));socklen_t len = sizeof(client_addr);ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &len);ERROR_CHECK(ret, -1, "recvfrom");printf("client: %s\n", buf);ret = read(STDIN_FILENO, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read");ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&client_addr, len);ERROR_CHECK(ret, -1, "sendto");ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &len);ERROR_CHECK(ret, -1, "recvfrom");printf("client: %s\n", buf);close(sockfd);return 0;
    }
    //client
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client3 192.168.1.1 1236
    initialize raady
    hello
    server:what are you doing ?
    hahaha
    //server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server3 192.168.1.1 1236
    initialize raady
    client: hellowhat are you doing ?
    client: hahaha
    e you doing ?

     可以发现UDP是一种保留消息边界的协议,无论用户态空间分配的空间是否足够 recvfrom总是会取出一个完整UDP报文,那么没有拷贝的用户态内存的数据会直接丢弃。

    实战:使用UDP的即时聊天

    类似基于TCP的即时聊天通信,使用UDP也可以实现即时聊天通信,考虑到UDP是无连接协议,客户端需要首先发送一个消息让服务端知道客户端的地址信息,然后再使用 select 监听网络读缓冲区和标准输入即可。

    需要特别注意的是,UDP通信不存在连接建立和断开过程,所以服务端无法知道客户端是否已经关闭套接字。

    //client.c
    int main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));int sockfd = socket(AF_INET, SOCK_DGRAM, 0);ERROR_CHECK(sockfd, -1, "socket");char *serverInf = "client is connected";sendto(sockfd, serverInf, strlen(serverInf), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));char buf[1024] = {0};fd_set rdset;int ret;socklen_t len = sizeof(server_addr);while(1){FD_ZERO(&rdset);FD_SET(STDIN_FILENO, &rdset);FD_SET(sockfd, &rdset);select(sockfd + 1, &rdset, NULL, NULL, NULL);if(FD_ISSET(STDIN_FILENO, &rdset)){memset(buf, 0, sizeof(buf));read(STDIN_FILENO, buf, sizeof(buf));sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&server_addr, len); }else if(FD_ISSET(sockfd, &rdset)){memset(buf, 0, sizeof(buf));ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&server_addr, &len);if(ret == 0){printf("disconnected\n");break;}printf("server: %s",buf);    }}close(sockfd);return 0;
    }
    //server.cint main(int argc, char const *argv[])
    {ARGS_CHECK(argc, 3);struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));int sockfd = socket(AF_INET, SOCK_DGRAM, 0);ERROR_CHECK(sockfd, -1, "socket");int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));ERROR_CHECK(ret, -1, "bind");char buf[1024] = {0};struct sockaddr_in client_addr;socklen_t len = sizeof(client_addr);ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&client_addr, &len);ERROR_CHECK(ret, -1, "recvfrom");printf("%s\n", buf);fd_set rdset;while(1){FD_ZERO(&rdset);FD_SET(STDIN_FILENO, &rdset);FD_SET(sockfd, &rdset);select(sockfd + 1, &rdset, NULL, NULL, NULL);if(FD_ISSET(STDIN_FILENO, &rdset)){memset(buf, 0, sizeof(buf));read(STDIN_FILENO, buf, sizeof(buf));sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&client_addr, len); }else if(FD_ISSET(sockfd, &rdset)){memset(buf, 0, sizeof(buf));ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&client_addr, &len);if(ret == 0){printf("disconnected\n");break;}printf("client: %s",buf);    }}close(sockfd);return 0;
    }
    //client
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./client4 192.168.1.1 1235
    hello I am client!
    what are you doing?
    server: not need to know
    server: goodbye!
    OK
    ^C
    //server
    (base) ubuntu@ubuntu:~/MyProject/Linux/net$ ./server4 172.20.74.205 1235
    hello I am client!client: what are you doing?
    not need to know
    goodbye!
    client: OK

    socket属性调整

    getsockopt

    getsockopt 函数是网络编程中用于获取套接字选项的函数。它允许程序员查询套接字的当前配置,获取各种参数的值,例如缓冲区大小、超时时间、是否允许重用地址等。

    int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);

    参数

    • sockfd

      • 类型int

      • 作用:套接字描述符,标识了要查询选项的套接字。

    • level

      • 类型int

      • 作用:指定选项所在的协议级别。常见的值包括:

        • SOL_SOCKET:套接字级别的选项。

        • IPPROTO_TCP:TCP 协议级别的选项。

        • IPPROTO_IP:IP 协议级别的选项。

        • IPPROTO_IPV6:IPv6 协议级别的选项。

    • optname

      • 类型int

      • 作用:指定要查询的选项名称。常见的选项包括:

        • SO_REUSEADDR:是否允许重用本地地址和端口。

        • SO_REUSEPORT:是否允许重用端口。

        • SO_BROADCAST:是否允许发送广播数据。

        • SO_KEEPALIVE:是否启用 TCP 保活机制。

        • SO_LINGER:套接字关闭时的行为。

        • SO_RCVBUF:接收缓冲区大小。

        • SO_SNDBUF:发送缓冲区大小。

        • TCP_NODELAY:是否禁用 Nagle 算法。

    • optval

      • 类型void *

      • 作用:指向存储选项值的缓冲区。查询结果将存储在这个缓冲区中。

    • optlen

      • 类型socklen_t *

      • 作用:指向一个 socklen_t 类型的变量,该变量在调用前应设置为 optval 指向的缓冲区的大小(以字节为单位)。调用后,optlen 指向的变量将被设置为实际存储在 optval 中的值的大小。

    返回值

    • 成功时返回 0

    • 失败时返回 -1,并设置 errno 以指示错误原因。

    setsockopt

    setsockopt 函数是网络编程中用于设置套接字选项的函数。它允许程序员在套接字级别上配置各种参数,从而影响套接字的行为。这些选项可以包括缓冲区大小、超时时间、是否允许重用地址等。

    int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

    参数

    • sockfd

      • 类型int

      • 作用:套接字描述符,标识了要设置选项的套接字。

    • level

      • 类型int

      • 作用:指定选项所在的协议级别。同 getsockopt 选项

    • optname

      • 类型int

      • 作用:指定要设置的选项名称。同 getsockopt 选项

    • optval

      • 类型const void *

      • 作用:指向存储选项值的缓冲区。选项值的类型取决于 optname

    • optlen

      • 类型socklen_t

      • 作用optval 指向的缓冲区的大小(以字节为单位)。

    返回值

    • 成功时返回 0

    • 失败时返回 -1,并设置 errno 以指示错误原因。

    其他系统调用

    socketpair

    用于创建一对相互连接的套接字。这对套接字可以用于进程间通信(IPC)。

    #include <sys/types.h>
    #include <sys/socket.h>int socketpair(int domain, int type, int protocol, int sv[2]);

    参数说明

    1. domain:

      • 指定套接字的通信域(协议族)。对于 socketpair(),通常使用 AF_UNIXAF_LOCAL,表示本地通信。

    2. type:

      • 指定套接字的类型。常见的类型包括:

        • SOCK_STREAM:流式套接字,提供可靠的双向字节流。

        • SOCK_DGRAM:数据报套接字,提供无连接的、不可靠的、固定大小的数据报。

        • SOCK_SEQPACKET:有序的、可靠的、固定大小的数据报。

        • SOCK_RAW:原始套接字,用于直接访问协议层。

    3. protocol:

      • 指定使用的协议。对于 AF_UNIX,通常设置为 0,表示默认协议。

    4. sv[2]:

      • 一个数组,用于存储创建的两个套接字的文件描述符。sv[0]sv[1] 是一对相互连接的套接字。

    返回值

    • 成功:返回 0,并通过 sv 参数返回两个套接字的文件描述符。

    • 失败:返回 -1,并通过 errno 设置错误码。

    示例:使用 socketpair 进行父子进程间通信

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <string.h>int main() {int sv[2];pid_t pid;// 创建一对套接字if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {perror("socketpair");exit(EXIT_FAILURE);}// 创建子进程pid = fork();if (pid == -1) {perror("fork");exit(EXIT_FAILURE);}if (pid == 0) {// 子进程close(sv[0]); // 关闭父进程端的套接字char buffer[100];int n;// 从父进程接收数据if ((n = read(sv[1], buffer, sizeof(buffer))) == -1) {perror("read");exit(EXIT_FAILURE);}buffer[n] = '\0'; // 确保字符串以空字符结尾printf("Child received: %s\n", buffer);// 向父进程发送数据const char *msg = "Hello from child\n";if (write(sv[1], msg, strlen(msg)) == -1) {perror("write");exit(EXIT_FAILURE);}close(sv[1]); // 关闭子进程端的套接字exit(EXIT_SUCCESS);} else {// 父进程close(sv[1]); // 关闭子进程端的套接字const char *msg = "Hello from parent\n";// 向子进程发送数据if (write(sv[0], msg, strlen(msg)) == -1) {perror("write");exit(EXIT_FAILURE);}char buffer[100];int n;// 从子进程接收数据if ((n = read(sv[0], buffer, sizeof(buffer))) == -1) {perror("read");exit(EXIT_FAILURE);}buffer[n] = '\0'; // 确保字符串以空字符结尾printf("Parent received: %s\n", buffer);close(sv[0]); // 关闭父进程端的套接字wait(NULL);   // 等待子进程退出}return 0;
    }

    socketpair创建套接子 与 使用管道通信相比 有以下一些区别:

    • socketpair():

      • 适用于需要双向通信的场景。这一点是它的优势

      • 提供更灵活的通信机制,支持多种套接字类型。

      • 可以扩展到网络通信。

    • pipe():

      • 适用于简单的单向通信场景。

      • 实现简单,开销小。

      • 仅限于本地进程间通信。

    sendmsg

    sendmsg() 是一个用于发送消息的系统调用,它比 send()write() 更为通用和强大。它允许发送带有多种附加信息(如文件描述符)的消息,通常用于高级的套接字编程。

    #include <sys/socket.h>ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

    参数说明

    • sockfd:

      • 套接字文件描述符,表示要发送消息的套接字。

    • msg:

      • 指向 struct msghdr 结构的指针,该结构定义了要发送的消息的内容和格式。struct msghdr 的定义如下:

    struct msghdr {void         *msg_name;       // 可选的地址socklen_t     msg_namelen;    // 地址长度struct iovec *msg_iov;        // 散列(scatter/gather)数组size_t        msg_iovlen;     // 散列数组中的元素数量void         *msg_control;    // 可选的控制信息size_t        msg_controllen; // 控制信息的长度int           msg_flags;      // 标志(通常为 0)
    };
    • msg_namemsg_namelen:用于指定目标地址(例如在 UDP 套接字中)。对于已连接的套接字(如 TCP),通常设置为 NULL0

    • msg_iovmsg_iovlen:定义了要发送的数据。msg_iov 是一个指向 struct iovec 数组的指针,每个 struct iovec 包含以下内容:

    struct iovec {void  *iov_base;    // 数据的起始地址size_t iov_len;     // 数据的长度
    };
    • msg_controlmsg_controllen:用于发送控制信息(如辅助数据)。控制信息通常用于传递文件描述符(通过 SCM_RIGHTS)或其他协议特定的信息。

    • msg_flags:通常设置为 0,但在某些情况下可以设置为 MSG_DONTWAIT(非阻塞模式)或其他标志。

    • flags:

      • 用于控制消息发送的行为。常见的标志包括:

        • MSG_DONTWAIT:非阻塞模式,即使套接字设置为阻塞模式,也会立即返回。

        • MSG_NOSIGNAL:防止发送 SIGPIPE 信号(当对方关闭连接时)。

        • MSG_EOR:表示消息结束(仅适用于某些协议)。

    返回值

    • 成功:返回发送的字节数。

    • 失败:返回 -1,并通过 errno 设置错误码。

    使用场景

    sendmsg() 的主要优势在于它可以同时发送多个数据块(通过 msg_iovmsg_iovlen)和控制信息(通过 msg_controlmsg_controllen)。这使得它特别适合以下场景:

    1. 发送文件描述符:通过 SCM_RIGHTS,可以在进程间传递文件描述符。

    2. 发送大量数据:通过散列(scatter/gather)I/O,可以高效地发送多个内存块。

    3. 高级协议控制:支持协议特定的控制信息。

    recvmsg

    recvmsg() 是一个用于接收消息的系统调用,与 sendmsg() 配合使用,支持从套接字接收复杂的消息。它不仅可以接收普通的数据,还可以接收控制信息(如文件描述符)。recvmsg() 是一个功能强大的工具,特别适用于需要处理散列(scatter/gather)I/O 或传递辅助数据的场景。

    #include <sys/socket.h>ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

     参数说明

    • sockfd:

      • 套接字文件描述符,表示要接收消息的套接字。

    • msg:

      • 指向 struct msghdr 结构的指针,该结构定义了接收消息的格式和内容。

    • msg_namemsg_namelen:用于接收发送方的地址(例如在 UDP 套接字中)。对于已连接的套接字(如 TCP),通常设置为 NULL0
    • msg_iovmsg_iovlen:定义了接收数据的缓冲区。msg_iov 是一个指向 struct iovec 数组的指针。

    • msg_controlmsg_controllen:用于接收控制信息(如辅助数据)。控制信息通常用于接收文件描述符(通过 SCM_RIGHTS)或其他协议特定的信息。

    • msg_flags:由 recvmsg() 设置,表示消息的接收状态(如是否为带外数据)。

    返回值

    • 成功:返回接收到的字节数。

    • 失败:返回 -1,并通过 errno 设置错误码。

    注意事项

    • 控制信息大小msg_controllen 必须足够大,以容纳控制信息。使用 CMSG_SPACE() 宏来计算所需的空间。

    • 文件描述符传递:通过 SCM_RIGHTS 传递文件描述符时,接收端会获得一个有效的文件描述符,但发送端的文件描述符不会被关闭。

    • 协议支持recvmsg()sendmsg() 主要用于套接字编程,但某些协议(如 TCP)可能不支持某些控制信息。

    • msg_flags:接收完成后,msg_flags 会被设置为消息的实际状态(如是否为带外数据)。如果需要检查这些标志,可以在调用后检查 msg_flags 的值。

    示例:使用 sendmsg 和 recvmsg 进行父子进程通信

    #define BUFFER_SIZE 1024// 发送文件描述符
    void send_fd(int sockfd, int fd_to_send) {struct msghdr msg = {0};struct iovec iov[1];char buffer[BUFFER_SIZE] = "Hello from parent";struct cmsghdr *cmsg;int *fd_ptr;// 初始化 msghdr 结构msg.msg_iov = iov;msg.msg_iovlen = 1;iov[0].iov_base = buffer;iov[0].iov_len = strlen(buffer) + 1;// 初始化控制信息msg.msg_control = malloc(CMSG_SPACE(sizeof(int)));msg.msg_controllen = CMSG_SPACE(sizeof(int));cmsg = CMSG_FIRSTHDR(&msg);cmsg->cmsg_level = SOL_SOCKET;cmsg->cmsg_type = SCM_RIGHTS;cmsg->cmsg_len = CMSG_LEN(sizeof(int));fd_ptr = (int *)CMSG_DATA(cmsg);*fd_ptr = fd_to_send;// 发送消息if (sendmsg(sockfd, &msg, 0) == -1) {perror("sendmsg");exit(EXIT_FAILURE);}free(msg.msg_control);
    }// 接收文件描述符
    int recv_fd(int sockfd) {struct msghdr msg = {0};struct iovec iov[1];char buffer[BUFFER_SIZE];struct cmsghdr *cmsg;int *fd_ptr;int received_fd = -1;// 初始化 msghdr 结构msg.msg_iov = iov;msg.msg_iovlen = 1;iov[0].iov_base = buffer;iov[0].iov_len = sizeof(buffer);// 初始化控制信息msg.msg_control = malloc(CMSG_SPACE(sizeof(int)));msg.msg_controllen = CMSG_SPACE(sizeof(int));// 接收消息if (recvmsg(sockfd, &msg, 0) == -1) {perror("recvmsg");exit(EXIT_FAILURE);}// 提取文件描述符cmsg = CMSG_FIRSTHDR(&msg);if (cmsg && cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {fd_ptr = (int *)CMSG_DATA(cmsg);received_fd = *fd_ptr;}free(msg.msg_control);return received_fd;
    }int main() {int sv[2];pid_t pid;// 创建套接字对if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {perror("socketpair");exit(EXIT_FAILURE);}// 创建子进程pid = fork();if (pid == -1) {perror("fork");exit(EXIT_FAILURE);}if (pid == 0) {// 子进程close(sv[0]); // 关闭父进程端的套接字// 接收文件描述符int received_fd = recv_fd(sv[1]);if (received_fd == -1) {fprintf(stderr, "Failed to receive file descriptor.\n");exit(EXIT_FAILURE);}// 从接收到的文件描述符读取数据char buffer[BUFFER_SIZE];ssize_t n = read(received_fd, buffer, sizeof(buffer));if (n == -1) {perror("read");exit(EXIT_FAILURE);}buffer[n] = '\0';printf("Child received message: %s\n", buffer);close(received_fd);close(sv[1]);exit(EXIT_SUCCESS);} else {// 父进程close(sv[1]); // 关闭子进程端的套接字// 打开一个文件描述符(例如标准输入)int fd_to_send = STDIN_FILENO;// 发送文件描述符send_fd(sv[0], fd_to_send);close(sv[0]);wait(NULL); // 等待子进程退出}return 0;
    }

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

    如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

    相关文章

    循环神经网络--NLP基础

    一、简单介绍NLP&#xff08;Natural Language Processing&#xff09;&#xff1a;自然语言处理是人工智能和语言领域的一个分支&#xff0c;它涉及计算机和人类语言之间的相互作用。二、NLP基础概念词表&#xff08;词库&#xff09;&#xff1a;文本数据集出现的所有单词的集…

    【Android】约束布局总结(1)

    三三要成为安卓糕手 零&#xff1a;创建布局文件方式 1&#xff1a;创建步骤ctrl alt 空格 设置根元素2&#xff1a;处理老版本约束布局 在一些老的工程中&#xff0c;constrainlayout可能没有办法被直接使用&#xff0c;这里需要手动添加依赖implementation androidx.const…

    S7-200 SMART 数字量 I/O 组态指南:从参数设置到实战案例

    在工业自动化控制中&#xff0c;PLC 的数字量输入&#xff08;DI&#xff09;和输出&#xff08;DO&#xff09;是连接传感器、执行器与控制系统的 “神经末梢”。西门子 S7-200 SMART 作为一款高性价比的小型 PLC&#xff0c;其数字量 I/O 的灵活组态直接影响系统的稳定性与响…

    可调谐激光器原理与设计 【DFB 与 DBR 激光器剖析】

    可调谐激光器原理与设计 【DFB 与 DBR 激光器剖析】1. 可调谐激光器的原理与分类简介2. DFB 与 DBR 激光器结构原理比较2.1 DFB&#xff08;Distributed Feedback Laser&#xff09;激光器2.2 DBR&#xff08;Distributed Bragg Reflector&#xff09;激光器2.3 DFB 激光器与 D…

    【前端工程化】前端项目开发过程中如何做好通知管理?

    在企业级后台系统中&#xff0c;通知是保障团队协作、监控系统状态和及时响应问题的重要手段。与 C 端产品不同&#xff0c;B 端更关注构建完成、部署状态、异常报警等关键节点的推送机制。 本文主要围绕通知场景、通知内容、通知渠道、自动化集成等方面展开&#xff0c;适用于…

    MySQL 9.4.0创新版发布,AI开始辅助编写发布说明

    2025 年 7 月 22 日&#xff0c;MySQL 9.4.0 正式发布。 作为一个创新版&#xff0c;MySQL 9.4.0 最大的创新应该就是使用 Oracle HeatWave GenAI 作为助手帮助编写版本发布说明了。难道下一步要开始用 AI 辅助编写数据库文档了&#xff1f; 该版本包含的核心功能更新以及问题修…

    基于WebSockets和OpenCV的安卓眼镜视频流GPU硬解码实现

    基于WebSockets和OpenCV的安卓眼镜视频流GPU硬解码实现 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家&#xff0c;觉得好请收藏。点击跳转到网站。 1. 项目概述 本项目旨在实现一个通过WebSockets接收…

    人大金仓 kingbase 连接数太多, 清理数据库连接数

    问题描述 kingbase 连接数太多, 清理数据库连接数 [rootFCVMDZSZNST25041 ~]# su root [rootFCVMDZSZNST25041 ~]# [rootFCVMDZSZNST25041 ~]# su kingbase [kingbaseFCVMDZSZNST25041 root]$ [kingbaseFCVMDZSZNST25041 root]$ ksql could not change directory to "/r…

    SpringMVC相关基础知识

    1. servlet.multipart 大小配置 SpringBoot 文件上传接口中有 MultipartFile 类型的文件参数,上传较大文件时报错: org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size exceeded; nested exception is java.lang.IllegalStateExceptio…

    HCIP第一次实验报告

    一.实验需求及拓扑图&#xff1a;二.实验需求分析根据提供的网络拓扑图和实验要求&#xff0c;以下是对实验需求的详细分析&#xff1a;R5作为ISP:R5只能进行IP地址配置&#xff0c;其所有接口均配置为公有IP地址。认证方式:R1和R5之间使用PPP的PAP认证&#xff0c;R5为主认证方…

    React入门学习——指北指南(第五节)

    React 交互性:过滤与条件渲染 在前文我们学习了 React 中事件处理和状态管理的基础。本节将聚焦两个重要的进阶技巧 ——条件渲染(根据状态动态显示不同 UI)和列表过滤(根据条件筛选数据),这两者是构建交互式应用的核心能力,能让界面根据用户操作呈现更智能的响应。 条…

    学习嵌入式的第二十九天-数据结构-(2025.7.16)线程控制:互斥与同步

    以下是您提供的文本内容的排版整理版本。我已根据内容主题将其分为几个主要部分&#xff08;互斥锁、信号量、死锁、IPC进程间通信、管道操作&#xff09;&#xff0c;并使用清晰的结构组织信息&#xff1a;代码片段用代码块格式&#xff08;指定语言为C&#xff09;突出显示。…

    COZE官方文档基础知识解读第六期 ——数据库和知识库

    一&#xff0c;一键直连数据上传&#xff0c;存储&#xff0c;使用 火山方舟的数据库和知识库的核心&#xff0c;都是基于开源的数据库产品&#xff08;mysql&#xff0c;向量数据库等&#xff09;&#xff0c;将数据库交互的逻辑封装在后端&#xff0c;与前端做耦合&#xff0…

    生产环境使用云服务器(centOS)部署和使用MongoDB

    部署MongoDB流程1. ​安装MongoDB​版本选择建议​CentOS 7​&#xff1a;推荐MongoDB 4.4.x&#xff08;兼容性好&#xff09;​CentOS 8/9​&#xff1a;建议最新稳定版&#xff08;如6.0&#xff09;&#xff0c;需单独安装mongodb-database-tools安装步骤1.添加官方仓库# 添…

    思博伦第二到三层测试仪(打流仪)TestCenter 2U硬件安装及机箱加电_双极未来

    &#xff08;1&#xff09;安装板卡&#xff1a;上图中共 4 个红色线框&#xff0c;上边两个红色线条框住的是机箱的左右两侧导轨&#xff0c;下边两条红色 线条框住的是板卡拉手条&#xff08;用于承载板卡PCB的金属板&#xff09;左右两边的边沿。 安装时将拉手条两边的边沿与…

    【华为】笔试真题训练_20250611

    本篇博客旨在记录自已的笔试刷题的练习&#xff0c;里面注有详细的代码注释以及和个人的思路想法&#xff0c;希望可以给同道之人些许帮助。本人也是小白&#xff0c;水平有限&#xff0c;如果文章中有什么错误或遗漏之处&#xff0c;望各位可以在评论区指正出来&#xff0c;各…

    新浪微博APP v14.5.0:连接世界的社交媒体平台

    新浪微博APP 是一款广受欢迎的社交媒体应用程序&#xff0c;凭借其强大的功能和丰富的社交生态&#xff0c;成为用户获取信息、表达观点、互动交流的重要平台。最新版 v14.5.0 内置了微博助手 v2.3.0&#xff0c;进一步提升了用户体验和功能多样性。 软件功能 1. 发布微博 用…

    静态枚举返回(简单实现字典功能)

    枚举缓存策略的实现与应用 通过静态Map缓存枚举类的Class对象&#xff0c;避免每次请求时重复反射加载。核心实现是一个包含枚举类名与对应Class映射的Registry类&#xff1a; public class EnumRegistry {private static final Map<String, Class<?>> ENUM_MAP …

    深分页性能问题分析与优化实践

    在日常测试工作中&#xff0c;我们经常会遇到分页查询接口&#xff0c;例如&#xff1a; GET /product/search?keyword&pageNum1&pageSize10乍看之下&#xff0c;这样的分页接口似乎并无性能问题&#xff0c;响应时间也很快。但在一次性能压测中&#xff0c;我们复现了…

    LeetCode——1957. 删除字符使字符串变好

    通过万岁&#xff01;&#xff01;&#xff01; 题目&#xff1a;给你一个字符串&#xff0c;然后让你删除几个字符串&#xff0c;让他变成好串&#xff0c;好串的定义就是不要出现连续的3个一样的字符。思路&#xff1a;首先就是要遍历字符串。我们将要返回的字符串定义为ret&…