您的位置:首页 > 理论基础 > 计算机网络

<C/C++> Socket编程Http下载的简单实现

2013-01-08 00:08 435 查看
下载原理: 网上介绍很多,就是按照Http协议,使用Socket连接并发送请求头给Http服务器,若服务器正确响应,返回请求文件数据,接收并写文件保存.至于Http协议的请求头及响应头的格式,这里不再赘述,请Google之.

实现: 为此,我封装了一个HttpDownload类.先上代码...(基于WinSock,移植时请注意部分函数)

(HttpDownload.h)

#ifndef HTTP_DOWNLOAD_H
#define HTTP_DOWNLOAD_H

#include <cstdio>
#include <string>
#include <winsock2.h>

class HttpDownload {
public:
HttpDownload(const char* hostAddr, const int port,
const char* getPath, const char* saveFileName);
~HttpDownload();
bool start();
void cancel();
void getPos(ULONGLONG& totalSize, ULONGLONG& downloadSize);
protected:
bool initSocket();                        //初始化Socket
bool sendRequest();                        //发送请求头
bool receiveData();                        //接收数据
bool closeTransfer();                    //关闭传输
private:
std::string m_hostAddr;                    //目标主机IP
int m_port;                                //HTTP端口号
std::string m_getPath;                    //目标文件相对路径
std::string m_saveFileName;                //保存文件路径
SOCKET m_sock;                            //Socket
FILE* m_fp;                                //保存文件指针
ULONGLONG m_fileTotalSize;                //目标文件总大小
ULONGLONG m_receivedDataSize;            //已接收数据大小
bool m_cancelFlag;                        //取消下载标记
};

#endif    //HTTP_DOWNLOAD_H


(HttpDownload.cpp)

#include "HttpDownload.h"

#define BUFFER_SIZE 1024

HttpDownload::HttpDownload(const char* hostAddr, const int port, const char* getPath, const char* saveFileName)
{
m_hostAddr = hostAddr;
m_port = port;
m_getPath = getPath;
m_saveFileName = saveFileName;
m_sock = 0;
m_fp = NULL;
m_fileTotalSize = 1;    //没给0,因为分母
m_receivedDataSize = 0;
m_cancelFlag = false;
}

HttpDownload::~HttpDownload()
{

}

bool HttpDownload::initSocket()
{
m_sock = socket(AF_INET, SOCK_STREAM, 0);
if (m_sock < 0)
return false;

//设置Socket为非阻塞模式
unsigned long mode = 1;
if (ioctlsocket(m_sock, FIONBIO, &mode) < 0)
return false;

if (m_hostAddr.empty())
return false;

struct sockaddr_in destaddr;
destaddr.sin_family = AF_INET;
destaddr.sin_port = htons(m_port);
destaddr.sin_addr.s_addr = inet_addr(m_hostAddr.c_str());

int nRet = connect(m_sock, (struct sockaddr*)&destaddr, sizeof(destaddr));
if (nRet == 0)    //如果立即连接成功
return true;
//虽直接返回,但未立即成功,用select等待看socket是否可写来判断connect是否成功
if (WSAGetLastError() != WSAEWOULDBLOCK)
return false;
int retryCount = 0;
while(1)
{
fd_set writeSet, exceptSet;
FD_ZERO(&writeSet);
FD_SET(m_sock, &writeSet);
exceptSet = writeSet;

struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;

int err = select((int)m_sock+1, NULL, &writeSet, &exceptSet, &timeout);
if (err < 0)    //出错
break;
if (err == 0)    //超时
{
if (++retryCount > 10)    //重试10次
return false;
continue;
}
if (FD_ISSET(m_sock, &writeSet))
return true;
if (FD_ISSET(m_sock, &exceptSet))
break;
}
return false;
}

bool HttpDownload::sendRequest()
{
if (m_getPath.empty())
return false;

char requestHeader[256];
//格式化请求头
int len = sprintf(requestHeader,
"GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"Range: bytes=%I64d-\r\n"    //从m_receivedDataSize位置开始
"Connection: close\r\n"
"\r\n",
m_getPath.c_str(), m_hostAddr.c_str(), m_receivedDataSize);

int nSendBytes = 0;    //已发送字节数
while(1)
{
fd_set writeSet;
FD_ZERO(&writeSet);
FD_SET(m_sock, &writeSet);

struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;

int err = select((int)m_sock+1, NULL, &writeSet, NULL, &timeout);
if (err < 0)
break;
if (err == 0)
continue;
int nBytes = send(m_sock, requestHeader+nSendBytes, len, 0);
if (nBytes < 0)
{
if (WSAGetLastError() != WSAEWOULDBLOCK)
break;
nBytes = 0;
}
nSendBytes += nBytes;    //若一次未发完,累计,循环send
len -= nBytes;
if (len == 0)
return true;
}
return false;
}

