最近在做一个物联网的病人监护系统,需要网络通信并支持同时连接多台设备。故学习了一下 Socket,顺便记录一下自己的实现方法。
服务器
Socket 本身的使用方法很简单,在此不多赘述。我们需要关心的是,如何将其封装到一个类中。
在进行通信时,服务器需要:
- 初始化套接字,指定协议族,类型。
- 将一个地址和端口绑定到套接字上。
- 监听连接请求。
- 接受连接请求并创建一个新的客户端描述符。
然后,服务器和客户端之间就可以:
- 接受数据
- 发送数据
- 关闭连接
使用步骤有了,于是,我们就可以将上面的每一个步骤定义为一个方法,以下是服务器类的定义:
#ifndef TCP_SERVER_H
#define TCP_SERVER_H
#include <string.h>
#include <unistd.h>
#include <QObject>
#include <QDebug>
#include <sys/socket.h>
#include <netinet/in.h>
class CTcpServer : public QObject
{
Q_OBJECT
public:
CTcpServer();
int Bind(int target_port);
int Listen(int max_connect);
void Accept();
void Receive();
int Send(char* message);
void Close();
signals:
void ReceivedData(QString data);
private:
int sockfd;
struct sockaddr_in serveraddr;
int clientfd;
char recv_content[512];
};
#endif // TCP_SERVER_H
服务器类的实现:
#include "tcp_server.h"
CTcpServer::CTcpServer()
{
sockfd=socket(AF_INET,SOCK_STREAM,0);
}
int CTcpServer::Bind(int target_port)
{
memset(&serveraddr,0,sizeof(struct sockaddr_in));
serveraddr.sin_family=AF_INET;
serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
serveraddr.sin_port=htons(target_port);
return bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(struct sockaddr));
}
int CTcpServer::Listen(int max_connect)
{
return listen(sockfd,max_connect);
}
void CTcpServer::Accept()
{
struct sockaddr_in clientaddr;
socklen_t len=sizeof(clientaddr);
clientfd=accept(sockfd,(struct sockaddr*)&clientaddr,&len);
}
void CTcpServer::Receive()
{
QString acc_data;
while(1)
{
memset(recv_content,0,sizeof(recv_content));
int recv_count=recv(clientfd,recv_content,512,0);
if(recv_count==0)
{
Close();
break;
}
//qDebug()<<"Receved: "<<recv_content;
acc_data+=QString::fromUtf8(recv_content);
}
emit ReceivedData(acc_data);
}
int CTcpServer::Send(char* message)
{
return send(clientfd,message,strlen(message),0);
}
void CTcpServer::Close()
{
close(clientfd);
}
客户端
客户端的实现要更为简单一些,方法什么的和服务器大差不差,直接放代码了。
定义:
#ifndef TCP_CILENT_H
#define TCP_CILENT_H
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <QDebug>
class CTcpClient
{
public:
CTcpClient();
int Connect(const char* server_addr,int port);
int Send(const char* message);
void Receive();
void Close();
private:
int sockfd;
struct sockaddr_in srvaddr;
char recv_content[128];
};
#endif // TCP_CILENT_H
实现:
#include "tcp_cilent.h"
CTcpClient::CTcpClient()
{
}
int CTcpClient::Connect(const char* server_addr,int port)
{
qDebug()<<"attempting to connect to server"<<server_addr<<"on port"<<port;
sockfd=socket(AF_INET,SOCK_STREAM,0);
memset(&srvaddr,0,sizeof(srvaddr));
srvaddr.sin_family=AF_INET;
srvaddr.sin_port=htons(port);
srvaddr.sin_addr.s_addr=inet_addr(server_addr);
return connect(sockfd,(struct sockaddr*)&srvaddr,sizeof(srvaddr));
}
int CTcpClient::Send(const char *message)
{
return send(sockfd,message,strlen(message),0);
}
void CTcpClient::Receive()
{
while(1)
{
memset(recv_content,0,sizeof(recv_content));
int recv_count=recv(sockfd,recv_content,128,0);
qDebug()<<"Received: "<<recv_content;
if(recv_count==0)
break;
}
}
void CTcpClient::Close()
{
close(sockfd);
}
多客户端支持
本节内容提供一个较为简单的多客户端支持方式,仅供参考。
我们知道,在服务器套接字接受连接是,会返回一个客户端描述符(一般代码里的clientfd就负责存储这个),通过这个描述符,我们可以和指定客户端进行通信。
但是一旦客户端断线,重新建立连接后,在服务器端就会使用一个新的描述符来与其通信。此时对于服务器而言,这个新的描述符相当于一个全新身份。这对辨别设备身份来说是一个难点。
感谢卡卡吞星老师绘制的超强配图!
另一种更简单的做法则是:给所有客户端一个唯一ID,每次发送数据时都在头上包含这个唯一ID,每次发完数据均马上关闭连接。服务器套接字则进行 accept 和 recv 死循环,每当对方关闭连接时就将接收到的数据发送一个信号出去,然后再次开始 accept。
这样就解决了更换连接后无法识别身份的问题,而且只需要单线程就可以完成多客户端通信(当然如果客户端数量过多的话也可以考虑同时多开几个线程进行 accept 的)。
附一段代码使用例以供参考:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(&tcp_s,SIGNAL(ReceivedData(QString)),this,SLOT(CheckData(QString)));
InitSocket();
QtConcurrent::run([this]{StartSocketUpdate();});
}
void MainWindow::InitSocket()
{
if(tcp_s.Bind(7777)<0)
{
qDebug()<<"bind error";
QMessageBox::StandardButton msgbox;
msgbox=QMessageBox::critical(this,tr("Error"),"Failed to start socket server",QMessageBox::Retry|QMessageBox::Cancel);
if(msgbox==QMessageBox::Retry)
{
std::exit(-1);
}
else if(msgbox==QMessageBox::Cancel)
std::exit(-1);
}
else
{
qDebug()<<"bind success on port 7777";
}
tcp_s.Listen(10);
}
void MainWindow::StartSocketUpdate()
{
while(1)
{
tcp_s.Accept();
tcp_s.Receive();
}
}
void MainWindow::CheckData(QString data)
{
qDebug()<<data;
}