linux下使用多线程编写的聊天室

Posted Newdawn_ALM

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux下使用多线程编写的聊天室相关的知识,希望对你有一定的参考价值。

  自从开始学linux网络编程后就想写个聊天室,一开始原本打算用多进程的方式来写,可是发觉进程间的通信有点麻烦,而且开销也大,后来想用多线程能不能实现呢,于是便去看了一下linux里线程的用法,实际上只需要知道 pthread_create 就差不多了,于是动手开干,用了两天时间,调试的过程挺痛苦的,一开始打算用纯C来撸,便用简单的数组来存储客户端的连接信息,可是运行时出现了一些很奇怪的问题,不知道是不是访问了临界资源,和线程间的互斥有关等等;奇怪的是,当改用STL的set或map时问题就解决了,但上网搜了下发现STL也不是线程安全的,至于到底是什么问题暂时不想去纠结了,可能是其它一些小细节的错误吧。先贴上代码:

首先是必要的头文件 header.h:

#ifndef  __HEADER_H
#define  __HEADER_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <error.h>
#include <signal.h>
#include <sys/wait.h>
#include <assert.h>

#include <pthread.h>

#define  bool  int                  // the 3 lines is for c originally
#define  true   1
#define  false  0

#define  PORT  9003
#define  BUF_LEN  1024              // 缓冲区大小
#define  MAX_CONNECTION  6          // 服务器允许的最大连接数,可自行更改

#define  For(i,s,t)  for(i = (s); i != (t); ++i)

#endif // __HEADER_H

  然后是客户端部分 client.cpp,相对来说简单一些:

#include "header.h"

// 客户端接收消息的线程函数
void* recv_func(void *args)
{
    char buf[BUF_LEN];
    int sock_fd = *(int*)args;
    while(true) {
        int n = recv(sock_fd, buf, BUF_LEN, 0);
        if(n <= 0)   break;                  // 这句很关键,一开始不知道可以用这个来判断通信是否结束,用了其它一些很奇葩的做法来结束并关闭 sock_fd 以避免 CLOSE_WAIT 和 FIN_WAIT2 状态的出现T.T
        write(STDOUT_FILENO, buf, n);
    }
    close(sock_fd);
    exit(0);
}

// 客户端和服务端进行通信的处理函数
void process(int sock_fd)
{
    pthread_t td;
    pthread_create(&td, NULL, recv_func, (void*)&sock_fd);      // 新开个线程来接收消息,避免了一读一写的原始模式,一开始竟把它放进 while 循环里面了,泪崩。。。

    char buf[BUF_LEN];
    while(true) {
        int n = read(STDIN_FILENO, buf, BUF_LEN);
        buf[n++] = \0;                            // 貌似标准读入不会有字符串结束符的,需要自己手动添加
        send(sock_fd, buf, n, 0);
    }
    close(sock_fd);
}

int main(int argc, char *argv[])
{
    assert(argc == 2);

    struct sockaddr_in cli;
    bzero(&cli, sizeof(cli));
    cli.sin_family = AF_INET;
    cli.sin_addr.s_addr = htonl(INADDR_ANY);
    cli.sin_port = htons(PORT);                     // 少了 htons 的话就连接不上了,因为小端机器的原因???

    int sc = socket(AF_INET, SOCK_STREAM, 0);
    if(sc < 0) {
        perror("socket error");
        exit(-1);
    }
    inet_pton(AF_INET, argv[1], &(cli.sin_addr));           // 用第一个参数作为连接服务器端的地址

    int err = connect(sc, (struct sockaddr*)&cli, sizeof(cli));
    if(err < 0) {
        perror("connect error");
        exit(-2);
    }
    process(sc);
    close(sc);

    return 0;
}

  最后是服务端 server.cpp:

#include <map>
#include "header.h"
using std::map;

map<int, struct sockaddr_in*> socks;         // 用于记录各个客户端,键是与客户端通信 socket 的文件描述符,值是对应的客户端的 sockaddr_in 的信息

