当前位置: 首页 > java >正文

Day35 TCP实时聊天程序实现(多线程)

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,导致连接失败

程序执行流程

服务器端启动流程

服务器(ser.c)操作系统创建监听套接字(socket)绑定地址(bind)开始监听(listen)准备就绪等待连接(accept)返回客户端连接创建发送/接收线程接收用户输入并发送接收并显示客户端消息loop[消息交互]服务器(ser.c)操作系统

客户端启动流程

客户端(cli.c)操作系统创建套接字(socket)连接服务器(connect)连接成功创建发送/接收线程接收用户输入并发送接收并显示服务器消息loop[消息交互]客户端(cli.c)操作系统

理想运行结果

服务器端启动

$ 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

退出机制说明

  1. 服务器发送退出

    • 服务器输入 #quit → 服务器进程立即终止 → 客户端接收线程检测到 #quit → 客户端进程终止
  2. 客户端发送退出

    • 客户端输入 #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) 立即终止进程(简单但非优雅,实际应用应关闭资源后退出)

程序局限性说明

  1. 退出机制使用 exit(0) 会导致资源未完全释放(如套接字未关闭)
  2. 单客户端设计(服务器只接受一个连接)
  3. 未处理粘包问题(依赖应用层协议)
  4. 客户端地址配置错误需手动修正(实际部署必须修改)

此实现满足作业基础要求,展示了TCP网络编程的核心流程和多线程通信模型,是理解网络应用开发的良好起点。

http://www.xdnf.cn/news/20037.html

相关文章:

  • 3 步搞定顶刊科研插图!用 GPT-5 反推提示词,Nano Banana 模型一键出图,附实操演示
  • 国内外开源大模型 LLM整理
  • 2025 年高教社杯全国大学生数学建模竞赛E 题 AI 辅助智能体测完整成品 思路 模型 代码 结果分享!全网首发高质量!!!
  • 【LeetCode】22、括号生成
  • 算法之二叉树
  • 【Python基础】 15 Rust 与 Python 基本类型对比笔记
  • C# 修改基类List中某一元素的子类类型
  • 11 月广州见!AUTO TECH China 2025 汽车内外饰展,解锁行业新趋势
  • Leetcode—3516. 找到最近的人【简单】
  • ORA-12547: TNS:lost contact
  • 算法模板(Java版)_字符串、并查集和堆
  • matlab版本粒子群算法(PSO)在路径规划中的应用
  • PDF批量加盖电子骑缝章的方法!高效办公必备
  • 每天学习一点点之湿敏等级以及肖特基二极管
  • C#之LINQ
  • wps的excel如何转为谷歌在线表格
  • testng.xml
  • Opencv: cv::LUT()深入解析图像块快速查表变换
  • sqlserver2008导入excel表数据遇到的问题
  • 无线路由器:从家庭上网到智慧互联的核心设备
  • 人工智能学习:LR和SVM的联系与区别?
  • AI助力软件UI概念设计:卓伊凡收到的客户设计图引发的思考
  • Node.js轻松生成动态二维码
  • C++对象模型的底层逻辑
  • 【数据分享】土地利用矢量shp数据分享-福建
  • 从关键词到语义理解:小陌引擎如何重构AI搜索优化逻辑?
  • Android 12 在 Rockchip 平台上的分区表parametet.txt 自动生成机制解析
  • 【单片机day03】
  • vue3存储/获取本地或会话存储,封装存储工具,结合pina使用存储
  • 电子病历空缺句的语言学特征描述与自动分类探析(以GPT-5为例)(下)