bool HttpDownload::receiveData()
{
char responseHeader[BUFFER_SIZE] = {0};

struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;

//接收响应头
int retryCount = 0;
int nRecvBytes = 0;    //已接收字节数
while (1)
{
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(m_sock, &readSet);
int nRet = select(m_sock+1, &readSet, NULL, NULL, &timeout);
if (nRet < 0)    //出错
return false;
if (nRet == 0)    //超时
{
if (++retryCount > 10)
return false;
continue;
}
retryCount = 0;
if (recv(m_sock, responseHeader+nRecvBytes, 1, 0) <= 0)
return false;
nRecvBytes++;
if (nRecvBytes >= BUFFER_SIZE)
return false;
if (nRecvBytes >= 4 &&
responseHeader[nRecvBytes-4]=='\r' && responseHeader[nRecvBytes-3]=='\n' &&
responseHeader[nRecvBytes-2]=='\r' && responseHeader[nRecvBytes-1]=='\n')
break;
}
responseHeader[nRecvBytes] = '\0';

if (strncmp(responseHeader, "HTTP/", 5))
return false;
int status = 0;
float version = 0.0;
ULONGLONG startPos, endPos, totalLength;
startPos = endPos = totalLength = 0;
if (sscanf(responseHeader, "HTTP/%f %d ", &version, &status) != 2)
return false;
char* findStr = strstr(responseHeader, "Content-Range: bytes ");
if (findStr == NULL)
return false;
if (sscanf(findStr, "Content-Range: bytes %I64d-%I64d/%I64d",
&startPos, &endPos, &totalLength) != 3)
return false;
if (status != 200 && status != 206 || totalLength == 0)
return false;
if (m_fileTotalSize == 1)    //第一次获取HTTP响应头,保存目标文件总大小
m_fileTotalSize = totalLength;
if (m_receivedDataSize != startPos)
return false;

//接收目标文件数据
retryCount = 0;
while (1)
{
char buf[BUFFER_SIZE] = {0};
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(m_sock, &readSet);

int nRet = select((int)m_sock+1, &readSet, NULL, NULL, &timeout);
if (nRet < 0)
break;
if (nRet == 0) {
if (++retryCount > 10)
break;
continue;
}
int length = recv(m_sock, buf, BUFFER_SIZE, 0);
if(length < 0)    //出错
return false;
if (length == 0)    //socket被优雅关闭
return true;
size_t written = fwrite(buf, sizeof(char), length, m_fp);
if(written < length)
return false;
m_receivedDataSize += length;
if (m_receivedDataSize == m_fileTotalSize)    //文件接收完毕
{
return true;
}
}
return false;
}

bool HttpDownload::closeTransfer()
{
if (m_sock > 0) {
if (closesocket(m_sock) < 0)
return false;
m_sock = 0;
}
else
m_sock = 0;
return true;
}

bool HttpDownload::start()
{
m_fp = fopen(m_saveFileName.c_str(), "wb");    //创建文件
if (m_fp == NULL)
return false;
bool errFlag = false;
while(1)
{
if (!initSocket() || !sendRequest() || !receiveData())
{
if (m_cancelFlag)
{
errFlag = true;
break;
}
if (!closeTransfer())
{
errFlag = true;
break;
}
Sleep(1000);
continue;
}
break;
}
if(m_fp != NULL)
{
fclose(m_fp);
m_fp = NULL;
}
if (errFlag)
return false;
return true;
}

void HttpDownload::cancel()
{
m_cancelFlag = true;
closeTransfer();
}

void HttpDownload::getPos(ULONGLONG& totalSize, ULONGLONG& downloadSize)
{
totalSize = m_fileTotalSize;
downloadSize = m_receivedDataSize;
}


其中4个主要函数功能如下:

1) initSocket() : 初始化Socket->设置Socket为非阻塞模式->connect()

值得注意的是,这里我采用了非阻塞的select模型.相对来说非阻塞模式可以减少开销,增加错误控制能力,显得更灵活.因此,Line 42-73

connect之,因为非阻塞,立即返回,得到返回值判断,通常不会立即成功,然后

while(1) {

  socket加入写描述符集

  用select检测写写描述符集,延时3秒,可写就返回true

  否则,重复10次(总共30秒),如果仍不成功,认为connect失败,返回false

}

