是listen函数阻塞等待连接,还是accept函数阻塞等待连接?
这两个函数的名字,听起来像listen一直在阻塞监听,有连接了就accept,但其实不是的。
调用
listen()
后,程序会立即返回,继续执行后续代码,不会因为等待客户端连接而停止。listen()
的主要作用是设置套接字的状态,告诉内核该套接字现在处于监听状态,可以接受客户端的连接请求。
accept()
函数用于接受客户端的连接请求。当有客户端连接到服务器时,accept()
会创建一个与客户端通信的新套接字,并返回这个新套接字的文件描述符。在默认情况下,accept()
函数是阻塞的。这意味着如果调用accept()
时没有客户端连接请求到来,程序会一直阻塞在accept()
调用处,直到有新的客户端连接请求到达。然而,如果将套接字设置为非阻塞模式,那么accept()
在没有客户端连接请求时会立即返回一个错误,而不是阻塞等待。
socket服务端accept之后就recv读数据,会不会这时候客户端还没send结束,导致没有数据可读呢?
socket服务端在accept之后就recv读数据,确实可能会遇到客户端还没send结束,导致没有数据可读的情况。以下是对这一问题的详细分析:
TCP协议特性
流式传输:TCP是面向流的协议,它将数据作为连续的字节流进行传输,而不是一个个独立的数据包。这意味着服务端的recv函数不会因为客户端未发送完整数据包而阻塞等待,而是会返回当前已接收到的数据。如果客户端还没有发送完数据,服务端的recv可能会读取到部分数据,甚至可能读取不到任何数据(即返回0),这取决于客户端发送数据的进度和网络状况。
缓冲机制:TCP协议栈会在内核中为套接字维护接收缓冲区。当客户端发送数据时,数据首先被存储在服务端的接收缓冲区中。服务端的recv函数从接收缓冲区中读取数据。如果客户端发送的数据还没有完全到达服务端的接收缓冲区,或者缓冲区中暂时没有可供读取的数据,recv函数可能会返回较少的数据量,甚至立即返回(如果在非阻塞模式下)。
服务端处理方式
阻塞模式:在默认的阻塞模式下,如果服务端的recv函数在调用时发现没有数据可读,它会阻塞在那里,直到有数据到达或者发生错误(如客户端关闭连接)才会返回。这种情况下,即使客户端还没有发送完数据,服务端也会一直等待,直到有数据可以读取。
非阻塞模式:如果服务端将套接字设置为非阻塞模式,那么recv函数在没有数据可读时不会阻塞,而是立即返回一个错误代码(通常是EAGAIN或EWOULDBLOCK),表示目前没有数据可供读取。这就需要服务端程序能够正确处理这种非阻塞情况下的返回值,避免因为误判而导致程序异常。
客户端发送情况
发送速度:如果客户端发送数据的速度比较慢,或者网络状况不佳导致数据传输延迟较大,那么服务端在接受连接后可能很快就调用了recv函数,但此时客户端可能只发送了一部分数据甚至还没有开始发送数据。在这种情况下,服务端的recv函数可能读取不到任何数据或者只能读取到部分数据。
分批次发送:有些应用程序可能会将数据分批次发送。例如,客户端先发送一个数据包头,然后暂停一下再发送数据内容。如果服务端在客户端发送包头后就立即调用recv函数,那么它可能只能读取到包头信息,而无法读取到后续的数据内容。
综上所述,为了避免这种情况带来的问题,服务端程序需要正确地处理recv函数的返回值,并且可能需要结合使用其他机制(如定时器、心跳包等)来确保数据的完整性和通信的可靠性。
服务端是一次性读完所有的数据,还是分次读取?
服务端的读取策略
阻塞模式:在默认情况下,recv函数是阻塞的。如果服务端调用recv函数时接收缓冲区中没有数据可读,那么recv函数会阻塞在那里,直到有数据到达或者发生错误(如客户端关闭连接)为止。在这种情况下,服务端可能会一直等待,直到接收到完整的数据或连接关闭。然而,这种策略可能导致服务端在高并发场景下性能下降,因为每个连接都可能因为等待数据而占用一个线程或进程。
非阻塞模式:为了提高性能和响应速度,服务端可以将套接字设置为非阻塞模式。在非阻塞模式下,recv函数在没有数据可读时不会阻塞,而是立即返回一个错误代码(通常是EAGAIN或EWOULDBLOCK)。这样,服务端可以在没有数据可读时继续处理其他任务或检查其他连接的状态。然而,这也要求服务端实现更复杂的逻辑来处理数据的读取和解析。
分批读取:考虑到网络延迟、数据包大小以及应用层协议的设计,服务端通常会采用分批读取的策略。即服务端每次只读取一定量的数据(如固定大小的缓冲区),然后进行处理。如果接收到的数据不足以构成一个完整的消息或数据单元,服务端会继续等待并读取后续到达的数据。这种策略可以平衡性能和资源利用率,避免因为等待单个大消息而导致整个服务端性能下降。
综上所述,服务端是否一次性读完所有的数据还是分次读取并没有一个固定的答案。它取决于具体的应用场景、网络状况、数据量大小以及服务端的设计和实现方式。
accept4函数
accept4
是accept
的一个扩展版本,它提供了更多的功能和灵活性。
accept4
和accept
都是用于在监听套接字上接受新的连接请求的系统调用,但它们之间存在一些关键的区别。以下是对这两个函数的详细比较:参数列表
accept
:原型为
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
。
accept4
:原型为
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
。与accept
相比,多了一个flags
参数,用于设置接受的套接字属性。如设置非阻塞模式或关闭执行时自动关闭文件描述符等。
socket请求和url请求有什么区别?
最近遇到一个问题,请别人帮忙看的时候,对方让提供访问的域名,但是我其实是通过socket去发起请求的,所以没有什么域名,于是就产生了这个疑问:socket请求和url请求有什么区别?
Socket是计算机网络中用于实现不同主机或进程间通信的端点抽象,基于TCP/IP协议族,通过创建Socket对象,指定目的IP地址和端口号,建立连接后实现数据的双向传输。
URL(Uniform Resource Locator)是统一资源定位符,用于完整地描述Internet上网页或其他资源的地址。URL请求通常指的是使用HTTP协议向这些资源地址发起的请求。
想到一个问题,那就是socket中,什么场景下需要结合poll/epoll来使用。
我们知道,poll是多路监听,监听多个socket,如果只有一个socket需要监听,应该用不上poll吧?直接bind-listen-accept即可,有连接来了就recv等等。但是,如果有多个socket,那么这时候就可以使用poll了吧?此时,是先把每个socketfd给bind-listen,然后再poll?poll到了之后就直接accept?accept通常也是阻塞的,这时候是不是因为有poll了所以肯定是有连接的因此不会阻塞了?
问下deepseek看看
使用 Socket 后是否需要结合 poll(或 select/epoll),取决于具体的通信模式、性能需求以及程序架构。以下是关键分析,帮助你判断是否需要使用 poll:
不需要 poll 的情况(简单阻塞模式)
适用场景
单线程顺序处理:程序只需处理一个 Socket,且可以接受阻塞等待。
简单请求-响应模型:客户端发送请求后,同步等待服务端回复,无并发需求。
低实时性要求:对延迟不敏感,允许线程在 recv() 或 accept() 时阻塞。
示例代码(无 poll)
// 客户端阻塞发送和接收 send(sockfd, request, strlen(request), 0); recv(sockfd, response, sizeof(response), 0); // 阻塞直到数据到达// 服务端阻塞监听 while (1) {int client_fd = accept(sockfd, NULL, NULL); // 阻塞直到新连接recv(client_fd, buf, sizeof(buf), 0); // 阻塞读取数据 }
优点
代码简单,逻辑直观。
无需管理多路复用或事件驱动。
缺点
无法同时处理多个连接或事件。
线程在阻塞时会闲置,浪费资源。
需要 poll 的情况(高性能/并发场景)
适用场景
多路复用:需要同时监听多个 Socket(如多个客户端连接或混合 TCP/UDP/RPMSG)。
非阻塞操作:避免线程因某个 Socket 的延迟而卡死,提升实时性。
超时控制:在指定时间内等待数据,超时后执行其他逻辑。
事件驱动架构:如高性能服务器、实时系统或 GUI 应用。
示例代码(使用 poll)
#include <poll.h>struct pollfd fds[2]; fds[0].fd = sockfd1; // RPMSG Socket fds[0].events = POLLIN; fds[1].fd = sockfd2; // TCP Socket fds[1].events = POLLIN;while (1) {int ret = poll(fds, 2, 1000); // 超时 1 秒if (ret < 0) {perror("poll");break;} else if (ret == 0) {printf("Timeout, no events.\n");continue;}// 检查 RPMSG Socket 是否有数据if (fds[0].revents & POLLIN) {recv(fds[0].fd, buf, sizeof(buf), 0);printf("RPMSG data: %s\n", buf);}// 检查 TCP Socket 是否有新连接if (fds[1].revents & POLLIN) {int client_fd = accept(fds[1].fd, NULL, NULL);// 处理新连接...} }
优点
高效并发:单线程即可处理多个 Socket。
灵活控制:支持超时、非阻塞和优先级事件处理。
资源友好:减少线程数量,降低上下文切换开销。
缺点
代码复杂度较高,需处理事件状态(revents)。
需要理解 poll/select/epoll 的差异(epoll 更适合大规模连接)。
在
poll
的使用中,既可以监听 监听 Socket(Listening Socket),也可以监听 已连接 Socket(Connected Socket),具体取决于你的程序角色(服务端或客户端)和需求。以下是详细说明:监听 监听 Socket(服务端场景)
用途:服务端通过监听 Socket 接受新的客户端连接(accept())。
触发条件:当有新连接请求到达时,poll 会返回该 Socket 的可读事件(POLLIN)。
代码示例:
int listen_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建监听 Socket bind(listen_fd, ...); // 绑定地址 listen(listen_fd, 5); // 开始监听struct pollfd fds[1]; fds[0].fd = listen_fd; // 监听 Socket fds[0].events = POLLIN; // 关注可读事件(新连接)while (1) {int ret = poll(fds, 1, 1000); // 等待事件if (ret > 0 && (fds[0].revents & POLLIN)) {int client_fd = accept(listen_fd, NULL, NULL); // 接受新连接printf("New client connected: %d\n", client_fd);} }
关键点:
仅服务端需要监听“监听 Socket”。
POLLIN 表示有新连接到达,需调用 accept()。
监听 已连接 Socket(服务端/客户端场景)
用途:检测已建立的连接是否有数据可读/可写,或是否发生错误。
触发条件:
POLLIN:数据到达,可调用 recv()。
POLLOUT:Socket 可发送数据(非阻塞模式下有用)。
POLLERR:Socket 发生错误。
代码示例:
int client_fd = accept(listen_fd, ...); // 服务端获取已连接 Socket // 或 int client_fd = connect(...); // 客户端建立连接struct pollfd fds[1]; fds[0].fd = client_fd; // 已连接 Socket fds[0].events = POLLIN; // 关注可读事件while (1) {int ret = poll(fds, 1, 1000);if (ret > 0 && (fds[0].revents & POLLIN)) {char buf[1024];int len = recv(client_fd, buf, sizeof(buf), 0); // 读取数据if (len > 0) printf("Received: %s\n", buf);} }
关键点:
适用于服务端(处理客户端连接)和客户端(监听服务端回复)。
可同时监听多个已连接 Socket(多客户端场景)。
比如,我们可以把多个连接socket放一起监听,有数据再去recv。
上面两种场景,总结就是,在accept之前poll,有连接就accept;再就是在recv之前poll,有数据就recv。这种场景下,accept或者recv设置成非阻塞也是可以的。
同时监听 监听 Socket 和 已连接 Socket(服务端经典模型)
服务端通常需要同时监听新连接和已连接 Socket 的数据:
struct pollfd fds[MAX_CLIENTS + 1]; fds[0].fd = listen_fd; // 监听 Socket fds[0].events = POLLIN;int nfds = 1; // 当前监听的 Socket 数量while (1) {int ret = poll(fds, nfds, -1); // 阻塞等待事件if (ret <= 0) continue;// 1. 检查新连接if (fds[0].revents & POLLIN) {int client_fd = accept(listen_fd, NULL, NULL);fds[nfds].fd = client_fd; // 将新连接加入 poll 监听fds[nfds].events = POLLIN;nfds++;}// 2. 检查已连接 Socket 的数据for (int i = 1; i < nfds; i++) {if (fds[i].revents & POLLIN) {char buf[1024];int len = recv(fds[i].fd, buf, sizeof(buf), 0);if (len <= 0) {close(fds[i].fd); // 连接关闭或错误fds[i].fd = -1; // 从 poll 中移除} else {printf("Data from client %d: %s\n", fds[i].fd, buf);}}} }
若是监听 Socket 的
POLLIN
,则调用accept()
;若是已连接 Socket,则调用recv()
。
accept跟三次握手有什么关系?
在 TCP 协议中,accept() 的调用时机确实与三次握手有关,但它的行为还受到操作系统内核和 Socket 配置的影响。以下是详细分析:
正常情况下:accept() 在三次握手完成后返回
流程:
客户端发送 SYN → 服务端(监听 Socket 收到 SYN,进入 SYN_RCVD 状态)。
服务端回复 SYN+ACK → 客户端。
客户端发送 ACK → 服务端(完成三次握手,连接进入 ESTABLISHED 状态,并加入内核的已完成连接队列)。
服务端的 accept() 从已完成队列中取出一个连接,返回新的已连接 Socket。
关键点:
accept() 本身不参与三次握手,它只是从内核队列中获取已建立的连接。
如果队列为空,accept() 会阻塞(默认行为),直到有新连接到达。
异常情况:accept() 可能延迟返回
(1) 已完成连接队列已满
内核会维护一个已完成连接队列(accept queue),其大小由 listen(fd, backlog) 的 backlog 参数和系统配置(如 net.core.somaxconn)共同决定。
如果队列满:
客户端发送的第三个 ACK 可能被丢弃,导致客户端认为连接已建立,但服务端 accept() 无法立即返回。
客户端后续发送的数据会被服务端忽略(触发客户端重传)。
(2) 监听 Socket 设置为非阻塞
如果监听 Socket 设置为非阻塞(fcntl(fd, F_SETFL, O_NONBLOCK)):
当已完成队列为空时,accept() 会立即返回 -1,并设置 errno=EAGAIN 或 EWOULDBLOCK。
需结合 poll/epoll 监听 POLLIN 事件后再调用 accept()。
(3) 客户端快速关闭连接
如果客户端在三次握手完成后立即发送 RST(如端口未开放),accept() 可能返回错误(取决于操作系统实现)。
close和四次挥手有什么关系?
在 TCP 协议中,
close()
系统调用会触发连接的四次挥手过程,这是 TCP 可靠关闭连接的标准机制。以下是详细分析:四次挥手的基本流程
当一端(假设为客户端)调用
close()
关闭 Socket 时,TCP 会启动四次挥手:FIN(客户端 → 服务端)
客户端调用
close()
后发送FIN
报文,进入FIN_WAIT_1
状态,表示不再发送数据。ACK(服务端 → 客户端)
服务端收到
FIN
后回复ACK
,进入CLOSE_WAIT
状态,此时服务端可能仍有数据要发送。FIN(服务端 → 客户端)
注意,这时候,只能服务端向客户端发消息,因为客户端已经不再发消息了。
服务端调用
close()
后发送FIN
报文,进入LAST_ACK
状态。ACK(客户端 → 服务端)
客户端收到
FIN
后回复ACK
,进入TIME_WAIT
状态,服务端收到后连接彻底关闭。上面其实就是两轮FIN和ACK。客户端先向服务端发送FIN(第一次),接着就收到服务端的ACK(第二次),这时候,客户端虽然不能发送但是还能接收数据,服务端也能向客户端发送数据。这时候,双方还处于半关闭的状态。接着等服务端发送FIN(第三次),客户端再返回ACK(第四次),服务端收到后就马上进入CLOSED状态,客户端在一段定时时间后也会进入CLOSED状态,这时双方才真正地断开了连接。
close()
在四次挥手中的作用(1) 主动关闭方(客户端)
close(sockfd); // 发送 FIN,启动四次挥手
行为:
内核立即发送
FIN
报文(若发送缓冲区无残留数据)。若对端未关闭,连接进入
FIN_WAIT_1
状态。(2) 被动关闭方(服务端)
// 服务端检测到 FIN 后,需显式调用 close() 完成挥手 char buf[1024]; int len = recv(sockfd, buf, sizeof(buf), 0); if (len == 0) { // 收到 FIN(EOF)close(sockfd); // 发送 FIN,完成四次挥手 }
行为:
收到
FIN
后,内核自动回复ACK
,但需应用程序调用close()
发送最终的FIN
。总结
close()
是触发四次挥手的应用程序接口,而四次挥手是 TCP 层的行为。完整挥手需要双方协作:主动关闭方发起,被动关闭方响应。
理解状态机(如
TIME_WAIT
)对调试高并发服务至关重要。
直接使用socket编程,是不是就不涉及什么应用层协议
直接使用 Socket 编程 仍然可能涉及 应用层协议,但具体是否涉及取决于你的实现方式。以下是关键分析:
Socket 是传输层接口,不强制应用层协议
Socket 编程(如 Linux 的
socket()
、Windows 的Winsock
)提供的是 传输层(TCP/UDP)的通信能力,它本身不限定应用层协议。你可以完全自定义数据格式,不依赖 HTTP、FTP 等标准协议。
也可以手动实现简单协议,例如:
// 发送一个自定义结构体(无标准应用层协议) struct CustomData {int id;float value;char name[32]; }; send(socket_fd, &data, sizeof(data), 0);
这种场景下,没有使用标准应用层协议,但仍需双方约定数据格式(类似私有协议)。
什么情况下不涉及应用层协议?
如果满足以下条件,可以认为 不涉及标准应用层协议:
数据格式完全由开发者定义(如裸二进制数据、自定义结构体)。
通信双方直接解析原始字节流,无需遵循 HTTP/MQTT 等规范。
仅用于局域网或特定设备间通信,无需跨网络兼容性。
示例:
嵌入式设备间通过 UDP 发送传感器数据(直接
sendto()
二进制数据)。游戏客户端和服务器的自定义协议(如 Protobuf + TCP)。
什么情况下仍会涉及应用层协议?
即使直接使用 Socket,以下情况仍会 隐式或显式依赖应用层协议:
使用标准协议库
例如:在 Socket 上手动实现 HTTP 请求(拼接
GET /path HTTP/1.1\r\nHost:...
)。虽然未用
libcurl
,但仍遵循 HTTP 规范(应用层协议)。依赖第三方序列化工具
例如:通过 Socket 发送 JSON/XML(虽非标准协议,但属于应用层数据格式)。
混合使用底层和高级协议
例如:用 Socket 传输 TLS 加密数据(TLS 属于会话层/表示层,但通常归类为应用层安全协议)。
典型场景对比
场景 是否涉及应用层协议? 说明 通过 Socket 发送 0xAA 0xBB ❌ 不涉及 纯二进制,无格式约定。 用 Socket 实现 HTTP 客户端 ✔️ 涉及 需遵循 HTTP 报文格式。 自定义协议(如 ID+LEN+DATA) ❌ 不涉及(严格来说算私有协议) 双方自行约定格式。 使用 Socket + Protobuf ⚠️ 部分涉及 Protobuf 是数据序列化格式,非标准协议。 关键结论
Socket 本身不强制应用层协议,但实际通信通常需要某种数据格式约定(即使是私有协议)。
完全避免应用层协议:需直接操作原始字节流,且双方硬编码解析逻辑。
“不涉及应用层协议” ≠ 无协议:开发者仍需定义数据结构和交互规则(只是未用现成标准)。
如果需要极简通信(如单片机间传输),可直接用 Socket + 二进制数据;若需兼容性(如 Web 服务),则需实现 HTTP 等应用层协议。
大多数网络应用层协议都是对socket的封装吗
是的,大多数网络应用层协议都可以看作是对 Socket API 的封装或更高层次的抽象,但具体实现方式可能有所不同。以下是详细分析:
1. Socket 的作用
Socket 是操作系统提供的 网络通信基础接口,位于传输层(TCP/UDP)之上,负责:
建立连接(如
connect()
、accept()
)数据传输(如
send()
、recv()
)管理网络地址和端口(如
bind()
、listen()
)应用层协议需要通过 Socket 实现底层通信,但协议本身会定义更高级的规则(如消息格式、状态机等)。
2. 应用层协议与 Socket 的关系
(1) 直接基于 Socket 实现的协议
许多经典协议直接封装 Socket API,例如:
HTTP/1.1:通过 TCP Socket 传输文本请求和响应。
SMTP/POP3:基于 TCP Socket 的明文命令交互。
FTP:使用两个 Socket(控制通道 + 数据通道)。
这些协议在 Socket 之上定义了 报文格式 和 交互逻辑(如 HTTP 的
GET / POST
方法)。(2) 使用中间库封装的协议
现代开发中,协议可能通过更高级的库实现,但这些库底层仍依赖 Socket:
HTTP/2、gRPC:使用库(如 libcurl、gRPC Core)封装 Socket,支持多路复用、二进制帧等。
WebSocket:基于 TCP Socket,但通过 HTTP 升级握手建立连接。
(3) 非 Socket 的实现
少数场景下,协议可能绕过标准 Socket:
自定义传输层:如 QUIC(基于 UDP 的 HTTP/3)直接操作 UDP Socket,并实现可靠性保证。
内核旁路技术:如 DPDK,但这类方案仍需底层网络接口支持。
3. 为什么说“封装”是合理的?
抽象复杂性:Socket 只提供字节流传输,而应用层协议需要处理:
报文分割(如 HTTP 的
Content-Length
)。状态管理(如 TLS 握手)。
错误恢复(如 SMTP 的重试机制)。
标准化交互:协议定义了语义(如 RESTful API 的 URI 和 Method),而 Socket 仅负责传输。
4. 例外情况
纯消息队列协议:如 AMQP、MQTT,可能依赖更底层的 Socket 封装库(如 ZeroMQ)。
RPC 框架:如 gRPC,抽象了 Socket,开发者只需关注接口定义(Protocol Buffers)。
总结
大多数应用层协议可以视为 Socket 的语义化封装,但现代开发中通常会通过高级库(如 HTTP 客户端、gRPC 框架)进一步隐藏 Socket 细节。协议的核心价值在于定义 通信规则,而 Socket 是其实现的 底层工具。
常见地址宏定义
Linux中常见的一些socket ip地址常量宏
在Linux编程中,使用socket进行网络通信时,经常需要指定IP地址。为了方便和标准化,系统定义了一些常见的IP地址常量宏。这些宏通常在头文件
<netinet/in.h>
中定义。以下是一些常见的IP地址常量宏:INADDR_ANY
值:
0x00000000
IP地址:
0.0.0.0
用途: 用于绑定一个套接字到所有可用的网络接口上。这通常用于服务器应用程序,使其能够从任何网络接口接收外部连接。
INADDR_LOOPBACK
值:
0x7f000001
IP地址:
127.0.0.1
用途: 表示本地回环地址(loopback address)。用于在同一台机器上的进程之间进行通信,不涉及任何网络接口。
INADDR_BROADCAST
值:
0xffffffff
IP地址:
255.255.255.255
用途: 表示广播地址。用于发送数据包到网络上的所有主机。
INADDR_NONE
值:
0xffffffff
IP地址:
255.255.255.255
用途: 表示无效的IP地址。当解析IP地址失败时,返回这个值。
IN_LOOPBACKNET
值:
127
用途: 表示回环网络号。用于识别回环地址。
IN_CLASSA_NET
值:
0xff000000
用途: 表示A类网络的网络掩码。
IN_CLASSB_NET
值:
0xffff0000
用途: 表示B类网络的网络掩码。
IN_CLASSC_NET
值:
0xffffff00
用途: 表示C类网络的网络掩码。
IN_CLASSD_NET
值:
0xe0000000
用途: 表示D类网络的网络掩码。
IN_MULTICAST(i)
值:
0xe0000000 + (i)
用途: 表示多播地址范围。
IN_EXPERIMENTAL(i)
值:
0xf0000000 + (i)
用途: 表示实验性地址范围。
IN_RFC3021_MASK
值:
0xffffff00
用途: 表示RFC 3021定义的默认子网掩码。
IN_CLASSA_NSHIFT, IN_CLASSA_HOST, IN_CLASSA_MAX
值:
8
,0xffffff00
,16777216
用途: 分别表示A类网络的子网位数、主机部分的掩码和最大主机数。
IN_CLASSB_NSHIFT, IN_CLASSB_HOST, IN_CLASSB_MAX
值:
16
,0xffff0000
,65536
用途: 分别表示B类网络的子网位数、主机部分的掩码和最大主机数。
IN_CLASSC_NSHIFT, IN_CLASSC_HOST, IN_CLASSC_MAX
值:
24
,0xffffff00
,256
用途: 分别表示C类网络的子网位数、主机部分的掩码和最大主机数。
这些常量宏在编写网络程序时非常有用,可以帮助开发者更方便地处理IP地址和网络相关的操作。
重点理解以下四种:
INADDR_ANY
INADDR_LOOPBACK
INADDR_BROADCAST
INADDR_NONE
INADDR_LOOPBACK
和INADDR_ANY
是两个在网络编程中常用的常量,它们分别代表不同的IP地址,用于指定服务器绑定的地址。以下是它们的主要区别:INADDR_LOOPBACK (127.0.0.1)
用途: 表示本地回环地址(loopback address)。
功能: 用于在同一台机器上的进程之间进行通信,不涉及任何网络接口。
典型场景: 主要用于测试和开发环境,确保服务只接受来自本地主机的连接。
安全性: 因为只有本地进程可以连接到这个地址,所以它提供了一种简单的方法来限制访问,仅允许本地访问。
INADDR_ANY (0.0.0.0)
用途: 表示所有可用的网络接口。
功能: 用于绑定一个套接字到所有可用的网络接口上,使得该服务可以从任何网络接口接收连接请求。
典型场景: 通常用于服务器应用程序,使其能够从任何网络接口接收外部连接。
灵活性: 提供更高的灵活性,因为服务器可以接受来自任何网络接口的连接,这在多网卡或多IP地址的环境中特别有用。
INADDR_BROADCAST
用途:表示广播地址,用于发送数据包到网络上的所有主机。
IP地址表示:
255.255.255.255
。使用场景:通常用于局域网中的广播通信,不会在当前路由器进行转发。
INADDR_NONE
用途:表示无效的IP地址,当解析IP地址失败时,返回这个值。
IP地址表示:
255.255.255.255
。使用场景:用于检查IP地址是否有效,或者在某些情况下表示未指定的IP地址。
INADDR_ANY 是一个在网络编程中常用的选项,它用于 socket 函数中,表示服务器应该绑定到所有可用的IP地址上。在多网卡的环境下,这意味着服务器可以接收到发送到任何网卡的连接请求。具体来说,INADDR_ANY 被定义为 0.0.0.0,这个地址在实际上代表了“不确定地址”或“所有地址”、“任意地址”。
代码示例
在使用 socket 进行网络编程时,如果你想让你的服务器监听某个端口,你可以在调用 bind() 函数时使用 INADDR_ANY。这样,无论数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,服务器都可以接收到。例如:
深入理解
当你的服务器有多个网卡,每个网卡上有不同的IP地址时,使用 INADDR_ANY 可以避免确定服务器上有哪些网络端口的麻烦。这在服务器操作系统可能随时增减IP地址的情况下尤其有用。此外,当服务器程序在 0.0.0.0 这个地址上进行监听时,它可以接收到所有网卡上指定端口的数据。
重要考虑
使用 INADDR_ANY 不会导致收到重复的数据包或重复发送数据。这是因为路由的关系,从客户端来的IP包只可能到达其中一个网卡。同时,服务器进程在发送数据时,操作系统会根据维护的路由表决定数据包应该从哪一个出站网关向目标发送。路由表记录有优先级别,不同的操作系统可能会有不同的处理方式。
客户端的情况
对于客户端,如果绑定 INADDR_ANY,在 TCP 连接中,系统调用会将其绑定到一个具体的IP地址。对于 UDP,即使使用 connect() 系统调用也不会绑定到一个具体地址,因为 UDP 是面向无连接的,系统只是将目标地址的信息记录在内部的socket数据结构中,供以后使用。
总的来说,INADDR_ANY 是一个非常有用的选项,它允许服务器在多网卡环境中灵活地接收来自任何网卡的连接请求,而无需单独绑定每个网卡的IP地址。这在实际应用中可以大大简化网络编程的复杂性,并提高服务器的可移植性和灵活性。
网络地址大小端转换
Linux网络通信----htonl()、htons()、ntohl()、ntohs()四个函数
参考:
Linux网络通信----htonl()、htons()、ntohl()、ntohs()四个函数_linux ntohs-CSDN博客
ip点分十进制
注意,C语言中不能直接识别ip地址的点分十进制,我们一般都要写对应的十六进制,比如:
0.0.0.0对应0x00000000;
127.0.0.1对应0x7f000001;
255.255.255.255对应0xffffffff
Netlink通信
Netlink内核和用户空间之间的通信
socket(AF_NETLINK, SOCK_RAW, 0)使用示例
在Linux中,
socket(AF_NETLINK, SOCK_RAW, 0)
用于创建一个Netlink套接字。Netlink是一种IPC(进程间通信)机制,主要用于内核和用户空间之间的通信。以下是一个简单的示例代码,演示如何使用Netlink套接字进行基本操作:#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <linux/netlink.h>#define NETLINK_USER 31 #define MAX_PAYLOAD 1024 /* maximum payload size*/struct sockaddr_nl src_addr, dest_addr; struct nlmsghdr *nlh = NULL; struct iovec iov; int sock_fd; struct msghdr msg;int main() {// Create a Netlink socketsock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_USER);if (sock_fd < 0) {perror("socket creation failed");return -1;}// Initialize source address structurememset(&src_addr, 0, sizeof(src_addr));src_addr.nl_family = AF_NETLINK;src_addr.nl_pid = getpid(); /* self pid */src_addr.nl_groups = 0; /* not in mcast groups */// Bind the socket to the source addressif (bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr)) < 0) {perror("bind failed");close(sock_fd);return -1;}// Initialize destination address structurememset(&dest_addr, 0, sizeof(dest_addr));dest_addr.nl_family = AF_NETLINK;dest_addr.nl_pid = 0; /* For Linux Kernel */dest_addr.nl_groups = 0; /* unicast */// Allocate memory for the netlink message header and payloadnlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);nlh->nlmsg_pid = getpid();nlh->nlmsg_flags = 0;// Fill the netlink message payloadstrcpy(NLMSG_DATA(nlh), "Hello from user space!");// Prepare the I/O vectoriov.iov_base = (void *)nlh;iov.iov_len = nlh->nlmsg_len;memset(&msg, 0, sizeof(msg));msg.msg_name = (void *)&dest_addr;msg.msg_namelen = sizeof(dest_addr);msg.msg_iov = &iov;msg.msg_iovlen = 1;// Send the message to the kernelif (sendmsg(sock_fd, &msg, 0) < 0) {perror("sendmsg failed");free(nlh);close(sock_fd);return -1;}printf("Message sent to kernel\n");// Clean upfree(nlh);close(sock_fd);return 0; }
解释:
创建Netlink套接字:使用
socket(AF_NETLINK, SOCK_RAW, NETLINK_USER)
创建一个Netlink套接字。NETLINK_USER
是一个自定义的协议号,可以根据需要更改。初始化源地址结构:设置源地址结构体
src_addr
,包括协议族、进程ID和组ID。绑定套接字:将套接字绑定到源地址。
初始化目标地址结构:设置目标地址结构体
dest_addr
,包括协议族、目标进程ID和组ID。这里的目标进程ID为0,表示发送给内核。分配并填充Netlink消息:分配内存并填充Netlink消息头和负载数据。
准备I/O向量:设置I/O向量以指向Netlink消息。
发送消息:使用
sendmsg
函数将消息发送到内核。清理资源:释放分配的内存并关闭套接字。
这个示例展示了如何创建和使用Netlink套接字与内核进行通信。根据具体需求,可以扩展此示例以处理更复杂的Netlink消息和响应。
setsockopt()
函数
setsockopt()
函数是一个用于在Linux和其他Unix-like系统中设置套接字选项的系统调用。它允许程序员调整套接字的行为或特性,以适应不同的网络环境和应用程序需求。以下是关于setsockopt()
函数的相关介绍:函数原型:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd
:要操作的套接字文件描述符。level
:指定套接字级别的选项,如SOL_SOCKET(通用选项)、IPPROTO_TCP(TCP层选项)等。optname
:选项名称,是一个整数值,代表具体的选项,如SO_REUSEADDR(重用地址)或SO_LINGER(linger选项)等。optval
:指向包含选项值的内存区域的指针。optlen
:选项值的长度。常用选项:
- 套接字层选项(
SOL_SOCKET
):如SO_REUSEADDR、SO_KEEPALIVE等。- TCP协议层选项(
IPPROTO_TCP
):如TCP_NODELAY、TCP_KEEPIDLE等。- IP协议层选项(
IPPROTO_IP
):如IP_TTL。使用示例:
setsockopt()
函数用于设置套接字选项,以调整套接字的行为。以下是如何使用setsockopt()
函数设置套接字选项的具体步骤和示例代码:步骤
- 创建套接字:使用
socket()
函数创建一个套接字。- 设置选项:使用
setsockopt()
函数设置所需的套接字选项。- 绑定、监听或连接:根据需要绑定到本地地址、监听连接请求或连接到远程服务器。
- 发送/接收数据:使用
send()
、recv()
等函数进行数据传输。- 关闭套接字:使用
close()
函数关闭套接字。示例代码
以下是一个具体的示例,展示如何使用
setsockopt()
函数设置SO_REUSEADDR选项,允许绑定到已被使用的地址。#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h>#define PORT 8080 #define BUFFER_SIZE 1024int main() {int server_fd;struct sockaddr_in address;int opt = 1;int addrlen = sizeof(address);char buffer[BUFFER_SIZE] = {0};const char *hello = "Hello from server";// 创建套接字文件描述符if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 设置SO_REUSEADDR选项if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {perror("setsockopt");close(server_fd);exit(EXIT_FAILURE);}// 填充服务器信息结构体address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY; // 自动选择IP地址address.sin_port = htons(PORT);// 绑定套接字到指定端口if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");close(server_fd);exit(EXIT_FAILURE);}// 监听连接请求if (listen(server_fd, 3) < 0) {perror("listen");close(server_fd);exit(EXIT_FAILURE);}printf("Server listening on port %d\n", PORT);// 接受客户端连接int new_socket;if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");close(server_fd);exit(EXIT_FAILURE);}// 读取客户端消息read(new_socket, buffer, BUFFER_SIZE);printf("Message from client: %s\n", buffer);// 发送响应消息给客户端send(new_socket, hello, strlen(hello), 0);printf("Hello message sent\n");// 关闭套接字close(new_socket);close(server_fd);return 0; }
解释
创建套接字:使用
socket()
函数创建一个TCP套接字。设置SO_REUSEADDR选项:通过
setsockopt()
函数设置SO_REUSEADDR选项,允许绑定到已被使用的地址。这对于开发和调试非常有用,因为可以快速重启服务器而无需等待端口释放。绑定、监听和接受连接:将套接字绑定到指定的端口并开始监听连接请求。当有客户端连接时,接受连接并处理数据。
发送/接收数据:从客户端读取数据并发送响应消息。
关闭套接字:在完成通信后,关闭套接字以释放资源。
通过这种方式,你可以灵活地配置套接字的行为,以满足不同的网络编程需求。
总的来说,通过
setsockopt()
函数,程序可以根据需要动态地配置套接字的行为,这对于实现高级网络功能非常关键。