一、项目概述与核心功能

本聊天室服务器基于 TCP 协议,通过select多路复用技术,实现单线程高效处理多个连接。其核心功能包括:

  • 稳定连接:利用 TCP 协议确保数据传输的可靠性;
  • 高效处理:借助select系统调用,提升服务器并发处理能力;
  • 多样交互:支持群聊和私聊两种模式;
  • 智能管理:设置 30 秒无活动超时机制,自动清理闲置连接;
  • 日志追踪:记录服务器关键事件与客户端活动,便于监控与调试;
  • 控制台广播:管理员可通过控制台向所有在线客户端发送消息。

二、核心技术点

  • select多路复用:允许单线程同时处理多个文件描述符(如监听套接字、客户端连接和标准输入),降低多线程编程的复杂度与资源消耗。
  • 链表数据结构:采用双向链表动态管理客户端连接信息,便于节点的添加、删除与遍历操作。
  • TCP socket 编程:完整实现套接字创建、地址绑定、端口监听和连接接受等网络通信核心操作。
  • 时间处理机制:通过高精度时间戳生成函数,实现超时检测与日志记录功能。

三、代码模块解析

3.1 时间戳工具函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取时间戳字符串
char* get_timestamp(char* buffer, size_t size) {
time_t tm_now = time(NULL);
struct tm *now = localtime(&tm_now);
snprintf(buffer, size, "[%02d:%02d:%02d]", now->tm_hour, now->tm_min, now->tm_sec);
return buffer;
}

struct tm *PrintfTime() {
char timestamp[20];
printf("%s ", get_timestamp(timestamp, sizeof(timestamp)));
time_t tm_now;
tm_now = time(NULL);
return localtime(&tm_now);
}

get_timestamp函数用于生成格式化的时间字符串(时:分:秒),PrintfTime函数则打印时间戳并返回当前时间结构。通过snprintf严格控制缓冲区大小,有效避免缓冲区溢出问题。

3.2 服务器初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int ReadyFun(char *argv1, char *argv2) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(sockfd, -1, "error socket");
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv1);
server_addr.sin_port = htons(atoi(argv2));
// 设置地址复用,允许服务器重启后立即使用同一端口
int res_addr = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &res_addr, sizeof(int));
int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
ERROR_CHECK(ret, -1, "error bind");
ret = listen(sockfd, 50);
ERROR_CHECK(ret, -1,"error listen");
PrintfTime();
printf("Is listening, waiting for a new connection\n");
return sockfd;
}

ReadyFun函数负责服务器的初始化,包括创建套接字、设置地址复用、绑定地址端口和开始监听。通过ERROR_CHECK宏统一处理函数调用失败的情况,提高代码可读性和错误处理的一致性。setsockopt函数设置地址复用选项,解决服务器重启时端口占用问题。

3.3 客户端管理数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 单个客户端信息结构体
typedef struct ClientFd {
int client_fd; // 客户端套接字描述符
struct ClientFd *next; // 链表节点指针
struct ClientFd *chat; // 私聊对象指针
time_t sec; // 最后活动时间戳
} ClientFd;

// 客户端链表管理结构体
typedef struct ListClientfd {
ClientFd *head; // 头节点
ClientFd *tail; // 尾节点
int count; // 客户端数量
int maxfd; // 最大文件描述符(用于select)
} ListCliFd;

使用链表结构管理客户端连接信息,方便动态添加和删除客户端。链表操作函数如AddCliSockfdDelCliSockfd,实现了客户端连接的动态管理,同时需注意内存分配与释放,避免内存泄漏。

3.4 消息处理机制

