基于 C 语言的多人聊天室系统实现 - 改版:从功能设计到代码解析

导言

在网络编程领域,聊天室系统是一个经典的实践案例,它涵盖了套接字通信、并发处理、数据结构应用等多个核心知识点。本文将从零开始,一步步讲解如何使用 C 语言实现一个支持多人聊天、超时管理和私聊功能的完整聊天室系统,包括服务器端和客户端的实现细节。同时,为了让代码更具可读性和可维护性,会对关键代码段添加详细注释,帮助读者更好地理解每一步的逻辑。

一、系统整体设计与功能规划

1.1 核心功能定位

本聊天室系统旨在实现一个轻量化的多人即时通信工具,通过逐步迭代的方式完成以下功能:

  • 基础双人通信:实现两个客户端之间通过服务器转发消息,为后续多人通信奠定基础。在开发初期先聚焦双人通信,能简化调试流程,快速验证消息转发逻辑。

  • 多人聊天功能:支持多个客户端同时连接并进行群聊,满足多人实时交流需求。该功能适用于在线讨论、小组协作等场景,是聊天室的核心使用场景。

  • 超时管理机制:对长时间不活跃的客户端进行自动清理,释放系统资源,保持服务器高效运行。当服务器连接数较多时,此机制能有效避免资源浪费,提升整体性能。

  • 私聊功能:允许客户端之间建立一对一的私密对话,保护用户隐私。用户在需要分享敏感信息或进行私人交流时,该功能可提供安全的沟通环境。

  • 服务器重连:客户端在服务器下线后能自动尝试重新连接,提升系统可用性。即使服务器临时故障,用户也无需手动频繁操作,保障通信连续性。

1.2 技术选型与架构设计

系统采用 C/S(客户端 / 服务器)架构,基于 TCP 协议实现可靠通信:

  • 服务器端:负责管理客户端连接、消息转发、状态监控,是整个系统的核心枢纽。服务器需具备高稳定性,处理大量并发请求,保证消息准确及时分发。

  • 客户端:提供用户交互界面,处理消息收发,为用户提供聊天交互入口。客户端设计需注重易用性,确保用户能流畅输入和查看消息。

  • 数据结构:使用链表存储在线客户端信息,方便动态管理客户端连接。链表结构插入和删除操作高效,适合频繁变化的在线客户端列表。

  • 并发处理:采用 select 多路复用机制实现对多个客户端的同时管理,提高服务器资源利用率。该机制能在单线程内处理多个 I/O 事件,降低资源消耗。

二、核心数据结构设计

在实现具体功能前,我们需要设计合适的数据结构来管理客户端信息,这是整个系统的基础。

2.1 客户端节点结构

1
2
3
4
5
6
7
8
typedef struct Node{
int client_fd; // 客户端套接字描述符,用于标识客户端连接,是与客户端通信的关键句柄
uint32_t addr; // 客户端IP地址,存储客户端的网络地址,采用32位无符号整数存储IPv4地址
int port; // 客户端端口号,用于区分同一IP下的不同客户端,确保通信唯一性
int chat_fd; // 私聊对象的套接字描述符(-1表示未进行私聊),记录当前私聊状态
int sec; // 最后活动时间戳,用于超时检测,记录客户端最后一次发送消息的时间
struct Node *next; // 链表节点指针,指向下一个节点,用于构建链表结构
}Node;

该结构存储了单个客户端的关键信息,包括网络标识(IP 和端口)、通信句柄(套接字描述符)、状态信息(活动时间和私聊状态)。通过这些信息,服务器可全面掌握客户端状态,进行精准管理。

2.2 客户端链表结构

1
2
3
4
5
typedef struct List{
Node *head; // 链表头节点,指向链表第一个节点,便于快速访问链表
Node *tail; // 链表尾节点,指向链表最后一个节点,支持高效的节点插入操作
int size; // 链表长度(在线客户端数量),记录当前在线客户端总数,方便统计和管理
}List;

