两个主要的功能:
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进行分割的代码是:
c++
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文件
c++
#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();//本机IP
this->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
c++
#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_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
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; //文本消息socket
QLabel *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文件:
c++
#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 1024
struct 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);
//bind
if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
printf("bind failed!\n");
}
//listen
listen(serv_sock, 5);
//使用epoll
epoll_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:1
event.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加入epoll
struct 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,buffer
if(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;
}