一、项目概述
这是一个基于 TCP + 多线程 的聊天室程序,支持多个客户端同时在线聊天。
1.1 功能特点
- 每个客户端连接后,服务器为其创建一个独立的线程处理消息
- 客户端发送的消息会被广播给所有在线的客户端
- 支持自定义昵称
- 输入
q或Q可退出聊天室
1.2 技术栈
| 技术 | 用途 |
|---|---|
| Socket | 网络通信 |
| 多线程 | 同时处理多个客户端 |
| 互斥量 | 保护共享数据(客户端列表) |
| pthread_detach | 线程自动回收 |
二、服务器端代码详解
2.1 头文件和全局变量
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // close(), read(), write()
#include <string.h> // memset(), strlen()
#include <arpa/inet.h> // htonl(), htons(), inet_ntoa()
#include <sys/socket.h> // socket(), bind(), listen(), accept()
#include <netinet/in.h> // sockaddr_in 结构体
#include <pthread.h> // 多线程相关函数
#define BUF_SIZE 100 // 消息缓冲区大小
#define MAX_CLNT 256 // 最大客户端数量
// 函数声明
void * handle_clnt(void * arg);
void send_msg(char * msg, int len);
void error_handling(char * msg);
int clnt_cnt = 0; // 当前连接的客户端数量
int clnt_socks[MAX_CLNT]; // 存储所有客户端套接字的数组
pthread_mutex_t mutx; // 互斥量,保护 clnt_cnt 和 clnt_socks
全局变量说明:
clnt_cnt和clnt_socks是多个线程共享的数据,必须用互斥量保护。- 为什么需要互斥量?因为主线程会添加新客户端,
handle_clnt线程会移除断开连接的客户端,send_msg会遍历数组发送消息。如果不加锁,可能出现数据错乱。
2.2 主函数:服务器启动与监听
c
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
int clnt_adr_sz;
pthread_t t_id;
// 检查命令行参数:需要端口号
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 1. 初始化互斥量(必须在创建线程之前)
pthread_mutex_init(&mutx, NULL);
// 2. 创建 TCP 套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
// 3. 初始化服务器地址结构
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET; // IPv4
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); // 本机任意 IP
serv_adr.sin_port = htons(atoi(argv[1])); // 命令行指定的端口
// 4. 绑定套接字到地址
if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
// 5. 开始监听(等待队列长度 5)
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
// 6. 主循环:持续接受客户端连接
while (1) {
clnt_adr_sz = sizeof(clnt_adr);
// 接受客户端连接(阻塞)
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
// 7. 加锁,将新客户端加入全局数组
pthread_mutex_lock(&mutx);
clnt_socks[clnt_cnt++] = clnt_sock;
pthread_mutex_unlock(&mutx);
// 8. 创建线程处理该客户端
// 注意:传递 clnt_sock 的地址,因为每个线程需要自己的客户端套接字
pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);
// 9. 将线程设为分离状态,线程结束后自动回收资源
pthread_detach(t_id);
// 10. 打印客户端连接信息
printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr));
}
// 理论上不会执行到这里(无限循环)
close(serv_sock);
return 0;
}
2.3 客户端处理线程函数
c
void * handle_clnt(void * arg)
{
// 1. 获取传入的客户端套接字(注意:arg 是指针,需要解引用)
int clnt_sock = *((int*)arg);
int str_len = 0, i;
char msg[BUF_SIZE];
// 2. 循环接收客户端发来的消息
// read 返回 0 表示客户端正常关闭连接,返回 -1 表示出错
while ((str_len = read(clnt_sock, msg, sizeof(msg))) != 0) {
// 将收到的消息广播给所有客户端
send_msg(msg, str_len);
}
// 3. 客户端断开连接,需要从全局数组中移除该客户端
pthread_mutex_lock(&mutx);
for (i = 0; i < clnt_cnt; i++) {
if (clnt_sock == clnt_socks[i]) {
// 找到该客户端,将后面的元素向前移动覆盖
while (i++ < clnt_cnt - 1) {
clnt_socks[i] = clnt_socks[i + 1];
}
break;
}
}
clnt_cnt--; // 客户端数量减 1
pthread_mutex_unlock(&mutx);
// 4. 关闭客户端套接字
close(clnt_sock);
return NULL;
}
2.4 广播消息函数
c
void send_msg(char * msg, int len) // 向所有在线客户端发送消息
{
int i;
// 加锁,防止遍历过程中数组被修改
pthread_mutex_lock(&mutx);
for (i = 0; i < clnt_cnt; i++) {
// 向每个客户端发送消息
write(clnt_socks[i], msg, len);
}
pthread_mutex_unlock(&mutx);
}
2.5 错误处理函数
c
void error_handling(char * msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
三、客户端代码详解
3.1 头文件和全局变量
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // read(), write(), close()
#include <string.h> // strlen(), strcmp()
#include <arpa/inet.h> // inet_addr()
#include <sys/socket.h> // socket(), connect()
#include <pthread.h> // 多线程
#define BUF_SIZE 100 // 消息缓冲区大小
#define NAME_SIZE 20 // 昵称最大长度
// 函数声明
void * send_msg(void * arg);
void * recv_msg(void * arg);
void error_handling(char * msg);
char name[NAME_SIZE] = "[DEFAULT]"; // 昵称(默认为 DEFAULT)
char msg[BUF_SIZE]; // 消息缓冲区
3.2 主函数:连接服务器并启动两个线程
c
int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in serv_addr;
pthread_t snd_thread, rcv_thread;
void * thread_return;
// 检查命令行参数:需要 IP、端口、昵称
if (argc != 4) {
printf("Usage : %s <IP> <port> <name>\n", argv[0]);
exit(1);
}
// 设置昵称(用方括号括起来,便于识别)
sprintf(name, "[%s]", argv[3]);
// 1. 创建 TCP 套接字
sock = socket(PF_INET, SOCK_STREAM, 0);
// 2. 初始化服务器地址结构
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]); // 服务器 IP
serv_addr.sin_port = htons(atoi(argv[2])); // 服务器端口
// 3. 连接服务器
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error");
// 4. 创建发送线程(负责从键盘读取并发送消息)
pthread_create(&snd_thread, NULL, send_msg, (void*)&sock);
// 5. 创建接收线程(负责接收服务器广播的消息)
pthread_create(&rcv_thread, NULL, recv_msg, (void*)&sock);
// 6. 等待两个线程结束(理论上其中一个线程会因输入 q 而退出)
pthread_join(snd_thread, &thread_return);
pthread_join(rcv_thread, &thread_return);
close(sock);
return 0;
}
3.3 发送线程
c
void * send_msg(void * arg) // 发送线程主函数
{
int sock = *((int*)arg);
char name_msg[NAME_SIZE + BUF_SIZE];
while (1) {
// 从键盘读取一行输入
fgets(msg, BUF_SIZE, stdin);
// 如果输入 q 或 Q,退出程序
if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n")) {
close(sock); // 关闭套接字
exit(0); // 退出进程
}
// 拼接昵称和消息:例如 "[张三] Hello"
sprintf(name_msg, "%s %s", name, msg);
// 发送给服务器
write(sock, name_msg, strlen(name_msg));
}
return NULL;
}
3.4 接收线程
c
void * recv_msg(void * arg) // 接收线程主函数
{
int sock = *((int*)arg);
char name_msg[NAME_SIZE + BUF_SIZE];
int str_len;
while (1) {
// 接收服务器广播的消息
str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1);
if (str_len == -1) // 出错
return (void*)-1;
// 添加字符串结束符并打印
name_msg[str_len] = 0;
fputs(name_msg, stdout);
}
return NULL;
}
3.5 错误处理函数
c
void error_handling(char *msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
四、运行方法
4.1 编译
bash
# 服务器端
gcc chat_server.c -o server -pthread
# 客户端
gcc chat_client.c -o client -pthread
4.2 运行
bash
# 终端1:启动服务器(端口 9190)
./server 9190
# 终端2:启动客户端1(昵称 张三)
./client 127.0.0.1 9190 张三
# 终端3:启动客户端2(昵称 李四)
./client 127.0.0.1 9190 李四
4.3 聊天示例
[张三] 大家好!
[李四] 你好,张三!
[张三] 今天天气不错
4.4 退出
输入 q 或 Q 即可退出聊天室。
五、关键知识点总结
| 知识点 | 代码位置 | 说明 |
|---|---|---|
| TCP Socket | socket(), bind(), listen(), accept() |
服务器端网络通信 |
| 客户端连接 | connect() |
主动连接服务器 |
| 多线程创建 | pthread_create() |
为每个客户端创建独立线程 |
| 线程分离 | pthread_detach() |
线程结束自动回收资源 |
| 互斥量 | pthread_mutex_lock/unlock |
保护共享数据(客户端列表) |
| 广播消息 | send_msg() |
遍历所有客户端发送消息 |
| 移除断开客户端 | handle_clnt() 中的循环 |
从数组中删除已断开连接的客户端 |