揭秘 Socket 的存储与阻塞唤醒机制,为你打开网络编程新世界
2022-12-30 02:07:23
网络编程的基础:Socket
Socket 的定义
在网络编程中,Socket 是一个抽象的概念,它代表应用程序与网络之间的通信端点。在 Linux 系统中,Socket 被存储在内核中,并以文件符的形式提供给应用程序。这意味着应用程序可以通过对 Socket 进行读写操作来与网络进行通信。
Socket 的存储结构
Socket 在内核中以文件符的形式存储。文件描述符是一个整数,它唯一地标识一个文件或设备。应用程序可以通过调用 socket()
函数创建 Socket,并得到一个与该 Socket 关联的文件描述符。
Socket 的存储结构包括以下几个部分:
- Socket 类型:Socket 类型指定了 Socket 的通信方式。常见的 Socket 类型有
SOCK_STREAM
和SOCK_DGRAM
。 - Socket 地址:Socket 地址指定了 Socket 的通信端点。Socket 地址可以是 IPv4 地址、IPv6 地址或 UNIX 域套接字地址。
- Socket 状态:Socket 状态指定了 Socket 的当前状态。常见的 Socket 状态有
LISTEN
、ESTABLISHED
、CLOSE_WAIT
等。 - Socket 选项:Socket 选项可以修改 Socket 的行为。常见的 Socket 选项有
SO_REUSEADDR
、SO_LINGER
等。
阻塞与唤醒机制
当 Socket 接收数据时,数据会存储在 Socket 的接收缓冲区中。应用程序可以通过调用 read()
函数从接收缓冲区中读取数据。如果接收缓冲区中没有数据,应用程序将被阻塞,直到数据到达为止。
为了避免应用程序长时间被阻塞,内核提供了 select()
和 poll()
等系统调用,允许应用程序在多个 Socket 上进行轮询,从而提高应用程序的并发性。当某个 Socket 上有数据到达时,内核会唤醒应用程序,应用程序就可以对该 Socket 进行读写操作。
select()
和 poll()
系统调用的工作原理如下:
- 应用程序调用
select()
或poll()
函数,并指定要轮询的 Socket 集合。 - 内核将应用程序挂起,直到以下情况之一发生:
- 有数据到达了某个 Socket。
- 有信号发送给了应用程序。
- 超时时间到了。
- 内核唤醒应用程序,并返回已就绪的 Socket 集合。
- 应用程序可以对已就绪的 Socket 进行读写操作。
代码示例
以下是一个使用 select()
系统调用实现多路复用服务器的代码示例:
#include <sys/socket.h>
#include <sys/select.h>
#include <stdio.h>
int main() {
// 创建一个监听套接字
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
perror("socket");
return -1;
}
// 绑定监听套接字到一个地址和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(listenfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind");
return -1;
}
// 监听套接字
if (listen(listenfd, 10) == -1) {
perror("listen");
return -1;
}
// 设置文件描述符集合
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listenfd, &rfds);
// 循环轮询文件描述符集合
while (1) {
// 复制文件描述符集合
fd_set readfds = rfds;
// 轮询文件描述符集合
int ret = select(listenfd + 1, &readfds, NULL, NULL, NULL);
if (ret == -1) {
perror("select");
break;
}
// 处理已就绪的套接字
if (FD_ISSET(listenfd, &readfds)) {
// 接受新连接
int connfd = accept(listenfd, NULL, NULL);
if (connfd == -1) {
perror("accept");
continue;
}
// 将新的连接添加到文件描述符集合中
FD_SET(connfd, &rfds);
}
for (int i = 0; i < FD_SETSIZE; i++) {
if (FD_ISSET(i, &readfds)) {
// 从已就绪的套接字中读取数据
char buf[1024];
int n = read(i, buf, sizeof(buf));
if (n == -1) {
perror("read");
FD_CLR(i, &rfds);
} else if (n == 0) {
// 客户端断开连接
FD_CLR(i, &rfds);
} else {
// 处理数据
printf("Received data: %s", buf);
// 向客户端写数据
n = write(i, buf, n);
if (n == -1) {
perror("write");
FD_CLR(i, &rfds);
}
}
}
}
}
// 关闭监听套接字
close(listenfd);
return 0;
}
常见问题解答
-
什么是 Socket?
Socket 是应用程序与网络之间的通信端点,应用程序可以通过 Socket 与网络进行通信。 -
Socket 是如何存储的?
Socket 在内核中以文件描述符的形式存储,文件描述符是唯一标识 Socket 的整数。 -
应用程序如何使用 Socket?
应用程序可以通过调用socket()
函数创建 Socket,并得到一个与该 Socket 关联的文件描述符。应用程序可以对 Socket 进行读写操作来与网络进行通信。 -
当应用程序读取 Socket 时会发生什么?
当应用程序读取 Socket 时,数据会从 Socket 的接收缓冲区中读取。如果接收缓冲区中没有数据,应用程序将被阻塞,直到数据到达为止。 -
如何避免应用程序被阻塞?
为了避免应用程序被阻塞,可以使用select()
和poll()
等系统调用对多个 Socket 进行轮询。当某个 Socket 上有数据到达时,内核会唤醒应用程序。