私聊命令处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool JudgeChat(ListCliFd *list,ClientFd *cli_p, char *buf) {
// 处理私聊命令:*chat [客户端ID]
if(strncmp(buf, "*chat ", 6) == 0) {
int chatnum = atoi(buf+6);
ClientFd *p = list->head->next;
while(p != NULL) {
if(chatnum == p->client_fd && p != cli_p) {
cli_p->chat = p;
send(cli_p->client_fd,"connect\n", 8, 0);
return 1;
}
p = p->next;
}
send(cli_p->client_fd,"Error\n",6,0);
}
// 结束私聊命令:*endchat
else if(strcmp(buf,"*endchat\n") == 0) {
cli_p->chat = NULL;
}
return 0;
}

客户端通过发送*chat [目标客户端ID]命令发起私聊,*endchat命令结束私聊。可进一步扩展私聊功能,提供更友好的提示信息。

消息广播功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int Broadcast(ListCliFd *list, ClientFd *cli_p, ClientFd *ignore_p, char* buf) {
// 接收客户端消息
if(cli_p != NULL) {
ssize_t ret = recv(cli_p->client_fd, buf, sizeof(buf) - 1, 0);
if(ret <= 0) {
// 处理客户端断开连接
PrintfTime();
printf("Client %d disconnected unexpectedly\n", cli_p->client_fd);
DelCliSockfd(list, cli_p, prev, &monitorset, sockfd);
memset(buf, 0, sizeof(buf));
return ret;
}
// 检查是否是私聊命令
if(JudgeChat(list,cli_p,buf)) {
return 1;
}
}
// 广播消息给所有客户端
char msg[1060]={0};
ClientFd *p = list->head->next;
while(p != NULL) {
memset(msg,0,sizeof(msg));
if(p == ignore_p) continue;
if(p != cli_p) {
// 格式化消息(区分服务器消息和客户端消息)
if(cli_p == NULL) {
snprintf(msg, sizeof(msg), "Server message:\n%s", buf);
}else{
snprintf(msg, sizeof(msg), "Client %d message:\n%s", cli_p->client_fd, buf);
}
send(p->client_fd, msg, strlen(msg), 0);
}
p = p->next;
}
return strlen(buf);
}

Broadcast函数实现消息群发,将消息发送给除发送者外的所有客户端,并区分服务器消息和客户端消息。需注意处理发送失败的情况,防止广播流程中断。

3.5 主循环与事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
int main(int argc, char *argv[]) {
ARGS_CHECK(argc, 3);
int sockfd = ReadyFun(argv[1], argv[2]);
// 初始化客户端链表
ListCliFd *list = (ListCliFd* )calloc(1, sizeof(ListCliFd));
list->head = (ClientFd* )calloc(1, sizeof(ClientFd));
list->tail = list->head;
list->maxfd = sockfd;
fd_set monitorset;
fd_set readyset;
FD_ZERO(&monitorset);
FD_SET(sockfd, &monitorset);
FD_SET(STDIN_FILENO, &monitorset);
char buf[1024];
while(1) {
// 复制文件描述符集合(select会修改集合)
memcpy(&readyset, &monitorset, sizeof(fd_set));
struct timeval timeout = {1, 0};
select(list->maxfd + 1, &readyset, NULL, NULL, &timeout);
// 处理新的客户端连接
if(FD_ISSET(sockfd, &readyset)) {
AcceptClient(sockfd,list, &monitorset);
}
// 处理客户端消息
ClientFd *p = list->head->next;
ClientFd *prev = list->head;
while(p != NULL) {
ClientFd *next_p = p->next;
// 检查客户端超时(30秒无活动)
if(time(NULL) - p->sec > 30) {
// 处理超时逻辑
PrintfTime();
printf("Client %d timeout\n", p->client_fd);
sprintf(buf, "Client %d timeout and disconnected\n", p->client_fd);
Broadcast(list, NULL, NULL, buf);
DelCliSockfd(list, p, prev, &monitorset);
}
// 处理客户端发送的消息
else if(FD_ISSET(p->client_fd, &readyset)) {
p->sec = time(NULL); // 更新活动时间
// 私聊模式处理
if(p->chat != NULL) {
int ret = recv(p->client_fd,buf,sizeof(buf),0);
if(JudgeChat(list, p, buf)) continue;
char chat_msg[1050]={0};
sprintf(chat_msg, "Client %d chat message:\n%s", p->client_fd, buf);
send(p->chat->client_fd, chat_msg, strlen(chat_msg), 0);
}
// 群聊模式处理
else{
int ret = Broadcast(list, p, NULL, buf);
// 处理客户端退出
if(ret <= 0||strcmp(buf, "exit\n")==0){
PrintfTime();
printf("Client %d exit by himself\n", p->client_fd);
sprintf(buf, "Client %d exit and disconnected\n",p->client_fd);
Broadcast(list, NULL, NULL, buf);
DelCliSockfd(list, p, prev, &monitorset);
}
else{
prev = p;
}
}
}
else{
prev = p;
}
p = next_p;
}
// 处理服务器控制台输入(广播服务器消息)
if(FD_ISSET(STDIN_FILENO, &readyset)){
int ret = read(STDIN_FILENO, buf, sizeof(buf));
ERROR_CHECK(ret, -1, "read");
Broadcast(list, NULL, NULL, buf);
}
}
// 资源释放(实际运行中服务器通常不会主动退出)
// ...
}

