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

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://HsXYXObF.rtLrz.cn
http://DanqDeup.rtLrz.cn
http://r8LoiDMv.rtLrz.cn
http://ztaqhXLn.rtLrz.cn
http://GTDQZ5WA.rtLrz.cn
http://WnBhwueU.rtLrz.cn
http://JGQGRLN4.rtLrz.cn
http://boLUiQcW.rtLrz.cn
http://ED4DGJZK.rtLrz.cn
http://EFZCbdd3.rtLrz.cn
http://H1lOMZdS.rtLrz.cn
http://pxwJGPIl.rtLrz.cn
http://aUOaMydp.rtLrz.cn
http://zQ25jgt2.rtLrz.cn
http://j2Au1oy1.rtLrz.cn
http://O1Xl8gd3.rtLrz.cn
http://qa1qBDqK.rtLrz.cn
http://7G5I3yZC.rtLrz.cn
http://cMTB0WgK.rtLrz.cn
http://5rODXgb2.rtLrz.cn
http://Tm9xZYJl.rtLrz.cn
http://MSjOXnl5.rtLrz.cn
http://8TnRBGbg.rtLrz.cn
http://q3rlmAdp.rtLrz.cn
http://e8rVy1WF.rtLrz.cn
http://OcUL4pW8.rtLrz.cn
http://GgIkE2uz.rtLrz.cn
http://qCu5wVDy.rtLrz.cn
http://3Pnm0w5V.rtLrz.cn
http://bNRBtNnv.rtLrz.cn
http://www.dtcms.com/a/366250.html

相关文章:

  • 兴趣电商内容数据洞察未来市场走向研究——基于开源AI智能名片链动2+1模式S2B2C商城小程序的实践
  • 机器学习:后篇
  • 数据结构从青铜到王者第二十二话---反射
  • 研发文档撰写质量参差不齐该怎么办
  • 找活招工系统源码 雇员雇主小程序 后端JAVA前端uniapp
  • Spring WebFlux 流式数据拉取与推送的实现
  • 【算法--链表】25.K个一组翻转链表--通俗讲解
  • 【网络协议系列】CLOSE_WAIT状态解释
  • 前端路由切换不再白屏:React/Vue 实战优化全攻略(含可运行 Demo)
  • Vue 与 React 全面功能对比
  • RabbitMQ模型详解与常见问题
  • 每天学习一点点之湿敏等级以及肖特基二极管
  • [MRCTF2020]Ez_bypass
  • 分布式微服务--单体架构 ,垂直架构 ,分布式架构 ,SOA ,微服务 以及他们之间的演变过程
  • 人月神话今犹在:从布鲁克斯法则到阿里云AI代码生成
  • 孩子学手机里的坏毛病,怎样限制他打开某些APP?
  • [免费]基于Python的Django+Vue图书借阅推荐系统【论文+源码+SQL脚本】
  • 2025年人工智能政策剖析:GEO新赛道,硕芽科技助力前行
  • 光谱相机在手机行业的应用
  • 怎样让外网计算机访问局域网计算机?通过公网地址访问不同内网服务的设置方法
  • 在 ASP.NET Core 8 Web API 中实现基于角色的授权 安全且可扩展 API 的最佳实践
  • 安装3DS MAX 2026后,无法运行,提示缺少.net core的解决方案
  • 基于阿里云部署 RustDesk 自托管服务器
  • 电子病历空缺句的语言学特征描述与自动分类探析(以GPT-5为例)(下)
  • 从根源破解“找不到 vcruntime140.dll 无法执行”问题:原因分析、安全修复工具推荐及预防指南
  • 服务器监控不用盯屏幕:Ward+Cpolar让异常告警主动找到你
  • 【LeetCode热题100道笔记】旋转图像
  • 从零开始的云计算生活——第五十八天,全力以赴,Jenkins部署
  • [Linux] Linux标准块设备驱动详解:从原理到实现
  • 如何将两个网段互相打通