通过链表结构可以动态管理所有在线客户端,支持高效的节点添加、删除和遍历操作,为多人聊天功能提供基础支持。无论是新客户端加入还是已有客户端退出,链表都能快速更新状态。

三、功能实现步骤详解

3.1 第一步:实现基础网络通信框架

3.1.1 服务器端初始化

服务器端的核心工作是创建套接字、绑定地址端口、监听连接请求,代码如下:

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
int main(int argc, char *argv[]){
// 检查命令行参数是否正确,确保传入了IP和端口,格式为./server [IP] [PORT]
ARGS_CHECK(argc,3);
// 创建TCP套接字,使用IPv4协议,流式套接字,基于TCP协议提供可靠连接
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_port = htons(atoi(argv[2]));
// 将IP地址字符串转换为网络字节序的二进制形式,便于网络传输
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
// 设置端口复用,避免服务器重启时出现地址占用错误,提升部署灵活性
int res_addr = 1;
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&res_addr,sizeof(int));
// 绑定套接字到指定地址和端口,建立网络连接基础
int b_ret = bind(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
// 检查绑定是否失败,若失败输出错误信息
ERROR_CHECK(b_ret,-1,"bind error");
// 开始监听连接请求,最大等待队列长度为50,控制连接请求积压数量
int lis = listen(sockfd,50);
// 检查监听是否失败,若失败输出错误信息
ERROR_CHECK(lis,-1,"listen error");
}

3.1.2 客户端连接实现

