多线程编程实战之“多人聊天小程序”

本文详细介绍了如何使用多线程、TCPsocket和互斥锁实现一个简单的多人聊天小程序,包括服务器端的主循环、新连接处理、数据交互以及客户端的连接、消息发送和接收。重点在于展示了如何在socket编程中结合多线程技术以实现并发服务。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.示例描述

因为最近在学多线程,并且用多线程做了一个“多人聊天小程序”,个人认为是吃透这个小程序,注释十分详细,所以需要先行记录下来,多线程的基础知识可以在示例之前给出。这个示例是基于TCP的socket多线程程序,实际上这个例子是综合使用socket、多线程和互斥锁,也就是“每来一个客户端连接,就开一个多线程去提供服务”,而在访问临界区资源的时候,就用互斥锁上锁,这样就把socket、多线程和互斥锁结合使用了。具体描述如下:
1)多个终端运行客户端程序,连接上服务器端;连接之后,可以发送消息
2)服务器端收到客户端发送的消息之后,将该消息广播给客户端
3)每个客户端都可以收到上述该条信息。
综述的效果类似于一个群聊。
下面分别从服务器端和客户端来实现socket程序。

2.服务器端

服务器端的主线程(main函数)代码结构和第一篇文章的socket编程框架是一致的:
1)绑定和监听;
2)开一个循环,不断提供接受连接请求;
3)每来一个连接请求,就开启一个线程去实现数据交互;
4)用线程分离数据交互的功能(t_main函数),对于每个连接,先读取客户端发送过来的数据,然后广播给所有客户端(send_msg_all函数);
5)断开连接则需要将对应的文件描述符从记录数组中移除,之后的广播则不会发送给该客户端了。

#include <pthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>

#define MAX_CONNECTION 100

//线程入口函数
void* t_main(void*);

//发消息给所有客户端
void send_msg_all(int, char*, int);

//因为有很多个用来连接产生的sockfd,服务器端要做的事是:将所有连接通道里的信息都取出来,然后打包在一起返回给每个客户端
//所以,使用数据来存储所有的连接
int socks[MAX_CONNECTION];
int sock_count = 0;//记录连接的个数

//使用互斥量加锁
pthread_mutex_t mutex;

int main()
{
    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    std::cout << "看门狗sokcet的sockfd: " << sockfd << std::endl;
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(8080);

    if (bind(sockfd, (sockaddr*)&addr, sizeof(addr)) == -1)
    {
        std::cout << "socket bind error..." << std::endl;
        return 0;
    }
    
    if (listen(sockfd, 5) == -1)
    {
        std::cout << "socket listen error..." << std::endl;
        return 0;
    }

    while (true)
    {
        sockaddr_in svr_addr;
        socklen_t len = sizeof(svr_addr);
        //accept函数返回的文件描述符是 客户端的sockfd
        //为什么accept函数的第三个参数是指针呢,因为可以修改传进来的地址长度
        int clt_sockfd = accept(sockfd, (sockaddr*)&svr_addr, &len);
        if (clt_sockfd == -1)
        {
            std::cout << "socket accept error..." << std::endl;
            return 0;
        }

        //将成功连接的sock,用全局变量存储
        //又因为是对全局变量的访问,安全起见(因为不是线程内部?),加锁
        pthread_mutex_lock(&mutex);
        std::cout << "有一个客户端连接上来了, sockfd = " << clt_sockfd << std::endl;
        socks[sock_count++] = clt_sockfd;
        std::cout << "现在的连接数: " << sock_count << ", sockfd分别是: ";
        for (int i = 0; i < sock_count; i++)
            std::cout << socks[i] << ", ";
        std::cout << std::endl;
        pthread_mutex_unlock(&mutex);

        //只有有客户端连接上来了,就开启一个线程进行连接
        pthread_t tid;
        pthread_create(&tid, nullptr, t_main, &clt_sockfd);
        //pthread_join(tid, nullptr);
        //用pthread_detach来引导线程的销毁
        pthread_detach(tid);

        //std::cout << "client ip: " << inet_ntoa(svr_addr.sin_addr) << std::endl;
    }
    close(sockfd);

}

//在线程函数中,进行数据交互
void* t_main(void* arg)
{
    int sockfd = *((int*)arg);

    //从当前线程服务的客户端连接通道里,读取数据出来
    char msg[BUFSIZ];

    /*
    而第一种写法由于操作符的优先级问题,导致size变量不能正确地反映读取的字节数,这会引起逻辑错误。
    在编写涉及赋值和比较操作的循环条件时,正确使用括号至关重要,以确保操作的顺序符合预期
    注意: !=操作符 优先于 =操作符执行
    */
    int size = 0;//size要在循环之前定义啊....
    //记住现在使用的是多线程,此时这线程内部的代码应该对于所有客户端的连接都会执行
    //所以,以下循环是在 从某个连接通道里不断地读取数据(即客户端在不停地往通道里写数据)
    //所以,当客户端断开连接了,即跳出循环
    while ((size = read(sockfd, msg, sizeof(msg))) != 0)
    {
        //std::cout << "sizeof(buff) = " << sizeof(msg) << ", size = " << size << std::endl;
        //将从这个连接通道读取出来的数据发送给 所有客户端
        send_msg_all(sockfd, msg, size);
    }
    //所以,断开连接之后,顺序执行到了以下代码
    //接下来的代码要做的事是: 移除断开连接的sock
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < sock_count; i++)
    {
        if (sockfd == socks[i])
        {
            std::cout << "sockfd = " << sockfd << ", 已经断开连接..." <<std::endl;
            //移除sockfd指向的连接通道
            while(i < sock_count - 1)
            {
                socks[i] = socks[i + 1];
                i++;
            }
            break;
        }
        
    }
    sock_count--;
    std::cout << "移除断开连接的socket之后, socks: ";
    for(int i = 0; i < sock_count; i++)
        std::cout << socks[i] << ", ";
    std::cout << std::endl;
    pthread_mutex_unlock(&mutex);

    //关闭sock放在线程函数里
    close(sockfd);
    return nullptr;
}