2) sendRequest() : 格式化Http请求字符串->用send发送之.send依然使用非阻塞select模型判断,同上.

注意的是:这里Http请求字符串里的"Range: bytes="字段用m_receivedDataSize来格式化,此成员变量用于保存请求的目标文件开始位置.目的是为实现断点续传,若传输文件过程中网络异常后,可重新发送请求头,则只需从已下载位置之后继续传输.

3) receiveData() : recv接收Http响应头字符串->取出响应头中的信息(如文件大小)->若响应信息正确,开始recv接收目标文件数据->写文件

recv依然使用非阻塞select模型判断,同上.

4) closeTransfer() : 关闭套接字.(这里因在windows下,所以使用closesocket)

因为代码中多有注释,细节就不再多解释了.

另外,给出3个接口函数:

1) start() : 创建文件->分别依次调用initSocket(),sendRequest(),receiveData()->关闭文件,套接字

在一个循环,不断判断initSocket(),sendRequest(),receiveData()是否成功,若任一失败(网络异常),调用closeTransfer(),然后重新来,直到下载完毕或被cancel()中断

2) cancel() : 关闭套接字,并将m_cancelFlag置true

3) getPos() : 用于得到当前文件下载的进度

最后,附上源码,包含一个实现下载的控制台程序例子(MinGW编译),上图

View Code

#include <cstdio>
#include "pthread.h"
#include "HttpDownload.h"
#include "InitWinSocket.h"

InitWinSocket init;
const char* g_progName = NULL;
const char* g_saveFileName = "savefilename";

void usage() {
printf("Usage: %s http://www.xxx.com/filename %s\n", g_progName, g_saveFileName);
}

void progressBar(float percent) {
const int numTotal = 50;
int numShow = (int)(numTotal * percent);
if (numShow == 0)
numShow = 1;
if (numShow > numTotal)
numShow = numTotal;
char sign[numTotal+1] = {0};
memset(sign, '=', numTotal);
printf("\r%.2f%%\t[%-*.*s]", percent*100, numTotal, numShow, sign);
fflush(stdout);
if (numShow == numTotal)
printf("\n");
}

void parseURL(const char* url, char* hostAddr, int& port, char* getPath) {
if (url == NULL || hostAddr == NULL || getPath == NULL)
return;
const char* temp = strstr(url, "http://");
if (temp == NULL)
return;
const char* hostStart = temp + strlen("http://");
const char* colon = strchr(hostStart, ':');
if (colon != NULL)    //表示存在冒号,有端口号
sscanf(hostStart, "%[^:]:%d%s", hostAddr, &port, getPath);
else
sscanf(hostStart, "%[^/]%s", hostAddr, getPath);
//通过主机名转IP地址
struct hostent* hostEntry;
hostEntry = gethostbyname(hostAddr);
if (hostEntry == NULL)
{
printf("Hostname not available!\n");
return;
}
struct in_addr inAddr = {0};
memcpy(&inAddr.s_addr, hostEntry->h_addr, sizeof(inAddr.s_addr));
strcpy(hostAddr, inet_ntoa(inAddr));
}

void* task(void* arg) {
HttpDownload* pObj = (HttpDownload*)arg;
if (pObj->start())
return ((void*)1);
else
return ((void*)0);
}

int main(int argc, char** argv) {
g_progName = strrchr(argv[0], '\\');
if (g_progName != NULL)
g_progName += 1;
else
g_progName = argv[0];
if (argc != 3 || strncmp(argv[1], "http://", strlen("http://")) != 0) {
usage();
return -1;
}
g_saveFileName = argv[2];

char hostAddr[256] = {0};
int port = 80;
char getPath[256] = {0};
parseURL(argv[1], hostAddr, port, getPath);

HttpDownload obj(hostAddr, port, getPath, g_saveFileName);    //创建下载类对象
pthread_t tid;
int err = pthread_create(&tid, NULL, task, &obj);
if (err != 0)
printf("Start Download Failed!\n");
else
printf("Start Downloading...\n");

ULONGLONG totalSize = 1;
ULONGLONG downloadSize = 0;
float percent = 0;
while (1) {
obj.getPos(totalSize, downloadSize);
percent = downloadSize/(float)totalSize;
progressBar(percent);
if (downloadSize == totalSize)
break;
Sleep(500);
}

void* ret = NULL;
pthread_join(tid, &ret);
if (ret)
printf("Download Finished.\n");
else
printf("Download Failed!\n");
return 0;
}


[b]附件: 源码下载[/b]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: