본문 바로가기

예제 실행

접속

접속 종료

예제 파일

06_ChatClient.zip
0.01MB
06_ChatServer.zip
0.01MB

Serv. 소스 코드

main.cpp

#include <QApplication>
#include "widget.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    // [ex.01]
    Widget w;
    w.show();

    return a.exec();
}

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "chatserver.h"

namespace Ui {
    class Widget;
}

// [ex.02]
// Widget 클래스 : QWidget 상속
class Widget : public QWidget
{
    Q_OBJECT

public:

    // [ex.02.1]
    explicit Widget(QWidget *parent = 0);

    // [ex.02.2]
    ~Widget();

private:
    Ui::Widget *ui;

    // [ex.02.1.1]
    QHostAddress m_hostAddress;
    qint16 m_hostPort;
    QString m_title;
    QHostAddress getMyIP();

    // [ex.02.1.2]
    ChatServer *server;

private slots:
    // [ex.02.3]
    void slot_updateClntCNT(int users);
    // [ex.02.4]
    void slot_showMSG(QString msg);
};

#endif // WIDGET_H

widget.cpp

#include <QtNetwork>

#include "widget.h"
#include "chatserver.h"
#include "ui_widget.h"

// [ex.02.1]
// Widget 클래스의 생성자
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
{
    ui->setupUi(this);

    // [ex.02.1.1]
    // 프로그램이 실행되는 디바이스의 IP, Port를 설정하고
    // Widget의 타이틀을 설정에 반영한다.
    m_hostAddress = getMyIP();
    m_hostPort = qint16(25000);
    m_title = "TCP 채팅 서버 (" + m_hostAddress.toString() + " " + QString::number(m_hostPort) + ")";
    setWindowTitle(m_title);

    // [ex.02.1.2]
    // Widget 클래스는, ChatServer 객체를 맴버로 가진다.
    // ChatServer 객체는 QTcpServer를 상속받고, 기능이 추가된 클래스다
    // Server 역할을 하는 객체를 세팅하고, 클라이언트의 연결 요청을 대기하게 한다.
    server = new ChatServer(this);
    server->listen(m_hostAddress, m_hostPort);

    // [ex.02.1.3]
    // server 객체에 새로운 클라이언트가 들어오는 시그널이 발생하면
    // Widget 클래스(=this, 자기 자신)의 slot_updateClntCNT 슬롯이 연결된다.
    connect(server, SIGNAL(signal_updateClntCNT(int)),
            this,     SLOT(slot_updateClntCNT(int)));

    // [ex.02.1.4]
    // server 객체에서 새로운 메시지가 들어오는 시그널이 발생하면
    // Widget 클래스(=this, 자기 자신)의 slot_showMSG 슬롯이 연결된다.
    connect(server, SIGNAL(signal_showNewMSG(QString)),
            this,     SLOT(slot_showMSG(QString)));
}

// [ex.02.2] Widget 클래스의 파괴자
Widget::~Widget()
{
    delete ui;
}

// [ex.02.3]
// server 객체에 새로운 클라이언트가 들어오면
// Widget 클래스(=this, 자기 자신)의 label Ui에 접속자 수를 업데이트 한다.
void Widget::slot_updateClntCNT(int users)
{
    QString str = QString("접속자수 : %1").arg(users);
    ui->label->setText(str);
}

// [ex.02.4]
// server 객체에 새로운 메시지가 들어오면
// Widget 클래스(=this, 자기 자신)의 textEdit Ui에 보여준다.
void Widget::slot_showMSG(QString msg)
{
    ui->textEdit->append(msg);
}

// 프로그램이 실행되는 디바이스의 IP 반환
QHostAddress Widget::getMyIP()
{
    QHostAddress myAddress;
    QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();

    // localhost(127.0.0.1) 가 아닌 것을 사용
    for (int i = 0; i < ipAddressesList.size(); ++i)
    {
        if (ipAddressesList.at(i) != QHostAddress::LocalHost &&
            ipAddressesList.at(i).toIPv4Address())
        {
            myAddress = ipAddressesList.at(i);
            break;
        }
    }

    // 인터넷이 연결되어 있지 않다면, localhost(127.0.0.1) 사용
    if (myAddress.toString().isEmpty())
        myAddress = QHostAddress(QHostAddress::LocalHost);

    return myAddress;
}

chatserver.h

#ifndef CHATSERVER_H
#define CHATSERVER_H

#include <QTcpServer>
#include <QTcpSocket>

// [ex.03]
// ChatServer 클래스: QTcpServer 상속 받음
class ChatServer : public QTcpServer
{
    Q_OBJECT

public:

    // [ex.03.1]
    ChatServer(QObject *parent=0);
    ~ChatServer();

protected:

    // [ex.03.2]
    // socketfd = Socket File Descriptor
    void incomingConnection(qintptr socketfd);

private:

