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

发布于:2025-09-05 ⋅ 阅读:(16) ⋅ 点赞:(0)

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网络编程的核心流程和多线程通信模型,是理解网络应用开发的良好起点。


网站公告

今日签到

点亮在社区的每一天
去签到