1. 网络编程关注的问题
1.1 连接建立
在网络编程中,建立连接是在两台计算机之间创建一个通信链路的过程。这通常涉及一台作为服务器的计算机(它开放一个端口并监听进来的连接请求)和一台作为客户端的计算机(它向服务器的特定端口发送连接请求)。一旦服务器接受了连接请求,连接就会被建立,并且两台计算机可以开始通过这个连接来互相发送和接收数据。
在C++中,我们通常使用Boost.Asio库来处理网络编程。建立连接的过程通常包括创建一个socket,然后使用它连接到服务器。
// 客户端
#include <boost/asio.hpp>
namespace ip = boost::asio::ip;
using tcp = ip::tcp;
int main() {
boost::asio::io_context context;
tcp::resolver resolver(context);
auto endpoints = resolver.resolve("localhost", "12345");
tcp::socket socket(context);
boost::asio::connect(socket, endpoints);
return 0;
}
1.2 连接断开
断开连接是结束两台计算机之间的通信链路的过程。这可以由服务器或客户端任何一方发起。当连接断开后,任何一方都不能再通过这个连接发送或接收数据。这通常涉及关闭相关的socket,释放相关的资源。断开连接的操作必须进行适当的处理,以防止资源泄露或是其它可能的网络问题。
断开连接通常通过关闭socket来实现。这可以通过调用socket的close
方法完成。
// 断开连接
socket.close();
1.3 消息到达
当一台计算机(客户端或服务器)收到另一台计算机通过网络连接发送的数据时,我们称之为消息到达。通常,服务器或客户端会有一个接收缓冲区用来存放接收到的数据,同时有一种机制(如事件、中断、回调函数等)来通知程序有新的数据到达,以便程序能够处理这些数据。
当服务器收到客户端的消息时,可以通过socket的read_some
或者async_read_some
方法来读取数据。
// 服务器端读取数据
char data[512];
size_t length = socket.read_some(boost::asio::buffer(data));
1.4 消息发送
消息发送是指一台计算机(客户端或服务器)通过网络连接向另一台计算机发送数据。这通常涉及将数据复制到发送缓冲区,并通知底层网络协议栈将数据发送出去。发送数据时需要注意的一点是,网络的带宽是有限的,如果发送数据的速度超过了网络的带宽,那么数据可能需要在发送缓冲区中排队等待,这可能导致程序的运行效率降低。因此,高效地使用网络带宽是网络编程中需要考虑的一个重要问题。
发送消息是网络编程的重要部分。在Boost.Asio中,我们可以使用socket的write_some
或async_write_some
方法来发送数据。
// 客户端发送消息
std::string message = "Hello, server!";
socket.write_some(boost::asio::buffer(message));
2. 网络IO职责
2.1 操作io
在计算机编程中,IO(输入/输出)操作是指对数据的读取(输入)和写入(输出)。网络IO是指通过网络进行数据的读取和写入。
-
操作方式
-
阻塞IO:阻塞IO是指在进行IO操作时,如果数据还未准备好,操作会被挂起(阻塞),直到数据准备好为止。在这期间,CPU不能进行其他操作。
char buffer[256]; ssize_t length = read(socket_fd, buffer, sizeof(buffer)); // 阻塞,直到数据准备好
-
非阻塞IO:非阻塞IO是指在进行IO操作时,如果数据未准备好,操作会立即返回一个错误,不会被挂起。程序需要不断地尝试IO操作,直到数据准备好为止。
int flags = fcntl(socket_fd, F_GETFL, 0); fcntl(socket_fd, F_SETFL, flags | O_NONBLOCK); // 设置为非阻塞模式 char buffer[256]; ssize_t length = read(socket_fd, buffer, sizeof(buffer)); // 非阻塞,立即返回
-
区别:阻塞IO简化了程序设计,但效率可能较低,特别是在数据未准备好的情况下。非阻塞IO在数据未准备好的情况下不会浪费CPU时间,但需要更复杂的程序设计来处理这种情况。
-
-
非阻塞io处理方式
- 连接建立:当客户端请求连接时,服务器应立即接受连接请求,以准备读取和写入数据。
- 连接断开:当连接不再需要时,应立即关闭连接以释放资源。
- 连接到达:当新的连接到达时,应立即处理新的连接请求。
- 消息发送:当有数据需要发送时,应立即尝试写入数据。如果不能立即写入所有的数据(例如,因为网络缓冲区已满),则应保存未发送的数据,并在稍后再次尝试发送。
- io函数只能检测一条连接的就绪状态以及操作一条连接的io数据:这意味着需要通过轮询所有的连接来处理IO事件。这可能需要复杂的数据结构来管理所有的连接,并且可能需要复杂的算法来有效地处理大量的连接。
2.2 检测io
IO检测是指确定是否可以读取或写入数据。在非阻塞IO中,这是一个重要的操作,因为它决定了何时可以执行IO操作。
-
连接建立
-
接受连接:
- socket:创建一个新的socket来接受新的连接。
- bind & listen:绑定socket到一个特定的端口,并开始监听连接请求。
- 监听读事件:当新的连接请求到达时,可以通过读事件来检测。
-
主动连接:
- socket:创建一个新的socket来发送连接请求。
- 监听写事件:当连接建立后,可以通过写事件来检测。
-
-
连接断开
- 被动断开:
- EPOLLRDHUP:这个事件表示连接的另一端关闭了它的写操作,也就是说,不能再向这个连接写入数据。
- EPOLLHUP:这个事件表示连接已经断开,不能再进行任何读写操作。
- 被动断开:
-
消息到达
- 监听客户fd的读事件:当客户端有数据发送时,服务器可以通过监听读事件来检测。
- EPOLLIN:这个事件表示可以从连接中读取数据。
-
消息发送
- 监听客户fd的写事件:当有数据需要发送到客户端时,服务器可以通过监听写事件来检测。
- 注意:通常是write没有把用户数据全部发送出去的情况下,需要监听写事件,当网络缓冲区有更多的空间时,可以继续发送数据。
在Linux中,可以使用epoll API来检测IO。以下是一些简单的示例:
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
epoll_event event;
event.data.fd = socket_fd;
event.events = EPOLLIN | EPOLLET;
int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event);
if (ret == -1) {
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
std::array<epoll_event, MAX_EVENTS> events;
while (true) {
int num_events = epoll_wait(epoll_fd, events.data(), MAX_EVENTS, -1);
for (int i = 0; i < num_events; i++) {
if (events[i].events & EPOLLIN) {
// 可以读取数据
} else if (events[i].events & EPOLLOUT) {
// 可以写入数据
} else if (events[i].events & EPOLLRDHUP) {
// 连接的另一端关闭了它的写操作
} else if (events[i].events & EPOLLHUP) {
// 连接已经断开
}
}
}