两个主要的功能:
1.文件传输
2.多人聊天室
要用的技术点:epoll模型
出现的bug总结:
解决1个bug:每次客户端挂掉以后,就会报9:Bad file descriptormain.
解决办法:在if (len < 0)条件里面加入break就可以了
bug2:每次只能传回一次数据,数据同步的问题?
解决办法:把readLine函数写成readAll,问题解决,但是没找到原因是什么???
bug3:只能有一个人登录聊天室
解决了只能一人登录的问题,现在可以多人登录,但是消息还不是共享的
解决了,通过引入cli_map<int,Client>,去掉消息中的while(true)循环解决掉了。
bug4:无法在dubug文件夹中双击QQ_client_server.exe运行,通过http://t.csdnimg.cn/b1yiN博客看明白了
这时确定了要用缓冲区开头写一个字符C或者字符F的形式确定是文本输入还是文件输入
bug5:如何进行修改,可以区分开文本输入还是文件输入?
思路:要在QT客户端进行修改了,加了一个name_flag标志位
在修改了标志位name_flag以后,只有第一次输入名字的时候,前面有C,后面没有\n,后面就都有了
所以第一次需要提醒一下(在文本框中写上,请输出姓名)
bug6:将writeText函数抽象化的时候,遇到几个问题
1.访问Client的成员变量
传入了map<int,Client>指针,如果要访问Client的成员变量的时候,要这样操作:
(*client_map)[fd].name,
2.遍历map<int,Client>结构体数组:
还有想要遍历map<int,Client>结构体数组的时候,使用迭代器:for(auto it = (*client_map).begin(); it != (*client_map).end(); ++it),这里it的成员变量类型为:map<int,Client>::iterate
3.写入数据的时候使用了write(it->first, buffer, sizeof(buffer));
这里的sizeof(buffer) = 8,是一个指针的大小,所以,要把变量替换为原先的1024
bug7:传输文件功能实现:
需要记录文件名,文件大小等文件的基本信息,于是有两种思路:
思路1:在客户端将文件头信息和文件内容信息分开发送
思路2:在服务端对收到的buffer进行文件解析。对于小文件来说,需要用&&&&作为分隔符,接收到的buffer进行分割的代码是:
char file_name[512] = "";
char file_size[64];
const char *d = "&&&&";
char *p;
//文件格式:F&&&&文件名&&&&文件大小&&&&文件内容
//第一次:F 不管
p = strtok(buffer,d);
//第二次 文件名
strcpy(file_name,strtok(NULL,d));
printf("second time:%s:%s\n",__FUNCTION__,file_name);
FILE *fp = fopen(file_name,"w");
if(fp == NULL){printf("无法写文件%s\n",file_name);exit(1);
}
//第三次 文件大小
strcpy(file_size,strtok(NULL,d));
int len = atoi(file_size);
printf("third time:%s:%d\n",__FUNCTION__,len);
//第四次 文件内容
strcpy(buffer,strtok(NULL,d));//buffer没有问题
printf("forth time:%s:%s\n",__FUNCTION__,buffer);
常用的一些转换方法
string a转换为char b[]的方法:strcpy(b,a);
QString转换为各种格式的数据的方法(要注意)
用printf(“%s”)打印string的方法:
string a;
printf(“%s”,a.c_str());
否则就会打印乱码
完整的代码:
QT客户端:
.pro文件中,要加
QT += core gui sql network widgets
mainwindows.cpp文件
#include "mainwindow.h"
#include "ui_mainwindow.h"//用来on_btnSend_clicked发送消息的时的标志位,保证发送姓名的时候不带有\n,同时保证开启连接时发送的消息开头带有C
int name_flag = 0;//用来传输文件名
int fileName_flag = 0;MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);//设置状态栏格式statusBar()->setMinimumHeight(25);statusBar()->setStyleSheet(QString("QStatusBar::item{border: 1px}")); // 不显示边框// m_loadSize = 4*1024; // 每次发送的文件数据大小
// m_totalBytes = 0;
// m_bytesWritten = 0;
// m_bytesToWrite = 0;m_tcpTextClient = new QTcpSocket(this); //创建socket变量m_tcpFileClient = new QTcpSocket(this);m_labSocketState = new QLabel("Socket状态:");//状态栏标签m_labSocketState->setMinimumWidth(250);statusBar()->addWidget(m_labSocketState);QString localIP = getLocalIP();//本机IPthis->setWindowTitle(this->windowTitle()+"----本机IP:"+localIP);ui->comboServer->addItem(localIP);//在下拉框里面可以看到追加的ip,即localIP//显示连接状态connect(m_tcpTextClient,SIGNAL(connected()),this,SLOT(onConnected()));connect(m_tcpTextClient,SIGNAL(disconnected()),this,SLOT(onDisconnected()));//状态改变时要更新connect(m_tcpTextClient,SIGNAL(stateChanged(QAbstractSocket::SocketState)),this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));//读取从客户端传回的数据,connect(m_tcpTextClient,SIGNAL(readyRead()),this,SLOT(onSocketReadyRead()));}MainWindow::~MainWindow()
{delete ui;
}//获得本地ip
QString MainWindow::getLocalIP()
{//获取本机IPv4地址QString hostName = QHostInfo::localHostName();//本机主机名QHostInfo hostInfo = QHostInfo::fromName(hostName);QString localIP="";QList<QHostAddress> addList = hostInfo.addresses();if(!addList.isEmpty()){for(int i = 0;i<addList.count();i++){QHostAddress aHost = addList.at(i);if(QAbstractSocket::IPv4Protocol==aHost.protocol()){localIP = aHost.toString();break;}}}return localIP;
}//-------------------------------------------------------------------------
//这里是客户端的连接部分void MainWindow::on_connectBt_clicked()
{//连接到服务器动作QString addr = ui->comboServer->currentText();quint16 port = ui->spinPort->value();m_tcpTextClient->connectToHost(addr, port);// m_bytesWritten = 0; // 初始化已发送字节为0
// m_tcpFileClient->connectToHost(addr, port);}void MainWindow::on_disconnectBt_clicked()
{//断开与服务器的连接动作if (m_tcpTextClient->state()==QAbstractSocket::ConnectedState)m_tcpTextClient->disconnectFromHost();name_flag = 0;
}void MainWindow::on_btnSend_clicked()
{//发送数据QString msg=ui->editMsg->toPlainText();m_tcpTextClient->write("C");
// qDebug() << msg;if (msg.isEmpty()){QMessageBox::information(this, "提示", "发送的消息不能为空!");return;}ui->plainTextEdit->appendPlainText("[out] "+msg);ui->editMsg->clear();ui->editMsg->setFocus();QByteArray str=msg.toUtf8();if(name_flag != 0){str.append('\n');}
// m_tcpTextClient->write(str.constData(),1024);name_flag = 1;qint64 flag = m_tcpTextClient->write(str);if(flag > 0){qDebug() << "success write,flag = " << flag;}else{qDebug() << "write error occurred";}}//显示连接的状态
void MainWindow::onConnected()
{ //connected()信号槽函数ui->plainTextEdit->appendPlainText("**已连接到服务器");ui->plainTextEdit->appendPlainText("**peer address:"+m_tcpTextClient->peerAddress().toString());ui->plainTextEdit->appendPlainText("**peer port:"+QString::number(m_tcpTextClient->peerPort()));ui->connectBt->setEnabled(false);ui->disconnectBt->setEnabled(true);
}//显示断开连接的状态
void MainWindow::onDisconnected()
{//disConnected()信号槽函数ui->plainTextEdit->appendPlainText("**已断开与服务器的连接");ui->connectBt->setEnabled(true);ui->disconnectBt->setEnabled(false);
}//文本读服务器的消息
void MainWindow::onSocketReadyRead()
{ //readyRead()信号槽函数while(m_tcpTextClient->canReadLine()){ui->plainTextEdit->appendPlainText("[in] "+ QString::fromUtf8(m_tcpTextClient->readAll()));
// printf("readLine()buffer:%d\n",sizeof(m_tcpTextClient->readLine(1024)));}
}//清空文本框消息
void MainWindow::on_clearBtn_clicked()
{ui->plainTextEdit->clear();
}//发送文件
void MainWindow::on_btnSendFile_clicked()
{//为了和发送消息进行区分qint64 flag = m_tcpTextClient->write("F");if(flag > 0){qDebug() << "success write,flag = " << flag;}else{qDebug() << "write error occurred";}startTransfer();
}void MainWindow::startTransfer() //实现文件大小等信息的发送
{//这里的m_fileName是在selectFile中找到的,已经赋值了QString currentFileName = m_fileName.right(m_fileName.size() - m_fileName.lastIndexOf('/')-1);//最开始传输,写一下文件名if(fileName_flag == 0){//注意这里write的参数是QByteArray类型或者是const char*类型,//即要将QString转化为QByteArray用.toUtf8()//将QByteArray转化为const char*,用.data()m_tcpTextClient->write("&&&&");int flag = m_tcpTextClient->write(currentFileName.toUtf8());if(flag < 0){//ui->plainTextEdit->appendPlainText(QString("传送文件 %1 成功").arg(m_fileName));ui->plainTextEdit->appendPlainText(QString("传送文件 %1 失败").arg(m_fileName));exit(1);}ui->plainTextEdit->appendPlainText(QString("正在传送 %1 文件...").arg(m_fileName));}//文件名和文件内容需要分两次发送,看看别人的解决思路//github中的解决思路是://发送端:读文件,进入循环发送文件,接收success信息,显示发送文件成功//接收端:F字母判断是接收文件,fopen打开文件的同时命名文件,进入循环,循环逻辑:recv以后fwrite文件,文件接收结束。给发送端发送success信息//接收两次,一次是文件名,一次是内容QFile file(m_fileName);bool isok = file.open(QIODevice::ReadWrite);qDebug() << "isok:" <<isok;//char buffer[1024];QFileInfo info(m_fileName);qDebug()<<"文件名字:"<< info.fileName();qDebug()<<"文件后缀:"<< info.suffix();qDebug()<<"文件大小:"<< info.size();qDebug()<<"文件创建时间:"<< info.created().toString("yyyy-MM-dd hh:mm:ss"); //yyyy代表年 MMd//发送文件大小m_tcpTextClient->write("&&&&");m_tcpTextClient->write(QByteArray::number(info.size()));m_tcpTextClient->waitForBytesWritten();if(isok){//用来存储文件内的内容QByteArray array;while(file.atEnd() == false){array += file.readLine();qDebug() << "readAll loop";}qDebug()<<"文件内容:"<< array;//输入分隔符,文件发送的格式为F&&&&文件名&&&&文件大小&&&&文件内容m_tcpTextClient->write("&&&&");//传输文件内容m_tcpTextClient->write(array);}}void MainWindow::on_btnSelectFile_clicked()
{m_fileName = QFileDialog::getOpenFileName(this, "选择文件");if (!m_fileName.isEmpty()){ui->plainTextEdit->appendPlainText(QString("打开文件 %1 成功!").arg(m_fileName));ui->btnSendFile->setEnabled(true);}}void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{//stateChange()信号槽函数switch(socketState){case QAbstractSocket::UnconnectedState:m_labSocketState->setText("scoket状态:UnconnectedState");break;case QAbstractSocket::HostLookupState:m_labSocketState->setText("scoket状态:HostLookupState");break;case QAbstractSocket::ConnectingState:m_labSocketState->setText("scoket状态:ConnectingState");break;case QAbstractSocket::ConnectedState:m_labSocketState->setText("scoket状态:ConnectedState");break;case QAbstractSocket::BoundState:m_labSocketState->setText("scoket状态:BoundState");break;case QAbstractSocket::ClosingState:m_labSocketState->setText("scoket状态:ClosingState");break;case QAbstractSocket::ListeningState:m_labSocketState->setText("scoket状态:ListeningState");}
}//下面是两个没有用到的槽函数(弃用)
void MainWindow::displayError(QAbstractSocket::SocketError) //显示错误
{ui->plainTextEdit->appendPlainText(m_tcpFileClient->errorString());m_tcpFileClient->close();ui->progressBar->reset();
}
// 更新进度条,实现文件的传送(弃用)
void MainWindow::updateClientProgress(qint64 numBytes)
{// 已经发送数据的大小m_bytesWritten += (int)numBytes;if(m_bytesToWrite > 0) // 剩余数据大小{// 从文件中取出数据到发送缓冲区,每次发送loadSize大小的数据,这里设置为4KB,如果剩余的数据不足4KB,就发送剩余数据的大小m_outBlock = m_localFile->read(qMin(m_bytesToWrite, m_loadSize));// 从发送缓冲区发送数据,计算发送完一次数据后还剩余数据的大小m_bytesToWrite -= (int)m_tcpFileClient->write(m_outBlock);// 清空发送缓冲区m_outBlock.resize(0);} else{m_localFile->close(); // 没有数据待发送,则关闭文件}//更新进度条ui->progressBar->setMaximum(m_totalBytes);ui->progressBar->setValue(m_bytesWritten);if(m_bytesWritten == m_totalBytes) //发送完毕{ui->plainTextEdit->appendPlainText(QString("传送文件 %1 成功").arg(m_fileName));m_localFile->close();}
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
//sql 要在.pro文件中加入sql
#include <QFile>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>//server 要在.pro文件中加入network
#include <QTcpServer>
#include <QHostInfo>
#include <QTcpSocket>#include <QSignalMapper>#include<QDebug>
#include<QMessageBox>//选择文件
#include <QFileDialog>//时间
#include <QDateTime>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();protected slots:void onNewConnection(); //有新的请求会调用//void onSocketStateChange(QAbstractSocket::SocketState socketState);//显示连接状态 暂不需要,可通过stateChanged信号调用void onClientConnected(int); //连接成功调用void onClientDisconnected(int); //连接断开调用void onSocketReadyRead(int); //有消息调用QSqlDatabase connectSql();private slots://文本读服务器的消息void onSocketReadyRead();//用来显示连接状态的槽函数void onSocketStateChange(QAbstractSocket::SocketState socketState);void onConnected();void onDisconnected();//连接动作void on_connectBt_clicked();void on_disconnectBt_clicked();//发送消息按钮,清空文本框按钮,选择文件按钮和发送文件按钮void on_btnSend_clicked();void on_clearBtn_clicked();void on_btnSelectFile_clicked();void on_btnSendFile_clicked();//发送文件大小等信息void startTransfer();//更新进度条,实现文件的传送(暂时没用到)void updateClientProgress(qint64 numBytes);void displayError(QAbstractSocket::SocketError);
private:Ui::MainWindow *ui;QSqlDatabase db; //数据库连接QTcpServer *tcpServer; //server连接QLabel* LabListenInfo; //状态栏显示正在连接QList <QTcpSocket *> tcpSocket;//TCP通信的Socket,消息格式为ip 端口号QList <bool> isfrist; //判断是否第一条消息QString getLocalIP(); //得到本机ip//项目里的QTcpSocket *m_tcpTextClient; //文本消息socketQLabel *m_labSocketState; //状态栏显示标签QString m_fileName; //保存文件路径//状态栏QLabel* labelStsInfo; // 状态栏提示信息QLabel* labelStsIP; // IP信息//发送文件需要的参数QFile *m_localFile; //要发送的文件qint64 m_totalBytes; //数据总大小qint64 m_bytesWritten; //已经发送数据大小qint64 m_bytesToWrite; //剩余数据大小qint64 m_loadSize; //每次发送数据的大小QByteArray m_outBlock; //数据缓冲区,即存放每次要发送的数据QTcpSocket *m_tcpFileClient; //文件消息socket};
#endif // MAINWINDOW_H
服务端的cpp文件:
#include <stdio.h>
#include<iostream>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <error.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>#include<map>
#include<fstream> using namespace std;
#define BUFFER_SIZE 1024struct Client{int sockfd;string name;string fileName;
};void writeText(map<int,Client> *client_map,char* buffer,int fd){cout << "unconvert buffer = " << buffer << endl;if((*client_map)[fd].name == ""){(*client_map)[fd].name = buffer+1;printf("(%d):%s,name:%s\n",__LINE__,__FUNCTION__,(*client_map)[fd].name.c_str());}else{//为了去除每次buffer前面的C字符string msg = buffer+1;string realmsg = '[' + (*client_map)[fd].name + ']' + msg;memset(buffer, 0, sizeof(buffer));strcpy(buffer, realmsg.c_str());cout << "realmsg:" <<realmsg << endl;cout << "buffer:" << buffer << endl;//转发给所有人,代码写到这儿了,还没有做好转发给所有人的功能for(auto it = (*client_map).begin(); it != (*client_map).end(); ++it){cout << "client_map.begin()->first=" << it->first << endl;// cout << "client_map.end()->first=" << (*client_map).end()->first << endl;printf("%s(%d):%s: server write buffer:%s\n", __FILE__, __LINE__, __FUNCTION__,buffer);printf("%s(%d):%s:sizeof(buffer):%d\n", __FILE__, __LINE__, __FUNCTION__,sizeof(*buffer));printf("%s(%d):%s:it->first:%d\n", __FILE__, __LINE__, __FUNCTION__,it->first);//这里的buffer是8个字节ssize_t write_result = write(it->first, buffer, BUFFER_SIZE);printf("(%d)write_result:%d \n", __LINE__ , write_result);if (write_result != BUFFER_SIZE) {printf("%s:server端写入失败\n",__FUNCTION__);}}}
}void writeFile(map<int,Client> *client_map,char buffer[],int fd){printf("%s(%d):buffer:%s\n",__FUNCTION__,__LINE__,buffer);char file_name[512] = "";char file_size[64];const char *d = "&&&&";char *p;//第一次:F 不管p = strtok(buffer,d);//第二次 文件名strcpy(file_name,strtok(NULL,d));printf("second time:%s:%s\n",__FUNCTION__,file_name);FILE *fp = fopen(file_name,"w");if(fp == NULL){printf("无法写文件%s\n",file_name);exit(1);}//第三次 文件大小strcpy(file_size,strtok(NULL,d));int len = atoi(file_size);printf("third time:%s:%d\n",__FUNCTION__,len);//第四次 文件内容strcpy(buffer,strtok(NULL,d));//buffer没有问题printf("forth time:%s:%s\n",__FUNCTION__,buffer);//写文件//fseek(fp,0,SEEK_SET); int flag = fwrite(buffer, sizeof(char), len, fp);if(flag < 0){printf("写入文件%s失败\n", file_name);} fclose(fp);
}void server103() {//设置参数int serv_sock, fileServ_sock ,cli_sock;struct sockaddr_in serv_addr, fileServ_addr ,cli_addr;socklen_t cliaddr_len = sizeof(cli_addr);//初始化serv_sock = socket(PF_INET, SOCK_STREAM, 0);serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = inet_addr("0.0.0.0");serv_addr.sin_port = htons(9527);//bindif (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {printf("bind failed!\n");}//listenlisten(serv_sock, 5);//使用epollepoll_event event;int enfd, event_cnt;enfd = epoll_create(100);if (enfd == -1) {printf("epoll create failed! \n%d:%s", errno, strerror(errno));close(serv_sock);return;}//需要多个epoll_event,搞100个吧epoll_event* all_event = new epoll_event[100];//对epoll做初始化,将服务端用epoll添加进来event.events = EPOLLIN;//printf("EPOLLIN:%d\n", EPOLLIN);//结果为EPOLLIN:1event.data.fd = serv_sock;epoll_ctl(enfd, EPOLL_CTL_ADD, serv_sock, &event);char buffer[BUFFER_SIZE] = ""; //消息传输的buffer大小printf("开始进入while循环 \n");//保存客户端的消息map<int, Client> client_map;//对客户端传入后的epoll处理while (true) {event_cnt = epoll_wait(enfd, all_event, 100, 1000);//这里的event_cnt就是事件发生的数量,连接算一个,发送数据算一个printf("event_cnt: %d\n", event_cnt); //结果为0 0 0 1(有操作的时候) 0 0 0 1(有操作时)//sleep(1);if (event_cnt == -1) {printf("epoll wait failed! \n%d:%s", errno, strerror(errno));close(serv_sock);return;}if (event_cnt == 0) continue;for (int i = 0; i < event_cnt; i++) {int fd = all_event[i].data.fd;cout << "fd=" << fd << endl;if ( fd == serv_sock) {cli_sock = accept(serv_sock, (struct sockaddr*)&cli_addr, &cliaddr_len);if(cli_sock < 0){printf("accept error\n");continue;}//将客户端的socket加入epollstruct epoll_event ev_client;ev_client.events = EPOLLIN;ev_client.data.fd = cli_sock;int ret = epoll_ctl(enfd, EPOLL_CTL_ADD, cli_sock, &ev_client);if(ret < 0){printf("epoll_ctl error\n");break;}//printf("%s正在连接",cli_addr.sin_addr.s_addr);//保存该客户端的信息struct Client client;client.sockfd = cli_sock;client.name = "";client_map[cli_sock] = client;printf("已连接用户: %s:%d\n all_event[i].data.fd = %d\n", inet_ntoa(cli_addr.sin_addr),htons(cli_addr.sin_port),client_map[cli_sock].sockfd);}//这里写的逻辑不对,但还不知道怎么改:有头绪了,用C和F区分开消息和文件else{//这里read中的socket要写成all_event[i].data.fd,不是serv_sock memset(buffer, 0, sizeof(buffer));ssize_t len = read(fd, buffer, sizeof(buffer));if (len <= 0) {//客户端断开连接了(或程序发生错误了),关掉客户端// printf("client closed,len < 0(%d)!\n", __LINE__);client_map.erase(fd); epoll_ctl(enfd, EPOLL_CTL_DEL, fd, NULL);close(fd);printf("client is closed! fd = %d \n", fd);break;}else{printf("%s(%d):%s read buffer:%s\n", __FILE__, __LINE__, __FUNCTION__, buffer);//输出的时候带上自己的名字,三个参数:client_map,fd,bufferif(buffer[0] == 'C'){writeText(&client_map,buffer,fd);}else if(buffer[0] == 'F'){writeFile(&client_map,buffer,fd);}}}}}delete[]all_event;close(serv_sock);}int main(int argc,char* argv[])
{server103();return 0;
}