// 群发消息给 socks 中的所有客户端
inline void send_all(const char *buf, int len)
{
    for(auto it = socks.begin(); it != socks.end(); ++it)
        send(it->first, buf, len, 0);
}

// 服务端端接收消息的线程函数
void* recv_func(void* args)
{
    int cfd = *(int*)args;
    char buf[BUF_LEN];
    while(true) {
        int n = recv(cfd, buf, BUF_LEN, 0);
        if(n <= 0)   break;                     // 关键的一句,用于作为结束通信的判断
        write(STDOUT_FILENO, buf, n);
        if(strcmp(buf, "bye\n") == 0) {         // 如果接收到客户端的 bye,就结束通信并从 socks 中删除相应的文件描述符,动态申请的空间也应在删除前释放
            printf("close connection with client %d.\n", cfd);
            free(socks[cfd]);
            socks.erase(cfd);
            break;
        }
        send_all(buf, n);           // 群发消息给所有已连接的客户端
    }
    close(cfd);                 // 关闭与这个客户端通信的文件描述符
}

// 和某一个客户端通信的线程函数
void* process(void *argv)
{
    pthread_t td;
    pthread_create(&td, NULL, recv_func, (void*)argv);         // 在主处理函数中再新开一个线程用于接收该客户端的消息

    int sc = *(int*)argv;
    char buf[BUF_LEN];
    while(true) {
        int n = read(STDIN_FILENO, buf, BUF_LEN);
        buf[n++] = \0;                // 和客户端一样需要自己手动添加字符串结束符
        send_all(buf, n);               // 服务端自己的信息输入需要发给所有客户端
    }
    close(sc);
}

int main(int argc, char *argv[])
{
    struct sockaddr_in serv;
    bzero(&serv, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    serv.sin_port = htons(PORT);

    int ss = socket(AF_INET, SOCK_STREAM, 0);
    if(ss < 0) {
        perror("socket error");
        return 1;
    }
    int err = bind(ss, (struct sockaddr*)&serv, sizeof(serv));
    if(err < 0) {
        perror("bind error");
        return 2;
    }
    err = listen(ss, 2);
    if(err < 0) {
        perror("listen error");
        return 3;
    }

    socks.clear();          // 清空 map
    socklen_t len = sizeof(struct sockaddr);

    while(true) {
        struct sockaddr_in *cli_addr = (struct sockaddr_in*)malloc(sizeof(struct sockaddr_in));
        int sc = accept(ss, (struct sockaddr*)cli_addr, &len);
        if(sc < 0) {
            free(cli_addr);
            continue;
        }
        if(socks.size() >= MAX_CONNECTION) {            // 当将要超过最大连接数时,就让那个客户端先等一下
            char buf[128] = "connections is too much, please waiting...\n";
            send(sc, buf, strlen(buf) + 1, 0);
            close(sc);
            free(cli_addr);
            continue;
        }
        socks[sc] = cli_addr;                        // 指向对应申请到的 sockaddr_in 空间
        printf("client %d connect me...\n", sc);

        pthread_t td;
        pthread_create(&td, NULL, process, (void*)&sc);       // 开一个线程来和 accept 的客户端进行交互
    }
    return 0;
}

  makefile文件:

all: server client
server: server.cpp
    g++ -std=c++11 -o server server.cpp -lpthread
client: client.cpp
    g++ -std=c++11 -o client client.cpp -lpthread
clean:
    rm -f *.o

  在我的ubuntu 14.04 64 位的机器上测试过没有什么问题,客户端与服务端能正常的交互和退出,能通过服务端接收其它客户端发送的消息,运行时cpu和内存占用情况正常,不会产生什么奇怪的bug。暂时只写了个终端的界面,客户端的UI迟点再去弄吧~

以上是关于linux下使用多线程编写的聊天室的主要内容,如果未能解决你的问题,请参考以下文章

linux C语言 TCP 多线程 简易聊天室

多个请求是多线程吗

Linux 多线程编程

Linux以下基于TCP多线程聊天室(server)

Linux下基于TCP协议的群聊系统设计(多线程+select)

从0实现基于Linux socket聊天室-增加数据加密功能-6