主循环通过select监听三类事件:新的客户端连接请求、已连接客户端发送的消息、服务器控制台输入。这种单线程设计实现了高效的并发事件处理,避免了多线程编程的复杂性。运行时建议定期检查服务器资源占用,确保程序稳定。

四、使用方法

  • 编译服务器程序:在终端执行gcc server.c -o server进行编译,若使用自定义错误检查库,需确保库文件正确链接。
  • 启动服务器:在终端输入./server [IP地址] [端口号],例如./server 127.0.0.1 8888
  • 客户端连接:使用支持 TCP 协议的客户端工具(如 Telnet、自定义客户端程序)连接到服务器指定的 IP 地址和端口。

客户端命令说明:

  • 群聊消息:直接输入消息内容并回车。
  • 发起私聊:输入*chat [客户端ID]
  • 结束私聊:输入*endchat
  • 退出聊天室:输入exit

五、总结

本项目通过select多路复用技术和 TCP socket 编程,在 C 语言环境下实现了一个功能较为完善的多人聊天室。利用链表管理客户端连接,配合时间处理机制实现超时检测,同时支持群聊、私聊等多种交互模式。通过模块化的代码设计,将服务器初始化、消息处理、客户端管理等功能分开实现,便于维护与扩展。

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
#include <arpa/inet.h>
#include <fcntl.h>
#include <my_header.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

/* Usage: */

// 获取时间戳字符串
char* get_timestamp(char* buffer, size_t size) {
time_t tm_now = time(NULL);
struct tm *now = localtime(&tm_now);
snprintf(buffer, size, "[%02d:%02d:%02d]", now->tm_hour, now->tm_min, now->tm_sec);
return buffer;
}

struct tm *PrintfTime(){
char timestamp[20];
printf("%s ", get_timestamp(timestamp, sizeof(timestamp)));
time_t tm_now;
tm_now = time(NULL);
return localtime(&tm_now);
}

//负责准备工作
int ReadyFun(char *argv1, char *argv2){
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(sockfd, -1, "error socket");
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv1);
server_addr.sin_port = htons(atoi(argv2));

int res_addr = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &res_addr, sizeof(int));
//可以随时重连
int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
ERROR_CHECK(ret, -1, "error bind");

ret = listen(sockfd, 50);
ERROR_CHECK(ret, -1,"error listen");
PrintfTime();
printf("Is listening, waiting for a new connection\n");
return sockfd;
}

//结构体实现通过链表存储信息1.accept接到的sockfd;2.指针
typedef struct ClientFd{
int client_fd; //接收
struct ClientFd *next; //节点
struct ClientFd *chat;
time_t sec; //计算发言时间;
}ClientFd;