客户端需要创建套接字并连接到服务器,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, char *argv[]){
// 检查命令行参数是否正确,格式为./client [IP] [PORT]
ARGS_CHECK(argc,3);
// 创建套接字,准备与服务器建立连接
int sockfd=socket(AF_INET,SOCK_STREAM,0);
// 检查套接字创建是否失败,若失败输出错误信息
ERROR_CHECK(sockfd,-1,"error socket");
// 获取服务器地址信息,封装成结构体
struct sockaddr_in server_addr=GetServerSockfd(argv[1],argv[2]);
// 连接到服务器,发起通信请求
int con=connect(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
// 检查连接是否失败,若失败输出错误信息
ERROR_CHECK(con,-1,"error connect");
}

3.2 第二步:实现双人通信功能

双人通信的核心是服务器能够接收一个客户端的消息并转发给另一个客户端。这需要服务器能够:

  • 接受客户端连接(AcceptClient函数)

  • 存储客户端信息(CreateClientFd函数)

  • 接收并转发消息(RecvAndSendClientSendClinetMes函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void AcceptClient(int sockfd,fd_set *monitorset,List* list){
// 接受新连接,获取客户端地址信息
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int cli_sockfd=accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
// 检查接受连接是否失败,若失败输出错误信息
ERROR_CHECK(cli_sockfd,-1,"error accept");
// 将新客户端添加到链表,便于后续管理
CreateClientFd(list,cli_sockfd,client_addr,monitorset);
// 通知其他客户端有新连接,更新在线状态
Node* p = list->head;
while(p != NULL){
if(p->client_fd != cli_sockfd){
char new_conn_msg[100];
sprintf(new_conn_msg, "新客户端%d已连接\n", cli_sockfd);
send(p->client_fd, new_conn_msg, strlen(new_conn_msg), 0);
}
p = p->next;
}
}

3.3 第三步:实现多人聊天功能

多人聊天在双人通信的基础上,需要将消息广播给所有在线客户端。这通过遍历客户端链表实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void SendClinetMes(List *list,char* buf,Node *p){
if(p != NULL && p->chat_fd != -1){
// 私聊状态,仅发送给私聊对象,确保消息私密性
SendMsg(p->chat_fd, p,buf);
}else{
// 群聊状态,广播给所有其他客户端,实现多人交流
Node* new_p = list->head;
while(new_p != NULL){
if(new_p != p){ // 不发给自己
SendMsg(new_p->client_fd,p, buf);
}
new_p = new_p->next;
}
}
}

服务器使用select函数实现 I/O 多路复用,同时监听多个客户端的消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while(1){
// 复制监控集合,避免select修改原始集合,确保下次监听准确
memcpy(&readyset,&monitorset,sizeof(fd_set));
// 使用select进行I/O多路复用,设置超时时间,避免长时间阻塞
select(1024,&readyset,NULL,NULL,&timeout);
// 处理新连接,及时响应客户端请求
if(FD_ISSET(sockfd,&readyset)){
AcceptClient(sockfd,&monitorset,list);
}
// 处理客户端消息,实现消息接收和转发
Node *list_p = list->head;
while(list_p != NULL){
Node *p=list_p->next;
if(FD_ISSET(list_p->client_fd,&readyset)){
RecvAndSendClient(list,list_p,buf,&monitorset);
}
list_p=p;
}
}

3.4 第四步:实现超时退出机制

超时退出机制需要服务器定期检查客户端的最后活动时间,对超过设定时间(本系统为 10 秒)未活动的客户端进行清理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Node *list_p = list->head;
while(list_p != NULL){
Node *p=list_p->next;
// 检查是否超时,判断客户端活跃度
if((time(NULL)-list_p->sec)>10){
char server_msg[1024];
sprintf(server_msg,"客户端%d超时退出",list_p->port);
SendClinetMes(list,server_msg,NULL);
// 关闭连接并从链表中删除,释放系统资源
close(list_p->client_fd);
DelectClientFd(list, list_p->client_fd,&monitorset);
}
list_p=p;
}

同时,每次客户端发送消息时更新其最后活动时间:

1
2
3
4
if(FD_ISSET(list_p->client_fd,&readyset)){
list_p->sec=time(NULL); // 更新活动时间,记录最新操作时刻
RecvAndSendClient(list,list_p,buf,&monitorset);
}

3.5 第五步:实现私聊功能

私聊功能允许客户端之间建立一对一的私密对话,实现思路是:

  1. 客户端通过特定指令(*chat 客户端ID)发起私聊请求
  2. 服务器记录私聊关系(chat_fd字段)
  3. 处于私聊状态的客户端消息仅发送给私聊对象
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
// 处理私聊请求
if(strncmp(buf,"*chat ",6) == 0){
int num_fd = atoi(buf+6); // 获取目标客户端ID,解析指令参数
Node *new_p = list->head;
while(new_p != NULL){
if(num_fd == new_p->client_fd){
// 建立双向私聊关系,确保双方都能通信
p->chat_fd = new_p->client_fd;
new_p->chat_fd = p->client_fd;
printf("客户端%d与%d开始私聊\n",p->client_fd,p->chat_fd);
return;
}
new_p = new_p->next;
}
}
// 处理结束私聊指令
if (strcmp(buf, "*endchat\n") == 0) {
// 解除私聊关系,恢复群聊状态
Node* target = list->head;
while (target != NULL && target->client_fd != p->chat_fd) {
target = target->next;
}
if (target != NULL) {
target->chat_fd = -1;
}
p->chat_fd = -1;
}

4. 客户端实现细节

客户端需要实现的核心功能包括:

  1. 与服务器建立连接

  2. 接收用户输入并发送给服务器

  3. 接收服务器转发的消息并显示

  4. 处理服务器下线后的重连逻辑

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
while(1){
FD_ZERO(&sockset);
FD_SET(sockfd,&sockset);
FD_SET(STDIN_FILENO,&sockset);
select(max_fd,&sockset,NULL,NULL,NULL);
// 处理服务器消息,接收并展示聊天内容
if(FD_ISSET(sockfd,&sockset)){
recv(sockfd,buf,sizeof(buf),0);
TimeNow();
printf("%s\n",buf);
// 处理服务器下线情况,自动尝试重连
if(strcmp(buf,"serverexit\n")==0){
printf("服务器下线,将尝试重连...\n");
while(1){
int new_sockfd=socket(AF_INET,SOCK_STREAM,0);
if(connect(new_sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr))!=-1){
close(sockfd);
sockfd=new_sockfd;
printf("重连成功\n");
break;
}
sleep(1);
}
}
}
// 处理用户输入,发送消息到服务器
if(FD_ISSET(STDIN_FILENO,&sockset)){
int ret=read(STDIN_FILENO,buf,sizeof(buf));
send(sockfd,buf,ret,0);
if(strcmp(buf,"exit\n")==0){
printf("退出聊天\n");
return 0;
}
}
}

五、系统测试与使用说明

5.1 编译与运行

  1. 编译服务器端:gcc server.c -o server,生成服务器可执行文件

  2. 编译客户端:gcc client.c -o client,生成客户端可执行文件

  3. 启动服务器:./server [127.0.0.1](http://127.0.0.1) 8888,在本地地址 [127.0.0.1](http://127.0.0.1)、端口 8888 启动服务器

  4. 启动客户端:./client [127.0.0.1](http://127.0.0.1) 8888(可启动多个客户端),连接到指定服务器进行测试

5.2 基本操作指令

  • 发送群聊消息:直接输入消息内容并回车,实现多人公开交流

  • 发起私聊*chat 客户端ID(客户端 ID 可从服务器通知中获取),开启一对一私密对话

  • 结束私聊*endchat,退出私聊模式恢复群聊

  • 退出聊天exit,关闭客户端连接退出系统

六、总结

本文通过逐步实现的方式,详细讲解了基于 C 语言的多人聊天室系统的开发过程。从基础的套接字通信到复杂的多人聊天和私聊功能,我们学习了如何使用链表管理客户端、如何利用 select 实现多路复用、如何设计消息转发机制等关键技术点。

客户端代码

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
#include <my_header.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
/* Usage:
* 第一步实现双人通信
* 第二步实现通过服务器的双人通信
* 第三步实现通过服务器的多人聊天
* 第四步实现30秒不通讯退出
* 第五步实现私聊
*/
struct sockaddr_in GetServerSockfd(char* addr,char* port){
struct sockaddr_in server_addr;
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(atoi(port));
server_addr.sin_addr.s_addr=inet_addr(addr);
return server_addr;
}
void TimeNow(){
time_t now=time(NULL);
struct tm *time_n=localtime(&now);
printf("[%02d:%02d:%02d]",time_n->tm_hour,time_n->tm_min,time_n->tm_sec);
}
int main(int argc, char *argv[]){
ARGS_CHECK(argc,3);
int sockfd=socket(AF_INET,SOCK_STREAM,0);
ERROR_CHECK(sockfd,-1,"error socket");
struct sockaddr_in server_addr=GetServerSockfd(argv[1],argv[2]);
//server_addr.sin_family=AF_INET;
//erver_addr.sin_port=htons(atoi(argv[2]));
//server_addr.sin_addr.s_addr=inet_addr(argv[1]);
//十进制转二进制
int con=connect(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
ERROR_CHECK(con,-1,"error connect");
fd_set sockset;
int port;
recv(sockfd,&port,sizeof(int),0);
printf("已经与服务端链接,输入exit退出;\n你的端口号是:%d\n",ntohs(port));
char buf[4068]={0};
while(1){
FD_ZERO(&sockset);
FD_SET(sockfd,&sockset);
FD_SET(STDIN_FILENO,&sockset);
int max_fd=1+((sockfd>STDIN_FILENO)?sockfd:STDIN_FILENO);
select(max_fd,&sockset,NULL,NULL,NULL);
if(FD_ISSET(sockfd,&sockset)){
recv(sockfd,buf,sizeof(buf),0);
TimeNow();
printf("%s\n",buf);
if(strcmp(buf,"exit\n")==0){
TimeNow();
printf("对面已经脱水\n");
}
if(strcmp(buf,"serverexit\n")==0||strlen(buf)==0){
TimeNow();
printf("服务器下线,将循环寻找服务器,直到新链接建立\n");
while(1){
int new_sockfd=socket(AF_INET,SOCK_STREAM,0);
ERROR_CHECK(new_sockfd,-1,"creat newfd");
if(connect(new_sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr))!=-1){
//这种方式可以考虑网络连接 recv(sockfd,client_addr,sizeof(client_addr),0);
close (sockfd);
sockfd=new_sockfd;
recv(sockfd,&port,sizeof(int),0);
printf("已经与服务端链接,输入exit退出;\n你的端口号是:%d\n",ntohs(port));
break;
}else{
close(new_sockfd);
}
sleep(1);
}
}
memset(buf,0,sizeof(buf));
}
if(FD_ISSET(STDIN_FILENO,&sockset)){
int ret=read(STDIN_FILENO,buf,sizeof(buf));
ERROR_CHECK(ret,-1,"error read");
send(sockfd,buf,ret,0);
if(ret==0||strcmp(buf,"exit\n")==0){
TimeNow();
printf("脱水\n");
return 0;
}
memset(buf,0,sizeof(buf));
}
}
close(sockfd);
return 0;
}

服务端代码

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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
#include <my_header.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <arpa/inet.h>
/* Usage:
* 第一步实现双人通信
* 第二步实现通过服务器的双人通信
* 第三步实现通过服务器的多人聊天
* 第四步实现30秒不通讯退出
* 第五步实现私聊
*/

//存储accept信息的链表,头尾节点,
typedef struct Node{
int client_fd;
uint32_t addr; //客户的网址
int port; //客户的端口
int chat_fd; //私聊
int sec;
struct Node *next;
}Node;
typedef struct List{
Node *head;
Node *tail;
int size;
}List;
void CreateClientFd(List *list,int cli_sockfd,struct sockaddr_in client_addr,fd_set *monitorset){
Node* newNode = (Node*)malloc(sizeof(Node));
ERROR_CHECK(newNode, NULL, "malloc error");

newNode->client_fd = cli_sockfd;
newNode->addr = client_addr.sin_addr.s_addr;
newNode->port = ntohs(client_addr.sin_port);
newNode->chat_fd = -1;
newNode->sec=time(NULL);
newNode->next = NULL;


if(list->head == NULL){
list->head = newNode;
list->tail = newNode;
}else{
list->tail->next = newNode;
list->tail = newNode;
}
list->size++;
FD_SET(cli_sockfd,monitorset);
}
void DelectClientFd(List *list, int cli_sockfd, fd_set *monitorset) {
if (list->head == NULL) return;

Node *prev = NULL;
Node *curr = list->head;
FD_CLR(cli_sockfd, monitorset);

// 查找要删除的节点
while (curr != NULL && curr->client_fd != cli_sockfd) {
prev = curr;
curr = curr->next;
}

// 未找到节点
if (curr == NULL) {
return;
}

// 解除与该客户端相关的私聊关系
Node* temp = list->head;
while (temp != NULL) {
if (temp->chat_fd == cli_sockfd) {
temp->chat_fd = -1;
send(temp->client_fd, "错误:私聊对象已下线,自动结束私聊\n", 34, 0);
}
temp = temp->next;
}

// 删除当前节点
if (prev == NULL) {
list->head = curr->next;
if (list->head == NULL) list->tail = NULL;
} else {
prev->next = curr->next;
if (curr == list->tail) list->tail = prev;
}
// 关键:彻底清理
FD_CLR(cli_sockfd, monitorset); // 从监听集合中移除
close(cli_sockfd); // 确保套接字关闭(双重保险)
free(curr); // 释放当前节点(关键修复:在删除节点后释放,而非NULL)
list->size--;
}
void TimeNow(){
time_t now = time(NULL);
struct tm *time_now = localtime(&now);
printf("[%02d:%02d:%02d]",time_now->tm_hour,time_now->tm_min,time_now->tm_sec);
}
void PrintfList(List *list){
Node*p = list->head;
printf("当前在线客户端列表:\n\n");
while(p != NULL){
printf("客户端描述符:%d 端口号是:%d\n\n",p->client_fd,p->port);
p = p->next;
}
}
void AcceptClient(int sockfd,fd_set *monitorset,List* list){
//接收新连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int cli_sockfd=accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
ERROR_CHECK(cli_sockfd,-1,"error accept");

printf("客户端%d已连接,端口号:%d\n\n", cli_sockfd, ntohs(client_addr.sin_port));

//发送客户自己的端口
send(cli_sockfd,&client_addr.sin_port,sizeof(client_addr.sin_port),0);
//添加到客户列表
CreateClientFd(list,cli_sockfd,client_addr,monitorset);
//添加到监听集合

Node* p = list->head;
while(p != NULL){
if(p->client_fd != cli_sockfd){
char new_conn_msg[100];
sprintf(new_conn_msg, "新客户端%d (端口号:%d)已连接,通过“*chat num”来私聊\n\n", cli_sockfd, ntohs(client_addr.sin_port));
send(p->client_fd, new_conn_msg, strlen(new_conn_msg), 0);
}
p = p->next;
}
PrintfList(list);
}
//p发出给new_p的信息
void SendMsg(int client_fd, Node* p, char* buf) {
char msg[4096] = {0}; // 扩大缓冲区,避免溢出
if (p == NULL) {
// 服务器消息:拼接前缀和内容
snprintf(msg, sizeof(msg), "服务器消息:%s", buf);
} else {
// 客户端消息:拼接客户端ID和内容
snprintf(msg, sizeof(msg), "%d号客户端:%s", p->client_fd, buf);
}
send(client_fd, msg, strlen(msg), 0); // 一次发送完整消息
}

void SendClinetMes(List *list,char* buf,Node *p){
if(p != NULL && p->chat_fd != -1){
SendMsg(p->chat_fd, p,buf);
}else{
// 广播消息给其他客户端
Node* new_p = list->head;
while(new_p != NULL){
if(new_p != p){
SendMsg(new_p->client_fd,p, buf);
}
new_p = new_p->next;
}
}
memset(buf,0,1024);
}
void RecvAndSendClient(List* list,Node *p,char* buf,fd_set *monitorset){
ssize_t ret = recv(p->client_fd, buf,1024, 0);
if(ret <= 0 || strcmp(buf, "exit\n") == 0){
TimeNow();
printf("客户端%d下线了,端口号:%d\n", p->client_fd,p->port);

// 从监听集合中移除该客户端
FD_CLR(p->client_fd,monitorset);

// 通知其他客户端
char offline_msg[1024];
sprintf(offline_msg, "端口号%d的客户端下线了\n", p->port);
SendClinetMes(list,offline_msg,p);

// 关闭客户端连接并从链表中删除
close(p->client_fd);
DelectClientFd(list, p->client_fd,monitorset);
memset(buf,0,1024);
return;
}
TimeNow();
printf("客户端 %d:%s\n",p->client_fd,buf);
if(strncmp(buf,"*chat ",6) == 0){
//输入想聊天的客户端
int num_fd = atoi(buf+6);
Node *new_p = list->head;
while(new_p != NULL){
if(num_fd == new_p->client_fd){
if(new_p->chat_fd != -1){
send(p->client_fd,"BUSY",4,0);
return;
}else{
p->chat_fd = new_p->client_fd;
new_p->chat_fd = p->client_fd;
memset(buf,0,1024);
printf("客户端%d想跟客户端%d私聊\n",p->client_fd,p->chat_fd);
char server_msg[1024];
sprintf(server_msg,"有客户端%d私聊短消息,输入 “*endchat“ 断开连接\n",p->client_fd);
send(p->chat_fd,server_msg,strlen(server_msg),0);
return;
}
}
new_p = new_p->next;
}
}
// 处理结束私聊指令:"*endchat"
if (strcmp(buf, "*endchat\n") == 0) {
if (p->chat_fd == -1) {
send(p->client_fd, "你未在私聊中\n", 14, 0);
memset(buf, 0, 1024);
return;
}

// 查找私聊对象并解除关系
Node* target = list->head;
while (target != NULL && target->client_fd != p->chat_fd) {
target = target->next;
}
if (target != NULL) {
target->chat_fd = -1;
send(target->client_fd, "对方已结束私聊\n", 30, 0);
}

p->chat_fd = -1;
send(p->client_fd, "已结束私聊\n", 30, 0);
memset(buf, 0, 1024);
return;
}

if(p->chat_fd != -1){
char chat_msg[1024];
sprintf(chat_msg,"[私聊%d]%s\n",p->client_fd,buf);
send(p->chat_fd,chat_msg,strlen(chat_msg),0);
memset(buf,0,1024);
return;
}
SendClinetMes(list,buf,p);
memset(buf,0,1024);
}
int main(int argc, char *argv[]){
ARGS_CHECK(argc,3);
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_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
//n十进制转二进制
int res_addr = 1;
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&res_addr,sizeof(int));
//可以随时重连

int b_ret = bind(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
ERROR_CHECK(b_ret,-1,"bind error");

int lis = listen(sockfd,50);
ERROR_CHECK(lis,-1,"listen error");
TimeNow();
printf("服务器开机,正在监听网络:%s:%d\n",inet_ntoa(server_addr.sin_addr),ntohs(server_addr.sin_port));

List *list = (List*)calloc(1,sizeof(List));
//存储客户端信息的表
fd_set monitorset; //监听集合
fd_set readyset; //就绪集合
FD_ZERO(&monitorset);
FD_SET(sockfd,&monitorset);
char buf[4068] = {0};
struct timeval timeout;
timeout.tv_sec=1;
timeout.tv_usec=0;
while(1){
memcpy(&readyset,&monitorset,sizeof(fd_set));
FD_SET(STDIN_FILENO,&readyset);
select(1024,&readyset,NULL,NULL,&timeout);
if(FD_ISSET(sockfd,&readyset)){
AcceptClient(sockfd,&monitorset,list);
}

if(FD_ISSET(STDIN_FILENO,&readyset)){
// 服务器向所有客户端广播消息
memset(buf,0,1024);
Node *p = list->head;
int ret = read(STDIN_FILENO,buf,sizeof(buf));
ERROR_CHECK(ret,-1, "error read");

if(ret == 0 || strcmp(buf, "exit\n") == 0){
TimeNow();
printf("服务器准备关闭\n");

// 通知所有客户端服务器即将关闭
while(p != NULL){
send(p->client_fd, "serverexit\n", 11, 0);
close(p->client_fd);
p = p->next;
}

free(list);
close(sockfd);
return 0;
}
SendClinetMes(list,buf,NULL);
memset(buf, 0, sizeof(buf));
}

Node *list_p = list->head;
while(list_p != NULL){
Node *p=list_p->next;
if((time(NULL)-list_p->sec)>100){
char server_msg[1024];
sprintf(server_msg,"客户端%d潜水太久,强制退出",list_p->port);
SendClinetMes(list,server_msg,NULL);
close(list_p->client_fd);
DelectClientFd(list, list_p->client_fd,&monitorset);
list_p=p;
continue;
}
if(FD_ISSET(list_p->client_fd,&readyset)){
list_p->sec=(time(NULL));
RecvAndSendClient(list,list_p,buf,&monitorset);
}
list_p=p;
}
}
free(list);
close(sockfd);
return 0;
}