项目概述
这个项目实现了一个简单的 多客户端聊天室,基于 C++ 编程语言,使用了 epoll 机制来管理多个客户端连接。项目分为客户端和服务器端两部分,客户端通过 socket
连接到服务器,服务器可以处理多个客户端的消息并进行广播。
服务器端代码详解
服务器端主要步骤
-
创建监听套接字:
- 使用
socket()
创建监听套接字。 - 绑定 IP 地址和端口号,通过
bind()
绑定。 - 使用
listen()
开始监听连接请求。
- 使用
-
创建 epoll 句柄:
- 使用
epoll_create1()
创建 epoll 句柄。 - 将监听套接字添加到 epoll 中,监听新的客户端连接。
- 使用
-
等待事件:
- 通过
epoll_wait()
等待事件发生,处理新的客户端连接或客户端发送的消息。
- 通过
-
处理客户端连接:
- 使用
accept()
接受新的客户端连接,并将新的客户端套接字添加到 epoll 中进行监听。
- 使用
-
消息处理与转发:
- 当客户端发送消息时,服务器读取消息并将其广播给其他所有已连接的客户端。
#include<iostream>
#include<arpa/inet.h>
#include<unistd.h>
#include<string>
#include<sys/epoll.h>
#include<stdio.h>
#include<map>
#define max_connect 128
struct client_info{
int fd_c;
std::string name;
std::string ip_addr;
int host_port;
};
void broadcast_client_count(const std::map<int, client_info>& mp_client) {
std::string client_count_msg = "当前连接的客户端数量: " + std::to_string(mp_client.size()) + "\n";
std::cout << client_count_msg << std::endl;
}
int main() {
int epld = epoll_create1(0);
if(epld < 0) {
perror("epoll 创建失败");
return -1;
}
// 创建服务器的套接字文件
int fd_sock = socket(AF_INET, SOCK_STREAM, 0);
if(fd_sock == -1) {
perror("创建套接字失败");
return -1;
}
// 绑定通信接口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd_sock, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1) {
perror("绑定失败");
return -1;
}
int fd_listen = listen(fd_sock, max_connect);
if(fd_listen == -1) {
perror("监听失败");
return -1;
}
// 将监听的socket放入epoll
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd_sock;
ret = epoll_ctl(epld, EPOLL_CTL_ADD, fd_sock, &ev);
if(ret < 0) {
perror("epoll_ctl 错误");
return -1;
}
std::map<int, client_info> mp_client;
// 循环监听
while(1) {
struct epoll_event evs[max_connect];
int n = epoll_wait(epld, evs, max_connect, -1);
if(n < 0) {
perror("epoll_wait 错误");
break;
}
for (int i = 0; i < n; i++) {
int fd = evs[i].data.fd;
// 新客户端连接
if(fd == fd_sock) {
struct sockaddr_in caddr;
int addrlen = sizeof(caddr);
int fd_acp = accept(fd_sock, (struct sockaddr*)&caddr, (socklen_t*)&addrlen);
if(fd_acp < 0) {
perror("accept 错误");
continue;
}
struct epoll_event ev_client;
ev_client.events = EPOLLIN;
ev_client.data.fd = fd_acp;
epoll_ctl(epld, EPOLL_CTL_ADD, fd_acp, &ev_client);
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(caddr.sin_addr), ip_str, INET_ADDRSTRLEN);
int port = ntohs(caddr.sin_port);
std::cout << "连接成功: " << ip_str << " 端口号: " << port << std::endl;
client_info single_cl;
single_cl.fd_c = fd_acp;
single_cl.name = "";
single_cl.ip_addr = ip_str;
single_cl.host_port = port;
mp_client[fd_acp] = single_cl;
broadcast_client_count(mp_client);
} else {
char buff[1024];
int n = read(fd, buff, 1024);
if(n <= 0) {
close(fd);
std::cout << "断开连接: " << mp_client[fd].ip_addr << " 端口号: " << mp_client[fd].host_port << std::endl;
epoll_ctl(epld, EPOLL_CTL_DEL, fd, 0);
mp_client.erase(fd);
broadcast_client_count(mp_client);
} else {
std::string msg(buff, n);
if (mp_client[fd].name == "") {
mp_client[fd].name = msg.substr(0, msg.find("\n"));
std::cout << "客户端 " << mp_client[fd].name << " 已连接." << std::endl;
} else {
std::string name = mp_client[fd].name;
for (auto pair : mp_client) {
if (pair.first != fd) {
std::string full_msg = '[' + name + "]: " + msg;
write(pair.first, full_msg.c_str(), full_msg.size());
}
}
}
}
}
}
}
close(epld);
close(fd_sock);
return 0;
}
客户端主要步骤
-
创建套接字:
- 使用
socket()
创建客户端套接字。
- 使用
-
连接服务器:
- 使用
connect()
与服务器建立连接。
- 使用
-
多线程:
- 启动两个线程,一个处理消息的读取(接收服务器的消息),另一个处理消息的写入(发送消息到服务器)。
-
消息发送与接收:
- 使用
send()
发送消息。 - 使用
recv()
接收服务器广播的消息。
- 使用
#include<iostream>
#include<cstring>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<thread>
void read_from_server(int sockfd) {
char buffer[1024];
while (true) {
ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
if (n <= 0) {
std::cerr << "Server closed the connection or error occurred." << std::endl;
break;
}
buffer[n] = '\0';
std::cout << buffer;
}
}
void write_to_server(int sockfd, const std::string& username) {
if (send(sockfd, username.c_str(), username.size(), 0) < 0) {
std::cerr << "Error sending username to server." << std::endl;
return;
}
std::string message;
while (std::getline(std::cin, message)) {
message += "\n";
if (send(sockfd, message.c_str(), message.size(), 0) < 0) {
std::cerr << "Error sending message to server." << std::endl;
break;
}
}
}
// 客户端 main 函数
int main() {
int fd_lis = socket(AF_INET, SOCK_STREAM, 0);
if(fd_lis == -1) {
perror("socket_listen error!");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET, "192.168.159.129", &saddr.sin_addr.s_addr);
int ret = connect(fd_lis, (struct sockaddr*)&saddr, sizeof(saddr));
if(ret == -1) {
perror("connect error");
return -1;
} else {
std::cout << "welcome to chat space! please enter first word as your chat name" << std::endl;
std::string username;
std::getline(std::cin, username);
// 启动读写线程
std::thread reader(read_from_server, fd_lis);
std::thread writer(write_to_server, fd_lis, username);
// 等待线程结束
reader.join();
writer.join();
close(fd_lis);
}
}
在服务器使用 epoll
进行网络编程时,epoll
能够同时监听多个事件,比如客户端的连接请求和已连接客户端的新消息。服务器通过监听套接字和客户端套接字的不同事件来区分客户端的连接和消息传递。
区分客户端连接与消息的关键
-
监听套接字 (
fd_sock
):- 当有新的客户端尝试连接服务器时,服务器的监听套接字会触发
EPOLLIN
事件,表示有新的连接请求。 - 服务器通过
accept()
来接受这个连接,并获取新的客户端套接字。
- 当有新的客户端尝试连接服务器时,服务器的监听套接字会触发
-
客户端套接字 (
fd_acp
):- 当已经连接的客户端发送消息时,客户端的套接字也会触发
EPOLLIN
事件。 - 服务器通过
read()
或recv()
从该套接字中读取客户端发送的消息。
- 当已经连接的客户端发送消息时,客户端的套接字也会触发
通过 epoll
的事件机制,可以轻松区分客户端的连接请求和新消息的到来。以下是具体如何区分这两者的示例代码。
int fd_sock = socket(AF_INET, SOCK_STREAM, 0); // 创建监听套接字
bind(fd_sock, (struct sockaddr*)&addr, sizeof(addr));
listen(fd_sock, max_connect);
// 将监听的套接字添加到 epoll 中监听
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd_sock;
epoll_ctl(epld, EPOLL_CTL_ADD, fd_sock, &ev);
while (1) {
struct epoll_event evs[max_connect];
int n = epoll_wait(epld, evs, max_connect, -1); // 等待事件
for (int i = 0; i < n; i++) {
int fd = evs[i].data.fd;
// 如果是监听套接字上的事件,表示有新连接
if (fd == fd_sock) {
struct sockaddr_in caddr;
int addrlen = sizeof(caddr);
int fd_acp = accept(fd_sock, (struct sockaddr*)&caddr, (socklen_t*)&addrlen);
// 将新的客户端套接字加入到 epoll 监听中
struct epoll_event ev_client;
ev_client.events = EPOLLIN;
ev_client.data.fd = fd_acp;
epoll_ctl(epld, EPOLL_CTL_ADD, fd_acp, &ev_client);
// 打印新连接的客户端信息
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(caddr.sin_addr), ip_str, INET_ADDRSTRLEN);
int port = ntohs(caddr.sin_port);
std::cout << "新客户端连接: " << ip_str << " 端口号: " << port << std::endl;
} else {
// 如果是已连接客户端的事件,表示客户端发送了新消息
char buff[1024];
int n = read(fd, buff, 1024);
if (n > 0) {
// 处理客户端发送的消息
std::string msg(buff, n);
std::cout << "收到消息: " << msg << std::endl;
// 将消息转发给其他客户端
for (auto pair : mp_client) {
if (pair.first != fd) {
std::string full_msg = "[客户端]: " + msg;
write(pair.first, full_msg.c_str(), full_msg.size());
}
}
} else if (n == 0) {
// 如果读到的数据为 0,表示客户端断开连接
close(fd);
std::cout << "客户端断开连接: " << mp_client[fd].ip_addr << std::endl;
epoll_ctl(epld, EPOLL_CTL_DEL, fd, 0);
mp_client.erase(fd);
}
}
}
}
具体流程说明
-
监听套接字上的事件 (
fd == fd_sock
):- 当有客户端连接时,
fd_sock
触发EPOLLIN
事件。 - 服务器调用
accept()
接受新的连接,并为新客户端创建一个新的套接字 (fd_acp
)。 - 新的客户端套接字也需要添加到
epoll
监听中,以便在后续客户端发送消息时可以检测到。
- 当有客户端连接时,
-
客户端套接字上的事件 (
fd != fd_sock
):- 当某个客户端发送消息时,其对应的客户端套接字会触发
EPOLLIN
事件。 - 服务器通过
read()
从该套接字读取数据,并将数据转发给其他所有已连接的客户端。
- 当某个客户端发送消息时,其对应的客户端套接字会触发
-
客户端断开连接:
- 如果
read()
返回值为0
,表示客户端已经断开连接,服务器会关闭该套接字并从epoll
监听列表中移除它。
- 如果
总结
- 客户端连接事件:通过
fd == fd_sock
来区分,表示有新的客户端连接。 - 客户端消息事件:通过
fd != fd_sock
来区分,表示已有客户端发送消息。
通过 epoll
的事件驱动机制,服务器可以同时处理多个客户端的连接和消息收发,并且在大规模并发场景下表现出色。