    QSet<QTcpSocket *> qset_clntSKTList;
    QMap<QTcpSocket *, QString> qmap_userList;

signals:

    void signal_updateClntCNT(int No_Users);
    void signal_showNewMSG(QString msg);

private slots:

    // [ex.03.3]
    void slot_readMSG();

    // [ex.03.4]
    void slot_sendUserInfoToAll();

    // [ex.03.5]
    void slot_disconnected();
};

#endif // CHATSERVER_H

 

chatserver.cpp

#include <QtWidgets>
#include <QRegExp>
#include <QDebug>

#include "chatserver.h"

// [ex.03.1]
// ChatServer의 생성자
ChatServer::ChatServer(QObject *parent): QTcpServer(parent)
{
}

ChatServer::~ChatServer()
{
    foreach(QTcpSocket *client, qset_clntSKTList)
        client->disconnectFromHost();
}

// [ex.03.2]
// 서버로 연결 요청이 들어오면 동작
void ChatServer::incomingConnection(qintptr socketfd)
{
    qDebug() << Q_FUNC_INFO;

    // [ex.03.2.1]
    // 새로운 연결 소켓을 만들고
    // 소켓에 파일 디스크립터를 식별한다.
    QTcpSocket *newConnectedSKT = new QTcpSocket(this);
    newConnectedSKT->setSocketDescriptor(socketfd);

    // [ex.03.2.2]
    // qset 을 사용해 연결된 소켓 객체들을 관리한다.
    // qset 에 저장된 연결된 소켓(클라이언트)의 수를 전달하는 시그널을 동작시킨다.
    qset_clntSKTList.insert(newConnectedSKT);
    emit signal_updateClntCNT(qset_clntSKTList.count());

    // [ex.03.2.3]
    // 새로운 연결 소켓이 만들어질때
    // 시그널을 동작시키고 정보 문자열을 매기변수로 넘긴다.
    QString str = QString("새로운 접속자: %1").arg(newConnectedSKT->peerAddress().toString());
    emit signal_showNewMSG(str);

    // [ex.03.2.4]
    // 새로운 연결 소켓에서 읽을 메시지가 들어와 readyRead() 시그널이 동작되면
    // ChatServer 클래스(=this, 자기 자신)의 slot_readyRead 슬롯 함수가 연결된다.
    connect(newConnectedSKT, SIGNAL(readyRead()),
            this,              SLOT(slot_readMSG()));

    // [ex.03.2.5]
    // 새로운 연결 소켓에서 disconnected 시그널이 동작되면
    // ChatServer 클래스(=this, 자기 자신)의 slot_disconnected 슬롯 함수가 연결된다.
    connect(newConnectedSKT, SIGNAL(disconnected()),
            this,              SLOT(slot_disconnected()));
}

// [ex.03.3]
void ChatServer::slot_readMSG()
{
    // [ex.03.3.1]
    // QObject *QObject::sender() const
    // slot 에서만 리턴 유효한 함수로, 슬롯이 동작시킨 시그널의 객체를 반환한다.
    // 즉, 읽을 메시지가 들어와 QTcpSocket의 readyRead() 시그널 소켓을 식별한다.
    QTcpSocket* senderSKT = (QTcpSocket*)sender();

    while(senderSKT->canReadLine())
    {
        QString line = QString::fromUtf8(senderSKT->readLine()).trimmed();
        // QString str = QString("Read line: %1").arg(line);
        // emit signal_showNewMSG(str);

        // "Reg"ular "Exp"ression, 정규표현식
        // regex_Me 객체 이름으로 ()안의 패턴이 정의된 정규 표현식이 생성됩니다.
        QRegExp regex_Me("^/me:(.*)$");

        // 클라이언트가 보낸 메시지가
        // /me:가 포함된
        // 접속자 이름을 알리는 메시지라면, 접속 정보를 모두에게 전송한다.
        if(regex_Me.indexIn(line) != -1)
        {
            QString user = regex_Me.cap(1);
            qmap_userList[senderSKT] = user;
            foreach(QTcpSocket *SKT, qset_clntSKTList)
            {
                SKT->write(QString("[서버]: \"%1\" 접속하셨습니다.").arg(user).toUtf8());
            }

            // [ex.03.4]
            slot_sendUserInfoToAll();
        }
        else if(qmap_userList.contains(senderSKT))
        {
            QString message = line;
            QString user = qmap_userList[senderSKT];
            QString str = QString("유저명: %1, 메시지: %2").arg(user, message);
            emit signal_showNewMSG(str);

            foreach(QTcpSocket *otherClient, qset_clntSKTList)
                otherClient->write(QString(user+":"+message+"\n\n").toUtf8());
        }
    }
}

