通过C语言搭建了有个TCP的服务器端,其中有些自定义函数直接写在下方供参考:
server.c
#include "server.h"
#include "client.h"
#include "message.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define MAX_EVENTS 1024
void run_server(int port) {
int listen_fd, epfd;
struct epoll_event ev, events[MAX_EVENTS];
// 创建监听socket
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
exit(1);
}
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
serv_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("bind");
exit(1);
}
if (listen(listen_fd, 10) < 0) {
perror("listen");
exit(1);
}
// 创建epoll
epfd = epoll_create(MAX_EVENTS);
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
printf("Server started on port %d\n", port);
while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
int sockfd = events[i].data.fd;
if (sockfd == listen_fd) {
// 新客户端连接
int conn_fd = accept(listen_fd, NULL, NULL);
add_client(conn_fd);
ev.events = EPOLLIN;
ev.data.fd = conn_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
printf("New client connected: %d\n", conn_fd);
} else if (events[i].events & EPOLLIN) {
// 处理客户端消息
char buf[1024] = {0};
int n = read(sockfd, buf, sizeof(buf));
if (n <= 0) {
printf("Client %d disconnected\n", sockfd);
remove_client(sockfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
} else {
buf[n] = '\0';
handle_message(sockfd, buf);
}
}
}
}
close(listen_fd);
}
client.c
#include "client.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static Client clients[MAX_CLIENTS];
void add_client(int fd) {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].fd == 0) {
clients[i].fd = fd;
snprintf(clients[i].name, sizeof(clients[i].name), "user%d", fd);
return;
}
}
}
void remove_client(int fd) {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].fd == fd) {
clients[i].fd = 0;
break;
}
}
}
Client* find_client_by_name(const char* name) {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].fd != 0 && strcmp(clients[i].name, name) == 0) {
return &clients[i];
}
}
return NULL;
}
void broadcast_message(int sender_fd, const char* msg) {
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].fd != 0 && clients[i].fd != sender_fd) {
write(clients[i].fd, msg, strlen(msg));
}
}
}
void private_message(int sender_fd, const char* target, const char* msg) {
Client* client = find_client_by_name(target);
if (client) {
write(client->fd, msg, strlen(msg));
} else {
const char* err = "User not found\n";
write(sender_fd, err, strlen(err));
}
}
message.c
#include "message.h"
#include "client.h"
#include <string.h>
#include <stdio.h>
void handle_message(int sender_fd, const char* msg) {
if (msg[0] == '@') {
// 私聊 @username: message
char target[32], text[1024];
if (sscanf(msg, "@%31[^:]: %[^\n]", target, text) == 2) {
private_message(sender_fd, target, text);
}
} else {
// 广播
broadcast_message(sender_fd, msg);
}
}
使用的构建方法是通过makefile,这里我贴上自己的makefile,供参考:
CC = gcc -Wall -g
OBJ = main.o server.o client.o message.o
chat_server: $(OBJ)
$(CC) -o $@ $(OBJ)
%.o: %.c
$(CC) -c $<
clean:
rm -f *.o chat_server
客户端测试的话,可以通过c++ QT来实现。之前想通过多线程的方式,每有一个连接就创建一个线程,后来看了企业的设计方案,使用的是epoll函数来处理大量的连接请求,底层是通过事件驱动的方式,当时一直不理解为什么会使用监听的socket的文件描述符和新建立连接的文件描述符进行比较来判断是否为新连接,通过查资料发现是自己理解的大方向出问题了,下面是我查资料总结如下:
sockfd == listen_fd
这个判断的核心作用是 “区分事件的来源”—— 明确当前触发事件的是 “监听 socket”(专门负责接新连接),而不是 “客户端 socket”(负责和已连接客户端通信)。
用 “现实场景类比” 理解:
假设你是一家公司的前台经理,负责处理两种事务:
- 新客户上门(对应 “新连接请求”):由公司大门(
listen_fd
)的门铃通知你。 - 老客户消息(对应 “客户端发数据”):由各个客户经理(
conn_fd
)的电话通知你。
门铃(listen_fd
)和电话(conn_fd
)是不同的 “设备”,你需要通过 “哪个设备响了” 来判断要处理哪种事务:
- 如果是门铃响了(
sockfd == listen_fd
)→ 处理新客户上门; - 如果是某个电话响了(
sockfd == 某个conn_fd
)→ 处理对应老客户的消息。
代码中的具体逻辑:
在 epoll
事件循环中,epoll_wait
会返回所有触发事件的文件描述符(sockfd
),但这些文件描述符可能来自两种 socket:
listen_fd
:监听 socket,唯一作用是接收新连接。conn_fd
:客户端 socket,每个已连接的客户端对应一个,用于收发数据。
if (sockfd == listen_fd)
就是通过 “文件描述符的值是否相等” 来判断:
- 相等:事件来自监听 socket → 必然是 “有新客户端要连接”(因为
listen_fd
只能干这个),所以执行accept()
接收新连接。 - 不相等:事件来自某个客户端 socket → 必然是 “该客户端发来了数据”,所以执行
read()
读取消息。
为什么能通过 “相等” 来判断?
- 文件描述符的唯一性:每个 socket(包括
listen_fd
和所有conn_fd
)的文件描述符都是唯一的整数(比如listen_fd
可能是 3,第一个conn_fd
是 4,第二个是 5 等)。 listen_fd
的特殊性:整个服务器只有一个listen_fd
,且它的唯一功能是接收新连接,不会参与数据收发,所以它的事件含义是确定的。
一句话总结:
if (sockfd == listen_fd)
就像在问:“这个事件是监听 socket 触发的吗?”
如果是,就说明有新客户端要连接;如果不是,就说明是已连接的客户端发来了消息。这是 epoll
模型中区分 “新连接” 和 “数据通信” 的核心判断。