typedef struct ListClientfd{
ClientFd *head;
ClientFd *tail;
int count;
int maxfd;
}ListCliFd;

//链表实现
void AddCliSockfd(ListCliFd *list, int clientfd){
list->tail->next = (ClientFd* )calloc(1, sizeof(ClientFd));
list->tail = list->tail->next;
list->tail->client_fd = clientfd;
list->tail->sec = time(NULL);
list->maxfd = list->maxfd > clientfd ? list->maxfd : clientfd;//更新最大文件描述符
list->count ++;
}

void DelCliSockfd(ListCliFd *list, ClientFd *p, ClientFd *prev, fd_set *monitorset){
PrintfTime();
printf("Client %d exit\n", p->client_fd);
FD_CLR(p->client_fd, monitorset);

if (p == list->tail) {
list->tail = prev;
prev->next = NULL;
} else {
prev->next = p->next;
}

close(p->client_fd);
free(p);
list->count--;
}

bool JudgeChat(ListCliFd *list,ClientFd *cli_p, char *buf){
if(strncmp(buf, "*chat ", 6) == 0){
int chatnum = atoi(buf+6);
ClientFd *p = list->head->next;
while(p != NULL){
if(chatnum == p->client_fd && p != cli_p){
cli_p->chat = p;
ssize_t send_ret = send(cli_p->client_fd,"connect\n", 8, 0);
if (send_ret == -1) {
perror("send connect");
}
return 1;
}
p = p->next;
}
ssize_t send_ret = send(cli_p->client_fd,"Error\n",6,0); // 添加换行符并修正长度
if (send_ret == -1) {
perror("send Error");
}
}else if(strcmp(buf,"*endchat\n") == 0){
cli_p->chat = NULL;
}
return 0;
}

int Broadcast(ListCliFd *list, ClientFd *cli_p, ClientFd *ignore_p, char* buf){
if(cli_p != NULL){
ssize_t ret = recv(cli_p->client_fd, buf, sizeof(buf) - 1, 0);
if(ret <= 0) {
if (ret == -1) {
perror("recv");
}
PrintfTime();
printf("Client %d disconnected unexpectedly\n", cli_p->client_fd);
DelCliSockfd(list, cli_p, prev, &monitorset, sockfd); // 假设 prev 和 monitorset 等变量可见
memset(buf, 0, sizeof(buf));
return ret;
}
PrintfTime();
printf("Client %d send: %s\n", cli_p->client_fd, buf);

if(JudgeChat(list,cli_p,buf)){
return 1;
}
}

char msg[1060]={0};
ClientFd *p = list->head->next;
while(p != NULL){
memset(msg,0,sizeof(msg));
if(p == ignore_p){
continue;
}
if(p != cli_p){
if(cli_p == NULL){
snprintf(msg, sizeof(msg), "Server message:\n%s", buf);
}else{
snprintf(msg, sizeof(msg), "Client %d message:\n%s", cli_p->client_fd, buf);
}
ssize_t send_ret = send(p->client_fd, msg, strlen(msg), 0);
if (send_ret == -1) {
perror("send");
PrintfTime();
printf("Client %d disconnected while sending message\n", p->client_fd);
DelCliSockfd(list, p, prev, &monitorset, sockfd); // 假设 prev 和 monitorset 等变量可见
}
}
p = p->next;
}
//服务端接受消息并转发
return strlen(buf);
}

//接收客户端的链接信号
void AcceptClient(int sockfd, ListCliFd *list, fd_set *monitorset){
struct sockaddr_in client_addr; //获取客户的地址信息
socklen_t size = sizeof(client_addr);
int new_fd = accept(sockfd, (struct sockaddr*)&client_addr, &size);
ERROR_CHECK(new_fd, -1, "error accept");

AddCliSockfd(list, new_fd);
FD_SET(new_fd, monitorset);

PrintfTime();
printf("Client %d connected, the number of clients is %d\n", new_fd, list->count);
char clientsnum[1024]={0};
sprintf(clientsnum, "Client %d connected, the number of clients is %d\n if you send no message, you will be disconnected after thirty seconds\n", new_fd,list->count);
Broadcast(list, NULL, NULL, clientsnum);
}