//类似于广播,将当前线程服务的客户端传输过来的数据 广播发送给每个已连接的客户端
void send_msg_all(int sockfd, char* buff, int size)
{
    std::cout << "从sockfd = " << sockfd << " 读取到数据: " << buff << std::endl;
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < sock_count; i++)
    {
        //std::cout << "there are some connection..." << std::endl;
        write(socks[i], buff, size);
    }
    pthread_mutex_unlock(&mutex);
}

3.客户端

客户端的代码结构和第一篇文章的socket编程框架是一致的:
1)注册服务器端地址信息,请求连接;
2)使用线程分离数据交互:开启两个线程分离读写(send_msg和recv_msg函数);
3)send_msg线程是输入客户端的消息,发送给服务器端;
4)recv_msg线程是从服务器端接收广播消息,打印出来。

#include <pthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>


//使用多线程实现读写分离
void* send_msg(void*);
void* recv_msg(void*);

//用来接收客户端程序的命令行参数
std::string name;

//似乎不用加锁
pthread_mutex_t mutex;

int main(int argc, char* argv[])
{
    name = argv[1];
    pthread_mutex_init(&mutex, nullptr);

    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    std::cout << name << "对应的sockfd是" << sockfd << std::endl;

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(8080);

    if (connect(sockfd, (sockaddr*)&addr, sizeof(addr)) == -1)
    {
        std::cout << "connect error..." << std::endl;
        return 0;
    }
    std::cout << "connect successfully...." << std::endl;


    pthread_t thread_send, thread_recv;
    pthread_create(&thread_send, nullptr, send_msg, &sockfd);
    pthread_create(&thread_recv, nullptr, recv_msg, &sockfd);

    //为什么不能用 pthread_detach ?
    //因为使用 pthread_detach 将主线程和子线程分离了,那么主线程继续顺序执行
    //执行到close(sockfd); 直接断开连接了。所以主线程必须等待数据交互完成
    // pthread_detach(thread_send);
    // pthread_detach(thread_recv);
    pthread_join(thread_send, nullptr);
    pthread_join(thread_recv, nullptr);

    std::cout << "close connection..." << std::endl;
    close(sockfd);
}

//将客户端输入的数据 发送给服务器端
void* send_msg(void* arg)
{
    std::cout << "send thread main..." << std::endl;
    int sockfd = *((int*)arg);
    while (true)
    {
        std::string msg;
        getline(std::cin, msg);//丢弃了换行
        std::string name_msg = "[" + name + "]: " + msg;//替换了C的格式化输入
        //std::cout << "name_msg: " << name_msg.c_str() << ", size  = " << name_msg.size() << std::endl;
        //pthread_mutex_lock(&mutex);
        write(sockfd, name_msg.c_str(), name_msg.size());
        //pthread_mutex_unlock(&mutex);

    }
    return nullptr;
}

//接收服务器端广播的消息
void* recv_msg(void* arg)
{
    std::cout << "recv thread main..." << std::endl;
    int sockfd = *((int*)arg);
    //std::string msg;
    char buff[BUFSIZ];
    //std::cout << "in recv thread main..." << std::endl;
    //int size = 0;
    while (true)
    {

        //int all_size = 0;
        // while ((size = read(sockfd, buff, BUFSIZ - 1)) != 0)
        // {
        //     all_size += size;
        // }
        //因为服务器端每次只广播一条数据,因此可以不用循环来接收
        int size = read(sockfd, buff, BUFSIZ - 1);
        buff[size] = 0;
        std::cout << buff << std::endl;
    }
    return nullptr;
}

4.效果图

以下图片是我开了一个服务器端,六个客户端来模拟多人聊天的截图。截图里面的输入输出信息十分详细地描述了客户端连接到服务器端,然后发送聊天消息给服务器端,服务器端作为中转 广播给所有连接的客户端,这样一个多人聊天的效果。
谈谈个人收获
1)首先是掌握了socket编程的框架,能够很快、很清晰地写出socket相关的代码;
2)然后在以上代码中,嵌入多线程相关的代码。这就考虑到需要用多线程分离的功能,分别是服务器端用一个线程分离了读写功能,客户端用两个线程分别分离了读写;
3)在线程入口函数中,实现对应的功能。有点类似自顶向下的面向过程编程;
4)最后,在编写代码的过程中,我发现其实比较难的是read/write等读写函数的使用,这其实是socket通信编程的核心。而其中,我认为更难的使用的是read之类的读取数据函数。因为目前的数据很简单,没有用到数据结构,所以也不需要定义协议。如果需要定义协议,那么read的读取就显得很重要了。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值