// [ex.03.4]
// 접속된 유저들 정보를 채팅방에 접속된 모든 사용자들에게 전송
// 새로운 접속자가 있거나, 접속자가 채팅방을 나갈때 사용
void ChatServer::slot_sendUserInfoToAll()
{
    //QStringList에 접속된 유저 정보를 담아
    QStringList userList2;
    foreach(QString user, qmap_userList.values())
        userList2 << user;

    // 모든 접속자에게 전송
    foreach(QTcpSocket *SKT, qset_clntSKTList)
        SKT->write(QString("\n[접속된유저:" + userList2.join(",") + "]\n").toUtf8());
}

// [ex.03.5]
// 접속된 사용자와 연결이 끊길때 처리
void ChatServer::slot_disconnected()
{
    // 접속 종료 시그널을 발생시킨 소켓을 찾아
    QTcpSocket *leftSKT = (QTcpSocket*)sender();


    // 접속 끊긴 소켓을 qset에서 제거하고
    // 접속자 수를 업데이트한다.
    qset_clntSKTList.remove(leftSKT);
    emit signal_updateClntCNT(qset_clntSKTList.count());

    // 접속 끊긴 소켓을 qmap에서 제거하고
    // 접속자 수를 업데이트한다.
    QString user = qmap_userList[leftSKT];
    qmap_userList.remove(leftSKT);

    // 서버 메시지창에 보여주고
    QString str = QString("*연결 종료: ID:%1, IP:%2").arg(user, leftSKT->peerAddress().toString());
    emit signal_showNewMSG(str);

    // 현재 접속된 유저 리스트만 재전송
    slot_sendUserInfoToAll();

    // 접속 종료 메시지를 연결된 모든 접속자에게 전송
    foreach(QTcpSocket *client, qset_clntSKTList)
        client->write(QString("서버: %1 접속종료").arg(user).toUtf8());
}

 


Clnt. 소스 코드

main.cpp

#include "widget.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    // [ex.01]
    // 채팅기능이 있는 Widget을 숨긴다.
    Widget w;
    w.hide();

    return a.exec();
}

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTcpSocket>

#include "loginwidget.h"

namespace Ui {
class Widget;
}

// [ex.02]
class Widget : public QWidget
{
    Q_OBJECT

public:

    // [ex.02.1]
    explicit Widget(QWidget *parent = nullptr);

    // [ex.02.2]
    ~Widget();

private:
    Ui::Widget *ui;

    // [ex.02.1.1]
    LoginWidget *loginWidget;
    // [ex.02.1.2]
    QTcpSocket *socket;

    QString    m_ipAddr;
    QString    m_userName;

private slots:

    // [ex.02.3]
    void slot_loginInfo(QString addr, QString name);

    // [ex.02.4]
    void slot_sendMyName();

    // [ex.02.5]
    void slot_readyRead();

    // [ex.02.6]
    void slot_sayButton_clicked();

    // [ex.02.7]
    void slot_disconnected();
};

#endif // WIDGET_H

widget.cpp

#include <QDebug>
#include <QRegExp>

#include "widget.h"
#include "ui_widget.h"


// [ex.02.1]
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{
    ui->setupUi(this);
    connect(ui->sayButton, &QPushButton::pressed,
            this,          &Widget::slot_sayButton_clicked);
    connect(ui->sayLineEdit , &QLineEdit::returnPressed,
            ui->sayButton,   &QPushButton::pressed);

    // [ex.02.1.1]
    // loginWidget을 먼저 보여주고 IP, Name을 입력받아, 현재(this) Widget에 넘긴다.
    // loginWidget 객체의 loginInfo 시그널이 발생되면
    // Widget 클래스(=this, 자기 자신)의 slot_loginInfo 슬롯이 연결된다.
    loginWidget = new LoginWidget();
    connect(loginWidget, &LoginWidget::signal_loginInfo,
            this,        &Widget::slot_loginInfo);
    loginWidget->show();

    // [ex.02.1.2]
    // 클라이어트에서 사용할 QTcpSocket 객체를 만들고
    socket = new QTcpSocket(this);
    connect(socket, SIGNAL(connected()),
            this,     SLOT(slot_sendMyName()));
    connect(socket, SIGNAL(readyRead()),
            this,     SLOT(slot_readyRead()));
    connect(socket, SIGNAL(disconnected()),
            this,     SLOT(slot_disconnected()));
}

// [ex.02.2]
Widget::~Widget()
{
    delete ui;
}

// [ex.02.3]
// 클라이언트가 지정된 서버 IP로 연결
void Widget::slot_loginInfo(QString addr, QString name)
{
    qDebug() << Q_FUNC_INFO << addr << name;
    m_ipAddr = addr;
    m_userName = name;
    socket->connectToHost(m_ipAddr, 25000);
}

