• 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++] = '';                            // 貌似标准读入不会有字符串结束符的,需要自己手动添加
            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
    ") == 0) {         // 如果接收到客户端的 bye,就结束通信并从 socks 中删除相应的文件描述符,动态申请的空间也应在删除前释放
                printf("close connection with client %d.
    ", 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++] = '';                // 和客户端一样需要自己手动添加字符串结束符
            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...
    ";
                send(sc, buf, strlen(buf) + 1, 0);
                close(sc);
                free(cli_addr);
                continue;
            }
            socks[sc] = cli_addr;                        // 指向对应申请到的 sockaddr_in 空间
            printf("client %d connect me...
    ", 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迟点再去弄吧~

    *****************************************************************************************************************************************

      今天试了下用 PyQt4 去写个客户端的界面,调了好一天,总算能看到点东西了,先上图:

      而命令行下的客户端(上面的 client.cpp 文件)的运行界面是这样子的:

      服务端的运行情况是:

      PyQt4 编写的客户端(pyqt_client.py)代码是:

    #!/usr/bin/env python
    #-*- coding: utf-8 -*-
    
    from PyQt4 import QtGui, QtCore
    import sys
    import socket
    import thread
    
    
    class Client(QtGui.QWidget):
    
        BUF_LEN = 1024
    
        def __init__(self, parent=None):
    
            QtGui.QWidget.__init__(self, parent)
    
            self.setWindowTitle(u'TCP客户端')
            self.resize(600, 500)
            self.center()
            layout = QtGui.QGridLayout(self)
    
            label_ip = QtGui.QLabel(u'远程主机IP:')
            layout.addWidget(label_ip, 0, 0, 1, 1)
            self.txt_ip = QtGui.QLineEdit('127.0.0.1')
            layout.addWidget(self.txt_ip, 0, 1, 1, 3)
    
            label_port = QtGui.QLabel(u'端口:')
            layout.addWidget(label_port, 0, 4, 1, 1)
            self.txt_port = QtGui.QLineEdit('9003')
            layout.addWidget(self.txt_port, 0, 5, 1, 3)
    
            self.isConnected = False
            self.btn_connect = QtGui.QPushButton(u'连接')
            self.connect(self.btn_connect, QtCore.SIGNAL(
                'clicked()'), self.myConnect)
            layout.addWidget(self.btn_connect, 0, 8, 1, 2)
    
            label_recvMessage = QtGui.QLabel(u'消息内容:')
            layout.addWidget(label_recvMessage, 1, 0, 1, 1)
    
            self.btn_clearRecvMessage = QtGui.QPushButton(u'↓ 清空消息框')
            self.connect(self.btn_clearRecvMessage, QtCore.SIGNAL(
                'clicked()'), self.myClearRecvMessage)
            layout.addWidget(self.btn_clearRecvMessage, 1, 7, 1, 3)
    
            self.txt_recvMessage = QtGui.QTextEdit()
            self.txt_recvMessage.setReadOnly(True)
            self.txt_recvMessage.setStyleSheet('background-color:yellow')
            layout.addWidget(self.txt_recvMessage, 2, 0, 1, 10)
    
            lable_name = QtGui.QLabel(u'姓名(ID):')
            layout.addWidget(lable_name, 3, 0, 1, 1)
            self.txt_name = QtGui.QLineEdit()
            layout.addWidget(self.txt_name, 3, 1, 1, 3)
    
            self.isSendName = QtGui.QRadioButton(u'发送姓名')
            self.isSendName.setChecked(False)
            layout.addWidget(self.isSendName, 3, 4, 1, 1)
    
            label_sendMessage = QtGui.QLabel(u' 输入框:')
            layout.addWidget(label_sendMessage, 4, 0, 1, 1)
            self.txt_sendMessage = QtGui.QLineEdit()
            self.txt_sendMessage.setStyleSheet("background-color:cyan")
            layout.addWidget(self.txt_sendMessage, 4, 1, 1, 7)
            self.btn_send = QtGui.QPushButton(u'发送')
            self.connect(self.btn_send, QtCore.SIGNAL('clicked()'), self.mySend)
            layout.addWidget(self.btn_send, 4, 8, 1, 2)
    
            self.btn_clearSendMessage = QtGui.QPushButton(u'↑ 清空输入框')
            self.connect(self.btn_clearSendMessage, QtCore.SIGNAL(
                'clicked()'), self.myClearSendMessage)
            layout.addWidget(self.btn_clearSendMessage, 5, 6, 1, 2)
            self.btn_quit = QtGui.QPushButton(u'退出')
            self.connect(self.btn_quit, QtCore.SIGNAL('clicked()'), self.myQuit)
            layout.addWidget(self.btn_quit, 5, 8, 1, 2)
    
        def myConnect(self):
            if self.isConnected == False:
                host = str(self.txt_ip.text())
                port = int(self.txt_port.text())
                try:
                    self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
                    self.client_socket.connect((host, port))
                except:
                    self.txt_recvMessage.append(u'服务器连接失败,请检查网络连接或者稍后再试。')
                    return
    
                thread.start_new_thread(self.recv_func, ())
                # td = MyThread(self)
                # td.start()
                self.txt_recvMessage.append(u'服务器连接成功!')
                self.setWindowTitle(self.windowTitle() + ' --> ' + host + ':' + str(port))
                self.isConnected = True
                self.btn_connect.setText(u'断开连接')
            else:
                self.disConnect()
    
        def disConnect(self):
            self.client_socket.close()
            self.txt_recvMessage.append(u'已断开与服务器的连接。')
            self.setWindowTitle(u'TCP客户端')
            self.isConnected = False
            self.btn_connect.setText(u'连接')
    
        def recv_func(self):
            while True:
                try:
                    data = self.client_socket.recv(Client.BUF_LEN)
                except:
                    break
                if not data or not len(data):
                    break
                data = data[:-1]
                self.txt_recvMessage.append(data.decode('utf8'))    # 很重要
            self.disConnect()
    
        def myClearRecvMessage(self):
            self.txt_recvMessage.setText('')
    
        def myClearSendMessage(self):
            self.txt_sendMessage.setText('')
    
        def mySend(self):
            if self.isSendName.isChecked() == True:
                data = self.txt_name.text()
                if data == '':
                    data = u'[匿名]'
                data =  str((data + ': ' + self.txt_sendMessage.text() + '
    ').toUtf8())
            else:
                data =  str((self.txt_sendMessage.text() + '
    ').toUtf8())
            try:
                self.client_socket.sendall(data)
            except:
                self.txt_recvMessage.append(u'消息发送失败...')
                return 
            self.txt_sendMessage.setText('')
    
        def myQuit(self):
            self.close()
    
        def center(self):
            screen = QtGui.QDesktopWidget().screenGeometry()
            size = self.geometry()
            self.move((screen.width() - size.width()) / 2,
                    (screen.height() - size.height()) / 2)
    
        def closeEvent(self, event):
            reply = QtGui.QMessageBox.question(self, u'消息', u'你确定要退出吗?',
                                             QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
            if reply == QtGui.QMessageBox.Yes:
                event.accept()
                try:
                    self.client_socket.close()
                except:
                    pass
            else:
                event.ignore()
    
    app = QtGui.QApplication(sys.argv)
    c = Client()
    c.show()
    sys.exit(app.exec_())

       虽然有点小bug,不过主要功能已经能很好地实现了,以后有时间再来修改下。

      Github地址:https://github.com/NewdawnALM/TcpThreadChats

  • 相关阅读:
    php生成excel
    gearmand
    开启Nginx的目录文件列表功能
    zend框架学习
    引用方法形成树
    智能指针实现
    图文例解C++类的多重继承与虚拟继承
    CC++定位崩溃代码行的方法
    C++函数重定义、重载、重写
    勤奋吧,一天一点,努力提高基本技能。
  • 原文地址:https://www.cnblogs.com/Newdawn/p/5509687.html
Copyright © 2020-2023  润新知