채팅 서버 예제 구현
<chatserver.h>
#ifndef CHATSERVER_H
#define CHATSERVER_H
#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
// ChatServer 클래스 선언: QTcpServer를 상속받아 채팅 서버 기능을 구현
class ChatServer : public QTcpServer
{
Q_OBJECT
public:
// 생성자: ChatServer 객체를 초기화하며, 부모 객체를 설정
ChatServer(QObject *parent = nullptr);
// 소멸자: 객체가 소멸될 때 호출됩니다.
~ChatServer();
private slots:
// 슬롯: 클라이언트로부터 데이터를 읽을 준비가 되었을 때 호출
void readyRead();
// 슬롯: 클라이언트가 연결을 끊었을 때 호출
void disconnected();
// 슬롯: 현재 접속 중인 사용자 목록을 클라이언트들에게 전송
void sendUserList();
signals:
// 시그널: 접속된 클라이언트 수가 변경되었을 때 발생
void clients_signal(int users);
// 시그널: 메시지가 전송되었을 때 발생
void message_signal(QString msg);
protected:
// 새로운 클라이언트 연결이 들어올 때 호출되는 함수. 소켓 파일 디스크립터를 매개변수로 받음.
void incomingConnection(qintptr socketfd);
private:
// 현재 연결된 클라이언트 소켓들을 저장하는 QSet. 중복되지 않는 클라이언트를 관리
QSet<QTcpSocket*> clients;
// 클라이언트 소켓과 사용자 이름을 매핑하여 관리하는 QMap
QMap<QTcpSocket*, QString> users;
};
#endif // CHATSERVER_H
- protected 접근 제한자에서 선언한 incomingConnection( ) 함수는 새로운 클라이언트가 접속하게 되면 호출되는 함수이다. clients_signal( ) 시그널은 이 함수에서 발생한다.
- 시그널의 인자로 현재 서버에 접속한 사용자 수를 인자로 넘겨준다.
- 그리고 클라이언트에 대한 메시지를 수신하 readyRead( ) Slot 함수가 호출될 수 있도록 이 함수를시그널과 연결한다.
- 따라서 클라이언트가 메시지를 보내면 readyRead( ) Slot 함수가 호출된다.
- disconnected( ) 시그널은 클라이언트 접속을 종료하면 이 시그널이 발생하고 이 시그널과 연결된 disconnected( ) Slot 함수가 호출된다.
<chatserver.cpp>
#include "chatserver.h"
#include <QRegularExpression>
#include <QRegularExpressionMatch>
// ChatServer 클래스 생성자, QTcpServer를 상속받아 초기화
ChatServer::ChatServer(QObject *parent)
: QTcpServer(parent)
{
}
// 새로운 클라이언트 연결 시 호출되는 함수
void ChatServer::incomingConnection(qintptr socketfd)
{
QTcpSocket *client = new QTcpSocket(this); // 새로운 QTcpSocket 생성
client->setSocketDescriptor(socketfd); // 소켓 파일 디스크립터 설정
clients.insert(client); // 클라이언트를 클라이언트 목록에 추가
emit clients_signal(clients.count()); // 현재 클라이언트 수를 시그널로 전달
// 새로운 클라이언트 접속 메시지 생성
QString str;
str = QString("New Member: %1")
.arg(client->peerAddress().toString());
emit message_signal(str); // 메시지 시그널을 통해 접속 알림
// 클라이언트의 readyRead, disconnected 시그널을 각각 슬롯에 연결
connect(client, SIGNAL(readyRead()), this,
SLOT(readyRead()));
connect(client, SIGNAL(disconnected()), this,
SLOT(disconnected()));
}
// 클라이언트로부터 데이터가 도착했을 때 호출되는 슬롯
void ChatServer::readyRead()
{
QTcpSocket *client = (QTcpSocket*)sender(); // 데이터를 보낸 클라이언트 소켓 식별
while(client->canReadLine()) // 클라이언트로부터 읽을 수 있는 데이터가 있는 동안 반복
{
QString line = QString::fromUtf8(client->readLine()).trimmed(); // 한 줄씩 읽기
// 읽은 데이터에 대한 메시지 생성
QString str;
str = QString("Read line: %1").arg(line);
emit message_signal(str); // 메시지 시그널을 통해 알림
// "/me:"로 시작하는 메시지를 체크하는 정규 표현식
QRegularExpression meRegex("^/me:(.*)$");
QRegularExpressionMatch match = meRegex.match(line);
if(match.hasMatch()) // 정규 표현식이 매칭되면
{
QString user = match.captured(1); // 유저명 추출
users[client] = user; // 클라이언트와 유저명을 매핑
// 모든 클라이언트에게 새로운 유저 접속 알림
foreach(QTcpSocket *client, clients)
{
client->write(QString("Server: %1 connected\n")
.arg(user).toUtf8());
}
// 유저 리스트 업데이트
//sendUserList();
}
else if(users.contains(client)) // 유저 목록에 해당 클라이언트가 있으면
{
QString message = line; // 메시지 내용
QString user = users[client]; // 유저명
// 유저명과 메시지를 포함한 알림 메시지 생성
QString str;
str = QString("User name: %1, Message: %2")
.arg(user, message);
emit message_signal(str); // 메시지 시그널을 통해 알림
// 다른 클라이언트에게 메시지 전송
foreach(QTcpSocket *otherClient, clients)
otherClient->write(QString(user+":"+message+"\n")
.toUtf8());
}
}
}
// 클라이언트가 연결을 끊었을 때 호출되는 슬롯
void ChatServer::disconnected()
{
QTcpSocket *client = (QTcpSocket*)sender(); // 연결이 끊긴 클라이언트 식별
// 연결 종료 메시지 생성
QString str;
str = QString("Disconnect: %1")
.arg(client->peerAddress().toString());
emit message_signal(str); // 메시지 시그널을 통해 알림
clients.remove(client); // 클라이언트를 클라이언트 목록에서 제거
emit clients_signal(clients.count()); // 현재 클라이언트 수를 시그널로 전달
QString user = users[client]; // 해당 클라이언트의 유저명
users.remove(client); // 유저 목록에서 제거
sendUserList(); // 유저 리스트 업데이트
foreach(QTcpSocket *client, clients)
client->write(QString("Server: %1 Disconnect").arg(user).toUtf8()); // 다른 클라이언트에게 연결 종료 알림
}
// 모든 클라이언트에게 현재 유저 목록을 전송
void ChatServer::sendUserList()
{
QStringList userList;
foreach(QString user, users.values())
userList << user; // 유저명을 리스트에 추가
// 모든 클라이언트에게 유저 목록 전송
foreach(QTcpSocket *client, clients)
client->write(QString("User:" + userList.join(",") + "\n").toUtf8());
}
// ChatServer 소멸자
ChatServer::~ChatServer()
{
deleteLater(); // 객체를 안전하게 삭제
}
- incomingConnection( ) 함수에서 QTcpSocket 을 새로 만드는 것은 새로운 클라이언트의 소켓 오브젝트를 생성하기 위함이다.
- 이 함수에서 생성된 QTcpSocket 클래스의 오브젝트는 users 라는 클라이언트 QMap 컨테이너에 저장된다.
- readyRead( ) 함수는 서버에 접속한 클라이언트가 메시지를 보내면 호출되는 Slot 함수이다.
- disconnected( ) 함수는 클라이언트가 접속을 종료하면 호출되는 Slot 함수이다.
- 이 함수에서는 users 라는 QMap 컨테이너에서 접속을 종료한 클라이언트의 QTcpSocket 클래스의 오브젝트를 제거한다.
<widget.h>
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget> // QWidget 클래스 포함
#include "chatserver.h" // ChatServer 클래스 포함
// Qt 네임스페이스 시작
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; } // UI를 위한 클래스 Widget의 네임스페이스 선언
QT_END_NAMESPACE
// Qt 네임스페이스 끝
// Widget 클래스 선언: QWidget을 상속받아 UI와 서버 통신을 처리
class Widget : public QWidget
{
Q_OBJECT
public:
// 생성자: Widget 객체를 초기화하며 부모 위젯을 설정할 수 있음
Widget(QWidget *parent = nullptr);
// 소멸자: Widget 객체가 소멸될 때 호출됨
~Widget();
private:
Ui::Widget *ui; // UI를 관리하는 포인터 (자동 생성되는 UI 관련 클래스)
ChatServer *server; // ChatServer 객체를 가리키는 포인터
private slots:
// 슬롯: 클라이언트 수가 변경될 때 호출됨. users는 접속된 클라이언트 수를 나타냄
void slot_clients(int users);
// 슬롯: 새로운 메시지가 수신되었을 때 호출됨. msg는 수신된 메시지를 나타냄
void slot_message(QString msg);
};
#endif // WIDGET_H
- Widget 클래스에서 slot_clients( ) Slot 함수는 새로운 클라이언트 접속하거나 접속을 종료 했을 때 ChatServer 클래스에서 발생하는 시그널
- 그리고 slot_message( ) 함수는 ChatServer 클래스에서 클라이언트가 메시지를 보내거나 접속을 종료하면 호출되는 Slot 함수이다.
#include "widget.h"
#include "./ui_widget.h"
// Widget 클래스의 생성자
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this); // UI 설정 초기화
// ChatServer 객체를 생성
server = new ChatServer();
// 서버에서 발생하는 clients_signal 신호와 slot_clients 슬롯을 연결
connect(server, SIGNAL(clients_signal(int)), this,
SLOT(slot_clients(int)));
// 서버에서 발생하는 message_signal 신호와 slot_message 슬롯을 연결
connect(server, SIGNAL(message_signal(QString)), this,
SLOT(slot_message(QString)));
// 서버가 모든 네트워크 인터페이스에서 포트 35000번으로 수신 대기
server->listen(QHostAddress::Any, 35000);
}
// 클라이언트 수가 변경될 때 호출되는 슬롯 함수
void Widget::slot_clients(int users)
{
// "Connected Members : X" 형식의 문자열을 생성하여 레이블에 표시
QString str = QString("Connected Members : %1").arg(users);
ui->label->setText(str);
}
// 새로운 메시지가 수신될 때 호출되는 슬롯 함수
void Widget::slot_message(QString msg)
{
// 수신된 메시지를 텍스트 편집 위젯에 추가
ui->textEdit->append(msg);
}
// Widget 클래스의 소멸자
Widget::~Widget()
{
// UI 자원을 해제
delete ui;
}
- Widget 클래스는 Qt의 QWidget을 상속받아 GUI 위젯을 정의합니다.
- Widget::Widget() 생성자에서 UI를 초기화하고, ChatServer 객체를 생성합니다. 서버에서 발생하는 신호(signal)를 슬롯(slot) 함수와 연결하여 이벤트를 처리합니다.
- server->listen() 호출로 서버가 모든 네트워크 인터페이스에서 포트 35000을 통해 연결 요청을 수신 대기합니다.
- slot_clients 함수는 현재 연결된 사용자 수를 업데이트하여 레이블에 표시합니다.
- slot_message 함수는 수신된 메시지를 텍스트 편집 위젯에 추가합니다.
채팅 클라이언트 예제 구현
채팅클라이언트는 화면이 2개로 구성되어 있다. 첫 번째는 아래 그림에서 보는 것과 같이로그인 화면이다.
로그인 위젯에서 보는 것과 같이 [login] 버튼을 클릭하면 서버 IP주소로 채팅 서버에 접속이 완료 된다.
그리고 로그인 위젯은 Hide 되고 채팅 클라이언트 위젯이 활성화(Show) 된다.
채팅 클라이언트 위젯에서 중간에 위치한 QTextEdit 위젯은 서버로부터 수신한 메시지를 출력한다.
하단의 QLineEdit 는 서버로 보낼 메시지를 입력한다. 그리고 [Send] 버튼을 클릭하면 메시지를 채팅 서버로 전송한다.
<loginwidget.h>
#ifndef LOGINWIDGET_H
#define LOGINWIDGET_H
#include <QWidget> // QWidget 클래스 포함
// Ui 네임스페이스: UI 파일에서 자동으로 생성된 LoginWidget 클래스 선언
namespace Ui {
class LoginWidget;
}
// LoginWidget 클래스 선언: QWidget을 상속받아 로그인 관련 기능을 처리
class LoginWidget : public QWidget
{
Q_OBJECT
public:
// 명시적 생성자: LoginWidget 객체를 초기화하며, 부모 위젯을 선택적으로 설정 가능
explicit LoginWidget(QWidget *parent = nullptr);
// 소멸자: LoginWidget 객체가 소멸될 때 호출됨
~LoginWidget();
private:
Ui::LoginWidget *ui; // UI를 관리하는 포인터 (자동 생성된 UI 관련 클래스)
private slots:
// 슬롯: 로그인 버튼이 클릭되었을 때 호출됨
void loginBtnClicked();
signals:
// 시그널: 로그인 정보(서버 주소와 사용자 이름)를 전달할 때 사용
void sig_loginInfo(QString addr, QString name);
};
#endif // LOGINWIDGET_H
- GUI 상에서 [Login] 버튼을 클릭하면 Server IP 와 User name 을 QString 에 저장한다.
- 그리고 QString 에 저장한 값을 sig_loginInfo( ) 시그널의 인자로 전달한다.
- 이 시그널은Widget 클래스의 Slot 함수와 연결된다.
<loginwidget.cpp>
#include "loginwidget.h"
#include "ui_loginwidget.h"
LoginWidget::LoginWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::LoginWidget)
{
ui->setupUi(this);
connect(ui->loginButton, &QPushButton::pressed,
this, &LoginWidget::loginBtnClicked);
}
void LoginWidget::loginBtnClicked()
{
QString serverIp = ui->ipLineEdit->text().trimmed();
QString name = ui->nameLineEdit->text().trimmed();
emit sig_loginInfo(serverIp, name);
}
LoginWidget::~LoginWidget()
{
delete ui;
}
<wiget.h>
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QTcpSocket>
#include "loginwidget.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
// Widget 클래스 선언: QWidget을 상속받아 사용자 인터페이스를 구현
class Widget : public QWidget
{
Q_OBJECT
public:
// 생성자: Widget 객체를 초기화하며, 부모 위젯을 설정
Widget(QWidget *parent = nullptr);
// 소멸자: Widget 객체가 소멸될 때 호출
~Widget();
private:
Ui::Widget *ui; // 사용자 인터페이스 객체
LoginWidget *loginWidget; // 로그인 위젯 객체
QTcpSocket *socket; // 서버와의 통신을 위한 소켓
QString ipAddr; // 서버 IP 주소
QString userName; // 사용자 이름
private slots:
// 슬롯: 로그인 정보가 전달되었을 때 호출, 서버 주소와 사용자 이름을 설정
void loginInfo(QString addr, QString name);
// 슬롯: 채팅 메시지 전송 버튼이 클릭되었을 때 호출
void sayButton_clicked();
// 슬롯: 서버와의 연결이 성공적으로 이루어졌을 때 호출
void connected();
// 슬롯: 서버로부터 데이터를 읽을 준비가 되었을 때 호출
void readyRead();
};
#endif // WIDGET_H
- loginInfo( ) Slot 함수는 LoginWidget 클래스에서 [로그인] 버튼을 클릭하면 로그인창에서 입력한 서버 IP주소와 사용자명을 전달하기 위한 시그널이 발생하며 이 시그널이발생하면 호출되는 Slot 함수이다.
- 첫 번째 인자는 서버 IP주소이며 두 번째 인자는 사용자 명이다.
<widget.cpp>
#include "widget.h" // Widget 클래스 헤더 파일 포함
#include "./ui_widget.h" // UI 관련 헤더 파일 포함
#include <QRegularExpression> // 정규 표현식을 사용하기 위한 헤더 파일 포함
#include <QRegularExpressionMatch> // 정규 표현식의 매칭 결과를 다루기 위한 헤더 포함
// 생성자: 위젯 초기화 및 필요한 연결 설정
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this); // UI 구성 요소 설정
loginWidget = new LoginWidget(); // 로그인 위젯 생성
// 로그인 정보가 입력되면 loginInfo 슬롯이 호출되도록 연결
connect(loginWidget, &LoginWidget::sig_loginInfo,
this, &Widget::loginInfo);
// "전송" 버튼이 눌렸을 때 sayButton_clicked 슬롯이 호출되도록 연결
connect(ui->sayButton, &QPushButton::pressed,
this, &Widget::sayButton_clicked);
loginWidget->show(); // 로그인 위젯 표시
socket = new QTcpSocket(this); // TCP 소켓 생성
// 서버로부터 데이터를 읽을 준비가 되면 readyRead 슬롯이 호출되도록 연결
connect(socket, SIGNAL(readyRead()),
this, SLOT(readyRead()));
// 서버와 연결이 완료되면 connected 슬롯이 호출되도록 연결
connect(socket, SIGNAL(connected()),
this, SLOT(connected()));
}
// 로그인 정보를 처리하는 슬롯: 서버에 연결
void Widget::loginInfo(QString addr, QString name)
{
ipAddr = addr; // 서버의 IP 주소 저장
userName = name; // 사용자 이름 저장
// 서버에 접속 시도 (포트는 35000번 사용)
socket->connectToHost(ipAddr, 35000);
}
// "전송" 버튼이 클릭되었을 때 호출되는 슬롯
void Widget::sayButton_clicked()
{
// 입력된 메시지를 가져와서 공백을 제거한 후 저장
QString message = ui->sayLineEdit->text().trimmed();
// 메시지가 비어 있지 않으면 서버로 전송
if(!message.isEmpty())
{
// 메시지 끝에 개행 문자를 붙여 UTF-8로 인코딩하여 서버로 전송
socket->write(QString(message + "\n").toUtf8());
}
// 입력 필드를 초기화하고 포커스를 다시 설정
ui->sayLineEdit->clear();
ui->sayLineEdit->setFocus();
}
// 서버와 연결이 완료되었을 때 호출되는 슬롯
void Widget::connected()
{
loginWidget->hide(); // 로그인 위젯 숨김
this->window()->show(); // 현재 창 표시
// 서버에 사용자의 이름을 전송
socket->write(QString("/me:" + userName + "\n").toUtf8());
}
// 서버로부터 데이터를 읽을 준비가 되었을 때 호출되는 슬롯
void Widget::readyRead()
{
// 서버로부터 읽을 수 있는 데이터가 있는 동안 반복
while(socket->canReadLine())
{
// 한 줄씩 데이터를 읽어와서 UTF-8로 디코딩하고 공백을 제거
QString line = QString::fromUtf8(socket->readLine()).trimmed();
// 정규 표현식: 메시지 형식을 '사용자:메시지'로 구분
QRegularExpression re("^([^:]+):(.*)$");
QRegularExpressionMatch match = re.match(line);
// 매칭이 성공하면 사용자명과 메시지를 추출
if(match.hasMatch())
{
QString user = match.captured(1); // 사용자명
QString message = match.captured(2); // 메시지 내용
// 채팅창에 사용자명과 메시지를 굵게 표시하여 추가
ui->roomTextEdit->append("<b>"+user+"</b>:"+message);
}
}
}
// 소멸자: 위젯이 소멸될 때 UI 메모리를 해제
Widget::~Widget()
{
delete ui;
}
- [Send] 버튼을 클릭하면 sayButton_clicked( ) Slot 함수가 호출된다.
- connected( ) Slot 함수는 채팅 서버와 연결이 완료되면 호출된다.
- readyRead( ) 는 채팅 서버가 메시지를 전송하면 호출되는 Slot 함수이다.
- 이 Slot 함수에서 메시지를 QTextEdit 위젯에 출력한다.
'Qt프로그램' 카테고리의 다른 글
TCP 프로토콜 / 동기 방식 비 동기 방식 구현 (0) | 2024.10.10 |
---|---|
TCP 프로토콜 기반 서버/클라이언트 접속구현 (0) | 2024.10.10 |
Qt데이터베이스 모듈 (2) | 2024.10.09 |
Model and View (1) | 2024.10.09 |
Container Classes (1) | 2024.10.08 |
댓글