// [ex.02.4]
// 클라이언트 연결 소켓이 서버와 연결되면
// loginWidget을 숨기고, 접속자(나)의 이름을 서버에 전송한다.
void Widget::slot_sendMyName()
{
    loginWidget->hide();

    this->window()->show();
    setWindowTitle("채팅 클라이언트 [ID:" + m_userName + "]");

    socket->write(QString("/me:" + m_userName + "\n").toUtf8());
}

// [ex.02.5]
void Widget::slot_readyRead()
{
    while(socket->canReadLine())
    {
        QString line = QString::fromUtf8(socket->readLine()).trimmed();

        QRegExp messageRegex("^([^:]+):(.*)$");

        if(messageRegex.indexIn(line) != -1)
        {
            QString user = messageRegex.cap(1);
            QString message = messageRegex.cap(2);

            ui->TextEdit_Chat->append("<b style=\"color:blue;\">"+user+"</b>: "+message);
        }
    }
}

// [ex.02.6]
void Widget::slot_sayButton_clicked()
{
    QString message = ui->sayLineEdit->text().trimmed();

    if(!message.isEmpty())
    {
        socket->write(QString(message + "\n").toUtf8());
    }

    ui->sayLineEdit->clear();
    ui->sayLineEdit->setFocus();
}

// [ex.02.7]
// 서버에서 연결을 소켓 연결 종료 정보를 표시한다.
void Widget::slot_disconnected()
{
    ui->TextEdit_Chat->append("-> 서버와 연결이 끊겼습니다");
    qDebug() << Q_FUNC_INFO << "서버로부터 접속 종료.";
}

loginwidget.h

#ifndef LOGINWIDGET_H
#define LOGINWIDGET_H

#include "qhostaddress.h"
#include <QWidget>

namespace Ui {
class LoginWidget;
}

// [ex.03]
class LoginWidget : public QWidget
{
    Q_OBJECT

public:
    // [ex.03.1]
    explicit LoginWidget(QWidget *parent = nullptr);
    // [ex.03.2]
    ~LoginWidget();

private:
    Ui::LoginWidget *ui;

    QString m_servIP;
    QString m_nickName;

    QHostAddress getMyIP();

signals:
    void signal_loginInfo(QString addr, QString name);

private slots:
    // [ex.03.3]
    void slot_loginBtnClicked();
};

#endif // LOGINWIDGET_H

login.cpp

#include <QtNetwork>
#include <QDateTime>

#include "loginwidget.h"
#include "ui_loginwidget.h"


// [ex.03.1]
// loginWidget을 먼저 보여주고 IP, Name을 입력받아,
// loginButton을 누르면, LoginWidget::slot_loginBtnClicked을 실행한다,
LoginWidget::LoginWidget(QWidget *parent) : QWidget(parent), ui(new Ui::LoginWidget)
{
    ui->setupUi(this);
    ui->ipLineEdit->setText(getMyIP().toString());
    ui->nameLineEdit->setText(QTime::currentTime().toString("ss초 zz밀리초"));
    connect(ui->loginButton, &QPushButton::pressed,
            this,            &LoginWidget::slot_loginBtnClicked);
    connect(ui->ipLineEdit , &QLineEdit::returnPressed,
            ui->loginButton,   &QPushButton::pressed);
    connect(ui->nameLineEdit , &QLineEdit::returnPressed,
            ui->loginButton,   &QPushButton::pressed);
}

// [ex.03.2]
LoginWidget::~LoginWidget()
{
    delete ui;
}

// [ex.03.3]
void LoginWidget::slot_loginBtnClicked()
{
    m_servIP = ui->ipLineEdit->text().trimmed();
    m_nickName = ui->nameLineEdit->text().trimmed();

    emit signal_loginInfo(m_servIP, m_nickName);
}

// 일반적으로 초기 테스트는 localhost(127.0.0.1) 를 사용해서 쓸 필요가 없는데
// 다른 팀원들 데스크탑 서버 접속 테스트하기 위해
// 비슷한 대역대 IP인 내 IP를 기본 세팅
QHostAddress LoginWidget::getMyIP()
{
    QHostAddress myAddress;
    QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();

    // localhost(127.0.0.1) 가 아닌 것을 사용
    for (int i = 0; i < ipAddressesList.size(); ++i)
    {
        if (ipAddressesList.at(i) != QHostAddress::LocalHost &&
            ipAddressesList.at(i).toIPv4Address())
        {
            myAddress = ipAddressesList.at(i);
            break;
        }
    }

    // 인터넷이 연결되어 있지 않다면, localhost(127.0.0.1) 사용
    if (myAddress.toString().isEmpty())
        myAddress = QHostAddress(QHostAddress::LocalHost);

    return myAddress;
}

 

 

 

 

 

 

 

 

BasicLike

어? 나 프로그래밍 좋아하네?