day35 TCP实时聊天程序实现(多线程)
程序功能说明
本作业实现了一个基于TCP协议的实时双向聊天程序,包含服务器端(ser.c)和客户端(cli.c)两个程序。核心功能特点:
- 实时双向通信:双方可同时发送和接收消息
- 优雅退出机制:输入
#quit
命令可使双方立即退出 - 多线程架构:每个连接使用两个独立线程处理收发操作
- 本地回环测试:默认配置在本机(127.0.0.1)进行通信
注意:客户端代码中存在一个关键问题(
INADDR_ANY
使用不当),实际运行需修改为具体服务器IP(如127.0.0.1),后文将详细说明
服务器端代码解析(ser.c)
#include <arpa/inet.h> // 网络字节序转换函数
#include <fcntl.h> // 文件控制操作
#include <netinet/in.h> // Internet地址族定义
#include <netinet/ip.h> // IP协议头定义
#include <pthread.h> // 多线程支持
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 标准库函数
#include <string.h> // 字符串操作
#include <sys/socket.h> // 套接字接口
#include <sys/types.h> // 基本系统数据类型
#include <time.h> // 时间函数(本程序未实际使用)
#include <unistd.h> // POSIX系统调用
// 定义sockaddr指针的简写类型
typedef struct sockaddr* SA;
// 发送线程:从标准输入读取消息并发送给客户端
void* th1(void* arg)
{
int conn = *(int*)arg; // 获取通信套接字描述符
while (1)
{
char buf[512] = {0}; // 消息缓冲区
printf("to cli:"); // 提示用户输入
fgets(buf, sizeof(buf), stdin); // 读取用户输入
// 发送消息到客户端
int ret = send(conn, buf, strlen(buf), 0);
if (ret < 0) // 发送失败处理
{
break;
}
// 检测退出命令(注意包含换行符)
if (0 == strcmp(buf, "#quit\n"))
{
exit(0); // 立即终止进程
}
}
exit(0); // 线程退出
}
// 接收线程:接收客户端消息并打印
void* th2(void* arg)
{
int conn = *(int*)arg; // 获取通信套接字描述符
while (1)
{
char buf[512] = {0}; // 消息缓冲区
// 从客户端接收数据
int ret = recv(conn, buf, sizeof(buf), 0);
if(ret <= 0) // 接收失败或连接关闭
{
break;
}
// 检测退出命令(注意无换行符)
if (0 == strcmp(buf, "#quit"))
{
exit(0); // 立即终止进程
}
// 打印客户端消息
printf("from cli:%s", buf);
fflush(stdout); // 确保立即输出
}
exit(0); // 线程退出
}
int main(int argc, char** argv)
{
// 创建监听套接字(IPv4 TCP)
int listfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listfd)
{
perror("scoket error\n");
return 1;
}
// 定义服务器和客户端地址结构
struct sockaddr_in ser, cli, other;
bzero(&ser, sizeof(ser)); // 清零服务器地址
bzero(&cli, sizeof(cli)); // 清零客户端地址
bzero(&other, sizeof(other)); // 清零备用地址
// 配置服务器地址
ser.sin_family = AF_INET; // IPv4协议
ser.sin_port = htons(50000); // 端口50000(网络字节序)
ser.sin_addr.s_addr = INADDR_ANY; // 绑定所有本地IP
// 绑定地址到套接字
int ret = bind(listfd, (SA)&ser, sizeof(ser));
if (-1 == ret)
{
perror("bind");
return 1;
}
// 开始监听(排队队列长度=3)
listen(listfd, 3);
socklen_t len = sizeof(cli);
// 接受客户端连接
int conn = accept(listfd, (SA)&cli, &len);
if (-1 == conn)
{
perror("accept");
return 1;
}
// 获取客户端连接信息
getpeername(conn, (SA)&other, &len);
printf("cli ip:%s port:%d\n",
inet_ntoa(other.sin_addr), // 转换IP为点分十进制
ntohs(other.sin_port)); // 转换端口为主机字节序
// 创建双向通信线程
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, th1, &conn); // 发送线程
pthread_create(&tid2, NULL, th2, &conn); // 接收线程
// 等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// 关闭套接字
close(listfd);
close(conn);
return 0;
}
客户端代码解析(cli.c)
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
// 定义sockaddr指针的简写类型
typedef struct sockaddr* SA;
// 发送线程:从标准输入读取消息并发送给服务器
void* th1(void* arg)
{
int conn = *(int*)arg; // 获取通信套接字
while (1)
{
char buf[512] = {0}; // 消息缓冲区
printf("to ser:"); // 提示输入
fgets(buf, sizeof(buf), stdin); // 读取用户输入
// 发送消息到服务器
int ret = send(conn, buf, strlen(buf), 0);
if (ret < 0) // 发送失败
{
break;
}
// 检测退出命令
if (0 == strcmp(buf, "#quit\n"))
{
exit(0); // 立即终止进程
}
}
exit(0); // 线程退出
}
// 接收线程:接收服务器消息并打印
void* th2(void* arg)
{
int conn = *(int*)arg; // 获取通信套接字
while (1)
{
char buf[512] = {0}; // 消息缓冲区
// 从服务器接收数据
int ret = recv(conn, buf, sizeof(buf), 0);
if (ret <= 0) // 接收失败
{
break;
}
// 检测退出命令
if (0 == strcmp(buf, "#quit"))
{
exit(0); // 立即终止进程
}
// 打印服务器消息
printf("from ser:%s", buf);
fflush(stdout); // 确保立即输出
}
exit(0); // 线程退出
}
int main(int argc, char** argv)
{
// 创建客户端套接字
int conn = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == conn)
{
perror("socket");
return 1;
}
// 配置服务器地址
struct sockaddr_in ser;
bzero(&ser, sizeof(ser));
ser.sin_family = AF_INET; // IPv4协议
ser.sin_port = htons(50000); // 服务器端口
ser.sin_addr.s_addr = INADDR_ANY; // ❗关键问题:此处应为服务器IP(如127.0.0.1)
// 连接服务器
int ret = connect(conn, (SA)&ser, sizeof(ser));
if (-1 == ret)
{
perror("connect error\n");
return 1;
}
// 创建双向通信线程
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, th1, &conn); // 发送线程
pthread_create(&tid2, NULL, th2, &conn); // 接收线程
// 等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
close(conn); // 关闭连接
return 0;
}
重要提示:客户端代码中的
ser.sin_addr.s_addr = INADDR_ANY
是严重错误。
INADDR_ANY
(0.0.0.0) 表示"任意本地地址",不能用于客户端连接目标。
正确做法:应替换为服务器实际IP,如inet_addr("127.0.0.1")
若不修改,客户端将尝试连接到 0.0.0.0:50000,导致连接失败
程序执行流程
服务器端启动流程
客户端启动流程
理想运行结果
服务器端启动
$ gcc ser.c -o ser -lpthread
$ ./ser
cli ip:127.0.0.1 port:50001 # 显示客户端连接信息
客户端启动(需先修正IP问题)
# 先修改cli.c中的INADDR_ANY为inet_addr("127.0.0.1")
$ gcc cli.c -o cli -lpthread
$ ./cli
聊天交互示例
// 服务器终端
cli ip:127.0.0.1 port:50001
to cli:Hello from server!
to cli:#quit
// 客户端终端
to ser:Hi server!
from ser:Hello from server!
from ser:#quit
退出机制说明
服务器发送退出:
- 服务器输入
#quit
→ 服务器进程立即终止 → 客户端接收线程检测到#quit
→ 客户端进程终止
- 服务器输入
客户端发送退出:
- 客户端输入
#quit
→ 客户端进程立即终止 → 服务器接收线程检测到#quit
→ 服务器进程终止
- 客户端输入
关键细节:
- 服务器检测退出命令时使用
strcmp(buf, "#quit\n")
(含换行符)- 客户端检测退出命令时使用
strcmp(buf, "#quit")
(无换行符)- 这种差异源于
fgets()
会保留输入末尾的换行符,而recv()
接收的数据不包含自动换行
核心知识点总结
知识点 | 说明 |
---|---|
TCP套接字创建 | socket(AF_INET, SOCK_STREAM, 0) 创建可靠的字节流连接 |
地址绑定 | bind() 将套接字绑定到特定IP和端口(服务器必须) |
监听与接受连接 | listen() 设置等待队列长度,accept() 阻塞等待客户端连接 |
客户端连接 | connect() 主动发起连接请求(需指定服务器IP和端口) |
多线程通信 | 双线程架构:发送线程独立于接收线程,实现全双工通信 |
消息边界处理 | 通过字符串比较检测退出命令,注意fgets() 保留换行符的特性 |
网络字节序转换 | htons() /ntohs() 处理端口字节序,inet_ntoa() 转换IP格式 |
连接信息获取 | getpeername() 获取对端连接信息(IP和端口) |
进程终止机制 | exit(0) 立即终止进程(简单但非优雅,实际应用应关闭资源后退出) |
程序局限性说明:
- 退出机制使用
exit(0)
会导致资源未完全释放(如套接字未关闭)- 单客户端设计(服务器只接受一个连接)
- 未处理粘包问题(依赖应用层协议)
- 客户端地址配置错误需手动修正(实际部署必须修改)
此实现满足作业基础要求,展示了TCP网络编程的核心流程和多线程通信模型,是理解网络应用开发的良好起点。