int main(int argc, char *argv[]){
ARGS_CHECK(argc, 3);
int sockfd = ReadyFun(argv[1], argv[2]);
//空头结点,方便增加和删除
ListCliFd *list = (ListCliFd* )calloc(1, sizeof(ListCliFd));
list->head = (ClientFd* )calloc(1, sizeof(ClientFd));
list->tail = list->head;
list->maxfd = sockfd;

fd_set monitorset;
fd_set readyset;
FD_ZERO(&monitorset);
FD_SET(sockfd, &monitorset);
FD_SET(STDIN_FILENO, &monitorset);

char buf[1024];
while(1){
memset(buf, 0, sizeof(buf));
memcpy(&readyset, &monitorset, sizeof(fd_set));
struct timeval timeout;
timeout.tv_sec = 1;
timeout.tv_usec = 0;
select(list->maxfd + 1, &readyset, NULL, NULL, &timeout);

//接收客户端
if(FD_ISSET(sockfd, &readyset)){
AcceptClient(sockfd,list, &monitorset);
}

//接收客户端信息
ClientFd *p = list->head->next;
ClientFd *prev = list->head;
while(p != NULL){
ClientFd *next_p = p->next;
// 检查超时
if(time(NULL) - p->sec > 30) {
PrintfTime();
printf("Client %d timeout\n", p->client_fd);
sprintf(buf, "Client %d timeout and disconnected\n", p->client_fd);
Broadcast(list, NULL, NULL, buf);
memset(buf, 0, sizeof(buf)); // 使用sizeof确保安全
if (prev != NULL) {
DelCliSockfd(list, p, prev, &monitorset);
}
} else if(FD_ISSET(p->client_fd, &readyset)){

//广播消息
p->sec = time(NULL);
int ret = 0;
if(p->chat != NULL){
PrintfTime();
printf("chat with client%d\n",p->chat->client_fd);
ret = recv(p->client_fd,buf,sizeof(buf),0);

if(JudgeChat(list, p, buf)){
continue;
}

char chat_msg[1050]={0};
sprintf(chat_msg, "Client %d chat message:\n%s", p->client_fd, buf);
PrintfTime();
printf("%s",chat_msg);
if (p->chat != NULL && p->chat->client_fd > 0) {
ssize_t send_ret = send(p->chat->client_fd, chat_msg, strlen(chat_msg), 0);
if (send_ret == -1) {
perror("send chat message");
}
}
}else{
ret = Broadcast(list, p, NULL, buf);
}
if(ret <= 0||strcmp(buf, "exit\n")==0){

PrintfTime();
printf("Client %d exit by himself\n", p->client_fd);
sprintf(buf, "Client %d exit and disconnected, the number of clients is %d\n",p->client_fd,list->count-1);
Broadcast(list, NULL, NULL, buf);
DelCliSockfd(list, p, prev, &monitorset);
}else{
prev = p;
}

}else{
prev = p;
} //退出情况下前指针不变
p = next_p;
}

//广播发送信息
if(FD_ISSET(STDIN_FILENO, &readyset)){
int ret = read(STDIN_FILENO, buf, sizeof(buf));
ERROR_CHECK(ret, -1, "read");
Broadcast(list, NULL, NULL, buf);
memset(buf, 0, sizeof(buf)); // 使用sizeof确保安全
}
}

// 释放链表资源
ClientFd *current = list->head;
while(current != NULL) {
ClientFd *next = current->next;
free(current);
current = next;
}

free(list);
close(sockfd);
return 0;
}