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遲點再去弄吧~

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

  今天試了下用 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() + '\n').toUtf8())
        else:
            data =  str((self.txt_sendMessage.text() + '\n').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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM